From db656726a877f13b4ba87317d243cad01a513821 Mon Sep 17 00:00:00 2001 From: Thomas Marsh Date: Sat, 24 Feb 2024 18:13:23 -0500 Subject: [PATCH] Add knightthrough --- src/games/knightthrough.rs | 239 +++++++++++++++++++++++++++++++++++++ src/games/mod.rs | 1 + src/util.rs | 15 ++- 3 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/games/knightthrough.rs diff --git a/src/games/knightthrough.rs b/src/games/knightthrough.rs new file mode 100644 index 0000000..a79d548 --- /dev/null +++ b/src/games/knightthrough.rs @@ -0,0 +1,239 @@ +#![allow(unused)] + +use super::bitboard; +use super::bitboard::BitBoard; +use crate::game::Game; +use crate::game::PlayerIndex; + +use serde::Serialize; +use std::fmt; + +#[derive(Copy, Clone, Serialize, Debug, Default)] +pub enum Player { + #[default] + Black, + White, +} + +impl Player { + fn next(self) -> Player { + match self { + Player::Black => Player::White, + Player::White => Player::Black, + } + } +} + +impl PlayerIndex for Player { + fn to_index(&self) -> usize { + *self as usize + } +} + +#[derive(Clone, Copy, Serialize, Debug, Hash, PartialEq, Eq)] +pub struct Move(u8, u8); + +impl Move { + #[inline(always)] + fn src(self) -> usize { + self.0 as usize + } + + #[inline(always)] + fn dst(self) -> usize { + self.1 as usize + } +} + +#[derive(Clone, Copy, Serialize, Debug)] +pub struct State { + black: BitBoard, + white: BitBoard, + turn: Player, + winner: bool, +} + +impl Default for State { + fn default() -> Self { + debug_assert!(N > 5); + debug_assert!(M > 0); + + let n = BitBoard::wall(bitboard::Direction::North); + let s = BitBoard::wall(bitboard::Direction::South); + + let black = n | n.shift_south(); + let white = s | s.shift_north(); + + Self { + black, + white, + turn: Player::Black, + winner: false, + } + } +} + +impl State { + #[inline(always)] + fn occupied(&self) -> BitBoard { + self.black | self.white + } + + #[inline(always)] + fn player(&self, player: Player) -> (BitBoard, BitBoard) { + match player { + Player::Black => (self.black, BitBoard::wall(bitboard::Direction::South)), + Player::White => (self.white, BitBoard::wall(bitboard::Direction::North)), + } + } + + #[inline(always)] + fn color(&self, index: usize) -> Player { + debug_assert!(self.occupied().get(index)); + if self.black.get(index) { + Player::Black + } else { + debug_assert!(self.white.get(index)); + Player::White + } + } + + fn moves(&self, actions: &mut Vec) { + if self.winner { + return; + } + + let (player, _) = self.player(self.turn); + let (opponent, _) = self.player(self.turn.next()); + let occupied = player | opponent; + + for src in player { + let mut knight_moves = BitBoard::empty(); + let (row, col) = BitBoard::::to_coord(src); + for (dx, dy) in [ + (2, 1), + (2, -1), + (-2, 1), + (-2, -1), + (1, 2), + (1, -2), + (-1, 2), + (-1, -2), + ] { + let (r, c) = (row as isize + dx, col as isize + dy); + if r >= 0 && r < N as isize && c >= 0 && c < M as isize { + knight_moves |= BitBoard::from_coord(r as usize, c as usize); + } + } + + let available = knight_moves & !player; + for dst in available { + actions.push(Move(src as u8, dst as u8)); + } + } + } + + #[inline] + fn apply(&mut self, action: &Move) -> Self { + debug_assert!(self.occupied().get(action.0 as usize)); + let src = BitBoard::from_index(action.0 as usize); + let dst = BitBoard::from_index(action.1 as usize); + let (mut player, goal) = self.player(self.turn); + player |= dst; + player &= !src; + let opponent = self.player(self.turn.next()).0 & !dst; + + match self.turn { + Player::Black => { + self.black = player; + self.white = opponent; + } + Player::White => { + self.white = player; + self.black = opponent; + } + } + + if player.intersects(goal) { + self.winner = true; + } else { + self.turn = self.turn.next(); + } + + *self + } +} + +#[derive(Clone)] + +pub struct Knightthrough; + +impl Game for Knightthrough { + type S = State; + type A = Move; + type P = Player; + + fn apply(mut state: State, action: &Move) -> State { + state.apply(action) + } + + fn generate_actions(state: &State, actions: &mut Vec) { + state.moves(actions); + } + + fn is_terminal(state: &State) -> bool { + state.winner + } + + fn player_to_move(state: &State) -> Player { + state.turn + } + + fn winner(state: &State) -> Option { + if state.winner { + Some(state.turn) + } else { + None + } + } + + fn notation(state: &Self::S, action: &Self::A) -> String { + const COL_NAMES: &[u8] = b"ABCDEFGH"; + let (row, col) = BitBoard::::to_coord(action.0 as usize); + format!("{}{}", COL_NAMES[col] as char, row + 1) + } + + fn num_players() -> usize { + 2 + } +} + +impl fmt::Display for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for row in (0..N).rev() { + for col in 0..M { + if self.black.get_at(row, col) { + write!(f, " X")?; + } else if self.white.get_at(row, col) { + write!(f, " O")?; + } else { + write!(f, " .")?; + } + } + writeln!(f)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::util::random_play; + + use super::*; + + #[test] + fn test_knightthrough() { + random_play::>(); + } +} diff --git a/src/games/mod.rs b/src/games/mod.rs index aee1675..1c18f2b 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -5,6 +5,7 @@ pub mod breakthrough; pub mod count; pub mod druid; pub mod gonnect; +pub mod knightthrough; pub mod nim; pub mod null; pub mod ttt; diff --git a/src/util.rs b/src/util.rs index d285420..60d87fa 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,6 +6,7 @@ use rand::rngs::SmallRng; use crate::game::{Game, PlayerIndex}; use crate::strategies; +use crate::strategies::random::Random; use crate::strategies::Search; use rayon::prelude::*; use std::ops::Add; @@ -158,16 +159,26 @@ where G::S: std::fmt::Display, G::P: std::fmt::Debug, { + let mut i = 0; let mut state = G::S::default(); - println!("state:\n{state}"); + println!("[{i}] state:\n{state}"); while !G::is_terminal(&state) { let action = search.choose_action(&state); state = G::apply(state, &action); - println!("state:\n{state}"); + i += 1; + println!("[{i}] state:\n{state}"); } println!("winner: {:?}", G::winner(&state)); } +pub fn random_play() +where + G::S: std::fmt::Display, + G::P: std::fmt::Debug, +{ + self_play(Random::::new()) +} + /// Play a round-robin tournament with the provided strategies. fn round_robin( strategies: &mut [AnySearch<'_, G>],