Click here to Skip to main content
15,881,882 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I've been teaching myself React.js by following the tic-tac-toe tutorial on reactjs.org,

https://reactjs.org/tutorial/tutorial.html.

I then added a minimax algorithm to make the game more interactive; however, the addition of an AI opponent also introduced a bug into the Time Travel feature of the original tutorial. The game is set up so that the user is always Player X and the AI is always Player O. When the player marks a square 'X' the AI immediately marks its square 'O' and the user always gets the first move.

Using the Time Travel feature, I can roll the game back to any turn, including turns for Player O. I can then mark a square as Player O and the AI then continues to play the game as Player X. This "switcheroo" is a bug I'd like to fix.

Below is the code for the game:

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css'

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  makeMove(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    
    const nextState = {
      history: history.concat([
        {
          squares: squares
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    };
    //return a Promise that resolves when setState completes
    return new Promise((resolve, reject) => {
      this.setState(nextState, resolve);
    });
  }

  async handleClick(i) {
    //apply player's move to square i
    await this.makeMove(i);
    //Apply bot's move after the user moves
    const squares = this.state.history[this.state.stepNumber].squares.slice();
    const bestSquare = findBestSquare(squares, this.state.xIsNext ? "X" : "O");
    
    if(bestSquare !== -1) {
      await this.makeMove(bestSquare);
    }
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } 
    else if (isBoardFilled(current.squares)) {
      status = "It's a tie!";
    }
    else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Game />);

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] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

function isBoardFilled(squares) {
  //this function is called to determine if there is a tie
  for (let i = 0; i < squares.length; i++) {
    if (squares[i] === null) {
      return false;
    }
  }
  return true;
}

function findBestSquare(squares, player) {
  const opponent = player === 'X' ? 'O' : 'X';
  
  const minimax = (squares, isMax) => {
    const winner = calculateWinner(squares);
    //if player wins, score is +1
    if (winner === player) return { square: -1, score: 1 };
    //if opponent wins, score is -1
    if (winner === opponent) return { square: -1, score: -1 };
    //if tie, score is 0
    if (isBoardFilled(squares)) return { square: -1, score: 0 };
    
    const best = { square: -1, score: isMax ? -1000 : 1000 };

    //loop through every square on the board
    for (let i = 0; i < squares.length; i++) {
      //if square is already filled, skip it
      if (squares[i]) {
        continue;
      }
      //if square is unfilled, then play the square
      squares[i] = isMax ? player : opponent;
      //recursively call minimax until the end of the game and get the score
      const score = minimax(squares, !isMax).score;
      //undo the move
      squares[i] = null;

      if (isMax) {
        if (score > best.score) {
          best.score = score;
          best.square = i;
        }
      }
      else {
        if (score < best.score) {
          best.score = score;
          best.square = i;
        }
      }
    }

    return best; //move that leads to the best score at the end of the game
  };

  return minimax(squares, true).square; //best move for the player at the current turn and the current board
}


What I have tried:

The easiest way I can think of to fix this issue is to only allow the user to Time Travel to previous turns for Player X, but I'm not sure how to go about doing this while also rolling back turns for Player O, as well.
Posted
Updated 14-Mar-23 19:53pm
Comments
Richard MacCutchan 15-Mar-23 5:22am    
You should ask the question at the tutorial website, so the author can advise you.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900