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 };