1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-08-13

この記事では,以下のサイトの追加(Extra)課題を解説をします.

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

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

はじめに

取り組む追加課題は,以下の5つです.

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

これらの課題は,難易度の低い順にリストアップされているので,素直に上から順番に取り組みます.
リファクタリングしたプログラム tutorialExtra/0.tsx を基に,追加課題を1つずつ実装します.

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

1. 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。1.tsx

これは,ボタン表示の一部を変更する課題です.
まず,ボタン表示に関するプログラムを特定します.
function Game内のconst movesが該当します.
const moves: JSX.Element[]なので,movesは配列です.
その一要素(現在の着手の部分)だけ,ボタンではないJSX.Elementに変更すれば良いです.

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

これは,function Boardreturn部分を書き直すリファクタリング課題です.
forループではなく,Arraymapを利用して実装するのが良いでしょう.
以下は,JavaScriptで連番の配列を生成するプログラムです.

[...Array(5)].map((_, i) => i) //=> [ 0, 1, 2, 3, 4 ]

これを応用して,課題を実装します.

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

threeSquaresboardJSX.Element[]型の変数です.
その要素にkeyが含まれていないと警告が出ます.

3. 手順を昇順または降順でソートできるトグルボタンを追加する。3.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 falseboolean型変数で管理するのが良いです.
変数名は,isXXXが良いです.

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

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

4. どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。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を以下のように改修します.

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マスを特定する機能を実装します.
これは関数calculateWinnerreturnだけ改修します.

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. 空きマスがある場合(変数squaresnullがある場合),statusを上書きする.

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

最終的に,関数Boardは以下になります.

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

これは,const moves = history.map((squares, move) => {...}で作成する着手履歴リストの表示を変更する課題です.
最初に,(row, col)文字列を格納する変数locationを用意し,それを表示するようにプログラムを変更します.

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)が出るようにします.

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 / 三目並べ」を修了したいと思います.

関連記事:

1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?