|
| 1 | +//! Amazons game |
| 2 | +
|
| 3 | +use crate::{ |
| 4 | + grid::{decompositions, move_top_left, vec_grid::VecGrid, FiniteGrid, Grid}, |
| 5 | + short::partizan::partizan_game::PartizanGame, |
| 6 | +}; |
| 7 | +use cgt_derive::Tile; |
| 8 | +use std::{fmt::Display, hash::Hash, str::FromStr}; |
| 9 | + |
| 10 | +/// Tile in the game of Amazons |
| 11 | +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Tile)] |
| 12 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| 13 | +pub enum Tile { |
| 14 | + /// Empty tile without stones |
| 15 | + #[tile(char('.'), default)] |
| 16 | + Empty, |
| 17 | + |
| 18 | + /// Tile with Left player's Amazon - black queen |
| 19 | + #[tile(char('x'))] |
| 20 | + Left, |
| 21 | + |
| 22 | + /// Tile with Right player's Amazon - white queen |
| 23 | + #[tile(char('o'))] |
| 24 | + Right, |
| 25 | + |
| 26 | + /// Stone |
| 27 | + #[tile(char('#'))] |
| 28 | + Stone, |
| 29 | +} |
| 30 | + |
| 31 | +impl Tile { |
| 32 | + #[inline] |
| 33 | + fn is_non_blocking(self: Tile) -> bool { |
| 34 | + self != Tile::Stone |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +/// Game of Amazons |
| 39 | +#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| 40 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| 41 | +pub struct Amazons<G = VecGrid<Tile>> { |
| 42 | + grid: G, |
| 43 | +} |
| 44 | + |
| 45 | +impl<G> Display for Amazons<G> |
| 46 | +where |
| 47 | + G: Grid<Item = Tile> + FiniteGrid, |
| 48 | +{ |
| 49 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 50 | + self.grid.display(f, '|') |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +impl<G> FromStr for Amazons<G> |
| 55 | +where |
| 56 | + G: Grid<Item = Tile> + FiniteGrid, |
| 57 | +{ |
| 58 | + type Err = (); |
| 59 | + |
| 60 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 61 | + Ok(Self::new(G::parse(s).ok_or(())?)) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +const DIRECTIONS: [(i32, i32); 8] = [ |
| 66 | + (-1, 0), |
| 67 | + (-1, 1), |
| 68 | + (0, 1), |
| 69 | + (1, 1), |
| 70 | + (1, 0), |
| 71 | + (1, -1), |
| 72 | + (0, -1), |
| 73 | + (-1, -1), |
| 74 | +]; |
| 75 | + |
| 76 | +impl<G> Amazons<G> |
| 77 | +where |
| 78 | + G: Grid<Item = Tile> + FiniteGrid, |
| 79 | +{ |
| 80 | + /// Create new Amazons game from a grid |
| 81 | + #[inline] |
| 82 | + pub fn new(grid: G) -> Self { |
| 83 | + Amazons { grid } |
| 84 | + } |
| 85 | + |
| 86 | + fn moves_for(&self, own_amazon: Tile) -> Vec<Self> |
| 87 | + where |
| 88 | + G: Clone + PartialEq, |
| 89 | + { |
| 90 | + let longer_side = self.grid.height().max(self.grid.width()); |
| 91 | + |
| 92 | + let mut moves = Vec::new(); |
| 93 | + for y in 0..self.grid.height() as i32 { |
| 94 | + for x in 0..self.grid.width() as i32 { |
| 95 | + if self.grid.get(x as u8, y as u8) == own_amazon { |
| 96 | + for (amazon_dir_x, amazon_dir_y) in DIRECTIONS { |
| 97 | + for k in 1..longer_side as i32 { |
| 98 | + let new_amazon_x = x + amazon_dir_x * k; |
| 99 | + let new_amazon_y = y + amazon_dir_y * k; |
| 100 | + |
| 101 | + if new_amazon_x < 0 |
| 102 | + || new_amazon_x >= self.grid.width() as i32 |
| 103 | + || new_amazon_y < 0 |
| 104 | + || new_amazon_y >= self.grid.height() as i32 |
| 105 | + || self.grid.get(new_amazon_x as u8, new_amazon_y as u8) |
| 106 | + != Tile::Empty |
| 107 | + { |
| 108 | + break; |
| 109 | + } |
| 110 | + let mut new_grid = self.grid.clone(); |
| 111 | + new_grid.set(x as u8, y as u8, Tile::Empty); |
| 112 | + new_grid.set(new_amazon_x as u8, new_amazon_y as u8, own_amazon); |
| 113 | + for (arrow_dir_x, arrow_dir_y) in DIRECTIONS { |
| 114 | + for l in 1..longer_side as i32 { |
| 115 | + let new_arrow_x = new_amazon_x + arrow_dir_x * l; |
| 116 | + let new_arrow_y = new_amazon_y + arrow_dir_y * l; |
| 117 | + |
| 118 | + if new_arrow_x < 0 |
| 119 | + || new_arrow_x >= new_grid.width() as i32 |
| 120 | + || new_arrow_y < 0 |
| 121 | + || new_arrow_y >= new_grid.height() as i32 |
| 122 | + || new_grid.get(new_arrow_x as u8, new_arrow_y as u8) |
| 123 | + != Tile::Empty |
| 124 | + { |
| 125 | + break; |
| 126 | + } |
| 127 | + let mut new_grid = new_grid.clone(); |
| 128 | + new_grid.set(new_arrow_x as u8, new_arrow_y as u8, Tile::Stone); |
| 129 | + let new_grid = move_top_left(&new_grid, Tile::is_non_blocking); |
| 130 | + moves.push(Self::new(new_grid)); |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + moves |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +impl<G> PartizanGame for Amazons<G> |
| 144 | +where |
| 145 | + G: Grid<Item = Tile> + FiniteGrid + Clone + Hash + Send + Sync + Eq, |
| 146 | +{ |
| 147 | + fn left_moves(&self) -> Vec<Self> { |
| 148 | + self.moves_for(Tile::Left) |
| 149 | + } |
| 150 | + |
| 151 | + fn right_moves(&self) -> Vec<Self> { |
| 152 | + self.moves_for(Tile::Right) |
| 153 | + } |
| 154 | + |
| 155 | + fn decompositions(&self) -> Vec<Self> { |
| 156 | + decompositions(&self.grid, Tile::is_non_blocking, Tile::Stone, &DIRECTIONS) |
| 157 | + .into_iter() |
| 158 | + .map(Self::new) |
| 159 | + .collect::<Vec<_>>() |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +#[cfg(test)] |
| 164 | +mod tests { |
| 165 | + use super::*; |
| 166 | + use crate::short::partizan::{ |
| 167 | + canonical_form::CanonicalForm, transposition_table::TranspositionTable, |
| 168 | + }; |
| 169 | + use std::str::FromStr; |
| 170 | + |
| 171 | + macro_rules! amazons { |
| 172 | + ($input:expr) => { |
| 173 | + Amazons::from_str($input).expect("Could not parse the game") |
| 174 | + }; |
| 175 | + } |
| 176 | + |
| 177 | + macro_rules! test_canonical_form { |
| 178 | + ($input:expr, $output:expr) => {{ |
| 179 | + let tt = TranspositionTable::new(); |
| 180 | + let pos: Amazons = amazons!($input); |
| 181 | + let cf = pos.canonical_form(&tt); |
| 182 | + let expected = CanonicalForm::from_str($output).unwrap().to_string(); |
| 183 | + assert_eq!(cf.to_string(), expected); |
| 184 | + }}; |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn canonical_form() { |
| 189 | + // Confirmed with cgsuite |
| 190 | + test_canonical_form!("x..#|....|.#.o", "{{6|{3|1, {3|0, {1/2|0}}}}, {6|{4*|-3, {3, {3|0, {1/2|0}}|-4}}}|-3, {0, {0|-2}, {1|-3}|-5}, {0, {0, *|0, {0, {1/2, {1|0}|v}|v}}|-5}, {{2, {2|0}|0, {0, {2|0, {2|0}}|0}}, {2, {3|0}|0, {0, {0, ^*|0}|-1}}, {{2|0}, {2|{1|1/4}, {2|0}}|v*, {1/2|{{0|-1}, {*|-1}|-1}}, {{0, ^*|0}|-1}}, {{3|0}, {3|1, {2|0}}, {3, {3|1}|1, {1|0, *}}|-1/16, {0|-1}, {*|-1}}|-5, {v, v*, {0, {0, ^*|0}|-1}|-5}, {{1/2|{-1/4, {0|-1}, {*|-1}|-1}}, {{1|1/4}|{-1/4|-1}}, {{1|{1|0}, {1|*}}|-1/2}|-5}}}"); |
| 191 | + } |
| 192 | +} |
0 commit comments