Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

Commit

Permalink
Release 0.4.3 (#16)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Nixinova authored Jul 26, 2021
1 parent 9637cf0 commit d5ec344
Show file tree
Hide file tree
Showing 17 changed files with 107 additions and 73 deletions.
2 changes: 0 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
* text=auto

package-lock.json -diff
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 4 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -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`.
35 changes: 23 additions & 12 deletions src/board/create-board.ts
Original file line number Diff line number Diff line change
@@ -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 = <Fen>fenParts[0];
const currentTurn = <Colour>fenParts[1];
const castling = <CastleString>fenParts[2];
const enpassantSquare = toUpperCase(<Cell>fenParts[3]);
const halfMoveCount = +fenParts[4];
const moveNumber = +fenParts[5];
return { fen, currentTurn, castling, enpassantSquare, halfMoveCount, moveNumber };
}
6 changes: 4 additions & 2 deletions src/board/create-fen.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -44,5 +45,6 @@ export default function createFenFromBoardArray(): string {
fenString += ' ' + gameData.halfMoveCount;
//move number
fenString += ' ' + gameData.moveNumber;
return fenString;

return fenString as Fen;
}
4 changes: 2 additions & 2 deletions src/board/undo.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand Down
8 changes: 8 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string>(str: T): Uppercase<T> {
return str.toUpperCase() as Uppercase<T>;
}

export function toLowerCase<T extends string>(str: T): Lowercase<T> {
return str.toLowerCase() as Lowercase<T>;
}
5 changes: 3 additions & 2 deletions src/pieces.ts
Original file line number Diff line number Diff line change
@@ -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';
}

Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof piece, number> = { p: 1, m: 3, b: 3, r: 5, q: 9 };
const pointsIncrease = pointEquivs[piece] ?? 0;
points[colour] += pointsIncrease;
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cell> | '-'} ${number} ${number}`;

export type CastleString = `${'K' | ''}${'Q' | ''}${'k' | ''}${'q' | ''}`;

export interface FenParts {
fen: Fen;
currentTurn: Colour;
castling: CastleString;
enpassantSquare: Cell;
halfMoveCount: number;
moveNumber: number;
};
2 changes: 1 addition & 1 deletion src/validation/all-moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/validation/is-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
42 changes: 23 additions & 19 deletions src/validation/make-move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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 = '-';
}
Expand All @@ -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;
}
Expand All @@ -98,24 +102,24 @@ 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();
if (events.pieceCaptured) logText += 'x';
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;
}
27 changes: 8 additions & 19 deletions src/validation/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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':
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/variables.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading

0 comments on commit d5ec344

Please sign in to comment.