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,
180}
181
182#[derive(Clone, Debug)]
185pub struct HexwebGame {
186 nodes: Vec<Node>,
187 lookup: BTreeMap<(i32, i32), NodeId>,
190 pieces: Vec<Piece>,
191 cell_piece: Vec<Option<PieceId>>,
193 initial: Vec<Option<PieceId>>,
195 moves: usize,
196 status: GameStatus,
197}
198
199pub fn hexagon(radius: u32) -> Vec<(i32, i32)> {
202 let r = radius as i32;
203 let mut coords = Vec::new();
204 for q in -r..=r {
205 for rr in (-r).max(-q - r)..=r.min(-q + r) {
206 coords.push((q, rr));
207 }
208 }
209 coords
210}
211
212type Cell = (i32, i32);
214
215fn rot((q, r): Cell) -> Cell {
217 (-r, q + r)
218}
219
220#[cfg(test)]
222fn rot_inv((q, r): Cell) -> Cell {
223 (r + q, -q)
224}
225
226fn mirror_v((q, r): Cell) -> Cell {
228 (-q, r + q)
229}
230
231fn mirror_h((q, r): Cell) -> Cell {
233 (q, -q - r)
234}
235
236type Orbit = (Cell, Option<Cell>);
239
240fn mirror_orbits(cells: &[Cell], mirror: fn(Cell) -> Cell) -> Vec<Orbit> {
243 let mut seen: BTreeSet<Cell> = BTreeSet::new();
244 let mut orbits = Vec::new();
245 for &cell in cells {
246 if seen.insert(cell) {
247 let image = mirror(cell);
248 let twin = (image != cell).then_some(image);
249 seen.extend(twin);
250 orbits.push((cell, twin));
251 }
252 }
253 orbits
254}
255
256fn is_connected(coords: &[Cell]) -> bool {
259 let set: BTreeSet<Cell> = coords.iter().copied().collect();
260 let start = coords[0];
261 let mut seen: BTreeSet<Cell> = BTreeSet::from([start]);
262 let mut stack = vec![start];
263 while let Some(cell) = stack.pop() {
264 for dir in Dir::ALL {
265 let (dq, dr) = dir.delta();
266 let next = (cell.0 + dq, cell.1 + dr);
267 if set.contains(&next) && seen.insert(next) {
268 stack.push(next);
269 }
270 }
271 }
272 seen.len() == coords.len()
273}
274
275fn symmetric_candidates(nodes: usize) -> Vec<Vec<Cell>> {
286 let pool = hexagon(2);
287 let mut found: BTreeSet<Vec<Cell>> = BTreeSet::new();
288 for mirror in [mirror_v, mirror_h] {
289 let orbits = mirror_orbits(&pool, mirror);
290 for mask in 0..1u64 << orbits.len() {
291 let mut coords: Vec<Cell> = Vec::new();
292 for (bit, &(cell, twin)) in orbits.iter().enumerate() {
293 if mask >> bit & 1 == 1 {
294 coords.push(cell);
295 coords.extend(twin);
296 }
297 }
298 if coords.len() != nodes || !is_connected(&coords) {
299 continue;
300 }
301 for times in 0..3u32 {
306 let mut candidate = coords.clone();
307 for _ in 0..times {
308 candidate = candidate.iter().copied().map(rot).collect();
309 }
310 candidate.sort_unstable();
311 found.insert(candidate);
312 }
313 }
314 }
315 found.into_iter().collect()
316}
317
318pub fn random_symmetric_board(nodes: usize, seed: u64) -> Vec<(i32, i32)> {
328 assert!(
329 (8..=16).contains(&nodes),
330 "random_symmetric_board supports 8..=16 nodes, got {nodes}"
331 );
332 let candidates = symmetric_candidates(nodes);
333 let mut rng = fastrand::Rng::with_seed(seed);
334 candidates[rng.usize(..candidates.len())].clone()
335}
336
337pub(crate) fn build_nodes(coords: &[(i32, i32)]) -> (Vec<Node>, BTreeMap<(i32, i32), NodeId>) {
340 let lookup: BTreeMap<(i32, i32), NodeId> = coords
341 .iter()
342 .enumerate()
343 .map(|(id, &coord)| (coord, id))
344 .collect();
345 let nodes = coords
346 .iter()
347 .map(|&(q, r)| Node {
348 q,
349 r,
350 neighbors: std::array::from_fn(|i| {
351 let (dq, dr) = Dir::ALL[i].delta();
352 lookup.get(&(q + dq, r + dr)).copied()
353 }),
354 })
355 .collect();
356 (nodes, lookup)
357}
358
359impl HexwebGame {
360 pub fn random(params: Params, seed: u64) -> Self {
372 validate_params(¶ms);
373 let (nodes, _) = build_nodes(¶ms.nodes);
374 let mut rng = fastrand::Rng::with_seed(seed);
375 let generated = crate::generator::generate(
376 &nodes,
377 params.pieces,
378 params.min_arrows,
379 params.max_arrows,
380 &mut rng,
381 );
382 let pieces: Vec<Piece> = generated
383 .arrows
384 .into_iter()
385 .map(|arrows| Piece { arrows })
386 .collect();
387 Self::from_parts(
388 nodes,
389 pieces,
390 generated.scramble.clone(),
391 generated.scramble,
392 )
393 }
394
395 pub(crate) fn from_parts(
396 nodes: Vec<Node>,
397 pieces: Vec<Piece>,
398 cell_piece: Vec<Option<PieceId>>,
399 initial: Vec<Option<PieceId>>,
400 ) -> Self {
401 debug_assert_eq!(nodes.len(), cell_piece.len());
402 debug_assert_eq!(cell_piece.len(), initial.len());
403 let lookup: BTreeMap<(i32, i32), NodeId> = nodes
404 .iter()
405 .enumerate()
406 .map(|(id, node)| ((node.q, node.r), id))
407 .collect();
408 Self {
409 nodes,
410 lookup,
411 pieces,
412 cell_piece,
413 initial,
414 moves: 0,
415 status: GameStatus::InProgress,
416 }
417 }
418
419 pub fn nodes(&self) -> &[Node] {
422 &self.nodes
423 }
424
425 pub fn node_count(&self) -> usize {
427 self.nodes.len()
428 }
429
430 pub fn piece_count(&self) -> usize {
432 self.pieces.len()
433 }
434
435 pub fn empty_count(&self) -> usize {
437 self.nodes.len() - self.pieces.len()
438 }
439
440 pub fn pieces(&self) -> &[Piece] {
442 &self.pieces
443 }
444
445 pub fn piece_at(&self, node: NodeId) -> Option<PieceId> {
447 self.cell_piece[node]
448 }
449
450 pub fn node_of(&self, piece: PieceId) -> Option<NodeId> {
452 self.cell_piece
453 .iter()
454 .position(|&occupant| occupant == Some(piece))
455 }
456
457 pub fn node_at(&self, q: i32, r: i32) -> Option<NodeId> {
459 self.lookup.get(&(q, r)).copied()
460 }
461
462 pub fn can_move(&self, piece: PieceId, to: NodeId) -> bool {
467 if self.status != GameStatus::InProgress || to >= self.nodes.len() {
468 return false;
469 }
470 match self.node_of(piece) {
471 Some(from) => from != to && self.cell_piece[to].is_none(),
472 None => false,
473 }
474 }
475
476 pub fn move_piece(&mut self, piece: PieceId, to: NodeId) -> bool {
481 let Some(from) = self.node_of(piece) else {
482 return false;
483 };
484 if !self.can_move(piece, to) {
485 return false;
486 }
487 self.cell_piece[from] = None;
488 self.cell_piece[to] = Some(piece);
489 self.moves += 1;
490 if self.is_solved() {
491 self.status = GameStatus::Won;
492 }
493 true
494 }
495
496 pub fn can_swap(&self, a: PieceId, b: PieceId) -> bool {
501 self.status == GameStatus::InProgress
502 && a != b
503 && self.node_of(a).is_some()
504 && self.node_of(b).is_some()
505 }
506
507 pub fn swap_pieces(&mut self, a: PieceId, b: PieceId) -> bool {
514 if !self.can_swap(a, b) {
515 return false;
516 }
517 let from = self.node_of(a).unwrap();
518 let to = self.node_of(b).unwrap();
519 self.cell_piece[from] = Some(b);
520 self.cell_piece[to] = Some(a);
521 if self.pieces[a] != self.pieces[b] {
522 self.moves += 1;
523 }
524 if self.is_solved() {
525 self.status = GameStatus::Won;
526 }
527 true
528 }
529
530 pub fn satisfied(&self, piece: PieceId) -> bool {
532 self.unsatisfied_arrows(piece).is_empty()
533 }
534
535 pub fn unsatisfied_arrows(&self, piece: PieceId) -> Arrows {
538 let Some(node) = self.node_of(piece) else {
539 return self.pieces[piece].arrows;
540 };
541 let mut result = Arrows::default();
542 for dir in self.pieces[piece].arrows.iter() {
543 match self.nodes[node].neighbors[dir as usize] {
544 Some(neighbor) if self.cell_piece[neighbor].is_some() => {}
545 _ => result = result.with(dir),
546 }
547 }
548 result
549 }
550
551 pub fn is_solved(&self) -> bool {
553 (0..self.pieces.len()).all(|piece| self.satisfied(piece))
554 }
555
556 pub fn moves(&self) -> usize {
558 self.moves
559 }
560
561 pub fn status(&self) -> GameStatus {
564 self.status
565 }
566
567 pub fn reset(&mut self) {
569 self.cell_piece = self.initial.clone();
570 self.moves = 0;
571 self.status = GameStatus::InProgress;
572 }
573
574 pub fn solution_count(&self, cap: usize) -> usize {
579 crate::solver::count(&self.pieces, &self.nodes, cap)
580 }
581
582 pub fn solution(&self) -> Option<Vec<NodeId>> {
585 crate::solver::solution(&self.pieces, &self.nodes)
586 }
587}
588
589fn validate_params(params: &Params) {
590 assert!(!params.nodes.is_empty(), "board needs at least one node");
591 let mut sorted = params.nodes.clone();
592 sorted.sort_unstable();
593 sorted.dedup();
594 assert_eq!(
595 sorted.len(),
596 params.nodes.len(),
597 "board nodes must be distinct coordinates"
598 );
599 let (built, _) = build_nodes(¶ms.nodes);
600 assert!(
601 built
602 .iter()
603 .all(|node| node.neighbors.iter().any(Option::is_some)),
604 "board has isolated nodes with no in-board neighbor"
605 );
606 assert!(
607 params.pieces >= 1,
608 "board needs at least one piece, got {}",
609 params.pieces
610 );
611 assert!(
612 params.pieces < params.nodes.len(),
613 "board needs at least one empty node: {} pieces on {} nodes",
614 params.pieces,
615 params.nodes.len()
616 );
617 assert!(
618 (1..=6).contains(¶ms.min_arrows)
619 && (params.min_arrows..=6).contains(¶ms.max_arrows),
620 "arrow range must be within 1..=6 with min <= max, got {}..={}",
621 params.min_arrows,
622 params.max_arrows
623 );
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 fn line_game() -> HexwebGame {
635 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1)]);
636 let pieces = vec![
637 Piece {
638 arrows: Arrows::from_dir(Dir::Up),
639 },
640 Piece {
641 arrows: Arrows::from_dir(Dir::Down),
642 },
643 ];
644 let cell_piece = vec![Some(1), Some(0), None];
645 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
646 }
647
648 #[test]
649 fn dir_delta_and_opposite_tables() {
650 let expected = [
651 (Dir::Up, (0, -1), Dir::Down),
652 (Dir::RightUp, (1, -1), Dir::LeftDown),
653 (Dir::RightDown, (1, 0), Dir::LeftUp),
654 (Dir::Down, (0, 1), Dir::Up),
655 (Dir::LeftDown, (-1, 1), Dir::RightUp),
656 (Dir::LeftUp, (-1, 0), Dir::RightDown),
657 ];
658 for &(dir, delta, opposite) in &expected {
659 assert_eq!(dir.delta(), delta);
660 assert_eq!(dir.opposite(), opposite);
661 assert_eq!(opposite.opposite(), dir);
662 }
663 assert_eq!(Dir::ALL.len(), 6);
664 }
665
666 #[test]
667 fn arrows_bitmask_round_trip() {
668 let mut arrows = Arrows::default();
669 assert!(arrows.is_empty());
670 assert_eq!(arrows.len(), 0);
671 arrows = arrows.with(Dir::Up).with(Dir::LeftDown);
672 assert_eq!(arrows.len(), 2);
673 assert!(arrows.contains(Dir::Up));
674 assert!(arrows.contains(Dir::LeftDown));
675 assert!(!arrows.contains(Dir::Down));
676 assert_eq!(
677 arrows.iter().collect::<Vec<_>>(),
678 vec![Dir::Up, Dir::LeftDown]
679 );
680 arrows = arrows.without(Dir::Up);
681 assert_eq!(arrows, Arrows::from_dir(Dir::LeftDown));
682 assert_eq!(Arrows::all().len(), 6);
683 }
684
685 #[test]
686 fn hexagon_sizes() {
687 assert_eq!(hexagon(0).len(), 1);
688 assert_eq!(hexagon(1).len(), 7);
689 assert_eq!(hexagon(2).len(), 19);
690 assert!(hexagon(1).contains(&(0, 0)));
691 assert!(hexagon(1).contains(&(-1, 1)));
692 assert!(!hexagon(1).contains(&(0, 2)));
693 }
694
695 fn conjugate(mirror: fn(Cell) -> Cell, k: u32) -> impl Fn(Cell) -> Cell {
698 move |cell: Cell| {
699 let mut c = cell;
700 for _ in 0..k {
701 c = rot_inv(c);
702 }
703 let mut out = mirror(c);
704 for _ in 0..k {
705 out = rot(out);
706 }
707 out
708 }
709 }
710
711 #[test]
712 fn random_boards_are_symmetric_connected_and_sized() {
713 let pool = hexagon(2);
714 for nodes in 8..=16 {
715 for seed in 0..32u64 {
716 let coords = random_symmetric_board(nodes, seed);
717 assert_eq!(coords.len(), nodes, "shape of {nodes} nodes, seed {seed}");
718 assert!(
719 coords.windows(2).all(|w| w[0] < w[1]),
720 "shape of {nodes} nodes, seed {seed} is not sorted"
721 );
722
723 let set: BTreeSet<Cell> = coords.iter().copied().collect();
725 let mut symmetric = false;
726 for mirror in [mirror_v, mirror_h] {
727 for k in 0..3u32 {
728 let map = conjugate(mirror, k);
729 symmetric |= set.iter().all(|&c| set.contains(&map(c)));
730 }
731 }
732 assert!(
733 symmetric,
734 "shape of {nodes} nodes, seed {seed} is not mirror-symmetric"
735 );
736
737 assert!(
738 coords.iter().all(|c| pool.contains(c)),
739 "shape of {nodes} nodes, seed {seed} leaves hexagon(2)"
740 );
741 assert!(
742 is_connected(&coords),
743 "shape of {nodes} nodes, seed {seed} is not connected"
744 );
745
746 assert_eq!(coords, random_symmetric_board(nodes, seed));
748 }
749 }
750 }
751
752 #[test]
753 fn every_size_offers_a_variety_of_shapes() {
754 for nodes in 8..=16 {
755 let candidates = symmetric_candidates(nodes);
756 println!("{nodes} nodes: {} shapes", candidates.len());
757 assert!(
758 candidates.len() >= 10,
759 "only {} shapes for {nodes} nodes",
760 candidates.len()
761 );
762 let unique: BTreeSet<&Vec<Cell>> = candidates.iter().collect();
763 assert_eq!(
764 unique.len(),
765 candidates.len(),
766 "duplicate shapes at {nodes} nodes"
767 );
768 }
769
770 let mut old8 = hexagon(1);
774 old8.push((0, -2));
775 old8.sort_unstable();
776 assert!(symmetric_candidates(8).contains(&old8));
777 let old16: Vec<Cell> = hexagon(2)
778 .into_iter()
779 .filter(|&c| !matches!(c, (0, 2) | (2, 0) | (-2, 2)))
780 .collect();
781 assert!(symmetric_candidates(16).contains(&old16));
782 }
783
784 #[test]
785 fn neighbor_tables_are_antisymmetric() {
786 for nodes in 8..=16 {
787 let coords = random_symmetric_board(nodes, nodes as u64);
788 let (built, _) = build_nodes(&coords);
789 for (id, node) in built.iter().enumerate() {
790 for (i, &neighbor) in node.neighbors.iter().enumerate() {
791 if let Some(neighbor) = neighbor {
792 let back = built[neighbor].neighbors[Dir::ALL[i].opposite() as usize];
793 assert_eq!(back, Some(id));
794 }
795 }
796 }
797 }
798 }
799
800 #[test]
801 fn accessors_report_the_board() {
802 let game = line_game();
803 assert_eq!(game.node_count(), 3);
804 assert_eq!(game.piece_count(), 2);
805 assert_eq!(game.empty_count(), 1);
806 assert_eq!(game.node_at(0, -1), Some(1));
807 assert_eq!(game.node_at(5, 5), None);
808 assert_eq!(game.piece_at(1), Some(0));
809 assert_eq!(game.piece_at(2), None);
810 assert_eq!(game.node_of(0), Some(1));
811 assert_eq!(game.pieces()[1].arrows, Arrows::from_dir(Dir::Down));
812 assert_eq!(game.nodes()[1].q, 0);
813 assert_eq!(game.nodes()[1].r, -1);
814 }
815
816 #[test]
817 fn starts_unsatisfied_and_reports_which_arrows() {
818 let game = line_game();
819 assert!(!game.is_solved());
820 assert_eq!(game.status(), GameStatus::InProgress);
821 assert_eq!(game.unsatisfied_arrows(0), Arrows::from_dir(Dir::Up));
823 assert_eq!(game.unsatisfied_arrows(1), Arrows::from_dir(Dir::Down));
825 assert!(!game.satisfied(0));
826 assert!(!game.satisfied(1));
827 }
828
829 #[test]
830 fn illegal_moves_are_rejected() {
831 let mut game = line_game();
832 assert!(!game.can_move(0, 1)); assert!(!game.can_move(0, 0)); assert!(!game.can_move(0, 99)); assert_eq!(game.moves(), 0);
836 assert!(!game.move_piece(0, 0));
837 assert!(!game.move_piece(0, 1));
838 assert_eq!(game.moves(), 0);
839 }
840
841 #[test]
842 fn winning_move_latches_and_reset_restores() {
843 let mut game = line_game();
844 assert!(game.move_piece(0, 2));
845 assert_eq!(game.moves(), 1);
846 assert!(game.is_solved());
847 assert_eq!(game.status(), GameStatus::Won);
848 assert_eq!(game.unsatisfied_arrows(0), Arrows::default());
849 assert!(game.satisfied(1));
850
851 assert!(!game.can_move(1, 1));
853 assert!(!game.move_piece(1, 1));
854 assert_eq!(game.moves(), 1);
855
856 game.reset();
857 assert_eq!(game.status(), GameStatus::InProgress);
858 assert_eq!(game.moves(), 0);
859 assert_eq!(game.piece_at(1), Some(0));
860 assert!(!game.is_solved());
861 }
862
863 fn long_line_game() -> HexwebGame {
869 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1), (0, 2)]);
870 let pieces = vec![
871 Piece {
872 arrows: Arrows::from_dir(Dir::Up),
873 },
874 Piece {
875 arrows: Arrows::from_dir(Dir::Down),
876 },
877 ];
878 let cell_piece = vec![Some(0), None, None, Some(1)];
879 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
880 }
881
882 fn identical_pair_game() -> HexwebGame {
885 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1), (0, 2)]);
886 let pieces = vec![
887 Piece {
888 arrows: Arrows::from_dir(Dir::Up),
889 },
890 Piece {
891 arrows: Arrows::from_dir(Dir::Up),
892 },
893 ];
894 let cell_piece = vec![Some(0), None, None, Some(1)];
895 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
896 }
897
898 #[test]
899 fn swapping_pieces_exchanges_nodes_and_counts_a_move() {
900 let mut game = long_line_game();
901 assert!(game.can_swap(0, 1));
902 let (node_a, node_b) = (game.node_of(0), game.node_of(1));
903 assert!(game.swap_pieces(0, 1));
904 assert_eq!(game.node_of(0), node_b);
905 assert_eq!(game.node_of(1), node_a);
906 assert_eq!(game.moves(), 1);
907 assert_eq!(game.status(), GameStatus::InProgress);
908 }
909
910 #[test]
911 fn winning_swap_latches_and_reset_restores() {
912 let mut game = line_game();
913 assert!(game.swap_pieces(0, 1));
916 assert_eq!(game.moves(), 1);
917 assert!(game.is_solved());
918 assert_eq!(game.status(), GameStatus::Won);
919
920 assert!(!game.can_swap(0, 1));
922 assert!(!game.swap_pieces(0, 1));
923 assert_eq!(game.moves(), 1);
924
925 game.reset();
926 assert_eq!(game.status(), GameStatus::InProgress);
927 assert_eq!(game.moves(), 0);
928 assert_eq!(game.node_of(0), Some(1));
929 assert_eq!(game.node_of(1), Some(0));
930 assert!(!game.is_solved());
931 }
932
933 #[test]
934 fn illegal_swaps_are_rejected() {
935 let mut game = long_line_game();
936 assert!(!game.can_swap(0, 0)); assert!(!game.can_swap(0, 7)); assert!(!game.swap_pieces(0, 0));
939 assert!(!game.swap_pieces(0, 7));
940 assert_eq!(game.moves(), 0);
941 assert_eq!(game.node_of(0), Some(0));
942 assert_eq!(game.node_of(1), Some(3));
943 }
944
945 #[test]
946 fn swapping_identical_pieces_does_not_count_a_move() {
947 let mut game = identical_pair_game();
948 assert!(game.can_swap(0, 1));
949 let (node_a, node_b) = (game.node_of(0), game.node_of(1));
950 assert!(game.swap_pieces(0, 1));
951 assert_eq!(game.node_of(0), node_b);
952 assert_eq!(game.node_of(1), node_a);
953 assert_eq!(game.moves(), 0);
956 assert_eq!(game.status(), GameStatus::InProgress);
957 }
958
959 #[test]
960 fn params_are_validated() {
961 let valid = Params {
962 nodes: random_symmetric_board(8, 0),
963 pieces: 6,
964 min_arrows: 2,
965 max_arrows: 3,
966 };
967 validate_params(&valid);
968
969 let mut dup = valid.clone();
970 dup.nodes[1] = dup.nodes[0];
971 assert!(std::panic::catch_unwind(|| validate_params(&dup)).is_err());
972
973 let mut full = valid.clone();
974 full.pieces = full.nodes.len();
975 assert!(std::panic::catch_unwind(|| validate_params(&full)).is_err());
976
977 let mut no_pieces = valid.clone();
978 no_pieces.pieces = 0;
979 assert!(std::panic::catch_unwind(|| validate_params(&no_pieces)).is_err());
980
981 let mut no_arrows = valid.clone();
982 no_arrows.min_arrows = 0;
983 assert!(std::panic::catch_unwind(|| validate_params(&no_arrows)).is_err());
984
985 let mut too_many = valid.clone();
986 too_many.max_arrows = 7;
987 assert!(std::panic::catch_unwind(|| validate_params(&too_many)).is_err());
988
989 let mut inverted = valid.clone();
990 inverted.min_arrows = 4;
991 inverted.max_arrows = 3;
992 assert!(std::panic::catch_unwind(|| validate_params(&inverted)).is_err());
993
994 let mut isolated = valid.clone();
995 isolated.nodes.push((100, 100));
996 assert!(std::panic::catch_unwind(|| validate_params(&isolated)).is_err());
997 }
998}