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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 3
|
||||||
66
build.rs
66
build.rs
|
|
@ -1,11 +1,18 @@
|
||||||
use std::{fs::File, io::Write, path::PathBuf};
|
use std::{fs::File, io::Write, path::PathBuf};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Player {
|
enum Player {
|
||||||
X,
|
X,
|
||||||
O,
|
O,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
Winner(Player),
|
||||||
|
InProgress,
|
||||||
|
Draw,
|
||||||
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
fn from_u8(num: u8) -> Option<Self> {
|
fn from_u8(num: u8) -> Option<Self> {
|
||||||
match num {
|
match num {
|
||||||
|
|
@ -15,14 +22,6 @@ impl Player {
|
||||||
_ => panic!("Invalid value {num}"),
|
_ => 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)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
@ -42,16 +41,42 @@ impl Board {
|
||||||
Some(Self(num))
|
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);
|
debug_assert!(index < 9);
|
||||||
|
|
||||||
let shifted = self.0 >> (index * 2);
|
let board = self.0;
|
||||||
|
|
||||||
|
let shifted = board >> (index * 2);
|
||||||
let masked = shifted & 0b11;
|
let masked = shifted & 0b11;
|
||||||
|
|
||||||
Player::from_u8(masked as u8)
|
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> {
|
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) {
|
if a == Some(Player::X) && b == Some(Player::X) && c == Some(Player::X) {
|
||||||
Some(Player::X)
|
Some(Player::X)
|
||||||
|
|
@ -65,12 +90,16 @@ fn winner(board: Board) -> Option<Player> {
|
||||||
macro_rules! test_row {
|
macro_rules! test_row {
|
||||||
($a:literal, $b:literal, $c:literal) => {
|
($a:literal, $b:literal, $c:literal) => {
|
||||||
match won_row(board.get($a), board.get($b), board.get($c)) {
|
match won_row(board.get($a), board.get($b), board.get($c)) {
|
||||||
Some(player) => return Some(player),
|
Some(player) => return State::Winner(player),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if board.iter().all(|x| x.is_some()) {
|
||||||
|
return State::Draw;
|
||||||
|
}
|
||||||
|
|
||||||
test_row!(0, 1, 2);
|
test_row!(0, 1, 2);
|
||||||
test_row!(3, 4, 5);
|
test_row!(3, 4, 5);
|
||||||
test_row!(6, 7, 8);
|
test_row!(6, 7, 8);
|
||||||
|
|
@ -81,15 +110,20 @@ fn winner(board: Board) -> Option<Player> {
|
||||||
|
|
||||||
test_row!(0, 4, 8);
|
test_row!(0, 4, 8);
|
||||||
test_row!(2, 4, 6);
|
test_row!(2, 4, 6);
|
||||||
None
|
State::InProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_win_table(file: &mut impl Write) {
|
fn calculate_win_table(file: &mut impl Write) {
|
||||||
for board in 0..(2u32.pow(18)) {
|
for board in 0..(2u32.pow(18)) {
|
||||||
let byte = match Board::new(board) {
|
let byte = match Board::new(board) {
|
||||||
Some(board) => {
|
Some(board) => {
|
||||||
let winner = winner(board);
|
let winner = result(board);
|
||||||
Player::as_u8(winner)
|
match winner {
|
||||||
|
State::Winner(Player::X) => 0,
|
||||||
|
State::Winner(Player::O) => 1,
|
||||||
|
State::InProgress => 2,
|
||||||
|
State::Draw => 3,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
28
src/board.rs
28
src/board.rs
|
|
@ -6,6 +6,13 @@ pub enum Player {
|
||||||
O,
|
O,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum State {
|
||||||
|
Winner(Player),
|
||||||
|
InProgress,
|
||||||
|
Draw,
|
||||||
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn opponent(self) -> Self {
|
pub fn opponent(self) -> Self {
|
||||||
match 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 {
|
match this {
|
||||||
Some(Player::X) => 0,
|
Some(Player::X) => 0,
|
||||||
Some(Player::O) => 1,
|
Some(Player::O) => 1,
|
||||||
|
|
@ -95,28 +102,35 @@ impl Board {
|
||||||
self.validate();
|
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 mut i = 0;
|
||||||
|
let this = self.clone();
|
||||||
std::iter::from_fn(move || {
|
std::iter::from_fn(move || {
|
||||||
let result = (i < 8).then(|| self.get(i));
|
let result = (i < 8).then(|| this.get(i));
|
||||||
i += 1;
|
i += 1;
|
||||||
result
|
result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn result(&self) -> Option<Player> {
|
pub fn result(&self) -> State {
|
||||||
win_table::result(self)
|
win_table::result(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod win_table {
|
mod win_table {
|
||||||
use super::{Board, Player};
|
use super::{Board, Player, State};
|
||||||
|
|
||||||
const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9);
|
const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9);
|
||||||
const WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table"));
|
const WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table"));
|
||||||
|
|
||||||
pub fn result(board: &Board) -> Option<Player> {
|
pub fn result(board: &Board) -> State {
|
||||||
Player::from_u8(WIN_TABLE[board.0 as usize]).unwrap()
|
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 {
|
impl Board {
|
||||||
pub fn play<A: GamePlayer, B: GamePlayer>(&mut self, a: &mut A, b: &mut B) -> Option<Player> {
|
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);
|
b.next_move(self, current_player);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(winner) = self.result() {
|
match self.result() {
|
||||||
return Some(winner);
|
State::Winner(player) => return Some(player),
|
||||||
|
State::Draw => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
State::InProgress => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
current_player = current_player.opponent();
|
current_player = current_player.opponent();
|
||||||
|
|
|
||||||
33
src/lib.rs
33
src/lib.rs
|
|
@ -1,16 +1,17 @@
|
||||||
mod board;
|
mod board;
|
||||||
mod game;
|
mod game;
|
||||||
|
mod minmax;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use board::Player;
|
pub use board::{Board, Player, State};
|
||||||
|
pub use minmax::PerfectPlayer;
|
||||||
pub use board::Board;
|
|
||||||
|
|
||||||
pub trait GamePlayer {
|
pub trait GamePlayer {
|
||||||
fn next_move(&mut self, board: &mut Board, this_player: Player);
|
fn next_move(&mut self, board: &mut Board, this_player: Player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct GreedyPlayer;
|
pub struct GreedyPlayer;
|
||||||
|
|
||||||
impl GamePlayer for GreedyPlayer {
|
impl GamePlayer for GreedyPlayer {
|
||||||
|
|
@ -20,6 +21,7 @@ impl GamePlayer for GreedyPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct HumanPlayer;
|
pub struct HumanPlayer;
|
||||||
|
|
||||||
impl GamePlayer for 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() {
|
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 mut board = Board::empty();
|
||||||
let result = board.play(&mut GreedyPlayer, &mut GreedyPlayer);
|
let result = board.play(&mut a, &mut b);
|
||||||
println!("{board}");
|
if print {
|
||||||
|
println!("{board}");
|
||||||
|
}
|
||||||
match result {
|
match result {
|
||||||
Some(winner) => {
|
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