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| "Invalid FEN".to_string())?;
53
54 let entry = OpeningEntry {
55 evaluation,
56 best_moves,
57 name,
58 eco_code,
59 };
60
61 self.entries.insert(fen.to_string(), entry);
62 Ok(())
63 }
64
65 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("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
155 {
156 let moves = vec![
157 (ChessMove::from_str("g8f6").unwrap(), 1.0), (ChessMove::from_str("f7f5").unwrap(), 0.6), (ChessMove::from_str("f8e7").unwrap(), 0.4), ];
161 let _ = self.add_opening(
162 &board.to_string(),
163 0.25,
164 moves,
165 "Italian Game".to_string(),
166 Some("C50".to_string()),
167 );
168 }
169
170 if let Ok(board) =
172 Board::from_str("r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
173 {
174 let moves = vec![
175 (ChessMove::from_str("a7a6").unwrap(), 1.0), (ChessMove::from_str("g8f6").unwrap(), 0.9), (ChessMove::from_str("f7f5").unwrap(), 0.4), (ChessMove::from_str("b8d4").unwrap(), 0.3), ];
180 let _ = self.add_opening(
181 &board.to_string(),
182 0.3,
183 moves,
184 "Ruy Lopez".to_string(),
185 Some("C60".to_string()),
186 );
187 }
188
189 if let Ok(board) =
191 Board::from_str("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
192 {
193 let moves = vec![
194 (ChessMove::from_str("g1f3").unwrap(), 1.0), (ChessMove::from_str("b1c3").unwrap(), 0.7), (ChessMove::from_str("f2f4").unwrap(), 0.5), ];
198 let _ = self.add_opening(
199 &board.to_string(),
200 0.3,
201 moves,
202 "Sicilian Defense".to_string(),
203 Some("B20".to_string()),
204 );
205 }
206
207 if let Ok(board) =
209 Board::from_str("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
210 {
211 let moves = vec![
212 (ChessMove::from_str("d2d4").unwrap(), 1.0), (ChessMove::from_str("d2d3").unwrap(), 0.5), (ChessMove::from_str("g1f3").unwrap(), 0.6), ];
216 let _ = self.add_opening(
217 &board.to_string(),
218 0.35,
219 moves,
220 "French Defense".to_string(),
221 Some("C00".to_string()),
222 );
223 }
224
225 if let Ok(board) =
227 Board::from_str("rnbqkbnr/pp1ppppp/2p5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
228 {
229 let moves = vec![
230 (ChessMove::from_str("d2d4").unwrap(), 1.0), (ChessMove::from_str("b1c3").unwrap(), 0.7), (ChessMove::from_str("f2f4").unwrap(), 0.4), ];
234 let _ = self.add_opening(
235 &board.to_string(),
236 0.3,
237 moves,
238 "Caro-Kann Defense".to_string(),
239 Some("B10".to_string()),
240 );
241 }
242
243 if let Ok(board) =
245 Board::from_str("rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1")
246 {
247 let moves = vec![
248 (ChessMove::from_str("d7d5").unwrap(), 1.0), (ChessMove::from_str("g8f6").unwrap(), 0.9), (ChessMove::from_str("f7f5").unwrap(), 0.4), ];
252 let _ = self.add_opening(
253 &board.to_string(),
254 0.2,
255 moves,
256 "Queen's Pawn Game".to_string(),
257 Some("D00".to_string()),
258 );
259 }
260
261 if let Ok(board) =
263 Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq - 0 2")
264 {
265 let moves = vec![
266 (ChessMove::from_str("d5c4").unwrap(), 0.7), (ChessMove::from_str("e7e6").unwrap(), 1.0), (ChessMove::from_str("c7c6").unwrap(), 0.8), (ChessMove::from_str("d5d4").unwrap(), 0.5), ];
271 let _ = self.add_opening(
272 &board.to_string(),
273 0.25,
274 moves,
275 "Queen's Gambit".to_string(),
276 Some("D06".to_string()),
277 );
278 }
279
280 if let Ok(board) =
282 Board::from_str("rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq - 0 1")
283 {
284 let moves = vec![
285 (ChessMove::from_str("g8f6").unwrap(), 1.0), (ChessMove::from_str("e7e5").unwrap(), 0.9), (ChessMove::from_str("c7c5").unwrap(), 0.8), (ChessMove::from_str("e7e6").unwrap(), 0.7), ];
290 let _ = self.add_opening(
291 &board.to_string(),
292 0.15,
293 moves,
294 "English Opening".to_string(),
295 Some("A10".to_string()),
296 );
297 }
298
299 if let Ok(board) =
301 Board::from_str("rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 2 4")
302 {
303 let moves = vec![
304 (ChessMove::from_str("e2e3").unwrap(), 0.9), (ChessMove::from_str("f2f3").unwrap(), 0.7), (ChessMove::from_str("a2a3").unwrap(), 0.8), (ChessMove::from_str("d1c2").unwrap(), 1.0), ];
309 let _ = self.add_opening(
310 &board.to_string(),
311 0.1,
312 moves,
313 "Nimzo-Indian Defense".to_string(),
314 Some("E20".to_string()),
315 );
316 }
317
318 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".to_string(),
332 Some("E60".to_string()),
333 );
334 }
335
336 if let Ok(board) =
338 Board::from_str("rnbqkb1r/1p2pppp/p2p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 0 6")
339 {
340 let moves = vec![
341 (ChessMove::from_str("c1e3").unwrap(), 1.0), (ChessMove::from_str("f2f3").unwrap(), 0.9), (ChessMove::from_str("h2h3").unwrap(), 0.7), ];
345 let _ = self.add_opening(
346 &board.to_string(),
347 0.2,
348 moves,
349 "Sicilian Najdorf".to_string(),
350 Some("B90".to_string()),
351 );
352 }
353 }
354
355 pub fn get_statistics(&self) -> OpeningBookStats {
357 let total_openings = self.entries.len();
358 let eco_coverage = self
359 .entries
360 .values()
361 .filter(|entry| entry.eco_code.is_some())
362 .count();
363
364 OpeningBookStats {
365 total_openings,
366 eco_coverage,
367 avg_moves_per_opening: if total_openings > 0 {
368 self.entries
369 .values()
370 .map(|entry| entry.best_moves.len())
371 .sum::<usize>() as f32
372 / total_openings as f32
373 } else {
374 0.0
375 },
376 }
377 }
378}
379
380#[derive(Debug, Clone)]
382pub struct OpeningBookStats {
383 pub total_openings: usize,
384 pub eco_coverage: usize,
385 pub avg_moves_per_opening: f32,
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_opening_book_creation() {
394 let book = OpeningBook::new();
395 assert_eq!(book.entries.len(), 0);
396 }
397
398 #[test]
399 fn test_standard_openings() {
400 let book = OpeningBook::with_standard_openings();
401 assert!(!book.entries.is_empty());
402
403 let board = Board::default();
405 let entry = book.lookup(&board);
406 assert!(entry.is_some());
407 assert_eq!(entry.unwrap().name, "Starting Position");
408 }
409
410 #[test]
411 fn test_opening_lookup() {
412 let mut book = OpeningBook::new();
413 let board = Board::default();
414
415 assert!(!book.contains(&board));
417
418 let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
420 book.add_opening(
421 &board.to_string(),
422 0.0,
423 moves,
424 "Test Opening".to_string(),
425 None,
426 )
427 .unwrap();
428
429 assert!(book.contains(&board));
431 let entry = book.lookup(&board).unwrap();
432 assert_eq!(entry.name, "Test Opening");
433 assert_eq!(entry.best_moves.len(), 1);
434 }
435
436 #[test]
437 fn test_comprehensive_opening_coverage() {
438 let book = OpeningBook::with_standard_openings();
439 let stats = book.get_statistics();
440
441 assert!(
443 stats.total_openings >= 12,
444 "Expected at least 12 openings, got {}",
445 stats.total_openings
446 );
447 assert!(
448 stats.eco_coverage >= 8,
449 "Expected at least 8 ECO codes, got {}",
450 stats.eco_coverage
451 );
452 assert!(
453 stats.avg_moves_per_opening >= 2.0,
454 "Expected average 2+ moves per opening"
455 );
456
457 let opening_names: Vec<_> = book.entries.values().map(|entry| &entry.name).collect();
459
460 let has_sicilian = opening_names.iter().any(|name| name.contains("Sicilian"));
461 let has_italian = opening_names.iter().any(|name| name.contains("Italian"));
462 let has_ruy_lopez = opening_names.iter().any(|name| name.contains("Ruy Lopez"));
463 let has_french = opening_names.iter().any(|name| name.contains("French"));
464
465 assert!(has_sicilian, "Should have Sicilian Defense");
466 assert!(has_italian, "Should have Italian Game");
467 assert!(has_ruy_lopez, "Should have Ruy Lopez");
468 assert!(has_french, "Should have French Defense");
469 }
470}