React Docs（Beta）のチュートリアルの発展問題をやってみたメモ

以前の記事で、React Docs（Beta）のチュートリアルで作った三目並べゲームを、TypeScript化しました。

https://qiita.com/onukichi/items/fa5919eae14d6c5a3db6

React Docs（Beta）のチュートリアルの最後には、以下の発展問題があります。

※DeepLで翻訳
>時間があるときや、新しいReactのスキルを練習したいときに、三目並べゲームにできる改善のアイデアを、難易度の高い順にいくつか挙げておきます。
>1. 現在の手に対してのみ、ボタンの代わりに「You are at move #...」と表示する。
>2. 碁盤をハードコードするのではなく、2つのループでマスを作るように書き換える。
>3. トグルボタンを追加して、手を昇順または降順に並べ替えられるようにする
>4. 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する (誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する)。
>5. 着手履歴のリストに各手順の位置を(col, row)の形式で表示する。
>
>https://beta.reactjs.org/learn/tutorial-tic-tac-toe#wrapping-up

この記事は、React Docs（Beta）のチュートリアルの発展問題をやってみたメモです。

# メモ
## 1. 現在の手に対してのみ、ボタンの代わりに「You are at move #...」と表示する。

currentMoveで現在の手を記録していますので、以下のように条件分岐を追加することで対応しました。
```typescript:src/components/Game.tsx
return (
  <li key={move}>
    {move === currentMove
      ? `You are at move #${currentMove}`
      : <button onClick={() => jumpTo(move)}>{ description }</button>
    }
  </li>
)
```

以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/4a41563d227d5e6c4a9e164d4214994363f54c1b

## 2. 碁盤をハードコードするのではなく、2つのループでマスを作るように書き換える。
div用のfor文とSquareコンポーネント用のfor文を2つ追加し、配列にそれぞれpushする形にしました。
```typescript:src/components/Game.tsx
const boardView = []
for(let i = 0; i < 3; i++) {
  const row = []
  for(let j = 0; j < 3; j++) {
    row.push(<Square value={squares[i * 3 + j]} onSquareClick={() => handleClick(i * 3 + j)} />)
  }
  boardView.push(<div className="board-row">{row}</div>)
}

return (
  <>
    <div className="status">{status}</div>
    { boardView }
  </>
);
```

以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/d4ba86c7b62853f8b61456f250f4448ae264fab8

## 3. トグルボタンを追加して、手を昇順または降順に並べ替えられるようにする

昇順と降順を制御するbooleanのstate`isAsc`を追加

```typescript:src/components/Game.tsx
export default function Game() {
  const [history, setHistory] = useState<BoardState[]>([[null, null, null, null, null, null, null, null, null]])
  const [currentMove, setCurrentMove] = useState<number>(0)
+ const [isAsc, setIsAsc] = useState<boolean>(true)
```

トグルボタンを設置し、state`isAsc`によって並び順を変更できるようにしました。
初めolタグのreversed属性を知らなかったので、リストの数字を変更するのに少しハマりました。
```typescript:src/components/Game.tsx
return (
  <div className="game">
    <div className="game-board">
      <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
    </div>
    <div className="game-info">
+     <button onClick={() => setIsAsc(!isAsc)}>{ isAsc ? 'Asc' : 'Desc' }</button>
+     <ol reversed={!isAsc}>{ isAsc ? moves : moves.reverse() }</ol>
```

以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/23b2576e6a002b69ddf9d1957e7d447b2e41bd29

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/8f40052a7fc712e65c6bd35d6464ac15b5025582

## 4. 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する (誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する)。

### 4.1 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する

勝った３つのマスはcalculateWinner関数で判定できます。変更前は`squares[a]`だけ返していましたが、オブジェクトに変更して勝者`player`と３つのマス`line`を返すようにします。
```typescript:src/components/Game.tsx
function calculateWinner(squares: BoardState) {
  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 {
+       player: squares[a],
+       line: [a, b, c]
+     }
    }
  }
  return null
}
```

Squareコンポーネントのpropsに`isWinnerSquare`を追加、trueなら勝因となった３つのマスと判別できるようにしました。
```typescript:src/components/Game.tsx
const boardView = []
for(let i = 0; i < 3; i++) {
  const row = []
  for(let j = 0; j < 3; j++) {
+   row.push(<Square key={i * 3 + j} value={squares[i * 3 + j]} onSquareClick={() => handleClick(i * 3 + j)} isWinnerSquare={ winner ? winner.line.includes(i * 3 + j) : false } />)
  }
  boardView.push(<div key={i} className="board-row">{row}</div>)
}
```


以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/ce32c09fd1bc5fb02276ee6d860e0a3a75c46ae6

### 4.2 誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する

引き分けの条件は、「winnerがfalse」かつ「squaresが全て埋まっている（nullの値が無い）」であるため、else ifで条件を追加し、Drawと表示されるようにしました。
```typescript:src/components/Game.tsx
  const winner = calculateWinner(squares)
  let status
  if(winner) {
    status = "Winner: " + winner.player
+ } else if(squares.filter(n => n === null).length === 0) {
+   status = "Draw"
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O")
  }
```

以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/2bf55c3d2b13d3afa7b64853b94896392d54e80a

## 5. 着手履歴のリストに各手順の位置を(col, row)の形式で表示する。

手の位置を記録するstate`location`を作成し、handlePlay関数で更新されるようにしました。handlePlay関数は、子のBoardコンポーネントのhandleClick関数で呼ばれます。
```typescript:src/components/Game.tsx
export default function Game() {
  const [history, setHistory] = useState<BoardState[]>([[null, null, null, null, null, null, null, null, null]])
+ const [location, setLocation] = useState<number[][]>([])
  const [currentMove, setCurrentMove] = useState<number>(0)
  const [isAsc, setIsAsc] = useState<boolean>(true)
  const xIsNext: boolean = currentMove % 2 === 0
  const currentSquares: BoardState = history[currentMove]

+ function handlePlay(nextSquares: BoardState, squareLocation: number) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]
    setHistory(nextHistory)
    setCurrentMove(nextHistory.length - 1)
+
+   const lines = [
+     [1,1],
+     [1,2],
+     [1,3],
+     [2,1],
+     [2,2],
+     [2,3],
+     [3,1],
+     [3,2],
+     [3,3]
+   ]
+   const nextLocation = [...location, lines[squareLocation]]
+   setLocation(nextLocation)
  }
```

着手履歴のリストの箇所で、locationを表示しています。
```typescript:src/components/Game.tsx
  const moves = history.map((squares, move) => {
    let description: string;
    if(move > 0) {
+     description = 'Go to move #' + move + ' colrow: (' + location[move - 1].join(',') + ')'
    } else {
      description = 'Go to game start'
    }
```


以下コード差分です。

https://github.com/onukichi/react-tutorial-tic-tac-toe/commit/f698b8ee97d708467889c9b897c967355bd55f31