mod cut;
use std::collections::BTreeMap;
use thiserror::Error;
use crate::{AliveZone, Point, Stone, StoneId};
use cut::{Pair, evaluate_pair_cut};
pub use cut::{
CutKind, MAX_BOUNDARY_CUT_DISTANCE, MAX_PAIR_CUT_DISTANCE, SAFE_BOUNDARY_DISTANCE,
SAFE_DISTANCE_EPSILON, SAFE_PAIR_DISTANCE,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum CutError {
#[error("no stone {stone} is on the board")]
NoSuchStone {
stone: StoneId,
},
#[error("stone {stone} cannot be cut from itself")]
SameStone {
stone: StoneId,
},
#[error("stones {a} and {b} are not the same colour")]
DifferentColors {
a: StoneId,
b: StoneId,
},
}
const CUT_CACHE_INVALIDATE_RADIUS: f64 = 2.01 * crate::STONE_DIAMETER;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum BoardEdge {
Left,
Right,
Top,
Bottom,
}
impl BoardEdge {
pub const ALL: [Self; 4] = [Self::Left, Self::Right, Self::Top, Self::Bottom];
#[must_use]
pub fn foot(self, position: Point, board_size: f64) -> Point {
match self {
Self::Left => Point::new(0.0, position.y),
Self::Right => Point::new(board_size, position.y),
Self::Top => Point::new(position.x, 0.0),
Self::Bottom => Point::new(position.x, board_size),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CutKey {
Pair {
low: StoneId,
high: StoneId,
},
Edge {
stone: StoneId,
edge: BoardEdge,
},
}
impl CutKey {
fn pair(one: StoneId, other: StoneId) -> Self {
Self::Pair {
low: one.min(other),
high: one.max(other),
}
}
}
#[derive(Clone, Copy, Debug)]
struct CutStatus {
start: Point,
end: Point,
kind: CutKind,
}
impl CutStatus {
#[allow(clippy::manual_midpoint)]
fn midpoint(self) -> Point {
Point::new(
(self.start.x + self.end.x) / 2.0,
(self.start.y + self.end.y) / 2.0,
)
}
}
#[derive(Clone, Copy, Debug)]
struct Candidate {
start: Point,
end: Point,
key: CutKey,
pair: Pair,
}
#[derive(Clone, Debug)]
pub struct Connectivity {
board_size: f64,
cache: BTreeMap<CutKey, CutStatus>,
}
impl Connectivity {
#[must_use]
pub const fn new(board_size: f64) -> Self {
Self {
board_size,
cache: BTreeMap::new(),
}
}
#[must_use]
pub const fn board_size(&self) -> f64 {
self.board_size
}
#[must_use]
pub fn cached_count(&self) -> usize {
self.cache.len()
}
pub fn invalidate_near(&mut self, position: Point) {
self.cache
.retain(|_, status| position.distance(status.midpoint()) > CUT_CACHE_INVALIDATE_RADIUS);
}
pub fn pair_cuttable(
&mut self,
zone: &mut AliveZone,
stones: &[Stone],
a: Stone,
b: Stone,
) -> Result<CutKind, CutError> {
if a.id == b.id {
return Err(CutError::SameStone { stone: a.id });
}
if a.color != b.color {
return Err(CutError::DifferentColors { a: a.id, b: b.id });
}
if a.position.distance(b.position) > MAX_PAIR_CUT_DISTANCE {
return Ok(CutKind::TooFar);
}
Ok(self.cut_kind(zone, stones, &pair_candidate(a, b)))
}
pub fn boundary_cuttable(
&mut self,
zone: &mut AliveZone,
stones: &[Stone],
stone: Stone,
edge: BoardEdge,
) -> CutKind {
let candidate = self.edge_candidate(stone, edge);
if stone.position.distance(candidate.end) > MAX_BOUNDARY_CUT_DISTANCE {
return CutKind::TooFar;
}
self.cut_kind(zone, stones, &candidate)
}
fn edge_candidate(&self, stone: Stone, edge: BoardEdge) -> Candidate {
let foot = edge.foot(stone.position, self.board_size);
let mirror = Point::new(
2.0 * foot.x - stone.position.x,
2.0 * foot.y - stone.position.y,
);
Candidate {
start: stone.position,
end: foot,
key: CutKey::Edge {
stone: stone.id,
edge,
},
pair: Pair {
a: stone,
b: mirror,
b_id: None,
},
}
}
fn cut_kind(
&mut self,
zone: &mut AliveZone,
stones: &[Stone],
candidate: &Candidate,
) -> CutKind {
if let Some(status) = self.cache.get(&candidate.key) {
return status.kind;
}
let kind = evaluate_pair_cut(zone, self.board_size, stones, candidate.pair);
self.cache.insert(
candidate.key,
CutStatus {
start: candidate.start,
end: candidate.end,
kind,
},
);
kind
}
}
fn pair_candidate(a: Stone, b: Stone) -> Candidate {
Candidate {
start: a.position,
end: b.position,
key: CutKey::pair(a.id, b.id),
pair: Pair {
a,
b: b.position,
b_id: Some(b.id),
},
}
}
#[cfg(test)]
mod tests;