+
+ This is a simple implementation of the classic board game,
+ implemented in React. It supports all possible moves, including
+ castling, and en passant .
+
+ {
+ getAllSettings().map(setting => {
+ return (
+
+
+ {
+ settingText(setting) +
+ (this.getSetting(setting) ? " On" : " Off")
+ }
+
+
+ );
+ })
+ }
+
+ Reset Game
+
+ }
+ />
+ : 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 (
+
+
+
{status}
+
+
+
+ {range(8).map(n => this.row(n))}
+ {this.renderPopup()}
+
+ );
+ }
+}
+
+export default Board;
+export { BLACK, WHITE, PAWN, ROOK, KNIGHT, BISHOP, QUEEN, KING, EMPTY };
diff --git a/src/board.test.js b/src/board.test.js
new file mode 100644
index 0000000..cbd7c84
--- /dev/null
+++ b/src/board.test.js
@@ -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);
+});
diff --git a/src/index.js b/src/index.js
index e25783c..88adff6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,716 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import Popup from './components/Popup';
+import Board from './board';
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 (
-
-
- );
-}
-
-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 (
- this.handleClick(i)}
- bgColor={bgColor}
- />
- );
- }
-
- row(r) {
- const i = r * 8;
- return (
-
- {range(8).map(n => this.renderSquare(n + i))}
-
- );
- }
-
- 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 ?
-
-
- This is a simple implementation of the classic board game,
- implemented in React. It supports all possible moves, including
- castling, and en passant .
-
- {
- getAllSettings().map(setting => {
- return (
-
-
- {
- settingText(setting) +
- (this.getSetting(setting) ? " On" : " Off")
- }
-
-
- );
- })
- }
-
- Reset Game
-
- }
- />
- : 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 (
-
-
-
{status}
-
-
-
- {range(8).map(n => this.row(n))}
- {this.renderPopup()}
-
- );
- }
-}
-
class Game extends React.Component {
constructor(props){
super(props);