Skip to content

Commit d816b42

Browse files
committed
feat: zener possible moves
1 parent 4abf82f commit d816b42

File tree

6 files changed

+152
-59
lines changed

6 files changed

+152
-59
lines changed

crates/game-solver/src/disjoint_game.rs

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,54 @@
11
use itertools::{Interleave, Itertools};
22
use thiserror::Error;
33

4+
use crate::{
5+
game::{Game, Normal, NormalImpartial},
6+
player::ImpartialPlayer,
7+
};
48
use std::{fmt::Debug, iter::Map};
5-
use crate::{game::{Game, Normal, NormalImpartial}, player::ImpartialPlayer};
69

710
/// Represents the disjoint sum of
811
/// two impartial normal combinatorial games.
9-
///
12+
///
1013
/// Since `Game` isn't object safe, we use `dyn Any` internally with downcast safety.
11-
///
14+
///
1215
/// We restrict games to being normal impartial to force implementation of the marker trait.
1316
#[derive(Clone)]
1417
pub struct DisjointImpartialNormalGame<L: Game + NormalImpartial, R: Game + NormalImpartial> {
1518
left: L,
16-
right: R
19+
right: R,
1720
}
1821

1922
#[derive(Clone)]
2023
pub enum DisjointMove<L: Game, R: Game> {
2124
LeftMove(L::Move),
22-
RightMove(R::Move)
25+
RightMove(R::Move),
2326
}
2427

2528
#[derive(Debug, Error, Clone)]
2629
pub enum DisjointMoveError<L: Game, R: Game> {
2730
#[error("Could not make the move on left: {0}")]
2831
LeftError(L::MoveError),
2932
#[error("Could not make the move on right: {0}")]
30-
RightError(R::MoveError)
33+
RightError(R::MoveError),
3134
}
3235

3336
type LeftMoveMap<L, R> = Box<dyn Fn(<L as Game>::Move) -> DisjointMove<L, R>>;
3437
type RightMoveMap<L, R> = Box<dyn Fn(<R as Game>::Move) -> DisjointMove<L, R>>;
3538

36-
impl<
37-
L: Game + Debug + NormalImpartial + 'static,
38-
R: Game + Debug + NormalImpartial + 'static
39-
> Normal for DisjointImpartialNormalGame<L, R> {}
39+
impl<L: Game + Debug + NormalImpartial + 'static, R: Game + Debug + NormalImpartial + 'static>
40+
Normal for DisjointImpartialNormalGame<L, R>
41+
{
42+
}
4043

41-
impl<
42-
L: Game + Debug + NormalImpartial + 'static,
43-
R: Game + Debug + NormalImpartial + 'static
44-
> NormalImpartial for DisjointImpartialNormalGame<L, R> {}
44+
impl<L: Game + Debug + NormalImpartial + 'static, R: Game + Debug + NormalImpartial + 'static>
45+
NormalImpartial for DisjointImpartialNormalGame<L, R>
46+
{
47+
}
4548

46-
impl<
47-
L: Game + Debug + NormalImpartial + 'static,
48-
R: Game + Debug + NormalImpartial + 'static
49-
> Game for DisjointImpartialNormalGame<L, R> {
49+
impl<L: Game + Debug + NormalImpartial + 'static, R: Game + Debug + NormalImpartial + 'static> Game
50+
for DisjointImpartialNormalGame<L, R>
51+
{
5052
type Move = DisjointMove<L, R>;
5153
type Iter<'a> = Interleave<
5254
Map<<L as Game>::Iter<'a>, LeftMoveMap<L, R>>,
@@ -61,36 +63,41 @@ impl<
6163
}
6264

6365
fn max_moves(&self) -> Option<usize> {
64-
self.left.max_moves()
65-
.map(
66-
|l| self.right.max_moves()
67-
.map(|r| l + r)
68-
).flatten()
66+
self.left
67+
.max_moves()
68+
.map(|l| self.right.max_moves().map(|r| l + r))
69+
.flatten()
6970
}
7071

7172
fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError> {
7273
match m {
73-
DisjointMove::LeftMove(l) =>
74-
self.left.make_move(l).map_err(|err| DisjointMoveError::LeftError(err)),
75-
DisjointMove::RightMove(r) =>
76-
self.right.make_move(r).map_err(|err| DisjointMoveError::RightError(err))
74+
DisjointMove::LeftMove(l) => self
75+
.left
76+
.make_move(l)
77+
.map_err(|err| DisjointMoveError::LeftError(err)),
78+
DisjointMove::RightMove(r) => self
79+
.right
80+
.make_move(r)
81+
.map_err(|err| DisjointMoveError::RightError(err)),
7782
}
7883
}
7984

8085
fn possible_moves(&self) -> Self::Iter<'_> {
8186
fn as_left<L: Game, R: Game>(m: L::Move) -> DisjointMove<L, R> {
8287
DisjointMove::LeftMove(m)
8388
}
84-
89+
8590
fn as_right<L: Game, R: Game>(m: R::Move) -> DisjointMove<L, R> {
8691
DisjointMove::RightMove(m)
8792
}
8893

89-
self.left.possible_moves()
94+
self.left
95+
.possible_moves()
9096
.map(Box::new(as_left) as LeftMoveMap<L, R>)
9197
.interleave(
92-
self.right.possible_moves()
93-
.map(Box::new(as_right) as RightMoveMap<L, R>)
98+
self.right
99+
.possible_moves()
100+
.map(Box::new(as_right) as RightMoveMap<L, R>),
94101
)
95102
}
96103

crates/game-solver/src/game.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub enum GameState<P: Player> {
1717
}
1818

1919
/// Marks a game as being 'normal' (a game has the 'normal play' convention).
20-
///
20+
///
2121
/// Rather, this means that the game is won by whoever plays last.
2222
/// Under this convention, no ties are possible: there has to exist a strategy
2323
/// for players to be able to force a win.
@@ -37,19 +37,19 @@ pub trait Normal: Game {
3737
/// the disjunctive sum of two games is equal to another normal-play game.
3838
pub trait NormalImpartial: Normal {
3939
/// Splits a game into multiple separate games.
40-
///
40+
///
4141
/// This function doesn't have to be necessarily optimal, but
4242
/// it makes normal impartial game analysis much quicker,
4343
/// using the technique described in [Nimbers Are Inevitable](https://arxiv.org/abs/1011.5841).
44-
///
44+
///
4545
/// Returns `Option::None`` if the game currently can not be split.
4646
fn split(&self) -> Option<Vec<Self>> {
4747
None
4848
}
4949
}
5050

5151
/// Marks a game as being 'misere' (a game has the 'misere play' convention).
52-
///
52+
///
5353
/// Rather, this means that the game is lost by whoever plays last.
5454
/// Under this convention, no ties are possible: there has to exist a strategy
5555
/// for players to be able to force a win.

crates/game-solver/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//! [the book](https://leodog896.github.io/game-solver/book) is
55
//! a great place to start.
66
7-
pub mod game;
87
pub mod disjoint_game;
8+
pub mod game;
99
pub mod player;
1010
pub mod stats;
1111
// TODO: reinforcement

crates/games/src/zener/gui.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

crates/games/src/zener/mod.rs

Lines changed: 105 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
pub mod gui;
55

66
use array2d::Array2D;
7-
use game_solver::{game::{Game, GameState}, player::PartizanPlayer};
7+
use game_solver::{
8+
game::{Game, GameState},
9+
player::PartizanPlayer,
10+
};
811
use thiserror::Error;
912

1013
#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
@@ -13,7 +16,7 @@ pub enum InnerCellType {
1316
Cross,
1417
Circle,
1518
Square,
16-
Star
19+
Star,
1720
}
1821

1922
#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
@@ -46,32 +49,43 @@ impl Default for Zener {
4649
board[(NUM_ROWS - 1, 3)] = vec![CellType(InnerCellType::Square, PartizanPlayer::Right)];
4750
board[(NUM_ROWS - 1, 2)] = vec![CellType(InnerCellType::Wave, PartizanPlayer::Right)];
4851
board[(NUM_ROWS - 1, 1)] = vec![CellType(InnerCellType::Cross, PartizanPlayer::Right)];
49-
board[(NUM_ROWS - 1, 0)] = vec![CellType(InnerCellType::Circle, PartizanPlayer::Right)];
52+
board[(NUM_ROWS - 1, 0)] = vec![CellType(InnerCellType::Circle, PartizanPlayer::Right)];
5053

5154
Self {
5255
board,
5356
player: PartizanPlayer::Left,
5457
compulsory: None,
5558
move_count: 0,
5659

57-
gutter: None
60+
gutter: None,
5861
}
5962
}
6063
}
6164

65+
#[derive(Clone, Copy, Debug)]
66+
pub enum ZenerPosition {
67+
Position(usize, usize),
68+
Gutter,
69+
}
70+
6271
#[derive(Error, Clone, Debug)]
6372
pub enum ZenerMoveError {
64-
#[error("can not move at {0:?} since there's no piece!")]
65-
NoPiece(ZenerPosition)
73+
#[error("can not move from {0:?} since there's no piece!")]
74+
NoPiece((usize, usize)),
75+
#[error("can not move a piece 'from' a non-existent position {0:?}")]
76+
FromOutOfBounds((usize, usize)),
77+
#[error("can not move a piece 'to' a non-existent position {0:?}")]
78+
ToOutOfBounds((usize, usize)),
79+
#[error("the gutter is filled - the game is already won!")]
80+
GutterFilled,
81+
#[error("can't move {want:?}: need to move {need:?}")]
82+
Compulsory { want: CellType, need: InnerCellType },
6683
}
6784

68-
/// (row, col)
69-
pub type ZenerPosition = (usize, usize);
70-
7185
#[derive(Clone, Copy)]
7286
pub struct ZenerMove {
73-
from: ZenerPosition,
74-
to: ZenerPosition
87+
from: (usize, usize),
88+
to: ZenerPosition,
7589
}
7690

7791
impl Game for Zener {
@@ -82,7 +96,6 @@ impl Game for Zener {
8296
type MoveError = ZenerMoveError;
8397

8498
fn max_moves(&self) -> Option<usize> {
85-
// TODO
8699
None
87100
}
88101

@@ -91,19 +104,91 @@ impl Game for Zener {
91104
}
92105

93106
fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError> {
107+
if self.gutter.is_some() {
108+
return Err(ZenerMoveError::GutterFilled);
109+
}
110+
111+
// check that to is in bounds
112+
if let ZenerPosition::Position(row, col) = m.to {
113+
if self.board.get(row, col).is_none() {
114+
return Err(ZenerMoveError::ToOutOfBounds((row, col)));
115+
}
116+
}
117+
94118
// get the piece to move
95-
let from_piece = self.board[m.from]
96-
.last()
97-
.ok_or(ZenerMoveError::NoPiece(m.from))?.clone();
98-
119+
let from_piece = self
120+
.board
121+
.get_mut(m.from.0, m.from.1)
122+
.ok_or(ZenerMoveError::FromOutOfBounds(m.from))?
123+
.pop()
124+
.ok_or(ZenerMoveError::NoPiece(m.from))?
125+
.clone();
126+
127+
if let Some(compulsory) = self.compulsory {
128+
if from_piece.0 != compulsory {
129+
return Err(ZenerMoveError::Compulsory {
130+
want: from_piece,
131+
need: compulsory,
132+
});
133+
}
134+
}
135+
99136
// add it on the 'to' stack.
100-
self.board[m.to].push(from_piece);
101-
137+
match m.to {
138+
ZenerPosition::Position(row, col) => self
139+
.board
140+
.get_mut(row, col)
141+
.expect("Guard check failed - this shouldn't happen.") // guaranteed with the guard check
142+
.push(from_piece),
143+
ZenerPosition::Gutter => unimplemented!(),
144+
}
145+
102146
Ok(())
103147
}
104148

105149
fn possible_moves(&self) -> Self::Iter<'_> {
106-
unimplemented!()
150+
if self.gutter.is_some() {
151+
return vec![].into_iter();
152+
}
153+
154+
let mut moves = Vec::new();
155+
156+
for (row, col) in self.board.indices_row_major() {
157+
if self.board.get(row, col).map(|cell| cell.last()).flatten().is_none() {
158+
continue;
159+
};
160+
161+
let offsets: Vec<(isize, isize)> = vec![(1, 0), (-1, 0), (0, 1), (0, -1)];
162+
for offset in offsets {
163+
let new_row = (row as isize) + offset.0;
164+
let new_col = (col as isize) + offset.1;
165+
166+
if new_row == -1 || new_row == (NUM_ROWS as isize) {
167+
moves.push(ZenerMove {
168+
from: (row, col),
169+
to: ZenerPosition::Gutter,
170+
});
171+
continue;
172+
}
173+
174+
let Ok(new_col) = new_col.try_into() else {
175+
continue;
176+
};
177+
178+
if self
179+
.board
180+
.get(new_row.try_into().unwrap(), new_col)
181+
.is_some()
182+
{
183+
moves.push(ZenerMove {
184+
from: (row, col),
185+
to: ZenerPosition::Position(new_row.try_into().unwrap(), new_col),
186+
});
187+
}
188+
}
189+
}
190+
191+
return moves.into_iter();
107192
}
108193

109194
fn player(&self) -> Self::Player {
@@ -113,7 +198,7 @@ impl Game for Zener {
113198
fn state(&self) -> GameState<Self::Player> {
114199
match self.gutter {
115200
None => GameState::Playable,
116-
Some(CellType(_, player)) => GameState::Win(player)
201+
Some(CellType(_, player)) => GameState::Win(player),
117202
}
118203
}
119204
}

crates/nimnim/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::{collections::HashSet, ops::Add};
22

33
/// A nimber is the size of a heap in a single-stack nim game.
4-
///
4+
///
55
/// Nim is crucial for loop-free* impartial combinatorial game theory analysis.
6-
///
6+
///
77
/// *This structure does not define utilities for loopy nimbers.
88
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
99
pub struct Nimber(pub usize);
@@ -22,7 +22,7 @@ impl Add for Nimber {
2222
pub fn mex(list: &[Nimber]) -> Option<Nimber> {
2323
let mut mex: Option<Nimber> = None;
2424
let mut set: HashSet<Nimber> = HashSet::with_capacity(list.len());
25-
25+
2626
for item in list {
2727
if set.insert(*item) {
2828
if item > &mex.unwrap_or(Nimber(0)) {

0 commit comments

Comments
 (0)