From 4b38e44a3af034c8eb0324ccec4326bfb79b700f Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Sat, 2 Jan 2021 10:15:03 -0500 Subject: [PATCH] Move Board/Piece to own file. Start unit testing --- src/board.js | 706 +++++++++++++++++++++++++++++++++++++++++++++ src/board.test.js | 51 ++++ src/index.js | 707 +--------------------------------------------- 3 files changed, 758 insertions(+), 706 deletions(-) create mode 100644 src/board.js create mode 100644 src/board.test.js diff --git a/src/board.js b/src/board.js new file mode 100644 index 0000000..bf8d9dc --- /dev/null +++ b/src/board.js @@ -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 ( + + ); +} + +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 ( + 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 ( +
+ +
+ ); + }) + } + + + + } + /> + : 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}

+ Settings icon: a gear + +
+ {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 ( -
- -
- ); - }) - } - - - - } - /> - : 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}

- Settings icon: a gear - -
- {range(8).map(n => this.row(n))} - {this.renderPopup()} -
- ); - } -} - class Game extends React.Component { constructor(props){ super(props);