DEV Community

FredLitt
FredLitt

Posted on

My Experience Building A Chess App In React

My Experience Building A Chess App In React

Hi, my name is Fred and I’m a chess player who has been learning to code using The Odin Project for the past 10 months. After gaining some familiarity with React, I thought it would be a fun challenge to try and build a chess application using React. I'm also interested in finding my first job as an entry level developer and would love to chat with anyone who is hiring or has suggestions on getting into the field.

What the App Does

1. Supports All Basic Rules of Chess

  1. Pieces are capable of performing all legal moves and possible moves are indicated with a circular highlight on the possible move square. The last played move’s squares are highlighted as well.

Image description

b. Castling is supported in either direction, and cannot be done if either the king or corresponding rook has moved, or if the king is in check or would move through check.

Image description

c. En passant, which proved to be one of the most challenging aspects of the game to program due to the amount of conditionals that must be met.

Per the Wiki link:

  • the capturing pawn must be on its fifth rank;
  • the captured pawn must be on an adjacent file and must have just moved two squares in a single move (i.e. a double-step move);
  • the capture can only be made on the move immediately after the enemy pawn makes the double-step move; otherwise, the right to capture it en passant is lost.

Image description

d. Checkmate: When the attacked king’s army has no means of salvaging their leader.

Image description

2. App Features

a. Move notation and captured piece tracker
Image description
b. Pawn Promotion
Image description
c. End of Game Detection. The current game recognizes checkmate and stalemate and creates a new game popup accordingly.
Image description
d. Changing board themes: LOOK at those pretty colors
Image description
e. Takeback button
Image description

How The App Is Built

1. The Game Logic

a. The Board Class
The board is represented in a 2d array of “square” objects, each with a unique coordinate and the presence or non-presence of a piece (which are themselves objects).

export class Board {
  constructor() {
    this.squares = []
    for (let row = 0; row < 8; row++) {
      const boardRow = []
      for (let col = 0; col < 8; col ++){
        const square = {
          piece: null,
          coordinate: [row, col]
          }
        boardRow.push(square)
        }
      this.squares.push(boardRow)
    }
Enter fullscreen mode Exit fullscreen mode

The board has a large variety of methods to manipulate itself and to gather information about the current board position...

getPossibleMoves(pieceToMove, fromSquare){
    const searchOptions = {
      board: this,
      fromSquare: fromSquare,
      squaresToFind: "possible moves"
    }
    this.selectedPiece.possibleMoves = pieceToMove.findSquares
    (searchOptions)
    this.markPossibleMoveSquares()
  }

updateBoard(startSquare, endSquare){
    startSquare.piece = null
    endSquare.piece = this.selectedPiece.piece
  }
Enter fullscreen mode Exit fullscreen mode

b. The Piece Classes
Each type of piece has its own class that is capable of

  • Finding the squares that it currently controls
  • Finding all the squares that it could possibly move to

It wasn’t until I started writing the logic for determining king moves that I realized just how distinct these two things were. For example:

Image description
Black could not move the knight to the X square as it would expose the black king, but the square is still a controlled square as the white king could not move there either

Therefore, each piece has a unique method for each case. In either case an array of coordinates is returned.

findSquares({board, fromSquare, squaresToFind}) {
    const [fromRow, fromCol] = fromSquare
    const knightMoves = {
      "NorthOneEastTwo": [fromRow - 1, fromCol + 2],
      "NorthTwoEastOne": [fromRow - 2, fromCol + 1],
      "SouthOneEastTwo": [fromRow + 1, fromCol + 2],
      "SouthTwoEastOne": [fromRow + 2, fromCol + 1],
      "NorthOneWestTwo": [fromRow - 1, fromCol - 2],
      "NorthTwoWestOne": [fromRow - 2, fromCol - 1],
      "SouthOneWestTwo": [fromRow + 1, fromCol - 2],
      "SouthTwoWestOne": [fromRow + 2, fromCol - 1]
    }
    if (squaresToFind === "controlled squares") {
      return this.findControlledSquares(board, fromSquare, knightMoves)
    }
    if (squaresToFind === "possible moves") {
      return this.findPossibleMoves(board, fromSquare, knightMoves)
    }
  }...
Enter fullscreen mode Exit fullscreen mode

A Shared Search Method for Long Range Pieces:
I discovered that the Queen, Rook and Bishop had similar patterns for finding possible and controlled squares. All of them are capable of moving as many squares as possible in a given direction until:

  • An enemy piece is reached (at which point a capture is possible)
  • The square before a friendly piece is reached
  • The edge of the board is reached

Each of these pieces iterate from their given starting coordinate in each of their possible directions, and continue iterating until one of these conditions is met. This enabled me write a generalized method that could be used by each of these pieces.

const findSquaresForLongRange = 
  ({piece, board, fromSquare, squaresToFind, pieceDirections}) => {
  const possibleSquares = []
  const [fromRow, fromCol] = fromSquare
  const completedDirections = []

    for (let i = 1; i < 8; i++) {
      const allDirections = {
        "North": [fromRow - i, fromCol],
        "South": [fromRow + i, fromCol],
        "East": [fromRow, fromCol + i],
        "West": [fromRow, fromCol - i],
        "NorthWest": [fromRow - i, fromCol - i],
        "NorthEast": [fromRow - i, fromCol + i],
        "SouthWest": [fromRow + i, fromCol - i],
        "SouthEast": [fromRow + i, fromCol + i]
      }
Enter fullscreen mode Exit fullscreen mode

Each piece simply needs to pass in the directions that they are capable of...

class Bishop {
  constructor(color) {
    this.type = "bishop"
    this.color = color
    if (color === "white") {
      this.symbol = pieceSymbols.whiteBishop
    } else if (color === "black") {
      this.symbol = pieceSymbols.blackBishop
    }
  }
  findSquares({board, fromSquare, squaresToFind}) {
    return findSquaresForLongRange({
      piece: this,
      pieceDirections: ["NorthWest", "NorthEast", "SouthWest", "SouthEast"],
      board,
      fromSquare,
      squaresToFind
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

directions that are not included will be skipped over immediately

for (const direction in allDirections) {

        if (!pieceDirections.includes(direction) || completedDirections.includes(direction)){
          continue;
        }
Enter fullscreen mode Exit fullscreen mode

c. End of Game Detection
Currently, the game is capable of detecting checkmate and stalemate.

The game detects an end of game by running a function that determines all of a player’s possible moves. The check detection method returns a boolean of whether a king’s square is contained in the opposing player’s attacked squares.

  • If player has possible moves → gameOver ≠ true
  • If player has no possible moves & is in check → “other player wins”
  • If player has no possible moves but is not in check → “stalemate”

2. The UI

The App function contains the following components, all of which rely on the data from the Board Object to determine what to render.

  • A conditionally appearing modal to start a new game (appears when game is over)
  • A BoardUI component which displays the chessboard, contains a pop up for pawn promotions and contains the game’s option buttons
  • A CapturedPieceContainer component for white pieces and for black pieces
  • A MoveList component that renders chess notation of the current game

The chessboard is contained by a BoardUI component, which uses the data from the Board classes 2d array of squares to render the current position.

<table 
        id="board"
        cellSpacing="0">
        <tbody>
        {gameDisplay.boardPosition.map((row, index) =>
          <tr 
            className="board-row"
            key={index}>
            {row.map((square) => 
              <td 
                className={getSquaresClass(square)}
                coordinate={square.coordinate}
                piece={square.piece}
                key={square.coordinate} 
                style={{
                  backgroundColor: isLightSquare(square.coordinate) ? lightSquareColor : darkSquareColor,
                  opacity: square.isLastPlayedMove ? 0.6 : 1.0
                  }}
                onClick={(e) => move(e)}>
                  {square.piece !== null && square.piece.symbol}   
                  {square.isPossibleMove && 
                    <span className="possible-move"></span>}       </td>)}
            </tr>)}
        </tbody>
      </table>
Enter fullscreen mode Exit fullscreen mode

The board is displayed using an HTML table. Squares containing a piece display the piece’s symbol and when a piece to move is selected, its possible move squares are given a colored element to highlight them.

A Possible Improvement...

An issue I ran into in my code dealt with the nature of how React knows when to update the interface. Although the Board object is very good at mutating itself, React won’t know to update because the object that is being referenced is the same. This forced me to create a method on Board that returns a copy of itself...

clone(){
    let newBoard = new Board()
    for (const property in this){
      newBoard[property] = this[property]
    }
    return newBoard
  }
Enter fullscreen mode Exit fullscreen mode

which could then be passed in for state changes...

setBoard(board.clone())
Enter fullscreen mode Exit fullscreen mode

However, this extra step does not really take full advantage of React. Taking a more functional approach to writing the methods in the Board class could remove the need for this. If I end up doing a large scale refactor of this project, I believe this would be a great opportunity for improvement and chance to make the best use of React’s capabilities.

A nested conditional component within BoardUI...

The BoardUI component also contains a conditionally rendered PromotionModal component, which relies on the BoardUI’s state to render the appropriately colored pieces as a pop-up

const [pawnPromotion, setPawnPromotion] = 
    useState({
      pawnIsPromoting: false,
      color: null,
      promotionSquare: null})
Enter fullscreen mode Exit fullscreen mode

Image description
Positioning this just the way I wanted took some effort, and I finally landed on making use of CSS calc() function and CSS variables to achieve my desired effect.

.promotion-pieces {
  ...
  position: fixed;
  top: 50%;
  left: calc(0.5 * (100vw - var(--board-length) - var(--move-list-width)) + 0.5 * var(--board-length));
  transform: translate(-50%, -50%);
  ...
}
Enter fullscreen mode Exit fullscreen mode

3. Game Options

a. New Game: Sets game to initial game settings, then sets the App’s state to a copy of that board

const createNewGame = () => {
    board.startNewGame()
    setBoard(board.clone())
  }
Enter fullscreen mode Exit fullscreen mode

b. Flip Board: Checks player currently at bottom of screen and rearranges the game’s squares in reverse order:

const flipBoard = () => {
    const updatedPosition = {}
    const boardToFlip = board.squares
    const flippedBoard = []

    if (gameDisplay.playerPerspective === "black"){
      for (let row = 7; row >= 0; row--){
        const boardRow = []
        for (let col = 7; col >= 0; col --){
          boardRow.push(boardToFlip[row][col])
        }
        flippedBoard.push(boardRow)
      }
      updatedPosition.playerPerspective = "white"
      updatedPosition.boardPosition = flippedBoard
      setGameDisplay(updatedPosition)
      return
    }

    if(gameDisplay.playerPerspective === "white"){
      for (let row = 0; row <= 7; row++){
        const boardRow = []
        for (let col = 0; col <= 7; col++){
          boardRow.push(boardToFlip[row][col])
        }
        flippedBoard.push(boardRow)
      }
      updatedPosition.playerPerspective = "black"
      updatedPosition.boardPosition = flippedBoard
      setGameDisplay(updatedPosition)
      return
    }
  }
Enter fullscreen mode Exit fullscreen mode

c. Takeback:

const takeback = () => {
// Create list of moves equal to the current game minus the last
    const movesToPlayBack = board.playedMoveList.slice(0, -1)

// Reset game
    createNewGame()

// Plays through the list of moves
    for (let i = 0; i < movesToPlayBack.length; i++){
      board.selectPieceToMove(movesToPlayBack[i].fromSquare)
      const targetSquare = movesToPlayBack[i].toSquare
      if (movesToPlayBack[i].moveData.promotionChoice){
        const pieceType = movesToPlayBack[i].moveData.promotionChoice
        const pieceColor = movesToPlayBack[i].piece.color
        const promotionChoice = findPiece(pieceColor, pieceType)
        return board.movePiece(targetSquare, promotionChoice)
      }
      board.movePiece(targetSquare)
    }
  }
Enter fullscreen mode Exit fullscreen mode

d. Board Theme: Sets CSS variables for colors to various color schemes

  const changeTheme = (lightSquareChoice, darkSquareChoice, highlightChoice) => {
    document.documentElement.style.setProperty("--light-square", lightSquareChoice)
    document.documentElement.style.setProperty("--dark-square", darkSquareChoice)
    document.documentElement.style.setProperty("--highlight", highlightChoice)
  }
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

This was by far my favorite coding project that I’ve worked on so far. The combination of my own personal love of chess and the challenge of accounting for all the complexity and nuances of the game was difficult but equally rewarding. Some things I’d consider adding at this point are:

  • 2-player network chess
  • End of game detection for fifty-move rule and threefold repetition
  • Different chess set options
  • Forward and back button on move list to look through a game
  • Draggable rather than clickable moves
  • Update codebase to TypeScript
  • Refactor in more of a functional rather than object oriented style

If I were to go back in time in my coding journey I think I would have attempted to start this project sooner than I did. Learning from the mistakes that I made during this project has helped me grow tremendously and I’m excited to continue building and see what I pick up along the way. Feel free to e-mail me if you're someone looking to hire a new dev!

Discussion (5)

Collapse
mattstone profile image
Matt Stone

Well done! Always cool to see unique projects.

Collapse
gabrielctroia profile image
Gabriel C. Troia

Hey @fredlitt! Awesome project, and I'm sure you learned a lot in the process. If you are interested in writing chess related code, shoot me a message. I'm the founder of chessroulette.live!

Cheers

Collapse
fredlitt profile image
FredLitt Author

Thank you Gabriel, Chessroulette looks awesome! I'll drop you a line.

Collapse
nguyenit67 profile image
Nguyen

So astounding project and impressive article. You really put so much dedication into this. Could you tell me how long it takes for you to write the chess app?

Collapse
fredlitt profile image
FredLitt Author

Thank you! I believe it took me close to 3 months to complete without around 1-2 hours a day spent on it