first commit
This commit is contained in:
178
packages/draughts/core/board.js
Normal file
178
packages/draughts/core/board.js
Normal file
@ -0,0 +1,178 @@
|
||||
import {
|
||||
cellIsEmpty,
|
||||
queenPiece,
|
||||
eachCell,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
shouldQueen,
|
||||
clonePosition,
|
||||
filterToLongestCaptures,
|
||||
cellWithinBounds,
|
||||
} from './utilities';
|
||||
import { Players, Pieces, GameStates } from '@draughts/core';
|
||||
|
||||
export class Board {
|
||||
constructor(position, playerToMove, firstMove = true) {
|
||||
this.position = position;
|
||||
this.playerToMove = playerToMove;
|
||||
this.firstMove = firstMove;
|
||||
|
||||
this.direction = this.playerToMove === Players.WHITE ? -1 : 1;
|
||||
this.moves = this._computeMoves();
|
||||
this.state = this._computeState();
|
||||
}
|
||||
|
||||
_computeState() {
|
||||
if (this.moves.length > 0) {
|
||||
return GameStates.PLAYING;
|
||||
}
|
||||
|
||||
let blackPieces = 0;
|
||||
let whitePieces = 0;
|
||||
|
||||
for (const { piece } of eachCell(this.position)) {
|
||||
if (pieceIsPlayer(piece, Players.WHITE)) {
|
||||
whitePieces = whitePieces + 1;
|
||||
} else if (pieceIsPlayer(piece, Players.BLACK)) {
|
||||
blackPieces = blackPieces + 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.playerToMove = Players.NONE;
|
||||
|
||||
if (blackPieces === 0) {
|
||||
return GameStates.WHITE_WON;
|
||||
}
|
||||
|
||||
if (whitePieces === 0) {
|
||||
return GameStates.BLACK_WON;
|
||||
}
|
||||
|
||||
return GameStates.DRAW;
|
||||
}
|
||||
|
||||
_computeMoves() {
|
||||
const starts = this._getStarts();
|
||||
const captures = this._getCaptures(starts);
|
||||
if (captures.length > 0) return captures;
|
||||
return this._getMoves(starts);
|
||||
}
|
||||
|
||||
_getStarts() {
|
||||
const starts = [];
|
||||
for (const { cell, piece } of eachCell(this.position)) {
|
||||
if (pieceIsPlayer(piece, this.playerToMove)) {
|
||||
starts.push(cell);
|
||||
}
|
||||
}
|
||||
return starts;
|
||||
}
|
||||
|
||||
_getCaptures(starts) {
|
||||
let captures = [];
|
||||
for (const start of starts) {
|
||||
captures = [
|
||||
...captures,
|
||||
...this._getNextCaptures(this.position, {
|
||||
captures: [],
|
||||
path: [start],
|
||||
}),
|
||||
];
|
||||
}
|
||||
return filterToLongestCaptures(captures);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
_getNextCaptures(initialPosition, move) {
|
||||
const captures = [];
|
||||
const start = move.path.at(-1);
|
||||
const piece = initialPosition[start.row][start.col];
|
||||
const possibleDRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||
|
||||
for (const dRow of possibleDRow) {
|
||||
for (const dCol of [-1, 1]) {
|
||||
const middle = {
|
||||
col: start.col + dCol * this.direction,
|
||||
row: start.row + dRow * this.direction,
|
||||
};
|
||||
const end = {
|
||||
col: middle.col + dCol * this.direction,
|
||||
row: middle.row + dRow * this.direction,
|
||||
};
|
||||
|
||||
if (!cellWithinBounds(end) || !cellIsEmpty(initialPosition, end))
|
||||
continue;
|
||||
|
||||
const middlePiece = initialPosition[middle.row][middle.col];
|
||||
if (
|
||||
pieceIsPlayer(middlePiece, Players.NONE) ||
|
||||
pieceIsPlayer(middlePiece, this.playerToMove)
|
||||
)
|
||||
continue;
|
||||
|
||||
const queened = shouldQueen(end, piece);
|
||||
const searchPosition = clonePosition(initialPosition);
|
||||
|
||||
searchPosition[start.row][start.col] = Pieces.NONE;
|
||||
searchPosition[middle.row][middle.col] = Pieces.NONE;
|
||||
searchPosition[end.row][end.col] = queened ? queenPiece(piece) : piece;
|
||||
|
||||
const nextMove = {
|
||||
captures: [...move.captures, middle],
|
||||
path: [...move.path, end],
|
||||
};
|
||||
|
||||
captures.push(nextMove);
|
||||
if (!queened)
|
||||
captures.push(...this._getNextCaptures(searchPosition, nextMove));
|
||||
}
|
||||
}
|
||||
|
||||
return filterToLongestCaptures(captures);
|
||||
}
|
||||
|
||||
_getMoves(starts) {
|
||||
const moves = [];
|
||||
|
||||
for (const start of starts) {
|
||||
const piece = this.position[start.row][start.col];
|
||||
const possibleDeltaRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||
for (const deltaRow of possibleDeltaRow) {
|
||||
for (const deltaCol of [-1, 1]) {
|
||||
const end = {
|
||||
col: start.col + deltaCol * this.direction,
|
||||
row: start.row + deltaRow * this.direction,
|
||||
};
|
||||
if (cellWithinBounds(end) && cellIsEmpty(this.position, end)) {
|
||||
moves.push({ captures: [], path: [start, end] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
|
||||
doMove({ path, captures }) {
|
||||
const start = path.at(0);
|
||||
const startPiece = this.position[start.row][start.col];
|
||||
const end = path.at(-1);
|
||||
|
||||
const endPiece = shouldQueen(end, startPiece)
|
||||
? queenPiece(startPiece)
|
||||
: startPiece;
|
||||
|
||||
const newPosition = clonePosition(this.position);
|
||||
newPosition[path.at(0).row][path.at(0).col] = Pieces.NONE;
|
||||
newPosition[end.row][end.col] = endPiece;
|
||||
|
||||
for (const capture of captures) {
|
||||
newPosition[capture.row][capture.col] = Pieces.NONE;
|
||||
}
|
||||
|
||||
const newPlayerToMove =
|
||||
this.playerToMove === Players.WHITE ? Players.BLACK : Players.WHITE;
|
||||
|
||||
return new Board(newPosition, newPlayerToMove, false);
|
||||
}
|
||||
}
|
||||
20
packages/draughts/core/constants/enums.js
Normal file
20
packages/draughts/core/constants/enums.js
Normal file
@ -0,0 +1,20 @@
|
||||
export const Pieces = {
|
||||
BLACK: 'b',
|
||||
BLACK_QUEEN: 'bq',
|
||||
NONE: '',
|
||||
WHITE: 'w',
|
||||
WHITE_QUEEN: 'wq',
|
||||
};
|
||||
|
||||
export const GameStates = {
|
||||
BLACK_WON: 'b',
|
||||
DRAW: 'd',
|
||||
PLAYING: 'p',
|
||||
WHITE_WON: 'w',
|
||||
};
|
||||
|
||||
export const Players = {
|
||||
BLACK: 'b',
|
||||
NONE: '',
|
||||
WHITE: 'w',
|
||||
};
|
||||
5
packages/draughts/core/constants/index.js
Normal file
5
packages/draughts/core/constants/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
export { Players, Pieces, GameStates } from './enums';
|
||||
|
||||
export { INITIAL_POSITION } from './positions';
|
||||
|
||||
export const BOARD_SIZE = 8;
|
||||
86
packages/draughts/core/constants/positions.js
Normal file
86
packages/draughts/core/constants/positions.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { Pieces } from './enums';
|
||||
|
||||
export const INITIAL_POSITION = [
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
],
|
||||
[
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
|
||||
[
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
],
|
||||
[
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
];
|
||||
22
packages/draughts/core/index.js
Normal file
22
packages/draughts/core/index.js
Normal file
@ -0,0 +1,22 @@
|
||||
export {
|
||||
Players,
|
||||
GameStates,
|
||||
Pieces,
|
||||
INITIAL_POSITION,
|
||||
BOARD_SIZE,
|
||||
} from './constants';
|
||||
|
||||
export { Board } from './board';
|
||||
|
||||
export {
|
||||
cellIsEmpty,
|
||||
queenPiece,
|
||||
eachCell,
|
||||
compareCells,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
shouldQueen,
|
||||
clonePosition,
|
||||
filterToLongestCaptures,
|
||||
cellWithinBounds,
|
||||
} from './utilities';
|
||||
81
packages/draughts/core/utilities/index.js
Normal file
81
packages/draughts/core/utilities/index.js
Normal file
@ -0,0 +1,81 @@
|
||||
import { BOARD_SIZE, Pieces, Players } from '@draughts/core';
|
||||
|
||||
const isQueenMap = {
|
||||
[Pieces.NONE]: false,
|
||||
[Pieces.BLACK]: false,
|
||||
[Pieces.WHITE]: false,
|
||||
[Pieces.BLACK_QUEEN]: true,
|
||||
[Pieces.WHITE_QUEEN]: true,
|
||||
};
|
||||
|
||||
export const pieceIsQueen = (piece) => isQueenMap[piece];
|
||||
|
||||
const isPlayerMap = {
|
||||
[Players.NONE]: new Set([Pieces.NONE]),
|
||||
[Players.WHITE]: new Set([Pieces.WHITE, Pieces.WHITE_QUEEN]),
|
||||
[Players.BLACK]: new Set([Pieces.BLACK, Pieces.BLACK_QUEEN]),
|
||||
};
|
||||
|
||||
export const pieceIsPlayer = (piece, player) => {
|
||||
return isPlayerMap[player].has(piece);
|
||||
};
|
||||
|
||||
export const cellWithinBounds = ({ row, col }) =>
|
||||
row < BOARD_SIZE && row >= 0 && col < BOARD_SIZE && col >= 0;
|
||||
|
||||
export const cellIsEmpty = (position, { row, col }) =>
|
||||
position[row][col] === Pieces.NONE;
|
||||
|
||||
export function* eachCell(position) {
|
||||
for (const [row, rowArray] of position.entries()) {
|
||||
for (const [col, piece] of rowArray.entries()) {
|
||||
yield { cell: { col, row }, piece };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const crownQueenMap = {
|
||||
[Pieces.BLACK]: Pieces.BLACK_QUEEN,
|
||||
[Pieces.WHITE]: Pieces.WHITE_QUEEN,
|
||||
[Pieces.BLACK_QUEEN]: Pieces.BLACK_QUEEN,
|
||||
[Pieces.WHITE_QUEEN]: Pieces.WHITE_QUEEN,
|
||||
};
|
||||
|
||||
export const queenPiece = (piece) => crownQueenMap[piece];
|
||||
|
||||
export const shouldQueen = (cell, piece) => {
|
||||
if (pieceIsQueen(piece)) return false;
|
||||
return (
|
||||
(pieceIsPlayer(piece, Players.WHITE) && cell.row === 0) ||
|
||||
(pieceIsPlayer(piece, Players.BLACK) && cell.row === BOARD_SIZE - 1)
|
||||
);
|
||||
};
|
||||
|
||||
export const filterToLongestCaptures = (moves) => {
|
||||
let longestCaptures = [];
|
||||
let recordLength = 0;
|
||||
for (const move of moves) {
|
||||
if (move.captures.length > recordLength) {
|
||||
longestCaptures = [move];
|
||||
recordLength = move.captures.length;
|
||||
} else if (move.captures.length === recordLength) {
|
||||
longestCaptures.push(move);
|
||||
}
|
||||
}
|
||||
return longestCaptures;
|
||||
};
|
||||
|
||||
export const clonePosition = (position) => [...position.map((row) => [...row])];
|
||||
|
||||
export const compareCells = (cellOne, cellTwo) => {
|
||||
if (!cellOne || !cellTwo) return false;
|
||||
return cellOne.row === cellTwo.row && cellOne.col === cellTwo.col;
|
||||
};
|
||||
|
||||
const formatPlayerMap = {
|
||||
[Players.NONE]: 'none',
|
||||
[Players.WHITE]: 'white',
|
||||
[Players.BLACK]: 'black',
|
||||
};
|
||||
|
||||
export const formatPlayer = (player) => formatPlayerMap[player];
|
||||
Reference in New Issue
Block a user