以前の記事で、React Docs(Beta)のチュートリアルで作った三目並べゲームを、TypeScript化しました。
React Docs(Beta)のチュートリアルの最後には、以下の発展問題があります。
※DeepLで翻訳
時間があるときや、新しいReactのスキルを練習したいときに、三目並べゲームにできる改善のアイデアを、難易度の高い順にいくつか挙げておきます。
- 現在の手に対してのみ、ボタンの代わりに「You are at move #...」と表示する。
- 碁盤をハードコードするのではなく、2つのループでマスを作るように書き換える。
- トグルボタンを追加して、手を昇順または降順に並べ替えられるようにする
- 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する (誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する)。
- 着手履歴のリストに各手順の位置を(col, row)の形式で表示する。
https://beta.reactjs.org/learn/tutorial-tic-tac-toe#wrapping-up
この記事は、React Docs(Beta)のチュートリアルの発展問題をやってみたメモです。
メモ
1. 現在の手に対してのみ、ボタンの代わりに「You are at move #...」と表示する。
currentMoveで現在の手を記録していますので、以下のように条件分岐を追加することで対応しました。
return (
<li key={move}>
{move === currentMove
? `You are at move #${currentMove}`
: <button onClick={() => jumpTo(move)}>{ description }</button>
}
</li>
)
以下コード差分です。
2. 碁盤をハードコードするのではなく、2つのループでマスを作るように書き換える。
div用のfor文とSquareコンポーネント用のfor文を2つ追加し、配列にそれぞれpushする形にしました。
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 }
</>
);
以下コード差分です。
3. トグルボタンを追加して、手を昇順または降順に並べ替えられるようにする
昇順と降順を制御するbooleanのstateisAsc
を追加
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)
トグルボタンを設置し、stateisAsc
によって並び順を変更できるようにしました。
初めolタグのreversed属性を知らなかったので、リストの数字を変更するのに少しハマりました。
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>
以下コード差分です。
4. 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する (誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する)。
4.1 誰かが勝ったとき、その原因となった3つのマスをハイライト表示する
勝った3つのマスはcalculateWinner関数で判定できます。変更前はsquares[a]
だけ返していましたが、オブジェクトに変更して勝者player
と3つのマスline
を返すようにします。
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なら勝因となった3つのマスと判別できるようにしました。
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>)
}
以下コード差分です。
4.2 誰も勝てなかったときは、結果が引き分けであることをメッセージで表示する
引き分けの条件は、「winnerがfalse」かつ「squaresが全て埋まっている(nullの値が無い)」であるため、else ifで条件を追加し、Drawと表示されるようにしました。
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")
}
以下コード差分です。
5. 着手履歴のリストに各手順の位置を(col, row)の形式で表示する。
手の位置を記録するstatelocation
を作成し、handlePlay関数で更新されるようにしました。handlePlay関数は、子のBoardコンポーネントのhandleClick関数で呼ばれます。
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を表示しています。
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'
}
以下コード差分です。