From 8b62b1ab38a6ea0edceff8de4866676c26a7aa2f Mon Sep 17 00:00:00 2001 From: Bananasmoothii <45853225+bananasmoothii@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:00:21 +0200 Subject: [PATCH] bot players now working --- src/bot.rs | 98 +++++++++++++++++++++++++++++++++++++++ src/game.rs | 4 +- src/game/connect4.rs | 96 +++++++++++++++----------------------- src/game/state.rs | 2 +- src/main.rs | 107 +++++++++---------------------------------- src/min_max.rs | 15 ++++-- src/min_max/node.rs | 2 +- src/scalar.rs | 2 +- 8 files changed, 173 insertions(+), 153 deletions(-) create mode 100644 src/bot.rs diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..99f55d3 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,98 @@ +use crate::game::player::Player; +use crate::game::Game; +use crate::min_max::node::GameNode; +use crate::scalar::Scalar; + +pub struct Bot { + player: G::Player, + game_tree: Option>, + max_depth: u32, + times: Vec, +} + +impl Bot { + pub fn new(player: G::Player, max_depth: u32) -> Self { + Self { + player, + /// game_tree should never be None + game_tree: Some(GameNode::new_root(G::new(), player, 0)), + max_depth, + times: Vec::new(), + } + } + + pub fn other_played(&mut self, play: G::InputCoordinate) -> Result<(), &str> { + let had_children = self + .game_tree + .as_ref() + .is_some_and(|tree| !tree.children().is_empty()); + let (is_known_move, mut new_game_tree) = self + .game_tree + .take() + .unwrap_or_else(|| panic!("game_tree should never be none, object is invalid")) + .try_into_child(play); + if is_known_move { + self.game_tree = Some(new_game_tree); + debug_assert!(self + .game_tree + .as_ref() + .is_some_and(|tree| tree.game().is_some())); + } else { + // Here, new_game_tree is actually game_tree, the ownership was given back to us + let result = new_game_tree + .expect_game_mut() + .play(self.player.other(), play); + if let Err(err) = result { + self.game_tree = Some(new_game_tree); + return Err(err); + } + if had_children { + println!("Unexpected move... Maybe you are a pure genius, or a pure idiot."); + } + let depth = new_game_tree.depth() + 1; + let game = new_game_tree.into_expect_game(); + self.game_tree = Some(GameNode::new_root(game, self.player, depth)); + } + Ok(()) + } + + pub fn play(&mut self) -> G::InputCoordinate { + let start = std::time::Instant::now(); + let game_tree = self + .game_tree + .as_mut() + .expect("Bot has not been initialized"); + game_tree.explore_children( + self.player, + self.max_depth, + game_tree.expect_game().plays() as u32, + ); + // println!("Tree:\n {}", game_tree.debug(3)); + // println!("Into best child..."); + self.game_tree = Some(self.game_tree.take().unwrap().into_best_child()); + + let game_tree = self.game_tree.as_ref().unwrap(); + + let time = start.elapsed().as_millis() as u64; + self.times.push(time); + println!("Done in {}ms", time); + + let weight_opt = game_tree.weight(); + if weight_opt.is_some_and(|it| it > G::Score::MAX().add_towards_0(1000)) { + println!("You're dead, sorry."); + } else if weight_opt.is_some_and(|it| it < G::Score::MIN().add_towards_0(1000)) { + println!("Ok I'm basically dead..."); + } + + debug_assert!(game_tree.game().is_some()); + game_tree.game_state.get_last_play().1.unwrap() + } + + pub fn average_time(&self) -> u64 { + self.times.iter().sum::() / self.times.len() as u64 + } + + pub fn expect_game(&self) -> &G { + self.game_tree.as_ref().unwrap().expect_game() + } +} diff --git a/src/game.rs b/src/game.rs index 4df95c9..a47953a 100644 --- a/src/game.rs +++ b/src/game.rs @@ -8,7 +8,7 @@ pub mod connect4; pub mod player; pub(crate) mod state; -pub trait Game: Clone + Send { +pub trait Game: Clone + Send + Sync { type Coordinate; type InputCoordinate: Copy + Eq + Ord + Hash + Display + Send + Sync; @@ -17,6 +17,8 @@ pub trait Game: Clone + Send { type Score: Scalar; + fn new() -> Self; + fn get(&self, coordinate: Self::Coordinate) -> Option<&Self::Player>; fn play<'a>( diff --git a/src/game/connect4.rs b/src/game/connect4.rs index 4138381..d5e423c 100644 --- a/src/game/connect4.rs +++ b/src/game/connect4.rs @@ -19,14 +19,6 @@ pub struct Power4 { } impl Power4 { - pub fn new() -> Power4 { - Power4 { - board: [[None; 7]; 6], - plays: 0, - last_played_coords: None, - } - } - /** * Returns all iterators for all lines having 4 or more cells */ @@ -150,7 +142,9 @@ impl Power4 { self.board[row as usize][column as usize] } - pub fn get_winner_coords(&self) -> Option<[::Coordinate; 4]> { + pub fn get_winner_coords( + &self, + ) -> Option<[::Coordinate; Self::CONNECT as usize]> { if self.last_played_coords.is_none() { return None; } @@ -159,8 +153,9 @@ impl Power4 { return None; } let last_coords = self.last_played_coords.unwrap(); + let connect = Self::CONNECT as usize; for mut line_iterator in self.lines_passing_at_longer_4(last_coords) { - let mut winner_coords: Vec<(isize, isize)> = Vec::with_capacity(7); + let mut winner_coords: Vec<(isize, isize)> = Vec::with_capacity(2 * connect - 1); let mut strike_player = NonZeroU8::new(1u8).unwrap(); let mut strike: u8 = 0; let mut cell_option = line_iterator.get_with_offset(0); @@ -170,11 +165,11 @@ impl Power4 { if strike_player == cell_player { strike += 1; - if strike == 4 { - let mut result = [(0usize, 0usize); 4]; + if strike == Self::CONNECT { + let mut result = [(0usize, 0usize); Self::CONNECT as usize]; let winner_coords_size = winner_coords.len(); - for i in 0..4 { - let (y, x) = winner_coords[winner_coords_size - 4 + i]; + for i in 0..connect { + let (y, x) = winner_coords[winner_coords_size - connect + i]; result[i] = (y as usize, x as usize); } return Some(result); @@ -219,6 +214,8 @@ impl Power4 { } count } + + const CONNECT: u8 = 4; // should be 4 for connect-4 } impl Game for Power4 { @@ -234,6 +231,14 @@ impl Game for Power4 { type Score = i32; + fn new() -> Power4 { + Power4 { + board: [[None; 7]; 6], + plays: 0, + last_played_coords: None, + } + } + fn get(&self, (row, column): (usize, usize)) -> Option<&NonZeroU8> { if row >= 6 || column >= 7 { return None; @@ -304,6 +309,13 @@ impl Game for Power4 { */ match strike { + Self::CONNECT => { + return if strike_player == player { + i32::MAX + } else { + i32::MIN + }; + } 2 => { if (is_playable(before3()) && is_playable(before2())) // space 2 before || (is_playable(after1()) && is_playable(after2())) @@ -328,13 +340,6 @@ impl Game for Power4 { } } } - 4 => { - return if strike_player == player { - i32::MAX - } else { - i32::MIN - }; - } _ => {} } } else { @@ -357,54 +362,25 @@ impl Game for Power4 { } } - /* - fn get_winner(&self) -> Option { - if self.last_played_coords.is_none() { - return None; - } - let last_coords = self.last_played_coords.unwrap(); - for mut line_iterator in self.lines_passing_at_longer_4(last_coords) { - let mut strike_player = NonZeroU8::new(1u8).unwrap(); - let mut strike: u8 = 0; - let mut cell_option = line_iterator.get_with_offset(0); - while let Some(cell) = cell_option { - if let Some(cell_player) = cell { - if strike_player == cell_player { - strike += 1; - - if strike == 4 { - return Some(cell_player); - } - } else { - strike_player = cell_player; - - strike = 1; - } - } else { - strike = 0; - } - cell_option = line_iterator.next(); - } - } - None - } - */ - fn get_winner(&self) -> Option { if self.last_played_coords.is_none() { return None; } let last_coords = self.last_played_coords.unwrap(); let counting_player = *self.get(last_coords).unwrap(); + let connect_minus1 = Self::CONNECT - 1; for count_direction in CountDirection::half_side() { // max is 3 because we don't count the middle/start cell - let count = self.count_in_direction(last_coords, count_direction, 3); - if count == 3 { + let count = self.count_in_direction(last_coords, count_direction, connect_minus1); + if count == connect_minus1 { return Some(counting_player); } - let count_opposite = - self.count_in_direction(last_coords, count_direction.opposite(), 3 - count); - if count + count_opposite == 3 { + let count_opposite = self.count_in_direction( + last_coords, + count_direction.opposite(), + connect_minus1 - count, + ); + if count + count_opposite == connect_minus1 { return Some(counting_player); } } @@ -412,7 +388,7 @@ impl Game for Power4 { } fn is_full(&self) -> bool { - for i in 0..6 { + for i in 0..7 { if self.board[0][i].is_none() { return false; } diff --git a/src/game/state.rs b/src/game/state.rs index 1666ed8..a5ac884 100644 --- a/src/game/state.rs +++ b/src/game/state.rs @@ -43,7 +43,7 @@ impl GameState { current_player.other(), last_input.expect("Cannot draw when no play has been made"), ), - _ => panic!("game is not at playing state"), + _ => panic!("game is not at playing state, but at {}", self), } } diff --git a/src/main.rs b/src/main.rs index 20ac009..d5c0ee5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,57 @@ use std::io; use std::num::NonZeroU8; +use crate::bot::Bot; use crate::game::connect4::Power4; use crate::game::player::Player; use crate::game::Game; -use crate::min_max::node::GameNode; +mod bot; mod game; mod min_max; mod scalar; fn main() { - let max_depth = 8; - - let mut times: Vec = Vec::new(); + let max_depth = 9; + let bot_vs_bot = true; let p1 = NonZeroU8::new(1).unwrap(); let p2 = NonZeroU8::new(2).unwrap(); let bot_player: NonZeroU8 = p2; - let mut current_player = if ask_start() { p1 } else { p2 }; + let mut current_player = if bot_vs_bot || ask_start() { p1 } else { p2 }; - let mut game_tree: GameNode = GameNode::new_root(Power4::new(), current_player, 0); + let mut bot: Bot = Bot::new(p2, max_depth); - let bot_vs_bot = false; // doesn't work properly yet - let mut game_tree_2 = GameNode::new_root(Power4::new(), current_player.other(), 0); + let mut other_bot: Bot = Bot::new(p1, max_depth); let mut p1_score: i32 = 0; loop { println!(); - game_tree.expect_game().print(); + bot.expect_game().print(); println!("Scores: {p1_score} for player 1"); println!(); println!("Player {current_player}'s turn"); if current_player == bot_player { - game_tree = bot_play(max_depth, &mut times, bot_player, game_tree); - game_tree_2 = GameNode::new_root( - game_tree.expect_game().clone(), - current_player, - game_tree.depth(), - ); + let play = bot.play(); + if bot_vs_bot { + other_bot.other_played(play).unwrap(); + } } else { if bot_vs_bot { - game_tree_2 = bot_play(max_depth, &mut times, bot_player.other(), game_tree_2); - game_tree = GameNode::new_root( - game_tree_2.expect_game().clone(), - current_player, - game_tree_2.depth(), - ) + let play = other_bot.play(); + bot.other_played(play).unwrap(); } else { - let (cont, tree) = player_play(current_player, game_tree); - game_tree = tree; - if cont { + let result = player_play(&mut bot); + if let Err(err) = result { + println!("Invalid move: {err}\n"); continue; } } } - let game = game_tree.expect_game(); + let game = bot.expect_game(); p1_score = game.get_score(p1); @@ -74,69 +67,13 @@ fn main() { } current_player = current_player.other(); } - println!( - "Average time: {}ms", - times.iter().sum::() / times.len() as u128 - ); + println!("Average time: {}ms", bot.average_time()); } -fn player_play( - current_player: NonZeroU8, - mut game_tree: GameNode, -) -> (bool, GameNode) { +fn player_play(bot: &mut Bot) -> Result<(), &str> { let column = get_user_input(); - let had_children = !game_tree.children().is_empty(); - let (is_known_move, mut new_game_tree) = game_tree.try_into_child(column - 1); - if is_known_move { - game_tree = new_game_tree; - debug_assert!(game_tree.game().is_some()); - } else { - // Here, new_game_tree is actually game_tree, the ownership was given back to us - if had_children { - println!("Unexpected move... Maybe you are a pure genius, or a pure idiot."); - } - let result = new_game_tree - .expect_game_mut() - .play(current_player, column - 1); - if let Err(e) = result { - println!("{}", e); - game_tree = new_game_tree; - return (true, game_tree); - } - let depth = new_game_tree.depth() + 1; - let game = new_game_tree.into_expect_game(); - let next_player = current_player.other(); - game_tree = GameNode::new_root(game, next_player, depth); - } - (false, game_tree) -} - -fn bot_play( - max_depth: u32, - times: &mut Vec, - bot_player: NonZeroU8, - mut game_tree: GameNode, -) -> GameNode { - let start = std::time::Instant::now(); - game_tree.explore_children( - bot_player, - max_depth, - game_tree.expect_game().plays() as u32, - ); - //println!("Tree:\n {}", game_tree.debug(3)); - //println!("Into best child..."); - game_tree = game_tree.into_best_child(); - let time = start.elapsed().as_millis(); - times.push(time); - println!("Done in {}ms", time); - let weight_opt = game_tree.weight(); - if weight_opt.is_some_and(|it| it > i32::MAX - 1000) { - println!("You're dead, sorry."); - } else if weight_opt.is_some_and(|it| it < i32::MIN + 1000) { - println!("Ok I'm basically dead..."); - } - debug_assert!(game_tree.game().is_some()); - game_tree + let play = column - 1; + bot.other_played(play) } fn ask_start() -> bool { diff --git a/src/min_max.rs b/src/min_max.rs index a8ead81..4e1884e 100644 --- a/src/min_max.rs +++ b/src/min_max.rs @@ -10,12 +10,13 @@ use crate::scalar::Scalar; pub mod node; -impl GameNode { +impl GameNode { pub fn explore_children(&mut self, bot_player: G::Player, max_depth: u32, real_plays: u32) { let now_playing = match self.game_state { PlayersTurn(playing_player, _) => playing_player, _ => panic!( - "Cannot explore children of a node that is not starting or played by a player" + "Cannot explore children of a node that is not starting or played by a player. Current state: {}", + self.game_state ), }; @@ -191,9 +192,15 @@ impl GameNode { } fn check_draw(&mut self) -> bool { + // consider draw as a loss for the bot, but not a loss as important as a real loss + let half_loose = G::Score::MIN().div(2); + if let Draw(_, _) = self.game_state { + // if we are here, it means that this function was called twice on the same node + self.expect_game().print(); + self.set_weight(Some(half_loose)); + return true; + } if self.game.as_ref().unwrap().is_full() { - // consider draw as a loss for the bot, but not a loss as important as a real loss - let half_loose = G::Score::MIN().div(2); self.set_weight(Some(half_loose)); self.game_state = self.game_state.to_draw(); return true; diff --git a/src/min_max/node.rs b/src/min_max/node.rs index 064e220..7fefcb2 100644 --- a/src/min_max/node.rs +++ b/src/min_max/node.rs @@ -124,7 +124,7 @@ impl GameNode { fn count_depth(&self) -> u32 { let mut max_depth = 0; for child in self.children.values() { - let child_depth = child.count_depth(); + let child_depth = child.count_depth() + 1; if child_depth > max_depth { max_depth = child_depth; } diff --git a/src/scalar.rs b/src/scalar.rs index 4bd104f..1c1ac18 100644 --- a/src/scalar.rs +++ b/src/scalar.rs @@ -3,7 +3,7 @@ use std::ops::{Add, Sub}; #[allow(non_snake_case)] pub trait Scalar: - Sized + Add + Sub + PartialEq + Ord + Copy + Clone + Display + Debug + Send + Sync + Sized + Add + Sub + PartialEq + Ord + Copy + Clone + Display + Debug + Send + Sync + From { fn MIN() -> Self; fn MAX() -> Self;