505 lines
16 KiB
Rust
505 lines
16 KiB
Rust
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<PlayerIdentifier> {
|
|
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<PlayerIdentifier> {
|
|
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<R: BoardRepresentation = PerPlayer> {
|
|
pub p1: PlayerState,
|
|
pub p2: PlayerState,
|
|
pub walls: R,
|
|
pub current_player: PlayerIdentifier,
|
|
}
|
|
|
|
impl<R: BoardRepresentation> GameState<R> {
|
|
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<f64> {
|
|
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<R: BoardRepresentation> Default for GameState<R> {
|
|
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<PlayerIdentifier>| {
|
|
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));
|
|
}
|
|
}
|