#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::{Commit, DeltaError, Game, GameOverError, GameStatus, StonePlayError};
use crate::connectivity::{BoardEdge, Connectivity, CutError, CutKind};
use crate::geometry::LineSegment;
use crate::{Color, GameDelta, Point, PointKey, Stone, StoneId};
const BOARD: f64 = 20.0;
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn play(game: &mut Game, x: f64, y: f64) -> GameDelta {
let snapped = game
.nearest_living_move(p(x, y))
.expect("the board has room");
game.try_move(snapped).expect("a legal move")
}
#[derive(Debug, PartialEq)]
struct Snapshot {
turn: u32,
stones: Vec<Stone>,
captures: (u32, u32),
status: GameStatus,
forced_eyes: Vec<Point>,
zone: Vec<(PointKey, Vec<(u64, bool)>)>,
}
impl Snapshot {
fn of(game: &Game) -> Self {
Self {
turn: game.turn(),
stones: game.stones().collect(),
captures: game.captures().into_parts(),
status: game.status(),
forced_eyes: game.alive_zone().forced_eyes().collect(),
zone: game.alive_zone().fingerprint(),
}
}
}
fn close(value: f64, expected: f64) -> bool {
(value - expected).abs() <= 1e-9 * expected.abs().max(1.0)
}
#[test]
fn an_empty_board_snaps_a_corner_probe_onto_the_inset_corner() {
let game = Game::new(BOARD);
assert_eq!(game.nearest_living_move(p(0.5, 0.5)), Some(p(1.0, 1.0)));
}
#[test]
fn a_probe_already_in_the_clear_is_its_own_answer() {
let game = Game::new(BOARD);
assert_eq!(game.nearest_living_move(p(7.25, 3.5)), Some(p(7.25, 3.5)));
}
#[test]
fn a_capture_leaves_a_forced_eye_to_snap_to() {
let game = corner_capture();
let snapped = game.nearest_living_move(p(0.5, 0.5)).unwrap();
assert!(snapped.distance(p(1.0, 1.0)) < 1e-9, "{snapped:?}");
assert!(game.alive_zone().has_forced_eye(p(1.0, 1.0)));
assert_eq!(game.validate(), Ok(()));
}
fn corner_capture() -> Game {
let mut game = Game::new(BOARD);
play(&mut game, 1.0, 1.0);
play(&mut game, 3.0, 1.0);
game.pass().unwrap();
play(&mut game, 1.0, 3.0);
game
}
#[test]
fn turns_captures_and_territory_all_follow_from_the_deltas() {
let game = corner_capture();
assert_eq!(game.turn(), 4);
assert_eq!(game.pending_color(), Color::Black);
assert_eq!(game.captures()[Color::White], 1);
assert_eq!(game.captures()[Color::Black], 0);
assert_eq!(game.status(), GameStatus::Playing);
assert_eq!(game.deltas().count(), 4);
assert_eq!(game.stone_count(), 2);
let territory = game.territory();
assert!(close(territory[Color::Black], 0.0));
assert!(close(territory[Color::White], BOARD * BOARD));
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn the_revision_moves_on_every_change_including_a_transient_one() {
let mut game = Game::new(BOARD);
let start = game.revision();
let stone = Stone::new(StoneId::new(0), Color::Black, p(5.0, 5.0));
game.apply_delta(GameDelta::placement(stone), Commit::Transient)
.unwrap();
assert_eq!(game.revision(), start + 1);
assert_eq!(game.turn(), 0, "a transient change commits no turn");
assert_eq!(game.stone_count(), 1, "but it does change the board");
game.pop_delta();
assert_eq!(game.revision(), start + 2, "and so does taking it back");
assert_eq!(game.stone_count(), 0);
}
#[test]
fn reading_the_diagram_twice_builds_it_once() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let first: *const _ = game.voronoi();
let second: *const _ = game.voronoi();
assert!(std::ptr::eq(first, second), "the memo is kept");
assert_eq!(
game.revision(),
3,
"reading it is not a change; the three are the move's own probe, its \
rollback, and the move itself"
);
play(&mut game, 12.0, 12.0);
assert_eq!(game.groups().count(), 2, "the change dropped the memo");
}
#[test]
fn a_pass_arms_the_next_one_and_an_undo_disarms_it_again() {
let mut game = Game::new(BOARD);
assert_eq!(game.status(), GameStatus::Playing);
game.pass().unwrap();
assert_eq!(game.status(), GameStatus::NextPassEnds);
play(&mut game, 5.0, 5.0);
assert_eq!(game.status(), GameStatus::Playing, "a move disarms it");
game.undo_move();
assert_eq!(game.status(), GameStatus::NextPassEnds);
assert_eq!(game.validate(), Ok(()));
game.undo_move();
assert_eq!(
game.status(),
GameStatus::Playing,
"with no turns played, nothing is armed"
);
}
#[test]
fn two_passes_in_a_row_end_the_game_and_it_then_takes_no_turns() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
game.pass().unwrap();
game.pass().unwrap();
assert_eq!(game.status(), GameStatus::Ended);
let before = Snapshot::of(&game);
let revision = game.revision();
assert_eq!(
game.pass(),
Err(GameOverError),
"a third pass is not a turn"
);
let snapped = game.nearest_living_move(p(9.0, 9.0)).unwrap();
assert_eq!(
game.try_move(snapped),
Err(StonePlayError::GameOver(GameOverError))
);
let stone = Stone::new(game.next_stone_id(), Color::Black, snapped);
for commit in [Commit::Turn, Commit::Transient] {
assert_eq!(
game.apply_delta(GameDelta::placement(stone), commit),
Err(DeltaError::GameOver(GameOverError))
);
}
assert_eq!(Snapshot::of(&game), before, "and nothing moved");
assert_eq!(game.revision(), revision);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn an_ended_game_still_answers_everything_it_answered_before() {
let mut game = corner_capture();
game.pass().unwrap();
game.pass().unwrap();
assert_eq!(game.status(), GameStatus::Ended);
let territory = game.territory();
assert!(close(territory[Color::White], BOARD * BOARD));
assert_eq!(game.stone_count(), 2);
assert_eq!(game.groups().count(), 1);
assert_eq!(game.captures()[Color::White], 1);
assert!(game.nearest_living_move(p(9.0, 9.0)).is_some());
assert_eq!(game.dead_stones(Color::White).len(), 0);
let stones: Vec<_> = game.stones().collect();
assert!(
game.pair_cuttable(stones[0].id, stones[1].id).is_ok(),
"a cut question carves a dead zone and takes it back, which an ended \
game still allows"
);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn undoing_the_second_pass_un_ends_the_game() {
let mut game = Game::new(BOARD);
game.pass().unwrap();
game.pass().unwrap();
assert!(
game.undo_move().is_some(),
"undo is allowed on an ended game"
);
assert_eq!(game.status(), GameStatus::NextPassEnds);
play(&mut game, 5.0, 5.0);
assert_eq!(game.status(), GameStatus::Playing);
assert_eq!(game.turn(), 2);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_replayed_log_that_carries_on_past_two_passes_is_refused() {
let stone = Stone::new(StoneId::new(2), Color::Black, p(5.0, 5.0));
let log = [
GameDelta::pass(),
GameDelta::pass(),
GameDelta::placement(stone),
];
assert_eq!(
Game::replay(BOARD, log).err(),
Some(DeltaError::GameOver(GameOverError)),
"a log of turns played after the game ended is not a game"
);
let game = Game::replay(BOARD, [GameDelta::pass(), GameDelta::pass()]).unwrap();
assert_eq!(game.status(), GameStatus::Ended);
assert_eq!(game.turn(), 2);
}
#[test]
fn the_delta_log_holds_the_committed_turns_and_nothing_else() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
game.pass().unwrap();
let probe = Stone::new(game.next_stone_id(), Color::White, p(9.0, 9.0));
game.apply_delta(GameDelta::placement(probe), Commit::Transient)
.unwrap();
assert_eq!(
game.deltas().count(),
2,
"the transient placement is not a turn"
);
assert_eq!(
game.last_placed_stone(),
None,
"the last committed turn was a pass"
);
game.pop_delta();
game.undo_move();
assert_eq!(
game.last_placed_stone().map(|stone| stone.position),
Some(p(5.0, 5.0))
);
}
#[test]
fn there_is_nothing_to_undo_on_an_empty_board() {
let mut game = Game::new(BOARD);
assert_eq!(game.undo_move(), None);
assert_eq!(game.turn(), 0);
}
fn surrounded_middle() -> Game {
let mut game = Game::new(BOARD);
play(&mut game, 10.0, 10.0);
play(&mut game, 7.9, 10.0);
game.pass().unwrap();
play(&mut game, 12.1, 10.0);
game.pass().unwrap();
play(&mut game, 10.0, 7.9);
game.pass().unwrap();
play(&mut game, 10.0, 12.1);
game
}
#[test]
fn a_stone_placed_near_a_forced_eye_consumes_it() {
let mut game = surrounded_middle();
assert_eq!(game.stone_count(), 4, "only the four white stones are left");
assert!(game.alive_zone().has_forced_eye(p(10.0, 10.0)));
game.pass().unwrap();
play(&mut game, 10.5, 10.5);
assert!(!game.alive_zone().has_forced_eye(p(10.0, 10.0)));
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn undoing_a_capture_takes_its_forced_eye_back_with_it() {
let mut game = surrounded_middle();
assert_eq!(game.alive_zone().forced_eyes().count(), 1);
assert_eq!(game.captures()[Color::White], 1);
game.undo_move();
assert_eq!(
game.stone_count(),
4,
"three white stones and the black one"
);
assert_eq!(game.alive_zone().forced_eyes().count(), 0);
assert_eq!(game.captures()[Color::White], 0);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_stone_played_exactly_on_one_forced_eye_leaves_the_others_alone() {
let mut game = Game::new(BOARD);
play(&mut game, 10.0, 10.0);
game.pass().unwrap();
play(&mut game, 12.0, 10.0);
for (index, (x, y)) in [
(8.0, 10.0),
(10.0, 12.0),
(10.0, 8.0),
(12.0, 12.0),
(12.0, 8.0),
(14.0, 10.0),
]
.into_iter()
.enumerate()
{
if index > 0 {
game.pass().unwrap();
}
play(&mut game, x, y);
}
assert!(game.alive_zone().has_forced_eye(p(10.0, 10.0)));
assert!(game.alive_zone().has_forced_eye(p(12.0, 10.0)));
let snapped = game.nearest_living_move(p(9.9, 10.0)).unwrap();
assert_eq!(snapped, p(10.0, 10.0));
game.try_move(snapped).unwrap();
assert_eq!(
game.alive_zone().forced_eyes().collect::<Vec<_>>(),
[p(12.0, 10.0)]
);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_surrounded_corner_stone_is_taken_and_leaves_an_eye() {
let game = corner_capture();
assert_eq!(game.stone_count(), 2);
assert!(game.stones().all(|stone| stone.color == Color::White));
assert!(game.alive_zone().has_forced_eye(p(1.0, 1.0)));
}
#[test]
fn a_surrounded_edge_stone_is_taken_too() {
let mut game = Game::new(BOARD);
game.pass().unwrap();
play(&mut game, 1.0, 5.0);
play(&mut game, 1.0, 7.0);
game.pass().unwrap();
play(&mut game, 1.0, 3.0);
game.pass().unwrap();
play(&mut game, 3.0, 5.0);
assert_eq!(game.stone_count(), 3);
assert!(game.stones().all(|stone| stone.color == Color::Black));
assert!(game.alive_zone().has_forced_eye(p(1.0, 5.0)));
assert_eq!(game.captures()[Color::Black], 1);
assert_eq!(game.captures()[Color::White], 0);
}
#[test]
fn a_self_capture_is_refused_and_changes_nothing() {
let mut game = corner_capture();
let before = Snapshot::of(&game);
let revision = game.revision();
let eye = game.nearest_living_move(p(1.0, 1.0)).unwrap();
let refused = game.try_move(eye);
assert_eq!(
refused,
Err(StonePlayError::SelfCapture {
position: p(1.0, 1.0),
color: Color::Black,
})
);
assert_eq!(Snapshot::of(&game), before);
assert_eq!(
game.revision(),
revision + 2,
"the board did change twice, and a caller watching the revision has to see it"
);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_position_no_centre_can_reach_is_refused_without_being_played() {
let mut game = Game::new(BOARD);
play(&mut game, 10.0, 10.0);
let before = Snapshot::of(&game);
assert_eq!(
game.try_move(p(10.5, 10.0)),
Err(StonePlayError::NotPlaceable {
position: p(10.5, 10.0)
})
);
assert_eq!(
game.try_move(p(0.5, 5.0)),
Err(StonePlayError::NotPlaceable {
position: p(0.5, 5.0)
})
);
assert_eq!(Snapshot::of(&game), before);
assert_eq!(
game.revision(),
3,
"a refusal is not a board change, and the move before it was three"
);
}
#[test]
fn every_position_the_zone_names_is_one_a_move_can_be_played_at() {
let board = 12.0;
let mut game = Game::new(board);
let mut played = 0;
let mut on_a_crossing = 0;
for step in 0..40_u32 {
let probe = p(
f64::from(step % 7) * board / 6.0,
f64::from(step / 7 % 7) * board / 6.0,
);
let Some(snapped) = game.nearest_living_move(probe) else {
break;
};
if !game.alive_zone().contains(snapped) {
on_a_crossing += 1;
}
match game.try_move(snapped) {
Ok(_) => played += 1,
Err(StonePlayError::SelfCapture { .. }) => {
game.pass().expect("the game is still taking turns");
if game.status().has_ended() {
break;
}
}
Err(error) => panic!("snapped position {snapped:?} rejected: {error}"),
}
}
assert!(played > 15, "only {played} positions were playable");
assert!(
on_a_crossing > 0,
"no snapped position landed on the covered side of the outline, so this \
proved nothing"
);
assert_eq!(game.validate(), Ok(()));
}
fn pack_with_black(board: f64) -> Game {
let mut game = Game::new(board);
let mut probe = 0_u32;
for _ in 0..500 {
let candidate = p(
f64::from(probe % 13) * board / 12.0,
f64::from(probe / 13 % 13) * board / 12.0,
);
probe += 7;
let Some(snapped) = game.nearest_living_move(candidate) else {
return game;
};
let stone = Stone::new(game.next_stone_id(), Color::Black, snapped);
game.apply_delta(GameDelta::placement(stone), Commit::Turn)
.unwrap();
}
panic!("the board never filled up");
}
#[test]
fn a_packed_board_captures_every_group_on_it() {
let mut game = pack_with_black(12.0);
assert!(
game.nearest_living_move(p(6.0, 6.0)).is_none(),
"the packing left room"
);
let last = game.undo_move().unwrap().new_stone.unwrap();
let survivors = game.stone_count();
assert!(survivors > 10, "only {survivors} stones fitted");
if game.pending_color() != Color::White {
game.pass().unwrap();
}
let white = Stone::new(game.next_stone_id(), Color::White, last.position);
game.apply_delta(GameDelta::placement(white), Commit::Transient)
.unwrap();
assert!(
game.alive_zone()
.closest_distance(LineSegment::new(p(1.0, 1.0), p(11.0, 11.0)))
.is_infinite()
);
assert_eq!(game.dead_stones(Color::Black).len(), survivors);
assert!(
!game.dead_stones(Color::White).is_empty(),
"the stone that filled the board is dead too, which is what makes this \
both a capture and a self-capture"
);
game.pop_delta();
let delta = game.try_move(last.position).expect("a legal move");
assert_eq!(delta.captured_stone_ids.len(), survivors);
assert_eq!(game.captures()[Color::White], survivors as u32);
assert_eq!(game.stone_count(), 1);
assert_eq!(game.dead_stones(Color::Black).len(), 0);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn undoing_a_capture_restores_the_board_bit_for_bit() {
let mut game = Game::new(BOARD);
play(&mut game, 1.0, 1.0);
play(&mut game, 3.0, 1.0);
game.pass().unwrap();
let before = Snapshot::of(&game);
let delta = play(&mut game, 1.0, 3.0);
assert_eq!(delta.captured_stone_ids, [StoneId::new(0)]);
assert_ne!(Snapshot::of(&game), before);
game.undo_move();
assert_eq!(
Snapshot::of(&game),
before,
"the alive zone's own segments have to come back identical, not merely equivalent"
);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn undoing_a_plain_move_restores_the_board_bit_for_bit() {
let mut game = Game::new(BOARD);
for (x, y) in [(6.0, 6.0), (8.5, 7.0), (7.0, 9.0), (4.0, 8.0)] {
play(&mut game, x, y);
}
let before = Snapshot::of(&game);
play(&mut game, 8.0, 10.5);
game.undo_move();
assert_eq!(Snapshot::of(&game), before);
}
#[test]
fn a_transient_placement_leaves_nothing_behind() {
let mut game = Game::new(BOARD);
play(&mut game, 6.0, 6.0);
play(&mut game, 8.5, 7.0);
let before = Snapshot::of(&game);
let probe = Stone::new(game.next_stone_id(), game.pending_color(), p(7.5, 9.0));
game.apply_delta(GameDelta::placement(probe), Commit::Transient)
.unwrap();
game.pop_delta();
assert_eq!(Snapshot::of(&game), before);
assert_eq!(game.turn(), 2, "no turn was ever committed");
}
#[test]
fn a_delta_that_captures_a_stone_that_is_not_there_is_refused() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let before = Snapshot::of(&game);
let stone = Stone::new(game.next_stone_id(), Color::White, p(9.0, 9.0));
let ghost = StoneId::new(41);
assert_eq!(
game.apply_delta(GameDelta::capture(stone, vec![ghost]), Commit::Turn),
Err(DeltaError::NoSuchStone { stone: ghost })
);
assert_eq!(
Snapshot::of(&game),
before,
"a refused delta changes nothing"
);
assert_eq!(game.revision(), 3);
}
#[test]
fn a_delta_that_reuses_a_stone_id_is_refused() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let clash = Stone::new(StoneId::new(0), Color::White, p(9.0, 9.0));
assert_eq!(
game.apply_delta(GameDelta::placement(clash), Commit::Turn),
Err(DeltaError::StoneExists {
stone: StoneId::new(0)
})
);
}
#[test]
fn a_delta_that_captures_the_same_stone_twice_is_refused() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let stone = Stone::new(game.next_stone_id(), Color::White, p(9.0, 9.0));
let victim = StoneId::new(0);
assert_eq!(
game.apply_delta(
GameDelta::capture(stone, vec![victim, victim]),
Commit::Turn
),
Err(DeltaError::CapturedTwice { stone: victim })
);
}
#[test]
fn a_pass_that_captures_is_refused() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let delta = GameDelta {
new_stone: None,
captured_stone_ids: vec![StoneId::new(0)],
};
assert_eq!(
game.apply_delta(delta, Commit::Turn),
Err(DeltaError::PassCaptures)
);
}
#[test]
fn a_delta_placing_a_stone_off_the_board_is_refused() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
let before = Snapshot::of(&game);
for position in [
p(BOARD + 1.0, 5.0),
p(-1.0, 5.0),
p(1e300, 1e300),
p(f64::NAN, 5.0),
p(5.0, f64::INFINITY),
] {
let stone = Stone::new(game.next_stone_id(), Color::White, position);
assert_eq!(
game.apply_delta(GameDelta::placement(stone), Commit::Turn),
Err(DeltaError::OffBoard {
stone: game.next_stone_id(),
position,
}),
"{position:?}"
);
}
assert_eq!(
Snapshot::of(&game),
before,
"a refused delta changes nothing"
);
assert_eq!(game.revision(), 3);
}
#[test]
fn the_board_rectangle_is_the_whole_domain_a_delta_may_use() {
let mut game = Game::new(BOARD);
let corner = Stone::new(StoneId::new(0), Color::Black, p(0.0, 0.0));
assert!(!game.alive_zone().is_placeable(p(0.0, 0.0)));
assert_eq!(
game.apply_delta(GameDelta::placement(corner), Commit::Turn),
Ok(())
);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_coordinate_that_is_not_a_number_is_not_placeable() {
let mut game = Game::new(BOARD);
play(&mut game, 10.0, 10.0);
let before = Snapshot::of(&game);
for position in [
p(f64::NAN, 5.0),
p(5.0, f64::NAN),
p(f64::NAN, f64::NAN),
p(f64::INFINITY, 5.0),
p(5.0, f64::NEG_INFINITY),
p(1e300, 1e300),
] {
assert!(!game.alive_zone().contains(position), "{position:?}");
assert!(!game.alive_zone().is_placeable(position), "{position:?}");
assert_eq!(
game.try_move(position),
Err(StonePlayError::NotPlaceable { position }),
"{position:?}"
);
}
assert_eq!(Snapshot::of(&game), before);
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_probe_that_is_not_a_number_snaps_to_nothing() {
let mut game = Game::new(BOARD);
play(&mut game, 10.0, 10.0);
assert_eq!(game.nearest_living_move(p(f64::NAN, 5.0)), None);
assert_eq!(game.nearest_living_move(p(5.0, f64::NAN)), None);
assert_eq!(game.nearest_living_move(p(f64::INFINITY, 5.0)), None);
let snapped = game.nearest_living_move(p(-50.0, -50.0));
assert_eq!(snapped, Some(p(1.0, 1.0)));
}
#[test]
fn a_board_that_is_not_a_number_holds_no_positions() {
let mut game = Game::new(f64::NAN);
assert!(!game.alive_zone().is_placeable(p(1.0, 1.0)));
assert_eq!(game.nearest_living_move(p(1.0, 1.0)), None);
assert_eq!(
game.try_move(p(1.0, 1.0)),
Err(StonePlayError::NotPlaceable {
position: p(1.0, 1.0)
})
);
}
#[test]
fn a_panic_while_a_transient_stone_is_down_leaves_the_board_as_it_was() {
let mut game = Game::new(BOARD);
play(&mut game, 5.0, 5.0);
play(&mut game, 9.0, 9.0);
let before = Snapshot::of(&game);
let revision = game.revision();
let stone = Stone::new(game.next_stone_id(), game.pending_color(), p(13.0, 13.0));
let escaped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
game.with_transient_stone(stone, |_| panic!("the question blew up"))
}));
assert!(escaped.is_err(), "the panic reached the caller");
assert_eq!(
Snapshot::of(&game),
before,
"the transient placement was taken back, down to the zone's segments"
);
assert_eq!(game.validate(), Ok(()));
assert_eq!(
game.revision(),
revision + 2,
"the revision only ever increases: one for the placement, one for the \
rollback"
);
assert!(game.try_move(p(13.0, 13.0)).is_ok());
assert_eq!(game.validate(), Ok(()));
}
#[test]
fn a_transient_question_is_answered_with_the_stone_on_the_board() {
let mut game = Game::new(BOARD);
let stone = Stone::new(game.next_stone_id(), Color::Black, p(5.0, 5.0));
let before = Snapshot::of(&game);
let seen = game
.with_transient_stone(stone, |game| (game.stone_count(), game.turn()))
.unwrap();
assert_eq!(seen, (1, 0), "on the board, but no turn committed");
assert_eq!(Snapshot::of(&game), before, "and taken straight back off");
}
#[test]
fn a_board_answers_whether_its_own_stones_can_be_cut() {
let mut game = Game::new(BOARD);
game.try_move(p(10.0, 10.0)).expect("a legal move");
game.try_move(p(16.0, 16.0)).expect("a legal move");
game.try_move(p(10.0, 12.5)).expect("a legal move");
let (black, other_black, white) = (StoneId::new(0), StoneId::new(2), StoneId::new(1));
let missing = StoneId::new(9);
assert_eq!(
game.pair_cuttable(black, other_black),
Ok(CutKind::Connected),
"a tight pair"
);
assert_eq!(
game.pair_cuttable(black, white),
Err(CutError::DifferentColors { a: black, b: white })
);
assert_eq!(
game.pair_cuttable(black, black),
Err(CutError::SameStone { stone: black })
);
assert_eq!(
game.pair_cuttable(black, missing),
Err(CutError::NoSuchStone { stone: missing })
);
assert_eq!(
game.boundary_cuttable(black, BoardEdge::Left),
Ok(CutKind::TooFar),
"ten units from the wall is nothing the rules will judge"
);
assert_eq!(
game.boundary_cuttable(missing, BoardEdge::Left),
Err(CutError::NoSuchStone { stone: missing })
);
}
#[test]
fn asking_whether_a_pair_can_be_cut_does_not_change_the_board() {
let mut game = corner_capture();
let before = Snapshot::of(&game);
let revision = game.revision();
for edge in BoardEdge::ALL {
let _ = game.boundary_cuttable(StoneId::new(0), edge);
}
let _ = game.pair_cuttable(StoneId::new(0), StoneId::new(1));
assert_eq!(Snapshot::of(&game), before);
assert_eq!(game.revision(), revision, "asking is not a change");
assert_eq!(game.validate(), Ok(()));
}
fn answers(game: &mut Game) -> Vec<CutKind> {
let ids: Vec<StoneId> = game.stones().map(|stone| stone.id).collect();
let mut all = Vec::new();
for (index, a) in ids.iter().enumerate() {
for b in ids.iter().skip(index + 1) {
all.extend(game.pair_cuttable(*a, *b));
}
}
for id in &ids {
for edge in BoardEdge::ALL {
all.extend(game.boundary_cuttable(*id, edge));
}
}
all
}
fn uncached_answers(game: &Game) -> Vec<CutKind> {
let stones: Vec<Stone> = game.stones().collect();
let mut zone = game.alive_zone().clone();
let mut connectivity = Connectivity::new(game.board_size());
let mut all = Vec::new();
for (index, a) in stones.iter().enumerate() {
for b in stones.iter().skip(index + 1) {
all.extend(connectivity.pair_cuttable(&mut zone, &stones, *a, *b));
}
}
for stone in &stones {
for edge in BoardEdge::ALL {
all.push(connectivity.boundary_cuttable(&mut zone, &stones, *stone, edge));
}
}
all
}
fn connections(game: &mut Game) -> usize {
answers(game)
.iter()
.filter(|kind| **kind == CutKind::Connected)
.count()
}
#[test]
fn no_cut_status_outlives_the_position_it_was_computed_for() {
let positions = [
(10.0, 10.0),
(13.0, 10.0),
(11.5, 8.0),
(11.5, 12.0),
(11.5, 14.5),
(11.5, 5.5),
];
let mut game = Game::new(BOARD);
let mut counts = Vec::new();
for (x, y) in positions {
play(&mut game, x, y);
game.pass().unwrap();
assert_eq!(
answers(&mut game),
uncached_answers(&game),
"after playing near ({x}, {y})"
);
counts.push(connections(&mut game));
}
let mut down = Vec::new();
while game.undo_move().is_some() {
let turn = game.turn();
assert_eq!(
answers(&mut game),
uncached_answers(&game),
"after undoing back to turn {turn}"
);
down.push(connections(&mut game));
}
assert_eq!(counts, vec![0, 0, 3, 5, 6, 7]);
assert_eq!(down, vec![7, 6, 6, 5, 5, 3, 3, 0, 0, 0, 0, 0]);
}
#[test]
fn a_transient_change_invalidates_around_itself_too() {
let mut game = Game::new(BOARD);
for (x, y) in [(10.0, 10.0), (13.0, 10.0), (11.5, 8.0)] {
play(&mut game, x, y);
game.pass().unwrap();
}
let settled = answers(&mut game);
assert_eq!(connections(&mut game), 3);
let preview = Stone::new(game.next_stone_id(), game.pending_color(), p(11.5, 12.0));
game.apply_delta(GameDelta::placement(preview), Commit::Transient)
.expect("a transient placement");
assert_eq!(answers(&mut game), uncached_answers(&game));
assert_eq!(
connections(&mut game),
5,
"the transient stone made two more connections"
);
game.pop_delta().expect("something to pop");
assert_eq!(
answers(&mut game),
settled,
"the pop dropped every status the transient stone produced"
);
}
#[test]
fn a_game_rebuilt_from_its_deltas_is_the_same_game() {
let mut played = corner_capture();
play(&mut played, 15.0, 15.0);
let replayed = Game::replay(BOARD, played.deltas().cloned()).expect("the log replays");
assert_eq!(Snapshot::of(&replayed), Snapshot::of(&played));
assert_eq!(replayed.revision(), 5, "one change per delta, and no more");
assert_eq!(replayed.validate(), Ok(()));
}