mirror of
https://github.com/Noratrieb/minmax.git
synced 2026-01-14 15:25:08 +01:00
slowly working
This commit is contained in:
parent
b666c4a0cb
commit
43d39fba30
7 changed files with 227 additions and 34 deletions
|
|
@ -6,3 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
66
build.rs
66
build.rs
|
|
@ -1,11 +1,18 @@
|
|||
use std::{fs::File, io::Write, path::PathBuf};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Player {
|
||||
X,
|
||||
O,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum State {
|
||||
Winner(Player),
|
||||
InProgress,
|
||||
Draw,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
fn from_u8(num: u8) -> Option<Self> {
|
||||
match num {
|
||||
|
|
@ -15,14 +22,6 @@ impl Player {
|
|||
_ => panic!("Invalid value {num}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_u8(this: Option<Player>) -> u8 {
|
||||
match this {
|
||||
Some(Player::X) => 0,
|
||||
Some(Player::O) => 1,
|
||||
None => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -42,16 +41,42 @@ impl Board {
|
|||
Some(Self(num))
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<Player> {
|
||||
fn validate(&self) {
|
||||
let board = self.0;
|
||||
for i in 0..16 {
|
||||
let next_step = board >> (i * 2);
|
||||
let mask = 0b11;
|
||||
let pos = next_step & mask;
|
||||
if pos >= 3 {
|
||||
panic!("Invalid bits, self: {board:0X}, bits: {pos:0X}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<Player> {
|
||||
self.validate();
|
||||
debug_assert!(index < 9);
|
||||
|
||||
let shifted = self.0 >> (index * 2);
|
||||
let board = self.0;
|
||||
|
||||
let shifted = board >> (index * 2);
|
||||
let masked = shifted & 0b11;
|
||||
|
||||
Player::from_u8(masked as u8)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Option<Player>> {
|
||||
let mut i = 0;
|
||||
let this = self.clone();
|
||||
std::iter::from_fn(move || {
|
||||
let result = (i < 8).then(|| this.get(i));
|
||||
i += 1;
|
||||
result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn winner(board: Board) -> Option<Player> {
|
||||
fn result(board: Board) -> State {
|
||||
fn won_row(a: Option<Player>, b: Option<Player>, c: Option<Player>) -> Option<Player> {
|
||||
if a == Some(Player::X) && b == Some(Player::X) && c == Some(Player::X) {
|
||||
Some(Player::X)
|
||||
|
|
@ -65,12 +90,16 @@ fn winner(board: Board) -> Option<Player> {
|
|||
macro_rules! test_row {
|
||||
($a:literal, $b:literal, $c:literal) => {
|
||||
match won_row(board.get($a), board.get($b), board.get($c)) {
|
||||
Some(player) => return Some(player),
|
||||
Some(player) => return State::Winner(player),
|
||||
None => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if board.iter().all(|x| x.is_some()) {
|
||||
return State::Draw;
|
||||
}
|
||||
|
||||
test_row!(0, 1, 2);
|
||||
test_row!(3, 4, 5);
|
||||
test_row!(6, 7, 8);
|
||||
|
|
@ -81,15 +110,20 @@ fn winner(board: Board) -> Option<Player> {
|
|||
|
||||
test_row!(0, 4, 8);
|
||||
test_row!(2, 4, 6);
|
||||
None
|
||||
State::InProgress
|
||||
}
|
||||
|
||||
fn calculate_win_table(file: &mut impl Write) {
|
||||
for board in 0..(2u32.pow(18)) {
|
||||
let byte = match Board::new(board) {
|
||||
Some(board) => {
|
||||
let winner = winner(board);
|
||||
Player::as_u8(winner)
|
||||
let winner = result(board);
|
||||
match winner {
|
||||
State::Winner(Player::X) => 0,
|
||||
State::Winner(Player::O) => 1,
|
||||
State::InProgress => 2,
|
||||
State::Draw => 3,
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
|
|
|||
28
src/board.rs
28
src/board.rs
|
|
@ -6,6 +6,13 @@ pub enum Player {
|
|||
O,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
Winner(Player),
|
||||
InProgress,
|
||||
Draw,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn opponent(self) -> Self {
|
||||
match self {
|
||||
|
|
@ -23,7 +30,7 @@ impl Player {
|
|||
})
|
||||
}
|
||||
|
||||
fn as_u8(this: Option<Player>) -> u8 {
|
||||
pub fn as_u8(this: Option<Player>) -> u8 {
|
||||
match this {
|
||||
Some(Player::X) => 0,
|
||||
Some(Player::O) => 1,
|
||||
|
|
@ -95,28 +102,35 @@ impl Board {
|
|||
self.validate();
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Option<Player>> + '_ {
|
||||
pub fn iter(&self) -> impl Iterator<Item = Option<Player>> {
|
||||
let mut i = 0;
|
||||
let this = self.clone();
|
||||
std::iter::from_fn(move || {
|
||||
let result = (i < 8).then(|| self.get(i));
|
||||
let result = (i < 8).then(|| this.get(i));
|
||||
i += 1;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn result(&self) -> Option<Player> {
|
||||
pub fn result(&self) -> State {
|
||||
win_table::result(self)
|
||||
}
|
||||
}
|
||||
|
||||
mod win_table {
|
||||
use super::{Board, Player};
|
||||
use super::{Board, Player, State};
|
||||
|
||||
const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9);
|
||||
const WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table"));
|
||||
|
||||
pub fn result(board: &Board) -> Option<Player> {
|
||||
Player::from_u8(WIN_TABLE[board.0 as usize]).unwrap()
|
||||
pub fn result(board: &Board) -> State {
|
||||
match WIN_TABLE[board.0 as usize] {
|
||||
0 => State::Winner(Player::X),
|
||||
1 => State::Winner(Player::X),
|
||||
2 => State::InProgress,
|
||||
3 => State::Draw,
|
||||
n => panic!("Invalid value {n} in table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
13
src/game.rs
13
src/game.rs
|
|
@ -1,4 +1,7 @@
|
|||
use crate::{board::Player, Board, GamePlayer};
|
||||
use crate::{
|
||||
board::{Player, State},
|
||||
Board, GamePlayer,
|
||||
};
|
||||
|
||||
impl Board {
|
||||
pub fn play<A: GamePlayer, B: GamePlayer>(&mut self, a: &mut A, b: &mut B) -> Option<Player> {
|
||||
|
|
@ -11,8 +14,12 @@ impl Board {
|
|||
b.next_move(self, current_player);
|
||||
}
|
||||
|
||||
if let Some(winner) = self.result() {
|
||||
return Some(winner);
|
||||
match self.result() {
|
||||
State::Winner(player) => return Some(player),
|
||||
State::Draw => {
|
||||
return None;
|
||||
}
|
||||
State::InProgress => {}
|
||||
}
|
||||
|
||||
current_player = current_player.opponent();
|
||||
|
|
|
|||
33
src/lib.rs
33
src/lib.rs
|
|
@ -1,16 +1,17 @@
|
|||
mod board;
|
||||
mod game;
|
||||
mod minmax;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use board::Player;
|
||||
|
||||
pub use board::Board;
|
||||
pub use board::{Board, Player, State};
|
||||
pub use minmax::PerfectPlayer;
|
||||
|
||||
pub trait GamePlayer {
|
||||
fn next_move(&mut self, board: &mut Board, this_player: Player);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GreedyPlayer;
|
||||
|
||||
impl GamePlayer for GreedyPlayer {
|
||||
|
|
@ -20,6 +21,7 @@ impl GamePlayer for GreedyPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HumanPlayer;
|
||||
|
||||
impl GamePlayer for HumanPlayer {
|
||||
|
|
@ -48,3 +50,28 @@ impl GamePlayer for HumanPlayer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomPlayer;
|
||||
|
||||
fn fun_random() -> u64 {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
std::collections::hash_map::RandomState::new()
|
||||
.build_hasher()
|
||||
.finish()
|
||||
}
|
||||
|
||||
impl GamePlayer for RandomPlayer {
|
||||
fn next_move(&mut self, board: &mut Board, this_player: Player) {
|
||||
loop {
|
||||
let next = (fun_random() % 9) as usize;
|
||||
match board.get(next) {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
board.set(next, Some(this_player));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/main.rs
44
src/main.rs
|
|
@ -1,13 +1,47 @@
|
|||
use minmax::{Board, GreedyPlayer};
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use minmax::{Board, GamePlayer, GreedyPlayer, HumanPlayer, PerfectPlayer, Player, RandomPlayer};
|
||||
|
||||
fn main() {
|
||||
let mut results = [0, 0, 0];
|
||||
|
||||
let start = SystemTime::now();
|
||||
|
||||
for _ in 0..10_000 {
|
||||
let result = play_round(PerfectPlayer::new(), PerfectPlayer::new(), false);
|
||||
let idx = Player::as_u8(result);
|
||||
results[idx as usize] += 1;
|
||||
}
|
||||
|
||||
println!("Winner counts");
|
||||
println!(" X: {}", results[0]);
|
||||
println!(" O: {}", results[1]);
|
||||
println!(" Draw: {}", results[2]);
|
||||
|
||||
let time = start.elapsed().unwrap();
|
||||
|
||||
println!("Completed in {}ms", time.as_millis());
|
||||
}
|
||||
|
||||
fn play_round<A: GamePlayer, B: GamePlayer>(mut a: A, mut b: B, print: bool) -> Option<Player> {
|
||||
let mut board = Board::empty();
|
||||
let result = board.play(&mut GreedyPlayer, &mut GreedyPlayer);
|
||||
println!("{board}");
|
||||
let result = board.play(&mut a, &mut b);
|
||||
if print {
|
||||
println!("{board}");
|
||||
}
|
||||
match result {
|
||||
Some(winner) => {
|
||||
println!("player {winner} won!");
|
||||
if print {
|
||||
println!("player {winner} won!");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if print {
|
||||
println!("a draw...")
|
||||
}
|
||||
}
|
||||
None => println!("a draw..."),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
|
|
|||
73
src/minmax.rs
Normal file
73
src/minmax.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::{
|
||||
board::{Player, State},
|
||||
Board, GamePlayer,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Score(isize);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PerfectPlayer {
|
||||
best_move: usize,
|
||||
}
|
||||
|
||||
impl PerfectPlayer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
best_move: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
fn minmax(&mut self, board: &mut Board, player: Player, depth: usize) -> Score {
|
||||
match board.result() {
|
||||
State::Winner(winner) => {
|
||||
if winner == player {
|
||||
Score(1)
|
||||
} else {
|
||||
Score(-1)
|
||||
}
|
||||
}
|
||||
State::Draw => Score(0),
|
||||
State::InProgress => {
|
||||
let mut max_value = Score(isize::MIN);
|
||||
|
||||
debug_assert!(
|
||||
!board.iter().all(|x| x.is_some()),
|
||||
"the board is full but state is InProgress"
|
||||
);
|
||||
|
||||
for (i, pos) in board.iter().enumerate() {
|
||||
if pos.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
board.set(i, Some(player));
|
||||
let value = self.minmax(board, player.opponent(), depth + 1);
|
||||
board.set(i, None);
|
||||
|
||||
if value > max_value {
|
||||
max_value = value;
|
||||
if depth == 0 {
|
||||
self.best_move = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
max_value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GamePlayer for PerfectPlayer {
|
||||
fn next_move(&mut self, board: &mut Board, this_player: Player) {
|
||||
self.best_move = usize::MAX;
|
||||
self.minmax(board, this_player, 0);
|
||||
|
||||
if self.best_move == usize::MAX {
|
||||
panic!("could not make a move");
|
||||
}
|
||||
|
||||
board.set(self.best_move, Some(this_player));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue