From d5ec344b8177f91422d497c1da5b1b92ca654b61 Mon Sep 17 00:00:00 2001 From: Nixinova Date: Mon, 26 Jul 2021 15:13:41 +1200 Subject: [PATCH] Release 0.4.3 (#16) * Update package-lock.json * Document browser output * Delete package-lock.json Lockfile not needed * Type cleanup * Document `makeMove` param 3 `completeMove?` * Minor cleanup * Return undefined colour when cell is empty Empty cells were being classified as black, leading to incorrect movement. Fixes #15 * Restore package-lock.json * Fix unchecked types * Use nonnull assertion instead Already checked for existence in the line above * Improve board typing * Add fen type * Fix move list checking * Version 0.4.3 --- .gitattributes | 2 -- package-lock.json | 8 +++---- package.json | 2 +- readme.md | 5 ++++- src/board/create-board.ts | 35 +++++++++++++++++++----------- src/board/create-fen.ts | 6 ++++-- src/board/undo.ts | 4 ++-- src/helpers.ts | 8 +++++++ src/pieces.ts | 5 +++-- src/points.ts | 3 ++- src/types.ts | 13 +++++++++++ src/validation/all-moves.ts | 2 +- src/validation/is-check.ts | 2 +- src/validation/make-move.ts | 42 ++++++++++++++++++++---------------- src/validation/validation.ts | 27 +++++++---------------- src/variables.ts | 4 ++-- test.js | 12 +++++++---- 17 files changed, 107 insertions(+), 73 deletions(-) diff --git a/.gitattributes b/.gitattributes index 6e2183a..176a458 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1 @@ * text=auto - -package-lock.json -diff diff --git a/package-lock.json b/package-lock.json index ec582ec..f62cac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fenfurnace", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -907,9 +907,9 @@ } }, "webpack": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.42.0.tgz", - "integrity": "sha512-Ln8HL0F831t1x/yPB/qZEUVmZM4w9BnHZ1EQD/sAUHv8m22hthoPniWTXEzFMh/Sf84mhrahut22TX5KxWGuyQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.43.0.tgz", + "integrity": "sha512-ex3nB9uxNI0azzb0r3xGwi+LS5Gw1RCRSKk0kg3kq9MYdIPmLS6UI3oEtG7esBaB51t9I+5H+vHmL3htaxqMSw==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", diff --git a/package.json b/package.json index ea80126..299d8d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fenfurnace", - "version": "0.4.2", + "version": "0.4.3", "description": "A chess engine designed to work entirely from a FEN position.", "keywords": [ "chess", diff --git a/readme.md b/readme.md index 955eaf3..986af32 100644 --- a/readme.md +++ b/readme.md @@ -26,8 +26,9 @@ FenFurnace is available [on npm](https://www.npmjs.org/package/fenfurnace): - Check that a given move obeys the rules of chess. - `validation.pieceInWay(startCell, endCell)` - Check if there are any pieces between two cells. -- `makeMove(startCell, endCell)` +- `makeMove(startCell, endCell, completeMove)` - Attempt to move a piece; returns `false` if invalid. + If `completeMove` is `false`, no move will actually be made; instead the move will just be tested for validity. - `undoMove()` - Undoes and returns the last move. - `findAllMoves(cell)` @@ -55,3 +56,5 @@ The following values are given in import `gameData`: ## Build Bundle local code for browser use with `npm run compile`. + +Functions are available under `window.fenFuncs`. diff --git a/src/board/create-board.ts b/src/board/create-board.ts index 425150e..433f807 100644 --- a/src/board/create-board.ts +++ b/src/board/create-board.ts @@ -1,23 +1,34 @@ import gameData from '../variables'; -import { Cell, Colour } from '../types'; +import { toLowerCase, toUpperCase } from '../helpers'; +import { Cell, Colour, PieceID, FenParts, CastleString, Fen } from '../types'; +import { isWhite } from '../pieces'; export default function createBoardArray(fenString: string): void { - const fenParts = fenString.split(' '); - - const currentBoard = fenParts[0].split('/'); - gameData.currentTurn = fenParts[1] as Colour; + const { fen, currentTurn, castling, enpassantSquare, halfMoveCount, moveNumber } = getFenParts(fenString); gameData.castling = { w: { k: false, q: false }, b: { k: false, q: false } }; - const castleString = fenParts[2]; + const castleString = castling; for (let i = 0; i < castleString.length; i++) { - const colour = castleString[i] === castleString[i].toUpperCase() ? 'w' : 'b' as Colour; - const side = castleString[i].toLowerCase() as ('k' | 'q'); + const colour = isWhite(castleString[i] as PieceID) ? 'w' : 'b'; + const side = toLowerCase(<'k' | 'q'>castleString[i]); gameData.castling[colour][side] = true; } - gameData.enpassantSquare = fenParts[3].toUpperCase() as Cell; - gameData.halfMoveCount = +fenParts[4]; - gameData.moveNumber = +fenParts[5]; + gameData.currentTurn = currentTurn; + gameData.enpassantSquare = enpassantSquare; + gameData.halfMoveCount = halfMoveCount; + gameData.moveNumber = moveNumber; - gameData.boardArray = currentBoard.map(val => val.replace(/[0-9]/g, n => '-'.repeat(+n))); + gameData.boardArray = fen.split('/').map(val => val.replace(/[0-9]/g, n => '-'.repeat(+n))); +} + +function getFenParts(fenString: string): FenParts { + const fenParts = fenString.split(' '); + const fen = fenParts[0]; + const currentTurn = fenParts[1]; + const castling = fenParts[2]; + const enpassantSquare = toUpperCase(fenParts[3]); + const halfMoveCount = +fenParts[4]; + const moveNumber = +fenParts[5]; + return { fen, currentTurn, castling, enpassantSquare, halfMoveCount, moveNumber }; } diff --git a/src/board/create-fen.ts b/src/board/create-fen.ts index 174d5ff..d6d1140 100644 --- a/src/board/create-fen.ts +++ b/src/board/create-fen.ts @@ -1,6 +1,7 @@ +import { Fen } from '../types'; import gameData from '../variables'; -export default function createFenFromBoardArray(): string { +export default function createFenFromBoardArray(): Fen { let fenString = ''; let blankSquares = 0; @@ -44,5 +45,6 @@ export default function createFenFromBoardArray(): string { fenString += ' ' + gameData.halfMoveCount; //move number fenString += ' ' + gameData.moveNumber; - return fenString; + + return fenString as Fen; } diff --git a/src/board/undo.ts b/src/board/undo.ts index ed4c211..a24fb1e 100644 --- a/src/board/undo.ts +++ b/src/board/undo.ts @@ -1,8 +1,8 @@ import gameData from '../variables'; import createBoardArray from './create-board'; -export default function undoMove(): string | undefined { - if (gameData.moveList.length === 0) return; +export default function undoMove(): string | void { + if (gameData.moveList.length <= 1) return; gameData.moveList.pop(); gameData.logList.pop(); const lastMove = gameData.moveList[gameData.moveList.length - 1]; diff --git a/src/helpers.ts b/src/helpers.ts index ec51ce0..45ae56e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -12,3 +12,11 @@ export function invertColour(colour: Colour): Colour { export function coordsToCell(x: number, y: number): Cell { return indexToLetter(x) + y as Cell; } + +export function toUpperCase(str: T): Uppercase { + return str.toUpperCase() as Uppercase; +} + +export function toLowerCase(str: T): Lowercase { + return str.toLowerCase() as Lowercase; +} diff --git a/src/pieces.ts b/src/pieces.ts index 0b42915..52a346a 100644 --- a/src/pieces.ts +++ b/src/pieces.ts @@ -1,8 +1,9 @@ import gameData from './variables'; import { Colour, Cell, PieceID } from './types'; -export function getColour(cell: Cell): Colour { +export function getColour(cell: Cell): Colour | undefined { const piece = getPieceInCell(cell); + if (!piece) return; return isWhite(piece) ? 'w' : 'b'; } @@ -39,7 +40,7 @@ export function add(piece: PieceID, cell: Cell): void { export function del(cell: Cell): void { // console.log('Deleting piece from', cell); - const col = (parseInt(cell[0], 36) - 9); + const col = parseInt(cell[0], 36) - 9; const row = 8 - (+cell[1]); let str = gameData.boardArray[row]; gameData.boardArray[row] = str.substr(0, col - 1) + '-' + str.substr(col); diff --git a/src/points.ts b/src/points.ts index 01fb538..dd4186e 100644 --- a/src/points.ts +++ b/src/points.ts @@ -7,7 +7,8 @@ export default function points(): { w: number, b: number } { for (let cell in row.split('')) { const colour = cell === cell.toUpperCase() ? 'w' : 'b'; const piece = cell.toLowerCase(); - const pointsIncrease = { p: 1, m: 3, b: 3, r: 5, q: 9 }[piece] ?? 0; + const pointEquivs: Record = { p: 1, m: 3, b: 3, r: 5, q: 9 }; + const pointsIncrease = pointEquivs[piece] ?? 0; points[colour] += pointsIncrease; } } diff --git a/src/types.ts b/src/types.ts index bc3b7e8..5659315 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,3 +10,16 @@ export type Piece = 'pawn' | 'rook' | 'bishop' | 'knight' | 'queen' | 'king'; export type PieceID = 'R' | 'r' | 'B' | 'b' | 'N' | 'n' | 'Q' | 'q' | 'K' | 'k' | '-'; export type EndingStatus = false | 'stalemate' | 'checkmate'; + +export type Fen = `${string} ${Colour} ${CastleString} ${Lowercase | '-'} ${number} ${number}`; + +export type CastleString = `${'K' | ''}${'Q' | ''}${'k' | ''}${'q' | ''}`; + +export interface FenParts { + fen: Fen; + currentTurn: Colour; + castling: CastleString; + enpassantSquare: Cell; + halfMoveCount: number; + moveNumber: number; +}; diff --git a/src/validation/all-moves.ts b/src/validation/all-moves.ts index 0f1ec8a..27c60d8 100644 --- a/src/validation/all-moves.ts +++ b/src/validation/all-moves.ts @@ -16,7 +16,7 @@ export default function getAllMoves(cell: Cell): Board { for (let j = 1; j <= 8; j++) { const targetCell = coordsToCell(j, i); - if (makeMove(cell, targetCell, { isTest: true })) { + if (makeMove(cell, targetCell, false)) { possibleSquares.push(targetCell); } createBoardArray(beforeState); diff --git a/src/validation/is-check.ts b/src/validation/is-check.ts index 18acf3e..2591d79 100644 --- a/src/validation/is-check.ts +++ b/src/validation/is-check.ts @@ -11,7 +11,7 @@ export default function isCheck(colour: Colour): boolean { const testCell = coordsToCell(j, i); const testPiece = pieces.getPieceInCell(testCell); if (testPiece.toLowerCase() === 'k') { - const kingColour = pieces.getColour(testCell); + const kingColour = pieces.getColour(testCell)!; kingCells[kingColour] = testCell; } } diff --git a/src/validation/make-move.ts b/src/validation/make-move.ts index 605e3ba..69d2a35 100644 --- a/src/validation/make-move.ts +++ b/src/validation/make-move.ts @@ -4,24 +4,29 @@ import isCheck from './is-check'; import * as pieces from '../pieces'; import * as validation from './validation'; import { Column, Row, Cell, PieceID } from '../types'; +import { invertColour } from '../helpers'; -export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { isTest?: boolean } = {}) { - // isTest=true: test the move instead of making it +export default function makeMove(startCell: Cell, endCell: Cell, completeMove: boolean = true): string | false { const piece = pieces.getPieceInCell(startCell); const colour = pieces.getColour(startCell); + const startLetter = startCell[0] as Column; + const startNumber = +startCell[1] as Row; + const endLetter = endCell[0] as Column; + const endNumber = +endCell[1] as Row; const beforeState = [...gameData.boardArray]; const events = { pieceCaptured: pieces.inCell(endCell), promoted: false, castled: false }; //must be same colour as move - if (colour !== gameData.currentTurn && !isTest) { - console.log('Failed to move', startCell, '->', endCell); + if (colour !== gameData.currentTurn) { + if (!completeMove) console.log('Failed to move', startCell, '->', endCell); return false; } //validate castling - if (piece.toLowerCase() === 'k' && Math.abs(endCell.charCodeAt(0) - startCell.charCodeAt(0)) === 2 && (endCell[1] === startCell[1]) && !isCheck(colour)) { - const isKingside = endCell.charCodeAt(0) - startCell.charCodeAt(0) > 0; + const colDelta = endCell.charCodeAt(0) - startCell.charCodeAt(0); + if (piece.toLowerCase() === 'k' && Math.abs(colDelta) === 2 && (endLetter === startLetter) && !isCheck(colour)) { + const isKingside = colDelta > 0; const side = isKingside ? 'k' : 'q'; if (gameData.castling[colour][side]) { const file: Column = isKingside ? 'H' : 'A'; @@ -48,10 +53,10 @@ export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { i else return false; //promotion - let isBackRank = endCell[1] === (colour === 'w' ? '8' : '1'); + const isBackRank = endNumber === (colour === 'w' ? 8 : 1); if (piece.toLowerCase() === 'p' && isBackRank) { if (!gameData.promotionPiece) { - console.error('NO PROMOTION PIECE FOUND'); + console.error('No promotion piece found.'); gameData.boardArray = beforeState; return false; } else { @@ -62,16 +67,15 @@ export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { i } } //enpassant check (take old enpassant pawn && update enpassant square) + const enpassantNumber = endNumber + (colour === 'w' ? -1 : +1) as Row; if (piece.toLowerCase() === 'p' && endCell === gameData.enpassantSquare) { - const enpassantNumber = colour === 'w' ? +endCell[1] - 1 : +endCell[1] + 1; - pieces.del(endCell[0] + enpassantNumber as Cell); + pieces.del(endLetter + enpassantNumber as Cell); gameData.halfMoveCount = 0; events.pieceCaptured = true; } - const deltaLetter = Math.abs(+endCell[1] - +startCell[1]); + const deltaLetter = Math.abs(endNumber - startNumber); if (piece.toLowerCase() === 'p' && deltaLetter === 2) { - const enpassantNumber = colour === 'w' ? (+endCell[1] - 1) : (+endCell[1] + 1); - gameData.enpassantSquare = endCell[0] + enpassantNumber as Cell; + gameData.enpassantSquare = endLetter + enpassantNumber as Cell; } else { gameData.enpassantSquare = '-'; } @@ -86,7 +90,7 @@ export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { i if (piece.toLowerCase() === 'k') { gameData.castling[colour] = { k: false, q: false }; } else if (piece.toLowerCase() === 'r') { - const isKingside = startCell[0] === 'H'; + const isKingside = startLetter === 'H'; const side = isKingside ? 'k' : 'q'; gameData.castling[colour][side] = false; } @@ -98,12 +102,12 @@ export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { i else gameData.halfMoveCount++; //change turn - gameData.currentTurn = gameData.currentTurn === 'w' ? 'b' : 'w'; + gameData.currentTurn = invertColour(gameData.currentTurn); //add to log let logText = ''; if (events.castled === true) { - logText += endCell.charCodeAt(0) - startCell.charCodeAt(0) > 0 ? 'O-O' : 'O-O-O'; + logText += colDelta > 0 ? 'O-O' : 'O-O-O'; } else { if (piece.toLowerCase() === 'p' && events.pieceCaptured) logText += startCell[0].toLowerCase(); else if (piece.toLowerCase() !== 'p') logText += piece.toUpperCase(); @@ -111,11 +115,11 @@ export default function makeMove(startCell: Cell, endCell: Cell, { isTest }: { i logText += endCell; if (events.promoted) logText += '=' + pieces.getPieceInCell(endCell).toUpperCase(); } - if (!isTest) gameData.logList.push(logText); + if (!completeMove) gameData.logList.push(logText); //update fen and move list - let fen = createFenFromBoardArray(); - if (!isTest) gameData.moveList.push(fen); + const fen = createFenFromBoardArray(); + if (!completeMove) gameData.moveList.push(fen); return fen; } diff --git a/src/validation/validation.ts b/src/validation/validation.ts index 0324b81..0b094f5 100644 --- a/src/validation/validation.ts +++ b/src/validation/validation.ts @@ -11,8 +11,8 @@ export function validateMove(startCell: Cell, endCell: Cell): boolean { export function isValid(startCell: Cell, endCell: Cell): boolean { let piece = pieces.getPieceInCell(startCell); let colour: Colour = piece === piece.toUpperCase() ? 'w' : 'b'; - let startNumber = parseInt(startCell[1]); - let endNumber = parseInt(endCell[1]); + let startNumber = +startCell[1]; + let endNumber = +endCell[1]; let deltaNumber = Math.abs(endNumber - startNumber); let deltaLetter = Math.abs(endCell.charCodeAt(0) - startCell.charCodeAt(0)); switch (piece.toLowerCase()) { @@ -21,8 +21,7 @@ export function isValid(startCell: Cell, endCell: Cell): boolean { case 'n': return deltaNumber + deltaLetter === 3 && deltaLetter !== 0 && deltaNumber !== 0; case 'k': - const singleMove = deltaLetter <= 1 && deltaNumber <= 1; - return (singleMove); + return deltaLetter <= 1 && deltaNumber <= 1; case 'b': return deltaLetter === deltaNumber; case 'q': @@ -44,14 +43,13 @@ export function pieceInWay(startCell: Cell, endCell: Cell): boolean { let piece = pieces.getPieceInCell(startCell); let colour = pieces.getColour(startCell); - let startNumber = parseInt(startCell[1]); - let endNumber = parseInt(endCell[1]); + let startNumber = +startCell[1]; + let endNumber = +endCell[1]; let deltaNumber = Math.abs(endNumber - startNumber); let startLetter = startCell[0]; let endLetter = endCell[0]; let deltaLetter = Math.abs(endCell.charCodeAt(0) - startCell.charCodeAt(0)); - // determine direction if (endLetter > startLetter) direction.l = 1; else if (endLetter < startLetter) direction.l = -1; @@ -64,18 +62,9 @@ export function pieceInWay(startCell: Cell, endCell: Cell): boolean { switch (piece.toLowerCase()) { case 'p': { if (deltaLetter === 0) { - if (colour === 'w') { - invalidMove = pieces.inCell(startCell[0] + (startNumber + 1) as Cell); - if (deltaNumber === 2 && !invalidMove) { - invalidMove = pieces.inCell(startCell[0] + (startNumber + 2) as Cell); - } - } - else { - invalidMove = pieces.inCell(startCell[0] + (startNumber - 1) as Cell); - if (deltaNumber === 2 && !invalidMove) { - invalidMove = pieces.inCell(startCell[0] + (startNumber - 2) as Cell); - } - } + const directionMult = colour === 'w' ? +1 : -1; + const forwardNum = deltaNumber === 2 && !invalidMove ? 2 : 1; + invalidMove = pieces.inCell(startCell[0] + (startNumber + 1 * directionMult * forwardNum) as Cell); } return invalidMove; } diff --git a/src/variables.ts b/src/variables.ts index 0371053..9e1db83 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,10 +1,10 @@ -import { Colour, Cell, Board, PieceID } from './types'; +import { Colour, Cell, Board, PieceID, Fen } from './types'; export default class GameData { static castling = { w: { k: true, q: true }, b: { k: true, q: true } }; static boardArray: Board = []; static enpassantSquare: Cell | '-' = '-'; - static moveList: string[] = []; + static moveList: Fen[] = []; static logList: string[] = []; static currentTurn: Colour = 'w'; static halfMoveCount: number = 0; diff --git a/test.js b/test.js index 565faa4..7484b66 100644 --- a/test.js +++ b/test.js @@ -14,18 +14,22 @@ function test() { setupBoard(); console.assert(gameData.boardArray[1] === 'p'.repeat(8), 'Pawns set up'); console.assert(gameData.castling.w.k && gameData.castling.b.q, 'Castling valid'); + createBoard('rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/R3K2R w KQkq - 4 3'); + console.assert(gameData.boardArray[1] === 'pppp-ppp', 'Pawn moved out of row 2'); + console.assert(gameData.boardArray[2] === '-----n--', 'Knight in row 3'); // Finding moves setupBoard(); + console.assert(findAllMoves('G1').join(',') === 'F3,A3', 'White knight can move'); console.assert(findAllMoves('A2').join(',') === 'A3,A4', 'Two valid pawn moves'); console.assert(findAllMoves('A4').length === 0, 'No valid empty moves'); // Moving - createBoard('rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/R3K2R w KQkq - 4 3'); - console.assert(gameData.boardArray[1] === 'pppp-ppp', 'Pawn moved out of row 2'); - console.assert(gameData.boardArray[2] === '-----n--', 'Knight in row 3'); + createBoard('rnb3n1/ppkpppbr/3q1P2/2P1P1pP/7P/2P5/PP6/RNBQKBNR w KQ - 1 15'); + console.assert(makeMove('D8', 'D5') !== false, 'Queen can move arbitrarily forward'); + console.assert(makeMove('C8', 'G5') !== false, 'Bishop can attack'); - // Castling & log output + // Castling createBoard('rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/R3K2R w KQkq - 4 3'); console.assert(gameData.boardArray[7] === 'R---K--R', 'King has not castled'); makeMove('E1', 'C1');