use std::collections::HashSet;
use std::fmt::{Debug, Display};
use num_complex::Complex;
use num_traits::ToPrimitive;
use super::angles::normalize_angle;
use super::grid::UnitSquareGrid;
use crate::cyclotomic::geometry::intersect_unit_segments;
use crate::cyclotomic::{IsRing, Units};
#[inline]
fn check_and_mark(seen: &mut u128, seg_id: usize) -> bool {
if seg_id >= 128 {
return true;
}
let bit = 1u128 << seg_id;
if *seen & bit != 0 {
false
} else {
*seen |= bit;
true
}
}
pub struct Turtle<T: IsRing> {
pub pos: T,
pub dir: i8,
}
impl<T: IsRing> Turtle<T> {
pub fn new(pos: T, dir: i8) -> Self {
Self { pos, dir }
}
}
impl<T: IsRing> Default for Turtle<T> {
fn default() -> Self {
Turtle {
pos: T::zero(),
dir: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Snake<T: IsRing> {
angles: Vec<i8>,
ang_sum: i64,
points: Vec<T>,
grid: UnitSquareGrid,
allow_intersections: bool,
visited: Option<HashSet<T>>,
saved_angle_0: Option<i8>,
}
impl<I: ToPrimitive, T: IsRing> TryFrom<&[I]> for Snake<T> {
type Error = &'static str;
fn try_from(angles: &[I]) -> Result<Self, Self::Error> {
let mut result = Self::new();
match result.extend_from_slice(angles) {
Ok(()) => Ok(result),
Err(_) => Err("Self-intersecting angle sequence!"),
}
}
}
impl<const N: usize, I: ToPrimitive, T: IsRing> TryFrom<&[I; N]> for Snake<T> {
type Error = &'static str;
fn try_from(angles: &[I; N]) -> Result<Self, Self::Error> {
Self::try_from(angles.as_slice())
}
}
impl<T: IsRing> Default for Snake<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: IsRing> Snake<T> {
pub fn new() -> Self {
let mut grid = UnitSquareGrid::new();
grid.add((0, 0), 0);
let visited = if T::turn() == 4 {
let mut s = HashSet::new();
s.insert(T::zero());
Some(s)
} else {
None
};
Self {
points: vec![T::zero(); 1],
angles: Vec::new(),
ang_sum: 0,
grid,
allow_intersections: false,
visited,
saved_angle_0: None,
}
}
pub fn new_unchecked() -> Self {
Self {
allow_intersections: true,
..Self::new()
}
}
pub fn from_slice_unchecked<I: ToPrimitive>(angles: &[I]) -> Self {
let mut result = Self::new_unchecked();
result.extend_from_slice(angles).unwrap(); result
}
pub fn from_slice_unsafe<I: ToPrimitive>(angles: &[I]) -> Self {
let mut result = Self::from_slice_unchecked(angles);
result.allow_intersections = false;
result
}
pub fn is_unchecked(&self) -> bool {
self.allow_intersections
}
pub fn len(&self) -> usize {
self.angles.len()
}
pub fn is_empty(&self) -> bool {
self.angles.is_empty()
}
pub fn angles(&self) -> &[i8] {
self.angles.as_slice()
}
pub fn angle_sum(&self) -> i64 {
self.ang_sum
}
pub fn representative(&self) -> &[T] {
self.points.as_slice()
}
pub fn is_closed(&self) -> bool {
!self.is_empty() && *self.points.last().unwrap() == T::zero()
}
pub fn offset(&self) -> T {
*self.points.last().unwrap()
}
pub fn direction(&self) -> i8 {
(self.ang_sum % (T::turn() as i64)) as i8
}
pub fn head(&self) -> Turtle<T> {
Turtle {
pos: self.offset(),
dir: self.direction(),
}
}
fn next_seg(&self, angle: i8) -> (T, T) {
let old_direction = self.direction();
let last = *self.points.last().unwrap();
let new_pt = last + (<T as Units>::unit(old_direction) * <T as Units>::unit(angle));
(last, new_pt)
}
fn add_unsafe(&mut self, angle: i8) {
let (_, new_pt) = self.next_seg(angle);
self.angles.push(angle);
self.ang_sum += angle as i64;
self.grid
.add(UnitSquareGrid::cell_of(new_pt), self.points.len());
self.points.push(new_pt);
if let Some(ref mut visited) = self.visited {
visited.insert(new_pt);
}
if self.is_closed() {
debug_assert!(self.saved_angle_0.is_none());
let target = (T::turn() as i64) * self.ang_sum.signum();
let original_angle_0 = self.angles[0];
let missing = target - (self.ang_sum - original_angle_0 as i64);
self.angles[0] = missing as i8;
self.ang_sum = target;
self.saved_angle_0 = Some(original_angle_0);
}
}
pub fn to_polyline(&self, turtle: Turtle<T>) -> Vec<T> {
let mut result = Self::new();
*result.points.last_mut().unwrap() = turtle.pos;
result.ang_sum = turtle.dir as i64;
for angle in self.angles.iter() {
result.add_unsafe(*angle);
}
result.points
}
pub fn to_polyline_f64(&self, turtle: Turtle<T>) -> Vec<(f64, f64)> {
self.to_polyline(turtle)
.iter()
.map(|p| {
let Complex { re: x, im: y } = p.complex64();
(x, y)
})
.collect()
}
#[cfg(test)]
fn cell_segs(&self, cells: &[(i64, i64)]) -> Vec<(T, T)> {
let mut seg_pt_indices: Vec<(usize, usize)> = Vec::new();
for pt_idx in self.grid.get_cells(cells) {
if pt_idx != 0 {
seg_pt_indices.push((pt_idx - 1, pt_idx));
}
if pt_idx != self.len() {
seg_pt_indices.push((pt_idx, pt_idx + 1));
}
}
seg_pt_indices.sort();
seg_pt_indices.dedup();
seg_pt_indices
.iter()
.map(|(ix1, ix2)| (self.points[*ix1], self.points[*ix2]))
.collect()
}
fn can_add(&self, angle: i8) -> Option<usize> {
if self.allow_intersections {
return None;
}
if self.is_closed() {
return Some(0);
}
let new_seg @ (prev_pt, new_pt) = self.next_seg(angle);
if let Some(ref visited) = self.visited {
if new_pt.is_zero() || !visited.contains(&new_pt) {
return None;
}
for i in 1..self.points.len() {
if self.points[i] == new_pt {
return Some(i);
}
}
unreachable!("visited contains new_pt but not found in points");
}
let new_pt_nz = !new_pt.is_zero();
let len = self.len();
let mut seen_segs: u128 = 0;
let mut conflict: Option<usize> = None;
let mut record = |idx: usize| {
conflict = Some(conflict.map_or(idx, |c| c.max(idx)));
};
for cell in UnitSquareGrid::edge_neighborhood_of(prev_pt, new_pt) {
for &pt_idx in self.grid.get(cell) {
if new_pt_nz && self.points[pt_idx] == new_pt {
record(pt_idx);
}
if pt_idx > 0 {
let seg_id = pt_idx - 1;
if check_and_mark(&mut seen_segs, seg_id) {
let s = (self.points[seg_id], self.points[pt_idx]);
if intersect_unit_segments(&new_seg, &s) {
record(seg_id);
}
}
}
if pt_idx < len {
let seg_id = pt_idx;
if check_and_mark(&mut seen_segs, seg_id) {
let s = (self.points[pt_idx], self.points[pt_idx + 1]);
if intersect_unit_segments(&new_seg, &s) {
record(seg_id);
}
}
}
}
}
conflict
}
pub fn add_diagnosed(&mut self, angle: i8) -> Option<usize> {
assert!(!self.is_closed(), "add_diagnosed: snake is already closed");
let a = normalize_angle::<T>(angle);
assert!(a.abs() != T::hturn());
let conflict = self.can_add(a);
if conflict.is_none() {
self.add_unsafe(a);
}
conflict
}
pub fn add(&mut self, angle: i8) -> bool {
if self.is_closed() {
return false;
}
self.add_diagnosed(angle).is_none()
}
pub fn pop(&mut self) -> Option<i8> {
let popped_angle = self.angles.pop()?;
let popped_point = self.points.pop().expect("points and angles in lockstep");
self.grid
.remove(UnitSquareGrid::cell_of(popped_point), self.points.len());
if let Some(visited) = self.visited.as_mut()
&& Some(&popped_point) != self.points.first()
{
visited.remove(&popped_point);
}
if let Some(original_angle_0) = self.saved_angle_0.take() {
if !self.angles.is_empty() {
self.angles[0] = original_angle_0;
}
self.ang_sum = self.angles.iter().map(|&a| a as i64).sum();
} else {
self.ang_sum -= popped_angle as i64;
}
Some(popped_angle)
}
pub fn extend_from_slice<I: ToPrimitive>(&mut self, angles: &[I]) -> Result<(), &str> {
for angle in angles {
if !self.add(angle.to_i8().unwrap()) {
return Err("Cannot extend, self-intersecting sequence!");
}
}
Ok(())
}
pub fn extend(&mut self, other: &Snake<T>) -> Result<(), &str> {
self.extend_from_slice(other.angles.as_slice())
}
pub fn concat(&self, other: &Snake<T>) -> Result<Self, &str> {
let errmsg = "Cannot concatenate, self-intersecting sequence!";
let mut result = Self::new();
match result.extend(self) {
Ok(()) => {}
Err(_) => return Err(errmsg),
}
match result.extend(other) {
Ok(()) => {}
Err(_) => return Err(errmsg),
}
Ok(result)
}
pub fn is_point_inside(&self) -> bool {
if !self.is_closed() {
return false;
}
true
}
}
impl<T: IsRing> PartialEq for Snake<T> {
fn eq(&self, other: &Self) -> bool {
self.angles == other.angles
}
}
impl<T: IsRing> Eq for Snake<T> {}
impl<T: IsRing> PartialOrd for Snake<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T: IsRing> Ord for Snake<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.angles.cmp(&other.angles)
}
}
impl<T: IsRing> Display for Snake<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.angles.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::{Ccw, SymNum, ZZ4, ZZ12};
use num_traits::{One, Zero};
#[test]
fn zz12_t_touch_rejected_at_edge_9() {
let mut s: Snake<ZZ12> = Snake::new();
let prefix: &[i8] = &[0, 5, -2, 5, -1, 5, -5, 4, 1, 1, 1, 5, -5, 5];
for (i, &a) in prefix.iter().enumerate() {
assert!(s.add(a), "ZZ12 prefix add #{i} (angle {a}) was rejected",);
}
assert_eq!(s.len(), prefix.len());
let conflict = s.add_diagnosed(3);
assert_eq!(
conflict,
Some(9),
"expected T-touch rejection at edge 9 (new endpoint = P9 + (√3-1)·ζ)"
);
}
#[test]
#[should_panic]
fn test_add_invalid_angle() {
let mut s: Snake<ZZ12> = Snake::new();
s.add(6);
}
#[test]
#[should_panic]
fn test_add_invalid_angle_neg() {
let mut s: Snake<ZZ12> = Snake::new();
s.add(-6);
}
#[test]
#[should_panic]
fn test_add_invalid_angle_mod() {
let mut s: Snake<ZZ12> = Snake::new();
s.add(18);
}
#[test]
fn test_basic() {
let mut s: Snake<ZZ12> = Snake::new();
assert!(s.is_empty());
assert!(!s.is_closed());
assert_eq!(s.len(), 0);
assert_eq!(s.angle_sum(), 0);
assert_eq!(s.direction(), 0);
assert_eq!(s.offset(), ZZ12::zero());
s.add(-9); assert!(!s.is_empty());
assert!(!s.is_closed());
assert_eq!(*s.angles().last().unwrap(), 3);
assert_eq!(s.direction(), 3);
assert_eq!(s.offset(), <ZZ12 as Units>::unit(3));
s.add(2);
s.add(2);
s.add(5);
assert_eq!(s.len(), 4);
let mut s2: Snake<ZZ12> = Snake::new();
s2.add(-2);
s2.add(0);
s2.add(5);
assert_eq!(s2.len(), 3);
let s3 = s.concat(&s2).unwrap();
s.extend(&s2).unwrap();
assert_eq!(s, s3);
assert_eq!(s.len(), 7);
assert_eq!(s.angle_sum(), 15);
assert_eq!(s.direction(), 3);
assert_eq!(s.head().dir, s.direction());
assert_eq!(s.head().pos, s.offset());
let l_exp: Vec<ZZ12> = s.representative().to_vec();
let l = s.to_polyline(Turtle::default());
assert_eq!(l, l_exp);
let t = Turtle {
pos: ZZ12::ccw().scale(7),
dir: -2,
};
let l2_exp: Vec<ZZ12> = l_exp
.iter()
.map(|pt| (*pt * <ZZ12 as Units>::unit(t.dir)) + t.pos)
.collect();
let l2 = s.to_polyline(t);
assert_eq!(l2, l2_exp);
}
#[test]
fn test_closed_snake() {
let sq1: Snake<ZZ12> = Snake::try_from(&[0, 3, 3, 3]).unwrap();
assert!(sq1.is_closed());
assert_eq!(sq1.angle_sum(), ZZ12::turn() as i64);
let sq2: Snake<ZZ12> = Snake::try_from(&[0, -3, -3, -3]).unwrap();
assert!(sq2.is_closed());
assert_eq!(sq2.angle_sum(), -ZZ12::turn() as i64);
let mut square: Snake<ZZ12> = Snake::new();
assert!(!square.is_closed());
for _ in 0..3 {
square.add(3);
}
assert!(!square.is_closed());
square.add(3);
assert!(square.is_closed()); }
#[test]
fn test_cell_segs() {
let mut s: Snake<ZZ12> = Snake::new();
assert!(s.cell_segs(&[(0, 0)]).is_empty());
s.add(0);
assert_eq!(s.cell_segs(&[(0, 0)]), &[(ZZ12::zero(), ZZ12::one())]);
s.add(0);
assert_eq!(s.cell_segs(&[(0, 0)]), &[(ZZ12::zero(), ZZ12::one())]);
assert_eq!(
s.cell_segs(&[(1, 0)]),
&[
(ZZ12::zero(), ZZ12::one()),
(ZZ12::one(), ZZ12::one().scale(2))
]
);
assert_eq!(
s.cell_segs(&[(2, 0)]),
&[(ZZ12::one(), ZZ12::one().scale(2))]
);
}
#[test]
fn test_add_diagnosed_crossing() {
let mut s3: Snake<ZZ12> = Snake::try_from(&[0, 4]).unwrap();
assert_eq!(s3.add_diagnosed(5), Some(0), "should cross edge 0");
assert!(!s3.add(5));
}
#[test]
fn test_add_diagnosed_vertex_revisit() {
let mut s: Snake<ZZ4> = Snake::try_from(&[0, 0, 1, 1]).unwrap();
assert_eq!(
s.add_diagnosed(1),
Some(1),
"should revisit vertex 1 (edge 1)"
);
}
#[test]
fn test_add_diagnosed_latest_conflict_brute() {
fn brute_conflicts<T: IsRing>(s: &Snake<T>, angle: i8) -> Vec<usize> {
let new_seg @ (_prev, new_pt) = s.next_seg(angle);
let new_pt_nz = !new_pt.is_zero();
let mut conflicts: Vec<usize> = Vec::new();
for i in 1..s.points.len() {
if new_pt_nz && s.points[i] == new_pt {
conflicts.push(i);
}
}
for seg_id in 0..s.len() {
let seg = (s.points[seg_id], s.points[seg_id + 1]);
if intersect_unit_segments(&new_seg, &seg) {
conflicts.push(seg_id);
}
}
conflicts
}
fn check<T: IsRing>(label: &str, prefix: &[i8]) {
let s: Snake<T> = Snake::try_from(prefix).expect("valid prefix");
for direction in (-T::hturn() + 1)..T::hturn() {
let expected_max = brute_conflicts(&s, direction).into_iter().max();
let got = s.clone().add_diagnosed(direction);
assert_eq!(
got, expected_max,
"{label}: prefix={prefix:?} angle={direction}: brute={expected_max:?}, got={got:?}"
);
}
}
check::<ZZ4>("zz4 L-walk", &[0, 0, 1, 1]);
check::<ZZ12>("zz12 zig", &[0, 5]);
check::<ZZ12>("zz12 longer zig", &[0, 5, -5, 5]);
check::<ZZ12>("zz12 spiral", &[0, 1, 1, 1, 1, 1]);
}
#[test]
#[should_panic(expected = "add_diagnosed: snake is already closed")]
fn test_add_diagnosed_panics_on_closed() {
let mut s: Snake<ZZ12> = Snake::try_from(&[3, 3, 3]).unwrap();
assert!(s.add(3)); s.add_diagnosed(0); }
#[test]
fn test_can_add() {
let mut s: Snake<ZZ12> = Snake::try_from(&[3, 3, 3]).unwrap();
assert!(s.add(3)); assert!(!s.add(0));
let mut s2: Snake<ZZ12> = Snake::try_from(&[0, 3, 3, 3]).unwrap();
assert!(!s2.add(3));
let mut s3: Snake<ZZ12> = Snake::try_from(&[0, 4]).unwrap();
assert!(!s3.add(5));
let mut s4: Snake<ZZ12> = Snake::new();
assert!(s4.add(0));
assert!(s4.add(5));
assert!(!s4.add(4));
assert!(s4.add(3));
assert!(!s4.add(5));
assert!(s4.add(4));
assert!(!s4.add(2));
assert!(s4.add(1));
assert!(!s4.add(5));
assert!(s4.add(4));
s4.extend(&Snake::try_from(&[1, 1, 3, 5]).unwrap()).unwrap();
assert_eq!(s4.len(), 10);
for i in (-ZZ12::hturn() + 1)..(ZZ12::hturn() - 1) {
assert!(!s4.add(i));
}
}
#[test]
fn test_eq_ord() {
let s1: Snake<ZZ12> = Snake::try_from(&[1, 2, 3]).unwrap();
let mut s2: Snake<ZZ12> = Snake::try_from(s1.angles.as_slice()).unwrap();
assert_eq!(s1, s2);
s2.add(4);
assert!(s1 != s2);
assert!(s1 < s2);
let s3: Snake<ZZ12> = Snake::try_from(&[-1, 4, 3]).unwrap();
assert!(s3 < s1);
assert!(s3 < s2);
let s4: Snake<ZZ12> = Snake::new();
assert!(s4 < s1);
assert!(s4 < s2);
assert!(s4 < s3);
assert!(vec![1, 2] < vec![1, 2, -1]);
assert!(vec![-1, 2, 3] < vec![1, 2]);
assert!(vec![-1, 2] < vec![1, 2, 3]);
}
#[test]
fn test_display() {
let mut s: Snake<ZZ12> = Snake::new();
for i in -2..2 {
s.add(i);
}
s.add(7);
assert_eq!(format!("{s}"), "[-2, -1, 0, 1, -5]");
}
#[test]
fn test_zz4_visited_fast_path() {
let mut s: Snake<ZZ4> = Snake::new();
assert!(s.visited.is_some());
assert!(s.add(0));
assert!(s.add(0));
assert!(s.add(1));
assert!(s.add(1));
assert!(!s.add(1));
}
#[test]
fn test_zz4_visited_matches_grid() {
let mut s_grid: Snake<ZZ12> = Snake::new();
let mut s_fast: Snake<ZZ4> = Snake::new();
let zz4_angles: &[i8] = &[0, 1, 1, 1];
for &a in zz4_angles {
assert_eq!(s_grid.add(a * 3), s_fast.add(a), "angle {a}");
}
assert!(s_fast.is_closed());
assert!(s_grid.is_closed());
}
fn snapshot<T: IsRing>(s: &Snake<T>) -> (Vec<i8>, i64, bool, T, i8) {
(
s.angles().to_vec(),
s.angle_sum(),
s.is_closed(),
s.offset(),
s.direction(),
)
}
#[test]
fn test_pop_on_empty_snake_returns_none() {
let mut s: Snake<ZZ12> = Snake::new();
assert!(s.pop().is_none());
assert!(s.is_empty());
}
#[test]
fn test_add_then_pop_round_trips_open_snake() {
let mut s: Snake<ZZ12> = Snake::new();
for a in [2i8, 2, 2, 2, 2] {
assert!(s.add(a));
}
let before = snapshot(&s);
assert!(s.add(2)); assert!(s.is_closed());
let popped = s.pop();
assert_eq!(popped, Some(2));
assert_eq!(snapshot(&s), before);
assert!(!s.is_closed());
}
#[test]
fn test_pop_undoes_retro_close_fixup() {
let mut s: Snake<ZZ12> = Snake::new();
for a in [2i8, 2, 2, 2, 2] {
assert!(s.add(a));
}
let pre_close = snapshot(&s);
assert!(!s.is_closed());
assert!(s.add(2)); assert!(s.is_closed());
assert_eq!(s.angle_sum().unsigned_abs(), 12);
let popped = s.pop();
assert_eq!(popped, Some(2));
assert_eq!(snapshot(&s), pre_close);
assert!(!s.is_closed());
assert_eq!(s.angles().len(), 5);
}
#[test]
fn test_repeated_add_pop_each_direction_is_idempotent() {
let mut s: Snake<ZZ12> = Snake::new();
for a in [2i8, 2, 2] {
assert!(s.add(a));
}
let baseline = snapshot(&s);
for direction in -5i8..=5 {
let pre = snapshot(&s);
if s.add(direction) {
let _ = s.pop();
}
assert_eq!(snapshot(&s), pre, "direction {direction}: state diverged");
}
assert_eq!(snapshot(&s), baseline);
}
#[test]
fn test_add_pop_visited_set_preserves_start() {
let mut s: Snake<ZZ4> = Snake::new();
for a in [1i8, 1, 1] {
assert!(s.add(a));
}
assert!(s.add(1));
assert!(s.is_closed());
let _ = s.pop();
assert!(!s.is_closed());
assert!(s.add(1), "re-adding closing step after pop must work");
assert!(s.is_closed());
}
#[test]
fn test_pop_decrements_grid_so_can_add_works_again() {
let mut s: Snake<ZZ12> = Snake::new();
assert!(s.add(3));
assert!(s.add(3));
let pre_grid_state = s.points.len();
assert!(s.pop().is_some());
assert_eq!(s.points.len(), pre_grid_state - 1);
assert!(s.add(3));
}
}