mod history;
mod validate;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::OnceLock;
use thiserror::Error;
use crate::alive_zone::DeadZoneError;
use crate::connectivity::{BoardEdge, Connectivity, CutError, CutKind};
use crate::geometry::point_is_on_board;
use crate::voronoi::GroupIter;
use crate::{AliveZone, Color, GameDelta, PerColor, Point, Stone, StoneId, Voronoi};
use history::{AppliedDelta, History};
pub use history::Commit;
pub use validate::BoardError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GameStatus {
Playing,
NextPassEnds,
Ended,
}
impl GameStatus {
#[must_use]
pub const fn has_ended(self) -> bool {
matches!(self, Self::Ended)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
#[error("the game ended on two passes and takes no further turns")]
pub struct GameOverError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum DeltaError {
#[error("stone {stone} is already on the board")]
StoneExists {
stone: StoneId,
},
#[error("stone {stone} is not on the board and cannot be captured")]
NoSuchStone {
stone: StoneId,
},
#[error("stone {stone} is captured twice by the same delta")]
CapturedTwice {
stone: StoneId,
},
#[error("a delta that places no stone cannot capture one")]
PassCaptures,
#[error("stone {stone}'s centre ({}, {}) is not on the board", .position.x, .position.y)]
OffBoard {
stone: StoneId,
position: Point,
},
#[error(transparent)]
DeadZone(#[from] DeadZoneError),
#[error(transparent)]
GameOver(#[from] GameOverError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum StonePlayError {
#[error("no stone may be placed at ({}, {})", .position.x, .position.y)]
NotPlaceable {
position: Point,
},
#[error("a {color:?} stone at ({}, {}) would capture only {color:?}'s own stones", .position.x, .position.y)]
SelfCapture {
position: Point,
color: Color,
},
#[error(transparent)]
GameOver(#[from] GameOverError),
#[error(transparent)]
Delta(#[from] DeltaError),
}
#[derive(Clone, Debug, PartialEq)]
enum Resolution {
Captures(Vec<Stone>),
SelfCapture,
}
#[derive(Clone, Debug)]
pub struct Game {
board_size: f64,
turn: u32,
captures: PerColor<u32>,
status: GameStatus,
revision: u64,
stones: BTreeMap<StoneId, Stone>,
captured: BTreeMap<StoneId, Stone>,
history: History,
zone: AliveZone,
voronoi: OnceLock<Voronoi>,
stone_list: OnceLock<Vec<Stone>>,
connectivity: Connectivity,
}
impl Game {
#[must_use]
pub fn new(board_size: f64) -> Self {
Self {
board_size,
turn: 0,
captures: PerColor::new(0, 0),
status: GameStatus::Playing,
revision: 0,
stones: BTreeMap::new(),
captured: BTreeMap::new(),
history: History::default(),
zone: AliveZone::new(board_size),
voronoi: OnceLock::new(),
stone_list: OnceLock::new(),
connectivity: Connectivity::new(board_size),
}
}
pub fn replay(
board_size: f64,
deltas: impl IntoIterator<Item = GameDelta>,
) -> Result<Self, DeltaError> {
let mut game = Self::new(board_size);
for delta in deltas {
game.apply_delta(delta, Commit::Turn)?;
}
Ok(game)
}
#[must_use]
pub const fn board_size(&self) -> f64 {
self.board_size
}
#[must_use]
pub const fn turn(&self) -> u32 {
self.turn
}
#[must_use]
pub const fn pending_color(&self) -> Color {
if self.turn % 2 == 0 {
Color::Black
} else {
Color::White
}
}
#[must_use]
pub const fn next_stone_id(&self) -> StoneId {
StoneId::new(self.turn)
}
#[must_use]
pub const fn captures(&self) -> PerColor<u32> {
self.captures
}
#[must_use]
pub const fn status(&self) -> GameStatus {
self.status
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.revision
}
pub fn stones(&self) -> impl ExactSizeIterator<Item = Stone> + '_ {
self.stones.values().copied()
}
#[must_use]
pub fn stone_count(&self) -> usize {
self.stones.len()
}
#[must_use]
pub fn stone(&self, id: StoneId) -> Option<Stone> {
self.stones.get(&id).copied()
}
#[must_use]
pub fn last_placed_stone(&self) -> Option<Stone> {
self.history
.last_committed()
.and_then(|delta| delta.new_stone)
}
pub fn deltas(&self) -> impl Iterator<Item = &GameDelta> {
self.history.committed()
}
#[must_use]
pub const fn alive_zone(&self) -> &AliveZone {
&self.zone
}
#[must_use]
pub fn nearest_living_move(&self, position: Point) -> Option<Point> {
self.zone.closest_point(position)
}
#[must_use]
pub fn voronoi(&self) -> &Voronoi {
self.voronoi.get_or_init(|| {
Voronoi::new(self.board_size, stone_list(&self.stone_list, &self.stones))
})
}
pub fn groups(&self) -> GroupIter<'_> {
self.voronoi().iter()
}
pub fn pair_cuttable(&mut self, a: StoneId, b: StoneId) -> Result<CutKind, CutError> {
let first = self.stone(a).ok_or(CutError::NoSuchStone { stone: a })?;
let second = self.stone(b).ok_or(CutError::NoSuchStone { stone: b })?;
let stones = stone_list(&self.stone_list, &self.stones);
self.connectivity
.pair_cuttable(&mut self.zone, stones, first, second)
}
pub fn boundary_cuttable(
&mut self,
stone: StoneId,
edge: BoardEdge,
) -> Result<CutKind, CutError> {
let placed = self.stone(stone).ok_or(CutError::NoSuchStone { stone })?;
let stones = stone_list(&self.stone_list, &self.stones);
Ok(self
.connectivity
.boundary_cuttable(&mut self.zone, stones, placed, edge))
}
#[must_use]
pub fn territory(&self) -> PerColor<f64> {
let voronoi = self.voronoi();
PerColor::new(
voronoi.player_area(Color::Black),
voronoi.player_area(Color::White),
)
}
#[must_use]
pub fn dead_stones(&self, color: Color) -> Vec<Stone> {
self.voronoi()
.groups(color)
.iter()
.filter(|group| !self.zone.cell_is_alive(group.rings()))
.flat_map(|group| group.stones().iter().copied())
.collect()
}
pub fn try_move(&mut self, position: Point) -> Result<GameDelta, StonePlayError> {
self.ensure_playing()?;
if !self.zone.is_placeable(position) {
return Err(StonePlayError::NotPlaceable { position });
}
let stone = Stone::new(self.next_stone_id(), self.pending_color(), position);
let resolution = self.with_transient_stone(stone, Self::resolve_captures)?;
let dead = match resolution {
Resolution::SelfCapture => {
return Err(StonePlayError::SelfCapture {
position,
color: stone.color,
});
}
Resolution::Captures(dead) => dead,
};
let mut captured: Vec<StoneId> = dead.iter().map(|stone| stone.id).collect();
captured.sort_unstable();
let delta = GameDelta::capture(stone, captured);
self.apply_delta(delta.clone(), Commit::Turn)?;
Ok(delta)
}
pub fn pass(&mut self) -> Result<GameDelta, GameOverError> {
self.ensure_playing()?;
let delta = GameDelta::pass();
let applied = self.apply_delta(delta.clone(), Commit::Turn);
debug_assert!(applied.is_ok(), "a pass cannot fail to apply");
Ok(delta)
}
pub fn undo_move(&mut self) -> Option<GameDelta> {
self.pop_delta()
}
fn ensure_playing(&self) -> Result<(), GameOverError> {
match self.status {
GameStatus::Playing | GameStatus::NextPassEnds => Ok(()),
GameStatus::Ended => Err(GameOverError),
}
}
fn derived_status(&self) -> GameStatus {
let mut recent = self.history.committed().rev();
let last = recent.next().is_some_and(GameDelta::is_pass);
let previous = recent.next().is_some_and(GameDelta::is_pass);
match (previous, last) {
(true, true) => GameStatus::Ended,
(_, true) => GameStatus::NextPassEnds,
(_, false) => GameStatus::Playing,
}
}
fn with_transient_stone<T>(
&mut self,
stone: Stone,
question: impl FnOnce(&Self) -> T,
) -> Result<T, DeltaError> {
struct Rollback<'game> {
game: &'game mut Game,
}
impl Drop for Rollback<'_> {
fn drop(&mut self) {
let _ = self.game.pop_delta();
}
}
self.apply_delta(GameDelta::placement(stone), Commit::Transient)?;
let guard = Rollback { game: self };
Ok(question(&*guard.game))
}
fn resolve_captures(&self) -> Resolution {
let mover = self.pending_color();
let enemy = self.dead_stones(mover.opposite());
if !enemy.is_empty() {
return Resolution::Captures(enemy);
}
let own = self.dead_stones(mover);
if own.is_empty() {
Resolution::Captures(Vec::new())
} else {
Resolution::SelfCapture
}
}
pub fn apply_delta(&mut self, delta: GameDelta, commit: Commit) -> Result<(), DeltaError> {
self.ensure_playing()?;
self.check(&delta)?;
let mut removed_forced_eyes = Vec::new();
if let Some(stone) = delta.new_stone {
self.stones.insert(stone.id, stone);
self.zone.remove_circle(stone.id, stone.position)?;
removed_forced_eyes = self.zone.remove_forced_eyes_near_point(stone.position);
}
for id in &delta.captured_stone_ids {
let stone = self
.stones
.remove(id)
.ok_or(DeltaError::NoSuchStone { stone: *id })?;
self.captured.insert(*id, stone);
self.zone.add_forced_eye(stone.position);
self.zone.reclaim_circle(*id)?;
}
if let Some(stone) = delta.new_stone {
self.captures[stone.color] += delta.captured_stone_ids.len() as u32;
}
match commit {
Commit::Turn => self.turn += 1,
Commit::Transient => {}
}
if let Some(stone) = delta.new_stone {
self.connectivity.invalidate_near(stone.position);
}
for id in &delta.captured_stone_ids {
if let Some(captured) = self.captured.get(id) {
self.connectivity.invalidate_near(captured.position);
}
}
self.voronoi = OnceLock::new();
self.stone_list = OnceLock::new();
self.history.push(AppliedDelta {
delta,
commit,
removed_forced_eyes,
});
self.status = self.derived_status();
self.revision += 1;
self.debug_validate();
Ok(())
}
pub fn pop_delta(&mut self) -> Option<GameDelta> {
let AppliedDelta {
delta,
commit,
removed_forced_eyes,
} = self.history.pop()?;
match commit {
Commit::Turn => self.turn = self.turn.saturating_sub(1),
Commit::Transient => {}
}
self.status = self.derived_status();
for id in &delta.captured_stone_ids {
let Some(stone) = self.captured.remove(id) else {
continue;
};
self.stones.insert(*id, stone);
let carved = self.zone.remove_circle(*id, stone.position);
debug_assert!(carved.is_ok(), "a restored stone's dead zone was carved");
self.zone.remove_forced_eye(stone.position);
}
if let Some(stone) = delta.new_stone {
let count = delta.captured_stone_ids.len() as u32;
self.captures[stone.color] = self.captures[stone.color].saturating_sub(count);
self.stones.remove(&stone.id);
let reclaimed = self.zone.reclaim_circle(stone.id);
debug_assert!(reclaimed.is_ok(), "the popped stone's dead zone was carved");
}
for eye in removed_forced_eyes {
self.zone.add_forced_eye(eye);
}
if let Some(stone) = delta.new_stone {
self.connectivity.invalidate_near(stone.position);
}
for id in &delta.captured_stone_ids {
if let Some(restored) = self.stones.get(id) {
self.connectivity.invalidate_near(restored.position);
}
}
self.voronoi = OnceLock::new();
self.stone_list = OnceLock::new();
self.revision += 1;
self.debug_validate();
Some(delta)
}
fn check(&self, delta: &GameDelta) -> Result<(), DeltaError> {
match delta.new_stone {
Some(stone) if !point_is_on_board(stone.position, self.board_size) => {
return Err(DeltaError::OffBoard {
stone: stone.id,
position: stone.position,
});
}
Some(stone)
if self.stones.contains_key(&stone.id) || self.zone.has_circle(stone.id) =>
{
return Err(DeltaError::StoneExists { stone: stone.id });
}
None if !delta.captured_stone_ids.is_empty() => {
return Err(DeltaError::PassCaptures);
}
Some(_) | None => {}
}
let mut seen = BTreeSet::new();
for id in &delta.captured_stone_ids {
if !self.stones.contains_key(id) {
return Err(DeltaError::NoSuchStone { stone: *id });
}
if !seen.insert(*id) {
return Err(DeltaError::CapturedTwice { stone: *id });
}
}
Ok(())
}
}
fn stone_list<'a>(
memo: &'a OnceLock<Vec<Stone>>,
stones: &BTreeMap<StoneId, Stone>,
) -> &'a [Stone] {
memo.get_or_init(|| stones.values().copied().collect())
}
#[cfg(test)]
mod tests;