voronoi-go 1.0.1

Core rules and engine for Voronoi Go.
Documentation
//! What the board remembers about a change it has already made.
//!
//! A [`GameDelta`] is the caller's record of a turn and stays exactly as the
//! caller wrote it. Undoing one needs two things the delta deliberately does not
//! carry:
//!
//! - **Whether the turn was committed.** The wire shape of a delta is the stone
//!   and its captures, and nothing else; whether applying it advances the turn
//!   is the caller's intent at application time, so it arrives as a [`Commit`]
//!   argument and is remembered here.
//! - **Which forced eyes it consumed.** A placement swallows every forced eye
//!   its dead zone covers, and restoring them is the only way a pop can be the
//!   exact inverse of the apply. That journal is the engine's, not the
//!   caller's.
//!
//! Both live in [`AppliedDelta`], which is what history is actually made of.

use crate::{GameDelta, Point};

/// Whether applying a delta commits a turn.
///
/// A committed delta is a turn of the game: it advances the turn counter and
/// appears in [`Game::deltas`](super::Game::deltas), which is the replay log.
/// A transient one changes the board without committing anything — capture
/// resolution places the stone this way to ask what died, and takes it straight
/// back off.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Commit {
    /// The change is a turn of the game.
    Turn,
    /// The change is a hypothetical: the board moves, the game does not.
    Transient,
}

/// A delta as the board applied it, with everything undoing it needs.
#[derive(Clone, Debug, PartialEq)]
pub(super) struct AppliedDelta {
    /// The caller's delta, untouched.
    pub delta: GameDelta,
    /// Whether it committed a turn.
    pub commit: Commit,
    /// The forced eyes the placement consumed, to be put back on a pop.
    pub removed_forced_eyes: Vec<Point>,
}

/// Every delta the board has applied and not yet popped, in order.
#[derive(Clone, Debug, Default)]
pub(super) struct History {
    /// Oldest first. The last entry is what a pop takes.
    entries: Vec<AppliedDelta>,
}

impl History {
    /// Records a delta as applied.
    pub fn push(&mut self, entry: AppliedDelta) {
        self.entries.push(entry);
    }

    /// Takes back the most recent delta.
    pub fn pop(&mut self) -> Option<AppliedDelta> {
        self.entries.pop()
    }

    /// The most recent committed turn, skipping past any transient change on
    /// top of it.
    pub fn last_committed(&self) -> Option<&GameDelta> {
        self.entries
            .iter()
            .rev()
            .find(|entry| entry.commit == Commit::Turn)
            .map(|entry| &entry.delta)
    }

    /// Every committed turn, oldest first — the replay log.
    ///
    /// Transient changes are left out: a hypothetical the engine placed and
    /// took back is not part of the game.
    ///
    /// Walkable from either end, because the status is a fact about the last
    /// two turns and reading it forwards would mean walking the whole game to
    /// answer a question about its tail.
    pub fn committed(&self) -> impl DoubleEndedIterator<Item = &GameDelta> {
        self.entries
            .iter()
            .filter(|entry| entry.commit == Commit::Turn)
            .map(|entry| &entry.delta)
    }

    /// How many committed turns there are.
    pub fn committed_count(&self) -> usize {
        self.committed().count()
    }

    /// Every delta, committed or not, oldest first.
    pub fn iter(&self) -> impl Iterator<Item = &AppliedDelta> {
        self.entries.iter()
    }
}