This React JS chess tutorial assumes working knowledge of JavaScript ES6 and React JS.
The application’s code is bootstrapped with create-react-app. The initial setup and stylesheet are taken from and built upon React’s official tic tac toe tutorial.
Full code for the game can be found here.
To play, head to demo.
Setup
To go along, create an application by following the steps at create-react-app. Remove any other file/directory except below:
|_ node_modules
|_ public
|_ src
|_ components
|_ helpers
|_ pieces
|_ index.css
|_ index.js
Change the contents of index.css
with:
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
background-color: #ECECEA;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: transparent;
border: 1px solid transparent;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 48px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 48px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
.icons-attribution, .game-status, .fallen-soldier-block{
margin-top: 20px;
min-height: 50px;
}
/*Board color scheme Wheat from http://omgchess.blogspot.com/2015/09/chess-board-color-schemes.html*/
.dark-square{
background-color: RGB(187,190,100);
}
.light-square{
background-color: RGB(234,240,206);
}
#player-turn-box{
width: 32px;
height: 32px;
border: 1px solid #000;
margin-bottom: 10px;
}
h3{
margin-bottom: 5px;
}
And that of index.js
with:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Game from './components/game.js'
ReactDOM.render(
<Game />,
document.getElementById('root')
);
So, without further ado, let’s dive into the code.
Piece
As React officially discourages inheritance of components and we need exactly that (plus polymorphism) for pieces, JavaScript class is the best data structure for Piece. In Piece we keep player (1 or 2) and icon image link (black pawn, white rook) information, both coming from specialized subclasses like Rook, Pawn etc.
export default class Piece {
constructor(player, iconUrl){
this.player = player;
this.style = {backgroundImage: "url('"+iconUrl+"')"};
}
}
(I’ve added another post to explain JavaScript inheritance and polymorphism in React application in summarized form)
Inherited Pieces
The inherited pieces are King, Queen, Bishop, Knight, Rook, and Pawn. They all call super()
with player and icon URL arguments.
All of them contain isMovePossible()
method which returns true if move is possible given the type of piece (e.g. diagonal movement for bishop) and the destination is either empty or enemy occupied; else returns false.
getSrcToDestPath()
which returns an array of all indexes that exist between given source and destination, excluding source and destination (for King, Knight and one-step-moving Pawn empty array is always returned).
Now, the code of all inherited pieces:
1. King
import Piece from './piece.js';
export default class King extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg"));
}
isMovePossible(src, dest){
return (src - 9 === dest ||
src - 8 === dest ||
src - 7 === dest ||
src + 1 === dest ||
src + 9 === dest ||
src + 8 === dest ||
src + 7 === dest ||
src - 1 === dest);
}
/**
* always returns empty array because of one step
* @return {[]}
*/
getSrcToDestPath(src, dest){
return [];
}
}
2. Queen
import Piece from './piece.js';
export default class Queen extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg"));
}
isMovePossible(src, dest){
let mod = src % 8;
let diff = 8 - mod;
return (Math.abs(src - dest) % 9 === 0 || Math.abs(src - dest) % 7 === 0) ||
(Math.abs(src - dest) % 8 === 0 || (dest >= (src - mod) && dest < (src + diff)));
}
/**
* get path between src and dest (src and dest exclusive)
* @param {num} src
* @param {num} dest
* @return {[array]}
*/
getSrcToDestPath(src, dest){
let path = [], pathStart, pathEnd, incrementBy;
if(src > dest){
pathStart = dest;
pathEnd = src;
}
else{
pathStart = src;
pathEnd = dest;
}
if(Math.abs(src - dest) % 8 === 0){
incrementBy = 8;
pathStart += 8;
}
else if(Math.abs(src - dest) % 9 === 0){
incrementBy = 9;
pathStart += 9;
}
else if(Math.abs(src - dest) % 7 === 0){
incrementBy = 7;
pathStart += 7;
}
else{
incrementBy = 1;
pathStart += 1;
}
for(let i = pathStart; i < pathEnd; i+=incrementBy){
path.push(i);
}
return path;
}
}
3. Bishop
import Piece from './piece.js';
export default class Bishop extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg"));
}
isMovePossible(src, dest){
return (Math.abs(src - dest) % 9 === 0 || Math.abs(src - dest) % 7 === 0);
}
/**
* get path between src and dest (src and dest exclusive)
* @param {num} src
* @param {num} dest
* @return {[array]}
*/
getSrcToDestPath(src, dest){
let path = [], pathStart, pathEnd, incrementBy;
if(src > dest){
pathStart = dest;
pathEnd = src;
}
else{
pathStart = src;
pathEnd = dest;
}
if(Math.abs(src - dest) % 9 === 0){
incrementBy = 9;
pathStart += 9;
}
else{
incrementBy = 7;
pathStart += 7;
}
for(let i = pathStart; i < pathEnd; i+=incrementBy){
path.push(i);
}
return path;
}
}
4. Knight
import Piece from './piece.js';
export default class Knight extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg"));
}
isMovePossible(src, dest){
return (src - 17 === dest ||
src - 10 === dest ||
src + 6 === dest ||
src + 15 === dest ||
src - 15 === dest ||
src - 6 === dest ||
src + 10 === dest ||
src + 17 === dest);
}
/**
* always returns empty array because of jumping
* @return {[]}
*/
getSrcToDestPath(){
return [];
}
}
5. Rook
import Piece from './piece.js';
export default class Rook extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg"));
}
isMovePossible(src, dest){
let mod = src % 8;
let diff = 8 - mod;
return (Math.abs(src - dest) % 8 === 0 || (dest >= (src - mod) && dest < (src + diff)));
}
/**
* get path between src and dest (src and dest exclusive)
* @param {num} src
* @param {num} dest
* @return {[array]}
*/
getSrcToDestPath(src, dest){
let path = [], pathStart, pathEnd, incrementBy;
if(src > dest){
pathStart = dest;
pathEnd = src;
}
else{
pathStart = src;
pathEnd = dest;
}
if(Math.abs(src - dest) % 8 === 0){
incrementBy = 8;
pathStart += 8;
}
else{
incrementBy = 1;
pathStart += 1;
}
for(let i = pathStart; i < pathEnd; i+=incrementBy){
path.push(i);
}
return path;
}
}
6. Pawn
initialPositions
is additional information we keep to know if it’s pawn first move, in which case we allow two steps
import Piece from './piece.js';
export default class Pawn extends Piece {
constructor(player){
super(player, (player === 1? "https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg" : "https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg"));
this.initialPositions = {
1: [48, 49, 50, 51, 52, 53, 54, 55],
2: [8, 9, 10, 11, 12, 13, 14, 15]
}
}
isMovePossible(src, dest, isDestEnemyOccupied){
if(this.player === 1){
if((dest === src - 8 && !isDestEnemyOccupied) || (dest === src - 16 && this.initialPositions[1].indexOf(src) !== -1)){
return true;
}
else if(isDestEnemyOccupied && (dest === src - 9 || dest === src - 7)){
return true;
}
}
else if(this.player === 2){
if((dest === src + 8 && !isDestEnemyOccupied) || (dest === src + 16 && this.initialPositions[2].indexOf(src) !== -1)){
return true;
}
else if(isDestEnemyOccupied && (dest === src + 9 || dest === src + 7)){
return true;
}
}
return false;
}
/**
* returns array of one if pawn moves two steps, else returns empty array
* @param {[type]} src [description]
* @param {[type]} dest [description]
* @return {[type]} [description]
*/
getSrcToDestPath(src, dest){
if(dest === src - 16){
return [src - 8];
}
else if(dest === src + 16){
return [src + 8];
}
return [];
}
}
Helper Method ‘initialiseChessBoard()’
Now we have pieces available, we use a helper function that returns an array of 64 (called squares), 32 of which are null and 32 filled with pieces.
New pawns, rooks, bishops, knights, king, queen are created for both players and placed in their right places in the array according to the starting position of the game.
import Bishop from '../pieces/bishop.js';
import King from '../pieces/king.js';
import Knight from '../pieces/knight.js';
import Pawn from '../pieces/pawn.js';
import Queen from '../pieces/queen.js';
import Rook from '../pieces/rook.js';
export default function initialiseChessBoard(){
const squares = Array(64).fill(null);
for(let i = 8; i < 16; i++){
squares[i] = new Pawn(2);
squares[i+40] = new Pawn(1);
}
squares[0] = new Rook(2);
squares[7] = new Rook(2);
squares[56] = new Rook(1);
squares[63] = new Rook(1);
squares[1] = new Knight(2);
squares[6] = new Knight(2);
squares[57] = new Knight(1);
squares[62] = new Knight(1);
squares[2] = new Bishop(2);
squares[5] = new Bishop(2);
squares[58] = new Bishop(1);
squares[61] = new Bishop(1);
squares[3] = new Queen(2);
squares[4] = new King(2);
squares[59] = new Queen(1);
squares[60] = new King(1);
return squares;
}
Components
Having all the basics in place, let’s move to components. There are four, all listed below.
1. Square
First we add stateless component Square. It renders one square in its color and piece (if exists), the complete information of which is received through props in the form of style object (contains image of piece), onClick, and shade of the square (dark or light).
import React from 'react';
import '../index.css';
export default function Square(props) {
return (
<button className={"square " + props.shade}
onClick={props.onClick}
style={props.style}>
</button>
);
}
2. Fallen Soldiers Block
Next we add a stateful component FallenSoldierBlock. It receives two arrays in props: whiteFallenSoldiers and blackFallenSoldiers, which it loops through to render squares in two rows. We don’t pass shade as we need transparent background.
import React from 'react';
import '../index.css';
import Square from './square.js';
export default class FallenSoldierBlock extends React.Component {
renderSquare(square, i, squareShade) {
return <Square
piece = {square}
style = {square.style}
/>
}
render() {
return (
<div>
<div className="board-row">{this.props.whiteFallenSoldiers.map((ws, index) =>
this.renderSquare(ws, index)
)}</div>
<div className="board-row">{this.props.blackFallenSoldiers.map((bs, index) =>
this.renderSquare(bs, index)
)}</div>
</div>
);
}
}
3. Board
Now we add Board component. It’s only task is to render the grid of 64 squares it gets from props, deciding for each square if its dark or light, and sending the relevant props to Square, namely, style, shade and onClick.
import React from 'react';
import '../index.css';
import Square from './square.js';
export default class Board extends React.Component {
renderSquare(i, squareShade) {
return <Square
piece = {this.props.squares[i]}
style = {this.props.squares[i]? this.props.squares[i].style : null}
shade = {squareShade}
onClick={() => this.props.onClick(i)}
/>
}
render() {
const board = [];
for(let i = 0; i < 8; i++){
const squareRows = [];
for(let j = 0; j < 8; j++){
const squareShade = (isEven(i) && isEven(j)) || (!isEven(i) && !isEven(j))? "light-square" : "dark-square";
squareRows.push(this.renderSquare((i*8) + j, squareShade));
}
board.push(<div className="board-row">{squareRows}</div>)
}
return (
<div>
{board}
</div>
);
}
}
function isEven(num){
return num % 2 == 0
}
4. Game
And finally the main component of the application, which encloses all other components: Game.
All the logic of game is kept in Game component, which renders Board, Fallen Soldiers, instructions/warnings etc.
import React from 'react';
import '../index.css';
import Board from './board.js';
import FallenSoldierBlock from './fallen-soldier-block.js';
import initialiseChessBoard from '../helpers/board-initialiser.js';
export default class Game extends React.Component {
constructor(){
super();
this.state = {
squares: initialiseChessBoard(),
whiteFallenSoldiers: [],
blackFallenSoldiers: [],
player: 1,
sourceSelection: -1,
status: '',
turn: 'white'
}
}
handleClick(i){
const squares = this.state.squares.slice();
if(this.state.sourceSelection === -1){
if(!squares[i] || squares[i].player !== this.state.player){
this.setState({status: "Wrong selection. Choose player " + this.state.player + " pieces."});
squares[i]? delete squares[i].style.backgroundColor: null;
}
else{
squares[i].style = {...squares[i].style, backgroundColor: "RGB(111,143,114)"}; // Emerald from http://omgchess.blogspot.com/2015/09/chess-board-color-schemes.html
this.setState({
status: "Choose destination for the selected piece",
sourceSelection: i
});
}
}
else if(this.state.sourceSelection > -1){
delete squares[this.state.sourceSelection].style.backgroundColor;
if(squares[i] && squares[i].player === this.state.player){
this.setState({
status: "Wrong selection. Choose valid source and destination again.",
sourceSelection: -1,
});
}
else{
const squares = this.state.squares.slice();
const whiteFallenSoldiers = this.state.whiteFallenSoldiers.slice();
const blackFallenSoldiers = this.state.blackFallenSoldiers.slice();
const isDestEnemyOccupied = squares[i]? true : false;
const isMovePossible = squares[this.state.sourceSelection].isMovePossible(this.state.sourceSelection, i, isDestEnemyOccupied);
const srcToDestPath = squares[this.state.sourceSelection].getSrcToDestPath(this.state.sourceSelection, i);
const isMoveLegal = this.isMoveLegal(srcToDestPath);
if(isMovePossible && isMoveLegal){
if(squares[i] !== null){
if(squares[i].player === 1){
whiteFallenSoldiers.push(squares[i]);
}
else{
blackFallenSoldiers.push(squares[i]);
}
}
console.log("whiteFallenSoldiers", whiteFallenSoldiers) ;
console.log("blackFallenSoldiers", blackFallenSoldiers);
squares[i] = squares[this.state.sourceSelection];
squares[this.state.sourceSelection] = null;
let player = this.state.player === 1? 2: 1;
let turn = this.state.turn === 'white'? 'black' : 'white';
this.setState({
sourceSelection: -1,
squares: squares,
whiteFallenSoldiers: whiteFallenSoldiers,
blackFallenSoldiers: blackFallenSoldiers,
player: player,
status: '',
turn: turn
});
}
else{
this.setState({
status: "Wrong selection. Choose valid source and destination again.",
sourceSelection: -1,
});
}
}
}
}
/**
* Check all path indices are null. For one steps move of pawn/others or jumping moves of knight array is empty, so move is legal.
* @param {[type]} srcToDestPath [array of board indices comprising path between src and dest ]
* @return {Boolean}
*/
isMoveLegal(srcToDestPath){
let isLegal = true;
for(let i = 0; i < srcToDestPath.length; i++){
if(this.state.squares[srcToDestPath[i]] !== null){
isLegal = false;
}
}
return isLegal;
}
render() {
return (
<div>
<div className="game">
<div className="game-board">
<Board
squares = {this.state.squares}
onClick = {(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<h3>Turn</h3>
<div id="player-turn-box" style={{backgroundColor: this.state.turn}}>
</div>
<div className="game-status">{this.state.status}</div>
<div className="fallen-soldier-block">
{<FallenSoldierBlock
whiteFallenSoldiers = {this.state.whiteFallenSoldiers}
blackFallenSoldiers = {this.state.blackFallenSoldiers}
/>
}
</div>
</div>
</div>
<div className="icons-attribution">
<div> <small> Chess Icons And Favicon (extracted) By en:User:Cburnett [<a href="http://www.gnu.org/copyleft/fdl.html">GFDL</a>, <a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA-3.0</a>, <a href="http://opensource.org/licenses/bsd-license.php">BSD</a> or <a href="http://www.gnu.org/licenses/gpl.html">GPL</a>], <a href="https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces">via Wikimedia Commons</a> </small></div>
</div>
</div>
);
}
}
State and handleClick logic further explained:
State
The state of the game includes:
-
squares - initialized with the help of
initialiseChessBoard()
helper function; 32 null, 32 pieces (16 of each player) -
whiteFallenSoldiers - empty array. Filled as white pieces fall
-
blackFallenSoldiers - same as whiteFallenSoldiers but for black pieces
-
player - 1 or 2. Tells which player’s turn it is
-
sourceSelection - -1 when nothing selected. Remains -1 till legal source square is clicked by the player, upon which sourceSelection stores the clicked index
-
status - to show error warning or instruction
-
turn - ‘white’ or ‘black’. The purpose of this additional string is to use it conveniently to tell which color turn is this (shown on top right)
handleClick Logic
-
slice the
squares
so as not to mutate it. -
check if its player first click or second (
sourceSelection
is -1 or 0 - 63). if -1 and click is valid (not clicked on opponent’s piece of empty square), assign sourceSelection the clicked index. Else show error message -
if
sourceSelection
> -1 (0 - 63) it means destination is clicked. Check if the destination is valid with the help ofisMoveLegal
andisMovePossible
; show error/instruction message accordingly.isMoveLegal
is Game’s method that tells the move from source to destination is possible or not based on the array of indexes returned by Piece’s methodisMovePossible
that tells if the piece is allowed to move this way (like Pawn movement of player 2)
Directory Structure
On complete development, the directory structure would appear as:
|_ node_modules
|_ public
|_ src
|_ components
|_ board.js
|_ fallen-soldiers_block.js
|_ game.js
|_ square.js
|_ helpers
|_ board-initialiser.js
|_ pieces
|_ bishop.js
|_ king.js
|_ knight.js
|_ pawn.js
|_ piece.js
|_ queen.js
|_ rook.js
|_ index.css
|_ index.js
And that’s it! The Game is ready except for few little things like checkmate, which can be (not so) easily implemented building on the current logic of pieces movement.
This game can also be made real time using ActionCable (Ruby on Rails), websocket (Node js) or Firebase.
Once again, the complete code is available here. And demo here.