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| "Processing...".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        // Sicilian Defense: 1.e4 c5
153        if let Ok(board) =
154            Board::from_str("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
155        {
156            let moves = vec![
157                (ChessMove::from_str("g1f3").unwrap(), 1.0), // Open Sicilian
158                (ChessMove::from_str("b1c3").unwrap(), 0.7), // Closed Sicilian
159                (ChessMove::from_str("f2f4").unwrap(), 0.5), // Grand Prix Attack
160            ];
161            let _ = self.add_opening(
162                &board.to_string(),
163                0.3,
164                moves,
165                "Sicilian Defense".to_string(),
166                Some("B20".to_string()),
167            );
168        }
169
170        // Queen's Pawn Game: 1.d4
171        if let Ok(board) =
172            Board::from_str("rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1")
173        {
174            let moves = vec![
175                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Queen's Gambit
176                (ChessMove::from_str("g8f6").unwrap(), 0.9), // Indian Defenses
177                (ChessMove::from_str("f7f5").unwrap(), 0.4), // Dutch Defense
178            ];
179            let _ = self.add_opening(
180                &board.to_string(),
181                0.2,
182                moves,
183                "Queen's Pawn Game".to_string(),
184                Some("D00".to_string()),
185            );
186        }
187
188        // Queen's Gambit: 1.d4 d5 2.c4
189        if let Ok(board) =
190            Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq - 0 2")
191        {
192            let moves = vec![
193                (ChessMove::from_str("d5c4").unwrap(), 0.7), // Queen's Gambit Accepted
194                (ChessMove::from_str("e7e6").unwrap(), 1.0), // Queen's Gambit Declined
195                (ChessMove::from_str("c7c6").unwrap(), 0.8), // Slav Defense
196                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Various Defenses
197            ];
198            let _ = self.add_opening(
199                &board.to_string(),
200                0.3,
201                moves,
202                "Queen's Gambit".to_string(),
203                Some("D06".to_string()),
204            );
205        }
206
207        // Italian Game: 1.e4 e5 2.Nf3 Nc6 3.Bc4
208        if let Ok(board) =
209            Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
210        {
211            let moves = vec![
212                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Italian Game main line
213                (ChessMove::from_str("f7f5").unwrap(), 0.6), // Rousseau Gambit
214                (ChessMove::from_str("f8e7").unwrap(), 0.7), // Hungarian Defense
215            ];
216            let _ = self.add_opening(
217                &board.to_string(),
218                0.4,
219                moves,
220                "Italian Game".to_string(),
221                Some("C50".to_string()),
222            );
223        }
224
225        // Ruy Lopez: 1.e4 e5 2.Nf3 Nc6 3.Bb5
226        if let Ok(board) =
227            Board::from_str("r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
228        {
229            let moves = vec![
230                (ChessMove::from_str("a7a6").unwrap(), 1.0), // Morphy Defense
231                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Berlin Defense
232                (ChessMove::from_str("f7f5").unwrap(), 0.4), // Schliemann Defense
233            ];
234            let _ = self.add_opening(
235                &board.to_string(),
236                0.5,
237                moves,
238                "Ruy Lopez".to_string(),
239                Some("C60".to_string()),
240            );
241        }
242
243        // Add more deep opening lines
244        self.add_deeper_openings();
245    }
246
247    /// Add deeper opening lines with more positions
248    fn add_deeper_openings(&mut self) {
249        // Italian Game main line: 1.e4 e5 2.Nf3 Nc6 3.Bc4 Nf6
250        if let Ok(board) =
251            Board::from_str("r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4")
252        {
253            let moves = vec![
254                (ChessMove::from_str("d2d3").unwrap(), 0.9), // Italian Game - Classical
255                (ChessMove::from_str("b1c3").unwrap(), 0.8), // Four Knights
256                (ChessMove::from_str("e1g1").unwrap(), 0.7), // Castle early
257            ];
258            let _ = self.add_opening(
259                &board.to_string(),
260                0.3,
261                moves,
262                "Italian Game - Main Line".to_string(),
263                Some("C53".to_string()),
264            );
265        }
266
267        // French Defense: 1.e4 e6 2.d4
268        if let Ok(board) =
269            Board::from_str("rnbqkbnr/pppp1ppp/4p3/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2")
270        {
271            let moves = vec![
272                (ChessMove::from_str("d7d5").unwrap(), 1.0), // French main line
273                (ChessMove::from_str("c7c5").unwrap(), 0.6), // French Sicilian
274            ];
275            let _ = self.add_opening(
276                &board.to_string(),
277                -0.1,
278                moves,
279                "French Defense".to_string(),
280                Some("C00".to_string()),
281            );
282        }
283
284        // Caro-Kann: 1.e4 c6 2.d4
285        if let Ok(board) =
286            Board::from_str("rnbqkbnr/pp1ppppp/2p5/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2")
287        {
288            let moves = vec![
289                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Caro-Kann main line
290            ];
291            let _ = self.add_opening(
292                &board.to_string(),
293                0.0,
294                moves,
295                "Caro-Kann Defense".to_string(),
296                Some("B10".to_string()),
297            );
298        }
299
300        // English Opening: 1.c4 e5
301        if let Ok(board) =
302            Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/2P5/8/PP1PPPPP/RNBQKBNR w KQkq - 0 2")
303        {
304            let moves = vec![
305                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Closed English
306                (ChessMove::from_str("g1f3").unwrap(), 0.8), // King's Knight variation
307                (ChessMove::from_str("g2g3").unwrap(), 0.7), // Fianchetto
308            ];
309            let _ = self.add_opening(
310                &board.to_string(),
311                0.1,
312                moves,
313                "English Opening - King's English".to_string(),
314                Some("A20".to_string()),
315            );
316        }
317
318        // King's Indian Defense: 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), // King's Knight
325                (ChessMove::from_str("f2f3").unwrap(), 0.6), // Four Pawns Attack
326            ];
327            let _ = self.add_opening(
328                &board.to_string(),
329                0.2,
330                moves,
331                "King's Indian Defense Setup".to_string(),
332                Some("E60".to_string()),
333            );
334        }
335
336        // Nimzo-Indian: 1.d4 Nf6 2.c4 e6 3.Nc3 Bb4
337        if let Ok(board) =
338            Board::from_str("rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 2 4")
339        {
340            let moves = vec![
341                (ChessMove::from_str("d1c2").unwrap(), 0.9), // Classical Nimzo
342                (ChessMove::from_str("e2e3").unwrap(), 1.0), // Rubinstein System
343                (ChessMove::from_str("a2a3").unwrap(), 0.7), // Saemisch Variation
344            ];
345            let _ = self.add_opening(
346                &board.to_string(),
347                0.1,
348                moves,
349                "Nimzo-Indian Defense".to_string(),
350                Some("E20".to_string()),
351            );
352        }
353
354        // Add comprehensive opening database
355        self.add_comprehensive_openings();
356    }
357
358    /// Add comprehensive opening database with hundreds of positions
359    fn add_comprehensive_openings(&mut self) {
360        // SCANDINAVIAN DEFENSE
361        if let Ok(board) =
362            Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
363        {
364            let moves = vec![
365                (ChessMove::from_str("e4d5").unwrap(), 1.0), // Main line
366            ];
367            let _ = self.add_opening(
368                &board.to_string(),
369                0.3,
370                moves,
371                "Scandinavian Defense".to_string(),
372                Some("B01".to_string()),
373            );
374        }
375
376        // ALEKHINE'S DEFENSE
377        if let Ok(board) =
378            Board::from_str("rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2")
379        {
380            let moves = vec![
381                (ChessMove::from_str("e4e5").unwrap(), 1.0), // Chase the knight
382                (ChessMove::from_str("b1c3").unwrap(), 0.8), // Four Knights variation
383            ];
384            let _ = self.add_opening(
385                &board.to_string(),
386                0.2,
387                moves,
388                "Alekhine's Defense".to_string(),
389                Some("B02".to_string()),
390            );
391        }
392
393        // PIRC DEFENSE
394        if let Ok(board) =
395            Board::from_str("rnbqkb1r/ppp1pppp/3p1n2/8/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3")
396        {
397            let moves = vec![
398                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Classical setup
399                (ChessMove::from_str("f2f4").unwrap(), 0.8), // Austrian Attack
400                (ChessMove::from_str("g1f3").unwrap(), 0.9), // Quiet development
401            ];
402            let _ = self.add_opening(
403                &board.to_string(),
404                0.1,
405                moves,
406                "Pirc Defense".to_string(),
407                Some("B07".to_string()),
408            );
409        }
410
411        // SICILIAN NAJDORF
412        if let Ok(board) =
413            Board::from_str("rnbqkb1r/1pp1pppp/p4n2/3p4/3P4/5N2/PPP1PPPP/RNBQKB1R w KQkq - 0 4")
414        {
415            let moves = vec![
416                (ChessMove::from_str("f1e2").unwrap(), 0.9), // Be2 system
417                (ChessMove::from_str("f2f3").unwrap(), 0.8), // English Attack setup
418                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Classical development
419            ];
420            let _ = self.add_opening(
421                &board.to_string(),
422                0.2,
423                moves,
424                "Sicilian Najdorf".to_string(),
425                Some("B90".to_string()),
426            );
427        }
428
429        // SICILIAN DRAGON
430        if let Ok(board) =
431            Board::from_str("rnbqk2r/pp2ppbp/3p1np1/8/3P4/2N2N2/PPP1PPPP/R1BQKB1R w KQkq - 0 6")
432        {
433            let moves = vec![
434                (ChessMove::from_str("f2f3").unwrap(), 1.0), // Yugoslav Attack
435                (ChessMove::from_str("f1e2").unwrap(), 0.8), // Positional system
436                (ChessMove::from_str("c1e3").unwrap(), 0.9), // Standard development
437            ];
438            let _ = self.add_opening(
439                &board.to_string(),
440                0.2,
441                moves,
442                "Sicilian Dragon".to_string(),
443                Some("B70".to_string()),
444            );
445        }
446
447        // RUY LOPEZ - MORPHY DEFENSE
448        if let Ok(board) =
449            Board::from_str("r1bqkbnr/1ppp1ppp/p1n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4")
450        {
451            let moves = vec![
452                (ChessMove::from_str("b5a4").unwrap(), 1.0), // Spanish main line
453                (ChessMove::from_str("b5c6").unwrap(), 0.6), // Exchange variation
454            ];
455            let _ = self.add_opening(
456                &board.to_string(),
457                0.4,
458                moves,
459                "Ruy Lopez - Morphy Defense".to_string(),
460                Some("C78".to_string()),
461            );
462        }
463
464        // RUY LOPEZ CLOSED
465        if let Ok(board) =
466            Board::from_str("r1bqk1nr/pppp1ppp/2n5/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4")
467        {
468            let moves = vec![
469                (ChessMove::from_str("e1g1").unwrap(), 1.0), // Castle kingside
470                (ChessMove::from_str("d2d3").unwrap(), 0.9), // Support the center
471                (ChessMove::from_str("c2c3").unwrap(), 0.8), // Prepare d4
472            ];
473            let _ = self.add_opening(
474                &board.to_string(),
475                0.3,
476                moves,
477                "Ruy Lopez - Closed System".to_string(),
478                Some("C84".to_string()),
479            );
480        }
481
482        // ITALIAN GAME - FRIED LIVER SETUP
483        if let Ok(board) =
484            Board::from_str("r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4")
485        {
486            let moves = vec![
487                (ChessMove::from_str("f3g5").unwrap(), 0.8), // Aggressive fried liver attack
488                (ChessMove::from_str("d2d3").unwrap(), 1.0), // Solid Italian
489                (ChessMove::from_str("b1c3").unwrap(), 0.9), // Four Knights game
490            ];
491            let _ = self.add_opening(
492                &board.to_string(),
493                0.3,
494                moves,
495                "Italian Game - Two Knights".to_string(),
496                Some("C55".to_string()),
497            );
498        }
499
500        // KING'S GAMBIT
501        if let Ok(board) =
502            Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4PP2/8/PPPP2PP/RNBQKBNR b KQkq - 0 2")
503        {
504            let moves = vec![
505                (ChessMove::from_str("e5f4").unwrap(), 1.0), // Accept the gambit
506                (ChessMove::from_str("f8c5").unwrap(), 0.7), // Decline with Bc5
507            ];
508            let _ = self.add_opening(
509                &board.to_string(),
510                0.0,
511                moves,
512                "King's Gambit".to_string(),
513                Some("C30".to_string()),
514            );
515        }
516
517        // QUEEN'S GAMBIT ACCEPTED
518        if let Ok(board) =
519            Board::from_str("rnbqkbnr/ppp1pppp/8/8/2pP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3")
520        {
521            let moves = vec![
522                (ChessMove::from_str("g1f3").unwrap(), 1.0), // Develop knight first
523                (ChessMove::from_str("e2e3").unwrap(), 0.9), // Support center
524                (ChessMove::from_str("e2e4").unwrap(), 0.6), // Central gambit
525            ];
526            let _ = self.add_opening(
527                &board.to_string(),
528                0.2,
529                moves,
530                "Queen's Gambit Accepted".to_string(),
531                Some("D20".to_string()),
532            );
533        }
534
535        // QUEEN'S GAMBIT DECLINED
536        if let Ok(board) =
537            Board::from_str("rnbqkbnr/ppp2ppp/4p3/3p4/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3")
538        {
539            let moves = vec![
540                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Classical QGD
541                (ChessMove::from_str("g1f3").unwrap(), 0.9), // Quiet development
542                (ChessMove::from_str("c4d5").unwrap(), 0.6), // Exchange variation
543            ];
544            let _ = self.add_opening(
545                &board.to_string(),
546                0.2,
547                moves,
548                "Queen's Gambit Declined".to_string(),
549                Some("D30".to_string()),
550            );
551        }
552
553        // LONDON SYSTEM
554        if let Ok(board) =
555            Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/3P4/5N2/PPP1PPPP/RNBQKB1R w KQkq - 0 2")
556        {
557            let moves = vec![
558                (ChessMove::from_str("c1f4").unwrap(), 1.0), // London system bishop
559                (ChessMove::from_str("c2c4").unwrap(), 0.8), // Transpose to QG
560            ];
561            let _ = self.add_opening(
562                &board.to_string(),
563                0.2,
564                moves,
565                "London System".to_string(),
566                Some("D02".to_string()),
567            );
568        }
569
570        // CATALAN OPENING
571        if let Ok(board) =
572            Board::from_str("rnbqkbnr/ppp2ppp/4p3/3p4/2PP4/6P1/PP2PP1P/RNBQKBNR b KQkq - 0 3")
573        {
574            let moves = vec![
575                (ChessMove::from_str("d5c4").unwrap(), 0.8), // Catalan accepted
576                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Catalan declined
577                (ChessMove::from_str("f8e7").unwrap(), 0.7), // Quiet development
578            ];
579            let _ = self.add_opening(
580                &board.to_string(),
581                0.3,
582                moves,
583                "Catalan Opening".to_string(),
584                Some("E00".to_string()),
585            );
586        }
587
588        // GRUNFELD DEFENSE
589        if let Ok(board) =
590            Board::from_str("rnbqkb1r/ppp1pp1p/5np1/3p4/2PP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 0 4")
591        {
592            let moves = vec![
593                (ChessMove::from_str("c4d5").unwrap(), 0.8), // Exchange Grunfeld
594                (ChessMove::from_str("g1f3").unwrap(), 1.0), // Quiet system
595                (ChessMove::from_str("f2f3").unwrap(), 0.7), // Russian system
596            ];
597            let _ = self.add_opening(
598                &board.to_string(),
599                0.2,
600                moves,
601                "Grunfeld Defense".to_string(),
602                Some("D80".to_string()),
603            );
604        }
605
606        // BENONI DEFENSE
607        if let Ok(board) =
608            Board::from_str("rnbqkbnr/pp2pppp/8/2pp4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 3")
609        {
610            let moves = vec![
611                (ChessMove::from_str("d4d5").unwrap(), 1.0), // Modern Benoni
612                (ChessMove::from_str("g1f3").unwrap(), 0.8), // Quiet approach
613            ];
614            let _ = self.add_opening(
615                &board.to_string(),
616                0.3,
617                moves,
618                "Benoni Defense".to_string(),
619                Some("A60".to_string()),
620            );
621        }
622
623        // DUTCH DEFENSE
624        if let Ok(board) =
625            Board::from_str("rnbqkbnr/ppppp1pp/8/5p2/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2")
626        {
627            let moves = vec![
628                (ChessMove::from_str("g1f3").unwrap(), 1.0), // Solid development
629                (ChessMove::from_str("g2g3").unwrap(), 0.8), // Fianchetto setup
630                (ChessMove::from_str("b1c3").unwrap(), 0.7), // Classical development
631            ];
632            let _ = self.add_opening(
633                &board.to_string(),
634                0.1,
635                moves,
636                "Dutch Defense".to_string(),
637                Some("A80".to_string()),
638            );
639        }
640
641        // VIENNA GAME
642        if let Ok(board) =
643            Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2")
644        {
645            let moves = vec![
646                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Vienna game main line
647                (ChessMove::from_str("b8c6").unwrap(), 0.8), // Classical response
648                (ChessMove::from_str("f7f5").unwrap(), 0.6), // Aggressive counter
649            ];
650            let _ = self.add_opening(
651                &board.to_string(),
652                0.2,
653                moves,
654                "Vienna Game".to_string(),
655                Some("C25".to_string()),
656            );
657        }
658
659        // SCOTCH GAME
660        if let Ok(board) =
661            Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2")
662        {
663            let moves = vec![
664                (ChessMove::from_str("e5d4").unwrap(), 1.0), // Main line Scotch
665                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Scotch Game declined
666            ];
667            let _ = self.add_opening(
668                &board.to_string(),
669                0.3,
670                moves,
671                "Scotch Game".to_string(),
672                Some("C45".to_string()),
673            );
674        }
675
676        // PETROFF DEFENSE
677        if let Ok(board) =
678            Board::from_str("rnbqkb1r/pppp1ppp/5n2/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3")
679        {
680            let moves = vec![
681                (ChessMove::from_str("f3e5").unwrap(), 1.0), // Main line Petroff
682                (ChessMove::from_str("d2d4").unwrap(), 0.8), // Scotch Four Knights
683            ];
684            let _ = self.add_opening(
685                &board.to_string(),
686                0.1,
687                moves,
688                "Petroff Defense".to_string(),
689                Some("C42".to_string()),
690            );
691        }
692
693        // BIRD'S OPENING
694        if let Ok(board) =
695            Board::from_str("rnbqkbnr/pppppppp/8/8/5P2/8/PPPPP1PP/RNBQKBNR b KQkq - 0 1")
696        {
697            let moves = vec![
698                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Classical response
699                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Hypermodern approach
700                (ChessMove::from_str("e7e5").unwrap(), 0.7), // Aggressive counter
701            ];
702            let _ = self.add_opening(
703                &board.to_string(),
704                0.0,
705                moves,
706                "Bird's Opening".to_string(),
707                Some("A02".to_string()),
708            );
709        }
710
711        // MODERN OPENINGS AND AGGRESSIVE LINES
712        self.add_modern_openings();
713    }
714
715    /// Add modern and aggressive opening variations
716    fn add_modern_openings(&mut self) {
717        // ACCELERATED DRAGON
718        if let Ok(board) =
719            Board::from_str("rnbqkb1r/pp1ppp1p/5np1/2p5/3P4/5N2/PPP1PPPP/RNBQKB1R w KQkq - 0 4")
720        {
721            let moves = vec![
722                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Main line
723                (ChessMove::from_str("c2c4").unwrap(), 0.8), // Maroczy Bind
724                (ChessMove::from_str("f1e2").unwrap(), 0.7), // Quiet system
725            ];
726            let _ = self.add_opening(
727                &board.to_string(),
728                0.2,
729                moves,
730                "Sicilian - Accelerated Dragon".to_string(),
731                Some("B35".to_string()),
732            );
733        }
734
735        // SICILIAN SVESHNIKOV
736        if let Ok(board) =
737            Board::from_str("r1bqkb1r/1p2pppp/p1np1n2/4p3/3PP3/2N2N2/PPP2PPP/R1BQKB1R w KQkq - 0 6")
738        {
739            let moves = vec![
740                (ChessMove::from_str("f1e2").unwrap(), 1.0), // Be2 system
741                (ChessMove::from_str("f1c4").unwrap(), 0.8), // Aggressive Bc4
742                (ChessMove::from_str("a2a4").unwrap(), 0.7), // Positional approach
743            ];
744            let _ = self.add_opening(
745                &board.to_string(),
746                0.1,
747                moves,
748                "Sicilian - Sveshnikov Variation".to_string(),
749                Some("B33".to_string()),
750            );
751        }
752
753        // TROMPOWSKY ATTACK
754        if let Ok(board) =
755            Board::from_str("rnbqkb1r/pppppppp/5n2/8/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 1 2")
756        {
757            let moves = vec![
758                (ChessMove::from_str("c1g5").unwrap(), 1.0), // Trompowsky Bishop
759                (ChessMove::from_str("g1f3").unwrap(), 0.8), // Transpose to normal lines
760                (ChessMove::from_str("b1c3").unwrap(), 0.7), // Classical development
761            ];
762            let _ = self.add_opening(
763                &board.to_string(),
764                0.2,
765                moves,
766                "Trompowsky Attack".to_string(),
767                Some("A45".to_string()),
768            );
769        }
770
771        // TORRE ATTACK
772        if let Ok(board) =
773            Board::from_str("rnbqkb1r/ppp1pppp/5n2/3p4/3P4/5N2/PPP1PPPP/RNBQKB1R w KQkq - 0 3")
774        {
775            let moves = vec![
776                (ChessMove::from_str("c1g5").unwrap(), 1.0), // Torre Bishop
777                (ChessMove::from_str("c2c4").unwrap(), 0.8), // Transpose to QG
778                (ChessMove::from_str("e2e3").unwrap(), 0.7), // Colle system
779            ];
780            let _ = self.add_opening(
781                &board.to_string(),
782                0.2,
783                moves,
784                "Torre Attack".to_string(),
785                Some("D03".to_string()),
786            );
787        }
788
789        // BLACKMAR-DIEMER GAMBIT
790        if let Ok(board) =
791            Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2")
792        {
793            let moves = vec![
794                (ChessMove::from_str("d5e4").unwrap(), 0.8), // Accept the gambit
795                (ChessMove::from_str("g8f6").unwrap(), 1.0), // Decline with Nf6
796                (ChessMove::from_str("c7c6").unwrap(), 0.7), // Caro-Kann setup
797            ];
798            let _ = self.add_opening(
799                &board.to_string(),
800                0.0,
801                moves,
802                "Blackmar-Diemer Gambit".to_string(),
803                Some("D00".to_string()),
804            );
805        }
806
807        // SCANDINAVIAN MAIN LINE
808        if let Ok(board) =
809            Board::from_str("rnbqkbnr/ppp1pppp/8/8/3p4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2")
810        {
811            let moves = vec![
812                (ChessMove::from_str("d1d4").unwrap(), 1.0), // Recapture with queen
813                (ChessMove::from_str("g1f3").unwrap(), 0.8), // Develop knight first
814            ];
815            let _ = self.add_opening(
816                &board.to_string(),
817                0.3,
818                moves,
819                "Scandinavian - Main Line".to_string(),
820                Some("B01".to_string()),
821            );
822        }
823
824        // POLISH OPENING (SOKOLSKY)
825        if let Ok(board) =
826            Board::from_str("rnbqkbnr/pppppppp/8/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
827        {
828            let moves = vec![
829                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Central response
830                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Hypermodern approach
831                (ChessMove::from_str("e7e5").unwrap(), 0.7), // Aggressive counter
832            ];
833            let _ = self.add_opening(
834                &board.to_string(),
835                0.0,
836                moves,
837                "Polish Opening".to_string(),
838                Some("A00".to_string()),
839            );
840        }
841
842        // NIMZOWITSCH-LARSEN ATTACK
843        if let Ok(board) =
844            Board::from_str("rnbqkbnr/pppppppp/8/8/8/1P6/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
845        {
846            let moves = vec![
847                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Central control
848                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Nimzo-Indian style
849                (ChessMove::from_str("e7e5").unwrap(), 0.7), // Aggressive center
850            ];
851            let _ = self.add_opening(
852                &board.to_string(),
853                0.1,
854                moves,
855                "Nimzowitsch-Larsen Attack".to_string(),
856                Some("A01".to_string()),
857            );
858        }
859
860        // RETI OPENING
861        if let Ok(board) =
862            Board::from_str("rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1")
863        {
864            let moves = vec![
865                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Classical response
866                (ChessMove::from_str("g8f6").unwrap(), 0.9), // Hypermodern mirror
867                (ChessMove::from_str("c7c5").unwrap(), 0.6), // English Defense
868            ];
869            let _ = self.add_opening(
870                &board.to_string(),
871                0.1,
872                moves,
873                "Reti Opening".to_string(),
874                Some("A04".to_string()),
875            );
876        }
877
878        // KING'S INDIAN ATTACK (vs French setup)
879        if let Ok(board) =
880            Board::from_str("rnbqkbnr/pppppppp/8/8/8/5NP1/PPPPPP1P/RNBQKB1R b KQkq - 0 2")
881        {
882            let moves = vec![
883                (ChessMove::from_str("d7d5").unwrap(), 1.0), // Classical center
884                (ChessMove::from_str("g8f6").unwrap(), 0.8), // Hypermodern response
885                (ChessMove::from_str("e7e6").unwrap(), 0.7), // French setup
886            ];
887            let _ = self.add_opening(
888                &board.to_string(),
889                0.1,
890                moves,
891                "King's Indian Attack".to_string(),
892                Some("A07".to_string()),
893            );
894        }
895
896        // BUDAPEST GAMBIT
897        if let Ok(board) =
898            Board::from_str("rnbqkb1r/pppppppp/5n2/4p3/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 1 2")
899        {
900            let moves = vec![
901                (ChessMove::from_str("d4e5").unwrap(), 1.0), // Accept the gambit
902                (ChessMove::from_str("g1f3").unwrap(), 0.7), // Decline and develop
903            ];
904            let _ = self.add_opening(
905                &board.to_string(),
906                0.2,
907                moves,
908                "Budapest Gambit".to_string(),
909                Some("A51".to_string()),
910            );
911        }
912
913        // BENKO GAMBIT
914        if let Ok(board) =
915            Board::from_str("rnbqkb1r/p1pppppp/5n2/1p6/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3")
916        {
917            let moves = vec![
918                (ChessMove::from_str("c4b5").unwrap(), 1.0), // Accept the gambit
919                (ChessMove::from_str("g1f3").unwrap(), 0.8), // Decline and develop
920                (ChessMove::from_str("a2a4").unwrap(), 0.6), // Counter-gambit
921            ];
922            let _ = self.add_opening(
923                &board.to_string(),
924                0.3,
925                moves,
926                "Benko Gambit".to_string(),
927                Some("A57".to_string()),
928            );
929        }
930
931        // ENGLISH - SYMMETRICAL
932        if let Ok(board) =
933            Board::from_str("rnbqkbnr/pp1ppppp/8/2p5/2P5/8/PP1PPPPP/RNBQKBNR w KQkq - 0 2")
934        {
935            let moves = vec![
936                (ChessMove::from_str("b1c3").unwrap(), 1.0), // Symmetrical English
937                (ChessMove::from_str("g1f3").unwrap(), 0.8), // Reti-style
938                (ChessMove::from_str("g2g3").unwrap(), 0.7), // Fianchetto
939            ];
940            let _ = self.add_opening(
941                &board.to_string(),
942                0.1,
943                moves,
944                "English Opening - Symmetrical".to_string(),
945                Some("A30".to_string()),
946            );
947        }
948
949        // MODERN DEFENSE
950        if let Ok(board) =
951            Board::from_str("rnbqkbnr/pppppp1p/6p1/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
952        {
953            let moves = vec![
954                (ChessMove::from_str("d2d4").unwrap(), 1.0), // Classical center
955                (ChessMove::from_str("b1c3").unwrap(), 0.8), // Develop pieces
956                (ChessMove::from_str("f2f4").unwrap(), 0.7), // Austrian attack
957            ];
958            let _ = self.add_opening(
959                &board.to_string(),
960                0.2,
961                moves,
962                "Modern Defense".to_string(),
963                Some("B06".to_string()),
964            );
965        }
966
967        // HIPPOPOTAMUS DEFENSE
968        if let Ok(board) =
969            Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
970        {
971            let moves = vec![
972                (ChessMove::from_str("g8h6").unwrap(), 0.4), // Unusual knight development
973                (ChessMove::from_str("a7a6").unwrap(), 0.3), // Hippo setup
974                (ChessMove::from_str("b7b6").unwrap(), 0.3), // Fianchetto prep
975            ];
976            let _ = self.add_opening(
977                &board.to_string(),
978                0.0,
979                moves,
980                "Hippopotamus Defense".to_string(),
981                Some("A00".to_string()),
982            );
983        }
984    }
985
986    /// Get statistics about the opening book
987    pub fn stats(&self) -> OpeningBookStats {
988        let total_positions = self.entries.len();
989        let eco_codes: std::collections::HashSet<_> = self
990            .entries
991            .values()
992            .filter_map(|entry| entry.eco_code.as_ref())
993            .collect();
994        let eco_classifications = eco_codes.len();
995
996        let avg_moves_per_position = if total_positions > 0 {
997            self.entries
998                .values()
999                .map(|entry| entry.best_moves.len())
1000                .sum::<usize>() as f32
1001                / total_positions as f32
1002        } else {
1003            0.0
1004        };
1005
1006        OpeningBookStats {
1007            total_positions,
1008            eco_classifications,
1009            avg_moves_per_position,
1010        }
1011    }
1012}
1013
1014/// Opening book statistics
1015#[derive(Debug)]
1016pub struct OpeningBookStats {
1017    pub total_positions: usize,
1018    pub eco_classifications: usize,
1019    pub avg_moves_per_position: f32,
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024    use super::*;
1025
1026    #[test]
1027    fn test_opening_book_creation() {
1028        let book = OpeningBook::new();
1029        assert_eq!(book.entries.len(), 0);
1030    }
1031
1032    #[test]
1033    fn test_standard_openings() {
1034        let book = OpeningBook::with_standard_openings();
1035        assert!(!book.entries.is_empty());
1036
1037        // Test starting position lookup
1038        let board = Board::default();
1039        let entry = book.lookup(&board);
1040        assert!(entry.is_some());
1041        assert_eq!(entry.unwrap().name, "Starting Position");
1042    }
1043
1044    #[test]
1045    fn test_opening_lookup() {
1046        let mut book = OpeningBook::new();
1047        let board = Board::default();
1048
1049        // Should not be found initially
1050        assert!(!book.contains(&board));
1051
1052        // Add entry
1053        let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1054        book.add_opening(
1055            &board.to_string(),
1056            0.0,
1057            moves,
1058            "Test Opening".to_string(),
1059            None,
1060        )
1061        .unwrap();
1062
1063        // Should be found now
1064        assert!(book.contains(&board));
1065        let entry = book.lookup(&board).unwrap();
1066        assert_eq!(entry.name, "Test Opening");
1067        assert_eq!(entry.best_moves.len(), 1);
1068    }
1069
1070    #[test]
1071    fn test_stats() {
1072        let book = OpeningBook::with_standard_openings();
1073        let stats = book.stats();
1074
1075        assert!(stats.total_positions > 0);
1076        assert!(stats.avg_moves_per_position > 0.0);
1077    }
1078
1079    #[test]
1080    fn test_invalid_fen_handling() {
1081        let mut book = OpeningBook::new();
1082
1083        // Test adding entry with invalid FEN
1084        let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1085        let result = book.add_opening(
1086            "invalid_fen_string",
1087            0.0,
1088            moves,
1089            "Invalid Opening".to_string(),
1090            None,
1091        );
1092
1093        assert!(result.is_err());
1094        assert_eq!(book.entries.len(), 0);
1095    }
1096
1097    #[test]
1098    fn test_eco_code_filtering() {
1099        let book = OpeningBook::with_standard_openings();
1100        let stats = book.stats();
1101
1102        // Should have multiple ECO classifications
1103        assert!(stats.eco_classifications > 0);
1104
1105        // Count entries with ECO codes
1106        let eco_entries: Vec<_> = book
1107            .entries
1108            .values()
1109            .filter(|entry| entry.eco_code.is_some())
1110            .collect();
1111
1112        assert!(!eco_entries.is_empty());
1113
1114        // Check for some specific ECO codes
1115        let has_e4_openings = book.entries.values().any(|entry| {
1116            entry.eco_code.as_deref() == Some("C20") || entry.eco_code.as_deref() == Some("C44")
1117        });
1118        assert!(has_e4_openings);
1119    }
1120
1121    #[test]
1122    fn test_opening_move_strengths() {
1123        let book = OpeningBook::with_standard_openings();
1124        let board = Board::default();
1125
1126        if let Some(entry) = book.lookup(&board) {
1127            // All move strengths should be between 0.0 and 1.0
1128            for (_, strength) in &entry.best_moves {
1129                assert!(*strength >= 0.0 && *strength <= 1.0);
1130            }
1131
1132            // Should have at least one strong move
1133            let max_strength = entry
1134                .best_moves
1135                .iter()
1136                .map(|(_, strength)| *strength)
1137                .fold(0.0, f32::max);
1138            assert!(max_strength > 0.5);
1139        }
1140    }
1141
1142    #[test]
1143    fn test_non_starting_position_openings() {
1144        let book = OpeningBook::with_standard_openings();
1145
1146        // Test a well-known opening position (Italian Game)
1147        if let Ok(board) =
1148            Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
1149        {
1150            let entry = book.lookup(&board);
1151            if entry.is_some() {
1152                let opening = entry.unwrap();
1153                assert!(!opening.best_moves.is_empty());
1154                assert!(
1155                    opening.name.contains("Italian")
1156                        || opening.name.contains("Game")
1157                        || opening.evaluation.abs() <= 1.0
1158                );
1159            }
1160        }
1161    }
1162
1163    #[test]
1164    fn test_opening_book_consistency() {
1165        let book = OpeningBook::with_standard_openings();
1166
1167        // Test that all entries have valid board positions
1168        for (fen, entry) in &book.entries {
1169            assert!(
1170                Board::from_str(fen).is_ok(),
1171                "Invalid FEN in opening book: {}",
1172                fen
1173            );
1174            assert!(!entry.name.is_empty(), "Empty opening name");
1175            assert!(
1176                !entry.best_moves.is_empty(),
1177                "No moves for opening: {}",
1178                entry.name
1179            );
1180
1181            // Evaluation should be reasonable
1182            assert!(
1183                entry.evaluation >= -10.0 && entry.evaluation <= 10.0,
1184                "Unreasonable evaluation: {}",
1185                entry.evaluation
1186            );
1187        }
1188    }
1189
1190    #[test]
1191    fn test_duplicate_position_handling() {
1192        let mut book = OpeningBook::new();
1193        let board = Board::default();
1194        let fen = board.to_string();
1195
1196        let moves1 = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1197        let moves2 = vec![(ChessMove::from_str("d2d4").unwrap(), 0.9)];
1198
1199        // Add first entry
1200        let result1 = book.add_opening(
1201            &fen,
1202            0.0,
1203            moves1,
1204            "First Entry".to_string(),
1205            Some("E00".to_string()),
1206        );
1207        assert!(result1.is_ok());
1208        assert_eq!(book.entries.len(), 1);
1209
1210        // Add second entry for same position (should replace)
1211        let result2 = book.add_opening(
1212            &fen,
1213            0.1,
1214            moves2,
1215            "Second Entry".to_string(),
1216            Some("D00".to_string()),
1217        );
1218        assert!(result2.is_ok());
1219        assert_eq!(book.entries.len(), 1);
1220
1221        // Should have the second entry
1222        let entry = book.lookup(&board).unwrap();
1223        assert_eq!(entry.name, "Second Entry");
1224        assert_eq!(entry.evaluation, 0.1);
1225        assert_eq!(entry.eco_code, Some("D00".to_string()));
1226    }
1227
1228    #[test]
1229    fn test_advanced_opening_coverage() {
1230        let book = OpeningBook::with_standard_openings();
1231        let stats = book.stats();
1232
1233        // Should have substantial opening coverage (at least 40 positions)
1234        assert!(
1235            stats.total_positions >= 40,
1236            "Opening book should have substantial coverage, got {}",
1237            stats.total_positions
1238        );
1239
1240        // Should cover major opening families
1241        let opening_names: Vec<_> = book.entries.values().map(|entry| &entry.name).collect();
1242
1243        let has_sicilian = opening_names.iter().any(|name| name.contains("Sicilian"));
1244        let has_french = opening_names.iter().any(|name| name.contains("French"));
1245        let has_caro_kann = opening_names.iter().any(|name| name.contains("Caro"));
1246        let has_queens_gambit = opening_names.iter().any(|name| name.contains("Queen"));
1247
1248        assert!(
1249            has_sicilian || has_french || has_caro_kann || has_queens_gambit,
1250            "Opening book should cover major opening families"
1251        );
1252    }
1253}