1use core::fmt;
2
3use std::collections::BTreeMap as Map;
4
5use crate::{
6 Game, Move, Position,
7 formats::{san::San, uci},
8 game,
9 position::Terminal,
10};
11
12pub const SAMPLE: &str = "";
14
15pub fn sample() -> impl Iterator<Item = Puzzle> {
16 SAMPLE.lines().map(|line| Puzzle::from_csv(line).expect("valid puzzle in sample"))
17}
18
19#[derive(Clone, Debug)]
20pub struct Puzzle {
21 pub mistake: Option<Move>,
22 pub position: Position,
23}
24
25impl Puzzle {
26 pub fn from_csv(line: &str) -> Option<Self> {
27 let (mistake, fen) = line.split_once(',')?;
28 let position = Position::from_fen(fen).ok()?;
29 let mistake = mistake.parse::<uci::Move>().ok()?.resolve_chess(&position.legal_plays())?;
30 Some(Self { mistake: Some(mistake), position })
31 }
32}
33
34impl Puzzle {
35 pub fn start(&self) -> Position {
36 match self.mistake {
37 Some(mistake) => self.position.apply_unchecked(mistake),
38 None => self.position,
39 }
40 }
41}
42
43#[derive(Clone, Debug, Default)]
44pub struct Solutions(pub Map<Move, Map<Move, Map<Move, Position>>>);
45
46#[derive(Clone)]
47pub struct MateIn2 {
48 pub position: Position,
49 pub mate_in_1: Map<Move, Position>,
50 pub solutions: Solutions,
51}
52
53fn is_checkmate(position: Position) -> bool {
54 position.terminal() == Some(Terminal::Checkmate)
55}
56
57fn san(position: Position, play: Move) -> game::options::Result<San> {
58 let mut game = Game::new(position);
59 Ok(game.start_mut().into_push(play)?.san())
60}
61
62impl fmt::Debug for MateIn2 {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 writeln!(f, "{:?}", self.position.board())?;
65
66 if !self.mate_in_1.is_empty() {
67 write!(f, "mate-in-1:")?;
68 for play1 in self.mate_in_1.keys() {
69 write!(f, " {}", san(self.position, *play1).map_err(|_| fmt::Error)?)?;
70 }
71 writeln!(f)?;
72 }
73
74 for (play1, replies) in &self.solutions.0 {
75 writeln!(f, "{}", san(self.position, *play1).map_err(|_| fmt::Error)?)?;
76 let defend = self.position.apply_unchecked(*play1);
77 for (defense, mates) in replies {
78 writeln!(f, " {}", san(defend, *defense).map_err(|_| fmt::Error)?)?;
79 let attack = defend.apply_unchecked(*defense);
80 for play2 in mates.keys() {
81 writeln!(f, " {}", san(attack, *play2).map_err(|_| fmt::Error)?)?;
82 }
83 }
84 }
85
86 Ok(())
87 }
88}
89
90impl MateIn2 {
91 pub fn new(puzzle: Puzzle) -> Option<Self> {
92 let position = puzzle.start();
93 let mut mate_in_1: Map<Move, Position> = Default::default();
96 let mut solutions = Solutions::default().0;
97 'play1: for play1 in position.legal_plays() {
99 let defend = position.apply_unchecked(play1);
100 let defenses = defend.legal_plays();
101
102 if let Some(terminal) = Terminal::new(defend.is_check(), defenses.is_empty()) {
105 if terminal == Terminal::Checkmate {
106 mate_in_1.insert(play1, defend);
107 }
108 continue;
109 }
110
111 let mut replies = Map::new();
112 for defense in defenses {
113 let attack = defend.apply_unchecked(defense);
114 let mut mates = Map::new();
115 for play2 in attack.legal_plays() {
116 let outcome = attack.apply_unchecked(play2);
117 if is_checkmate(outcome) {
118 mates.entry(play2).insert_entry(outcome);
119 }
120 }
121
122 if mates.is_empty() {
123 continue 'play1;
124 }
125 replies.insert(defense, mates);
126 }
127
128 solutions.insert(play1, replies);
129 }
130
131 if solutions.is_empty() {
132 return None;
133 }
134
135 Some(Self { position, mate_in_1, solutions: Solutions(solutions) })
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 #[ignore = "exploratory scan of the external puzzle dataset"]
145 fn search() {
146 let mut count = 0;
147 let mut i = 0;
148 let total = 807425.0;
149 let mut perc = 5.0;
150 for position in sample() {
151 i += 1;
152 if 100.0 * i as f32 / total > perc {
153 println!("{perc}% - {i} of 807425");
154 perc += 5.0;
155 }
156 let Some(puzzle) = MateIn2::new(position) else {
157 continue;
158 };
159 if !puzzle.mate_in_1.is_empty() {
160 println!("{puzzle:?}");
161 count += 1;
162 if count > 10 {
163 break;
164 }
165 }
166 }
174 }
175}