diff --git a/first b/first deleted file mode 100755 index 8b71a9f..0000000 Binary files a/first and /dev/null differ diff --git a/second b/second deleted file mode 100755 index 0085b65..0000000 Binary files a/second and /dev/null differ diff --git a/src/connect4/board.rs b/src/connect4/board.rs index 572fa87..4a5331b 100644 --- a/src/connect4/board.rs +++ b/src/connect4/board.rs @@ -1,6 +1,9 @@ -use std::ops::Index; +use std::{ + fmt::{Display, Write}, + ops::{Index, IndexMut}, +}; -use crate::{Player, State}; +use crate::{minmax::Score, Game, GamePlayer, Player, State}; type Position = Option; @@ -12,11 +15,12 @@ const BOARD_POSITIONS: usize = WIDTH * HEIGTH; /// 7 8 9 10 11 12 13 /// 14 15 16 17 18 19 20 /// 21 22 23 24 25 26 27 -pub struct Board { +#[derive(Clone)] +pub struct Connect4 { positions: [Position; BOARD_POSITIONS], } -impl Board { +impl Connect4 { pub fn new() -> Self { Self { positions: [None; BOARD_POSITIONS], @@ -86,9 +90,30 @@ impl Board { }) .unwrap_or(State::InProgress) } + + fn rate(&self, player: Player) -> Score { + #[rustfmt::skip] + const WIN_COUNT_TABLE: [i32; BOARD_POSITIONS] = [ + 3, 4, 6, 7, 6, 4, 3, + 2, 4, 6, 7, 6, 4, 2, + 2, 4, 6, 7, 6, 4, 2, + 3, 4, 6, 7, 6, 4, 2, + ]; + + let score_player = |player: Player| { + self.positions + .iter() + .enumerate() + .filter(|(_, state)| **state == Some(player)) + .map(|(pos, _)| WIN_COUNT_TABLE[pos]) + .sum::() + }; + + Score::new(score_player(player) - score_player(player.opponent())) + } } -impl Index for Board { +impl Index for Connect4 { type Output = Position; fn index(&self, index: usize) -> &Self::Output { @@ -96,13 +121,87 @@ impl Index for Board { } } +impl IndexMut for Connect4 { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.positions[index] + } +} + +impl Game for Connect4 { + type Move = usize; + + const REASONABLE_SEARCH_DEPTH: Option = Some(5); + + fn empty() -> Self { + Self::new() + } + + fn possible_moves(&self) -> impl Iterator { + let board = self.clone(); + (0..WIDTH).filter(move |col| board[*col].is_none()) + } + + fn result(&self) -> State { + Connect4::result(&self) + } + + fn make_move(&mut self, position: Self::Move, player: Player) { + for i in 0..3 { + let prev = position + (i * WIDTH); + let next = position + ((i + 1) * WIDTH); + + if self[next].is_some() { + self[prev] = Some(player); + break; + } + } + + let bottom = position + (3 * WIDTH); + self[bottom] = Some(player); + } + + fn undo_move(&mut self, position: Self::Move) { + for i in 0..4 { + let pos = position + (i * WIDTH); + + if self[pos].is_some() { + self[pos] = None; + } + } + } + + fn rate(&self, player: Player) -> Score { + Connect4::rate(&self, player) + } +} + +impl Display for Connect4 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for i in 0..HEIGTH { + for j in 0..WIDTH { + let index = (i * WIDTH) + j; + match self[index] { + Some(player) => { + write!(f, "\x1B[33m {player}\x1B[0m ")?; + } + None => { + write!(f, "\x1B[35m{index:3 }\x1B[0m ")?; + } + } + } + f.write_char('\n')?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use crate::{Player, State}; - use super::Board; + use super::Connect4; - fn parse_board(board: &str) -> Board { + fn parse_board(board: &str) -> Connect4 { let positions = board .chars() .filter(|char| !char.is_whitespace()) @@ -119,7 +218,7 @@ mod tests { board.chars().filter(|c| !c.is_whitespace()).count() )); - Board { positions } + Connect4 { positions } } fn test(board: &str, state: State) { diff --git a/src/connect4/mod.rs b/src/connect4/mod.rs index 90d1407..1e8a90c 100644 --- a/src/connect4/mod.rs +++ b/src/connect4/mod.rs @@ -1 +1,6 @@ -pub mod board; \ No newline at end of file +use self::board::Connect4; + +pub use player::HumanPlayer; + +pub mod board; +pub mod player; diff --git a/src/connect4/player.rs b/src/connect4/player.rs new file mode 100644 index 0000000..4cf8b73 --- /dev/null +++ b/src/connect4/player.rs @@ -0,0 +1,35 @@ +use std::io::Write; + +use crate::{Game, GamePlayer, Player}; + +use super::Connect4; + +#[derive(Clone, Default)] +pub struct HumanPlayer; + +impl GamePlayer for HumanPlayer { + fn next_move(&mut self, board: &mut Connect4, this_player: Player) { + loop { + print!("{board}where to put the next {this_player}? (0-7): "); + + std::io::stdout().flush().unwrap(); + let mut buf = String::new(); + std::io::stdin().read_line(&mut buf).unwrap(); + + match buf.trim().parse() { + Ok(number) if number < 7 => match board[number] { + None => { + board.make_move(number, this_player); + return; + } + Some(_) => { + println!("Field is occupied already.") + } + }, + Ok(_) | Err(_) => { + println!("Invalid input.") + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 2ab2a4e..216bb3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,9 @@ -#![feature(never_type, try_trait_v2, return_position_impl_trait_in_trait)] +#![feature( + never_type, + try_trait_v2, + return_position_impl_trait_in_trait, + let_chains +)] #![allow(incomplete_features)] pub mod connect4; @@ -7,13 +12,56 @@ pub mod tic_tac_toe; mod player; -use self::minmax::GameBoard; +use std::fmt::Display; + +use minmax::Score; pub use player::{Player, State}; -pub trait Game { - type Board: GameBoard; +pub trait GamePlayer: Default { + fn next_move(&mut self, board: &mut G, this_player: Player); } -pub trait GamePlayer: Default { - fn next_move(&mut self, board: &mut G::Board, this_player: Player); +pub trait Game: Display { + type Move: Copy; + + const REASONABLE_SEARCH_DEPTH: Option; + + fn empty() -> Self; + + fn possible_moves(&self) -> impl Iterator; + + fn result(&self) -> State; + + /// Only called if [`GameBoard::REASONABLE_SEARCH_DEPTH`] is `Some`. + fn rate(&self, player: Player) -> Score; + + fn make_move(&mut self, position: Self::Move, player: Player); + + fn undo_move(&mut self, position: Self::Move); + + fn play, B: GamePlayer>( + &mut self, + x: &mut A, + o: &mut B, + ) -> Option { + let mut current_player = Player::X; + + loop { + if current_player == Player::X { + x.next_move(self, current_player); + } else { + o.next_move(self, current_player); + } + + match self.result() { + State::Winner(player) => return Some(player), + State::Draw => { + return None; + } + State::InProgress => {} + } + + current_player = current_player.opponent(); + } + } } diff --git a/src/main.rs b/src/main.rs index cc07429..ad43ee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,25 @@ #![allow(unused_imports)] -use std::time::SystemTime; +use std::{fmt::Display, time::SystemTime}; use minmax::{ - tic_tac_toe::{Board, GreedyPlayer, HumanPlayer, PerfectPlayer, TicTacToe}, - GamePlayer, Player, + connect4::{self, board::Connect4}, + tic_tac_toe::{GreedyPlayer, HumanPlayer, PerfectPlayer, TicTacToe}, + Game, GamePlayer, Player, }; fn main() { + play::(true); +} + +#[allow(dead_code)] +fn tic_tac_toe_stats() { let mut results = [0, 0, 0]; let start = SystemTime::now(); for _ in 0..100 { - let result = play_round::(false); + let result = play::(false); let idx = Player::as_u8(result); results[idx as usize] += 1; } @@ -28,23 +34,24 @@ fn main() { println!("Completed in {}ms", time.as_millis()); } -fn play_round, O: GamePlayer>(print: bool) -> Option { - let mut board = Board::empty(); +fn play, O: GamePlayer, G: Game>(print: bool) -> Option { + let mut board = G::empty(); let result = board.play(&mut X::default(), &mut O::default()); if print { - println!("{board}"); - } - match result { - Some(winner) => { - if print { - println!("player {winner} won!"); - } - } - None => { - if print { - println!("a draw...") - } - } + print_result(result, board); } result } + +fn print_result(result: Option, board: impl Display) { + println!("{board}"); + + match result { + Some(winner) => { + println!("player {winner} won!"); + } + None => { + println!("a draw...") + } + } +} diff --git a/src/minmax.rs b/src/minmax.rs index c342466..95dc173 100644 --- a/src/minmax.rs +++ b/src/minmax.rs @@ -2,25 +2,17 @@ use std::ops::Neg; use crate::{Game, GamePlayer, Player, State}; -pub trait GameBoard { - type Move: Copy; - - fn possible_moves(&self) -> impl Iterator; - - fn result(&self) -> State; - - fn make_move(&mut self, position: Self::Move, player: Player); - - fn undo_move(&mut self, position: Self::Move); -} - #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Score(i8); +pub struct Score(i32); impl Score { - const LOST: Self = Self(i8::MIN); + const LOST: Self = Self(i32::MIN); const TIE: Self = Self(0); - const WON: Self = Self(i8::MAX); + const WON: Self = Self(i32::MAX); + + pub fn new(int: i32) -> Self { + Self(int) + } } impl Neg for Score { @@ -32,25 +24,26 @@ impl Neg for Score { } #[derive(Clone)] -pub struct PerfectPlayer { +pub struct PerfectPlayer { best_move: Option, } -impl Default for PerfectPlayer { +impl Default for PerfectPlayer { fn default() -> Self { Self::new() } } -impl PerfectPlayer { +impl PerfectPlayer { pub fn new() -> Self { Self { best_move: None } } fn minmax(&mut self, board: &mut B, player: Player, depth: usize) -> Score { - if depth < 2 { - //print!("{board}{}| playing {player}: ", " ".repeat(depth)); + if let Some(max_depth) = B::REASONABLE_SEARCH_DEPTH && depth >= max_depth { + return board.rate(player); } + match board.result() { State::Winner(winner) => { if winner == player { @@ -83,8 +76,8 @@ impl PerfectPlayer { } } -impl GamePlayer for PerfectPlayer { - fn next_move(&mut self, board: &mut G::Board, this_player: Player) { +impl GamePlayer for PerfectPlayer { + fn next_move(&mut self, board: &mut G, this_player: Player) { self.best_move = None; self.minmax(board, this_player, 0); diff --git a/src/tic_tac_toe/board.rs b/src/tic_tac_toe/board.rs index 166109e..7acaf6b 100644 --- a/src/tic_tac_toe/board.rs +++ b/src/tic_tac_toe/board.rs @@ -1,11 +1,11 @@ use std::fmt::{Display, Write}; -use crate::{minmax::GameBoard, Player, State}; +use crate::{minmax::Score, Player, State, Game}; #[derive(Clone)] -pub struct Board(u32); +pub struct TicTacToe(u32); -impl Board { +impl TicTacToe { pub fn empty() -> Self { // A = 1010 // 18 bits - 9 * 2 bits - 4.5 nibbles @@ -75,14 +75,14 @@ impl Board { } mod win_table { - use super::Board; + use super::TicTacToe; use crate::{Player, State}; const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9); static WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table")); - pub fn result(board: &Board) -> State { + pub fn result(board: &TicTacToe) -> State { match WIN_TABLE[board.0 as usize] { 0 => State::Winner(Player::X), 1 => State::Winner(Player::X), @@ -93,7 +93,7 @@ mod win_table { } } -impl Display for Board { +impl Display for TicTacToe { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for i in 0..3 { for j in 0..3 { @@ -113,9 +113,15 @@ impl Display for Board { } } -impl GameBoard for Board { +impl Game for TicTacToe { type Move = usize; + const REASONABLE_SEARCH_DEPTH: Option = None; + + fn empty() -> Self { + Self::empty() + } + fn possible_moves(&self) -> impl Iterator { debug_assert!( !self.iter().all(|x| x.is_some()), @@ -129,7 +135,11 @@ impl GameBoard for Board { } fn result(&self) -> State { - Board::result(self) + TicTacToe::result(self) + } + + fn rate(&self, _: Player) -> Score { + unimplemented!("we always finish the board") } fn make_move(&mut self, position: Self::Move, player: Player) { @@ -143,11 +153,11 @@ impl GameBoard for Board { #[cfg(test)] mod tests { - use super::{Board, Player}; + use super::{Player, TicTacToe}; #[test] fn board_field() { - let mut board = Board::empty(); + let mut board = TicTacToe::empty(); board.set(0, None); board.set(8, Some(Player::X)); board.set(4, Some(Player::O)); diff --git a/src/tic_tac_toe/game.rs b/src/tic_tac_toe/game.rs index 1ce6b02..004a9f5 100644 --- a/src/tic_tac_toe/game.rs +++ b/src/tic_tac_toe/game.rs @@ -1,13 +1,17 @@ use crate::{GamePlayer, Player, State}; -use super::{board::Board, TicTacToe}; +use super::TicTacToe; -impl Board { +impl TicTacToe { pub fn default_play, O: GamePlayer>() -> Option { Self::empty().play(&mut X::default(), &mut O::default()) } - pub fn play, B: GamePlayer>(&mut self, x: &mut A, o: &mut B) -> Option { + pub fn play, B: GamePlayer>( + &mut self, + x: &mut A, + o: &mut B, + ) -> Option { let mut current_player = Player::X; for _ in 0..9 { diff --git a/src/tic_tac_toe/mod.rs b/src/tic_tac_toe/mod.rs index 1efddec..d72e14e 100644 --- a/src/tic_tac_toe/mod.rs +++ b/src/tic_tac_toe/mod.rs @@ -1,26 +1,17 @@ -use crate::Game; - mod board; mod game; mod perfect; mod player; -pub use {board::Board, perfect::PerfectPlayer, player::*}; - -pub struct TicTacToe; - -impl Game for TicTacToe { - type Board = board::Board; -} +pub use {board::TicTacToe, perfect::PerfectPlayer, player::*}; #[cfg(test)] mod tests { - use crate::{tic_tac_toe::board::Board, GamePlayer, Player}; + use crate::{tic_tac_toe::board::TicTacToe, GamePlayer, Player}; use super::{ perfect::PerfectPlayer, player::{GreedyPlayer, RandomPlayer}, - TicTacToe, }; fn assert_win_ratio, O: GamePlayer>( @@ -30,7 +21,7 @@ mod tests { let mut results = [0u64, 0, 0]; for _ in 0..runs { - let result = Board::default_play::(); + let result = TicTacToe::default_play::(); let idx = Player::as_u8(result); results[idx as usize] += 1; } diff --git a/src/tic_tac_toe/perfect.rs b/src/tic_tac_toe/perfect.rs index a93c8cf..738a7d6 100644 --- a/src/tic_tac_toe/perfect.rs +++ b/src/tic_tac_toe/perfect.rs @@ -2,7 +2,7 @@ use std::ops::Neg; use crate::{GamePlayer, Player, State}; -use super::{board::Board, TicTacToe}; +use super::TicTacToe; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum Score { @@ -41,7 +41,7 @@ impl PerfectPlayer { } } - fn minmax(&mut self, board: &mut Board, player: Player, depth: usize) -> Score { + fn minmax(&mut self, board: &mut TicTacToe, player: Player, depth: usize) -> Score { if depth < 2 { //print!("{board}{}| playing {player}: ", " ".repeat(depth)); } @@ -105,7 +105,7 @@ impl PerfectPlayer { } impl GamePlayer for PerfectPlayer { - fn next_move(&mut self, board: &mut Board, this_player: Player) { + fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { self.best_move = usize::MAX; self.minmax(board, this_player, 0); diff --git a/src/tic_tac_toe/player.rs b/src/tic_tac_toe/player.rs index 88f315b..a82ed66 100644 --- a/src/tic_tac_toe/player.rs +++ b/src/tic_tac_toe/player.rs @@ -2,13 +2,13 @@ use std::io::Write; use crate::{GamePlayer, Player}; -use super::{board::Board, TicTacToe}; +use super::TicTacToe; #[derive(Clone, Default)] pub struct GreedyPlayer; impl GamePlayer for GreedyPlayer { - fn next_move(&mut self, board: &mut Board, this_player: Player) { + fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { let first_free = board.iter().position(|p| p.is_none()).unwrap(); board.set(first_free, Some(this_player)); } @@ -18,7 +18,7 @@ impl GamePlayer for GreedyPlayer { pub struct HumanPlayer; impl GamePlayer for HumanPlayer { - fn next_move(&mut self, board: &mut Board, this_player: Player) { + fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { loop { print!("{board}where to put the next {this_player}? (0-8): "); @@ -55,7 +55,7 @@ fn fun_random() -> u64 { } impl GamePlayer for RandomPlayer { - fn next_move(&mut self, board: &mut Board, this_player: Player) { + fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { loop { let next = (fun_random() % 9) as usize; match board.get(next) {