1use std::collections::{BTreeMap, BTreeSet};
10
11pub type NodeId = usize;
13
14pub type PieceId = usize;
16
17#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
31#[repr(u8)]
32pub enum Dir {
33 Up = 0,
34 RightUp,
35 RightDown,
36 Down,
37 LeftDown,
38 LeftUp,
39}
40
41impl Dir {
42 pub const ALL: [Dir; 6] = [
45 Dir::Up,
46 Dir::RightUp,
47 Dir::RightDown,
48 Dir::Down,
49 Dir::LeftDown,
50 Dir::LeftUp,
51 ];
52
53 pub fn delta(self) -> (i32, i32) {
55 match self {
56 Dir::Up => (0, -1),
57 Dir::RightUp => (1, -1),
58 Dir::RightDown => (1, 0),
59 Dir::Down => (0, 1),
60 Dir::LeftDown => (-1, 1),
61 Dir::LeftUp => (-1, 0),
62 }
63 }
64
65 pub fn opposite(self) -> Dir {
67 match self {
68 Dir::Up => Dir::Down,
69 Dir::RightUp => Dir::LeftDown,
70 Dir::RightDown => Dir::LeftUp,
71 Dir::Down => Dir::Up,
72 Dir::LeftDown => Dir::RightUp,
73 Dir::LeftUp => Dir::RightDown,
74 }
75 }
76}
77
78#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)]
84pub struct Arrows(u8);
85
86impl Arrows {
87 pub fn from_dir(dir: Dir) -> Self {
89 Self(1 << dir as u8)
90 }
91
92 pub fn all() -> Self {
94 Self(0b0011_1111)
95 }
96
97 pub fn contains(self, dir: Dir) -> bool {
99 self.0 & (1 << dir as u8) != 0
100 }
101
102 pub fn with(self, dir: Dir) -> Self {
104 Self(self.0 | (1 << dir as u8))
105 }
106
107 pub fn without(self, dir: Dir) -> Self {
109 Self(self.0 & !(1 << dir as u8))
110 }
111
112 pub fn len(self) -> usize {
114 self.0.count_ones() as usize
115 }
116
117 pub fn is_empty(self) -> bool {
119 self.0 == 0
120 }
121
122 pub(crate) fn bits(self) -> u8 {
125 self.0
126 }
127
128 pub fn iter(self) -> impl Iterator<Item = Dir> {
130 Dir::ALL.into_iter().filter(move |&dir| self.contains(dir))
131 }
132}
133
134impl IntoIterator for Arrows {
135 type Item = Dir;
136 type IntoIter = std::vec::IntoIter<Dir>;
137
138 fn into_iter(self) -> Self::IntoIter {
139 self.iter().collect::<Vec<_>>().into_iter()
140 }
141}
142
143#[derive(Clone, Copy, PartialEq, Eq, Debug)]
145pub struct Piece {
146 pub arrows: Arrows,
147}
148
149#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
151pub enum GameStatus {
152 #[default]
153 InProgress,
154 Won,
155}
156
157#[derive(Clone, Copy, Debug)]
159pub struct Node {
160 pub q: i32,
161 pub r: i32,
162 pub neighbors: [Option<NodeId>; 6],
164}
165
166#[derive(Clone, Debug)]
168pub struct Params {
169 pub nodes: Vec<(i32, i32)>,
172 pub pieces: usize,
176 pub min_arrows: usize,
178 pub max_arrows: usize,
181}
182
183#[derive(Clone, Debug)]
186pub struct HexwebGame {
187 nodes: Vec<Node>,
188 lookup: BTreeMap<(i32, i32), NodeId>,
191 pieces: Vec<Piece>,
192 cell_piece: Vec<Option<PieceId>>,
194 initial: Vec<Option<PieceId>>,
196 moves: usize,
197 status: GameStatus,
198}
199
200pub fn hexagon(radius: u32) -> Vec<(i32, i32)> {
203 let r = radius as i32;
204 let mut coords = Vec::new();
205 for q in -r..=r {
206 for rr in (-r).max(-q - r)..=r.min(-q + r) {
207 coords.push((q, rr));
208 }
209 }
210 coords
211}
212
213type Cell = (i32, i32);
215
216fn rot((q, r): Cell) -> Cell {
218 (-r, q + r)
219}
220
221#[cfg(test)]
223fn rot_inv((q, r): Cell) -> Cell {
224 (r + q, -q)
225}
226
227fn mirror_v((q, r): Cell) -> Cell {
229 (-q, r + q)
230}
231
232fn mirror_h((q, r): Cell) -> Cell {
234 (q, -q - r)
235}
236
237type Orbit = (Cell, Option<Cell>);
240
241fn mirror_orbits(cells: &[Cell], mirror: fn(Cell) -> Cell) -> Vec<Orbit> {
244 let mut seen: BTreeSet<Cell> = BTreeSet::new();
245 let mut orbits = Vec::new();
246 for &cell in cells {
247 if seen.insert(cell) {
248 let image = mirror(cell);
249 let twin = (image != cell).then_some(image);
250 seen.extend(twin);
251 orbits.push((cell, twin));
252 }
253 }
254 orbits
255}
256
257fn is_connected(coords: &[Cell]) -> bool {
260 let set: BTreeSet<Cell> = coords.iter().copied().collect();
261 let start = coords[0];
262 let mut seen: BTreeSet<Cell> = BTreeSet::from([start]);
263 let mut stack = vec![start];
264 while let Some(cell) = stack.pop() {
265 for dir in Dir::ALL {
266 let (dq, dr) = dir.delta();
267 let next = (cell.0 + dq, cell.1 + dr);
268 if set.contains(&next) && seen.insert(next) {
269 stack.push(next);
270 }
271 }
272 }
273 seen.len() == coords.len()
274}
275
276fn symmetric_candidates(nodes: usize) -> Vec<Vec<Cell>> {
287 let pool = hexagon(2);
288 let mut found: BTreeSet<Vec<Cell>> = BTreeSet::new();
289 for mirror in [mirror_v, mirror_h] {
290 let orbits = mirror_orbits(&pool, mirror);
291 for mask in 0..1u64 << orbits.len() {
292 let mut coords: Vec<Cell> = Vec::new();
293 for (bit, &(cell, twin)) in orbits.iter().enumerate() {
294 if mask >> bit & 1 == 1 {
295 coords.push(cell);
296 coords.extend(twin);
297 }
298 }
299 if coords.len() != nodes || !is_connected(&coords) {
300 continue;
301 }
302 for times in 0..3u32 {
307 let mut candidate = coords.clone();
308 for _ in 0..times {
309 candidate = candidate.iter().copied().map(rot).collect();
310 }
311 candidate.sort_unstable();
312 found.insert(candidate);
313 }
314 }
315 }
316 found.into_iter().collect()
317}
318
319pub fn random_symmetric_board(nodes: usize, seed: u64) -> Vec<(i32, i32)> {
329 assert!(
330 (8..=16).contains(&nodes),
331 "random_symmetric_board supports 8..=16 nodes, got {nodes}"
332 );
333 let candidates = symmetric_candidates(nodes);
334 let mut rng = fastrand::Rng::with_seed(seed);
335 candidates[rng.usize(..candidates.len())].clone()
336}
337
338pub(crate) fn build_nodes(coords: &[(i32, i32)]) -> (Vec<Node>, BTreeMap<(i32, i32), NodeId>) {
341 let lookup: BTreeMap<(i32, i32), NodeId> = coords
342 .iter()
343 .enumerate()
344 .map(|(id, &coord)| (coord, id))
345 .collect();
346 let nodes = coords
347 .iter()
348 .map(|&(q, r)| Node {
349 q,
350 r,
351 neighbors: std::array::from_fn(|i| {
352 let (dq, dr) = Dir::ALL[i].delta();
353 lookup.get(&(q + dq, r + dr)).copied()
354 }),
355 })
356 .collect();
357 (nodes, lookup)
358}
359
360impl HexwebGame {
361 pub fn random(params: Params, seed: u64) -> Self {
373 validate_params(¶ms);
374 let (nodes, _) = build_nodes(¶ms.nodes);
375 let mut rng = fastrand::Rng::with_seed(seed);
376 let generated = crate::generator::generate(
377 &nodes,
378 params.pieces,
379 params.min_arrows,
380 params.max_arrows,
381 &mut rng,
382 );
383 let pieces: Vec<Piece> = generated
384 .arrows
385 .into_iter()
386 .map(|arrows| Piece { arrows })
387 .collect();
388 Self::from_parts(
389 nodes,
390 pieces,
391 generated.scramble.clone(),
392 generated.scramble,
393 )
394 }
395
396 pub(crate) fn from_parts(
397 nodes: Vec<Node>,
398 pieces: Vec<Piece>,
399 cell_piece: Vec<Option<PieceId>>,
400 initial: Vec<Option<PieceId>>,
401 ) -> Self {
402 debug_assert_eq!(nodes.len(), cell_piece.len());
403 debug_assert_eq!(cell_piece.len(), initial.len());
404 let lookup: BTreeMap<(i32, i32), NodeId> = nodes
405 .iter()
406 .enumerate()
407 .map(|(id, node)| ((node.q, node.r), id))
408 .collect();
409 Self {
410 nodes,
411 lookup,
412 pieces,
413 cell_piece,
414 initial,
415 moves: 0,
416 status: GameStatus::InProgress,
417 }
418 }
419
420 pub fn nodes(&self) -> &[Node] {
423 &self.nodes
424 }
425
426 pub fn node_count(&self) -> usize {
428 self.nodes.len()
429 }
430
431 pub fn piece_count(&self) -> usize {
433 self.pieces.len()
434 }
435
436 pub fn empty_count(&self) -> usize {
438 self.nodes.len() - self.pieces.len()
439 }
440
441 pub fn pieces(&self) -> &[Piece] {
443 &self.pieces
444 }
445
446 pub fn piece_at(&self, node: NodeId) -> Option<PieceId> {
448 self.cell_piece[node]
449 }
450
451 pub fn node_of(&self, piece: PieceId) -> Option<NodeId> {
453 self.cell_piece
454 .iter()
455 .position(|&occupant| occupant == Some(piece))
456 }
457
458 pub fn node_at(&self, q: i32, r: i32) -> Option<NodeId> {
460 self.lookup.get(&(q, r)).copied()
461 }
462
463 pub fn can_move(&self, piece: PieceId, to: NodeId) -> bool {
468 if self.status != GameStatus::InProgress || to >= self.nodes.len() {
469 return false;
470 }
471 match self.node_of(piece) {
472 Some(from) => from != to && self.cell_piece[to].is_none(),
473 None => false,
474 }
475 }
476
477 pub fn move_piece(&mut self, piece: PieceId, to: NodeId) -> bool {
482 let Some(from) = self.node_of(piece) else {
483 return false;
484 };
485 if !self.can_move(piece, to) {
486 return false;
487 }
488 self.cell_piece[from] = None;
489 self.cell_piece[to] = Some(piece);
490 self.moves += 1;
491 if self.is_solved() {
492 self.status = GameStatus::Won;
493 }
494 true
495 }
496
497 pub fn can_swap(&self, a: PieceId, b: PieceId) -> bool {
502 self.status == GameStatus::InProgress
503 && a != b
504 && self.node_of(a).is_some()
505 && self.node_of(b).is_some()
506 }
507
508 pub fn swap_pieces(&mut self, a: PieceId, b: PieceId) -> bool {
515 if !self.can_swap(a, b) {
516 return false;
517 }
518 let from = self.node_of(a).unwrap();
519 let to = self.node_of(b).unwrap();
520 self.cell_piece[from] = Some(b);
521 self.cell_piece[to] = Some(a);
522 if self.pieces[a] != self.pieces[b] {
523 self.moves += 1;
524 }
525 if self.is_solved() {
526 self.status = GameStatus::Won;
527 }
528 true
529 }
530
531 pub fn satisfied(&self, piece: PieceId) -> bool {
533 self.unsatisfied_arrows(piece).is_empty()
534 }
535
536 pub fn unsatisfied_arrows(&self, piece: PieceId) -> Arrows {
541 let Some(node) = self.node_of(piece) else {
542 return self.pieces[piece].arrows;
543 };
544 let mut result = Arrows::default();
545 for dir in self.pieces[piece].arrows.iter() {
546 let satisfied = match self.nodes[node].neighbors[dir as usize] {
547 Some(neighbor) => match self.cell_piece[neighbor] {
548 Some(other) => self.pieces[other].arrows.contains(dir.opposite()),
549 None => false,
550 },
551 None => false,
552 };
553 if !satisfied {
554 result = result.with(dir);
555 }
556 }
557 result
558 }
559
560 pub fn is_solved(&self) -> bool {
563 (0..self.pieces.len()).all(|piece| self.satisfied(piece))
564 }
565
566 pub fn moves(&self) -> usize {
568 self.moves
569 }
570
571 pub fn status(&self) -> GameStatus {
574 self.status
575 }
576
577 pub fn reset(&mut self) {
579 self.cell_piece = self.initial.clone();
580 self.moves = 0;
581 self.status = GameStatus::InProgress;
582 }
583
584 pub fn solution_count(&self, cap: usize) -> usize {
589 crate::solver::count(&self.pieces, &self.nodes, cap)
590 }
591
592 pub fn solution(&self) -> Option<Vec<NodeId>> {
595 crate::solver::solution(&self.pieces, &self.nodes)
596 }
597}
598
599fn validate_params(params: &Params) {
600 assert!(!params.nodes.is_empty(), "board needs at least one node");
601 let mut sorted = params.nodes.clone();
602 sorted.sort_unstable();
603 sorted.dedup();
604 assert_eq!(
605 sorted.len(),
606 params.nodes.len(),
607 "board nodes must be distinct coordinates"
608 );
609 let (built, _) = build_nodes(¶ms.nodes);
610 assert!(
611 built
612 .iter()
613 .all(|node| node.neighbors.iter().any(Option::is_some)),
614 "board has isolated nodes with no in-board neighbor"
615 );
616 assert!(
617 params.pieces >= 1,
618 "board needs at least one piece, got {}",
619 params.pieces
620 );
621 assert!(
622 params.pieces < params.nodes.len(),
623 "board needs at least one empty node: {} pieces on {} nodes",
624 params.pieces,
625 params.nodes.len()
626 );
627 assert!(
628 (1..=6).contains(¶ms.min_arrows)
629 && (params.min_arrows..=6).contains(¶ms.max_arrows),
630 "arrow range must be within 1..=6 with min <= max, got {}..={}",
631 params.min_arrows,
632 params.max_arrows
633 );
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 fn line_game() -> HexwebGame {
645 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1)]);
646 let pieces = vec![
647 Piece {
648 arrows: Arrows::from_dir(Dir::Up),
649 },
650 Piece {
651 arrows: Arrows::from_dir(Dir::Down),
652 },
653 ];
654 let cell_piece = vec![Some(1), Some(0), None];
655 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
656 }
657
658 #[test]
659 fn dir_delta_and_opposite_tables() {
660 let expected = [
661 (Dir::Up, (0, -1), Dir::Down),
662 (Dir::RightUp, (1, -1), Dir::LeftDown),
663 (Dir::RightDown, (1, 0), Dir::LeftUp),
664 (Dir::Down, (0, 1), Dir::Up),
665 (Dir::LeftDown, (-1, 1), Dir::RightUp),
666 (Dir::LeftUp, (-1, 0), Dir::RightDown),
667 ];
668 for &(dir, delta, opposite) in &expected {
669 assert_eq!(dir.delta(), delta);
670 assert_eq!(dir.opposite(), opposite);
671 assert_eq!(opposite.opposite(), dir);
672 }
673 assert_eq!(Dir::ALL.len(), 6);
674 }
675
676 #[test]
677 fn arrows_bitmask_round_trip() {
678 let mut arrows = Arrows::default();
679 assert!(arrows.is_empty());
680 assert_eq!(arrows.len(), 0);
681 arrows = arrows.with(Dir::Up).with(Dir::LeftDown);
682 assert_eq!(arrows.len(), 2);
683 assert!(arrows.contains(Dir::Up));
684 assert!(arrows.contains(Dir::LeftDown));
685 assert!(!arrows.contains(Dir::Down));
686 assert_eq!(
687 arrows.iter().collect::<Vec<_>>(),
688 vec![Dir::Up, Dir::LeftDown]
689 );
690 arrows = arrows.without(Dir::Up);
691 assert_eq!(arrows, Arrows::from_dir(Dir::LeftDown));
692 assert_eq!(Arrows::all().len(), 6);
693 }
694
695 #[test]
696 fn hexagon_sizes() {
697 assert_eq!(hexagon(0).len(), 1);
698 assert_eq!(hexagon(1).len(), 7);
699 assert_eq!(hexagon(2).len(), 19);
700 assert!(hexagon(1).contains(&(0, 0)));
701 assert!(hexagon(1).contains(&(-1, 1)));
702 assert!(!hexagon(1).contains(&(0, 2)));
703 }
704
705 fn conjugate(mirror: fn(Cell) -> Cell, k: u32) -> impl Fn(Cell) -> Cell {
708 move |cell: Cell| {
709 let mut c = cell;
710 for _ in 0..k {
711 c = rot_inv(c);
712 }
713 let mut out = mirror(c);
714 for _ in 0..k {
715 out = rot(out);
716 }
717 out
718 }
719 }
720
721 #[test]
722 fn random_boards_are_symmetric_connected_and_sized() {
723 let pool = hexagon(2);
724 for nodes in 8..=16 {
725 for seed in 0..32u64 {
726 let coords = random_symmetric_board(nodes, seed);
727 assert_eq!(coords.len(), nodes, "shape of {nodes} nodes, seed {seed}");
728 assert!(
729 coords.windows(2).all(|w| w[0] < w[1]),
730 "shape of {nodes} nodes, seed {seed} is not sorted"
731 );
732
733 let set: BTreeSet<Cell> = coords.iter().copied().collect();
735 let mut symmetric = false;
736 for mirror in [mirror_v, mirror_h] {
737 for k in 0..3u32 {
738 let map = conjugate(mirror, k);
739 symmetric |= set.iter().all(|&c| set.contains(&map(c)));
740 }
741 }
742 assert!(
743 symmetric,
744 "shape of {nodes} nodes, seed {seed} is not mirror-symmetric"
745 );
746
747 assert!(
748 coords.iter().all(|c| pool.contains(c)),
749 "shape of {nodes} nodes, seed {seed} leaves hexagon(2)"
750 );
751 assert!(
752 is_connected(&coords),
753 "shape of {nodes} nodes, seed {seed} is not connected"
754 );
755
756 assert_eq!(coords, random_symmetric_board(nodes, seed));
758 }
759 }
760 }
761
762 #[test]
763 fn every_size_offers_a_variety_of_shapes() {
764 for nodes in 8..=16 {
765 let candidates = symmetric_candidates(nodes);
766 println!("{nodes} nodes: {} shapes", candidates.len());
767 assert!(
768 candidates.len() >= 10,
769 "only {} shapes for {nodes} nodes",
770 candidates.len()
771 );
772 let unique: BTreeSet<&Vec<Cell>> = candidates.iter().collect();
773 assert_eq!(
774 unique.len(),
775 candidates.len(),
776 "duplicate shapes at {nodes} nodes"
777 );
778 }
779
780 let mut old8 = hexagon(1);
784 old8.push((0, -2));
785 old8.sort_unstable();
786 assert!(symmetric_candidates(8).contains(&old8));
787 let old16: Vec<Cell> = hexagon(2)
788 .into_iter()
789 .filter(|&c| !matches!(c, (0, 2) | (2, 0) | (-2, 2)))
790 .collect();
791 assert!(symmetric_candidates(16).contains(&old16));
792 }
793
794 #[test]
795 fn neighbor_tables_are_antisymmetric() {
796 for nodes in 8..=16 {
797 let coords = random_symmetric_board(nodes, nodes as u64);
798 let (built, _) = build_nodes(&coords);
799 for (id, node) in built.iter().enumerate() {
800 for (i, &neighbor) in node.neighbors.iter().enumerate() {
801 if let Some(neighbor) = neighbor {
802 let back = built[neighbor].neighbors[Dir::ALL[i].opposite() as usize];
803 assert_eq!(back, Some(id));
804 }
805 }
806 }
807 }
808 }
809
810 #[test]
811 fn accessors_report_the_board() {
812 let game = line_game();
813 assert_eq!(game.node_count(), 3);
814 assert_eq!(game.piece_count(), 2);
815 assert_eq!(game.empty_count(), 1);
816 assert_eq!(game.node_at(0, -1), Some(1));
817 assert_eq!(game.node_at(5, 5), None);
818 assert_eq!(game.piece_at(1), Some(0));
819 assert_eq!(game.piece_at(2), None);
820 assert_eq!(game.node_of(0), Some(1));
821 assert_eq!(game.pieces()[1].arrows, Arrows::from_dir(Dir::Down));
822 assert_eq!(game.nodes()[1].q, 0);
823 assert_eq!(game.nodes()[1].r, -1);
824 }
825
826 #[test]
827 fn starts_unsatisfied_and_reports_which_arrows() {
828 let game = line_game();
829 assert!(!game.is_solved());
830 assert_eq!(game.status(), GameStatus::InProgress);
831 assert_eq!(game.unsatisfied_arrows(0), Arrows::from_dir(Dir::Up));
833 assert_eq!(game.unsatisfied_arrows(1), Arrows::from_dir(Dir::Down));
835 assert!(!game.satisfied(0));
836 assert!(!game.satisfied(1));
837 }
838
839 #[test]
840 fn arrows_without_reciprocal_stay_unsatisfied() {
841 let (nodes, _) = build_nodes(&[(0, 0), (0, -1)]);
844 let up = Piece {
845 arrows: Arrows::from_dir(Dir::Up),
846 };
847 let pieces = vec![up, up];
848 let cell_piece = vec![Some(0), Some(1)];
849 let game = HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece);
850 assert_eq!(game.unsatisfied_arrows(0), Arrows::from_dir(Dir::Up));
851 assert!(!game.is_solved());
852
853 let (nodes, _) = build_nodes(&[(0, 0), (0, -1)]);
855 let pieces = vec![
856 Piece {
857 arrows: Arrows::from_dir(Dir::Up),
858 },
859 Piece {
860 arrows: Arrows::from_dir(Dir::Down),
861 },
862 ];
863 let cell_piece = vec![Some(0), Some(1)];
864 let game = HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece);
865 assert_eq!(game.unsatisfied_arrows(0), Arrows::default());
866 assert_eq!(game.unsatisfied_arrows(1), Arrows::default());
867 assert!(game.is_solved());
868 }
869
870 #[test]
871 fn illegal_moves_are_rejected() {
872 let mut game = line_game();
873 assert!(!game.can_move(0, 1)); assert!(!game.can_move(0, 0)); assert!(!game.can_move(0, 99)); assert_eq!(game.moves(), 0);
877 assert!(!game.move_piece(0, 0));
878 assert!(!game.move_piece(0, 1));
879 assert_eq!(game.moves(), 0);
880 }
881
882 #[test]
883 fn winning_move_latches_and_reset_restores() {
884 let mut game = line_game();
885 assert!(game.move_piece(0, 2));
886 assert_eq!(game.moves(), 1);
887 assert!(game.is_solved());
888 assert_eq!(game.status(), GameStatus::Won);
889 assert_eq!(game.unsatisfied_arrows(0), Arrows::default());
890 assert!(game.satisfied(1));
891
892 assert!(!game.can_move(1, 1));
894 assert!(!game.move_piece(1, 1));
895 assert_eq!(game.moves(), 1);
896
897 game.reset();
898 assert_eq!(game.status(), GameStatus::InProgress);
899 assert_eq!(game.moves(), 0);
900 assert_eq!(game.piece_at(1), Some(0));
901 assert!(!game.is_solved());
902 }
903
904 fn long_line_game() -> HexwebGame {
910 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1), (0, 2)]);
911 let pieces = vec![
912 Piece {
913 arrows: Arrows::from_dir(Dir::Up),
914 },
915 Piece {
916 arrows: Arrows::from_dir(Dir::Down),
917 },
918 ];
919 let cell_piece = vec![Some(0), None, None, Some(1)];
920 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
921 }
922
923 fn identical_pair_game() -> HexwebGame {
926 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1), (0, 2)]);
927 let pieces = vec![
928 Piece {
929 arrows: Arrows::from_dir(Dir::Up),
930 },
931 Piece {
932 arrows: Arrows::from_dir(Dir::Up),
933 },
934 ];
935 let cell_piece = vec![Some(0), None, None, Some(1)];
936 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
937 }
938
939 #[test]
940 fn swapping_pieces_exchanges_nodes_and_counts_a_move() {
941 let mut game = long_line_game();
942 assert!(game.can_swap(0, 1));
943 let (node_a, node_b) = (game.node_of(0), game.node_of(1));
944 assert!(game.swap_pieces(0, 1));
945 assert_eq!(game.node_of(0), node_b);
946 assert_eq!(game.node_of(1), node_a);
947 assert_eq!(game.moves(), 1);
948 assert_eq!(game.status(), GameStatus::InProgress);
949 }
950
951 #[test]
952 fn winning_swap_latches_and_reset_restores() {
953 let mut game = line_game();
954 assert!(game.swap_pieces(0, 1));
957 assert_eq!(game.moves(), 1);
958 assert!(game.is_solved());
959 assert_eq!(game.status(), GameStatus::Won);
960
961 assert!(!game.can_swap(0, 1));
963 assert!(!game.swap_pieces(0, 1));
964 assert_eq!(game.moves(), 1);
965
966 game.reset();
967 assert_eq!(game.status(), GameStatus::InProgress);
968 assert_eq!(game.moves(), 0);
969 assert_eq!(game.node_of(0), Some(1));
970 assert_eq!(game.node_of(1), Some(0));
971 assert!(!game.is_solved());
972 }
973
974 #[test]
975 fn illegal_swaps_are_rejected() {
976 let mut game = long_line_game();
977 assert!(!game.can_swap(0, 0)); assert!(!game.can_swap(0, 7)); assert!(!game.swap_pieces(0, 0));
980 assert!(!game.swap_pieces(0, 7));
981 assert_eq!(game.moves(), 0);
982 assert_eq!(game.node_of(0), Some(0));
983 assert_eq!(game.node_of(1), Some(3));
984 }
985
986 #[test]
987 fn swapping_identical_pieces_does_not_count_a_move() {
988 let mut game = identical_pair_game();
989 assert!(game.can_swap(0, 1));
990 let (node_a, node_b) = (game.node_of(0), game.node_of(1));
991 assert!(game.swap_pieces(0, 1));
992 assert_eq!(game.node_of(0), node_b);
993 assert_eq!(game.node_of(1), node_a);
994 assert_eq!(game.moves(), 0);
997 assert_eq!(game.status(), GameStatus::InProgress);
998 }
999
1000 #[test]
1001 fn params_are_validated() {
1002 let valid = Params {
1003 nodes: random_symmetric_board(8, 0),
1004 pieces: 6,
1005 min_arrows: 2,
1006 max_arrows: 3,
1007 };
1008 validate_params(&valid);
1009
1010 let mut dup = valid.clone();
1011 dup.nodes[1] = dup.nodes[0];
1012 assert!(std::panic::catch_unwind(|| validate_params(&dup)).is_err());
1013
1014 let mut full = valid.clone();
1015 full.pieces = full.nodes.len();
1016 assert!(std::panic::catch_unwind(|| validate_params(&full)).is_err());
1017
1018 let mut no_pieces = valid.clone();
1019 no_pieces.pieces = 0;
1020 assert!(std::panic::catch_unwind(|| validate_params(&no_pieces)).is_err());
1021
1022 let mut no_arrows = valid.clone();
1023 no_arrows.min_arrows = 0;
1024 assert!(std::panic::catch_unwind(|| validate_params(&no_arrows)).is_err());
1025
1026 let mut too_many = valid.clone();
1027 too_many.max_arrows = 7;
1028 assert!(std::panic::catch_unwind(|| validate_params(&too_many)).is_err());
1029
1030 let mut inverted = valid.clone();
1031 inverted.min_arrows = 4;
1032 inverted.max_arrows = 3;
1033 assert!(std::panic::catch_unwind(|| validate_params(&inverted)).is_err());
1034
1035 let mut isolated = valid.clone();
1036 isolated.nodes.push((100, 100));
1037 assert!(std::panic::catch_unwind(|| validate_params(&isolated)).is_err());
1038 }
1039}