diff --git a/app/src/chess.ts b/app/src/chess.ts index ac9b6e5..63dea71 100644 --- a/app/src/chess.ts +++ b/app/src/chess.ts @@ -1,6 +1,7 @@ +import nearley from 'nearley'; import filter from 'lodash/filter'; import isEqual from 'lodash/isEqual'; -import find from 'lodash/find'; +import uniqWith from 'lodash/uniqWith'; import { postMessage, squareToCoords, @@ -16,14 +17,13 @@ import { TArea, TPiece, IMoveTemplate, - IPotentialMoves, IMove, TFromTo, - TMoveType, - Nullable, } from './types'; import { i18n } from './i18n'; +import grammar, { Move as ParsedMove } from './grammar/move.cjs'; + /** * Check if input is valid square name */ @@ -137,7 +137,7 @@ export function makeMove( /** * Get exact from and to coords from move data */ -export function getLegalMoves(board: IChessboard, potentialMoves: IPotentialMoves) : IMove[] { +export function getLegalMoves(board: IChessboard, potentialMoves: IMoveTemplate[]) : IMove[] { if (!board || !potentialMoves.length || !board.isPlayersMove()) { return []; } @@ -176,7 +176,7 @@ export function getLegalMoves(board: IChessboard, potentialMoves: IPotentialMove ]; }); - return excludeConflictingMoves(legalMoves); + return excludeConflictingMoves(pickMostSpecificMoves(legalMoves)); } /** @@ -197,136 +197,80 @@ export function excludeConflictingMoves(moves: IMove[]) : IMove[] { } /** - * Parse message input by user + * Sometimes returned moves are essentially the same or similar + * This method omits less specific moves + * Example1: + * Input: [{ piece: '.', from: 'b4', to: 'b5' }, { piece: 'p', from: 'b4', to: 'b5' }] + * Output: [{ piece: 'p', from: 'b4', to: 'b5' }] */ -export function parseMoveInput(input: string): IPotentialMoves { - return [ - ...parseUCI(input), - ...parseAlgebraic(input), - ]; +function pickMostSpecificMoves(moves: IMove[]) : IMove[] { + const result: IMove[] = []; + const movesDict: Record = {}; + moves.forEach(move => { + if (!movesDict[move.from + move.to]) { + movesDict[move.from + move.to] = move; + } else { + // Override with the most specific piece + if (movesDict[move.from + move.to].piece === '.' && move.piece !== '.') { + movesDict[move.from + move.to] = move; + } + } + }); + return Object.values(movesDict); } /** - * Parse simplest move format: 'e2e4' + * Parse message input by user */ -export function parseUCI(input: string) : IPotentialMoves { - const filteredSymbols = input.replace(/( |-)+/g, ''); - const fromSquare = filteredSymbols.slice(0, 2); - const toSquare = filteredSymbols.slice(2, 4); - const promotion = filteredSymbols.slice(4, 5); - - if (validateSquareName(fromSquare) && validateSquareName(toSquare)) { - const result: IMoveTemplate = { - piece: '.', - from: fromSquare, - to: toSquare, - }; - - if (promotion) { - result.promotionPiece = promotion; - } - - return [result]; +export function parseMoveInput(input: string): IMoveTemplate[] { + const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); + + try { + parser.feed(input); + const parsed = uniqWith(parser.results, isEqual) as ParsedMove[]; // Cast to expected type + const results: IMoveTemplate[] = []; + parsed.forEach(p => { + const movesTemplates = mapParseResult(p); + movesTemplates.forEach(mt => results.push(mt)); + }); + return results; + } catch (error) { + // Do nothing } return []; } -/** - * Extract all possible information from algebraic notation - */ -export function parseAlgebraic(input: string): IPotentialMoves { - // ignore UCI notation - if (/^\s*[a-h][1-8][a-h][1-8][rqknb]?\s*$/.test(input)) { - return []; - } - - let moveString = input.replace(/[\s\-\(\)]+/g, ''); - const moves: IPotentialMoves = []; - - if (/[o0][o0][o0]/i.test(moveString)) { - return [ - // white long castling - { - piece: 'k', - from: 'e1', - to: 'c1', - }, - // black long castling - { - piece: 'k', - from: 'e8', - to: 'c8', - } - ]; - } else if (/[o0][o0]/i.test(moveString)) { - return [ - // white short castling - { - piece: 'k', - from: 'e1', - to: 'g1', - }, - // black short castling - { - piece: 'k', - from: 'e8', - to: 'g8', - } - ]; - } - - - const pawnRegex = /^([a-h])?(x)?([a-h])([1-8])(e\.?p\.?)?(=[qrnbQRNB])?[+#]?$/; - const pawnResult = moveString.match(pawnRegex); - if (pawnResult) { - const [ - _, - fromFile, - isCapture, - toFile, - toRank, - enPassant, - promotion, - ] = pawnResult; - - if (fromFile === toFile) { - // Do nothing - // This disables moves like `bb4` for pawns to avoid ambiguity with bishops - } else { - const move: IMoveTemplate = { - piece: 'p', - from: `${fromFile || '.'}.`, - to: `${toFile || '.'}${toRank || '.'}`, - }; - - if (promotion) { - move.promotionPiece = promotion[1].toLowerCase(); +function mapParseResult(move: ParsedMove): IMoveTemplate[] { + if (move.type === 'castling') { + if (move.castlingData.kind === 'long') { + return [ + { piece: 'k', from: 'e1', to: 'c1' }, // white long castling + { piece: 'k', from: 'e8', to: 'c8' } // black long castling + ]; + } else if (move.castlingData.kind === 'short') { + return [ + { piece: 'k', from: 'e1', to: 'g1' }, // white short castling + { piece: 'k', from: 'e8', to: 'g8' } // black short castling + ]; + } + } else if (move.type === 'uci') { + return [move.uciData]; + } else if (move.type === 'algebraic') { + if (move.algebraicData.type === 'pawn') { + const fromFile = move.algebraicData.pawnData.from[0]; + const toFile = move.algebraicData.pawnData.to[0]; + if (fromFile === toFile) { + // Do nothing + // This disables moves like `bb4` for pawns to avoid ambiguity with bishops + return []; + } else { + return [move.algebraicData.pawnData]; } - - moves.push(move); + } else if (move.algebraicData.type === 'piece') { + return [move.algebraicData.pieceData]; } } - const pieceRegex = /^([RQKNBrqknb])([a-h])?([1-8])?(x)?([a-h])([1-8])?[+#]?$/; - const pieceResult = moveString.match(pieceRegex); - if (pieceResult) { - const [ - _, - pieceName, - fromFile, - fromVer, - isCapture, - toFile, - toRank, - ] = pieceResult; - - moves.push({ - piece: (pieceName).toLowerCase(), - from: `${fromFile || '.'}${fromVer || '.'}`, - to: `${toFile || '.'}${toRank || '.'}`, - }); - } - - return moves; + return []; } diff --git a/app/src/grammar/move.cjs b/app/src/grammar/move.cjs new file mode 100644 index 0000000..a44855f --- /dev/null +++ b/app/src/grammar/move.cjs @@ -0,0 +1,94 @@ +// Generated automatically by nearley, version 2.20.1 +// http://github.com/Hardmath123/nearley +(function () { +function id(x) { return x[0]; } +var grammar = { + Lexer: undefined, + ParserRules: [ + {"name": "Move", "symbols": ["UciMove"], "postprocess": (data) => ({ type: "uci", uciData: data[0] })}, + {"name": "Move", "symbols": ["AlgebraicMove"], "postprocess": (data) => ({ type: "algebraic", algebraicData: data[0] })}, + {"name": "Move", "symbols": ["CastlingMove"], "postprocess": (data) => ({ type: "castling", castlingData: data[0] })}, + {"name": "UciMove", "symbols": ["UciCoord", "UciCoord", "UciPromotion"], "postprocess": (data) => { + const result = { piece: ".", from: data[0], to: data[1] }; + if (data[2]) { + result.promotionPiece = data[2] + } + return result; + } }, + {"name": "UciCoord", "symbols": ["File", "Rank"], "postprocess": (data) => data[0] + data[1]}, + {"name": "UciPromotion", "symbols": ["NotKingPiece"], "postprocess": (d) => d[0]}, + {"name": "UciPromotion", "symbols": [], "postprocess": () => undefined}, + {"name": "AlgebraicMove", "symbols": ["PieceMove", "CheckOrMate"], "postprocess": (data) => ({ type: "piece", pieceData: data[0] })}, + {"name": "AlgebraicMove", "symbols": ["PawnMove", "CheckOrMate"], "postprocess": (data) => ({ type: "pawn", pawnData: data[0] })}, + {"name": "PawnMove", "symbols": ["MaybeFile", "Capture", "File", "Rank", "Promotion"], "postprocess": (data) => { + const result = { piece: "p", from: data[0] + '.', to: data[2] + data[3] }; + if (data[4]) { + result.promotionPiece = data[4] + } + return result; + } }, + {"name": "PieceMove", "symbols": ["Piece", "MaybeFile", "MaybeRank", "Capture", "File", "Rank"], "postprocess": (data) => ({ + piece: data[0], + from: data[1] + data[2], + to: data[4] + data[5], + }) }, + {"name": "CastlingMove", "symbols": ["ShortCastling"], "postprocess": (data) => ({ kind: 'short' })}, + {"name": "CastlingMove", "symbols": ["LongCastling"], "postprocess": (data) => ({ kind: 'long' })}, + {"name": "LongCastling", "symbols": ["CastlingChar", "CastlingSeparator", "CastlingChar", "CastlingSeparator", "CastlingChar"]}, + {"name": "ShortCastling", "symbols": ["CastlingChar", "CastlingSeparator", "CastlingChar"]}, + {"name": "CastlingSeparator", "symbols": [{"literal":"-"}]}, + {"name": "CastlingSeparator", "symbols": []}, + {"name": "CastlingChar", "symbols": [{"literal":"o"}]}, + {"name": "CastlingChar", "symbols": [{"literal":"O"}]}, + {"name": "CastlingChar", "symbols": [{"literal":"0"}]}, + {"name": "Capture", "symbols": [{"literal":"x"}]}, + {"name": "Capture", "symbols": []}, + {"name": "Promotion", "symbols": [{"literal":"="}, "NotKingPiece"], "postprocess": (data) => data[1]}, + {"name": "Promotion", "symbols": [], "postprocess": () => undefined}, + {"name": "Piece", "symbols": ["NotKingPiece"], "postprocess": (d) => d[0]}, + {"name": "Piece", "symbols": ["KingPiece"], "postprocess": (d) => d[0]}, + {"name": "NotKingPiece", "symbols": [{"literal":"r"}], "postprocess": () => "r"}, + {"name": "NotKingPiece", "symbols": [{"literal":"n"}], "postprocess": () => "n"}, + {"name": "NotKingPiece", "symbols": [{"literal":"b"}], "postprocess": () => "b"}, + {"name": "NotKingPiece", "symbols": [{"literal":"q"}], "postprocess": () => "q"}, + {"name": "NotKingPiece", "symbols": [{"literal":"R"}], "postprocess": () => "r"}, + {"name": "NotKingPiece", "symbols": [{"literal":"N"}], "postprocess": () => "n"}, + {"name": "NotKingPiece", "symbols": [{"literal":"B"}], "postprocess": () => "b"}, + {"name": "NotKingPiece", "symbols": [{"literal":"Q"}], "postprocess": () => "q"}, + {"name": "CheckOrMate", "symbols": ["Check"]}, + {"name": "CheckOrMate", "symbols": ["Mate"]}, + {"name": "Check", "symbols": [{"literal":"+"}]}, + {"name": "Check", "symbols": []}, + {"name": "Mate", "symbols": [{"literal":"#"}]}, + {"name": "Mate", "symbols": []}, + {"name": "KingPiece", "symbols": [{"literal":"K"}], "postprocess": () => "k"}, + {"name": "KingPiece", "symbols": [{"literal":"k"}], "postprocess": () => "k"}, + {"name": "File", "symbols": [{"literal":"a"}], "postprocess": () => "a"}, + {"name": "File", "symbols": [{"literal":"b"}], "postprocess": () => "b"}, + {"name": "File", "symbols": [{"literal":"c"}], "postprocess": () => "c"}, + {"name": "File", "symbols": [{"literal":"d"}], "postprocess": () => "d"}, + {"name": "File", "symbols": [{"literal":"e"}], "postprocess": () => "e"}, + {"name": "File", "symbols": [{"literal":"f"}], "postprocess": () => "f"}, + {"name": "File", "symbols": [{"literal":"g"}], "postprocess": () => "g"}, + {"name": "File", "symbols": [{"literal":"h"}], "postprocess": () => "h"}, + {"name": "MaybeFile", "symbols": ["File"], "postprocess": (d) => d[0]}, + {"name": "MaybeFile", "symbols": [], "postprocess": () => '.'}, + {"name": "Rank", "symbols": [{"literal":"1"}]}, + {"name": "Rank", "symbols": [{"literal":"2"}]}, + {"name": "Rank", "symbols": [{"literal":"3"}]}, + {"name": "Rank", "symbols": [{"literal":"4"}]}, + {"name": "Rank", "symbols": [{"literal":"5"}]}, + {"name": "Rank", "symbols": [{"literal":"6"}]}, + {"name": "Rank", "symbols": [{"literal":"7"}]}, + {"name": "Rank", "symbols": [{"literal":"8"}]}, + {"name": "MaybeRank", "symbols": ["Rank"], "postprocess": (d) => d[0]}, + {"name": "MaybeRank", "symbols": [], "postprocess": () => '.'} +] + , ParserStart: "Move" +} +if (typeof module !== 'undefined'&& typeof module.exports !== 'undefined') { + module.exports = grammar; +} else { + window.grammar = grammar; +} +})(); diff --git a/app/src/grammar/move.cjs.d.ts b/app/src/grammar/move.cjs.d.ts new file mode 100644 index 0000000..30b44e9 --- /dev/null +++ b/app/src/grammar/move.cjs.d.ts @@ -0,0 +1,33 @@ +import { CompiledRules } from 'nearley'; +export type Move = + | { type: 'uci'; uciData: UciMove } + | { type: 'algebraic'; algebraicData: AlgebraicMove } + | { type: 'castling'; castlingData: CastlingMove }; +export type UciMove = { piece: '.', from: UciCoord; to: UciCoord, promotionPiece?: Promotion }; +export type UciCoord = string; +export type AlgebraicMove = + | { type: 'piece'; pieceData: PieceMove } + | { type: 'pawn'; pawnData: PawnMove }; +export type PawnMove = { + piece: 'p'; + from: string; + to: string; + promotionPiece?: Promotion; +}; +export type PieceMove = { + piece: Piece; + from: string; + to: string; +}; +export type CastlingMove = { kind: 'short' } | { kind: 'long' }; +export type CastlingChar = string; +export type Capture = 'x'; +export type Promotion = NotKingPiece; +export type Piece = NotKingPiece | KingPiece; +export type NotKingPiece = 'r' | 'n' | 'b' | 'q'; +export type KingPiece = 'k'; +export type File = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h'; +export type Rank = string; + +declare const grammar:CompiledRules; +export default grammar; diff --git a/app/src/grammar/move.ne b/app/src/grammar/move.ne new file mode 100644 index 0000000..ad9163b --- /dev/null +++ b/app/src/grammar/move.ne @@ -0,0 +1,86 @@ +Move + -> UciMove {% (data) => ({ type: "uci", uciData: data[0] }) %} + | AlgebraicMove {% (data) => ({ type: "algebraic", algebraicData: data[0] }) %} + | CastlingMove {% (data) => ({ type: "castling", castlingData: data[0] }) %} + +UciMove + -> + UciCoord UciCoord UciPromotion + {% (data) => { + const result = { piece: ".", from: data[0], to: data[1] }; + if (data[2]) { + result.promotionPiece = data[2] + } + return result; + } %} +UciCoord -> File Rank {% (data) => data[0] + data[1] %} +UciPromotion + -> NotKingPiece {% (d) => d[0] %} + | null {% () => undefined %} + +AlgebraicMove + -> PieceMove CheckOrMate {% (data) => ({ type: "piece", pieceData: data[0] }) %} + | PawnMove CheckOrMate {% (data) => ({ type: "pawn", pawnData: data[0] }) %} +PawnMove + -> + MaybeFile Capture File Rank Promotion + {% (data) => { + const result = { piece: "p", from: data[0] + '.', to: data[2] + data[3] }; + if (data[4]) { + result.promotionPiece = data[4] + } + return result; + } %} +PieceMove -> Piece MaybeFile MaybeRank Capture File Rank + {% (data) => ({ + piece: data[0], + from: data[1] + data[2], + to: data[4] + data[5], + }) %} +CastlingMove + -> ShortCastling {% (data) => ({ kind: 'short' }) %} + | LongCastling {% (data) => ({ kind: 'long' }) %} +LongCastling -> CastlingChar CastlingSeparator CastlingChar CastlingSeparator CastlingChar +ShortCastling ->CastlingChar CastlingSeparator CastlingChar +CastlingSeparator -> "-" | null +CastlingChar -> "o" | "O" | "0" +Capture -> "x" | null +Promotion + -> "=" NotKingPiece {% (data) => data[1] %} + | null {% () => undefined %} +Piece + -> NotKingPiece {% (d) => d[0] %} + | KingPiece {% (d) => d[0] %} +NotKingPiece + -> "r" {% () => "r" %} + | "n" {% () => "n" %} + | "b" {% () => "b" %} + | "q" {% () => "q" %} + | "R" {% () => "r" %} + | "N" {% () => "n" %} + | "B" {% () => "b" %} + | "Q" {% () => "q" %} +CheckOrMate -> Check | Mate +Check -> "+" | null +Mate -> "#" | null + +KingPiece + -> "K" {% () => "k" %} + | "k" {% () => "k" %} + +File + -> "a" {% () => "a" %} + | "b" {% () => "b" %} + | "c" {% () => "c" %} + | "d" {% () => "d" %} + | "e" {% () => "e" %} + | "f" {% () => "f" %} + | "g" {% () => "g" %} + | "h" {% () => "h" %} +MaybeFile + -> File {% (d) => d[0] %} + | null {% () => '.' %} +Rank -> "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" +MaybeRank + -> Rank {% (d) => d[0] %} + | null {% () => '.' %} diff --git a/app/src/types.ts b/app/src/types.ts index 20311ca..53838b0 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -38,8 +38,6 @@ export interface IMoveTemplate { promotionPiece?: TPiece } -export type IPotentialMoves = IMoveTemplate[]; - export interface IMove extends IMoveTemplate { from: TArea } diff --git a/app/test/test-chess.ts b/app/test/test-chess.ts index 14880ab..008ccea 100644 --- a/app/test/test-chess.ts +++ b/app/test/test-chess.ts @@ -4,8 +4,6 @@ import assert from 'assert'; jsDomGlobal(); import { - parseAlgebraic, - parseUCI, getLegalMoves, parseMoveInput, } from '../src/chess'; @@ -16,101 +14,92 @@ import { function getExecutionTime(fn: Function, cycles: number = 1000): number { const start = Date.now(); - for (let i = 0; i < cycles; i++) { - fn(); - } + for (let i = 0; i < cycles; i++) fn(); const end = Date.now(); return end - start; } -describe('parseAlgebraic', function() { - it('parses short algebraic moves', function() { - assert.deepEqual(parseAlgebraic('Rd2'), [{ +describe('parseMoveInput', function() { + it('parses short piece moves (Rd2)', function() { + assert.deepEqual(parseMoveInput('Rd2'), [{ piece: 'r', from: '..', to: 'd2', }]); }); - it('parses pawn moves', function() { - assert.deepEqual(parseAlgebraic('d2'), [{ + it('parses short pawn moves (d2)', function() { + assert.deepEqual(parseMoveInput('d2'), [{ piece: 'p', from: '..', to: 'd2', }]); }); - it('parses full moves', function() { - assert.deepEqual(parseAlgebraic('Re2d2'), [{ + it('parses piece full moves (Re2d2)', function() { + assert.deepEqual(parseMoveInput('Re2d2'), [{ piece: 'r', from: 'e2', to: 'd2', }]); }); - it('parses pawn captures', function() { - assert.deepEqual(parseAlgebraic('exd3'), [{ - piece: 'p', - from: 'e.', - to: 'd3', - }]); - - // en passant - assert.deepEqual(parseAlgebraic('exd3e.p.'), [{ + it('parses pawn captures (exd3)', function() { + assert.deepEqual(parseMoveInput('exd3'), [{ piece: 'p', from: 'e.', to: 'd3', }]); }); - it('parses piece captures', function() { - assert.deepEqual(parseAlgebraic('Rxd2'), [{ + it('parses piece captures (Rxd2)', function() { + assert.deepEqual(parseMoveInput('Rxd2'), [{ piece: 'r', from: '..', to: 'd2', }]); }); - it('parses full piece captures', function() { - assert.deepEqual(parseAlgebraic('Re2xd2'), [{ + it('parses full piece captures (Re2xd2)', function() { + assert.deepEqual(parseMoveInput('Re2xd2'), [{ piece: 'r', from: 'e2', to: 'd2', }]); }); - it('parses partial disambiguation', function() { - assert.deepEqual(parseAlgebraic('R2xd2'), [{ + it('parses partial disambiguation (R2xd2, Rexd2)', function() { + assert.deepEqual(parseMoveInput('R2xd2'), [{ piece: 'r', from: '.2', to: 'd2', }]); - assert.deepEqual(parseAlgebraic('Rexd2'), [{ + assert.deepEqual(parseMoveInput('Rexd2'), [{ piece: 'r', from: 'e.', to: 'd2', }]); }); - it('allows to mark a check', function() { - assert.deepEqual(parseAlgebraic('Rd2+'), [{ + it('allows to mark a check (Rd2+)', function() { + assert.deepEqual(parseMoveInput('Rd2+'), [{ piece: 'r', from: '..', to: 'd2', }]); }); - it('allows to mark a mate', function() { - assert.deepEqual(parseAlgebraic('Rd2#'), [{ + it('allows to mark a mate (Rd2#)', function() { + assert.deepEqual(parseMoveInput('Rd2#'), [{ piece: 'r', from: '..', to: 'd2', }]); }); - it('parses castling', function() { - assert.deepEqual(parseAlgebraic('o-o'), [ + it('parses castling (o-o, 0-0, ooo, 0-0-0)', function() { + assert.deepEqual(parseMoveInput('o-o'), [ { piece: 'k', from: 'e1', @@ -123,7 +112,7 @@ describe('parseAlgebraic', function() { } ]); - assert.deepEqual(parseAlgebraic('0-0'), [ + assert.deepEqual(parseMoveInput('0-0'), [ { piece: 'k', from: 'e1', @@ -136,7 +125,7 @@ describe('parseAlgebraic', function() { } ]); - assert.deepEqual(parseAlgebraic('ooo'), [ + assert.deepEqual(parseMoveInput('ooo'), [ { piece: 'k', from: 'e1', @@ -149,7 +138,7 @@ describe('parseAlgebraic', function() { } ]); - assert.deepEqual(parseAlgebraic('0-0-0'), [ + assert.deepEqual(parseMoveInput('0-0-0'), [ { piece: 'k', from: 'e1', @@ -163,12 +152,12 @@ describe('parseAlgebraic', function() { ]); }); - it('ignores not-existing pieces and squares', function() { - assert.deepEqual(parseAlgebraic('Xd2'), []); + it('ignores not-existing pieces and squares (Xd2)', function() { + assert.deepEqual(parseMoveInput('Xd2'), []); }); - it('parses pawn promotion', function() { - assert.deepEqual(parseAlgebraic('d8=Q'), [{ + it('parses pawn promotion (d8=Q)', function() { + assert.deepEqual(parseMoveInput('d8=Q'), [{ piece: 'p', from: '..', to: 'd8', @@ -176,27 +165,27 @@ describe('parseAlgebraic', function() { }]); }); - it('ignores promotion for pieces', function() { - assert.deepEqual(parseAlgebraic('Nd8=Q'), []); + it('ignores promotion for pieces (Nd8=Q)', function() { + assert.deepEqual(parseMoveInput('Nd8=Q'), []); }); - describe('allows lowercase piece letter if unambiguous', function() { + describe('allows lowercase piece letter if unambiguous (b3, Bb3, bc4, bxb3)', function() { it('b3', function () { - assert.deepEqual(parseAlgebraic('b3'), [{ + assert.deepEqual(parseMoveInput('b3'), [{ piece: 'p', from: '..', to: 'b3', }]); }); it('Bb3', function () { - assert.deepEqual(parseAlgebraic('Bb3'), [{ + assert.deepEqual(parseMoveInput('Bb3'), [{ piece: 'b', from: '..', to: 'b3', }]); }); it('bc4', function () { - assert.deepEqual(parseAlgebraic('bc4'), [ + assert.deepEqual(parseMoveInput('bc4'), [ { piece: 'p', from: 'b.', @@ -212,7 +201,7 @@ describe('parseAlgebraic', function() { it('bxb3', function () { // "From file" coordinate is redundant in case of a pawn move // Thus moves like that are interpreted as bishop moves - assert.deepEqual(parseAlgebraic('bxb3'), [ + assert.deepEqual(parseMoveInput('bxb3'), [ { piece: 'b', from: '..', @@ -222,30 +211,31 @@ describe('parseAlgebraic', function() { }); it('b2c3', function () { // This looks like a UCI move. Let's parse it as UCI - assert.deepEqual(parseAlgebraic('b2c3'), []); + assert.deepEqual(parseMoveInput('b2c3'), [ + { + from: 'b2', + piece: '.', + to: 'c3', + }, + { + from: '.2', + piece: 'b', + to: 'c3', + }, + ]); }); }); - it('returns null for UCI', function() { - assert.deepEqual(parseAlgebraic('e2e4'), []); - }); - - it('returns null for UCI with promotion', function() { - assert.deepEqual(parseAlgebraic('e7e8n'), []); - }); -}); - -describe('parseUCI', function() { - it('parses short algebraic moves', function() { - assert.deepEqual(parseUCI('e2e4'), [{ + it('parses simple UCL (e2e4)', function() { + assert.deepEqual(parseMoveInput('e2e4'), [{ piece: '.', from: 'e2', to: 'e4', }]); }); - it('parses promotion', function() { - assert.deepEqual(parseUCI('e7e8n'), [{ + it('parses promotion UCL (e7e8n)', function() { + assert.deepEqual(parseMoveInput('e7e8n'), [{ piece: '.', from: 'e7', promotionPiece: 'n', @@ -253,33 +243,14 @@ describe('parseUCI', function() { }]); }); - it('ignores non-existing squares', function() { - assert.deepEqual(parseUCI('x2e4'), []); - }); - - it('ignores other formats', function() { - assert.deepEqual(parseUCI('♞f3'), []); + it('ignores non-existing UCL squares (x2e4)', function() { + assert.deepEqual(parseMoveInput('x2e4'), []); }); -}); -describe('parseMoveInput', function() { - it('parces algebraic', function() { - assert.deepEqual(parseMoveInput('Nf3'), [{ - piece: 'n', - from: '..', - to: 'f3', - }]); + it('ignores other formats (♞f3)', function() { + assert.deepEqual(parseMoveInput('♞f3'), []); }); - it('parces UCI', function() { - assert.deepEqual(parseMoveInput('e2e4'), [{ - piece: '.', - from: 'e2', - to: 'e4', - }]); - }); - - describe('Parses queries in time', function() { const MOVES = [ 'd2', @@ -457,4 +428,16 @@ describe('getLegalMoves', function() { to: 'd8', }]); }); + + it('resolves ambiguity if the results are the same', function() { + const board = getChessBoardWithPieces([ + {color: 2, type: 'b', area: 'b5'}, + ]); + const result = getLegalMoves(board, parseMoveInput('b5d7')); + assert.deepEqual(result, [{ + from: 'b5', + piece: 'b', + to: 'd7', + }]); + }); }); diff --git a/package-lock.json b/package-lock.json index bc01a8d..c1aa0c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,8 +6,10 @@ "": { "name": "chess-com-helper", "dependencies": { + "@types/nearley": "^2.11.5", "domify": "^1.4.1", "lodash": "^4.17.21", + "nearley": "^2.20.1", "svg.js": "^2.6.5" }, "devDependencies": { @@ -266,6 +268,11 @@ "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", "dev": true }, + "node_modules/@types/nearley": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz", + "integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA==" + }, "node_modules/@types/node": { "version": "16.4.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", @@ -1357,6 +1364,11 @@ "node": ">=0.3.1" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -2734,6 +2746,11 @@ "node": ">=10" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2752,6 +2769,32 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -3102,12 +3145,29 @@ "node": ">=0.4.x" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, "node_modules/ramda": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", "dev": true }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3206,6 +3266,14 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4488,6 +4556,11 @@ "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", "dev": true }, + "@types/nearley": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz", + "integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA==" + }, "@types/node": { "version": "16.4.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", @@ -5363,6 +5436,11 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -6395,6 +6473,11 @@ } } }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6407,6 +6490,24 @@ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -6665,12 +6766,26 @@ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "dev": true }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, "ramda": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", "dev": true }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6748,6 +6863,11 @@ "signal-exit": "^3.0.2" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 5c373f9..13cd51a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "e2e": "cypress run --browser chrome --record --headed", "e2e-dev": "cypress open", "pack": "rm app.zip; npm run build-prod; cd app && zip -r ../app.zip *", - "release": "node ./release.js" + "release": "node ./release.js", + "compile-grammar": "nearleyc app/src/grammar/move.ne -o app/src/grammar/move.cjs" }, "devDependencies": { "@types/assert": "^1.5.5", @@ -30,8 +31,10 @@ "webpack-cli": "^4.9.0" }, "dependencies": { + "@types/nearley": "^2.11.5", "domify": "^1.4.1", "lodash": "^4.17.21", + "nearley": "^2.20.1", "svg.js": "^2.6.5" } }