0
1

React公式チュートリアルの三目並べに追加機能を実装してみた #5(各ターンで打った手を記憶しろ!編)

Last updated at Posted at 2023-10-22

はじめに

はい、ということでReact公式チュートリアル追加機能実装第五弾やっていきたいと思います。
実装は今回で最後ですので気合入れていきます!

最後に実装する機能はこちら↓
「Display the location for each move in the format (row, col) in the move history list.」(日本語訳:移動履歴リストに各移動の位置を(行、列)の形式で表示する。)です!

image.png

もうここまで来たらなんとなくどのように実装すればいいか予想がつくようになってきましたね。
それでは早速実装していきましょう。

実装

まずは実装の方針を考えます。
移動履歴リストにマスの位置を表示させるためには、手を打つ(ボードをクリックする)際に打ったマスを記憶する必要がありそうです。
そしてその記憶したマスの位置を、移動履歴ボタン(.game-info-btn)をレンダリングする時に渡してあげればよさそうですね。

まずはクリックした際にマスの位置を記憶します。

App.js
function handleClick(i) {
    const row = Math.floor(i / 3);
    const col = i % 3;
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    nextSquares[i] = xIsNext ? 'X' : 'O';
    onPlay(nextSquares, { row, col });
  }

次にGameコンポーネント内で移動履歴ボタンをなんやかんやしているので記憶したマスの位置を渡して表示できるようにしてあげましょう。

App.js
export default function Game() {
  const [history, setHistory] = useState([{ squares: Array(9).fill(null), position: null }]); // ←マスの位置を受け取れるように修正
  const [currentMove, setCurrentMove] = useState(0);
  const [isAscending, setIsAscending] = useState(true);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove].squares;

  function handlePlay(nextSquares, position) {
    const nextHistory = [...history.slice(0, currentMove + 1), { squares: nextSquares, position }];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  *** 省略 ***

  const moves = history.map((step, move) => {
    const { row, col } = step.position || {};  // position が存在しない場合に備えて、デフォルト値を設定
    const description = move > 0
      ? 'Go to move #' + move + ` (row: ${row}, col: ${col})`
      : 'Go to game start';
      const buttonClass = move === currentMove ? 'game-info-btn current-move' : 'game-info-btn';
    return (
      <li className="game-info-item" key={move}>
        <button className={buttonClass} onClick={() => jumpTo(move)}>
          {description}
        </button>
      </li>
    );
  });

  
  *** 省略 ***

  return (
    <div className="game">
      <div className="game-board">
      <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <button className="toggle-button"  onClick={toggleSortOrder}>Toggle Sort Order</button>
        <ol className="game-info-list">{sortedMoves}</ol>
      </div>
    </div>
  );
}

このように修正すると、、、

Animation.gif

打ったマスの位置が移動履歴ボタンの横に表示されるようになりました!

長かったReact公式チュートリアル三目並べへの追加機能実装もこれにて完了です、、!

以下最終的なソースコードになります。

App.js
import { useState } from 'react';
import './App.css';

function Square({ value, onSquareClick, isWinningSquare }) {
  const className = isWinningSquare ? 'square winning' : 'square';
  return (
    <button className={className} onClick={onSquareClick}>
      {value}
    </button>
  );
}

function Board({ xIsNext, squares, onPlay }) {
  function handleClick(i) {
    const row = Math.floor(i / 3);
    const col = i % 3;
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    nextSquares[i] = xIsNext ? 'X' : 'O';
    onPlay(nextSquares, { row, col });
  }

  const winner = calculateWinner(squares);
  const winningLine = winner ? winner.winningLine : null;
  let status;
  if (winner) {
    status = 'Winner: ' + winner.winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  const boardRows = [];
  for (let row = 0; row < 3; row++) {
    const squaresInRow = [];
    for (let col = 0; col < 3; col++) {
      const squareIndex = row * 3 + col;
      const isWinningSquare = winningLine && winningLine.includes(squareIndex);
      squaresInRow.push(
        <Square
          key={squareIndex}
          value={squares[squareIndex]}
          onSquareClick={() => handleClick(squareIndex)}
          isWinningSquare={isWinningSquare}
        />
      );
    }
    boardRows.push(
      <div key={row} className="board-row">
        {squaresInRow}
      </div>
    );
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-table">{boardRows}</div>
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([{ squares: Array(9).fill(null), position: null }]);
  const [currentMove, setCurrentMove] = useState(0);
  const [isAscending, setIsAscending] = useState(true);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove].squares;

  function handlePlay(nextSquares, position) {
    const nextHistory = [...history.slice(0, currentMove + 1), { squares: nextSquares, position }];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  const moves = history.map((step, move) => {
    const { row, col } = step.position || {};
    const description = move > 0
      ? 'Go to move #' + move + ` (row: ${row}, col: ${col})`
      : 'Go to game start';
      const buttonClass = move === currentMove ? 'game-info-btn current-move' : 'game-info-btn';
    return (
      <li className="game-info-item" key={move}>
        <button className={buttonClass} onClick={() => jumpTo(move)}>
          {description}
        </button>
      </li>
    );
  });

  function toggleSortOrder() {
    setIsAscending(prevIsAscending => !prevIsAscending);
  }

  const sortedMoves = isAscending ? moves : moves.slice().reverse();

  return (
    <div className="game">
      <div className="game-board">
      <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <button className="toggle-button"  onClick={toggleSortOrder}>Toggle Sort Order</button>
        <ol className="game-info-list">{sortedMoves}</ol>
      </div>
    </div>
  );
}

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 {winner: squares[a], winningLine: lines[i]};
    }
  }
  return null;
}
App.css
.game {
  margin: 50px auto;
}
.game-board {
  margin-bottom: 20px;
}

.status {
  margin-bottom: 30px;
  font-size: 24px;
  font-weight: bold;
}

.board-table {
  border-collapse: collapse;
  margin: 0 auto;
}

.board-row {
  display: flex;
}

.square {
  width: 100px;
  height: 100px;
  border: 2px solid #000;
  font-size: 24px;
  cursor: pointer;
  background-color: #ffffff;
}

.square:hover {
  background-color: #c8e7fa;
}

.game-info {
  text-align: center;
}

.game-info-list {
  padding: 0;
  list-style-type: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.game-info-item {
  width: 100%;
  text-align: center;
}

.game-info-btn {
  background-color: #9cd9ff;
  padding: 15px 30px;
  border: none;
  border-radius: 5px;
  font-size: 20px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.game-info-btn:hover {
  background-color: #69c6ff;
}

.game-info-btn.current-move {
  background-color: #d7d7d7;
}

.toggle-button {
  background-color: #8affc1;
  padding: 15px 30px;
  border: none;
  border-radius: 5px;
  font-size: 20px;
  cursor: pointer;
  transition: background-color 0.3s;
  text-align: center;
  display: inline-block;
}

.toggle-button:hover {
  background-color: #45ff9c;
}
.square.winning {
  background-color: yellow;
}

では、せっかく作ったのでこのゲームをインターネットの海に公開するところまで行きたいと思います。
次回、「作ったゲームをAWSで公開してみた」編やってみましょう!!(゚∀゚ )

次の記事↓

React公式チュートリアルの三目並べに追加機能を実装してみた #6(AWSで公開しろ!編)

他の関連記事↓

React公式チュートリアルの三目並べに追加機能を実装してみた #1(現在のターンを示せ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #2(ループ処理でボードを表示しろ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #3(トグルボタンを追加しろ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #4(勝利のマスを強調しろ!編)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1