From 12f88059ee3e4bc8518aec0dcab8115a6941c1d7 Mon Sep 17 00:00:00 2001 From: John Doknjas Date: Tue, 4 Nov 2025 20:09:14 -0800 Subject: [PATCH 1/3] Remove the premove trimming logic, as lila now has it. --- src/config.ts | 1 - src/premove.ts | 193 +++--------------------------------------- src/state.ts | 1 - src/types.ts | 1 - tests/premove.test.ts | 160 ++++++++++++---------------------- 5 files changed, 65 insertions(+), 291 deletions(-) diff --git a/src/config.ts b/src/config.ts index 9ca5f9a..897c121 100644 --- a/src/config.ts +++ b/src/config.ts @@ -47,7 +47,6 @@ export interface Config { castle?: boolean; // whether to allow king castle premoves dests?: cg.Key[]; // premove destinations for the current selection customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]} - unrestrictedPremoves?: boolean; // if falsy, the positions of friendly pieces will be used to trim premove options additionalPremoveRequirements?: cg.Mobility; events?: { set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set diff --git a/src/premove.ts b/src/premove.ts index 539ce72..9976770 100644 --- a/src/premove.ts +++ b/src/premove.ts @@ -3,204 +3,35 @@ import * as cg from './types.js'; import { HeadlessState } from './state.js'; import { Mobility, MobilityContext } from './types.js'; -const isDestOccupiedByFriendly = (ctx: MobilityContext): boolean => ctx.friendlies.has(ctx.dest.key); +const pawn: Mobility = (ctx: MobilityContext) => + util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) <= 1 && + (util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) > 0 + ? ctx.dest.pos[1] === ctx.orig.pos[1] + (ctx.color === 'white' ? 1 : -1) + : util.pawnDirAdvance(...ctx.orig.pos, ...ctx.dest.pos, ctx.color === 'white')); -const isDestOccupiedByEnemy = (ctx: MobilityContext): boolean => ctx.enemies.has(ctx.dest.key); +const knight: Mobility = (ctx: MobilityContext) => util.knightDir(...ctx.orig.pos, ...ctx.dest.pos); -const anyPieceBetween = (orig: cg.Pos, dest: cg.Pos, pieces: cg.Pieces): boolean => - util.squaresBetween(...orig, ...dest).some(s => pieces.has(s)); +const bishop: Mobility = (ctx: MobilityContext) => util.bishopDir(...ctx.orig.pos, ...ctx.dest.pos); -const canEnemyPawnAdvanceToSquare = (pawnStart: cg.Key, dest: cg.Key, ctx: MobilityContext): boolean => { - const piece = ctx.enemies.get(pawnStart); - if (piece?.role !== 'pawn') return false; - const step = piece.color === 'white' ? 1 : -1; - const startPos = util.key2pos(pawnStart); - const destPos = util.key2pos(dest); - return ( - util.pawnDirAdvance(...startPos, ...destPos, piece.color === 'white') && - !anyPieceBetween(startPos, [destPos[0], destPos[1] + step], ctx.allPieces) - ); -}; - -const canEnemyPawnCaptureOnSquare = (pawnStart: cg.Key, dest: cg.Key, ctx: MobilityContext): boolean => { - const enemyPawn = ctx.enemies.get(pawnStart); - return ( - enemyPawn?.role === 'pawn' && - util.pawnDirCapture(...util.key2pos(pawnStart), ...util.key2pos(dest), enemyPawn.color === 'white') && - (ctx.friendlies.has(dest) || - canBeCapturedBySomeEnemyEnPassant( - util.squareShiftedVertically(dest, enemyPawn.color === 'white' ? -1 : 1), - ctx.friendlies, - ctx.enemies, - ctx.lastMove, - )) - ); -}; - -const canSomeEnemyPawnAdvanceToDest = (ctx: MobilityContext): boolean => - [...ctx.enemies.keys()].some(key => canEnemyPawnAdvanceToSquare(key, ctx.dest.key, ctx)); - -const isDestControlledByEnemy = (ctx: MobilityContext, pieceRolesExclude?: cg.Role[]): boolean => { - const square: cg.Pos = ctx.dest.pos; - return [...ctx.enemies].some(([key, piece]) => { - const piecePos = util.key2pos(key); - return ( - !pieceRolesExclude?.includes(piece.role) && - ((piece.role === 'pawn' && util.pawnDirCapture(...piecePos, ...square, piece.color === 'white')) || - (piece.role === 'knight' && util.knightDir(...piecePos, ...square)) || - (piece.role === 'bishop' && util.bishopDir(...piecePos, ...square)) || - (piece.role === 'rook' && util.rookDir(...piecePos, ...square)) || - (piece.role === 'queen' && util.queenDir(...piecePos, ...square)) || - (piece.role === 'king' && util.kingDirNonCastling(...piecePos, ...square))) && - (!['bishop', 'rook', 'queen'].includes(piece.role) || !anyPieceBetween(piecePos, square, ctx.allPieces)) - ); - }); -}; - -const isFriendlyOnDestAndAttacked = (ctx: MobilityContext): boolean => - isDestOccupiedByFriendly(ctx) && - (canBeCapturedBySomeEnemyEnPassant(ctx.dest.key, ctx.friendlies, ctx.enemies, ctx.lastMove) || - isDestControlledByEnemy(ctx)); - -const canBeCapturedBySomeEnemyEnPassant = ( - potentialSquareOfFriendlyPawn: cg.Key | undefined, - friendlies: cg.Pieces, - enemies: cg.Pieces, - lastMove?: cg.Key[], -): boolean => { - if (!potentialSquareOfFriendlyPawn || (lastMove && potentialSquareOfFriendlyPawn !== lastMove[1])) - return false; - const pos = util.key2pos(potentialSquareOfFriendlyPawn); - const friendly = friendlies.get(potentialSquareOfFriendlyPawn); - return ( - friendly?.role === 'pawn' && - pos[1] === (friendly.color === 'white' ? 3 : 4) && - (!lastMove || util.diff(util.key2pos(lastMove[0])[1], pos[1]) === 2) && - [1, -1].some(delta => { - const k = util.pos2key([pos[0] + delta, pos[1]]); - return !!k && enemies.get(k)?.role === 'pawn'; - }) - ); -}; - -const isPathClearEnoughOfFriendliesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => { - if (ctx.unrestrictedPremoves) return true; - const squaresBetween = util.squaresBetween(...ctx.orig.pos, ...ctx.dest.pos); - if (isPawnAdvance) squaresBetween.push(ctx.dest.key); - const squaresOfFriendliesBetween = squaresBetween.filter(s => ctx.friendlies.has(s)); - if (!squaresOfFriendliesBetween.length) return true; - const firstSquareOfFriendliesBetween = squaresOfFriendliesBetween[0]; - const nextSquare = util.squareShiftedVertically( - firstSquareOfFriendliesBetween, - ctx.color === 'white' ? -1 : 1, - ); - return ( - squaresOfFriendliesBetween.length === 1 && - canBeCapturedBySomeEnemyEnPassant( - firstSquareOfFriendliesBetween, - ctx.friendlies, - ctx.enemies, - ctx.lastMove, - ) && - !!nextSquare && - !squaresBetween.includes(nextSquare) - ); -}; - -const isPathClearEnoughOfEnemiesForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => { - if (ctx.unrestrictedPremoves) return true; - const squaresBetween = util.squaresBetween(...ctx.orig.pos, ...ctx.dest.pos); - if (isPawnAdvance) squaresBetween.push(ctx.dest.key); - const squaresOfEnemiesBetween = squaresBetween.filter(s => ctx.enemies.has(s)); - if (squaresOfEnemiesBetween.length > 1) return false; - if (!squaresOfEnemiesBetween.length) return true; - const enemySquare = squaresOfEnemiesBetween[0]; - const enemy = ctx.enemies.get(enemySquare); - if (!enemy || enemy.role !== 'pawn') return true; - - const enemyStep = enemy.color === 'white' ? 1 : -1; - const squareAbove = util.squareShiftedVertically(enemySquare, enemyStep); - const enemyPawnDests: cg.Key[] = squareAbove - ? [ - ...util.adjacentSquares(squareAbove).filter(s => canEnemyPawnCaptureOnSquare(enemySquare, s, ctx)), - ...[squareAbove, util.squareShiftedVertically(squareAbove, enemyStep)] - .filter(s => !!s) - .filter(s => canEnemyPawnAdvanceToSquare(enemySquare, s, ctx)), - ] - : []; - const badSquares = [...squaresBetween, ctx.orig.key]; - return enemyPawnDests.some(square => !badSquares.includes(square)); -}; - -const isPathClearEnoughForPremove = (ctx: MobilityContext, isPawnAdvance: boolean): boolean => - isPathClearEnoughOfFriendliesForPremove(ctx, isPawnAdvance) && - isPathClearEnoughOfEnemiesForPremove(ctx, isPawnAdvance); - -const pawn: Mobility = (ctx: MobilityContext) => { - const step = ctx.color === 'white' ? 1 : -1; - if (util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) > 1) return false; - if (!util.diff(ctx.orig.pos[0], ctx.dest.pos[0])) - return ( - util.pawnDirAdvance(...ctx.orig.pos, ...ctx.dest.pos, ctx.color === 'white') && - isPathClearEnoughForPremove(ctx, true) - ); - if (ctx.dest.pos[1] !== ctx.orig.pos[1] + step) return false; - if (ctx.unrestrictedPremoves || isDestOccupiedByEnemy(ctx)) return true; - if (isDestOccupiedByFriendly(ctx)) return isDestControlledByEnemy(ctx); - else - return ( - canSomeEnemyPawnAdvanceToDest(ctx) || - canBeCapturedBySomeEnemyEnPassant( - util.pos2key([ctx.dest.pos[0], ctx.dest.pos[1] + step]), - ctx.friendlies, - ctx.enemies, - ctx.lastMove, - ) || - isDestControlledByEnemy(ctx, ['pawn']) - ); -}; - -const knight: Mobility = (ctx: MobilityContext) => - util.knightDir(...ctx.orig.pos, ...ctx.dest.pos) && - (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx)); - -const bishop: Mobility = (ctx: MobilityContext) => - util.bishopDir(...ctx.orig.pos, ...ctx.dest.pos) && - isPathClearEnoughForPremove(ctx, false) && - (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx)); - -const rook: Mobility = (ctx: MobilityContext) => - util.rookDir(...ctx.orig.pos, ...ctx.dest.pos) && - isPathClearEnoughForPremove(ctx, false) && - (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx)); +const rook: Mobility = (ctx: MobilityContext) => util.rookDir(...ctx.orig.pos, ...ctx.dest.pos); const queen: Mobility = (ctx: MobilityContext) => bishop(ctx) || rook(ctx); const king: Mobility = (ctx: MobilityContext) => - (util.kingDirNonCastling(...ctx.orig.pos, ...ctx.dest.pos) && - (ctx.unrestrictedPremoves || !isDestOccupiedByFriendly(ctx) || isFriendlyOnDestAndAttacked(ctx))) || + util.kingDirNonCastling(...ctx.orig.pos, ...ctx.dest.pos) || (ctx.canCastle && ctx.orig.pos[1] === ctx.dest.pos[1] && ctx.orig.pos[1] === (ctx.color === 'white' ? 0 : 7) && ((ctx.orig.pos[0] === 4 && ((ctx.dest.pos[0] === 2 && ctx.rookFilesFriendlies.includes(0)) || (ctx.dest.pos[0] === 6 && ctx.rookFilesFriendlies.includes(7)))) || - ctx.rookFilesFriendlies.includes(ctx.dest.pos[0])) && - (ctx.unrestrictedPremoves || - /* The following checks if no non-rook friendly piece is in the way between the king and its castling destination. - Note that for the Chess960 edge case of Kb1 "long castling", the check passes even if there is a piece in the way - on c1. But this is fine, since premoving from b1 to a1 as a normal move would have already returned true. */ - util - .squaresBetween(...ctx.orig.pos, ctx.dest.pos[0] > ctx.orig.pos[0] ? 7 : 1, ctx.dest.pos[1]) - .map(s => ctx.allPieces.get(s)) - .every(p => !p || util.samePiece(p, { role: 'rook', color: ctx.color })))); + ctx.rookFilesFriendlies.includes(ctx.dest.pos[0]))); const mobilityByRole = { pawn, knight, bishop, rook, queen, king }; export function premove(state: HeadlessState, key: cg.Key): cg.Key[] { const pieces = state.pieces, - canCastle = state.premovable.castle, - unrestrictedPremoves = !!state.premovable.unrestrictedPremoves; + canCastle = state.premovable.castle; const piece = pieces.get(key); if (!piece || piece.color === state.turnColor) return []; const color = piece.color, @@ -215,7 +46,6 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] { allPieces: pieces, friendlies: friendlies, enemies: enemies, - unrestrictedPremoves: unrestrictedPremoves, color: color, canCastle: canCastle, rookFilesFriendlies: Array.from(pieces) @@ -225,5 +55,6 @@ export function premove(state: HeadlessState, key: cg.Key): cg.Key[] { .map(([k]) => util.key2pos(k)[0]), lastMove: state.lastMove, }; + // todo - remove more properties from MobilityContext that aren't used in this file, and adjust as needed in lila. return util.allPosAndKey.filter(dest => mobility({ ...partialCtx, dest })).map(pk => pk.key); } diff --git a/src/state.ts b/src/state.ts index 6f27331..0c59e3b 100644 --- a/src/state.ts +++ b/src/state.ts @@ -52,7 +52,6 @@ export interface HeadlessState { dests?: cg.Key[]; // premove destinations for the current selection customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]} current?: cg.KeyPair; // keys of the current saved premove ["e2" "e4"] - unrestrictedPremoves?: boolean; // if falsy, the positions of friendly pieces will be used to trim premove options additionalPremoveRequirements: cg.Mobility; events: { set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set diff --git a/src/types.ts b/src/types.ts index aaef235..9099b0d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -124,7 +124,6 @@ export type MobilityContext = { allPieces: Pieces; friendlies: Pieces; enemies: Pieces; - unrestrictedPremoves: boolean; color: Color; canCastle: boolean; rookFilesFriendlies: number[]; diff --git a/tests/premove.test.ts b/tests/premove.test.ts index d174884..a7af63c 100644 --- a/tests/premove.test.ts +++ b/tests/premove.test.ts @@ -4,27 +4,22 @@ import { defaults, HeadlessState } from '../src/state'; import * as fen from '../src/fen'; import * as util from '../src/util'; -const diagonallyOpposite = (square: cg.Key): cg.Key => - util.pos2keyUnsafe(util.key2pos(square).map(n => 7 - n) as cg.Pos); +const invertSquare = (square: cg.Key): cg.Key => { + const asPos = util.key2pos(square); + return util.pos2keyUnsafe([asPos[0], 7 - asPos[1]] as cg.Pos); +}; const invertPieces = (pieces: cg.Pieces): cg.Pieces => new Map( [...pieces].map(([key, piece]) => [ - diagonallyOpposite(key), + invertSquare(key), { role: piece.role, color: util.opposite(piece.color) }, ]), ); -const makeState = ( - pieces: cg.Pieces, - trimPremoves: boolean, - lastMove: cg.Key[] | undefined, - turnColor: cg.Color, -): HeadlessState => { +const makeState = (pieces: cg.Pieces, turnColor: cg.Color): HeadlessState => { const state = defaults(); state.pieces = pieces; - if (!trimPremoves) state.premovable.unrestrictedPremoves = true; - state.lastMove = lastMove; state.turnColor = turnColor; return state; }; @@ -32,11 +27,10 @@ const makeState = ( const testPosition = ( pieces: cg.Pieces, turnColor: cg.Color, - lastMove: cg.Key[] | undefined, expectedPremoves: Map>, checkInverseToo: boolean, ): void => { - const state = makeState(pieces, true, lastMove, turnColor); + const state = makeState(pieces, turnColor); for (const [from, expectedDests] of expectedPremoves) { expect(new Set(premove(state, from))).toEqual(expectedDests); } @@ -47,112 +41,64 @@ const testPosition = ( testPosition( invertPieces(pieces), util.opposite(turnColor), - lastMove?.map(sq => diagonallyOpposite(sq)), new Map( [...expectedPremoves].map(([start, dests]) => [ - diagonallyOpposite(start), - new Set(Array.from(dests, diagonallyOpposite)), + invertSquare(start), + new Set(Array.from(dests, invertSquare)), ]), ), false, ); }; -test('premoves are trimmed appropriately', () => { +test('premoves are found', () => { const expectedPremoves = new Map>([ - ['f8', new Set(['g8', 'e8', 'd8', 'c8', 'f7', 'f6', 'f5', 'f4', 'f3', 'f2', 'f1'])], - ['f1', new Set(['h1', 'g1', 'e1', 'd1', 'c1', 'b1', 'a1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7'])], - ['g5', new Set(['h5', 'f5', 'e5', 'd5', 'c5', 'g6'])], - ['h8', new Set(['h7', 'g8'])], - ['e3', new Set(['g3', 'f3', 'd3', 'c3', 'b3', 'e2', 'e1', 'e4', 'e5', 'e6'])], - ['d6', new Set(['e6', 'f6', 'g6', 'c6', 'b6', 'a6', 'd7', 'd8', 'd5', 'd4', 'd3', 'd2', 'd1'])], - ['h1', new Set(['h2', 'h3', 'h4', 'g1', 'f1', 'g2', 'f3', 'e4', 'd5', 'c6', 'b7'])], - ['a4', new Set(['b3', 'b4', 'c4', 'd4', 'e4', 'f4', 'a5', 'a6'])], - ['c5', new Set(['c4', 'c3', 'd4', 'e3', 'd5', 'e5', 'f5', 'c6', 'b6', 'a7', 'b4'])], - ['a8', new Set(['b8', 'b7', 'a7'])], - ['c8', new Set(['e7', 'b6', 'a7'])], - ['g4', new Set(['h2', 'f6', 'e5', 'e3', 'f2'])], - ['c2', new Set(['e1', 'e3', 'd4', 'b4', 'a1'])], - ['b2', new Set(['c1', 'a1', 'c3', 'd4', 'e5', 'f6'])], - ['c7', new Set(['d8', 'b8', 'b6', 'a5'])], - ['h6', new Set(['h5'])], - ['g7', new Set(['g6', 'f6'])], - ['e5', new Set(['e4', 'd4'])], - ['b5', new Set(['b4'])], - ['a3', new Set([])], + ['a7', new Set(['a6', 'a5', 'b6'])], + ['b7', new Set(['b6', 'b5', 'a6', 'c6'])], + ['c7', new Set(['c6', 'c5', 'b6', 'd6'])], + ['d7', new Set(['d6', 'd5', 'c6', 'e6'])], + ['e7', new Set(['e6', 'e5', 'd6', 'f6'])], + ['f6', new Set(['f5', 'e5', 'g5'])], + ['g7', new Set(['g6', 'g5', 'f6', 'h6'])], + ['h7', new Set(['h6', 'h5', 'g6'])], + ['a8', new Set(['a7', 'a6', 'a5', 'a4', 'a3', 'a2', 'a1', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8'])], + ['b8', new Set(['a6', 'c6', 'd7'])], + ['c8', new Set(['a6', 'b7', 'd7', 'e6', 'f5', 'g4', 'h3'])], + [ + 'd8', + new Set([ + 'd7', + 'd6', + 'd5', + 'd4', + 'd3', + 'd2', + 'd1', + 'e8', + 'f8', + 'g8', + 'h8', + 'c8', + 'b8', + 'a8', + 'e7', + 'f6', + 'g5', + 'h4', + 'c7', + 'b6', + 'a5', + ]), + ], + ['e8', new Set(['d8', 'f8', 'd7', 'e7', 'f7', 'g8', 'h8', 'c8', 'a8'])], + ['f8', new Set(['g7', 'h6', 'e7', 'd6', 'c5', 'b4', 'a3'])], + ['g8', new Set(['h6', 'f6', 'e7'])], + ['h8', new Set(['h7', 'h6', 'h5', 'h4', 'h3', 'h2', 'h1', 'g8', 'f8', 'e8', 'd8', 'c8', 'b8', 'a8'])], ]); + testPosition( - fen.read('k1n2r1r/2bP2p1/3r3p/Ppq1pPr1/qP4n1/p3r1P1/PbnP2KP/R4r1q w - - 0 1'), + fen.read('rnbqkbnr/ppppp1pp/5p2/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'), 'white', - ['e7', 'e5'], - expectedPremoves, - true, - ); -}); - -test('anticipate all en passant captures if no last move', () => { - const expectedPremoves = new Map>([ - ['a2', new Set(['b1', 'b3', 'c4', 'd5', 'e6', 'f7', 'g8'])], - ['h2', new Set(['g1', 'g3', 'f4', 'e5', 'd6', 'c7', 'b8'])], - ['h3', new Set(['g3', 'f3', 'e3', 'd3', 'h4', 'h5'])], - ['f5', new Set(['e5', 'd5', 'c5', 'b5', 'a5', 'f6', 'f7', 'f8', 'f4', 'f3'])], - ['c4', new Set(['c5'])], - ['f4', new Set([])], - ['g5', new Set(['g6'])], - ['d3', new Set(['d4', 'e4'])], - ]); - testPosition( - fen.read('8/8/8/5RPp/1pP1pP2/3Pp2R/B6B/8 b - - 0 1'), - 'black', - undefined, - expectedPremoves, - true, - ); -}); - -test('horde no en passant for first to third rank', () => { - const expectedPremoves = new Map>([ - ['f1', new Set(['f2', 'f3'])], - ['g3', new Set(['g4', 'h4'])], - ]); - testPosition( - fen.read('rnbqkbnr/ppppppp1/8/8/8/6Pp/8/5P2 w kq - 0 1'), - 'black', - ['g1', 'g3'], - expectedPremoves, - true, - ); -}); - -test('prod bug report lichess-org/lila#18224', () => { - const expectedPremoves = new Map>([ - ['a8', new Set(['a7', 'a6', 'a5', 'a4', 'a3', 'a2', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8'])], - ['f2', new Set(['f3', 'g3'])], - ['g2', new Set(['h1', 'g1', 'f1', 'h2', 'h3', 'g3', 'f3'])], - ]); - testPosition( - fen.read('R7/6k1/8/8/5pp1/8/p4PK1/r7 b - - 0 56'), - 'black', - ['h2', 'g2'], - expectedPremoves, - false, - ); -}); - -test('promotion premove allowed', () => { - const expectedPremoves = new Map>([ - ['c7', new Set(['c8', 'b8', 'd8'])], - ['e7', new Set(['d8', 'e8', 'f8'])], - ['g7', new Set(['f8'])], - ['g8', new Set(['f8', 'e8', 'd8', 'c8', 'b8', 'a8'])], - ['h7', new Set(['g8'])], - ['a7', new Set(['a8', 'b8'])], - ['c1', new Set(['b1', 'b2', 'c2', 'd2', 'd1'])], - ]); - testPosition( - fen.read('n3r1RB/PkP1P1PP/8/8/8/8/8/2K5 b - - 0 1'), - 'black', - undefined, expectedPremoves, true, ); From b54a4530fe263797858090e79e20a07de9aec82b Mon Sep 17 00:00:00 2001 From: John Doknjas Date: Tue, 4 Nov 2025 20:24:03 -0800 Subject: [PATCH 2/3] Non-functional change, make code slightly clearer. --- src/premove.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/premove.ts b/src/premove.ts index 9976770..a557666 100644 --- a/src/premove.ts +++ b/src/premove.ts @@ -5,7 +5,7 @@ import { Mobility, MobilityContext } from './types.js'; const pawn: Mobility = (ctx: MobilityContext) => util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) <= 1 && - (util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) > 0 + (util.diff(ctx.orig.pos[0], ctx.dest.pos[0]) === 1 ? ctx.dest.pos[1] === ctx.orig.pos[1] + (ctx.color === 'white' ? 1 : -1) : util.pawnDirAdvance(...ctx.orig.pos, ...ctx.dest.pos, ctx.color === 'white')); From 03faf238219c69c1fb1e6aa19fdd0fc1d185e448 Mon Sep 17 00:00:00 2001 From: John Doknjas Date: Tue, 11 Nov 2025 15:46:42 -0800 Subject: [PATCH 3/3] Add todo comment about `castle`. --- src/premove.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/premove.ts b/src/premove.ts index a557666..279bbdb 100644 --- a/src/premove.ts +++ b/src/premove.ts @@ -30,6 +30,7 @@ const king: Mobility = (ctx: MobilityContext) => const mobilityByRole = { pawn, knight, bishop, rook, queen, king }; export function premove(state: HeadlessState, key: cg.Key): cg.Key[] { + // TODO - remove `castle` once https://github.com/lichess-org/lila/pull/18630 is merged. const pieces = state.pieces, canCastle = state.premovable.castle; const piece = pieces.get(key);