diff --git a/package.json b/package.json index 02387c33..ed492f19 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "tauri": "tauri", "format": "biome format --write ./src", "test": "vitest run", - "lint": "biome check ./src", + "lint": "tsc --noEmit && biome check ./src", "lint:fix": "biome check --apply ./src" }, "dependencies": { diff --git a/src-tauri/src/chess.rs b/src-tauri/src/chess.rs index 1fed74e1..4d6e9c30 100644 --- a/src-tauri/src/chess.rs +++ b/src-tauri/src/chess.rs @@ -108,7 +108,7 @@ impl EngineProcess { async fn set_options(&mut self, options: EngineOptions) -> Result<(), Error> { let fen: Fen = options.fen.parse()?; - let mut pos: Chess = match fen.into_position(CastlingMode::Standard) { + let mut pos: Chess = match fen.into_position(CastlingMode::Chess960) { Ok(p) => p, Err(e) => e.ignore_too_much_material()?, }; @@ -249,7 +249,7 @@ fn parse_uci_attrs( ) -> Result { let mut best_moves = BestMoves::default(); - let mut pos: Chess = match fen.clone().into_position(CastlingMode::Standard) { + let mut pos: Chess = match fen.clone().into_position(CastlingMode::Chess960) { Ok(p) => p, Err(e) => e.ignore_too_much_material()?, }; @@ -586,7 +586,7 @@ 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 chess: Chess = fen.clone().into_position(CastlingMode::Chess960)?; let mut fens: Vec<(Fen, Vec, bool)> = vec![(fen, vec![], false)]; options.moves.iter().enumerate().for_each(|(i, m)| { @@ -780,7 +780,7 @@ mod tests { fn pos(fen: &str) -> Chess { let fen: Fen = fen.parse().unwrap(); - Chess::from_setup(fen.into_setup(), CastlingMode::Standard).unwrap() + Chess::from_setup(fen.into_setup(), CastlingMode::Chess960).unwrap() } #[test] diff --git a/src-tauri/src/db/search.rs b/src-tauri/src/db/search.rs index 1d2ceff2..2b4ef033 100644 --- a/src-tauri/src/db/search.rs +++ b/src-tauri/src/db/search.rs @@ -46,7 +46,7 @@ pub enum PositionQuery { impl PositionQuery { pub fn exact_from_fen(fen: &str) -> Result { let position: Chess = - Fen::from_ascii(fen.as_bytes())?.into_position(shakmaty::CastlingMode::Standard)?; + Fen::from_ascii(fen.as_bytes())?.into_position(shakmaty::CastlingMode::Chess960)?; let pawn_home = get_pawn_home(position.board()); let material = get_material_count(position.board()); Ok(PositionQuery::Exact(ExactData { @@ -172,7 +172,7 @@ fn get_move_after_match( ) -> Result, Error> { let mut chess = if let Some(fen) = fen { let fen = Fen::from_ascii(fen.as_bytes())?; - Chess::from_setup(fen.into_setup(), shakmaty::CastlingMode::Standard)? + Chess::from_setup(fen.into_setup(), shakmaty::CastlingMode::Chess960)? } else { Chess::default() }; @@ -415,7 +415,7 @@ mod tests { fn assert_partial_match(fen1: &str, fen2: &str) { let query = PositionQuery::partial_from_fen(fen1).unwrap(); let fen = Fen::from_ascii(fen2.as_bytes()).unwrap(); - let chess = Chess::from_setup(fen.into_setup(), shakmaty::CastlingMode::Standard).unwrap(); + let chess = Chess::from_setup(fen.into_setup(), shakmaty::CastlingMode::Chess960).unwrap(); assert!(query.matches(&chess)); } diff --git a/src/components/boards/Board.tsx b/src/components/boards/Board.tsx index 73355d47..8c8d0b0b 100644 --- a/src/components/boards/Board.tsx +++ b/src/components/boards/Board.tsx @@ -20,11 +20,8 @@ import { Annotation, TimeControlField, getMaterialDiff, - makeClk, - moveToKey, parseKeyboardMove, parseTimeControl, - parseUci, } from "@/utils/chess"; import { chessopsError, @@ -56,8 +53,15 @@ import { } from "@tabler/icons-react"; import { DrawShape } from "chessground/draw"; import { Color } from "chessground/types"; -import { NormalMove, Square, SquareName, parseSquare } from "chessops"; -import { chessgroundDests } from "chessops/compat"; +import { + NormalMove, + Square, + SquareName, + makeSquare, + parseSquare, + parseUci, +} from "chessops"; +import { chessgroundDests, chessgroundMove } from "chessops/compat"; import { makeSan } from "chessops/san"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { memo, useContext, useEffect, useMemo, useState } from "react"; @@ -182,14 +186,14 @@ function Board({ setInvisible(false); dispatch({ type: "MAKE_MOVE", - payload: san, + payload: move, }); setPendingMove(null); } } else { dispatch({ type: "MAKE_MOVE", - payload: san, + payload: move, clock: pos.turn === "white" ? whiteTime : blackTime, }); setPendingMove(null); @@ -202,7 +206,9 @@ function Board({ for (const [i, moves] of entries) { if (i < 4) { for (const [j, move] of moves.entries()) { - const { from, to } = parseUci(move); + const m = parseUci(move)! as NormalMove; + const from = makeSquare(m.from)!; + const to = makeSquare(m.to)!; if (shapes.find((s) => s.orig === from && s.dest === to)) continue; shapes.push({ orig: from, @@ -412,7 +418,7 @@ function Board({ {currentNode.annotation && currentNode.move && ( )} @@ -484,7 +490,13 @@ function Board({ }} turnColor={turn} check={pos?.isCheck()} - lastMove={editingMode ? undefined : moveToKey(currentNode.move)} + lastMove={ + editingMode + ? undefined + : currentNode.move + ? chessgroundMove(currentNode.move) + : undefined + } premovable={{ enabled: false, }} diff --git a/src/components/boards/BoardGame.tsx b/src/components/boards/BoardGame.tsx index fed6978a..29abb5b2 100644 --- a/src/components/boards/BoardGame.tsx +++ b/src/components/boards/BoardGame.tsx @@ -6,7 +6,7 @@ import { tabsAtom, } from "@/atoms/atoms"; import { events, GoMode, commands } from "@/bindings"; -import { TimeControlField, getMainLine, parseUci } from "@/utils/chess"; +import { TimeControlField, getMainLine } from "@/utils/chess"; import { positionFromFen } from "@/utils/chessops"; import { EngineSettings, LocalEngine } from "@/utils/engines"; import { getNodeAtPath, treeIteratorMainLine } from "@/utils/treeReducer"; @@ -33,6 +33,7 @@ import { IconPlus, IconZoomCheck, } from "@tabler/icons-react"; +import { parseUci } from "chessops"; import { INITIAL_FEN } from "chessops/fen"; import { useAtom, useAtomValue } from "jotai"; import { @@ -406,7 +407,7 @@ function BoardGame() { ) { dispatch({ type: "APPEND_MOVE", - payload: parseUci(ev[0].uciMoves[0]), + payload: parseUci(ev[0].uciMoves[0])!, clock: (pos.turn === "white" ? whiteTime : blackTime) ?? undefined, }); } diff --git a/src/components/boards/CompleteMoveCell.tsx b/src/components/boards/CompleteMoveCell.tsx index 5b0012e1..16b4cfdc 100644 --- a/src/components/boards/CompleteMoveCell.tsx +++ b/src/components/boards/CompleteMoveCell.tsx @@ -29,7 +29,7 @@ function CompleteMoveCell({ commentHTML: string; annotation: Annotation; showComments: boolean; - move?: string; + move?: string | null; first?: boolean; isCurrentVariation: boolean; isStart: boolean; diff --git a/src/components/boards/GameNotation.tsx b/src/components/boards/GameNotation.tsx index bb20cb5a..f97acdda 100644 --- a/src/components/boards/GameNotation.tsx +++ b/src/components/boards/GameNotation.tsx @@ -216,7 +216,7 @@ const RenderVariationTree = memo( annotation={variation.annotation} commentHTML={variation.commentHTML} halfMoves={variation.halfMoves} - move={variation.move?.san} + move={variation.san} movePath={[...path, variations.indexOf(variation)]} showComments={showComments} isCurrentVariation={shallowEqual( @@ -252,7 +252,7 @@ const RenderVariationTree = memo( annotation={variations[0].annotation} commentHTML={variations[0].commentHTML} halfMoves={variations[0].halfMoves} - move={variations[0].move?.san} + move={variations[0].san} movePath={[...path, 0]} showComments={showComments} isCurrentVariation={shallowEqual([...path, 0], currentPath)} diff --git a/src/components/common/EvalChart.tsx b/src/components/common/EvalChart.tsx index cf9d26ed..46ddd88c 100644 --- a/src/components/common/EvalChart.tsx +++ b/src/components/common/EvalChart.tsx @@ -47,7 +47,7 @@ const EvalChart = (props: EvalChartProps) => { const [pos, error] = positionFromFen(node.fen); if (pos) { if (pos.isCheckmate()) { - return node.move?.color === "w" ? 1 : -1; + return pos?.turn === "white" ? 1 : -1; } if (pos.isStalemate()) { return 0; @@ -87,13 +87,13 @@ const EvalChart = (props: EvalChartProps) => { const nodes = getNodes(); for (let i = 0; i < nodes.length; i++) { const currentNode = nodes[i]; - const move = currentNode.node.move!; const yValue = getYValue(currentNode.node); + const [pos] = positionFromFen(currentNode.node.fen); yield { name: `${Math.ceil(currentNode.node.halfMoves / 2)}.${ - move.color === "w" ? "" : ".." - } ${move.san}${currentNode.node.annotation}`, + pos?.turn === "white" ? "" : ".." + } ${currentNode.node.san}${currentNode.node.annotation}`, evalText: getEvalText(currentNode.node), yValue: yValue ?? "none", movePath: currentNode.position, diff --git a/src/components/files/opening.ts b/src/components/files/opening.ts index a63fc3c5..4e090fd1 100644 --- a/src/components/files/opening.ts +++ b/src/components/files/opening.ts @@ -24,7 +24,7 @@ export function buildFromTree( if ( item.node.children.length === 0 || isPrefix(item.position, start) || - !item.node.children[0].move + !item.node.children[0].san ) { continue; } @@ -35,7 +35,7 @@ export function buildFromTree( cards.push({ fen: item.node.fen, position: item.position, - answer: item.node.children[0].move?.san, + answer: item.node.children[0].san, repetitions: 0, level: "unseen", }); diff --git a/src/components/panels/analysis/TablebaseInfo.tsx b/src/components/panels/analysis/TablebaseInfo.tsx index 8a8f4737..84e1d0cb 100644 --- a/src/components/panels/analysis/TablebaseInfo.tsx +++ b/src/components/panels/analysis/TablebaseInfo.tsx @@ -9,6 +9,7 @@ import { Stack, Text, } from "@mantine/core"; +import { parseUci } from "chessops"; import { useContext } from "react"; import useSWRImmutable from "swr/immutable"; import { P, match } from "ts-pattern"; @@ -72,7 +73,7 @@ function TablebaseInfo({ onClick={() => { dispatch({ type: "MAKE_MOVE", - payload: m.san, + payload: parseUci(m.uci)!, }); }} className={classes.info} diff --git a/src/components/puzzles/PuzzleBoard.tsx b/src/components/puzzles/PuzzleBoard.tsx index cb656233..9c84c0ee 100644 --- a/src/components/puzzles/PuzzleBoard.tsx +++ b/src/components/puzzles/PuzzleBoard.tsx @@ -1,14 +1,20 @@ import { showCoordinatesAtom } from "@/atoms/atoms"; import { Chessground } from "@/chessground/Chessground"; import { chessboard } from "@/styles/Chessboard.css"; -import { moveToKey } from "@/utils/chess"; import { positionFromFen } from "@/utils/chessops"; import { Completion, Puzzle } from "@/utils/puzzles"; import { getNodeAtPath, treeIteratorMainLine } from "@/utils/treeReducer"; import { Box } from "@mantine/core"; import { useForceUpdate } from "@mantine/hooks"; -import { Chess, NormalMove, makeUci, parseSquare, parseUci } from "chessops"; -import { chessgroundDests } from "chessops/compat"; +import { + Chess, + Move, + NormalMove, + makeUci, + parseSquare, + parseUci, +} from "chessops"; +import { chessgroundDests, chessgroundMove } from "chessops/compat"; import { parseFen, parsePiece } from "chessops/fen"; import equal from "fast-deep-equal"; import { useAtomValue } from "jotai"; @@ -52,16 +58,7 @@ function PuzzleBoard({ let currentMove = 0; if (puzzle) { for (const { node } of treeIter) { - if ( - node.move && - makeUci({ - from: parseSquare(node.move.from), - to: parseSquare(node.move.to), - promotion: node.move.promotion - ? parsePiece(node.move.promotion)?.role - : undefined, - }) === puzzle.moves[currentMove] - ) { + if (node.move && makeUci(node.move) === puzzle.moves[currentMove]) { currentMove++; } else { break; @@ -79,14 +76,12 @@ function PuzzleBoard({ const turn = pos?.turn || "white"; const showCoordinates = useAtomValue(showCoordinatesAtom); - function checkMove(move: string) { + function checkMove(move: Move) { if (!pos) return; const newPos = pos.clone(); - newPos.play(parseUci(move)!); - if ( - puzzle && - (puzzle.moves[currentMove] === move || newPos.isCheckmate()) - ) { + const uci = makeUci(move); + newPos.play(move); + if (puzzle && (puzzle.moves[currentMove] === uci || newPos.isCheckmate())) { if (currentMove === puzzle.moves.length - 1) { if (puzzle.completion !== "incorrect") { changeCompletion("correct"); @@ -135,13 +130,7 @@ function PuzzleBoard({ cancelMove={() => setPendingMove(null)} confirmMove={(p) => { if (pendingMove) { - checkMove( - makeUci({ - from: pendingMove.from, - to: pendingMove.to, - promotion: p, - }), - ); + checkMove({ ...pendingMove, promotion: p }); setPendingMove(null); } }} @@ -173,12 +162,14 @@ function PuzzleBoard({ ) { setPendingMove(move); } else { - checkMove(makeUci(move)); + checkMove(move); } }, }, }} - lastMove={moveToKey(currentNode.move)} + lastMove={ + currentNode.move ? chessgroundMove(currentNode.move) : undefined + } turnColor={turn} fen={currentNode.fen} check={pos?.isCheck()} diff --git a/src/components/puzzles/Puzzles.tsx b/src/components/puzzles/Puzzles.tsx index 5271c4c2..50329a2e 100644 --- a/src/components/puzzles/Puzzles.tsx +++ b/src/components/puzzles/Puzzles.tsx @@ -5,7 +5,6 @@ import { tabsAtom, } from "@/atoms/atoms"; import { commands } from "@/bindings"; -import { parseUci } from "@/utils/chess"; import { unwrap } from "@/utils/invoke"; import { Completion, @@ -34,7 +33,7 @@ import { } from "@mantine/core"; import { useLocalStorage, useSessionStorage } from "@mantine/hooks"; import { IconPlus, IconX, IconZoomCheck } from "@tabler/icons-react"; -import { Chess } from "chessops"; +import { Chess, parseUci } from "chessops"; import { parseFen } from "chessops/fen"; import { useAtom, useSetAtom } from "jotai"; import { useContext, useEffect, useState } from "react"; @@ -82,7 +81,7 @@ function Puzzles({ id }: { id: string }) { function setPuzzle(puzzle: { fen: string; moves: string[] }) { dispatch({ type: "SET_FEN", payload: puzzle.fen }); - dispatch({ type: "MAKE_MOVE", payload: parseUci(puzzle.moves[0]) }); + dispatch({ type: "MAKE_MOVE", payload: parseUci(puzzle.moves[0])! }); } function generatePuzzle(db: string) { @@ -272,7 +271,7 @@ function Puzzles({ id }: { id: string }) { for (let i = 0; i < curPuzzle.moves.length; i++) { dispatch({ type: "MAKE_MOVE", - payload: parseUci(curPuzzle.moves[i]), + payload: parseUci(curPuzzle.moves[i])!, mainline: true, }); await new Promise((r) => setTimeout(r, 500)); diff --git a/src/utils/chess.ts b/src/utils/chess.ts index 65eb2e6e..a4c736b0 100644 --- a/src/utils/chess.ts +++ b/src/utils/chess.ts @@ -1,13 +1,12 @@ import { Score, commands } from "@/bindings"; import { MantineColor } from "@mantine/core"; import { invoke } from "@tauri-apps/api"; -import { Chess, Move, Square } from "chess.js"; import { DrawShape } from "chessground/draw"; -import { Key } from "chessground/types"; -import { Color, Role, makeSquare, makeUci, parseSquare } from "chessops"; -import { INITIAL_FEN, parseFen, parsePiece } from "chessops/fen"; +import { Color, Role, makeSquare, makeUci } from "chessops"; +import { INITIAL_FEN, makeFen, parseFen } from "chessops/fen"; import { isPawns, parseComment } from "chessops/pgn"; -import { warn } from "tauri-plugin-log-api"; +import { makeSan, parseSan } from "chessops/san"; +import { positionFromFen } from "./chessops"; import { Outcome } from "./db"; import { harmonicMean, mean } from "./misc"; import { INITIAL_SCORE, formatScore, getAccuracy, getCPLoss } from "./score"; @@ -90,7 +89,7 @@ export function getMoveText( const moveNumber = Math.ceil(tree.halfMoves / 2); let moveText = ""; - if (tree.move) { + if (tree.san) { if (isBlack) { if (opt.isFirst) { moveText += `${moveNumber}... `; @@ -98,7 +97,7 @@ export function getMoveText( } else { moveText += `${moveNumber}. `; } - moveText += tree.move.san; + moveText += tree.san; if (opt.glyphs) { moveText += tree.annotation; } @@ -155,15 +154,7 @@ export function getMainLine(root: TreeNode): string[] { while (node.children.length > 0) { node = node.children[0]; if (node.move) { - moves.push( - makeUci({ - from: parseSquare(node.move.from), - to: parseSquare(node.move.to), - promotion: node.move.promotion - ? parsePiece(node.move.promotion)?.role - : undefined, - }), - ); + moves.push(makeUci(node.move)); } } return moves; @@ -175,15 +166,7 @@ export function getVariationLine(root: TreeNode, position: number[]): string[] { for (const pos of position) { node = node.children[pos]; if (node.move) { - moves.push( - makeUci({ - from: parseSquare(node.move.from), - to: parseSquare(node.move.to), - promotion: node.move.promotion - ? parsePiece(node.move.promotion)?.role - : undefined, - }), - ); + moves.push(makeUci(node.move)); } } return moves; @@ -265,16 +248,6 @@ export function getPGN( } return pgn.trim(); } -export function moveToKey(move: Move | null) { - return move ? ([move.from, move.to] as Key[]) : []; -} - -export function parseUci(move: string) { - const from = move.substring(0, 2) as Square; - const to = move.substring(2, 4) as Square; - const promotion = move.length === 5 ? move[4] : undefined; - return { from, to, promotion }; -} export function parseKeyboardMove(san: string, fen: string) { function cleanSan(san: string) { @@ -288,28 +261,20 @@ export function parseKeyboardMove(san: string, fen: string) { return san; } - function makeMove(fen: string, san: string) { - const chess = new Chess(fen); - const move = chess.move(san); - if (move) { - return { - from: move.from as Square, - to: move.to as Square, - promotion: move.promotion, - }; - } + const [pos] = positionFromFen(fen); + if (!pos) { return null; } - try { - return makeMove(fen, san); - } catch (e) { - try { - return makeMove(fen, cleanSan(san)); - } catch (e) { - warn(e as string); - return null; - } + const move = parseSan(pos, san); + if (move) { + return move; + } + const newSan = cleanSan(san); + const newMove = parseSan(pos, newSan); + if (newMove) { + return newMove; } + return null; } export async function getOpening( @@ -417,17 +382,21 @@ function innerParsePGN( } else if (token.type === "Nag") { root.annotation = NAG_INFO.get(token.value) || ""; } else if (token.type === "San") { - const chess = new Chess(root.fen); - let m: Move; - try { - m = chess.move(token.value); - } catch (error) { + const [pos, error] = positionFromFen(root.fen); + if (error) { + continue; + } + const move = parseSan(pos, token.value); + if (!move) { continue; } + const san = makeSan(pos, move); + pos.play(move); const newTree = createNode({ - fen: chess.fen(), - move: m, + fen: makeFen(pos.toSetup()), + move, + san, halfMoves: root.halfMoves + 1, }); root.children.push(newTree); diff --git a/src/utils/repertoire.ts b/src/utils/repertoire.ts index 324381b5..20b3831f 100644 --- a/src/utils/repertoire.ts +++ b/src/utils/repertoire.ts @@ -94,7 +94,7 @@ export async function openingReport({ // Check if there's any opening with a 5% or more frequency that isn't a child of item.node for (const opening of filteredOpenings) { const child = item.node.children.find( - (child) => child.move?.san === opening.move, + (child) => child.san === opening.move, ); if (!child && opening.move !== "*") { missingMoves.push({ diff --git a/src/utils/tests/treeReducer.test.ts b/src/utils/tests/treeReducer.test.ts index ee129a30..538b9b25 100644 --- a/src/utils/tests/treeReducer.test.ts +++ b/src/utils/tests/treeReducer.test.ts @@ -1,25 +1,28 @@ import { Chess } from "chess.js"; +import { parseUci } from "chessops"; import { expect, test } from "vitest"; -import { MoveAnalysis } from "../chess"; import treeReducer, { TreeState, defaultTree } from "../treeReducer"; const chess = new Chess(); -const e4 = chess.move("e4"); -const d5 = chess.move("d5"); +const e4 = parseUci("e2e4")!; +const d5 = parseUci("d7d5")!; const treeE4D5: () => TreeState = () => ({ ...defaultTree(), position: [0, 0], root: { fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", move: null, + san: null, children: [ { fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", move: e4, + san: "e4", children: [ { fen: "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", move: d5, + san: "d5", children: [], score: null, depth: null, @@ -40,7 +43,8 @@ const treeE4D5: () => TreeState = () => ({ }, { fen: "rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1", - move: new Chess().move("d4"), + move: parseUci("d2d4")!, + san: "d4", children: [], score: null, depth: null, @@ -183,10 +187,12 @@ test("should handle MAKE_MOVE", () => { root: { fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", move: null, + san: null, children: [ { fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - move: new Chess().move("e4"), + move: e4, + san: "e4", children: [], score: null, depth: null, @@ -209,10 +215,7 @@ test("should handle MAKE_MOVE", () => { expectState({ res: treeReducer(initialState, { type: "MAKE_MOVE", - payload: { - from: "e2", - to: "e4", - }, + payload: e4, }), initialState, expectedState, @@ -549,36 +552,17 @@ test("promote 2", () => { root: { fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", move: null, + san: null, children: [ { fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - move: { - color: "w", - piece: "p", - from: "e2", - to: "e4", - san: "e4", - flags: "b", - lan: "e2e4", - before: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - after: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - }, + move: e4, + san: "e4", children: [ { fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - move: { - color: "b", - piece: "p", - from: "e7", - to: "e5", - san: "e5", - flags: "b", - lan: "e7e5", - before: - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - after: - "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - }, + move: parseUci("e7e5")!, + san: "e5", children: [], score: null, depth: null, @@ -590,19 +574,8 @@ test("promote 2", () => { }, { fen: "rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - move: { - color: "b", - piece: "p", - from: "e7", - to: "e6", - san: "e6", - flags: "n", - lan: "e7e6", - before: - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - after: - "rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - }, + move: parseUci("d7d6")!, + san: "d6", children: [], score: null, depth: null, @@ -646,36 +619,17 @@ test("promote 2", () => { root: { fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", move: null, + san: null, children: [ { fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - move: { - color: "w", - piece: "p", - from: "e2", - to: "e4", - san: "e4", - flags: "b", - lan: "e2e4", - before: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - after: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - }, + move: e4, + san: "e4", children: [ { fen: "rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - move: { - color: "b", - piece: "p", - from: "e7", - to: "e6", - san: "e6", - flags: "n", - lan: "e7e6", - before: - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - after: - "rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - }, + move: parseUci("d7d6")!, + san: "d6", children: [], score: null, depth: null, @@ -687,19 +641,8 @@ test("promote 2", () => { }, { fen: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - move: { - color: "b", - piece: "p", - from: "e7", - to: "e5", - san: "e5", - flags: "b", - lan: "e7e5", - before: - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", - after: - "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", - }, + move: parseUci("e7e5")!, + san: "e5", children: [], score: null, depth: null, diff --git a/src/utils/treeReducer.ts b/src/utils/treeReducer.ts index c4c7bce4..a5aca146 100644 --- a/src/utils/treeReducer.ts +++ b/src/utils/treeReducer.ts @@ -1,9 +1,11 @@ import { BestMoves, Score } from "@/bindings"; -import { Chess, Move, Square } from "chess.js"; import { DrawShape } from "chessground/draw"; -import { INITIAL_FEN, parseFen } from "chessops/fen"; +import { Move, isNormal } from "chessops"; +import { INITIAL_FEN, makeFen, parseFen } from "chessops/fen"; +import { makeSan, parseSan } from "chessops/san"; import { match } from "ts-pattern"; import { Annotation } from "./chess"; +import { positionFromFen } from "./chessops"; import { Outcome } from "./db"; import { isPrefix } from "./misc"; import { getAnnotation } from "./score"; @@ -18,6 +20,7 @@ export interface TreeState { export interface TreeNode { fen: string; move: Move | null; + san: string | null; children: TreeNode[]; score: Score | null; depth: number | null; @@ -73,6 +76,7 @@ export function defaultTree(fen?: string): TreeState { root: { fen: fen?.trim() ?? INITIAL_FEN, move: null, + san: null, children: [], score: null, depth: null, @@ -97,15 +101,18 @@ export function defaultTree(fen?: string): TreeState { export function createNode({ fen, move, + san, halfMoves, }: { move: Move; + san: string; fen: string; halfMoves: number; }): TreeNode { return { fen, move, + san, children: [], score: null, depth: null, @@ -191,26 +198,14 @@ export type TreeAction = | { type: "SET_START"; payload: number[] } | { type: "MAKE_MOVE"; - payload: - | { - from: Square; - to: Square; - promotion?: string; - } - | string; + payload: Move | string; changePosition?: boolean; mainline?: boolean; clock?: number; } | { type: "APPEND_MOVE"; - payload: - | { - from: Square; - to: Square; - promotion?: string; - } - | string; + payload: Move; clock?: number; } | { type: "MAKE_MOVES"; payload: string[]; mainline?: boolean } @@ -263,6 +258,15 @@ const treeReducer = (state: TreeState, action: TreeAction) => { .with( { type: "MAKE_MOVE" }, ({ payload, changePosition, mainline, clock }) => { + if (typeof payload === "string") { + const node = getNodeAtPath(state.root, state.position); + if (!node) return; + const [pos] = positionFromFen(node.fen); + if (!pos) return; + const move = parseSan(pos, payload); + if (!move) return; + payload = move; + } if (clock) { const node = getNodeAtPath(state.root, state.position); if (!node) return; @@ -287,8 +291,14 @@ const treeReducer = (state: TreeState, action: TreeAction) => { }) .with({ type: "MAKE_MOVES" }, ({ payload, mainline }) => { state.dirty = true; + const node = getNodeAtPath(state.root, state.position); + const [pos] = positionFromFen(node.fen); + if (!pos) return; for (const move of payload) { - makeMove({ state, move, last: false, mainline }); + const m = parseSan(pos, move); + if (!m) return; + pos.play(m); + makeMove({ state, move: m, last: false, mainline }); } }) .with({ type: "GO_TO_START" }, () => { @@ -395,10 +405,14 @@ function is50MoveRule(state: TreeState) { let count = 0; for (const i of state.position) { count += 1; + const [pos] = positionFromFen(node.fen); + if (!pos) return false; if ( - node.move?.captured || - node.move?.promotion || - node.move?.piece === "p" + node.move && + isNormal(node.move) && + (node.move.promotion || + pos.board.get(node.move.to) || + pos.board.get(node.move.from)?.role === "pawn") ) { count = 0; } @@ -415,7 +429,7 @@ function makeMove({ mainline = false, }: { state: TreeState; - move: { from: Square; to: Square; promotion?: string } | string; + move: Move; last: boolean; changePosition?: boolean; mainline?: boolean; @@ -426,27 +440,26 @@ function makeMove({ : state.position; const moveNode = getNodeAtPath(state.root, position); if (!moveNode) return; - const chess = new Chess(moveNode.fen); - let m: Move; - try { - m = chess.move(move); - } catch (e) { - return; - } - if (chess.isGameOver()) { - if (chess.isCheckmate()) { - state.headers.result = chess.turn() === "w" ? "0-1" : "1-0"; + const [pos] = positionFromFen(moveNode.fen); + if (!pos) return; + const san = makeSan(pos, move); + pos.play(move); + if (pos.isEnd()) { + if (pos.isCheckmate()) { + state.headers.result = pos.turn === "white" ? "0-1" : "1-0"; } - if (chess.isDraw()) { + if (pos.isStalemate() || pos.isInsufficientMaterial()) { state.headers.result = "1/2-1/2"; } } - if (isThreeFoldRepetition(state, chess.fen()) || is50MoveRule(state)) { + const newFen = makeFen(pos.toSetup()); + + if (isThreeFoldRepetition(state, newFen) || is50MoveRule(state)) { state.headers.result = "1/2-1/2"; } - const i = moveNode.children.findIndex((n) => n.move?.san === m.san); + const i = moveNode.children.findIndex((n) => n.san === san); if (i !== -1) { if (changePosition) { if (state.position === position) { @@ -458,8 +471,9 @@ function makeMove({ } else { state.dirty = true; const newMoveNode = createNode({ - fen: chess.fen(), - move: m, + fen: newFen, + move, + san, halfMoves: moveNode.halfMoves + 1, }); if (mainline) { @@ -533,7 +547,8 @@ function addAnalysis( const setup = parseFen(state.root.fen).unwrap(); const initialColor = setup.turn; while (cur !== undefined && i < analysis.length) { - if (!new Chess(cur.fen).isGameOver()) { + const [pos] = positionFromFen(cur.fen); + if (pos && !pos.isEnd() && analysis[i].best.length > 0) { cur.score = analysis[i].best[0].score; if (analysis[i].novelty) { cur.commentHTML = "Novelty"; @@ -559,7 +574,7 @@ function addAnalysis( color, prevMoves, analysis[i].is_sacrifice, - cur.move?.san ?? "", + cur.move ? makeSan(pos, cur.move) : "", ); } cur = cur.children[0];