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: { heldPiece: 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; } }; const 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(); const moveIndex = board.getIndex(move.x, move.y); board.state.squares[moveIndex] = board.state.squares[i]; 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) { const i = this.getIndex(move.passant.x, move.passant.y); squares[i] = 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 " + move.castle + " with " + [rookX, move.castle[1]]); const rookStart = this.getIndex(rookX, move.castle[1]); const rookLanding = this.getIndex(move.castle[0], move.castle[1]); squares[rookLanding] = squares[rookStart]; squares[rookStart] = 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 (both explained below).

At the moment there is no support for online play, or even a simple AI opponent, but both options are currently being examined.

Castling

Castling is a special move that allows a king and a rook to move at the same time, under certain circumstances:

If these conditions are met, the king can move 2 spaces toward the rook, and the rook moves into the space the king skipped over.

En Passant

Most people know that a pawn can jump two spaces the first time it moves. En passant (French for "in passing") is a rare move that can counter such a double-jump.

For example, say a white pawn double-jumps, and lands side-by-side with a black pawn. That black pawn can then move into the space that was jumped over (right behind the white pawn), and capture its enemy!

It's not useful every game, but is very powerful in the right situation!

Settings

{ getAllSettings().map(setting => { return (
); }) }

Assets have been borrowed from Wikipedia.

} /> : 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 {Piece}; export { BLACK, WHITE, PAWN, ROOK, KNIGHT, BISHOP, QUEEN, KING, EMPTY };