1use chess::{Board, ChessMove};
2use std::collections::HashMap;
3use std::str::FromStr;
4
5#[derive(Debug, Clone)]
7pub struct OpeningEntry {
8 pub evaluation: f32,
9 pub best_moves: Vec<(ChessMove, f32)>, pub name: String,
11 pub eco_code: Option<String>, }
13
14#[derive(Clone)]
16pub struct OpeningBook {
17 entries: HashMap<String, OpeningEntry>,
19}
20
21impl Default for OpeningBook {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl OpeningBook {
28 pub fn new() -> Self {
30 Self {
31 entries: HashMap::new(),
32 }
33 }
34
35 pub fn with_standard_openings() -> Self {
37 let mut book = Self::new();
38 book.add_standard_openings();
39 book
40 }
41
42 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 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 pub fn lookup(&self, board: &Board) -> Option<&OpeningEntry> {
67 let fen = board.to_string();
68 self.entries.get(&fen)
69 }
70
71 pub fn contains(&self, board: &Board) -> bool {
73 let fen = board.to_string();
74 self.entries.contains_key(&fen)
75 }
76
77 pub fn get_all_openings(&self) -> &HashMap<String, OpeningEntry> {
79 &self.entries
80 }
81
82 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 fn add_standard_openings(&mut self) {
96 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), (ChessMove::from_str("d2d4").unwrap(), 0.9), (ChessMove::from_str("g1f3").unwrap(), 0.8), (ChessMove::from_str("c2c4").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("c7c5").unwrap(), 0.9), (ChessMove::from_str("e7e6").unwrap(), 0.7), (ChessMove::from_str("c7c6").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("f2f4").unwrap(), 0.6), (ChessMove::from_str("b1c3").unwrap(), 0.5), ];
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 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), (ChessMove::from_str("b1c3").unwrap(), 0.7), (ChessMove::from_str("f2f4").unwrap(), 0.5), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.9), (ChessMove::from_str("f7f5").unwrap(), 0.4), ];
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 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), (ChessMove::from_str("e7e6").unwrap(), 1.0), (ChessMove::from_str("c7c6").unwrap(), 0.8), (ChessMove::from_str("g8f6").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("f7f5").unwrap(), 0.6), (ChessMove::from_str("f8e7").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), (ChessMove::from_str("f7f5").unwrap(), 0.4), ];
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 self.add_deeper_openings();
245 }
246
247 fn add_deeper_openings(&mut self) {
249 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), (ChessMove::from_str("b1c3").unwrap(), 0.8), (ChessMove::from_str("e1g1").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("c7c5").unwrap(), 0.6), ];
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 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), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), (ChessMove::from_str("g2g3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.9), (ChessMove::from_str("f2f3").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("e2e3").unwrap(), 1.0), (ChessMove::from_str("a2a3").unwrap(), 0.7), ];
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 self.add_comprehensive_openings();
356 }
357
358 fn add_comprehensive_openings(&mut self) {
360 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), ];
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 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), (ChessMove::from_str("b1c3").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("f2f4").unwrap(), 0.8), (ChessMove::from_str("g1f3").unwrap(), 0.9), ];
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 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), (ChessMove::from_str("f2f3").unwrap(), 0.8), (ChessMove::from_str("b1c3").unwrap(), 1.0), ];
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 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), (ChessMove::from_str("f1e2").unwrap(), 0.8), (ChessMove::from_str("c1e3").unwrap(), 0.9), ];
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 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), (ChessMove::from_str("b5c6").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("d2d3").unwrap(), 0.9), (ChessMove::from_str("c2c3").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("d2d3").unwrap(), 1.0), (ChessMove::from_str("b1c3").unwrap(), 0.9), ];
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 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), (ChessMove::from_str("f8c5").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("e2e3").unwrap(), 0.9), (ChessMove::from_str("e2e4").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.9), (ChessMove::from_str("c4d5").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("c2c4").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 1.0), (ChessMove::from_str("f8e7").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 1.0), (ChessMove::from_str("f2f3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("g2g3").unwrap(), 0.8), (ChessMove::from_str("b1c3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("b8c6").unwrap(), 0.8), (ChessMove::from_str("f7f5").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("d2d4").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), (ChessMove::from_str("e7e5").unwrap(), 0.7), ];
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 self.add_modern_openings();
713 }
714
715 fn add_modern_openings(&mut self) {
717 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), (ChessMove::from_str("c2c4").unwrap(), 0.8), (ChessMove::from_str("f1e2").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("f1c4").unwrap(), 0.8), (ChessMove::from_str("a2a4").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), (ChessMove::from_str("b1c3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("c2c4").unwrap(), 0.8), (ChessMove::from_str("e2e3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 1.0), (ChessMove::from_str("c7c6").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), (ChessMove::from_str("e7e5").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), (ChessMove::from_str("e7e5").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.9), (ChessMove::from_str("c7c5").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("g8f6").unwrap(), 0.8), (ChessMove::from_str("e7e6").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), (ChessMove::from_str("a2a4").unwrap(), 0.6), ];
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 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), (ChessMove::from_str("g1f3").unwrap(), 0.8), (ChessMove::from_str("g2g3").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("b1c3").unwrap(), 0.8), (ChessMove::from_str("f2f4").unwrap(), 0.7), ];
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 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), (ChessMove::from_str("a7a6").unwrap(), 0.3), (ChessMove::from_str("b7b6").unwrap(), 0.3), ];
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 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#[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 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 assert!(!book.contains(&board));
1051
1052 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 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 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 assert!(stats.eco_classifications > 0);
1104
1105 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 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 for (_, strength) in &entry.best_moves {
1129 assert!(*strength >= 0.0 && *strength <= 1.0);
1130 }
1131
1132 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 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 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 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 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 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 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 assert!(
1235 stats.total_positions >= 40,
1236 "Opening book should have substantial coverage, got {}",
1237 stats.total_positions
1238 );
1239
1240 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}