Move Board/Piece to own file. Start unit testing
This commit is contained in:
parent
6cad21b79f
commit
4b38e44a3a
|
@ -0,0 +1,706 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Popup from './components/Popup';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
|
||||||
|
const BLACK = 0;
|
||||||
|
const WHITE = 1;
|
||||||
|
|
||||||
|
const PAWN = 0;
|
||||||
|
const ROOK = 1;
|
||||||
|
const KNIGHT = 2;
|
||||||
|
const BISHOP = 3;
|
||||||
|
const QUEEN = 4;
|
||||||
|
const KING = 5;
|
||||||
|
|
||||||
|
const EMPTY = -1;
|
||||||
|
|
||||||
|
const Images = [
|
||||||
|
'./white_pawn.svg',
|
||||||
|
'./white_rook.svg',
|
||||||
|
'./white_knight.svg',
|
||||||
|
'./white_bishop.svg',
|
||||||
|
'./white_queen.svg',
|
||||||
|
'./white_king.svg',
|
||||||
|
|
||||||
|
'./black_pawn.svg',
|
||||||
|
'./black_rook.svg',
|
||||||
|
'./black_knight.svg',
|
||||||
|
'./black_bishop.svg',
|
||||||
|
'./black_queen.svg',
|
||||||
|
'./black_king.svg',
|
||||||
|
];
|
||||||
|
|
||||||
|
const SHUFFLING_ENABLED = 0;
|
||||||
|
|
||||||
|
function getAllSettings() {
|
||||||
|
return [SHUFFLING_ENABLED];
|
||||||
|
}
|
||||||
|
|
||||||
|
function settingText(setting) {
|
||||||
|
switch(setting) {
|
||||||
|
case SHUFFLING_ENABLED:
|
||||||
|
return "Shuffle Back Row";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function range(n) {
|
||||||
|
return Array.from(Array(n).keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageFromPiece(piece) {
|
||||||
|
if (piece && piece.type >= 0) {
|
||||||
|
const image = piece.color === WHITE ? piece.type : piece.type + 6;
|
||||||
|
return Images[image];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Square(props) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="square"
|
||||||
|
onClick={props.onClick}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${imageFromPiece(props.piece)})`,
|
||||||
|
backgroundSize: `100%`,
|
||||||
|
backgroundColor: props.bgColor,
|
||||||
|
}}
|
||||||
|
title={props.piece == null ? "" : props.piece.getInfoText()}
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Piece {
|
||||||
|
constructor(color, type) {
|
||||||
|
this.color = color;
|
||||||
|
this.type = type;
|
||||||
|
this.passantable = false;
|
||||||
|
this.moves = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setType(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoText() {
|
||||||
|
if(this.moves === 1) {
|
||||||
|
return "Has made 1 move"
|
||||||
|
} else {
|
||||||
|
return "Has made " + this.moves + " moves"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.type === EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFull() {
|
||||||
|
return !this.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
isBlack() {
|
||||||
|
return this.color === BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
isWhite() {
|
||||||
|
return this.color === WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnemyOf(piece) {
|
||||||
|
if (this.color === EMPTY || piece.color === EMPTY) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return this.color !== piece.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isFriendOf(piece) {
|
||||||
|
if (this.color === EMPTY || piece.color === EMPTY) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return this.color === piece.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is(type) {
|
||||||
|
return this.type === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoved() {
|
||||||
|
return this.moves !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasntMoved() {
|
||||||
|
return !this.hasMoved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Board extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = (props && props.text) ?
|
||||||
|
this.stateFromText(props.text) : this.originalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
setHand(hand) {
|
||||||
|
this.setState({
|
||||||
|
squares: this.state.squares,
|
||||||
|
blackIsNext: this.state.blackIsNext,
|
||||||
|
hand: hand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
let board = new Board();
|
||||||
|
board.state.squares = this.state.squares.slice();
|
||||||
|
board.state.blackIsNext = this.state.blackIsNext;
|
||||||
|
board.state.hand = {
|
||||||
|
heldPiece: this.state.hand.heldPiece,
|
||||||
|
};
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFromText(text) {
|
||||||
|
text = text.replace(/[\n]+/g, '');
|
||||||
|
const squares = text.substring(1);
|
||||||
|
return {
|
||||||
|
hand: null,
|
||||||
|
blackIsNext: text[0].toUpperCase() === 'B',
|
||||||
|
squares: squares.split('').map(c => {
|
||||||
|
const type = c.toLowerCase();
|
||||||
|
const color = c === type ? WHITE : BLACK;
|
||||||
|
switch (type) {
|
||||||
|
case 'r':
|
||||||
|
return new Piece(color, ROOK);
|
||||||
|
case 'n':
|
||||||
|
return new Piece(color, KNIGHT);
|
||||||
|
case 'b':
|
||||||
|
return new Piece(color, BISHOP);
|
||||||
|
case 'q':
|
||||||
|
return new Piece(color, QUEEN);
|
||||||
|
case 'k':
|
||||||
|
return new Piece(color, KING);
|
||||||
|
case 'p':
|
||||||
|
return new Piece(color, PAWN);
|
||||||
|
default:
|
||||||
|
return new Piece(EMPTY, EMPTY);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffledBackRow() {
|
||||||
|
return "rnbqkbnr".split('').sort(() => Math.random() - 0.5).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffledBackRowState() {
|
||||||
|
const backRow = this.shuffledBackRow();
|
||||||
|
const text = ["B", backRow, "pppppppp",
|
||||||
|
"________", "________", "________", "________",
|
||||||
|
"PPPPPPPP", backRow.toUpperCase()].join('');
|
||||||
|
return this.stateFromText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
textFromState() {
|
||||||
|
const turn = (this.state.blackIsNext? 'B' : 'W');
|
||||||
|
return turn + this.state.squares.map(square => {
|
||||||
|
if (!square) {
|
||||||
|
return '_';
|
||||||
|
}
|
||||||
|
let color = (c) => {
|
||||||
|
return square.color === BLACK ? c.toUpperCase() : c;
|
||||||
|
};
|
||||||
|
switch (square.type) {
|
||||||
|
case ROOK:
|
||||||
|
return color('r');
|
||||||
|
case KNIGHT:
|
||||||
|
return color('n');
|
||||||
|
case BISHOP:
|
||||||
|
return color('b');
|
||||||
|
case QUEEN:
|
||||||
|
return color('q');
|
||||||
|
case KING:
|
||||||
|
return color('k');
|
||||||
|
case PAWN:
|
||||||
|
return color('p');
|
||||||
|
default:
|
||||||
|
return '_';
|
||||||
|
}
|
||||||
|
}).join('');;
|
||||||
|
}
|
||||||
|
|
||||||
|
doReset() {
|
||||||
|
this.setState(this.getSetting(SHUFFLING_ENABLED) ?
|
||||||
|
this.shuffledBackRowState() :
|
||||||
|
this.originalState());
|
||||||
|
this.setState({
|
||||||
|
showPopup: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
originalState() {
|
||||||
|
let squares = [];
|
||||||
|
const mainRow = [ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK];
|
||||||
|
function add(num, color, type) {
|
||||||
|
for(let i = 0; i < num; i++) {
|
||||||
|
squares.push(new Piece(color, type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRow.forEach(type => add(1, WHITE, type));
|
||||||
|
add(8, WHITE, PAWN);
|
||||||
|
add(32, EMPTY, EMPTY);
|
||||||
|
add(8, BLACK, PAWN);
|
||||||
|
mainRow.forEach(type => add(1, BLACK, type));
|
||||||
|
|
||||||
|
return ({
|
||||||
|
squares,
|
||||||
|
blackIsNext: true,
|
||||||
|
hand: {
|
||||||
|
heldPiece: null,
|
||||||
|
},
|
||||||
|
showPopup: false,
|
||||||
|
settings: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getXandY(i) {
|
||||||
|
const x = i % 8;
|
||||||
|
const y = Math.floor(i / 8);
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndex(x, y) {
|
||||||
|
return x + (y * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidXY(x, y) {
|
||||||
|
return x < 8 && x >=0 && y < 8 && y >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
squareCount() {
|
||||||
|
return this.state.squares.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
pieceAtIndex(i) {
|
||||||
|
return i >= 0 && i < 64 ? this.state.squares[i] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pieceAt(x, y) {
|
||||||
|
if (this.isValidXY(x, y)) {
|
||||||
|
return this.state.squares[this.getIndex(x, y)];
|
||||||
|
} else {
|
||||||
|
return new Piece(EMPTY, EMPTY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidMovesAt(piece, x, y) {
|
||||||
|
let moves = [];
|
||||||
|
const tryAddMove = (x, y) => {
|
||||||
|
if (this.isValidXY(x, y)) {
|
||||||
|
if(this.pieceAt(x, y).isEmpty()) {
|
||||||
|
moves.push({x, y});
|
||||||
|
// Keep searching
|
||||||
|
return 0;
|
||||||
|
} else if (piece.isEnemyOf(this.pieceAt(x, y))) {
|
||||||
|
moves.push({x, y});
|
||||||
|
}
|
||||||
|
// Stop searching
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function addBunch(xFunc, yFunc, isUp) {
|
||||||
|
for (let i = 1; i < 8; i++) {
|
||||||
|
if(tryAddMove(xFunc(i), yFunc(i)) !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (piece.is(PAWN)) {
|
||||||
|
const pieceIsBlack = piece.isBlack();
|
||||||
|
const shift = pieceIsBlack ? -1 : 1;
|
||||||
|
const startLine = pieceIsBlack ? 6 : 1;
|
||||||
|
|
||||||
|
// Check for en passant
|
||||||
|
const left = this.pieceAt(x - 1, y);
|
||||||
|
const right = this.pieceAt(x + 1, y);
|
||||||
|
if (left && left.passantable && left.isEnemyOf(piece)) {
|
||||||
|
moves.push({x: x - 1, y: y + shift, passant: {x: x - 1, y}})
|
||||||
|
}
|
||||||
|
if (right && right.passantable && right.isEnemyOf(piece)) {
|
||||||
|
moves.push({x: x + 1, y: y + shift, passant: {x: x + 1, y}})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pieceAt(x, y + shift).isEmpty()) {
|
||||||
|
moves.push({x, y: y + shift});
|
||||||
|
// Pawn moving two spaces becomes en-passantable
|
||||||
|
if (y === startLine && this.pieceAt(x, y + (shift * 2)).isEmpty()) {
|
||||||
|
moves.push({x, y: y + (shift * 2), passantable: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[x + 1, x - 1].forEach(x => {
|
||||||
|
const y2 = y + shift;
|
||||||
|
if (this.isValidXY(x, y2) && piece.isEnemyOf(this.pieceAt(x, y2))) {
|
||||||
|
moves.push({x, y: y2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (piece.is(ROOK)) {
|
||||||
|
addBunch(n => {return x;}, n => {return y + n;});
|
||||||
|
addBunch(n => {return x;}, n => {return y - n;});
|
||||||
|
addBunch(n => {return x + n;}, n => {return y;});
|
||||||
|
addBunch(n => {return x - n;}, n => {return y;});
|
||||||
|
} else if (piece.is(BISHOP)) {
|
||||||
|
addBunch(n => {return x + n;}, n => {return y + n;});
|
||||||
|
addBunch(n => {return x - n;}, n => {return y + n;});
|
||||||
|
addBunch(n => {return x + n;}, n => {return y - n;});
|
||||||
|
addBunch(n => {return x - n;}, n => {return y - n;});
|
||||||
|
} else if (piece.is(QUEEN)) {
|
||||||
|
const [rook, bishop] =
|
||||||
|
[new Piece(piece.color, ROOK), new Piece(piece.color, BISHOP)];
|
||||||
|
moves = moves.concat(this.getValidMovesAt(rook, x, y));
|
||||||
|
moves = moves.concat(this.getValidMovesAt(bishop, x, y));
|
||||||
|
} else if (piece.is(KNIGHT)) {
|
||||||
|
[
|
||||||
|
[2, 1], [2, -1], [-2, 1], [-2, -1],
|
||||||
|
[1, 2], [1, -2], [-1, 2], [-1, -2],
|
||||||
|
].forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
|
||||||
|
} else if (piece.is(KING)) {
|
||||||
|
[[1, 1], [1, -1], [-1, 1], [-1, -1], [0, 1], [0, -1], [1, 0], [-1, 0]]
|
||||||
|
.forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
|
||||||
|
if (piece.hasntMoved()) {
|
||||||
|
const kingIndex = this.findIndex(piece);
|
||||||
|
const [x, y] = this.getXandY(kingIndex);
|
||||||
|
|
||||||
|
let leftRook = this.pieceAt(0, y);
|
||||||
|
if(leftRook.is(ROOK) && leftRook.hasntMoved()) {
|
||||||
|
// Check if spaces between rook and king are empty
|
||||||
|
if(this.pieceAt(1, y).isEmpty() &&
|
||||||
|
this.pieceAt(2, y).isEmpty() &&
|
||||||
|
this.pieceAt(3, y).isEmpty()) {
|
||||||
|
// Check if between space puts king in check
|
||||||
|
let board = this.clone();
|
||||||
|
board.state.squares[board.getIndex(x - 1, y)] = piece;
|
||||||
|
board.state.squares[kingIndex].isEmpty();
|
||||||
|
if(board.inCheck(piece) == null) {
|
||||||
|
moves.push({x: x - 2, y, castle: [x - 1, y]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightRook = this.pieceAt(7, y);
|
||||||
|
if(rightRook.is(ROOK) && rightRook.hasntMoved()) {
|
||||||
|
// Check if spaces between rook and king are empty
|
||||||
|
if(this.pieceAt(5, y).isEmpty() &&
|
||||||
|
this.pieceAt(6, y).isEmpty()) {
|
||||||
|
// Check if between space puts king in check
|
||||||
|
let board = this.clone();
|
||||||
|
board.state.squares[board.getIndex(x + 1, y)] = piece;
|
||||||
|
board.state.squares[kingIndex].isEmpty();
|
||||||
|
if(board.inCheck(piece) == null) {
|
||||||
|
moves.push({x: x + 2, y, castle: [x + 1, y]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
|
||||||
|
findIndex(piece) {
|
||||||
|
for(let i = 0; i < this.squareCount(); i++) {
|
||||||
|
const check = this.state.squares[i];
|
||||||
|
if(check.type === piece.type && check.color === piece.color) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceBetween(i1, i2) {
|
||||||
|
const [pos1X, pos1Y] = this.getXandY(i1);
|
||||||
|
const [pos2X, pos2Y] = this.getXandY(i2);
|
||||||
|
|
||||||
|
let a = pos1X - pos2X;
|
||||||
|
a = a * a;
|
||||||
|
|
||||||
|
let b = pos1Y - pos2Y;
|
||||||
|
b = b * b;
|
||||||
|
|
||||||
|
return Math.sqrt(a + b);
|
||||||
|
}
|
||||||
|
|
||||||
|
inCheck(piece) {
|
||||||
|
const kingPos = this.getXandY(this.findIndex(piece));
|
||||||
|
|
||||||
|
for(let i = 0; i < this.squareCount(); i++) {
|
||||||
|
if(piece.isEnemyOf(this.pieceAtIndex(i))) {
|
||||||
|
const moves = this.getValidMoves(i);
|
||||||
|
for(let j = 0; j < moves.length; j++) {
|
||||||
|
if(moves[j].x === kingPos[0] && moves[j].y === kingPos[1]) {
|
||||||
|
return piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
whoInCheck() {
|
||||||
|
const blackKing = this.inCheck(new Piece(BLACK, KING));
|
||||||
|
return blackKing ? blackKing : this.inCheck(new Piece(WHITE, KING));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkmate() {
|
||||||
|
const checkedKing = this.whoInCheck();
|
||||||
|
if (checkedKing != null) {
|
||||||
|
// For each square
|
||||||
|
for(let i = 0; i < this.squareCount(); i++) {
|
||||||
|
const piece = this.pieceAtIndex(i);
|
||||||
|
// If that piece is on the checked team
|
||||||
|
if (piece != null && piece.isFriendOf(checkedKing)) {
|
||||||
|
// For each move of the above piece
|
||||||
|
const moves = this.getValidMoves(i)
|
||||||
|
for(const move of moves) {
|
||||||
|
// Copy the board
|
||||||
|
const board = this.clone();
|
||||||
|
board.state.squares[board.getIndex(move.x, move.y)] = board.state.squares[i];
|
||||||
|
board.state.squares[i].isEmpty();
|
||||||
|
const check = board.inCheck(checkedKing);
|
||||||
|
if (check == null || check.color !== checkedKing.color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidMoves(source) {
|
||||||
|
const [x, y] = this.getXandY(source);
|
||||||
|
|
||||||
|
const piece = this.pieceAtIndex(source);
|
||||||
|
return this.getValidMovesAt(piece, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidMove(source, dest) {
|
||||||
|
const [destX, destY] = this.getXandY(dest);
|
||||||
|
|
||||||
|
for (const move of this.getValidMoves(source)) {
|
||||||
|
if (destX === move.x && destY === move.y) {
|
||||||
|
return move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoldingPiece() {
|
||||||
|
return this.heldPiece() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
heldPiece() {
|
||||||
|
return (this.state && this.state.hand) ? this.state.hand.heldPiece : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeMove(from, to) {
|
||||||
|
const squares = this.state.squares.slice();
|
||||||
|
const move = this.isValidMove(from, to)
|
||||||
|
if (move) {
|
||||||
|
if (move.passant) {
|
||||||
|
squares[this.getIndex(move.passant.x, move.passant.y)] = new Piece(EMPTY, EMPTY);
|
||||||
|
}
|
||||||
|
if (move.castle) {
|
||||||
|
// .castle holds the position where the rook should end up
|
||||||
|
// King moved left
|
||||||
|
const rookX = move.castle[0] > move.x ? 0 : 7;
|
||||||
|
console.log("Replace ");
|
||||||
|
console.log(move.castle);
|
||||||
|
console.log("With ");
|
||||||
|
console.log([rookX, move.castle[1]]);
|
||||||
|
squares[this.getIndex(move.castle[0], move.castle[1])] =
|
||||||
|
squares[this.getIndex(rookX, move.castle[1])];
|
||||||
|
squares[this.getIndex(rookX, move.castle[1])] = new Piece(EMPTY, EMPTY);
|
||||||
|
}
|
||||||
|
// Remove existing passantable states
|
||||||
|
squares.forEach(square => {
|
||||||
|
if (square) {
|
||||||
|
square.passantable = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (move.passantable) {
|
||||||
|
squares[from].passantable = true;
|
||||||
|
}
|
||||||
|
const y = this.getXandY(to)[1];
|
||||||
|
squares[to] = squares[from];
|
||||||
|
squares[from] = new Piece(EMPTY, EMPTY);
|
||||||
|
if (squares[to].type === PAWN && (y === 0 || y === 7)) {
|
||||||
|
squares[to].setType(QUEEN);
|
||||||
|
}
|
||||||
|
squares[to].moves++;
|
||||||
|
this.setState({
|
||||||
|
squares: squares,
|
||||||
|
blackIsNext: !this.state.blackIsNext,
|
||||||
|
hand: {
|
||||||
|
heldPiece: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(i) {
|
||||||
|
if (this.checkmate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.isHoldingPiece()) {
|
||||||
|
// Copy the board
|
||||||
|
let board = this.clone();
|
||||||
|
board.state.squares[i] = board.state.squares[board.heldPiece()];
|
||||||
|
board.state.squares[this.heldPiece()] = new Piece(EMPTY, EMPTY);
|
||||||
|
|
||||||
|
const moversKing = this.state.blackIsNext ?
|
||||||
|
new Piece(BLACK, KING) : new Piece(WHITE, KING);
|
||||||
|
if (board.inCheck(moversKing) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.makeMove(this.heldPiece(), i) !== 0) {
|
||||||
|
this.setHand({
|
||||||
|
heldPiece: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (this.state.squares[i].isFull()) {
|
||||||
|
const isSquareBlack = this.state.squares[i].isBlack();
|
||||||
|
if(isSquareBlack === this.state.blackIsNext) {
|
||||||
|
this.setHand({
|
||||||
|
heldPiece: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSquare(i) {
|
||||||
|
const plainBg = (i + (Math.floor(i / 8))) % 2 === 0 ? "white" : "#666";
|
||||||
|
const bgColor = this.heldPiece() === i ? "#5D98E6" : plainBg;
|
||||||
|
return (
|
||||||
|
<Square
|
||||||
|
key={"square-" + i}
|
||||||
|
piece={this.state.squares[i]}
|
||||||
|
onClick={() => this.handleClick(i)}
|
||||||
|
bgColor={bgColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
row(r) {
|
||||||
|
const i = r * 8;
|
||||||
|
return (
|
||||||
|
<div className="board-row" key={"row=" + r}>
|
||||||
|
{range(8).map(n => this.renderSquare(n + i))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePopup() {
|
||||||
|
this.setState({
|
||||||
|
showPopup: !this.state.showPopup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setSetting(name, value) {
|
||||||
|
let settings = this.state.settings;
|
||||||
|
settings[name] = value;
|
||||||
|
this.setState({
|
||||||
|
settings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSetting(name) {
|
||||||
|
return this.state.settings[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSetting(name) {
|
||||||
|
console.log("toggle " + settingText(name));
|
||||||
|
let settings = this.state.settings;
|
||||||
|
settings[name] = !settings[name];
|
||||||
|
console.log(settings[name]);
|
||||||
|
this.setState({
|
||||||
|
settings: settings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPopup() {
|
||||||
|
return (this.state.showPopup ?
|
||||||
|
<Popup
|
||||||
|
header='QuickChess'
|
||||||
|
closePopup={this.togglePopup.bind(this)}
|
||||||
|
body={<div>
|
||||||
|
|
||||||
|
<p>This is a simple implementation of the classic board game,
|
||||||
|
implemented in React. It supports all possible moves, including
|
||||||
|
castling, and <em>en passant</em>.</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
getAllSettings().map(setting => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={this.toggleSetting.bind(this, setting)}>
|
||||||
|
{
|
||||||
|
settingText(setting) +
|
||||||
|
(this.getSetting(setting) ? " On" : " Off")
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<button onClick={this.doReset.bind(this)}>Reset Game</button>
|
||||||
|
|
||||||
|
</div>}
|
||||||
|
/>
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Board class can't (always) include a settings button if used as a demo. */
|
||||||
|
render() {
|
||||||
|
const checkMsg = this.whoInCheck() ? "Check! " : "";
|
||||||
|
const isCheckmate = this.checkmate();
|
||||||
|
const namedPlayer = isCheckmate ?
|
||||||
|
!this.state.blackIsNext : this.state.blackIsNext
|
||||||
|
const color = namedPlayer ? 'Black' : 'White';
|
||||||
|
|
||||||
|
const status = isCheckmate ? "Checkmate! " + color + " Wins!" :
|
||||||
|
checkMsg + color + "'s Turn";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{textAlign: `center`,}}>
|
||||||
|
<div className="status">
|
||||||
|
<h1 style={{display: "inline-block"}}>{status}</h1>
|
||||||
|
<img
|
||||||
|
className="icon"
|
||||||
|
alt="Settings icon: a gear"
|
||||||
|
style={{height: "5vh", float: "right"}}
|
||||||
|
src="./gear.svg" onClick={this.togglePopup.bind(this)}>
|
||||||
|
</img>
|
||||||
|
</div>
|
||||||
|
{range(8).map(n => this.row(n))}
|
||||||
|
{this.renderPopup()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Board;
|
||||||
|
export { BLACK, WHITE, PAWN, ROOK, KNIGHT, BISHOP, QUEEN, KING, EMPTY };
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Board from './board';
|
||||||
|
import * as game from './board';
|
||||||
|
|
||||||
|
it('is created correctly', () => {
|
||||||
|
const board = new Board();
|
||||||
|
const rows = ['B',
|
||||||
|
'rnbqkbnr',
|
||||||
|
'pppppppp',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'PPPPPPPP',
|
||||||
|
'RNBQKBNR'
|
||||||
|
];
|
||||||
|
expect(board.textFromState()).toBe(rows.join(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is created from text correctly', () => {
|
||||||
|
const rows = ['B',
|
||||||
|
'pppppppp',
|
||||||
|
'pppppppp',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'PPPPPPPP',
|
||||||
|
'PPPPPPPP'
|
||||||
|
];
|
||||||
|
const board = new Board({text: rows.join('')});
|
||||||
|
expect(board.textFromState()).toBe(rows.join(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects an obvious checkmate', () => {
|
||||||
|
const rows = ['B',
|
||||||
|
'q_______',
|
||||||
|
'q______K',
|
||||||
|
'q_______',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'________',
|
||||||
|
'k_______'
|
||||||
|
];
|
||||||
|
const board = new Board({text: rows.join('')});
|
||||||
|
const inCheck = board.whoInCheck();
|
||||||
|
expect(inCheck.type).toEqual(game.KING);
|
||||||
|
expect(inCheck.color).toEqual(game.BLACK);
|
||||||
|
});
|
707
src/index.js
707
src/index.js
|
@ -1,716 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import Popup from './components/Popup';
|
import Board from './board';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
|
||||||
const BLACK = 0;
|
|
||||||
const WHITE = 1;
|
|
||||||
|
|
||||||
const PAWN = 0;
|
|
||||||
const ROOK = 1;
|
|
||||||
const KNIGHT = 2;
|
|
||||||
const BISHOP = 3;
|
|
||||||
const QUEEN = 4;
|
|
||||||
const KING = 5;
|
|
||||||
|
|
||||||
const EMPTY = -1;
|
|
||||||
|
|
||||||
const Images = [
|
|
||||||
'./white_pawn.svg',
|
|
||||||
'./white_rook.svg',
|
|
||||||
'./white_knight.svg',
|
|
||||||
'./white_bishop.svg',
|
|
||||||
'./white_queen.svg',
|
|
||||||
'./white_king.svg',
|
|
||||||
|
|
||||||
'./black_pawn.svg',
|
|
||||||
'./black_rook.svg',
|
|
||||||
'./black_knight.svg',
|
|
||||||
'./black_bishop.svg',
|
|
||||||
'./black_queen.svg',
|
|
||||||
'./black_king.svg',
|
|
||||||
];
|
|
||||||
|
|
||||||
const SHUFFLING_ENABLED = 0;
|
|
||||||
|
|
||||||
function getAllSettings() {
|
|
||||||
return [SHUFFLING_ENABLED];
|
|
||||||
}
|
|
||||||
|
|
||||||
function settingText(setting) {
|
|
||||||
switch(setting) {
|
|
||||||
case SHUFFLING_ENABLED:
|
|
||||||
return "Shuffle Back Row";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function range(n) {
|
|
||||||
return Array.from(Array(n).keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
function imageFromPiece(piece) {
|
|
||||||
if (piece && piece.type >= 0) {
|
|
||||||
const image = piece.color === WHITE ? piece.type : piece.type + 6;
|
|
||||||
return Images[image];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Square(props) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="square"
|
|
||||||
onClick={props.onClick}
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${imageFromPiece(props.piece)})`,
|
|
||||||
backgroundSize: `100%`,
|
|
||||||
backgroundColor: props.bgColor,
|
|
||||||
}}
|
|
||||||
title={props.piece == null ? "" : props.piece.getInfoText()}
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Piece {
|
|
||||||
constructor(color, type) {
|
|
||||||
this.color = color;
|
|
||||||
this.type = type;
|
|
||||||
this.passantable = false;
|
|
||||||
this.moves = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setType(type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoText() {
|
|
||||||
if(this.moves === 1) {
|
|
||||||
return "Has made 1 move"
|
|
||||||
} else {
|
|
||||||
return "Has made " + this.moves + " moves"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty() {
|
|
||||||
return this.type === EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
isFull() {
|
|
||||||
return !this.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
isBlack() {
|
|
||||||
return this.color === BLACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
isWhite() {
|
|
||||||
return this.color === WHITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnemyOf(piece) {
|
|
||||||
if (this.color === EMPTY || piece.color === EMPTY) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return this.color !== piece.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isFriendOf(piece) {
|
|
||||||
if (this.color === EMPTY || piece.color === EMPTY) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return this.color === piece.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is(type) {
|
|
||||||
return this.type === type;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoved() {
|
|
||||||
return this.moves !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasntMoved() {
|
|
||||||
return !this.hasMoved();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Board extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = (props && props.text) ?
|
|
||||||
{
|
|
||||||
squares: this.stateFromText(props.text),
|
|
||||||
blackIsNext: true,
|
|
||||||
hand: {
|
|
||||||
heldPiece: null,
|
|
||||||
},
|
|
||||||
settings: {},
|
|
||||||
} : this.originalState();
|
|
||||||
}
|
|
||||||
|
|
||||||
setHand(hand) {
|
|
||||||
this.setState({
|
|
||||||
squares: this.state.squares,
|
|
||||||
blackIsNext: this.state.blackIsNext,
|
|
||||||
hand: hand,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clone() {
|
|
||||||
let board = new Board();
|
|
||||||
board.state.squares = this.state.squares.slice();
|
|
||||||
board.state.blackIsNext = this.state.blackIsNext;
|
|
||||||
board.state.hand = {
|
|
||||||
heldPiece: this.state.hand.heldPiece,
|
|
||||||
};
|
|
||||||
return board;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateFromText(text) {
|
|
||||||
text = text.replace(/[\n]+/g, '');
|
|
||||||
const squares = text.substring(1);
|
|
||||||
return {
|
|
||||||
hand: null,
|
|
||||||
blackIsNext: text[0].toUpperCase() === 'B',
|
|
||||||
squares: squares.split('').map(c => {
|
|
||||||
const type = c.toLowerCase();
|
|
||||||
const color = c === type ? WHITE : BLACK;
|
|
||||||
switch (type) {
|
|
||||||
case 'r':
|
|
||||||
return new Piece(color, ROOK);
|
|
||||||
case 'n':
|
|
||||||
return new Piece(color, KNIGHT);
|
|
||||||
case 'b':
|
|
||||||
return new Piece(color, BISHOP);
|
|
||||||
case 'q':
|
|
||||||
return new Piece(color, QUEEN);
|
|
||||||
case 'k':
|
|
||||||
return new Piece(color, KING);
|
|
||||||
case 'p':
|
|
||||||
return new Piece(color, PAWN);
|
|
||||||
default:
|
|
||||||
return new Piece(EMPTY, EMPTY);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffledBackRow() {
|
|
||||||
return "rnbqkbnr".split('').sort(() => Math.random() - 0.5).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffledBackRowState() {
|
|
||||||
const backRow = this.shuffledBackRow();
|
|
||||||
const text = ["B", backRow, "pppppppp",
|
|
||||||
"________", "________", "________", "________",
|
|
||||||
"PPPPPPPP", backRow.toUpperCase()].join('');
|
|
||||||
return this.stateFromText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
textFromState() {
|
|
||||||
const turn = (this.state.blackIsNext? 'B' : 'W');
|
|
||||||
return turn + this.state.squares.map(square => {
|
|
||||||
if (!square) {
|
|
||||||
return '_';
|
|
||||||
}
|
|
||||||
let color = (c) => {
|
|
||||||
return square.color === BLACK ? c.toUpperCase() : c;
|
|
||||||
};
|
|
||||||
switch (square.type) {
|
|
||||||
case ROOK:
|
|
||||||
return color('r');
|
|
||||||
case KNIGHT:
|
|
||||||
return color('n');
|
|
||||||
case BISHOP:
|
|
||||||
return color('b');
|
|
||||||
case QUEEN:
|
|
||||||
return color('q');
|
|
||||||
case KING:
|
|
||||||
return color('k');
|
|
||||||
case PAWN:
|
|
||||||
return color('p');
|
|
||||||
default:
|
|
||||||
return '_';
|
|
||||||
}
|
|
||||||
}).join('');;
|
|
||||||
}
|
|
||||||
|
|
||||||
doReset() {
|
|
||||||
this.setState(this.getSetting(SHUFFLING_ENABLED) ?
|
|
||||||
this.shuffledBackRowState() :
|
|
||||||
this.originalState());
|
|
||||||
this.setState({
|
|
||||||
showPopup: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
originalState() {
|
|
||||||
let squares = [];
|
|
||||||
const mainRow = [ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK];
|
|
||||||
function add(num, color, type) {
|
|
||||||
for(let i = 0; i < num; i++) {
|
|
||||||
squares.push(new Piece(color, type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainRow.forEach(type => add(1, WHITE, type));
|
|
||||||
add(8, WHITE, PAWN);
|
|
||||||
add(32, EMPTY, EMPTY);
|
|
||||||
add(8, BLACK, PAWN);
|
|
||||||
mainRow.forEach(type => add(1, BLACK, type));
|
|
||||||
|
|
||||||
return ({
|
|
||||||
squares,
|
|
||||||
blackIsNext: true,
|
|
||||||
hand: {
|
|
||||||
heldPiece: null,
|
|
||||||
},
|
|
||||||
showPopup: false,
|
|
||||||
settings: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getXandY(i) {
|
|
||||||
const x = i % 8;
|
|
||||||
const y = Math.floor(i / 8);
|
|
||||||
return [x, y];
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndex(x, y) {
|
|
||||||
return x + (y * 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidXY(x, y) {
|
|
||||||
return x < 8 && x >=0 && y < 8 && y >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
squareCount() {
|
|
||||||
return this.state.squares.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
pieceAtIndex(i) {
|
|
||||||
return i >= 0 && i < 64 ? this.state.squares[i] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pieceAt(x, y) {
|
|
||||||
if (this.isValidXY(x, y)) {
|
|
||||||
return this.state.squares[this.getIndex(x, y)];
|
|
||||||
} else {
|
|
||||||
return new Piece(EMPTY, EMPTY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidMovesAt(piece, x, y) {
|
|
||||||
let moves = [];
|
|
||||||
const tryAddMove = (x, y) => {
|
|
||||||
if (this.isValidXY(x, y)) {
|
|
||||||
if(this.pieceAt(x, y).isEmpty()) {
|
|
||||||
moves.push({x, y});
|
|
||||||
// Keep searching
|
|
||||||
return 0;
|
|
||||||
} else if (piece.isEnemyOf(this.pieceAt(x, y))) {
|
|
||||||
moves.push({x, y});
|
|
||||||
}
|
|
||||||
// Stop searching
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function addBunch(xFunc, yFunc, isUp) {
|
|
||||||
for (let i = 1; i < 8; i++) {
|
|
||||||
if(tryAddMove(xFunc(i), yFunc(i)) !== 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (piece.is(PAWN)) {
|
|
||||||
const pieceIsBlack = piece.isBlack();
|
|
||||||
const shift = pieceIsBlack ? -1 : 1;
|
|
||||||
const startLine = pieceIsBlack ? 6 : 1;
|
|
||||||
|
|
||||||
// Check for en passant
|
|
||||||
const left = this.pieceAt(x - 1, y);
|
|
||||||
const right = this.pieceAt(x + 1, y);
|
|
||||||
if (left && left.passantable && left.isEnemyOf(piece)) {
|
|
||||||
moves.push({x: x - 1, y: y + shift, passant: {x: x - 1, y}})
|
|
||||||
}
|
|
||||||
if (right && right.passantable && right.isEnemyOf(piece)) {
|
|
||||||
moves.push({x: x + 1, y: y + shift, passant: {x: x + 1, y}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.pieceAt(x, y + shift).isEmpty()) {
|
|
||||||
moves.push({x, y: y + shift});
|
|
||||||
// Pawn moving two spaces becomes en-passantable
|
|
||||||
if (y === startLine && this.pieceAt(x, y + (shift * 2)).isEmpty()) {
|
|
||||||
moves.push({x, y: y + (shift * 2), passantable: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[x + 1, x - 1].forEach(x => {
|
|
||||||
const y2 = y + shift;
|
|
||||||
if (this.isValidXY(x, y2) && piece.isEnemyOf(this.pieceAt(x, y2))) {
|
|
||||||
moves.push({x, y: y2});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (piece.is(ROOK)) {
|
|
||||||
addBunch(n => {return x;}, n => {return y + n;});
|
|
||||||
addBunch(n => {return x;}, n => {return y - n;});
|
|
||||||
addBunch(n => {return x + n;}, n => {return y;});
|
|
||||||
addBunch(n => {return x - n;}, n => {return y;});
|
|
||||||
} else if (piece.is(BISHOP)) {
|
|
||||||
addBunch(n => {return x + n;}, n => {return y + n;});
|
|
||||||
addBunch(n => {return x - n;}, n => {return y + n;});
|
|
||||||
addBunch(n => {return x + n;}, n => {return y - n;});
|
|
||||||
addBunch(n => {return x - n;}, n => {return y - n;});
|
|
||||||
} else if (piece.is(QUEEN)) {
|
|
||||||
const [rook, bishop] =
|
|
||||||
[new Piece(piece.color, ROOK), new Piece(piece.color, BISHOP)];
|
|
||||||
moves = moves.concat(this.getValidMovesAt(rook, x, y));
|
|
||||||
moves = moves.concat(this.getValidMovesAt(bishop, x, y));
|
|
||||||
} else if (piece.is(KNIGHT)) {
|
|
||||||
[
|
|
||||||
[2, 1], [2, -1], [-2, 1], [-2, -1],
|
|
||||||
[1, 2], [1, -2], [-1, 2], [-1, -2],
|
|
||||||
].forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
|
|
||||||
} else if (piece.is(KING)) {
|
|
||||||
[[1, 1], [1, -1], [-1, 1], [-1, -1], [0, 1], [0, -1], [1, 0], [-1, 0]]
|
|
||||||
.forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
|
|
||||||
if (piece.hasntMoved()) {
|
|
||||||
const kingIndex = this.findIndex(piece);
|
|
||||||
const [x, y] = this.getXandY(kingIndex);
|
|
||||||
|
|
||||||
let leftRook = this.pieceAt(0, y);
|
|
||||||
if(leftRook.is(ROOK) && leftRook.hasntMoved()) {
|
|
||||||
// Check if spaces between rook and king are empty
|
|
||||||
if(this.pieceAt(1, y).isEmpty() &&
|
|
||||||
this.pieceAt(2, y).isEmpty() &&
|
|
||||||
this.pieceAt(3, y).isEmpty()) {
|
|
||||||
// Check if between space puts king in check
|
|
||||||
let board = this.clone();
|
|
||||||
board.state.squares[board.getIndex(x - 1, y)] = piece;
|
|
||||||
board.state.squares[kingIndex].isEmpty();
|
|
||||||
if(board.inCheck(piece) == null) {
|
|
||||||
moves.push({x: x - 2, y, castle: [x - 1, y]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rightRook = this.pieceAt(7, y);
|
|
||||||
if(rightRook.is(ROOK) && rightRook.hasntMoved()) {
|
|
||||||
// Check if spaces between rook and king are empty
|
|
||||||
if(this.pieceAt(5, y).isEmpty() &&
|
|
||||||
this.pieceAt(6, y).isEmpty()) {
|
|
||||||
// Check if between space puts king in check
|
|
||||||
let board = this.clone();
|
|
||||||
board.state.squares[board.getIndex(x + 1, y)] = piece;
|
|
||||||
board.state.squares[kingIndex].isEmpty();
|
|
||||||
if(board.inCheck(piece) == null) {
|
|
||||||
moves.push({x: x + 2, y, castle: [x + 1, y]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return moves;
|
|
||||||
}
|
|
||||||
|
|
||||||
findIndex(piece) {
|
|
||||||
for(let i = 0; i < this.squareCount(); i++) {
|
|
||||||
const check = this.state.squares[i];
|
|
||||||
if(check.type === piece.type && check.color === piece.color) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
distanceBetween(i1, i2) {
|
|
||||||
const [pos1X, pos1Y] = this.getXandY(i1);
|
|
||||||
const [pos2X, pos2Y] = this.getXandY(i2);
|
|
||||||
|
|
||||||
let a = pos1X - pos2X;
|
|
||||||
a = a * a;
|
|
||||||
|
|
||||||
let b = pos1Y - pos2Y;
|
|
||||||
b = b * b;
|
|
||||||
|
|
||||||
return Math.sqrt(a + b);
|
|
||||||
}
|
|
||||||
|
|
||||||
inCheck(piece) {
|
|
||||||
const kingPos = this.getXandY(this.findIndex(piece));
|
|
||||||
|
|
||||||
for(let i = 0; i < this.squareCount(); i++) {
|
|
||||||
if(piece.isEnemyOf(this.pieceAtIndex(i))) {
|
|
||||||
const moves = this.getValidMoves(i);
|
|
||||||
for(let j = 0; j < moves.length; j++) {
|
|
||||||
if(moves[j].x === kingPos[0] && moves[j].y === kingPos[1]) {
|
|
||||||
return piece;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
whoInCheck() {
|
|
||||||
const blackKing = this.inCheck(new Piece(BLACK, KING));
|
|
||||||
return blackKing ? blackKing : this.inCheck(new Piece(WHITE, KING));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkmate() {
|
|
||||||
const checkedKing = this.whoInCheck();
|
|
||||||
if (checkedKing != null) {
|
|
||||||
// For each square
|
|
||||||
for(let i = 0; i < this.squareCount(); i++) {
|
|
||||||
const piece = this.pieceAtIndex(i);
|
|
||||||
// If that piece is on the checked team
|
|
||||||
if (piece != null && piece.isFriendOf(checkedKing)) {
|
|
||||||
// For each move of the above piece
|
|
||||||
const moves = this.getValidMoves(i)
|
|
||||||
for(const move of moves) {
|
|
||||||
// Copy the board
|
|
||||||
const board = this.clone();
|
|
||||||
board.state.squares[board.getIndex(move.x, move.y)] = board.state.squares[i];
|
|
||||||
board.state.squares[i].isEmpty();
|
|
||||||
const check = board.inCheck(checkedKing);
|
|
||||||
if (check == null || check.color !== checkedKing.color) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidMoves(source) {
|
|
||||||
const [x, y] = this.getXandY(source);
|
|
||||||
|
|
||||||
const piece = this.pieceAtIndex(source);
|
|
||||||
return this.getValidMovesAt(piece, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidMove(source, dest) {
|
|
||||||
const [destX, destY] = this.getXandY(dest);
|
|
||||||
|
|
||||||
for (const move of this.getValidMoves(source)) {
|
|
||||||
if (destX === move.x && destY === move.y) {
|
|
||||||
return move;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isHoldingPiece() {
|
|
||||||
return this.heldPiece() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
heldPiece() {
|
|
||||||
return (this.state && this.state.hand) ? this.state.hand.heldPiece : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeMove(from, to) {
|
|
||||||
const squares = this.state.squares.slice();
|
|
||||||
const move = this.isValidMove(from, to)
|
|
||||||
if (move) {
|
|
||||||
if (move.passant) {
|
|
||||||
squares[this.getIndex(move.passant.x, move.passant.y)] = new Piece(EMPTY, EMPTY);
|
|
||||||
}
|
|
||||||
if (move.castle) {
|
|
||||||
// .castle holds the position where the rook should end up
|
|
||||||
// King moved left
|
|
||||||
const rookX = move.castle[0] > move.x ? 0 : 7;
|
|
||||||
console.log("Replace ");
|
|
||||||
console.log(move.castle);
|
|
||||||
console.log("With ");
|
|
||||||
console.log([rookX, move.castle[1]]);
|
|
||||||
squares[this.getIndex(move.castle[0], move.castle[1])] =
|
|
||||||
squares[this.getIndex(rookX, move.castle[1])];
|
|
||||||
squares[this.getIndex(rookX, move.castle[1])] = new Piece(EMPTY, EMPTY);
|
|
||||||
}
|
|
||||||
// Remove existing passantable states
|
|
||||||
squares.forEach(square => {
|
|
||||||
if (square) {
|
|
||||||
square.passantable = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (move.passantable) {
|
|
||||||
squares[from].passantable = true;
|
|
||||||
}
|
|
||||||
const y = this.getXandY(to)[1];
|
|
||||||
squares[to] = squares[from];
|
|
||||||
squares[from] = new Piece(EMPTY, EMPTY);
|
|
||||||
if (squares[to].type === PAWN && (y === 0 || y === 7)) {
|
|
||||||
squares[to].setType(QUEEN);
|
|
||||||
}
|
|
||||||
squares[to].moves++;
|
|
||||||
this.setState({
|
|
||||||
squares: squares,
|
|
||||||
blackIsNext: !this.state.blackIsNext,
|
|
||||||
hand: {
|
|
||||||
heldPiece: null,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(i) {
|
|
||||||
if (this.checkmate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isHoldingPiece()) {
|
|
||||||
// Copy the board
|
|
||||||
let board = this.clone();
|
|
||||||
board.state.squares[i] = board.state.squares[board.heldPiece()];
|
|
||||||
board.state.squares[this.heldPiece()] = new Piece(EMPTY, EMPTY);
|
|
||||||
|
|
||||||
const moversKing = this.state.blackIsNext ?
|
|
||||||
new Piece(BLACK, KING) : new Piece(WHITE, KING);
|
|
||||||
if (board.inCheck(moversKing) != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.makeMove(this.heldPiece(), i) !== 0) {
|
|
||||||
this.setHand({
|
|
||||||
heldPiece: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (this.state.squares[i].isFull()) {
|
|
||||||
const isSquareBlack = this.state.squares[i].isBlack();
|
|
||||||
if(isSquareBlack === this.state.blackIsNext) {
|
|
||||||
this.setHand({
|
|
||||||
heldPiece: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSquare(i) {
|
|
||||||
const plainBg = (i + (Math.floor(i / 8))) % 2 === 0 ? "white" : "#666";
|
|
||||||
const bgColor = this.heldPiece() === i ? "#5D98E6" : plainBg;
|
|
||||||
return (
|
|
||||||
<Square
|
|
||||||
key={"square-" + i}
|
|
||||||
piece={this.state.squares[i]}
|
|
||||||
onClick={() => this.handleClick(i)}
|
|
||||||
bgColor={bgColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
row(r) {
|
|
||||||
const i = r * 8;
|
|
||||||
return (
|
|
||||||
<div className="board-row" key={"row=" + r}>
|
|
||||||
{range(8).map(n => this.renderSquare(n + i))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePopup() {
|
|
||||||
this.setState({
|
|
||||||
showPopup: !this.state.showPopup
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setSetting(name, value) {
|
|
||||||
let settings = this.state.settings;
|
|
||||||
settings[name] = value;
|
|
||||||
this.setState({
|
|
||||||
settings
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getSetting(name) {
|
|
||||||
return this.state.settings[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSetting(name) {
|
|
||||||
console.log("toggle " + settingText(name));
|
|
||||||
let settings = this.state.settings;
|
|
||||||
settings[name] = !settings[name];
|
|
||||||
console.log(settings[name]);
|
|
||||||
this.setState({
|
|
||||||
settings: settings,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPopup() {
|
|
||||||
return (this.state.showPopup ?
|
|
||||||
<Popup
|
|
||||||
header='QuickChess'
|
|
||||||
closePopup={this.togglePopup.bind(this)}
|
|
||||||
body={<div>
|
|
||||||
|
|
||||||
<p>This is a simple implementation of the classic board game,
|
|
||||||
implemented in React. It supports all possible moves, including
|
|
||||||
castling, and <em>en passant</em>.</p>
|
|
||||||
|
|
||||||
{
|
|
||||||
getAllSettings().map(setting => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={this.toggleSetting.bind(this, setting)}>
|
|
||||||
{
|
|
||||||
settingText(setting) +
|
|
||||||
(this.getSetting(setting) ? " On" : " Off")
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<button onClick={this.doReset.bind(this)}>Reset Game</button>
|
|
||||||
|
|
||||||
</div>}
|
|
||||||
/>
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Board class can't (always) include a settings button if used as a demo. */
|
|
||||||
render() {
|
|
||||||
const checkMsg = this.whoInCheck() ? "Check! " : "";
|
|
||||||
const isCheckmate = this.checkmate();
|
|
||||||
const namedPlayer = isCheckmate ?
|
|
||||||
!this.state.blackIsNext : this.state.blackIsNext
|
|
||||||
const color = namedPlayer ? 'Black' : 'White';
|
|
||||||
|
|
||||||
const status = isCheckmate ? "Checkmate! " + color + " Wins!" :
|
|
||||||
checkMsg + color + "'s Turn";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{textAlign: `center`,}}>
|
|
||||||
<div className="status">
|
|
||||||
<h1 style={{display: "inline-block"}}>{status}</h1>
|
|
||||||
<img
|
|
||||||
className="icon"
|
|
||||||
alt="Settings icon: a gear"
|
|
||||||
style={{height: "5vh", float: "right"}}
|
|
||||||
src="./gear.svg" onClick={this.togglePopup.bind(this)}>
|
|
||||||
</img>
|
|
||||||
</div>
|
|
||||||
{range(8).map(n => this.row(n))}
|
|
||||||
{this.renderPopup()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Game extends React.Component {
|
class Game extends React.Component {
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
|
|
Loading…
Reference in New Issue