voronoi-go 1.0.1

Core rules and engine for Voronoi Go.
Documentation
//! The unit of board change.

use crate::{Stone, StoneId};

/// One turn's change to the board: a stone placed (or not, for a pass) and the
/// stones that placement captured.
///
/// A delta is the whole record of a turn, and it is what history is made of —
/// replaying every delta of a game into a fresh board of the same size
/// reproduces the position exactly.
///
/// It is immutable from the engine's point of view: applying one never writes
/// back into it. Whatever a rollback needs to remember beyond the delta itself
/// is the engine's bookkeeping, not the caller's.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct GameDelta {
    /// The stone placed this turn, or `None` when the turn was a pass.
    pub new_stone: Option<Stone>,
    /// The stones this placement captured, ascending by id. Always empty for a
    /// pass — passing never captures.
    pub captured_stone_ids: Vec<StoneId>,
}

impl GameDelta {
    /// A turn that placed `stone` and captured nothing.
    #[must_use]
    pub const fn placement(stone: Stone) -> Self {
        Self {
            new_stone: Some(stone),
            captured_stone_ids: Vec::new(),
        }
    }

    /// A turn that placed `stone` and captured the listed stones.
    #[must_use]
    pub fn capture(stone: Stone, captured_stone_ids: Vec<StoneId>) -> Self {
        Self {
            new_stone: Some(stone),
            captured_stone_ids,
        }
    }

    /// A pass.
    #[must_use]
    pub const fn pass() -> Self {
        Self {
            new_stone: None,
            captured_stone_ids: Vec::new(),
        }
    }

    /// Whether this turn was a pass.
    #[must_use]
    pub const fn is_pass(&self) -> bool {
        self.new_stone.is_none()
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used)]

    use super::GameDelta;
    use crate::{Color, Point, Stone, StoneId};

    fn stone() -> Stone {
        Stone::new(StoneId::new(4), Color::White, Point::new(1.5, -2.0))
    }

    #[test]
    fn a_pass_places_nothing_and_captures_nothing() {
        let delta = GameDelta::pass();
        assert!(delta.is_pass());
        assert!(delta.captured_stone_ids.is_empty());
        assert_eq!(delta, GameDelta::default());
    }

    #[test]
    fn a_placement_is_not_a_pass() {
        let delta = GameDelta::placement(stone());
        assert!(!delta.is_pass());
        assert_eq!(delta.new_stone, Some(stone()));
    }

    #[test]
    fn a_capture_carries_its_victims() {
        let delta = GameDelta::capture(stone(), vec![StoneId::new(1), StoneId::new(2)]);
        assert_eq!(delta.captured_stone_ids, [StoneId::new(1), StoneId::new(2)]);
    }
}