use std::collections::BTreeSet;
use thiserror::Error;
use crate::clipping::{ShapeId, ShapeKind, StructureError};
use crate::{Point, StoneId};
use super::AliveZone;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum ZoneError {
#[error("the clipping structure is corrupt: {0}")]
Structure(#[from] StructureError),
#[error("stone {stone}'s dead zone names shape {shape:?}, which is not in the graph")]
MissingDeadZone {
stone: StoneId,
shape: ShapeId,
},
#[error("stone {stone}'s dead zone names shape {shape:?}, which is a board edge")]
NotADeadZone {
stone: StoneId,
shape: ShapeId,
},
#[error("shape {shape:?} is named as the dead zone of more than one stone")]
SharedDeadZone {
shape: ShapeId,
},
#[error("shape {shape:?} is a dead zone that no stone names")]
UnnamedDeadZone {
shape: ShapeId,
},
#[error("the temporary circle names shape {shape:?}, which is not a dead zone in the graph")]
StrandedTempCircle {
shape: ShapeId,
},
#[error("the forced eye at {point:?} is filed under another point's key")]
MisfiledForcedEye {
point: Point,
},
}
impl AliveZone {
pub fn validate(&self) -> Result<(), ZoneError> {
self.graph.validate()?;
let mut named: BTreeSet<ShapeId> = BTreeSet::new();
for shape in &self.temp_circles {
let shape = *shape;
let is_dead_zone = self
.graph
.shape(shape)
.is_some_and(|entry| matches!(entry.kind(), ShapeKind::DeadZone(_)));
if !is_dead_zone {
return Err(ZoneError::StrandedTempCircle { shape });
}
named.insert(shape);
}
for (stone, shape) in &self.dead_zones {
let (stone, shape) = (*stone, *shape);
let Some(entry) = self.graph.shape(shape) else {
return Err(ZoneError::MissingDeadZone { stone, shape });
};
if !matches!(entry.kind(), ShapeKind::DeadZone(_)) {
return Err(ZoneError::NotADeadZone { stone, shape });
}
if !named.insert(shape) {
return Err(ZoneError::SharedDeadZone { shape });
}
}
for (id, shape) in self.graph.shapes() {
if matches!(shape.kind(), ShapeKind::DeadZone(_)) && !named.contains(&id) {
return Err(ZoneError::UnnamedDeadZone { shape: id });
}
}
for (key, point) in self.forced_eyes.entries() {
if point.key() != key {
return Err(ZoneError::MisfiledForcedEye { point });
}
}
Ok(())
}
pub(super) fn debug_validate(&self) {
if cfg!(debug_assertions) {
if let Err(error) = self.validate() {
panic!("the alive zone is corrupt: {error}");
}
}
}
#[cfg(test)]
pub(crate) fn fingerprint(&self) -> Vec<(crate::PointKey, Vec<(u64, bool)>)> {
let mut shapes: Vec<(crate::PointKey, Vec<(u64, bool)>)> = self
.graph
.shape_ids()
.filter_map(|shape| {
let kind = self.graph.shape(shape)?.kind();
let mut segments: Vec<(u64, bool)> = self
.graph
.node_ids(shape)
.into_iter()
.filter_map(|id| {
let start = self.graph.segment(id)?.start().to_bits();
Some((start, self.graph.is_active(id)))
})
.collect();
segments.sort_unstable();
Some((kind.offset_to_point(0.0).key(), segments))
})
.collect();
shapes.sort_unstable();
shapes
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::ZoneError;
use crate::alive_zone::AliveZone;
use crate::clipping::{ShapeId, StructureError};
use crate::{Point, PointKey, StoneId};
const BOARD: f64 = 20.0;
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn dangling_shape(zone: &mut AliveZone) -> ShapeId {
let ghost = zone.graph.add_dead_zone(p(15.0, 15.0));
let _ = zone.graph.remove_shape(ghost);
ghost
}
fn populated() -> AliveZone {
let mut zone = AliveZone::new(BOARD);
zone.remove_circle(StoneId::new(0), p(2.0, 10.0)).unwrap();
zone.remove_circle(StoneId::new(1), p(3.0, 11.5)).unwrap();
zone.add_forced_eye(p(9.0, 9.0));
assert_eq!(zone.validate(), Ok(()));
zone
}
#[test]
fn a_healthy_zone_validates() {
assert_eq!(populated().validate(), Ok(()));
}
#[test]
fn corruption_underneath_surfaces_through_the_zone() {
let error = ZoneError::from(StructureError::EmptyIndexEntry);
assert!(matches!(error, ZoneError::Structure(_)));
assert!(error.to_string().starts_with("the clipping structure is"));
}
#[test]
fn a_dead_zone_that_has_gone_is_caught() {
let mut zone = populated();
let ghost = dangling_shape(&mut zone);
zone.dead_zones.insert(StoneId::new(9), ghost);
assert!(matches!(
zone.validate(),
Err(ZoneError::MissingDeadZone { .. })
));
}
#[test]
fn a_stone_naming_a_board_edge_is_caught() {
let mut zone = populated();
let edge = zone.graph.shape_ids().next().unwrap();
zone.dead_zones.insert(StoneId::new(9), edge);
assert!(matches!(
zone.validate(),
Err(ZoneError::NotADeadZone { .. })
));
}
#[test]
fn two_stones_naming_one_dead_zone_is_caught() {
let mut zone = populated();
let shared = *zone.dead_zones.get(&StoneId::new(0)).unwrap();
zone.dead_zones.insert(StoneId::new(9), shared);
assert!(matches!(
zone.validate(),
Err(ZoneError::SharedDeadZone { .. })
));
}
#[test]
fn a_dead_zone_no_stone_names_is_caught() {
let mut zone = populated();
zone.dead_zones.remove(&StoneId::new(0));
assert!(matches!(
zone.validate(),
Err(ZoneError::UnnamedDeadZone { .. })
));
}
#[test]
fn a_live_temporary_circle_is_named_by_the_zone_itself() {
let mut zone = populated();
zone.with_temp_circle(p(9.0, 4.0), |zone| {
assert_eq!(zone.validate(), Ok(()));
assert_eq!(zone.temp_circles.len(), 1);
});
assert!(zone.temp_circles.is_empty());
assert_eq!(zone.validate(), Ok(()));
}
#[test]
fn a_temporary_circle_naming_nothing_is_caught() {
let mut zone = populated();
let ghost = dangling_shape(&mut zone);
zone.temp_circles.push(ghost);
assert!(matches!(
zone.validate(),
Err(ZoneError::StrandedTempCircle { .. })
));
}
#[test]
fn a_forced_eye_under_the_wrong_key_is_caught() {
let mut zone = populated();
zone.forced_eyes
.by_point
.insert(PointKey::new(1.0, 1.0), p(4.0, 4.0));
assert!(matches!(
zone.validate(),
Err(ZoneError::MisfiledForcedEye { .. })
));
}
#[test]
fn a_panic_out_of_a_compound_operation_puts_the_structure_check_back() {
let mut zone = populated();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
zone.compound(|_| panic!("something went wrong mid-carve"));
}));
assert!(unwound.is_err());
assert!(!zone.graph.validation_is_deferred());
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "the alive zone is corrupt")]
fn a_mutation_on_a_corrupt_zone_panics() {
let mut zone = populated();
let ghost = dangling_shape(&mut zone);
zone.dead_zones.insert(StoneId::new(9), ghost);
zone.add_forced_eye(p(1.5, 1.5));
}
}