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 }
970
971 pub fn stats(&self) -> OpeningBookStats {
973 let total_positions = self.entries.len();
974 let eco_codes: std::collections::HashSet<_> = self
975 .entries
976 .values()
977 .filter_map(|entry| entry.eco_code.as_ref())
978 .collect();
979 let eco_classifications = eco_codes.len();
980
981 let avg_moves_per_position = if total_positions > 0 {
982 self.entries
983 .values()
984 .map(|entry| entry.best_moves.len())
985 .sum::<usize>() as f32
986 / total_positions as f32
987 } else {
988 0.0
989 };
990
991 OpeningBookStats {
992 total_positions,
993 eco_classifications,
994 avg_moves_per_position,
995 }
996 }
997}
998
999#[derive(Debug)]
1001pub struct OpeningBookStats {
1002 pub total_positions: usize,
1003 pub eco_classifications: usize,
1004 pub avg_moves_per_position: f32,
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010
1011 #[test]
1012 fn test_opening_book_creation() {
1013 let book = OpeningBook::new();
1014 assert_eq!(book.entries.len(), 0);
1015 }
1016
1017 #[test]
1018 fn test_standard_openings() {
1019 let book = OpeningBook::with_standard_openings();
1020 assert!(!book.entries.is_empty());
1021
1022 let board = Board::default();
1024 let entry = book.lookup(&board);
1025 assert!(entry.is_some());
1026 assert_eq!(entry.unwrap().name, "Starting Position");
1027 }
1028
1029 #[test]
1030 fn test_opening_lookup() {
1031 let mut book = OpeningBook::new();
1032 let board = Board::default();
1033
1034 assert!(!book.contains(&board));
1036
1037 let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1039 book.add_opening(
1040 &board.to_string(),
1041 0.0,
1042 moves,
1043 "Test Opening".to_string(),
1044 None,
1045 )
1046 .unwrap();
1047
1048 assert!(book.contains(&board));
1050 let entry = book.lookup(&board).unwrap();
1051 assert_eq!(entry.name, "Test Opening");
1052 assert_eq!(entry.best_moves.len(), 1);
1053 }
1054
1055 #[test]
1056 fn test_stats() {
1057 let book = OpeningBook::with_standard_openings();
1058 let stats = book.stats();
1059
1060 assert!(stats.total_positions > 0);
1061 assert!(stats.avg_moves_per_position > 0.0);
1062 }
1063
1064 #[test]
1065 fn test_invalid_fen_handling() {
1066 let mut book = OpeningBook::new();
1067
1068 let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1070 let result = book.add_opening(
1071 "invalid_fen_string",
1072 0.0,
1073 moves,
1074 "Invalid Opening".to_string(),
1075 None,
1076 );
1077
1078 assert!(result.is_err());
1079 assert_eq!(book.entries.len(), 0);
1080 }
1081
1082 #[test]
1083 fn test_eco_code_filtering() {
1084 let book = OpeningBook::with_standard_openings();
1085 let stats = book.stats();
1086
1087 assert!(stats.eco_classifications > 0);
1089
1090 let eco_entries: Vec<_> = book
1092 .entries
1093 .values()
1094 .filter(|entry| entry.eco_code.is_some())
1095 .collect();
1096
1097 assert!(!eco_entries.is_empty());
1098
1099 let has_e4_openings = book.entries.values().any(|entry| {
1101 entry.eco_code.as_deref() == Some("C20") || entry.eco_code.as_deref() == Some("C44")
1102 });
1103 assert!(has_e4_openings);
1104 }
1105
1106 #[test]
1107 fn test_opening_move_strengths() {
1108 let book = OpeningBook::with_standard_openings();
1109 let board = Board::default();
1110
1111 if let Some(entry) = book.lookup(&board) {
1112 for (_, strength) in &entry.best_moves {
1114 assert!(*strength >= 0.0 && *strength <= 1.0);
1115 }
1116
1117 let max_strength = entry
1119 .best_moves
1120 .iter()
1121 .map(|(_, strength)| *strength)
1122 .fold(0.0, f32::max);
1123 assert!(max_strength > 0.5);
1124 }
1125 }
1126
1127 #[test]
1128 fn test_non_starting_position_openings() {
1129 let book = OpeningBook::with_standard_openings();
1130
1131 if let Ok(board) =
1133 Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
1134 {
1135 let entry = book.lookup(&board);
1136 if entry.is_some() {
1137 let opening = entry.unwrap();
1138 assert!(!opening.best_moves.is_empty());
1139 assert!(
1140 opening.name.contains("Italian")
1141 || opening.name.contains("Game")
1142 || opening.evaluation.abs() <= 1.0
1143 );
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn test_opening_book_consistency() {
1150 let book = OpeningBook::with_standard_openings();
1151
1152 for (fen, entry) in &book.entries {
1154 assert!(
1155 Board::from_str(fen).is_ok(),
1156 "Invalid FEN in opening book: {fen}",
1157 );
1158 assert!(!entry.name.is_empty(), "Empty opening name");
1159 assert!(
1160 !entry.best_moves.is_empty(),
1161 "No moves for opening: {}",
1162 entry.name
1163 );
1164
1165 assert!(
1167 entry.evaluation >= -10.0 && entry.evaluation <= 10.0,
1168 "Unreasonable evaluation: {}",
1169 entry.evaluation
1170 );
1171 }
1172 }
1173
1174 #[test]
1175 fn test_duplicate_position_handling() {
1176 let mut book = OpeningBook::new();
1177 let board = Board::default();
1178 let fen = board.to_string();
1179
1180 let moves1 = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
1181 let moves2 = vec![(ChessMove::from_str("d2d4").unwrap(), 0.9)];
1182
1183 let result1 = book.add_opening(
1185 &fen,
1186 0.0,
1187 moves1,
1188 "First Entry".to_string(),
1189 Some("E00".to_string()),
1190 );
1191 assert!(result1.is_ok());
1192 assert_eq!(book.entries.len(), 1);
1193
1194 let result2 = book.add_opening(
1196 &fen,
1197 0.1,
1198 moves2,
1199 "Second Entry".to_string(),
1200 Some("D00".to_string()),
1201 );
1202 assert!(result2.is_ok());
1203 assert_eq!(book.entries.len(), 1);
1204
1205 let entry = book.lookup(&board).unwrap();
1207 assert_eq!(entry.name, "Second Entry");
1208 assert_eq!(entry.evaluation, 0.1);
1209 assert_eq!(entry.eco_code, Some("D00".to_string()));
1210 }
1211
1212 #[test]
1213 fn test_advanced_opening_coverage() {
1214 let book = OpeningBook::with_standard_openings();
1215 let stats = book.stats();
1216
1217 assert!(
1219 stats.total_positions >= 40,
1220 "Opening book should have substantial coverage, got {}",
1221 stats.total_positions
1222 );
1223
1224 let opening_names: Vec<_> = book.entries.values().map(|entry| &entry.name).collect();
1226
1227 let has_sicilian = opening_names.iter().any(|name| name.contains("Sicilian"));
1228 let has_french = opening_names.iter().any(|name| name.contains("French"));
1229 let has_caro_kann = opening_names.iter().any(|name| name.contains("Caro"));
1230 let has_queens_gambit = opening_names.iter().any(|name| name.contains("Queen"));
1231
1232 assert!(
1233 has_sicilian || has_french || has_caro_kann || has_queens_gambit,
1234 "Opening book should cover major opening families"
1235 );
1236 }
1237}