Make a simple backgammon game based on React

Today I am learning React and read the official documentation. There is an example of the Jiugongge game. I want to write a case of backgammon myself.

Let’s look at the effect first: (X and O represent black and white respectively)

The main function:

1. Click on the chessboard position to render the black and white chess pieces (render the same position once, without overwriting).

2. When 5 pieces of a kind of chess piece are connected together, the game ends and the victory of that kind of chess piece is prompted.

3. In the history record at the bottom, you can click the record to return to the recorded step.

According to the Jiugongge game example provided by the React official documentation, it can be seen that the main changes in backgammon include the size of the board and the determination of winning or losing.

The official documentation renders the chessboard via handwriting:

return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );

So much code needs to be written for a 9*9 chessboard. If you want to write a 10*10 chessboard, you don’t have to write all the code to write the chessboard, so we can reduce unnecessary code by rendering a chessboard in a loop (you can change N*N at will) chessboard), the code is as follows (the example is a 9*9 chessboard):

return (
    <>
      {/* Traverse the rendering board */}
      {Array(9).fill(null).map((_, row) => (
        <div className="board-row" key={row}>
          {Array(9).fill(null).map((_, col) => (
            <Square
              key={col}
              val={square[row * 9 + col]}
              onSquareClick={() => handleClick(row * 9 + col)}
            />
          ))}
        </div>
      ))}
    </>
  )

Judgment of winning and losing function:

According to the React documentation, the winning or losing function he wrote was also the result of handwriting:

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i + + ) {
    const [a, b, c] = lines[i];
    if (squares[a] & amp; & amp; squares[a] === squares[b] & amp; & amp; squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

It also encapsulates the result of this victory. In order to facilitate the application on different boards and games under different rules, a separate js file is encapsulated (the encapsulated code is a bit messy and needs to be optimized, but it does not affect the function (mainly because it is too difficult) , I don’t want to touch it anymore after writing it)):

export function Win(count, width) {
    // Unreasonable return empty
    if (count > width) return []
    // horizontal
    function transverse(count, width) {
        const allResult = []
        // Starting position of the first line
        let startIndexRow = []
        for (let sir = 0; sir < width - count + 1; sir + + ) {
            startIndexRow.push(sir)
        }
        // Fixed starting position for each line
        for (let st = 0; st < startIndexRow.length; st + + ) {
            //each column
            for (let i = 0; i < width; i + + ) {
                // Each successful result
                let result = []
                //Start position of each line
                let startIndex = i * width + startIndexRow[st]
                //[0,1,2],[1,2,3]
                for (let j = startIndex; j < (startIndex + count); j + + ) {
                    result.push(j)
                }
                allResult.push(result)
            }
        }
        return allResult
    }
    // portrait
    function direction(count, width) {
        const allResult = []
        // Starting position of the first column
        let startIndexCol = []
        for (let sic = 0; sic < width - count + 1; sic + + ) {
            startIndexCol.push(sic * width)
        }
        // Fixed starting position for each column
        for (let st = 0; st < startIndexCol.length; st + + ) {
            // each line
            for (let i = 0; i < width; i + + ) {
                // Each successful result
                let result = []
                //Start position of each column
                let startIndex = i + startIndexCol[st]
                // [0,width,2*width],[1,width + 1,[width*2 + 1]]
                for (let j = startIndex; j < (startIndex + count * width); j + = width) {
                    result.push(j)
                }
                allResult.push(result)
            }
        }
        return allResult
    }
    // slash (\)
    function slash(count, width) {
        const allResult = []
        // Starting position of the first line
        let startIndexRow = []
        for (let sir = 0; sir < width - count + 1; sir + + ) {
            startIndexRow.push(sir)
        }
        // Fixed starting position for each column
        for (let st = 0; st < startIndexRow.length; st + + ) {
            // each line
            for (let i = 0; i < width - count + 1; i + + ) {
                // Each successful result
                let result = []
                //Start position of each column
                let startIndex = i * width + startIndexRow[st]
                // [0,0 + width + 1,0 + 2*width + 2],[1,1 + width + 1,1 + 2*width + 2]
                for (let j = 0; j < count; j + + ) {
                    const res = startIndex + (width + 1) * j
                    result.push(res)
                }
                allResult.push(result)
            }
        }
        return allResult
    }
    // backslash (/)
    function Anticline(count, width) {
        const allResult = []
        // Starting position of the first line
        let startIndexRow = []
        for (let sir = count - 1; sir < width; sir + + ) {
            startIndexRow.push(sir)
        }
        // Fixed starting position for each column
        for (let st = 0; st < startIndexRow.length; st + + ) {
            // each line
            for (let i = 0; i < width - count + 1; i + + ) {
                // Each successful result
                let result = []
                //Start position of each column
                let startIndex = i * width + startIndexRow[st]
                // [2,2 + width-1,2 + 2*width-2]
                for (let j = 0; j < count; j + + ) {
                    const res = startIndex + (width - 1) * j
                    result.push(res)
                }
                allResult.push(result)
            }
        }
        return allResult
    }
    return [...transverse(count, width), ...direction(count, width), ...slash(count, width), ...Anticline(count, width)]
}

Mainly pass two parameters, the first is the number of wins, the second is the width of the chessboard (square chessboard)

The first parameter of the backgammon example is 5 (5 pieces determine victory)

For the overall directory structure, the encapsulated js is placed in Win.js in utils. If you introduce the call in the project, the following is a complete example (Win.js is the encapsulated method above and will not be copied again):

App.js:

import { useState } from 'react';
import { Win } from './utils/Win';
// Each step
const Square = ({ val, onSquareClick }) => {
  return <span className="square" onClick={onSquareClick}>{val}</span>
}

// Backgammon board
function Board({ isIndex, square, onPlay }) {
  function handleClick(i) {
    if (square[i] || calculateWinner(square)) {
      return
    }
    const nextSquares = square.slice();
    if (isIndex) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    onPlay(nextSquares);
  }
  const winner = calculateWinner(square)
  let status;
  if (winner) {
    status = winner + 'win'
  } else {
    status = 'Next step:' + (isIndex ? "X" : "O")
  }
  return (
    <>
      <div className='status'>{status}</div>
      {/* Traverse the rendering board */}
      {Array(9).fill(null).map((_, row) => (
        <div className="board-row" key={row}>
          {Array(9).fill(null).map((_, col) => (
            <Square
              key={col}
              val={square[row * 9 + col]}
              onSquareClick={() => handleClick(row * 9 + col)}
            />
          ))}
        </div>
      ))}
    </>
  )
  // Determine the outcome of the game
  function calculateWinner(squares) {
    const lines = Win(5, 9);
    for (let i = 0; i < lines.length; i + + ) {
      const [a, b, c, d, e] = lines[i];
      if (squares[a] & amp; & amp; squares[a] === squares[b] & amp; & amp; squares[a] === squares[c] & amp; & amp; squares[a] === squares[d] & amp; & amp; squares[a] === squares[e]) {
        return squares[a];
      }
    }
    return null;
  }
}

// Entire artboard
export default function Game() {
  const [isIndex, setIsIndex] = useState(true);
  const [history, setHistory] = useState([Array(81).fill(null)]); //Record the state position of the entire chessboard at each step
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares])
    setIsIndex(!isIndex)
  }

  function jumpTo(index) {
    setIsIndex(index % 2 === 0)
    setHistory([...history.slice(0, index + 1)])
  }

  const moves = history.map((item, index) => {
    let description;
    if (index > 0) {
      description = `Step ${index}`;
    } else {
      description = 'initial';
    }
    return (
      <li key={index}>
        <button onClick={() => jumpTo(index)}>{description}</button>
      </li>
    );
  });
  return (
    <div className="game">
      <div className="game-board">
        <Board isIndex={isIndex} square={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

index.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.square {
  display: inline-block;
  width: 30px;
  height: 30px;
  background-color: #efefef;
  border: 1px solid #ccc;
  cursor: pointer;
  overflow: hidden;
}

.status,
.board-row {
  /* width: 300px; */
  height: 30px;
  line-height: 30px;
  text-align: center;
}

.game-info {
  margin-top: 20px;
  text-align: center;
}