Skip to content

Commit 2c0c131

Browse files
committed
Add initial Amazons implementation
1 parent 118b330 commit 2c0c131

File tree

4 files changed

+204
-11
lines changed

4 files changed

+204
-11
lines changed

src/grid.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ fn bfs<G, T>(
149149
y: u8,
150150
is_non_blocking: fn(T) -> bool,
151151
blocking_tile: T,
152-
directions: &[(i64, i64)],
152+
directions: &[(i32, i32)],
153153
) -> G
154154
where
155155
T: Copy + Default,
@@ -166,13 +166,13 @@ where
166166
new_grid.set(qx, qy, grid.get(qx, qy));
167167

168168
for (dx, dy) in directions {
169-
let lx = (qx as i64) + dx;
170-
let ly = (qy as i64) + dy;
169+
let lx = (qx as i32) + dx;
170+
let ly = (qy as i32) + dy;
171171

172172
if lx >= 0
173-
&& lx < (grid.width() as i64)
173+
&& lx < (grid.width() as i32)
174174
&& ly >= 0
175-
&& ly < (grid.height() as i64)
175+
&& ly < (grid.height() as i32)
176176
&& is_non_blocking(grid.get(lx as u8, ly as u8))
177177
&& is_non_blocking(visited.get(lx as u8, ly as u8))
178178
{
@@ -189,7 +189,7 @@ pub fn decompositions<G, T>(
189189
grid: &G,
190190
is_non_blocking: fn(T) -> bool,
191191
blocking_tile: T,
192-
directions: &[(i64, i64)],
192+
directions: &[(i32, i32)],
193193
) -> Vec<G>
194194
where
195195
T: Copy + Default,

src/short/partizan/games.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Partizan games under normal play i.e. the player that cannot move in their turn loses.
22
3+
pub mod amazons;
34
pub mod domineering;
45
pub mod fission;
56
pub mod ski_jumps;

src/short/partizan/games/amazons.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
}

src/short/partizan/games/fission.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@ where
8888
&& self.grid.get(prev_x, prev_y) == Tile::Empty
8989
&& self.grid.get(next_x, next_y) == Tile::Empty
9090
{
91-
let mut new_grid: Self = self.clone();
92-
new_grid.grid.set(x, y, Tile::Empty);
93-
new_grid.grid.set(prev_x, prev_y, Tile::Stone);
94-
new_grid.grid.set(next_x, next_y, Tile::Stone);
95-
moves.push(new_grid);
91+
let mut new_grid = self.clone().grid;
92+
new_grid.set(x, y, Tile::Empty);
93+
new_grid.set(prev_x, prev_y, Tile::Stone);
94+
new_grid.set(next_x, next_y, Tile::Stone);
95+
moves.push(Self::new(new_grid));
9696
}
9797
}
9898
}

0 commit comments

Comments
 (0)