LoginSignup
1
0

More than 1 year has passed since last update.

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

Posted at

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

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で現在の手を記録していますので、以下のように条件分岐を追加することで対応しました。

src/components/Game.tsx
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する形にしました。

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

以下コード差分です。

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

昇順と降順を制御するbooleanのstateisAscを追加

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)

トグルボタンを設置し、stateisAscによって並び順を変更できるようにしました。
初めolタグのreversed属性を知らなかったので、リストの数字を変更するのに少しハマりました。

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>

以下コード差分です。

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

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

勝った3つのマスはcalculateWinner関数で判定できます。変更前はsquares[a]だけ返していましたが、オブジェクトに変更して勝者playerと3つのマスlineを返すようにします。

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なら勝因となった3つのマスと判別できるようにしました。

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>)
}

以下コード差分です。

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

引き分けの条件は、「winnerがfalse」かつ「squaresが全て埋まっている(nullの値が無い)」であるため、else ifで条件を追加し、Drawと表示されるようにしました。

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")
  }

以下コード差分です。

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

手の位置を記録するstatelocationを作成し、handlePlay関数で更新されるようにしました。handlePlay関数は、子のBoardコンポーネントのhandleClick関数で呼ばれます。

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を表示しています。

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'
    }

以下コード差分です。

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