Skip to main content

chess/
puzzle.rs

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
12// pub const SAMPLE: &str = include_str!("../../tmp/lichess_mate_in_2.csv");
13pub 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        // position.is_mate_in_2().then(Self { position })
94        // let legal = position.legal_plays();
95        let mut mate_in_1: Map<Move, Position> = Default::default();
96        let mut solutions = Solutions::default().0;
97        // first move of our solution
98        'play1: for play1 in position.legal_plays() {
99            let defend = position.apply_unchecked(play1);
100            let defenses = defend.legal_plays();
101
102            // already checkmate? problem was a mate-in-1
103            // stalemate => bad move, suppress
104            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            // if puzzle.solutions.0.values().any(|defenses| defenses.len() > 1) {
167            //     println!("{puzzle:?}");
168            //     count += 1;
169            //     if count > 10 {
170            //         break;
171            //     }
172            // }
173        }
174    }
175}