diff --git a/src-tauri/src/chess.rs b/src-tauri/src/chess.rs index 9f62d4d5..0ebd7e38 100644 --- a/src-tauri/src/chess.rs +++ b/src-tauri/src/chess.rs @@ -427,6 +427,7 @@ pub async fn get_best_moves( pub struct MoveAnalysis { best: Vec, novelty: bool, + maybe_brilliant: bool, } #[derive(Deserialize, Debug, Default, Type)] @@ -456,14 +457,20 @@ pub async fn analyze_game( let fen = Fen::from_ascii(options.fen.as_bytes())?; let mut chess: Chess = fen.clone().into_position(CastlingMode::Standard)?; - let mut fens: Vec = vec![fen]; + let mut fens: Vec<(Fen, bool)> = vec![(fen, false)]; moves.iter().for_each(|m| { let san = San::from_ascii(m.as_bytes()).unwrap(); let m = san.to_move(&chess).unwrap(); + let mut previous_setup = chess.clone().into_setup(EnPassantMode::Legal); + previous_setup.swap_turn(); chess.play_unchecked(&m); + let current_setup = chess.clone().into_setup(EnPassantMode::Legal); if !chess.is_game_over() { - fens.push(Fen::from_position(chess.clone(), EnPassantMode::Legal)); + fens.push(( + Fen::from_position(chess.clone(), EnPassantMode::Legal), + count_attacked_material(&previous_setup) <= count_attacked_material(¤t_setup), + )); } }); @@ -473,7 +480,7 @@ pub async fn analyze_game( let mut novelty_found = false; - for (i, fen) in fens.iter().enumerate() { + for (i, (fen, maybe_brilliant)) in fens.iter().enumerate() { app.emit_all( "report_progress", ProgressPayload { @@ -527,9 +534,10 @@ pub async fn analyze_game( } for (i, analysis) in analysis.iter_mut().enumerate() { - let fen = &fens[i]; + let fen = &fens[i].0; let query = PositionQuery::exact_from_fen(&fen.to_string())?; + analysis.maybe_brilliant = fens[i].1; if options.annotate_novelties && !novelty_found { if let Some(reference) = options.reference_db.clone() { analysis.novelty = !is_position_in_db(reference, query, state.clone()).await?; @@ -552,6 +560,71 @@ pub async fn analyze_game( Ok(analysis) } +fn count_attacked_material(pos: &Setup) -> i32 { + let mut attacked_material = 0; + let mut seen_attacked = Vec::new(); + for s in Square::ALL.iter() { + if let Some(piece) = pos.board.piece_at(*s) { + if piece.color == pos.turn { + let squares_attacked = pos.board.attacks_from(*s); + for square in Square::ALL.iter() { + if squares_attacked.contains(*square) && !seen_attacked.contains(square) { + if let Some(attacked_piece) = pos.board.piece_at(*square) { + seen_attacked.push(*square); + if attacked_piece.color != pos.turn { + attacked_material += match attacked_piece.role { + Role::Pawn => 1, + Role::Knight => 3, + Role::Bishop => 3, + Role::Rook => 5, + Role::Queen => 9, + Role::King => 1000, + } + } + } + } + } + } + } + } + attacked_material +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_count_attacked_material() { + assert_eq!(count_attacked_material(&Setup::default()), 0); + + let fen: Fen = "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2" + .parse() + .unwrap(); + assert_eq!(count_attacked_material(&fen.into_setup()), 1); + + let fen: Fen = "r1bqkbnr/ppp1pppp/2n5/1B1p4/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3" + .parse() + .unwrap(); + assert_eq!(count_attacked_material(&fen.into_setup()), 1); + + let fen: Fen = "r1bqkbnr/ppp2ppp/2n5/1B1pp3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4" + .parse() + .unwrap(); + assert_eq!(count_attacked_material(&fen.into_setup()), 5); + + let fen: Fen = "r1bqkbnr/ppp2ppp/2B5/3pp3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 0 4" + .parse() + .unwrap(); + assert_eq!(count_attacked_material(&fen.into_setup()), 4); + + let fen: Fen = "r1bqkbnr/ppp2ppp/2B5/3pp3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq" + .parse() + .unwrap(); + assert_eq!(count_attacked_material(&fen.into_setup()), 1003); + } +} + #[tauri::command] pub async fn get_single_best_move( skill_level: usize, diff --git a/src/bindings.ts b/src/bindings.ts index 4c2df77b..394415ce 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -20,7 +20,7 @@ try { else return { status: "error", error: e as any }; } }, -async analyzeGame(moves: string[], engine: string, goMode: GoMode, options: AnalysisOptions) : Promise<__Result__<{ best: BestMoves[]; novelty: boolean }[], string>> { +async analyzeGame(moves: string[], engine: string, goMode: GoMode, options: AnalysisOptions) : Promise<__Result__<{ best: BestMoves[]; novelty: boolean; maybe_brilliant: boolean }[], string>> { try { return { status: "ok", data: await TAURI_INVOKE("plugin:tauri-specta|analyze_game", { moves, engine, goMode, options }) }; } catch (e) { diff --git a/src/components/panels/analysis/ReportModal.tsx b/src/components/panels/analysis/ReportModal.tsx index d85660ab..f863bf95 100644 --- a/src/components/panels/analysis/ReportModal.tsx +++ b/src/components/panels/analysis/ReportModal.tsx @@ -54,7 +54,6 @@ function ReportModal({ function analyze() { setInProgress(true); toggleReportingMode(); - console.log(form.values); commands .analyzeGame( moves, diff --git a/src/utils/score.ts b/src/utils/score.ts index f63d1c2a..21983f65 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -69,9 +69,9 @@ export function getAccuracy(prev: Score, next: Score, color: Color): number { const { prevCP, nextCP } = normalizeScores(prev, next, color); return minMax( 103.1668 * - Math.exp(-0.04354 * (getWinChance(prevCP) - getWinChance(nextCP))) - - 3.1669 + - 1, + Math.exp(-0.04354 * (getWinChance(prevCP) - getWinChance(nextCP))) - + 3.1669 + + 1, 0, 100 ); @@ -87,7 +87,9 @@ export function getAnnotation( prev: Score, next: Score, color: Color, - prevMoves: BestMoves[] + prevMoves: BestMoves[], + maybe_brilliant: boolean, + move: string ): Annotation { const { prevCP, nextCP } = normalizeScores(prev, next, color); const winChanceDiff = getWinChance(prevCP) - getWinChance(nextCP); @@ -101,8 +103,16 @@ export function getAnnotation( } if (prevMoves.length > 1) { - const scores = normalizeScores(prevMoves[0].score, prevMoves[1].score, color); - if (getWinChance(scores.prevCP) - getWinChance(scores.nextCP) > 10) { + const scores = normalizeScores( + prevMoves[0].score, + prevMoves[1].score, + color + ); + if ( + getWinChance(scores.prevCP) - getWinChance(scores.nextCP) > 10 && + maybe_brilliant && + move === prevMoves[0].sanMoves[0] + ) { return "!"; } } diff --git a/src/utils/treeReducer.ts b/src/utils/treeReducer.ts index fea634ff..7e99a6be 100644 --- a/src/utils/treeReducer.ts +++ b/src/utils/treeReducer.ts @@ -200,7 +200,7 @@ export type TreeAction = | { type: "SET_FEN"; payload: string } | { type: "SET_SCORE"; payload: Score } | { type: "SET_SHAPES"; payload: DrawShape[] } - | { type: "ADD_ANALYSIS"; payload: { best: BestMoves[], novelty: boolean }[] } + | { type: "ADD_ANALYSIS"; payload: { best: BestMoves[], novelty: boolean, maybe_brilliant: boolean }[] } | { type: "PROMOTE_VARIATION"; payload: number[] }; const treeReducer = (state: TreeState, action: TreeAction) => { @@ -388,7 +388,7 @@ export function getColorFromFen(fen: string): "w" | "b" { return "b"; } -function addAnalysis(state: TreeState, analysis: { best: BestMoves[], novelty: boolean }[]) { +function addAnalysis(state: TreeState, analysis: { best: BestMoves[], novelty: boolean, maybe_brilliant: boolean }[]) { let cur = state.root; let i = 0; const initialColor = getColorFromFen(state.root.fen); @@ -407,7 +407,7 @@ function addAnalysis(state: TreeState, analysis: { best: BestMoves[], novelty: b } const curScore = analysis[i].best[0].score; const color = i % 2 === (initialColor === "w" ? 1 : 0) ? "w" : "b"; - cur.annotation = getAnnotation(prevScore, curScore, color, prevMoves); + cur.annotation = getAnnotation(prevScore, curScore, color, prevMoves, analysis[i].maybe_brilliant, cur.move?.san ?? ""); } cur = cur.children[0]; i++;