From 2fa753182445fff8136a30166b1fe2decac69408 Mon Sep 17 00:00:00 2001 From: nils <48135649+Nilstrieb@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:37:52 +0100 Subject: [PATCH] player --- .gitignore | 1 + Cargo.lock | 7 +++ Cargo.toml | 8 +++ build.rs | 108 ++++++++++++++++++++++++++++++++ src/board.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/game.rs | 23 +++++++ src/lib.rs | 19 ++++++ src/main.rs | 13 ++++ 8 files changed, 352 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/board.rs create mode 100644 src/game.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..69d4be2 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6054685 --- /dev/null +++ b/Cargo.toml @@ -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] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..dcbf0fe --- /dev/null +++ b/build.rs @@ -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 { + match num { + 0 => Some(Player::X), + 1 => Some(Player::O), + 2 => None, + _ => panic!("Invalid value {num}"), + } + } + + fn as_u8(this: Option) -> 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 { + 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 { + 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 { + fn won_row(a: Option, b: Option, c: Option) -> Option { + 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"); +} diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..4b3dbd0 --- /dev/null +++ b/src/board.rs @@ -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, ()> { + Ok(match num { + 0 => Some(Player::X), + 1 => Some(Player::O), + 2 => None, + _ => return Err(()), + }) + } + + fn as_u8(this: Option) -> 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 { + 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) { + 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> + '_ { + 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 { + 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::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}")); + } +} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..5e0ef68 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,23 @@ +use crate::{board::Player, Board, GamePlayer}; + +impl Board { + pub fn play(&mut self, a: &mut A, b: &mut B) -> Option { + 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 + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5500473 --- /dev/null +++ b/src/lib.rs @@ -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)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..60efe51 --- /dev/null +++ b/src/main.rs @@ -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..."), + } +}