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, pub walls_left: u8, } impl PlayerState { pub const INITIAL_WALLS: u8 = 10; pub const P1_START: Self = Self::new(4, 0, Self::INITIAL_WALLS); pub const P2_START: Self = Self::new(4, 8, Self::INITIAL_WALLS); pub const fn new(x: u8, y: u8, walls_left: u8) -> Self { let mut res = Self { xy: 0, walls_left }; res.set_x(x); res.set_y(y); res } } impl PlayerState { pub const fn x(&self) -> u8 { self.xy & 0b00001111 } pub const fn y(&self) -> u8 { (self.xy & 0b11110000) >> 4 } pub const fn set_x(&mut self, x: u8) { // zero out x self.xy &= 0b11110000; // write the x part self.xy |= x & 0b00001111; } pub const fn set_y(&mut self, y: u8) { // zero out y self.xy &= 0b00001111; // write the y part self.xy |= (y & 0b00001111) << 4; } } #[derive(Copy, Clone)] pub struct WallState { verticals: [u8; 9], horizontals: [u8; 9], } impl WallState { #[inline] fn block_cleaned_hori(&mut self, byte_idx: u8, bit: u8) { self.horizontals[byte_idx as usize] |= 1 << bit; } #[inline] fn block_cleaned_verti(&mut self, byte_idx: u8, bit: u8) { self.verticals[byte_idx as usize] |= 1 << bit; } #[inline] fn can_walk_between_cleaned_hori(&self, byte_idx: u8, bit: u8) -> bool { (self.horizontals[byte_idx as usize] >> bit) & 1 != 0 } #[inline] fn can_walk_between_cleaned_verti(&self, byte_idx: u8, bit: u8) -> bool { (self.verticals[byte_idx as usize] >> bit) & 1 != 0 } fn block(&mut self, from_x: u8, from_y: u8, to_x: u8, to_y: u8) { match (from_x.wrapping_sub(to_x), from_y.wrapping_sub(to_y)) { (1, 0) => self.block_cleaned_verti(to_y, to_x), (0xff, 0) => self.block_cleaned_verti(from_y, from_x), (0, 1) => self.block_cleaned_hori(to_x, to_y), (0, 0xff) => self.block_cleaned_hori(from_x, from_y), _ => unreachable!(), } } pub fn can_walk_between(&self, from_x: u8, from_y: u8, to_x: u8, to_y: u8) -> bool { !match (from_x.wrapping_sub(to_x), from_y.wrapping_sub(to_y)) { (1, 0) => self.can_walk_between_cleaned_verti(to_y, to_x), (0xff, 0) => self.can_walk_between_cleaned_verti(from_y, from_x), (0, 1) => self.can_walk_between_cleaned_hori(to_x, to_y), (0, 0xff) => self.can_walk_between_cleaned_hori(from_x, from_y), _ => unreachable!(), } } pub fn can_place(&self, x: u8, y: u8, vertical: bool) -> bool { if vertical { self.can_walk_between(x, y, x + 1, y) && self.can_walk_between(x, y + 1, x + 1, y + 1) } else { self.can_walk_between(x, y, x, y + 1) && self.can_walk_between(x + 1, y, x + 1, y + 1) } } pub fn place(&mut self, x: u8, y: u8, vertical: bool) { if vertical { self.block(x, y, x + 1, y); self.block(x, y + 1, x + 1, y + 1); } else { self.block(x, y, x, y + 1); self.block(x + 1, y, x + 1, y + 1); } } } impl Default for WallState { fn default() -> Self { Self { verticals: Default::default(), horizontals: Default::default(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PlayerIdentifier { P1, P2, } impl PlayerIdentifier { pub fn swap(&mut self) { use PlayerIdentifier::*; *self = match self { P1 => P2, P2 => P1, }; } pub const fn y_goal(&self) -> u8 { match self { PlayerIdentifier::P1 => 8, PlayerIdentifier::P2 => 0, } } pub const fn color(&self) -> &str { match self { PlayerIdentifier::P1 => "\x1b[38;2;0;140;247m", PlayerIdentifier::P2 => "\x1b[38;2;255;0;0m", } } } #[derive(Clone, Copy)] pub struct GameState { pub p1: PlayerState, pub p2: PlayerState, pub walls: R, pub current_player: PlayerIdentifier, } impl GameState { pub fn current_player_state(&self) -> &PlayerState { match self.current_player { PlayerIdentifier::P1 => &self.p1, PlayerIdentifier::P2 => &self.p2, } } pub fn current_player_state_mut(&mut self) -> &mut PlayerState { match self.current_player { PlayerIdentifier::P1 => &mut self.p1, PlayerIdentifier::P2 => &mut self.p2, } } pub fn mcts_result(&self) -> Option { let p1_won = self.p1.y() == PlayerIdentifier::P1.y_goal(); let p2_won = self.p2.y() == PlayerIdentifier::P2.y_goal(); let outcome_for_p1 = match (p1_won, p2_won) { (false, false) => return None, (true, false) => 1.0, (false, true) => -1.0, (true, true) => 0.0, }; Some(match self.current_player { PlayerIdentifier::P1 => outcome_for_p1, 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 { fn default() -> Self { Self { p1: PlayerState::P1_START, p2: PlayerState::P2_START, walls: Default::default(), current_player: PlayerIdentifier::P1, } } } impl Display for GameState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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", self.p1.walls_left, d1.at(self.p1.x(), self.p1.y()) )?; writeln!( f, "P2: {} walls, {} away from win", self.p2.walls_left, d2.at(self.p2.x(), self.p2.y()) )?; 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 { 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}\x1b[0m")?; if x != 8 { let wall_above = !self.walls.can_walk_between(x, y, x + 1, y); let wall_below = y != 0 && !self.walls.can_walk_between(x, y - 1, x + 1, y - 1); let wall_left = y != 0 && !self.walls.can_walk_between(x, y, x, y - 1); let wall_right = y != 0 && !self.walls.can_walk_between(x + 1, y, x + 1, y - 1); 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) } 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) }, )?; } } 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}\x1b[0m")?; } let player = if self.p1.x() == x && self.p1.y() == y { format!("\x1b[1m{}P1\x1b[0m", PlayerIdentifier::P1.color()) } else if self.p2.x() == x && self.p2.y() == y { format!("\x1b[1m{}P2\x1b[0m", PlayerIdentifier::P2.color()) } else { // format!("{:^2}", dm.at(x, y)) " ".to_owned() }; write!(f, "{player}")?; } 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, "──")?; } write!(f, "╯")?; Ok(()) } } #[cfg(test)] mod tests { use crate::gamestate::WallState; #[test] fn test_blocking() { let mut w = WallState::default(); assert!(w.can_walk_between(0, 0, 1, 0)); w.block(0, 0, 1, 0); assert!(!w.can_walk_between(0, 0, 1, 0)); w.block(8, 7, 8, 8); w.block(0, 8, 1, 8); assert!(w.can_walk_between(0, 0, 0, 1)); w.block(0, 0, 0, 1); assert!(!w.can_walk_between(0, 0, 0, 1)); assert!(w.can_walk_between(0, 7, 0, 8)); w.block(0, 7, 0, 8); assert!(!w.can_walk_between(0, 7, 0, 8)); assert!(w.can_walk_between(7, 0, 8, 0)); w.block(7, 0, 8, 0); assert!(!w.can_walk_between(7, 0, 8, 0)); } }