This commit is contained in:
nora 2022-10-31 14:37:52 +01:00
commit 2fa7531824
No known key found for this signature in database
8 changed files with 352 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

7
Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "minmax"
version = "0.1.0"

8
Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "minmax"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

108
build.rs Normal file
View file

@ -0,0 +1,108 @@
use std::{fs::File, io::Write, path::PathBuf};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Player {
X,
O,
}
impl Player {
fn from_u8(num: u8) -> Option<Self> {
match num {
0 => Some(Player::X),
1 => Some(Player::O),
2 => None,
_ => 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)]
struct Board(u32);
impl Board {
fn new(num: u32) -> Option<Board> {
for i in 0..16 {
let next_step = num >> (i * 2);
let mask = 0b11;
let pos = next_step & mask;
if pos == 3 {
return None;
}
}
Some(Self(num))
}
fn get(&self, index: usize) -> Option<Player> {
debug_assert!(index < 9);
let shifted = self.0 >> (index * 2);
let masked = shifted & 0b11;
Player::from_u8(masked as u8)
}
}
fn winner(board: Board) -> 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) {
Some(Player::X)
} else if a == Some(Player::O) && b == Some(Player::O) && c == Some(Player::O) {
Some(Player::O)
} else {
None
}
}
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),
None => {}
}
};
}
test_row!(0, 1, 2);
test_row!(3, 4, 5);
test_row!(6, 7, 8);
test_row!(0, 3, 6);
test_row!(1, 4, 7);
test_row!(2, 5, 8);
test_row!(0, 4, 8);
test_row!(2, 4, 6);
None
}
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)
}
None => 0,
};
file.write_all(&[byte]).expect("write file");
}
}
fn main() {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR");
let win_table_path = PathBuf::from(out_dir).join("win_table");
let mut win_table_file = File::create(win_table_path).expect("create win table file");
calculate_win_table(&mut win_table_file);
win_table_file.flush().expect("flushing file");
}

173
src/board.rs Normal file
View file

@ -0,0 +1,173 @@
use std::fmt::{Display, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Player {
X,
O,
}
impl Player {
pub fn opponent(self) -> Self {
match self {
Self::X => Self::O,
Self::O => Self::X,
}
}
fn from_u8(num: u8) -> Result<Option<Self>, ()> {
Ok(match num {
0 => Some(Player::X),
1 => Some(Player::O),
2 => None,
_ => return Err(()),
})
}
fn as_u8(this: Option<Player>) -> u8 {
match this {
Some(Player::X) => 0,
Some(Player::O) => 1,
None => 2,
}
}
}
impl Display for Player {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::X => "X",
Self::O => "O",
})
}
}
#[derive(Clone)]
pub struct Board(u32);
impl Board {
pub fn empty() -> Self {
// A = 1010
// 18 bits - 9 * 2 bits - 4.5 nibbles
Self(0x0002AAAA)
}
#[cfg(not(debug_assertions))]
fn validate(&self) {}
#[cfg(debug_assertions)]
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 board = self.0;
let shifted = board >> (index * 2);
let masked = shifted & 0b11;
Player::from_u8(masked as u8).unwrap()
}
pub fn set(&mut self, index: usize, value: Option<Player>) {
debug_assert!(index < 9);
self.validate();
let value = Player::as_u8(value) as u32;
let value = value << (index * 2);
let mask = 0b11 << (index * 2);
let current_masked_off_new = self.0 & !mask;
let result = value | current_masked_off_new;
self.0 = result;
self.validate();
}
pub fn iter(&self) -> impl Iterator<Item = Option<Player>> + '_ {
let mut i = 0;
std::iter::from_fn(move || {
let result = (i < 8).then(|| self.get(i));
i += 1;
result
})
}
pub fn result(&self) -> Option<Player> {
win_table::result(self)
}
}
mod win_table {
use super::{Board, Player};
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()
}
}
impl Display for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in 0..3 {
for j in 0..3 {
let index = i * 3 + j;
match self.get(index) {
Some(player) => {
write!(f, "\x1B[1m{player}\x1B[0m ")?;
}
None => {
write!(f, "\x1B[37m{index}\x1B[0m ")?;
}
}
}
f.write_char('\n')?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Board, Player};
#[test]
fn board_field() {
let mut board = Board::empty();
board.set(0, None);
board.set(8, Some(Player::X));
board.set(4, Some(Player::O));
board.set(5, Some(Player::X));
let expected = [
None,
None,
None,
None,
Some(Player::O),
Some(Player::X),
None,
None,
Some(Player::X),
];
board
.iter()
.zip(expected.into_iter())
.enumerate()
.for_each(|(idx, (actual, expected))| assert_eq!(actual, expected, "Position {idx}"));
}
}

23
src/game.rs Normal file
View file

@ -0,0 +1,23 @@
use crate::{board::Player, Board, GamePlayer};
impl Board {
pub fn play<A: GamePlayer, B: GamePlayer>(&mut self, a: &mut A, b: &mut B) -> Option<Player> {
let mut current_player = Player::X;
for _ in 0..9 {
if current_player == Player::X {
a.next_move(self, current_player);
} else {
b.next_move(self, current_player);
}
if let Some(winner) = self.result() {
return Some(winner);
}
current_player = current_player.opponent();
}
None
}
}

19
src/lib.rs Normal file
View file

@ -0,0 +1,19 @@
mod board;
mod game;
use board::Player;
pub use board::Board;
pub trait GamePlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player);
}
pub struct GreedyPlayer;
impl GamePlayer for GreedyPlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player) {
let first_free = board.iter().position(|p| p.is_none()).unwrap();
board.set(first_free, Some(this_player));
}
}

13
src/main.rs Normal file
View file

@ -0,0 +1,13 @@
use minmax::{Board, GreedyPlayer};
fn main() {
let mut board = Board::empty();
let result = board.play(&mut GreedyPlayer, &mut GreedyPlayer);
println!("{board}");
match result {
Some(winner) => {
println!("player {winner} won!");
}
None => println!("a draw..."),
}
}