chess_vector_engine/
opening_book.rs

1use chess::{Board, ChessMove};
2use std::collections::HashMap;
3use std::str::FromStr;
4
5/// Opening book entry containing position evaluation and recommended moves
6#[derive(Debug, Clone)]
7pub struct OpeningEntry {
8    pub evaluation: f32,
9    pub best_moves: Vec<(ChessMove, f32)>, // (move, relative_strength)
10    pub name: String,
11    pub eco_code: Option<String>, // ECO (Encyclopedia of Chess Openings) code
12}
13
14/// Opening book for chess positions
15#[derive(Clone)]
16pub struct OpeningBook {
17    /// Map from FEN string to opening entry
18    entries: HashMap<String, OpeningEntry>,
19}
20
21impl Default for OpeningBook {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl OpeningBook {
28    /// Create a new opening book
29    pub fn new() -> Self {
30        Self {
31            entries: HashMap::new(),
32        }
33    }
34
35    /// Create a basic opening book with common openings
36    pub fn with_standard_openings() -> Self {
37        let mut book = Self::new();
38        book.add_standard_openings();
39        book
40    }
41
42    /// Add an opening entry
43    pub fn add_opening(
44        &mut self,
45        fen: &str,
46        evaluation: f32,
47        best_moves: Vec<(ChessMove, f32)>,
48        name: String,
49        eco_code: Option<String>,
50    ) -> Result<(), String> {
51        // Validate FEN by parsing it
52        Board::from_str(fen).map_err(|_e| "Invalid FEN".to_string())?;
53
54        let entry = OpeningEntry {
55            evaluation,
56            best_moves,
57            name,
58            eco_code,
59        };
60
61        self.entries.insert(fen.to_string(), entry);
62        Ok(())
63    }
64
65    /// Look up position in opening book
66    pub fn lookup(&self, board: &Board) -> Option<&OpeningEntry> {
67        let fen = board.to_string();
68        self.entries.get(&fen)
69    }
70
71    /// Check if position is in opening book
72    pub fn contains(&self, board: &Board) -> bool {
73        let fen = board.to_string();
74        self.entries.contains_key(&fen)
75    }
76
77    /// Get all known openings
78    pub fn get_all_openings(&self) -> &HashMap<String, OpeningEntry> {
79        &self.entries
80    }
81
82    /// Get a random opening move from the starting position
83    pub fn get_random_opening(&self) -> Option<ChessMove> {
84        use rand::seq::SliceRandom;
85        let board = Board::default();
86        if let Some(entry) = self.lookup(&board) {
87            let moves: Vec<ChessMove> = entry.best_moves.iter().map(|(mv, _)| *mv).collect();
88            moves.choose(&mut rand::thread_rng()).copied()
89        } else {
90            None
91        }
92    }
93
94    /// Add standard chess openings
95    fn add_standard_openings(&mut self) {
96        // Starting position
97        if let Ok(board) =
98            Board::from_str("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
99        {
100            let moves = vec![
101                (ChessMove::from_str("e2e4").unwrap(), 1.0), // King's Pawn
102                (ChessMove::from_str("d2d4").unwrap(), 0.9), // Queen's Pawn
103                (ChessMove::from_str("g1f3").unwrap(), 0.8), // King's Knight
104                (ChessMove::from_str("c2c4").unwrap(), 0.7), // English Opening
105            ];
106            let _ = self.add_opening(
107                &board.to_string(),
108                0.0,
109                moves,
110                "Starting Position".to_string(),
111                None,
112            );
113        }
114
115        // King's Pawn Game: 1.e4
116        if let Ok(board) =
117            Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
118        {
119            let moves = vec![
120                (ChessMove::from_str("e7e5").unwrap(), 1.0), // King's Pawn response
121                (ChessMove::from_str("c7c5").unwrap(), 0.9), // Sicilian Defense
122                (ChessMove::from_str("e7e6").unwrap(), 0.7), // French Defense
123                (ChessMove::from_str("c7c6").unwrap(), 0.6), // Caro-Kann Defense
124            ];
125            let _ = self.add_opening(
126                &board.to_string(),
127                0.25,
128                moves,
129                "King's Pawn Game".to_string(),
130                Some("B00".to_string()),
131            );
132        }
133
134        // King's Pawn Game: 1.e4 e5
135        if let Ok(board) =
136            Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
137        {
138            let moves = vec![
139                (ChessMove::from_str("g1f3").unwrap(), 1.0), // King's Knight Attack
140                (ChessMove::from_str("f2f4").unwrap(), 0.6), // King's Gambit
141                (ChessMove::from_str("b1c3").unwrap(), 0.5), // Vienna Game
142            ];
143            let _ = self.add_opening(
144                &board.to_string(),
145                0.15,
146                moves,
147                "Open Game".to_string(),
148                Some("C20".to_string()),
149            );
150        }
151
152        // Italian Game: 1.e4 e5 2.Nf3 Nc6 3.Bc4
153        if let Ok(board) =
154            Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
155        {
156            let moves = vec![
157                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Italian Game main line
158                (ChessMove::from_str("f7f5").unwrap(), 0.6), // Rousseau Gambit
159                (ChessMove::from_str("f8e7").unwrap(), 0.4), // Hungarian Defense
160            ];
161            let _ = self.add_opening(
162                &board.to_string(),
163                0.25,
164                moves,
165                "Italian Game".to_string(),
166                Some("C50".to_string()),
167            );
168        }
169
170        // Ruy Lopez: 1.e4 e5 2.Nf3 Nc6 3.Bb5
171        if let Ok(board) =
172            Board::from_str("r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
173        {
174            let moves = vec![
175                (ChessMove::from_str("a7a6").unwrap(), 1.0), // Morphy Defense
176                (ChessMove::from_str("g8f6").unwrap(), 0.9), // Berlin Defense
177                (ChessMove::from_str("f7f5").unwrap(), 0.4), // Schliemann Defense
178                (ChessMove::from_str("b8d4").unwrap(), 0.3), // Bird Defense
179            ];
180            let _ = self.add_opening(
181                &board.to_string(),
182                0.3,
183                moves,
184                "Ruy Lopez".to_string(),
185                Some("C60".to_string()),
186            );
187        }
188
189        // Sicilian Defense: 1.e4 c5
190        if let Ok(board) =
191            Board::from_str("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
192        {
193            let moves = vec![
194                (ChessMove::from_str("g1f3").unwrap(), 1.0), // Open Sicilian
195                (ChessMove::from_str("b1c3").unwrap(), 0.7), // Closed Sicilian
196                (ChessMove::from_str("f2f4").unwrap(), 0.5), // Grand Prix Attack
197            ];
198            let _ = self.add_opening(
199                &board.to_string(),
200                0.3,
201                moves,
202                "Sicilian Defense".to_string(),
203                Some("B20".to_string()),
204            );
205        }
206
207        // French Defense: 1.e4 e6
208        if let Ok(board) =
209            Board::from_str("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
210        {
211            let moves = vec![
212                (ChessMove::from_str("d2d4").unwrap(), 1.0), // French Defense main line
213                (ChessMove::from_str("d2d3").unwrap(), 0.5), // King's Indian Attack
214                (ChessMove::from_str("g1f3").unwrap(), 0.6), // Two Knights Defense
215            ];
216            let _ = self.add_opening(
217                &board.to_string(),
218                0.35,
219                moves,
220                "French Defense".to_string(),
221                Some("C00".to_string()),
222            );
223        }
224
225        // Caro-Kann Defense: 1.e4 c6
226        if let Ok(board) =
227            Board::from_str("rnbqkbnr/pp1ppppp/2p5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
228        {
229            let moves = vec![
230                (ChessMove::from_str("d2d4").unwrap(), 1.0), // Caro-Kann main line
231                (ChessMove::from_str("b1c3").unwrap(), 0.7), // Two Knights Attack
232                (ChessMove::from_str("f2f4").unwrap(), 0.4), // Hillbilly Attack
233            ];
234            let _ = self.add_opening(
235                &board.to_string(),
236                0.3,
237                moves,
238                "Caro-Kann Defense".to_string(),
239                Some("B10".to_string()),
240            );
241        }
242
243        // Queen's Pawn Game: 1.d4
244        if let Ok(board) =
245            Board::from_str("rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1")
246        {
247            let moves = vec![
248                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Queen's Gambit
249                (ChessMove::from_str("g8f6").unwrap(), 0.9), // Indian Defenses
250                (ChessMove::from_str("f7f5").unwrap(), 0.4), // Dutch Defense
251            ];
252            let _ = self.add_opening(
253                &board.to_string(),
254                0.2,
255                moves,
256                "Queen's Pawn Game".to_string(),
257                Some("D00".to_string()),
258            );
259        }
260
261        // Queen's Gambit: 1.d4 d5 2.c4
262        if let Ok(board) =
263            Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq - 0 2")
264        {
265            let moves = vec![
266                (ChessMove::from_str("d5c4").unwrap(), 0.7), // Queen's Gambit Accepted
267                (ChessMove::from_str("e7e6").unwrap(), 1.0), // Queen's Gambit Declined
268                (ChessMove::from_str("c7c6").unwrap(), 0.8), // Slav Defense
269                (ChessMove::from_str("d5d4").unwrap(), 0.5), // Marshall Defense
270            ];
271            let _ = self.add_opening(
272                &board.to_string(),
273                0.25,
274                moves,
275                "Queen's Gambit".to_string(),
276                Some("D06".to_string()),
277            );
278        }
279
280        // English Opening: 1.c4
281        if let Ok(board) =
282            Board::from_str("rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq - 0 1")
283        {
284            let moves = vec![
285                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Anglo-Indian
286                (ChessMove::from_str("e7e5").unwrap(), 0.9), // Reversed Sicilian
287                (ChessMove::from_str("c7c5").unwrap(), 0.8), // Symmetrical English
288                (ChessMove::from_str("e7e6").unwrap(), 0.7), // Anglo-French
289            ];
290            let _ = self.add_opening(
291                &board.to_string(),
292                0.15,
293                moves,
294                "English Opening".to_string(),
295                Some("A10".to_string()),
296            );
297        }
298
299        // Nimzo-Indian Defense: 1.d4 Nf6 2.c4 e6 3.Nc3 Bb4
300        if let Ok(board) =
301            Board::from_str("rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 2 4")
302        {
303            let moves = vec![
304                (ChessMove::from_str("e2e3").unwrap(), 0.9), // Rubinstein System
305                (ChessMove::from_str("f2f3").unwrap(), 0.7), // Kmoch Variation
306                (ChessMove::from_str("a2a3").unwrap(), 0.8), // Saemisch Variation
307                (ChessMove::from_str("d1c2").unwrap(), 1.0), // Classical Variation
308            ];
309            let _ = self.add_opening(
310                &board.to_string(),
311                0.1,
312                moves,
313                "Nimzo-Indian Defense".to_string(),
314                Some("E20".to_string()),
315            );
316        }
317
318        // King's Indian Defense setup: 1.d4 Nf6 2.c4 g6
319        if let Ok(board) =
320            Board::from_str("rnbqkb1r/pppppp1p/5np1/8/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3")
321        {
322            let moves = vec![
323                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Classical setup
324                (ChessMove::from_str("g1f3").unwrap(), 0.9), // Fianchetto setup
325                (ChessMove::from_str("f2f3").unwrap(), 0.6), // Saemisch Variation
326            ];
327            let _ = self.add_opening(
328                &board.to_string(),
329                0.2,
330                moves,
331                "King's Indian Defense".to_string(),
332                Some("E60".to_string()),
333            );
334        }
335
336        // Sicilian Najdorf: 1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6
337        if let Ok(board) =
338            Board::from_str("rnbqkb1r/1p2pppp/p2p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 0 6")
339        {
340            let moves = vec![
341                (ChessMove::from_str("c1e3").unwrap(), 1.0), // English Attack
342                (ChessMove::from_str("f2f3").unwrap(), 0.9), // Be3 system
343                (ChessMove::from_str("h2h3").unwrap(), 0.7), // Positional setup
344            ];
345            let _ = self.add_opening(
346                &board.to_string(),
347                0.2,
348                moves,
349                "Sicilian Najdorf".to_string(),
350                Some("B90".to_string()),
351            );
352        }
353    }
354
355    /// Get opening book statistics for evaluation
356    pub fn get_statistics(&self) -> OpeningBookStats {
357        let total_openings = self.entries.len();
358        let eco_coverage = self
359            .entries
360            .values()
361            .filter(|entry| entry.eco_code.is_some())
362            .count();
363
364        OpeningBookStats {
365            total_openings,
366            eco_coverage,
367            avg_moves_per_opening: if total_openings > 0 {
368                self.entries
369                    .values()
370                    .map(|entry| entry.best_moves.len())
371                    .sum::<usize>() as f32
372                    / total_openings as f32
373            } else {
374                0.0
375            },
376        }
377    }
378}
379
380/// Statistics about the opening book coverage
381#[derive(Debug, Clone)]
382pub struct OpeningBookStats {
383    pub total_openings: usize,
384    pub eco_coverage: usize,
385    pub avg_moves_per_opening: f32,
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_opening_book_creation() {
394        let book = OpeningBook::new();
395        assert_eq!(book.entries.len(), 0);
396    }
397
398    #[test]
399    fn test_standard_openings() {
400        let book = OpeningBook::with_standard_openings();
401        assert!(!book.entries.is_empty());
402
403        // Test starting position lookup
404        let board = Board::default();
405        let entry = book.lookup(&board);
406        assert!(entry.is_some());
407        assert_eq!(entry.unwrap().name, "Starting Position");
408    }
409
410    #[test]
411    fn test_opening_lookup() {
412        let mut book = OpeningBook::new();
413        let board = Board::default();
414
415        // Should not be found initially
416        assert!(!book.contains(&board));
417
418        // Add entry
419        let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
420        book.add_opening(
421            &board.to_string(),
422            0.0,
423            moves,
424            "Test Opening".to_string(),
425            None,
426        )
427        .unwrap();
428
429        // Should be found now
430        assert!(book.contains(&board));
431        let entry = book.lookup(&board).unwrap();
432        assert_eq!(entry.name, "Test Opening");
433        assert_eq!(entry.best_moves.len(), 1);
434    }
435
436    #[test]
437    fn test_comprehensive_opening_coverage() {
438        let book = OpeningBook::with_standard_openings();
439        let stats = book.get_statistics();
440
441        // Should have substantial opening coverage
442        assert!(
443            stats.total_openings >= 12,
444            "Expected at least 12 openings, got {}",
445            stats.total_openings
446        );
447        assert!(
448            stats.eco_coverage >= 8,
449            "Expected at least 8 ECO codes, got {}",
450            stats.eco_coverage
451        );
452        assert!(
453            stats.avg_moves_per_opening >= 2.0,
454            "Expected average 2+ moves per opening"
455        );
456
457        // Test that major openings are covered
458        let opening_names: Vec<_> = book.entries.values().map(|entry| &entry.name).collect();
459
460        let has_sicilian = opening_names.iter().any(|name| name.contains("Sicilian"));
461        let has_italian = opening_names.iter().any(|name| name.contains("Italian"));
462        let has_ruy_lopez = opening_names.iter().any(|name| name.contains("Ruy Lopez"));
463        let has_french = opening_names.iter().any(|name| name.contains("French"));
464
465        assert!(has_sicilian, "Should have Sicilian Defense");
466        assert!(has_italian, "Should have Italian Game");
467        assert!(has_ruy_lopez, "Should have Ruy Lopez");
468        assert!(has_french, "Should have French Defense");
469    }
470}