diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 7d061072..2fca99f6 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -68,6 +68,9 @@ pub enum Error { #[error("Missing reference database")] MissingReferenceDatabase, + #[error("No opening found")] + NoOpeningFound, + #[error("No match found")] NoMatchFound, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index de0503b8..b5813998 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -47,7 +47,7 @@ use crate::{ chess::get_best_moves, db::{edit_db_info, get_db_info, get_games, get_players}, fs::download_file, - opening::get_opening_from_fen, + opening::{get_opening_from_fen, search_opening_name}, }; use tokio::sync::{RwLock, Semaphore}; @@ -204,6 +204,7 @@ fn main() { download_file, get_best_moves, get_opening_from_fen, + search_opening_name, get_puzzle, get_games, get_players, diff --git a/src-tauri/src/opening.rs b/src-tauri/src/opening.rs index 65a0027c..73de6b5f 100644 --- a/src-tauri/src/opening.rs +++ b/src-tauri/src/opening.rs @@ -1,20 +1,17 @@ -use std::collections::HashMap; - use log::info; use serde::{Deserialize, Serialize}; -use shakmaty::{ - fen::Fen, - san::San, - zobrist::{Zobrist64, ZobristHash}, - CastlingMode, Chess, EnPassantMode, Position, -}; +use shakmaty::{fen::Fen, san::San, Chess, EnPassantMode, Position}; use lazy_static::lazy_static; +use strsim::jaro_winkler; + +use crate::error::Error; -#[derive(Serialize, Debug)] +#[derive(Serialize, Debug, Clone)] pub struct Opening { eco: String, name: String, + fen: String, } #[derive(Deserialize)] @@ -33,30 +30,70 @@ const TSV_DATA: [&[u8]; 5] = [ ]; #[tauri::command] -pub fn get_opening_from_fen(fen: &str) -> Result<&str, &str> { - let fen: Fen = fen.parse().or(Err("Invalid FEN"))?; - let pos: Chess = fen - .into_position(CastlingMode::Standard) - .or(Err("Invalid Position"))?; - let hash: Zobrist64 = pos.zobrist_hash(EnPassantMode::Legal); +pub fn get_opening_from_fen(fen: &str) -> Result<&str, Error> { OPENINGS - .get(&hash) + .iter() + .find(|o| o.fen == fen) .map(|o| o.name.as_str()) - .ok_or("No opening found") + .ok_or(Error::NoOpeningFound) } -pub fn get_opening_from_eco(eco: &str) -> Result<&str, &str> { +pub fn get_opening_from_eco(eco: &str) -> Result<&str, Error> { OPENINGS - .values() + .iter() .find(|o| o.eco == eco) .map(|o| o.name.as_str()) - .ok_or("No opening found") + .ok_or(Error::NoOpeningFound) +} + +#[tauri::command] +pub async fn search_opening_name(query: String) -> Result, Error> { + let mut best_matches: Vec<(Opening, f64)> = Vec::new(); + + for opening in OPENINGS.iter() { + if best_matches.iter().any(|(m, _)| m.name == opening.name) { + continue; + } + + let score = jaro_winkler(&query, &opening.name); + + if best_matches.len() < 15 { + best_matches.push((opening.clone(), score)); + best_matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + } else if let Some(min_score) = best_matches.last().map(|(_, s)| *s) { + if score > min_score { + best_matches.pop(); + best_matches.push((opening.clone(), score)); + best_matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + } + } + } + + if !best_matches.is_empty() { + let best_matches_names = best_matches.iter().map(|(o, _)| o.clone()).collect(); + Ok(best_matches_names) + } else { + Err(Error::NoMatchFound) + } } lazy_static! { - static ref OPENINGS: HashMap = { + static ref OPENINGS: Vec = { info!("Initializing openings table..."); - let mut map = HashMap::new(); + + let mut positions = vec![ + Opening { + eco: "Extra".to_string(), + name: "Starting Position".to_string(), + fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string(), + }, + Opening { + eco: "Extra".to_string(), + name: "Empty Board".to_string(), + fen: "8/8/8/8/8/8/8/8 w - - 0 1".to_string(), + }, + ]; + for tsv in TSV_DATA { let mut rdr = csv::ReaderBuilder::new().delimiter(b'\t').from_reader(tsv); for result in rdr.deserialize() { @@ -67,16 +104,15 @@ lazy_static! { pos.play_unchecked(&san.to_move(&pos).expect("legal move")); } } - map.insert( - pos.zobrist_hash(EnPassantMode::Legal), - Opening { - eco: record.eco, - name: record.name, - }, - ); + let fen = Fen::from_position(pos.clone(), EnPassantMode::Legal); + positions.push(Opening { + eco: record.eco, + name: record.name, + fen: fen.to_string(), + }); } } - map + positions }; } diff --git a/src/components/panels/info/FenInput.tsx b/src/components/panels/info/FenInput.tsx index 2a3617ba..4d57e7b6 100644 --- a/src/components/panels/info/FenInput.tsx +++ b/src/components/panels/info/FenInput.tsx @@ -13,21 +13,6 @@ type ItemProps = { group?: string; }; -const POSITIONS: ItemProps[] = [ - { - label: "Empty position", - value: "8/8/8/8/8/8/8/8 w - - 0 1", - }, - { - label: "Starting position", - value: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - }, - { - label: "Ruy Lopez", - value: "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 1", - }, -]; - const SelectItem = forwardRef(function SelectItem( { label, value: fen, ...others }: ItemProps, ref @@ -53,12 +38,7 @@ type Castlingrights = { function FenInput({ currentFen }: { currentFen: string }) { const dispatch = useContext(TreeDispatchContext); - const [error, setError] = useState(undefined); - let positions = POSITIONS; - if (!positions.find((p) => p.value === currentFen)) { - positions = [...positions, { label: currentFen, value: currentFen }]; - } let chess: Chess | null; let whiteCastling: Castlingrights; let blackCastling: Castlingrights; @@ -90,40 +70,12 @@ function FenInput({ currentFen }: { currentFen: string }) { } } - function addFen(fen: string) { - if (fen) { - invoke<{ valid: boolean; error?: string }>("validate_fen", { - fen, - }).then((v) => { - if (v.valid) { - dispatch({ type: "SET_FEN", payload: fen }); - setError(undefined); - } else if (v.error) { - setError(capitalize(v.error)); - } - }); - } - return fen; - } - return ( FEN - ([ + { label: currentFen, value: currentFen }, + ]); + const [error, setError] = useState(undefined); + const dispatch = useContext(TreeDispatchContext); + + function addFen(fen: string) { + if (fen) { + invoke<{ valid: boolean; error?: string }>("validate_fen", { + fen, + }).then((v) => { + if (v.valid) { + dispatch({ type: "SET_FEN", payload: fen }); + setError(undefined); + } else if (v.error) { + setError(capitalize(v.error)); + } + }); + } + return fen; + } + + async function searchOpening(name: string) { + const results = await invoke< + { + eco: string; + name: string; + fen: string; + }[] + >("search_opening_name", { + query: name, + }); + setData([ + { + label: name, + value: name, + }, + ...results.map((p) => ({ + label: p.name, + value: p.fen, + })), + ]); + } + + return ( +