TypeScript × React-Tutorial: Tic-Tac-Toe / 三目並べ Extra

この記事では，以下のサイトの追加（Extra）課題を解説をします．

https://react.dev/learn/tutorial-tic-tac-toe

https://ja.react.dev/learn/tutorial-tic-tac-toe

自分が自習時に作成したプログラムとサイトは，以下になります．

https://github.com/tomtkg/React-Tutorial

https://tomtkg.github.io/React-Tutorial/

「React-Tutorial: Tic-Tac-Toe / 三目並べ」にTypeScriptで取り組みました．
[前回の記事](https://qiita.com/tomtkg/items/a489006b85f5fb9896aa)では，チュートリアルにおいて自分が分からなかった箇所，詰まった箇所を中心に解説をしました．
今回は，追加課題に対する自分なりの解答を解説します．
（サイトに模範解答の記載はなく，追加課題は複数の方法で実現できます）

# はじめに
取り組む追加課題は，以下の5つです．
1. 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
2. マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。
3. 手順を昇順または降順でソートできるトグルボタンを追加する。
4. どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。
5. 着手履歴リストで、各着手の場所を (row, col) という形式で表示する。

これらの課題は，難易度の低い順にリストアップされているので，素直に上から順番に取り組みます．
リファクタリングしたプログラム [tutorialExtra/0.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/0.tsx) を基に，追加課題を1つずつ実装します．

[https://github.com/tomtkg/React-Tutorial](https://github.com/tomtkg/React-Tutorial) と [https://tomtkg.github.io/React-Tutorial](https://tomtkg.github.io/React-Tutorial)を別で開いて，差分を確認しながら記事を読むことをお勧めします．

# 1. 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。[1.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/1.tsx)
これは，ボタン表示の一部を変更する課題です．
まず，ボタン表示に関するプログラムを特定します．
`function Game`内の`const moves`が該当します．
`const moves: JSX.Element[]`なので，`moves`は配列です．
その一要素（現在の着手の部分）だけ，ボタンではない`JSX.Element`に変更すれば良いです．
```diff_tsx: 1.tsx
  const moves = history.map((_squares, move) => {
+   // 1. For the current move only, show “You are at move #…” instead of a button.
+   if (move === currentMove) {
+     return <li key={move}> {/* assign proper keys */}
+       <b>{"You are at move #" + move}</b>
+     </li>
+   }
    const description = move > 0 ?
      'Go to move #' + move :
      'Go to game start';
    return (
      <li key={move}> {/* assign proper keys */}
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });
```
表示メッセージもその機能（ボタンの有無）も他の着手履歴とは異なります．
そのため，`if`文を書いて条件に該当する場合は即`return ...`するのが良いと思います．

# 2. マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。[2.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/2.tsx)
これは，`function Board`の`return`部分を書き直すリファクタリング課題です．
`for`ループではなく，`Array`と`map`を利用して実装するのが良いでしょう．
以下は，JavaScriptで連番の配列を生成するプログラムです．
```tsx
[...Array(5)].map((_, i) => i) //=> [ 0, 1, 2, 3, 4 ]
```
これを応用して，課題を実装します．
```diff_tsx: 2.tsx
+ // 2. Rewrite Board to use two loops to make the squares instead of hardcoding them.
+ const board = [...Array(3)].map((_, i) => {
+   let threeSquares = [...Array(3)].map((_, j) => {
+     let n = 3 * i + j;
+     return <Square key={n} value={squares[n]} onSquareClick={() => handleClick(n)} />
+   });
+   return <div key={i} className="board-row">{threeSquares}</div>
+ });
+
  return (
    <>
      <div className="status">{status}</div>
+       {board}
-       <div className="board-row">
-         <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
-         <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
-         <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
-       </div>
-       <div className="board-row">
-         <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
-         <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
-         <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
-       </div>
-       <div className="board-row">
-         <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
-         <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
-         <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
-       </div>
    </>
  );
```
`threeSquares`と`board`は`JSX.Element[]`型の変数です．
その要素に`key`が含まれていないと警告が出ます．

# 3. 手順を昇順または降順でソートできるトグルボタンを追加する。[3.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/3.tsx)
この課題は，先に実装結果を掲載し，その後に解説をします．
```diff_tsx: 3.tsx
export default function Game() {
  ...
+ // 3. Add a toggle button that lets you sort the moves in either ascending or descending order.
+ const [isAsending, setIsAsending] = useState(true);
+
  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
+       Now sort by: 
+       <button onClick={() => setIsAsending(!isAsending)}>
+         {isAsending ? "Asending" : "Descending"}
+       </button>
+       <ol>{isAsending ? moves : moves.reverse()}</ol>
-       <ol>{moves}</ol>
      </div>
    </div>
  );
}
```
まず，昇順または降順の状態を管理する機能が必要です．
これは，`const [isAsending, setIsAsending] = useState(true)`で実現します．
2値しか取らない情報は，`true` or `false`の`boolean`型変数で管理するのが良いです．
変数名は，`isXXX`が良いです．

次に，手順を昇順または降順にする機能を用意します．
手順`moves`は，要素が昇順の`histry`から作成されます．
そのため，`isAsending`の状態によってそのまま表示するか，逆順に表示するか制御すれば良いです．
単純な条件文なので，三項演算子（条件演算子）を利用するの良いです．
`<ol>{isAsending ? moves : moves.reverse()}</ol>`

そして，昇順または降順の状態を切り替える機能を用意します．
`button`と`onClick`，アロー関数（`=>`）を利用します．
ボタンクリック時に`isAsending`を反転（`!`）した値を`isAsending`として設定(`set`)します．
最後に文章など，見た目の表示を整えたら完成です．

# 4. どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。[4.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/4.tsx)
この課題を達成するには，以下の3つの機能が必要です．
* マス目を普通表示，ハイライト表示する機能: `Square` (, `css`)
* 勝者と勝利につながった3マスを特定する機能: `calculateWinner`
* 引き分け時に特別なメッセージを表示する機能: `Board`

機能毎に実装を考えます．

まず，この課題では「マス目をハイライト表示する」という表示デザインの改修があります．
表示デザインの改修は，`App.tsx`で対応するのではなく`css`を改修するのが良いです．
`.win { background: #f66 }`を`css`の適当な箇所に追記します．
`App.tsx`において，`className="square"`を指定すると背景無し（白），`className="square win"`を指定すると背景がハイライト（#f66）になります．

汎用性を考えると，`className`は外部から指定できると良いです．
関数`Square`を以下のように改修します．
```diff_tsx: 4.tsx
type Props = {
+ className: string,
  value: string,
  onSquareClick: any,
}
...
- function Square({ value, onSquareClick }: Props) {
+ function Square({ className, value, onSquareClick }: Props) {
  return (
-   <button className="square" onClick={onSquareClick}>
+   <button className={className} onClick={onSquareClick}>
      {value}
    </button>
  );
}
```

次に，勝者と勝利につながった3マスを特定する機能を実装します．
これは関数`calculateWinner`の`return`だけ改修します．
```diff_tsx: 4.tsx
function calculateWinner(squares: string[]) {
...
  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 { player: squares[a], line: [a, b, c] };
    }
  }
  return null;
}
```

そして，引き分け時に特別なメッセージを表示する機能を実装します．
方針は，以下の通りです．
1. 初期値`let status = "Draw"`とする．
2. 勝者がいた場合（関数`calculateWinner`の出力がnullでない場合），`status`を上書きする．
3. 空きマスがある場合（変数`squares`に`null`がある場合），`status`を上書きする．

3.は，2.を満たさない場合のみ考えます．
変数`squares`が`string[]`型の場合，`any[]`型に変更すると空きマスが簡単に確認できします．

最終的に，関数`Board`は以下になります．
```diff_tsx: 4.tsx
function Board({ xIsNext, squares, onPlay }: BoardProps) {
...
- const winner = calculateWinner(squares);
- const status = winner ?
-   "Winner: " + winner :
-   "Next player: " + (xIsNext ? "X" : "O");
+ // 4. 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).
+ let status = "Draw", line: number[] = [];
+ const win = calculateWinner(squares);
+ if (win) {
+   status = "Winner: " + win.player;
+   line = win.line
+ } else if (squares.includes(null)) {
+   status = "Next player: " + (xIsNext ? "X" : "O");
+ }

  // 2. Rewrite Board to use two loops to make the squares instead of hardcoding them.
  const board = [...Array(3)].map((_, i) => {
    let threeSquares = [...Array(3)].map((_, j) => {
      let n = 3 * i + j;
      return <Square
        key={n}
+       className={"square " + (line.includes(n) ? "win" : null)}
        onSquareClick={() => handleClick(n)}
        value={squares[n]}
      />
    });
    return <div key={i} className="board-row">{threeSquares}</div>
  });
...
```


# 5.  着手履歴リストで、各着手の場所を (row, col) という形式で表示する。[5.tsx](https://github.com/tomtkg/React-Tutorial/blob/main/src/tutorialExtra/5.tsx)
これは，`const moves = history.map((squares, move) => {...}`で作成する着手履歴リストの表示を変更する課題です．
最初に，`(row, col)`文字列を格納する変数`location`を用意し，それを表示するようにプログラムを変更します．
```diff_tsx: 5.tsx
const moves = history.map((_squares, move) => {
+   // 5. Display the location for each move in the format (row, col) in the move history list.
+   let location = '';
+
    // 1. For the current move only, show “You are at move #…” instead of a button.
    if (move === currentMove) {
      return <li key={move}> {/* assign proper keys */}
-       <b>{"You are at move #" + move}</b>
+       <b>{"You are at move #" + move + location}</b>
      </li>
    }
    const description = move > 0 ?
-     'Go to move #' + move :
+     'Go to move #' + move + location :
      'Go to game start';
    return (
      <li key={move}> {/* assign proper keys */}
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });
```
次に，変数`location`を変更します．
このとき，ゲームスタート時の表示は，`You are at move #0`から変更しません．
その後の表示は，`You are at move #3 (3,1)`や`Go to move #5 (2,3)`が出るようにします．
```diff_tsx: 5.tsx
- const moves = history.map((_squares, move) => {
+ const moves = history.map((squares, move) => {
    // 5. Display the location for each move in the format (row, col) in the move history list.
    let location = '';
+   if (move > 0) {
+     const index = history[move - 1].findIndex((e, i) => e !== squares[i]);
+     location = ' (' + (index % 3 + 1) + ',' + (Math.floor(index / 3) + 1) + ')';
+   }
```
まず，1つ前の`history`を確認して，内容に差分がある`index`を特定します．
`index`は，0 ～ 8のいずれかの値になります．
そして，`index`から着手場所の位置を計算し，表示したい文字列で`location`を上書きします．

その他の実装方法として，着手場所の位置をメモリに保存する方法が考えられます．
自分は大規模な改修をしたくなかったので，既に保存してある情報を活用し，`const moves = history.map((squares, move) => {...}`の中だけ改修する方法を採用しました．

# 終わりに
この記事では，「React-Tutorial: Tic-Tac-Toe / 三目並べ」の追加課題を解説しました．
追加課題は，正解が無いため実装に苦労しました．
最後は，成果をweb上に公開する方法を解説して「React-Tutorial: Tic-Tac-Toe / 三目並べ」を修了したいと思います．

関連記事:

https://qiita.com/tomtkg/items/a489006b85f5fb9896aa

https://qiita.com/tomtkg/items/3e6173a231fdbe463302