React公式チュートリアルの三目並べに追加機能を実装してみた #4（勝利のマスを強調しろ！編）

## はじめに
それではReact公式チュートリアルに追加機能を実装する第四弾を始めていきたいと思います。
今回追加する機能はこちら↓
「When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw).」（日本語訳：誰かが勝った場合は、勝利の原因となった 3 つの四角形が強調表示する。誰も勝てなかった場合は、結果が引き分けであることを示すメッセージを表示する。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/8e48d2ef-a926-201a-b5ac-6529780ab65f.png)

今回はどんな機能を実装すればいいかわかりやすいですね！
それでは早速実装していきましょう！

## 実装
まずは勝利した場合、勝利条件を満たした列を強調させる機能から実装していきましょう。
この機能を実現させるには勝利判定をしている箇所で、勝利条件を満たしたマスを情報として返してあげることが必要そうですね。

さて、勝利判定はどこでしているのか探してみると、、、

```App.js
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;
}
```
ここで判定しているっぽいですね。
ここに記述のある`lines[i]`が勝利条件を満たしたラインのマスのことっぽいです。
なので`lines[i]`も返却するようにしてあげましょうか。

```App.js
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;
}

```
そしたらこの`winningLine`に該当するマスに色を変更する用のクラスを付与してあげましょう。


```App.js
function Board({ xIsNext, squares, onPlay }) {
  // ...（省略）...

  const winner = calculateWinner(squares);
  const winningLine = winner ? winner.winningLine : null;  // 追加

  // ...（省略）...

  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>
    );
  }

  // ...（省略）...
}

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

```

はい、これで勝利が決定した場合、勝利条件を満たしたマスに色を変更する用のクラスが付与されるようになったはずです。

あとは色変更用のCSSを設定するのも忘れないようにしましょう。


```App.css
.square.winning {
  background-color: yellow;
}
```

ちゃんと色が変わるようになっているかブラウザで確認してみましょう！

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/23bd1f34-f2e4-ca99-7cf0-ee29d2dbefd1.gif)

色変わってる～～！！
これで追加機能第四弾「勝利マスの強調」も実装できました。

最後に現時点でのソースコードを載せておきます。


```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) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    nextSquares[i] = xIsNext ? 'X' : 'O';
    onPlay(nextSquares);
  }

  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([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const [isAscending, setIsAscending] = useState(true);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

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

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

  const moves = history.map((squares, move) => {
    const description = move > 0 ? 'Go to move #' + move : '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;
}
```

### 次の記事↓
[React公式チュートリアルの三目並べに追加機能を実装してみた #5（各ターンで打った手を記憶しろ！編）](https://qiita.com/Ryukuu0919/items/cdcd08987f6b727798f9)

### 他の関連記事↓
[React公式チュートリアルの三目並べに追加機能を実装してみた #1（現在のターンを示せ！編）](https://qiita.com/Ryukuu0919/items/50dc599c4f5acf4478b9)
[React公式チュートリアルの三目並べに追加機能を実装してみた #2（ループ処理でボードを表示しろ！編）](https://qiita.com/Ryukuu0919/items/7ec65b2964f611454c5e)
[React公式チュートリアルの三目並べに追加機能を実装してみた #3（トグルボタンを追加しろ！編）](https://qiita.com/Ryukuu0919/items/f22b4d6d13095b305cd3)
[React公式チュートリアルの三目並べに追加機能を実装してみた #6（AWSで公開しろ！編）](https://qiita.com/Ryukuu0919/items/eec4be8768b57601f27a)