| 
 | 1 | +import React from 'react';  | 
 | 2 | +import ReactDOM from 'react-dom';  | 
 | 3 | +import './index.css';  | 
 | 4 | + | 
 | 5 | + | 
 | 6 | +// Initialization of variables.  | 
 | 7 | +let human, computer;  | 
 | 8 | + | 
 | 9 | +// Dummy components. Only returns JSX to render when called by Board.  | 
 | 10 | +function Square(props) {  | 
 | 11 | +  return (  | 
 | 12 | +    <button className="square" onClick={props.onClick} value={props.number}>  | 
 | 13 | +      {props.value}  | 
 | 14 | +    </button>  | 
 | 15 | +  );  | 
 | 16 | +}  | 
 | 17 | + | 
 | 18 | +function Button(props) {  | 
 | 19 | +  return (  | 
 | 20 | +    <button className="reset" onClick={props.onClick}>  | 
 | 21 | +      Reset?  | 
 | 22 | +    </button>  | 
 | 23 | +  );  | 
 | 24 | +}  | 
 | 25 | + | 
 | 26 | + | 
 | 27 | +// The actual board. Where all of the rendering of the actual game is, with the algorithim.  | 
 | 28 | + | 
 | 29 | +class Board extends React.Component {  | 
 | 30 | +  constructor(props) {  | 
 | 31 | +    super(props);  | 
 | 32 | +    this.state = {  | 
 | 33 | +      squares: Array(9).fill(null), // Board definition.  | 
 | 34 | +      xIsNext: props.isX, // Recieving from Game based on user input. Returns true or false  | 
 | 35 | +    }  | 
 | 36 | +  }  | 
 | 37 | + | 
 | 38 | + | 
 | 39 | +// Calls the square and adds clicking functions to it.  | 
 | 40 | +  renderSquare(i) {  | 
 | 41 | +    return (  | 
 | 42 | +      <Square  | 
 | 43 | +        value={this.state.squares[i]}  | 
 | 44 | +        number={i} // testing purposes  | 
 | 45 | +        onClick={() => this.handleClick(i)}  | 
 | 46 | +      />  | 
 | 47 | +    );  | 
 | 48 | +  }  | 
 | 49 | + | 
 | 50 | + | 
 | 51 | +// changes the state everytime a button is clicked, but tests if the game isn't over before  | 
 | 52 | +  handleClick(i) {  | 
 | 53 | + | 
 | 54 | +    computer = this.state.xIsNext ? 'O' : 'X';  | 
 | 55 | +    human = this.state.xIsNext ? 'X' : 'O';  | 
 | 56 | + | 
 | 57 | +    const squares = this.state.squares.slice();  | 
 | 58 | +    if (calculateWinner(squares) || squares[i] || isItATie(squares)) {  | 
 | 59 | +      return;  | 
 | 60 | +    }  | 
 | 61 | +    squares[i] = this.state.xIsNext ? 'X' : 'O';  | 
 | 62 | +    this.setState({  | 
 | 63 | +      squares: squares,  | 
 | 64 | +      xIsNext: !this.state.xIsNext,  | 
 | 65 | +    },  | 
 | 66 | +    this.AIDecider  | 
 | 67 | +    );  | 
 | 68 | +  }  | 
 | 69 | + | 
 | 70 | + | 
 | 71 | +// resets the board when the button is clicked.  | 
 | 72 | +  resetClick() {  | 
 | 73 | +    this.setState({  | 
 | 74 | +      squares: Array(9).fill(null), // Clears the board.  | 
 | 75 | +      xIsNext: this.props.isX, // if true, returns 'X'. else, returns 'O'.  | 
 | 76 | +    });  | 
 | 77 | +  }  | 
 | 78 | + | 
 | 79 | + | 
 | 80 | +// Easier to just define the function and keep calling it with this  | 
 | 81 | +  copyBoard(board) {  | 
 | 82 | +    return board.slice();  | 
 | 83 | +  }  | 
 | 84 | + | 
 | 85 | +  /*  | 
 | 86 | +  Checks to make sure if the move that AIDecider, minScore, and maxScore is valid.  | 
 | 87 | +  If not, doesn't return anything.  | 
 | 88 | +  If it is valid, it returns every possible combination in arrays.  | 
 | 89 | +  */  | 
 | 90 | + | 
 | 91 | +  validMove(index, player, board) {  | 
 | 92 | +    let thisCopy = this.copyBoard(board);  | 
 | 93 | +    if (thisCopy[index] === null) {  | 
 | 94 | +      thisCopy[index] = player;  | 
 | 95 | +      return thisCopy;  | 
 | 96 | +    } else {  | 
 | 97 | +      return null;  | 
 | 98 | +    }  | 
 | 99 | +  }  | 
 | 100 | + | 
 | 101 | +  /*  | 
 | 102 | +  The actual decider, and beginning of the MiniMax algorithim.  | 
 | 103 | +  Calls maxScore, which calls minScore until it finds an index.  | 
 | 104 | +  Then, copies the board and gives back the board with the decision.  | 
 | 105 | +  */  | 
 | 106 | + | 
 | 107 | +  AIDecider() {  | 
 | 108 | +    let boardCopy = this.copyBoard(this.state.squares);  | 
 | 109 | +    let move = null;  | 
 | 110 | +    let bestMoveScore = -100;  | 
 | 111 | +    let newBoard = null;  | 
 | 112 | +    let publishedBoard = null;  | 
 | 113 | +    // doesn't do anything if the game is over  | 
 | 114 | +    if (calculateWinner(boardCopy) === computer || calculateWinner(boardCopy) === human || isItATie(boardCopy)) {  | 
 | 115 | +      return null;  | 
 | 116 | +    }  | 
 | 117 | + | 
 | 118 | +    /*  | 
 | 119 | +    The initial loop that suggests any and all moves possible. Calls maxScore which  | 
 | 120 | +    has its own loop which adds a move and calls validMove  | 
 | 121 | +    (therefore putting it a move ahead), and maxScore calls minScore that has its own loop  | 
 | 122 | +    (therefore putting it two moves ahead). Then, decides by point incentive whether  | 
 | 123 | +    it is a good move or not.  | 
 | 124 | +    */  | 
 | 125 | + | 
 | 126 | +    for (let i = 0; i < boardCopy.length; i++) {  | 
 | 127 | +      newBoard = this.validMove(i, computer, boardCopy);  | 
 | 128 | +      if (newBoard) {  | 
 | 129 | +        let moveScore = this.maxScore(newBoard);  | 
 | 130 | +        if (moveScore > bestMoveScore) {  | 
 | 131 | +          bestMoveScore = moveScore;  | 
 | 132 | +          move = i;  | 
 | 133 | +        }  | 
 | 134 | +      }  | 
 | 135 | +    }  | 
 | 136 | + | 
 | 137 | +    /*  | 
 | 138 | +    When the loop is over, it will assign the index that is the best move to play.  | 
 | 139 | +    Assigns the new index with whatever the computer is playing (X or O), then adds it onto  | 
 | 140 | +    a copy of the board (publishedBoard) where it will be change the state of the board.  | 
 | 141 | +    setTimeout given for user experience (giving the illusion that the computer is "thinking").  | 
 | 142 | +    */  | 
 | 143 | + | 
 | 144 | +    publishedBoard = boardCopy;  | 
 | 145 | +    publishedBoard[move] = computer;  | 
 | 146 | +    setTimeout(() => {  | 
 | 147 | +      this.setState({  | 
 | 148 | +        squares: publishedBoard,  | 
 | 149 | +        xIsNext: !this.state.xIsNext  | 
 | 150 | +      });  | 
 | 151 | +    }, 300);  | 
 | 152 | +  }  | 
 | 153 | + | 
 | 154 | +  /*  | 
 | 155 | +  minScore and maxScore call on each other where they add more possible moves onto  | 
 | 156 | +  the copied board until it is either a tie, or someone has won with the theoretical  | 
 | 157 | +  boards. Then, returns a number value where the loop in AIDecider will start again  | 
 | 158 | +  until the loop runs out.  | 
 | 159 | +  */  | 
 | 160 | + | 
 | 161 | +  minScore(board) {  | 
 | 162 | +    if (calculateWinner(board) === human) {  | 
 | 163 | +      return -10;  | 
 | 164 | +    } else if (calculateWinner(board) === computer) {  | 
 | 165 | +      return 10;  | 
 | 166 | +    } else if (isItATie(board)) {  | 
 | 167 | +      return 0;  | 
 | 168 | +    } else {  | 
 | 169 | +      let bestMoveValue = -100;  | 
 | 170 | +      let move = 0;  | 
 | 171 | +      for (let i = 0; i < board.length; i++) {  | 
 | 172 | +        let newBoard = this.validMove(i, computer, board);  | 
 | 173 | +        if (newBoard) {  | 
 | 174 | +          let predictedMoveValue = this.maxScore(newBoard);  | 
 | 175 | +          if (predictedMoveValue > bestMoveValue) {  | 
 | 176 | +            bestMoveValue = predictedMoveValue;  | 
 | 177 | +            move = i;  | 
 | 178 | +          }  | 
 | 179 | +        }  | 
 | 180 | +      }  | 
 | 181 | +      return bestMoveValue;  | 
 | 182 | +    }  | 
 | 183 | +  }  | 
 | 184 | + | 
 | 185 | +  // Called first by AIDecider, then calls minScore  | 
 | 186 | + | 
 | 187 | +  maxScore(board) {  | 
 | 188 | +    if (calculateWinner(board) === human) {  | 
 | 189 | +      return -10;  | 
 | 190 | +    } else if (calculateWinner(board) === computer) {  | 
 | 191 | +      return 10;  | 
 | 192 | +    } else if (isItATie(board)) {  | 
 | 193 | +      return 0;  | 
 | 194 | +    } else {  | 
 | 195 | +      let bestMoveValue = 100;  | 
 | 196 | +      let move = 0;  | 
 | 197 | +      for (let i = 0; i < board.length; i++) {  | 
 | 198 | +        let newBoard = this.validMove(i, human, board);  | 
 | 199 | +        if (newBoard) {  | 
 | 200 | +          let predictedMoveValue = this.minScore(newBoard);  | 
 | 201 | +          if (predictedMoveValue < bestMoveValue) {  | 
 | 202 | +            bestMoveValue = predictedMoveValue;  | 
 | 203 | +            move = i;  | 
 | 204 | +          }  | 
 | 205 | +        }  | 
 | 206 | +      }  | 
 | 207 | +      return bestMoveValue;  | 
 | 208 | +    }  | 
 | 209 | +  }  | 
 | 210 | + | 
 | 211 | +  render() {  | 
 | 212 | +    const winner = calculateWinner(this.state.squares);  | 
 | 213 | +    const tie = isItATie(this.state.squares);  | 
 | 214 | +    let status, button = null;  | 
 | 215 | + | 
 | 216 | +    if (winner) {  | 
 | 217 | +      status = "Winner: " + winner;  | 
 | 218 | +      button = <Button onClick={() => this.resetClick()}/>  | 
 | 219 | +    } else if (tie) {  | 
 | 220 | +      status = "It's a tie!";  | 
 | 221 | +      button = <Button onClick={() => this.resetClick()}/>  | 
 | 222 | +    } else {  | 
 | 223 | +      status = "Your turn, " + (this.state.xIsNext ? 'X' : 'O');  | 
 | 224 | +    }  | 
 | 225 | + | 
 | 226 | +    return (  | 
 | 227 | +      <div>  | 
 | 228 | +        <div className="status">{status}</div>  | 
 | 229 | +        <div style={{textAlign: 'center'}}>{button}</div>  | 
 | 230 | +        <div className="board-row">  | 
 | 231 | +          {this.renderSquare(0)}  | 
 | 232 | +          {this.renderSquare(1)}  | 
 | 233 | +          {this.renderSquare(2)}  | 
 | 234 | +        </div>  | 
 | 235 | +        <div className="board-row">  | 
 | 236 | +          {this.renderSquare(3)}  | 
 | 237 | +          {this.renderSquare(4)}  | 
 | 238 | +          {this.renderSquare(5)}  | 
 | 239 | +        </div>  | 
 | 240 | +        <div className="board-row">  | 
 | 241 | +          {this.renderSquare(6)}  | 
 | 242 | +          {this.renderSquare(7)}  | 
 | 243 | +          {this.renderSquare(8)}  | 
 | 244 | +        </div>  | 
 | 245 | +      </div>  | 
 | 246 | +    );  | 
 | 247 | +  }  | 
 | 248 | +}  | 
 | 249 | + | 
 | 250 | + | 
 | 251 | +// Initialization of game. Passes the choice of the user to Board, where the actual game will render.  | 
 | 252 | +class Game extends React.Component {  | 
 | 253 | +  constructor() {  | 
 | 254 | +    super();  | 
 | 255 | +    this.state = {  | 
 | 256 | +      hasChosen: false,  | 
 | 257 | +      isX: true,  | 
 | 258 | +    }  | 
 | 259 | +  }  | 
 | 260 | + | 
 | 261 | +  question() {  | 
 | 262 | +    return (  | 
 | 263 | +      <div className='questionRow'>  | 
 | 264 | +        Tic Tac Toe  | 
 | 265 | +        <br />  | 
 | 266 | +        <br />  | 
 | 267 | +        X or O?  | 
 | 268 | +        <br />  | 
 | 269 | +        <button className='button' onClick={() => this.test(true)}>X</button>  | 
 | 270 | +        <button className='button' onClick={() => this.test(false)}>O</button>  | 
 | 271 | +      </div>  | 
 | 272 | +    );  | 
 | 273 | +  }  | 
 | 274 | + | 
 | 275 | +  test(bool) {  | 
 | 276 | +    if (bool) {  | 
 | 277 | +      this.setState({  | 
 | 278 | +        hasChosen: true,  | 
 | 279 | +        isX: true,  | 
 | 280 | +      });  | 
 | 281 | +    } else {  | 
 | 282 | +      this.setState({  | 
 | 283 | +        hasChosen: true,  | 
 | 284 | +        isX: false,  | 
 | 285 | +      });  | 
 | 286 | +    }  | 
 | 287 | +  }  | 
 | 288 | + | 
 | 289 | +  render() {  | 
 | 290 | +    let board = null;  | 
 | 291 | + | 
 | 292 | +    if (!this.state.hasChosen) {  | 
 | 293 | +      board = this.question();  | 
 | 294 | +    } else {  | 
 | 295 | +      board = <Board isX={this.state.isX} />  | 
 | 296 | +    }  | 
 | 297 | + | 
 | 298 | +    return (  | 
 | 299 | +      <div>  | 
 | 300 | +        <div className="game">  | 
 | 301 | +          <div className="game-board">  | 
 | 302 | +          {board}  | 
 | 303 | +          </div>  | 
 | 304 | +        </div>  | 
 | 305 | +      </div>  | 
 | 306 | +    );  | 
 | 307 | +  }  | 
 | 308 | +}  | 
 | 309 | + | 
 | 310 | +/*  | 
 | 311 | +Where it calculates if there is a possible winner with the MiniMax loops, or ties.  | 
 | 312 | +Note: status does not render until there is an actual winner (not theoretical winners  | 
 | 313 | +made up by MiniMax), tested by the actual state of the board.  | 
 | 314 | +*/  | 
 | 315 | + | 
 | 316 | +function calculateWinner(squares) {  | 
 | 317 | +  const lines = [  | 
 | 318 | +    [0, 1, 2],  | 
 | 319 | +    [3, 4, 5],  | 
 | 320 | +    [6, 7, 8],  | 
 | 321 | +    [0, 3, 6],  | 
 | 322 | +    [1, 4, 7],  | 
 | 323 | +    [2, 5, 8],  | 
 | 324 | +    [0, 4, 8],  | 
 | 325 | +    [2, 4, 6],  | 
 | 326 | +  ];  | 
 | 327 | +  for (let i = 0; i < lines.length; i++) {  | 
 | 328 | +    const [a, b, c] = lines[i];  | 
 | 329 | +    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {  | 
 | 330 | +      return squares[a];  | 
 | 331 | +    }  | 
 | 332 | +  }  | 
 | 333 | +  return null;  | 
 | 334 | +}  | 
 | 335 | + | 
 | 336 | +function isItATie(squares) {  | 
 | 337 | +  let copy = squares.slice();  | 
 | 338 | +  if (copy.some(element => !element)) {  | 
 | 339 | +    return false;  | 
 | 340 | +  } else {  | 
 | 341 | +    return true;  | 
 | 342 | +  }  | 
 | 343 | +}  | 
 | 344 | + | 
 | 345 | +ReactDOM.render(  | 
 | 346 | +  <Game />,  | 
 | 347 | +  document.getElementById('root')  | 
 | 348 | +);  | 
0 commit comments