この記事では,以下のサイトの追加(Extra)課題を解説をします.
自分が自習時に作成したプログラムとサイトは,以下になります.
「React-Tutorial: Tic-Tac-Toe / 三目並べ」にTypeScriptで取り組みました.
前回の記事では,チュートリアルにおいて自分が分からなかった箇所,詰まった箇所を中心に解説をしました.
今回は,追加課題に対する自分なりの解答を解説します.
(サイトに模範解答の記載はなく,追加課題は複数の方法で実現できます)
はじめに
取り組む追加課題は,以下の5つです.
- 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。
- マス目を全部ハードコードするのではなく、Board を 2 つのループを使ってレンダーするよう書き直す。
- 手順を昇順または降順でソートできるトグルボタンを追加する。
- どちらかが勝利したときに、勝利につながった 3 つのマス目をハイライト表示する。引き分けになった場合は、引き分けになったという結果をメッセージに表示する。
- 着手履歴リストで、各着手の場所を (row, col) という形式で表示する。
これらの課題は,難易度の低い順にリストアップされているので,素直に上から順番に取り組みます.
リファクタリングしたプログラム tutorialExtra/0.tsx を基に,追加課題を1つずつ実装します.
https://github.com/tomtkg/React-Tutorial と https://tomtkg.github.io/React-Tutorialを別で開いて,差分を確認しながら記事を読むことをお勧めします.
1. 現在の着手の部分だけ、ボタンではなく “You are at move #…” というメッセージを表示するようにする。1.tsx
これは,ボタン表示の一部を変更する課題です.
まず,ボタン表示に関するプログラムを特定します.
function Game
内のconst moves
が該当します.
const moves: JSX.Element[]
なので,moves
は配列です.
その一要素(現在の着手の部分)だけ,ボタンではないJSX.Element
に変更すれば良いです.
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 Board
のreturn
部分を書き直すリファクタリング課題です.
for
ループではなく,Array
とmap
を利用して実装するのが良いでしょう.
以下は,JavaScriptで連番の配列を生成するプログラムです.
[...Array(5)].map((_, i) => i) //=> [ 0, 1, 2, 3, 4 ]
これを応用して,課題を実装します.
+ // 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
この課題は,先に実装結果を掲載し,その後に解説をします.
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
この課題を達成するには,以下の3つの機能が必要です.
- マス目を普通表示,ハイライト表示する機能:
Square
(,css
) - 勝者と勝利につながった3マスを特定する機能:
calculateWinner
- 引き分け時に特別なメッセージを表示する機能:
Board
機能毎に実装を考えます.
まず,この課題では「マス目をハイライト表示する」という表示デザインの改修があります.
表示デザインの改修は,App.tsx
で対応するのではなくcss
を改修するのが良いです.
.win { background: #f66 }
をcss
の適当な箇所に追記します.
App.tsx
において,className="square"
を指定すると背景無し(白),className="square win"
を指定すると背景がハイライト(#f66)になります.
汎用性を考えると,className
は外部から指定できると良いです.
関数Square
を以下のように改修します.
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
だけ改修します.
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;
}
そして,引き分け時に特別なメッセージを表示する機能を実装します.
方針は,以下の通りです.
- 初期値
let status = "Draw"
とする. - 勝者がいた場合(関数
calculateWinner
の出力がnullでない場合),status
を上書きする. - 空きマスがある場合(変数
squares
にnull
がある場合),status
を上書きする.
3.は,2.を満たさない場合のみ考えます.
変数squares
がstring[]
型の場合,any[]
型に変更すると空きマスが簡単に確認できします.
最終的に,関数Board
は以下になります.
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
を用意し,それを表示するようにプログラムを変更します.
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)
が出るようにします.
- 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 / 三目並べ」を修了したいと思います.
関連記事: