diff --git a/src/gamestate.rs b/src/gamestate.rs index 56ba3b6..7015743 100644 --- a/src/gamestate.rs +++ b/src/gamestate.rs @@ -1,9 +1,83 @@ -use std::fmt::Display; - -use simple_mcts::Game; +use std::fmt::{Display, Formatter}; use crate::pathfind::GoalDistanceMap; +pub trait BoardRepresentation: Default + Clone + Copy { + fn can_walk_between(&self, from_x: u8, from_y: u8, to_x: u8, to_y: u8) -> bool; + fn blocked_by_player( + &self, + from_x: u8, + from_y: u8, + to_x: u8, + to_y: u8, + ) -> Option { + None + } + + fn can_place(&self, x: u8, y: u8, vertical: bool) -> bool; + fn place(&mut self, x: u8, y: u8, vertical: bool, current_player: PlayerIdentifier); +} + +#[derive(Default, Clone, Copy)] +pub struct Compact(WallState); +impl BoardRepresentation for Compact { + fn can_walk_between(&self, from_x: u8, from_y: u8, to_x: u8, to_y: u8) -> bool { + self.0.can_walk_between(from_x, from_y, to_x, to_y) + } + + fn can_place(&self, x: u8, y: u8, vertical: bool) -> bool { + self.0.can_place(x, y, vertical) + } + + fn place(&mut self, x: u8, y: u8, vertical: bool, _current_player: PlayerIdentifier) { + self.0.place(x, y, vertical); + } +} + +#[derive(Default, Clone, Copy)] +pub struct PerPlayer { + p1: WallState, + p2: WallState, +} +impl BoardRepresentation for PerPlayer { + fn can_walk_between(&self, from_x: u8, from_y: u8, to_x: u8, to_y: u8) -> bool { + self.p1.can_walk_between(from_x, from_y, to_x, to_y) + && self.p2.can_walk_between(from_x, from_y, to_x, to_y) + } + fn blocked_by_player( + &self, + from_x: u8, + from_y: u8, + to_x: u8, + to_y: u8, + ) -> Option { + match ( + !self.p1.can_walk_between(from_x, from_y, to_x, to_y), + !self.p2.can_walk_between(from_x, from_y, to_x, to_y), + ) { + (false, false) => None, + (true, false) => Some(PlayerIdentifier::P1), + (false, true) => Some(PlayerIdentifier::P2), + (true, true) => unreachable!(), + } + } + + fn can_place(&self, x: u8, y: u8, vertical: bool) -> bool { + self.p1.can_place(x, y, vertical) && self.p2.can_place(x, y, vertical) + } + + fn place(&mut self, x: u8, y: u8, vertical: bool, current_player: PlayerIdentifier) { + match current_player { + PlayerIdentifier::P1 => { + self.p1.place(x, y, vertical); + } + PlayerIdentifier::P2 => { + self.p2.place(x, y, vertical); + } + } + } +} + #[derive(Clone, Copy, PartialEq)] pub struct PlayerState { xy: u8, @@ -145,17 +219,24 @@ impl PlayerIdentifier { PlayerIdentifier::P2 => 0, } } + + pub const fn color(&self) -> &str { + match self { + PlayerIdentifier::P1 => "\x1b[38;2;0;0;255m", + PlayerIdentifier::P2 => "\x1b[38;2;255;0;0m", + } + } } #[derive(Clone, Copy)] -pub struct GameState { +pub struct GameState { pub p1: PlayerState, pub p2: PlayerState, - pub walls: WallState, + pub walls: R, pub current_player: PlayerIdentifier, } -impl GameState { +impl GameState { pub fn current_player_state(&self) -> &PlayerState { match self.current_player { PlayerIdentifier::P1 => &self.p1, @@ -186,9 +267,13 @@ impl GameState { PlayerIdentifier::P2 => -1.0 * outcome_for_p1, }) } + + pub fn place(&mut self, x: u8, y: u8, vertical: bool) { + self.walls.place(x, y, vertical, self.current_player) + } } -impl Default for GameState { +impl Default for GameState { fn default() -> Self { Self { p1: PlayerState::P1_START, @@ -204,6 +289,36 @@ impl Display for GameState { let d1 = GoalDistanceMap::new(&self.walls, PlayerIdentifier::P1); let d2 = GoalDistanceMap::new(&self.walls, PlayerIdentifier::P2); + let crossing = |f: &mut Formatter<'_>, + wall_above, + wall_below, + wall_left, + wall_right, + pi: Option| { + if let Some(pi) = pi { + write!(f, "{}", pi.color())?; + } + match (wall_above, wall_below, wall_left, wall_right) { + (false, false, false, false) => write!(f, "·"), + (false, true, false, false) => write!(f, "╵"), + (true, false, false, false) => write!(f, "╷"), + (false, false, true, false) => write!(f, "╴"), + (false, false, false, true) => write!(f, "╶"), + (true, true, false, false) => write!(f, "│"), + (false, false, true, true) => write!(f, "─"), + (true, false, true, false) => write!(f, "┐"), + (false, true, false, true) => write!(f, "└"), + (false, true, true, false) => write!(f, "┘"), + (true, false, false, true) => write!(f, "┌"), + (true, true, true, false) => write!(f, "┤"), + (true, false, true, true) => write!(f, "┬"), + (false, true, true, true) => write!(f, "┴"), + (true, true, false, true) => write!(f, "├"), + (true, true, true, true) => write!(f, "┼"), + }?; + write!(f, "\x1b[0m") + }; + writeln!( f, "P1: {} walls, {} away from win", @@ -216,17 +331,42 @@ impl Display for GameState { self.p2.walls_left, d2.at(self.p2.x(), self.p2.y()) )?; - writeln!(f, "╭──┬──┬──┬──┬──┬──┬──┬──┬──╮")?; + write!(f, "╭──")?; + for x in 1..9 { + crossing( + f, + !self.walls.can_walk_between(x - 1, 0, x, 0), + false, + true, + true, + self.walls.blocked_by_player(x - 1, 0, x, 0), + )?; + write!(f, "──")?; + } + writeln!(f, "╮")?; + for y in 0..9 { if y > 0 { - write!(f, "├")?; + crossing( + f, + true, + true, + false, + !self.walls.can_walk_between(0, y - 1, 0, y), + self.walls.blocked_by_player(0, y - 1, 0, y), + )?; + for x in 0..9 { let wall = if self.walls.can_walk_between(x, y - 1, x, y) { - ' ' + " ".to_string() } else { - '─' + if let Some(pi) = self.walls.blocked_by_player(x, y - 1, x, y) { + format!("{}─", pi.color()) + } else { + "─".to_string() + } }; - write!(f, "{wall}{wall}")?; + write!(f, "{wall}{wall}\x1b[0m")?; if x != 8 { let wall_above = !self.walls.can_walk_between(x, y, x + 1, y); let wall_below = @@ -235,45 +375,81 @@ impl Display for GameState { let wall_right = y != 0 && !self.walls.can_walk_between(x + 1, y, x + 1, y - 1); - match (wall_above, wall_below, wall_left, wall_right) { - (false, false, false, false) => write!(f, "·")?, - (false, true, false, false) => write!(f, "╵")?, - (true, false, false, false) => write!(f, "╷")?, - (false, false, true, false) => write!(f, "╴")?, - (false, false, false, true) => write!(f, "╶")?, - (true, true, false, false) => write!(f, "│")?, - (false, false, true, true) => write!(f, "─")?, - (false, true, true, false) => write!(f, "┐")?, - (true, false, false, true) => write!(f, "└")?, - (true, false, true, false) => write!(f, "┘")?, - (false, true, false, true) => write!(f, "┌")?, - (true, true, true, false) => write!(f, "┤")?, - (false, true, true, true) => write!(f, "┬")?, - (true, false, true, true) => write!(f, "┴")?, - (true, true, false, true) => write!(f, "├")?, - (true, true, true, true) => write!(f, "┼")?, + let mut neighboring_colors = Vec::with_capacity(4); + + if wall_above && let Some(pi) = self.walls.blocked_by_player(x, y, x + 1, y) + { + neighboring_colors.push(pi) + } + if wall_below + && let Some(pi) = self.walls.blocked_by_player(x, y - 1, x + 1, y - 1) + { + neighboring_colors.push(pi) + } + if wall_left && let Some(pi) = self.walls.blocked_by_player(x, y, x, y - 1) + { + neighboring_colors.push(pi) + } + if wall_right + && let Some(pi) = self.walls.blocked_by_player(x + 1, y, x + 1, y - 1) + { + neighboring_colors.push(pi) } - // write!(f, "┼")?; + let num_p1 = neighboring_colors + .iter() + .filter(|i| matches!(i, PlayerIdentifier::P1)) + .count(); + let num_p2 = neighboring_colors + .iter() + .filter(|i| matches!(i, PlayerIdentifier::P2)) + .count(); + + crossing( + f, + wall_above, + wall_below, + wall_left, + wall_right, + if num_p1 == 0 && num_p2 == 0 { + None + } else if num_p1 > num_p2 { + Some(PlayerIdentifier::P1) + } else { + Some(PlayerIdentifier::P2) + }, + )?; } } - write!(f, "┤\n│")?; + crossing( + f, + true, + true, + !self.walls.can_walk_between(8, y - 1, 8, y), + false, + self.walls.blocked_by_player(8, y - 1, 8, y), + )?; + write!(f, "\n│")?; } else { write!(f, "│")?; } for x in 0..9 { if x > 0 { let wall = if self.walls.can_walk_between(x - 1, y, x, y) { - ' ' + " ".to_string() } else { - '│' + if let Some(pi) = self.walls.blocked_by_player(x - 1, y, x, y) { + format!("{}│", pi.color()) + } else { + "│".to_string() + } }; - write!(f, "{wall}")?; + write!(f, "{wall}\x1b[0m")?; } let player = if self.p1.x() == x && self.p1.y() == y { - "\x1b[1mP1\x1b[0m".to_owned() + format!("\x1b[1m{}P1\x1b[0m", PlayerIdentifier::P1.color()) } else if self.p2.x() == x && self.p2.y() == y { - "\x1b[1mP2\x1b[0m".to_owned() + format!("\x1b[1m{}P2\x1b[0m", PlayerIdentifier::P2.color()) } else { // format!("{:^2}", dm.at(x, y)) " ".to_owned() @@ -282,7 +458,18 @@ impl Display for GameState { } writeln!(f, "│")?; } - writeln!(f, "╰──┴──┴──┴──┴──┴──┴──┴──┴──╯")?; + write!(f, "╰──")?; + for x in 1..9 { + crossing( + f, + false, + !self.walls.can_walk_between(x - 1, 8, x, 8), + true, + true, + self.walls.blocked_by_player(x - 1, 8, x, 8), + )?; + write!(f, "──")?; + } Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 915ac73..08f8fa3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,21 @@ -use std::thread; -use std::time::Duration; - use simple_mcts::Game; use simple_mcts::GameEvaluator; use simple_mcts::Mcts; +use simple_mcts::MctsBatchConfig; use simple_mcts::MctsError; +use crate::gamestate::BoardRepresentation; use crate::gamestate::GameState; -use crate::gamestate::PlayerIdentifier; +use crate::gamestate::PerPlayer; mod gamestate; mod pathfind; -struct Quoridor { - state: GameState, +struct Quoridor { + state: GameState, } -impl Default for Quoridor { +impl Default for Quoridor { fn default() -> Self { Self { state: Default::default(), @@ -27,8 +26,8 @@ impl Default for Quoridor { const NUM_NEXT_STATES: usize = 64 /* horizontal */ + 64 /* vertical */ + 4 /* move directions */ + 2 /* blocked jumps */; -impl Game for Quoridor { - type State = GameState; +impl Game for Quoridor { + type State = GameState; fn new() -> Self { Self { @@ -101,7 +100,7 @@ impl Game for Quoridor { let mut set_block = |i: usize, vertical| { let x = (i / 8) as u8; let y = (i % 8) as u8; - self.state.walls.place(x, y, vertical); + self.state.place(x, y, vertical); }; match action { @@ -170,13 +169,10 @@ impl GameEvaluator for ProgressEvaluator { } fn main() -> Result<(), MctsError> { - let mut g = Quoridor::default(); - g.state.walls.place(4, 4, false); - let mut mcts: Mcts = Mcts::::new(); let evaluator = ProgressEvaluator; - while g.get_result().is_none() { + while mcts.get_game().get_result().is_none() { for _ in 0..5000 { mcts.iterate(&evaluator)?; } @@ -196,6 +192,8 @@ fn main() -> Result<(), MctsError> { // println!("{top_5:?}"); // let best_action_index = x.last().map(|(index, _)| *index).unwrap_or(0); + println!("\x1b[2J"); + println!("\x1b[2H"); println!("best action: {best_action_index}"); mcts.play(best_action_index)?; diff --git a/src/pathfind.rs b/src/pathfind.rs index a00f345..3f3bfb6 100644 --- a/src/pathfind.rs +++ b/src/pathfind.rs @@ -1,13 +1,13 @@ use std::collections::VecDeque; -use crate::gamestate::{PlayerIdentifier, WallState}; +use crate::gamestate::{BoardRepresentation, PlayerIdentifier, WallState}; pub struct GoalDistanceMap { distances: [[u8; 9]; 9], } impl GoalDistanceMap { - pub fn new(w: &WallState, for_player: PlayerIdentifier) -> Self { + pub fn new(w: &R, for_player: PlayerIdentifier) -> Self { let mut todo = VecDeque::with_capacity(9 * 9); let mut res = [[u8::MAX; 9]; 9];