React チュートリアル発展 追加課題

React公式チュートリアル追加課題の実装に関してまとめます。前回の記事で関数コンポーネント化を行った後の続きです。まだの方は先に確認して頂ければと思います。
https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b

## 追加実装の流れ
- 公式チュートリアルのタイムトラベル機能実装まで完了(https://ja.reactjs.org/tutorial/tutorial.html#wrapping-up)
- [モジュール化](https://qiita.com/nishiwaki_ff/items/9305672c82c169e06327)
- [関数コンポーネントに書き換え](https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b)
- 追加課題の実装 <font color="Red">**⇦この記事**</font>


## 追加課題1 解説
課題1の内容は、**履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する**です。この課題を実装するには次の2つのことを実現する必要があります。

1. 着手の位置をstateで保持する
1. (col, row) のフォーマットで表示する

着手の位置や、それを表示する履歴情報を持っているのはGameコンポーネントです。では順番にGameコンポーネントに修正を加えます。

### 着手の位置をstateで保持する

ボードが要素9つの配列になってるので、そのインデックスを着手の位置として保持しようと思います。最終的に(col, row) のフォーマットで表示する必要があるものの、インデックスから計算で導き出すことができるので、インデックスのみをstateに保持させます。

```js:Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null // 着手の位置を保持する為pointを追加
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i // pointにインデックスを代入
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const desc = move ?
      'Go to move #' + move + '(' + step.point + ')' : // 確認の為着手位置を表示
      'Go to game start';
  // 以下変更無し
```

![kadai1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/63726dd0-34df-4a61-fc65-5f021bebf6fb.jpeg)

ここまで修正を終えるとこのような結果になると思います。あとは表示を修正すれば完成です。

### (col, row) のフォーマットで表示する

```js:Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1; // colを算出
    const row = (step.point / 3 + 1) | 0; // rowを算出
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; // 指定のフォーマットに修正
    const desc = move ? goToMove : 'Go to game start'; // 1行に修正
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });
  // 以下変更無し
```

colとrowを算出した後、(col, row) のフォーマットで表示させています。rowに関しては[ビット論理和](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_OR)を用いて小数点以下を切り捨てています。[ビット論理和](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_OR)の代わりに[Math.floor関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/floor)を使うことも可能です。

![kadai1-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/0bf487a0-36df-4a28-83ad-26ccdfc8f226.jpeg)

以上で課題1は終了です。このような結果になります。


## 追加課題2 解説
課題2の内容は、**着手履歴のリスト中で現在選択されているアイテムをボールドにする**です。この課題を実装するには次の2つのことを実現する必要があります。

1. 履歴の表示をボールドにする
1. 現在選択されているアイテムのみをボールドにする

### 履歴の表示をボールドにする
まずは着手履歴リストのbuttonタグにclassを追加します。

```js:Game.js
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={'bold'} // class追加
        >
          {desc}
        </button>
      </li>
    );
  });
  // 以下変更無し
```

追加したclassに対応する記述をcssに追加します。

```css:index.css
/* 追加 */
.bold {
  font-weight: bold;
}
```

![kadai2-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/0c9ac35a-65f2-be4f-8f96-063838b18976.jpeg)

履歴の表示が全てボールドになります。

### 現在選択されているアイテムのみをボールドにする

最後に現在選択されているアイテムのみボールドに修正していきます。現在選択されているアイテムを判定するにはstemNumberが使えます。

```js:Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          // 現在選択されているアイテムだけclassNameをboldにする
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });
  // 以下変更無し
```

![kadai2-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/a436bec2-b2b9-ad27-90f4-366fe3a6b80a.jpeg)

以上で課題2が終了です。

## 追加課題3 解説
課題3の内容は、**Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える**です。この課題は繰り返し文を利用して実装します。まずは一部だけfor文で書き換えます。

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
      />
    );
  };

  const maxCol = 3;
  const rowBoard1 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 0 + col;
    rowBoard1.push(renderSquare(index));
  }

  return (
    <div>
      <div className="board-row">
        {rowBoard1}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};

export default Board;
```

rowBoard1の箇所を追加及び修正しました。しかしこの時点で警告が出ます。
![kadai3-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/899cb8ce-7c68-73bb-5a7b-bdd3ed3c9c2b.jpeg)
keyが必要なので追加します。

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i} // key追加
      />
    );
  };
// 以下変更無し
```

これで警告も消えたので先程と同様にfor文で書き換えていきます。

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const maxCol = 3;
  const rowBoard1 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 0 + col;
    rowBoard1.push(renderSquare(index));
  }

  const rowBoard2 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 1 + col;
    rowBoard2.push(renderSquare(index));
  }

  const rowBoard3 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 2 + col;
    rowBoard3.push(renderSquare(index));
  }

  return (
    <div>
      <div className="board-row">
        {rowBoard1}
      </div>
      <div className="board-row">
        {rowBoard2}
      </div>
      <div className="board-row">
        {rowBoard3}
      </div>
    </div>
  );
};

export default Board;
```

明らかに冗長なのでさらにfor文を追加してネストさせます。

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      rowBoard.push(renderSquare(index));
    }
    squareBoard.push(
      <div className="board-row">
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;
```

これでほぼ完成ですが、ここで再び警告が出ます。
![kadai3-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/b4d5385b-e65e-8d03-e7ed-b606c8b16655.jpeg)
先程と同様にkeyを追加すると警告が消えます。

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      rowBoard.push(renderSquare(index));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row} // key追加
      >
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;
```
以上で課題3が終了です。

## 追加課題4 解説
課題4の内容は、**着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する**です。次の順番で実装していきます。

1. 現在は昇順で表示されているので、降順で表示させる
1. 昇順と降順を判定するためのstateを追加
1. ボタンを追加し、ボタン押下で並べ替えができるようにする

### 降順で表示させる
降順で表示させるにはreverse()メソッドを用いてmovesの順序を入れ替えることで実現できます。

```js:Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  moves.reverse(); // movesの順序入れ替え
// 以下変更無し
```
![kadai4-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/abe8e788-f845-3da9-7d3f-aa95269a385d.jpeg)

### 昇順と降順を判定するためのstateを追加

```js:Game.js
const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false); // state追加

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  // stateの値で昇順と降順を判定し条件分岐
  if (movesOrder) {
    moves.reverse();
  }
// 以下変更無し
```

### ボタンを追加し、ボタン押下で並べ替えができるようにする
まずはボタンを追加します。

```js:Game.js
  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        {/* ボタン追加 */}
        <button>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
// 以下変更無し
```
![kadai4-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/28e0e304-97c5-b0d1-ef4d-2d5fd772ebf9.jpeg)
ボタンを追加しましたが、まだ押下しても何も変化しません。最後にボタンを押下した時にstateの値が変わるようイベントを追加します。

```js:Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        {/* イベント追加 */}
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

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

export default Game;
```
以上で課題4が終了です。

## 追加課題5 解説
課題5の内容は、**どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする**です。次の順番で実装していきます。

1. calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする
1. 3つのマスの情報を用いて該当箇所をハイライトする

### calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする
calculateWinner関数の中身とそれを呼び出していた箇所を修正します。

```js:Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares).winner || squares[i]) { // winnerを参照するよう修正
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares).winner; // winnerを参照するよう修正
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

const 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]
  ];

  // 勝者とマスの情報をもつオブジェクトを用意
  const result = {
    winner: null,
    winLine: []
  }

  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]) {
      result.winner = squares[a]; // 勝者をresultに代入
      result.winLine = result.winLine.concat(lines[i]); // 勝利に繋がったマスの情報をresultに代入
    }
  }
  return result; // resultを返す
};

export default Game;
```

### 3つのマスの情報を用いて該当箇所をハイライトする
calculateWinner関数から返るマスの情報をまずはBoardコンポーネントに渡します。

```js:Game.js
  const current = history[stepNumber];
  const {winner, winLine} = calculateWinner(current.squares); // winnerとwinLineを分割代入
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        {/* propsにwinLineを追加 */}
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
          winLine={winLine}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
// 以下変更無し
```
これでBoardコンポーネントでwinLineが参照できるようになります。次にBoardコンポーネントを修正します。propsで受け取ったwinLineにindexが含まれるかどうか確認し、結果をSquareコンポーネントに渡します。

```js:Board.js
const Board = (props) => {
  const renderSquare = (i, isHighlight) => { // isHighlightでハイライト有無の情報を受け取る
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
        // props追加
        isHighlight={isHighlight}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      const isHighlight = props.winLine.includes(index); // winLineにindexが含まれるかどうか確認
      rowBoard.push(renderSquare(index, isHighlight));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row}
      >
        {rowBoard}
      </div>
    );
  }
// 以下変更無し
```
これでSquareコンポーネントでハイライト有無の情報が参照できるようになります。その情報を用いてハイライトするようSquareコンポーネントを修正します。

```js:Square.js
const Square = (props) => {
  const className = props.isHighlight ? 'square-highlight' : 'square';
  return (
    <button className={className} onClick={props.onClick}>
      {props.value}
    </button>
  );
};

export default Square;
```

最後に対応するcssを追加して完了です。

```css:index.css
/* 追加 */
.square-highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}
```
![kadai5-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/bb9535a3-1209-95b4-ce7e-dfb457b83d0f.jpeg)

## 追加課題6 解説
課題6の内容は、**どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する**です。次の順番で実装していきます。

1. calculateWinner関数で引き分けかどうかの情報も返すようにする
1. 引き分けの場合、引き分けのメッセージを表示する

### calculateWinner関数で引き分けかどうかの情報も返すようにする

```js:Game.js
const 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]
  ];
  const result = {
    winner: null,
    winLine: [],
    isDraw: false // 追加
  }
  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]) {
      result.winner = squares[a];
      result.winLine = result.winLine.concat(lines[i]);
    }
  }
  // 勝者が決まっておらず、かつマスが全て埋まっている時が引き分けになる
  if (result.winner === null && !squares.includes(null)) {
    result.isDraw = true;
  }
  return result;
};

export default Game;
```

### 引き分けの場合、引き分けのメッセージを表示する

```js:Game.js
  const current = history[stepNumber];
  const {winner, winLine, isDraw} = calculateWinner(current.squares); // isDrawも参照できるように書き換え
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  // 引き分けだった場合はstatusを変更
  if (isDraw) {
    status = 'Draw';
  }
// 以下変更無し
```

これで引き分けの場合はDrawのメッセージが表示されるようになります。

![kadai6-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482619/c8c2b6c0-7bc6-ce29-a4be-2c2b05e2d6cb.jpeg)

## まとめ
以上で全ての実装が終了です。
最後に最終結果のコードがこちらです。

```js:index.js
import ReactDOM from 'react-dom';
import './index.css';
import Game from './Game.js';

ReactDOM.render(<Game />, document.getElementById("root"));
```

```js:Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares).winner || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const {winner, winLine, isDraw} = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  if (isDraw) {
    status = 'Draw';
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
          winLine={winLine}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

const 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]
  ];
  const result = {
    winner: null,
    winLine: [],
    isDraw: false
  }
  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]) {
      result.winner = squares[a];
      result.winLine = result.winLine.concat(lines[i]);
    }
  }
  if (result.winner === null && !squares.includes(null)) {
    result.isDraw = true;
  }
  return result;
};

export default Game;
```

```js:Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i, isHighlight) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
        isHighlight={isHighlight}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      const isHighlight = props.winLine.includes(index);
      rowBoard.push(renderSquare(index, isHighlight));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row}
      >
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;
```

```js:Square.js
const Square = (props) => {
  const className = props.isHighlight ? 'square-highlight' : 'square';
  return (
    <button className={className} onClick={props.onClick}>
      {props.value}
    </button>
  );
};

export default Square;
```

```css:index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square__highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.bold {
  font-weight: bold;
}

.square-highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}
```