use chess::{Board, ChessMove};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct OpeningEntry {
pub evaluation: f32,
pub best_moves: Vec<(ChessMove, f32)>, pub name: String,
pub eco_code: Option<String>, }
#[derive(Clone)]
pub struct OpeningBook {
entries: HashMap<String, OpeningEntry>,
}
impl Default for OpeningBook {
fn default() -> Self {
Self::new()
}
}
impl OpeningBook {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn with_standard_openings() -> Self {
let mut book = Self::new();
book.add_standard_openings();
book
}
pub fn add_opening(
&mut self,
fen: &str,
evaluation: f32,
best_moves: Vec<(ChessMove, f32)>,
name: String,
eco_code: Option<String>,
) -> Result<(), String> {
Board::from_str(fen).map_err(|_e| "Invalid FEN".to_string())?;
let entry = OpeningEntry {
evaluation,
best_moves,
name,
eco_code,
};
self.entries.insert(fen.to_string(), entry);
Ok(())
}
pub fn lookup(&self, board: &Board) -> Option<&OpeningEntry> {
let fen = board.to_string();
self.entries.get(&fen)
}
pub fn contains(&self, board: &Board) -> bool {
let fen = board.to_string();
self.entries.contains_key(&fen)
}
pub fn get_all_openings(&self) -> &HashMap<String, OpeningEntry> {
&self.entries
}
pub fn get_random_opening(&self) -> Option<ChessMove> {
use rand::seq::SliceRandom;
let board = Board::default();
if let Some(entry) = self.lookup(&board) {
let moves: Vec<ChessMove> = entry.best_moves.iter().map(|(mv, _)| *mv).collect();
moves.choose(&mut rand::thread_rng()).copied()
} else {
None
}
}
fn add_standard_openings(&mut self) {
if let Ok(board) =
Board::from_str("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.0,
moves,
"Starting Position".to_string(),
None,
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.25,
moves,
"King's Pawn Game".to_string(),
Some("B00".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
{
let moves = vec![
(ChessMove::from_str("g1f3").unwrap(), 1.0), (ChessMove::from_str("f2f4").unwrap(), 0.6), (ChessMove::from_str("b1c3").unwrap(), 0.5), ];
let _ = self.add_opening(
&board.to_string(),
0.15,
moves,
"Open Game".to_string(),
Some("C20".to_string()),
);
}
if let Ok(board) =
Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
{
let moves = vec![
(ChessMove::from_str("g8f6").unwrap(), 1.0), (ChessMove::from_str("f7f5").unwrap(), 0.6), (ChessMove::from_str("f8e7").unwrap(), 0.4), ];
let _ = self.add_opening(
&board.to_string(),
0.25,
moves,
"Italian Game".to_string(),
Some("C50".to_string()),
);
}
if let Ok(board) =
Board::from_str("r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.3,
moves,
"Ruy Lopez".to_string(),
Some("C60".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
{
let moves = vec![
(ChessMove::from_str("g1f3").unwrap(), 1.0), (ChessMove::from_str("b1c3").unwrap(), 0.7), (ChessMove::from_str("f2f4").unwrap(), 0.5), ];
let _ = self.add_opening(
&board.to_string(),
0.3,
moves,
"Sicilian Defense".to_string(),
Some("B20".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
{
let moves = vec![
(ChessMove::from_str("d2d4").unwrap(), 1.0), (ChessMove::from_str("d2d3").unwrap(), 0.5), (ChessMove::from_str("g1f3").unwrap(), 0.6), ];
let _ = self.add_opening(
&board.to_string(),
0.35,
moves,
"French Defense".to_string(),
Some("C00".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pp1ppppp/2p5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
{
let moves = vec![
(ChessMove::from_str("d2d4").unwrap(), 1.0), (ChessMove::from_str("b1c3").unwrap(), 0.7), (ChessMove::from_str("f2f4").unwrap(), 0.4), ];
let _ = self.add_opening(
&board.to_string(),
0.3,
moves,
"Caro-Kann Defense".to_string(),
Some("B10".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1")
{
let moves = vec![
(ChessMove::from_str("d7d5").unwrap(), 1.0), (ChessMove::from_str("g8f6").unwrap(), 0.9), (ChessMove::from_str("f7f5").unwrap(), 0.4), ];
let _ = self.add_opening(
&board.to_string(),
0.2,
moves,
"Queen's Pawn Game".to_string(),
Some("D00".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq - 0 2")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.25,
moves,
"Queen's Gambit".to_string(),
Some("D06".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq - 0 1")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.15,
moves,
"English Opening".to_string(),
Some("A10".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 2 4")
{
let moves = vec![
(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), ];
let _ = self.add_opening(
&board.to_string(),
0.1,
moves,
"Nimzo-Indian Defense".to_string(),
Some("E20".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkb1r/pppppp1p/5np1/8/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3")
{
let moves = vec![
(ChessMove::from_str("b1c3").unwrap(), 1.0), (ChessMove::from_str("g1f3").unwrap(), 0.9), (ChessMove::from_str("f2f3").unwrap(), 0.6), ];
let _ = self.add_opening(
&board.to_string(),
0.2,
moves,
"King's Indian Defense".to_string(),
Some("E60".to_string()),
);
}
if let Ok(board) =
Board::from_str("rnbqkb1r/1p2pppp/p2p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 0 6")
{
let moves = vec![
(ChessMove::from_str("c1e3").unwrap(), 1.0), (ChessMove::from_str("f2f3").unwrap(), 0.9), (ChessMove::from_str("h2h3").unwrap(), 0.7), ];
let _ = self.add_opening(
&board.to_string(),
0.2,
moves,
"Sicilian Najdorf".to_string(),
Some("B90".to_string()),
);
}
}
pub fn get_statistics(&self) -> OpeningBookStats {
let total_openings = self.entries.len();
let eco_coverage = self
.entries
.values()
.filter(|entry| entry.eco_code.is_some())
.count();
OpeningBookStats {
total_openings,
eco_coverage,
avg_moves_per_opening: if total_openings > 0 {
self.entries
.values()
.map(|entry| entry.best_moves.len())
.sum::<usize>() as f32
/ total_openings as f32
} else {
0.0
},
}
}
}
#[derive(Debug, Clone)]
pub struct OpeningBookStats {
pub total_openings: usize,
pub eco_coverage: usize,
pub avg_moves_per_opening: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_opening_book_creation() {
let book = OpeningBook::new();
assert_eq!(book.entries.len(), 0);
}
#[test]
fn test_standard_openings() {
let book = OpeningBook::with_standard_openings();
assert!(!book.entries.is_empty());
let board = Board::default();
let entry = book.lookup(&board);
assert!(entry.is_some());
assert_eq!(entry.unwrap().name, "Starting Position");
}
#[test]
fn test_opening_lookup() {
let mut book = OpeningBook::new();
let board = Board::default();
assert!(!book.contains(&board));
let moves = vec![(ChessMove::from_str("e2e4").unwrap(), 1.0)];
book.add_opening(
&board.to_string(),
0.0,
moves,
"Test Opening".to_string(),
None,
)
.unwrap();
assert!(book.contains(&board));
let entry = book.lookup(&board).unwrap();
assert_eq!(entry.name, "Test Opening");
assert_eq!(entry.best_moves.len(), 1);
}
#[test]
fn test_comprehensive_opening_coverage() {
let book = OpeningBook::with_standard_openings();
let stats = book.get_statistics();
assert!(
stats.total_openings >= 12,
"Expected at least 12 openings, got {}",
stats.total_openings
);
assert!(
stats.eco_coverage >= 8,
"Expected at least 8 ECO codes, got {}",
stats.eco_coverage
);
assert!(
stats.avg_moves_per_opening >= 2.0,
"Expected average 2+ moves per opening"
);
let opening_names: Vec<_> = book.entries.values().map(|entry| &entry.name).collect();
let has_sicilian = opening_names.iter().any(|name| name.contains("Sicilian"));
let has_italian = opening_names.iter().any(|name| name.contains("Italian"));
let has_ruy_lopez = opening_names.iter().any(|name| name.contains("Ruy Lopez"));
let has_french = opening_names.iter().any(|name| name.contains("French"));
assert!(has_sicilian, "Should have Sicilian Defense");
assert!(has_italian, "Should have Italian Game");
assert!(has_ruy_lopez, "Should have Ruy Lopez");
assert!(has_french, "Should have French Defense");
}
}