Skip to content

Commit 3a34d19

Browse files
committed
feat: normal/misere marker traits, disjoint games
1 parent 4135443 commit 3a34d19

File tree

10 files changed

+153
-60
lines changed

10 files changed

+153
-60
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use itertools::{Interleave, Itertools};
2+
use thiserror::Error;
3+
4+
use std::{fmt::Debug, iter::Map};
5+
use crate::{game::{Game, Normal, NormalImpartial}, player::ImpartialPlayer};
6+
7+
/// Represents the disjoint sum of
8+
/// two impartial normal combinatorial games.
9+
///
10+
/// Since `Game` isn't object safe, we use `dyn Any` internally with downcast safety.
11+
#[derive(Clone)]
12+
pub struct DisjointImpartialNormalGame<L: Game, R: Game> {
13+
left: L,
14+
right: R
15+
}
16+
17+
#[derive(Clone)]
18+
pub enum DisjointMove<L: Game, R: Game> {
19+
LeftMove(L::Move),
20+
RightMove(R::Move)
21+
}
22+
23+
#[derive(Debug, Error, Clone)]
24+
pub enum DisjointMoveError<L: Game, R: Game> {
25+
#[error("Could not make the move on left: {0}")]
26+
LeftError(L::MoveError),
27+
#[error("Could not make the move on right: {0}")]
28+
RightError(R::MoveError)
29+
}
30+
31+
type LeftMoveMap<L, R> = Box<dyn Fn(<L as Game>::Move) -> DisjointMove<L, R>>;
32+
type RightMoveMap<L, R> = Box<dyn Fn(<R as Game>::Move) -> DisjointMove<L, R>>;
33+
34+
impl<L: Game + Debug + 'static, R: Game + Debug + 'static> Normal for DisjointImpartialNormalGame<L, R> {}
35+
impl<L: Game + Debug + 'static, R: Game + Debug + 'static> NormalImpartial for DisjointImpartialNormalGame<L, R> {}
36+
impl<L: Game + Debug + 'static, R: Game + Debug + 'static> Game for DisjointImpartialNormalGame<L, R> {
37+
type Move = DisjointMove<L, R>;
38+
type Iter<'a> = Interleave<
39+
Map<<L as Game>::Iter<'a>, LeftMoveMap<L, R>>,
40+
Map<<R as Game>::Iter<'a>, RightMoveMap<L, R>>
41+
> where L: 'a, R: 'a, L::Move: 'a, R::Move: 'a;
42+
43+
type Player = ImpartialPlayer;
44+
type MoveError = DisjointMoveError<L, R>;
45+
46+
fn move_count(&self) -> usize {
47+
self.left.move_count() + self.right.move_count()
48+
}
49+
50+
fn max_moves(&self) -> Option<usize> {
51+
self.left.max_moves()
52+
.map(
53+
|l| self.right.max_moves()
54+
.map(|r| l + r)
55+
).flatten()
56+
}
57+
58+
fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError> {
59+
match m {
60+
DisjointMove::LeftMove(l) =>
61+
self.left.make_move(l).map_err(|err| DisjointMoveError::LeftError(err)),
62+
DisjointMove::RightMove(r) =>
63+
self.right.make_move(r).map_err(|err| DisjointMoveError::RightError(err))
64+
}
65+
}
66+
67+
fn possible_moves(&self) -> Self::Iter<'_> {
68+
fn as_left<L: Game, R: Game>(m: L::Move) -> DisjointMove<L, R> {
69+
DisjointMove::LeftMove(m)
70+
}
71+
72+
fn as_right<L: Game, R: Game>(m: R::Move) -> DisjointMove<L, R> {
73+
DisjointMove::RightMove(m)
74+
}
75+
76+
self.left.possible_moves()
77+
.map(Box::new(as_left) as LeftMoveMap<L, R>)
78+
.interleave(
79+
self.right.possible_moves()
80+
.map(Box::new(as_right) as RightMoveMap<L, R>)
81+
)
82+
}
83+
84+
fn state(&self) -> crate::game::GameState<Self::Player> {
85+
<Self as Normal>::state(&self)
86+
}
87+
88+
fn player(&self) -> Self::Player {
89+
ImpartialPlayer::Next
90+
}
91+
}

crates/game-solver/src/game.rs

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,49 @@ pub enum GameState<P: Player> {
1616
Win(P),
1717
}
1818

19-
/// Defines the 'state' the game is in.
19+
/// Marks a game as being 'normal' (a game has the 'normal play' convention).
20+
///
21+
/// Rather, this means that the game is won by whoever plays last.
22+
/// Under this convention, no ties are possible: there has to exist a strategy
23+
/// for players to be able to force a win.
2024
///
21-
/// Generally used by a game solver for better optimizations.
22-
///
23-
/// This is usually wrapped in an Option, as there are many games that do not classify
24-
/// as being under 'Normal' or 'Misere.' (i.e. tic-tac-toe)
25-
#[non_exhaustive]
26-
pub enum StateType {
27-
/// If a game is under 'normal play' convention, the last player to move wins.
28-
/// There are no ties in this variant.
29-
///
30-
/// Learn more: <https://en.wikipedia.org/wiki/Normal_play_convention>
31-
Normal,
32-
/// If a game is under 'misere play' convention, the last player to move loses.
33-
/// There are no ties in this variant.
34-
///
35-
/// Learn more: <https://en.wikipedia.org/wiki/Mis%C3%A8re#Mis%C3%A8re_game>
36-
Misere,
25+
/// Learn more: <https://en.wikipedia.org/wiki/Normal_play_convention>
26+
pub trait Normal: Game {
27+
fn state(&self) -> GameState<Self::Player> {
28+
if self.possible_moves().next().is_none() {
29+
GameState::Win(self.player().previous())
30+
} else {
31+
GameState::Playable
32+
}
33+
}
3734
}
3835

39-
impl StateType {
40-
pub fn state<T>(&self, game: &T) -> GameState<T::Player>
41-
where
42-
T: Game,
43-
{
44-
if game.possible_moves().next().is_none() {
45-
GameState::Win(match self {
46-
Self::Misere => game.player(),
47-
Self::Normal => game.player().previous(),
48-
})
36+
/// Normal impartial games have the special property of being splittable: i.e.,
37+
/// the disjunctive sum of two games is equal to another normal-play game.
38+
pub trait NormalImpartial: Normal {
39+
/// Splits a game into multiple separate games.
40+
///
41+
/// This function doesn't have to be necessarily optimal, but
42+
/// it makes normal impartial game analysis much quicker,
43+
/// using the technique described in [Nimbers Are Inevitable](https://arxiv.org/abs/1011.5841).
44+
///
45+
/// Returns `Option::None`` if the game currently can not be split.
46+
fn split(&self) -> Option<Vec<Self>> {
47+
None
48+
}
49+
}
50+
51+
/// Marks a game as being 'misere' (a game has the 'misere play' convention).
52+
///
53+
/// Rather, this means that the game is lost by whoever plays last.
54+
/// Under this convention, no ties are possible: there has to exist a strategy
55+
/// for players to be able to force a win.
56+
///
57+
/// Learn more: <https://en.wikipedia.org/wiki/Mis%C3%A8re#Mis%C3%A8re_game>
58+
pub trait Misere: Game {
59+
fn state<T>(&self) -> GameState<Self::Player> {
60+
if self.possible_moves().next().is_none() {
61+
GameState::Win(self.player())
4962
} else {
5063
GameState::Playable
5164
}
@@ -72,8 +85,6 @@ pub trait Game: Clone {
7285

7386
type Player: Player;
7487

75-
const STATE_TYPE: Option<StateType>;
76-
7788
/// Returns the amount of moves that have been played
7889
fn move_count(&self) -> usize;
7990

@@ -131,12 +142,11 @@ pub trait Game: Clone {
131142
/// Returns the current state of the game.
132143
/// Used for verifying initialization and is commonly called.
133144
///
134-
/// If `Self::STATE_TYPE` isn't None,
135145
/// the following implementation can be used:
136146
///
137147
/// ```ignore
138148
/// fn state(&self) -> GameState<Self::Player> {
139-
/// Self::STATE_TYPE.unwrap().state(self)
149+
/// <Self as Normal>::state(&self) // or Misere if misere.
140150
/// }
141151
/// ```
142152
fn state(&self) -> GameState<Self::Player>;

crates/game-solver/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! a great place to start.
66
77
pub mod game;
8+
pub mod disjoint_game;
89
pub mod player;
910
pub mod stats;
1011
// TODO: reinforcement

crates/games/src/chomp/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::Error;
66
use array2d::Array2D;
77
use clap::Args;
88
use game_solver::{
9-
game::{Game, GameState, StateType},
9+
game::{Game, GameState, Normal, NormalImpartial},
1010
player::ImpartialPlayer,
1111
};
1212
use serde::{Deserialize, Serialize};
@@ -77,14 +77,15 @@ pub enum ChompMoveError {
7777

7878
pub type ChompMove = NaturalMove<2>;
7979

80+
impl Normal for Chomp {}
81+
impl NormalImpartial for Chomp {}
82+
8083
impl Game for Chomp {
8184
type Move = ChompMove;
8285
type Iter<'a> = std::vec::IntoIter<Self::Move>;
8386
type Player = ImpartialPlayer;
8487
type MoveError = ChompMoveError;
8588

86-
const STATE_TYPE: Option<StateType> = Some(StateType::Normal);
87-
8889
fn max_moves(&self) -> Option<usize> {
8990
Some(self.width * self.height)
9091
}
@@ -124,7 +125,7 @@ impl Game for Chomp {
124125
}
125126

126127
fn state(&self) -> GameState<Self::Player> {
127-
Self::STATE_TYPE.unwrap().state(self)
128+
<Self as Normal>::state(&self)
128129
}
129130
}
130131

crates/games/src/domineering/mod.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::Error;
66
use array2d::Array2D;
77
use clap::Args;
88
use game_solver::{
9-
game::{Game, GameState, StateType},
9+
game::{Game, GameState, Normal},
1010
player::{PartizanPlayer, Player},
1111
};
1212
use serde::{Deserialize, Serialize};
@@ -113,14 +113,14 @@ impl<const WIDTH: usize, const HEIGHT: usize> Domineering<WIDTH, HEIGHT> {
113113
}
114114
}
115115

116+
impl<const WIDTH: usize, const HEIGHT: usize> Normal for Domineering<WIDTH, HEIGHT> {}
117+
116118
impl<const WIDTH: usize, const HEIGHT: usize> Game for Domineering<WIDTH, HEIGHT> {
117119
type Move = DomineeringMove;
118120
type Iter<'a> = std::vec::IntoIter<Self::Move>;
119121
type Player = PartizanPlayer;
120122
type MoveError = DomineeringMoveError;
121123

122-
const STATE_TYPE: Option<StateType> = Some(StateType::Normal);
123-
124124
fn max_moves(&self) -> Option<usize> {
125125
Some(WIDTH * HEIGHT)
126126
}
@@ -183,11 +183,7 @@ impl<const WIDTH: usize, const HEIGHT: usize> Game for Domineering<WIDTH, HEIGHT
183183
}
184184

185185
fn state(&self) -> GameState<Self::Player> {
186-
if self.possible_moves().len() == 0 {
187-
GameState::Win(self.player().next())
188-
} else {
189-
GameState::Playable
190-
}
186+
<Self as Normal>::state(&self)
191187
}
192188

193189
fn player(&self) -> Self::Player {

crates/games/src/nim/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pub mod gui;
55
use anyhow::Error;
66
use clap::Args;
77
use game_solver::{
8-
game::{Game, GameState, StateType},
8+
game::{Game, GameState, Normal, NormalImpartial},
99
player::ImpartialPlayer,
1010
};
1111
use serde::{Deserialize, Serialize};
@@ -51,6 +51,8 @@ pub enum NimMoveError {
5151
},
5252
}
5353

54+
impl Normal for Nim {}
55+
impl NormalImpartial for Nim {}
5456
impl Game for Nim {
5557
/// where Move is a tuple of the heap index and the number of objects to remove
5658
type Move = NimMove;
@@ -60,8 +62,6 @@ impl Game for Nim {
6062
type Player = ImpartialPlayer;
6163
type MoveError = NimMoveError;
6264

63-
const STATE_TYPE: Option<StateType> = Some(StateType::Normal);
64-
6565
fn max_moves(&self) -> Option<usize> {
6666
Some(self.max_moves)
6767
}
@@ -108,7 +108,7 @@ impl Game for Nim {
108108
}
109109

110110
fn state(&self) -> GameState<Self::Player> {
111-
Self::STATE_TYPE.unwrap().state(self)
111+
<Self as Normal>::state(&self)
112112
}
113113

114114
fn player(&self) -> Self::Player {

crates/games/src/order_and_chaos/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::{anyhow, Error};
66
use array2d::Array2D;
77
use clap::Args;
88
use game_solver::{
9-
game::{Game, GameState, StateType},
9+
game::{Game, GameState},
1010
player::PartizanPlayer,
1111
};
1212
use serde::{Deserialize, Serialize};
@@ -122,8 +122,6 @@ impl<
122122
type Player = PartizanPlayer;
123123
type MoveError = OrderAndChaosMoveError;
124124

125-
const STATE_TYPE: Option<StateType> = None;
126-
127125
fn max_moves(&self) -> Option<usize> {
128126
Some(WIDTH * HEIGHT)
129127
}

crates/games/src/reversi/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use anyhow::Error;
77
use array2d::Array2D;
88
use clap::Args;
99
use game_solver::{
10-
game::{Game, GameState, StateType},
10+
game::{Game, GameState},
1111
player::{PartizanPlayer, Player},
1212
};
1313
use serde::{Deserialize, Serialize};
@@ -133,8 +133,6 @@ impl Game for Reversi {
133133
type Player = PartizanPlayer;
134134
type MoveError = array2d::Error;
135135

136-
const STATE_TYPE: Option<StateType> = None;
137-
138136
fn max_moves(&self) -> Option<usize> {
139137
Some(WIDTH * HEIGHT)
140138
}

crates/games/src/sprouts/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
use anyhow::Error;
1010
use clap::Args;
1111
use game_solver::{
12-
game::{Game, StateType},
12+
game::{Game, Normal, NormalImpartial},
1313
player::ImpartialPlayer,
1414
};
1515
use itertools::Itertools;
@@ -91,15 +91,15 @@ pub enum SproutsMoveError {
9191

9292
const MAX_SPROUTS: usize = 3;
9393

94+
impl Normal for Sprouts {}
95+
impl NormalImpartial for Sprouts {}
9496
impl Game for Sprouts {
9597
type Move = SproutsMove;
9698
type Iter<'a> = std::vec::IntoIter<Self::Move>;
9799

98100
type Player = ImpartialPlayer;
99101
type MoveError = SproutsMoveError;
100102

101-
const STATE_TYPE: Option<StateType> = Some(StateType::Normal);
102-
103103
fn max_moves(&self) -> Option<usize> {
104104
// TODO: i actually want to find what the proper paper is, but
105105
// https://en.wikipedia.org/wiki/Sprouts_(game)#Maximum_number_of_moves
@@ -215,7 +215,7 @@ impl Game for Sprouts {
215215
}
216216

217217
fn state(&self) -> game_solver::game::GameState<Self::Player> {
218-
Self::STATE_TYPE.unwrap().state(self)
218+
<Self as Normal>::state(&self)
219219
}
220220
}
221221

0 commit comments

Comments
 (0)