LoginSignup
1
0

More than 1 year has passed since last update.

React チュートリアル発展 追加課題

Posted at

React公式チュートリアル追加課題の実装に関してまとめます。前回の記事で関数コンポーネント化を行った後の続きです。まだの方は先に確認して頂ければと思います。
https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b

追加実装の流れ

追加課題1 解説

課題1の内容は、履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示するです。この課題を実装するには次の2つのことを実現する必要があります。

  1. 着手の位置をstateで保持する
  2. (col, row) のフォーマットで表示する

着手の位置や、それを表示する履歴情報を持っているのはGameコンポーネントです。では順番にGameコンポーネントに修正を加えます。

着手の位置をstateで保持する

ボードが要素9つの配列になってるので、そのインデックスを着手の位置として保持しようと思います。最終的に(col, row) のフォーマットで表示する必要があるものの、インデックスから計算で導き出すことができるので、インデックスのみをstateに保持させます。

Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null // 着手の位置を保持する為pointを追加
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i // pointにインデックスを代入
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const desc = move ?
      'Go to move #' + move + '(' + step.point + ')' : // 確認の為着手位置を表示
      'Go to game start';
  // 以下変更無し

kadai1.jpg

ここまで修正を終えるとこのような結果になると思います。あとは表示を修正すれば完成です。

(col, row) のフォーマットで表示する

Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1; // colを算出
    const row = (step.point / 3 + 1) | 0; // rowを算出
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; // 指定のフォーマットに修正
    const desc = move ? goToMove : 'Go to game start'; // 1行に修正
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });
  // 以下変更無し

colとrowを算出した後、(col, row) のフォーマットで表示させています。rowに関してはビット論理和を用いて小数点以下を切り捨てています。ビット論理和の代わりにMath.floor関数を使うことも可能です。

kadai1-2.jpg

以上で課題1は終了です。このような結果になります。

追加課題2 解説

課題2の内容は、着手履歴のリスト中で現在選択されているアイテムをボールドにするです。この課題を実装するには次の2つのことを実現する必要があります。

  1. 履歴の表示をボールドにする
  2. 現在選択されているアイテムのみをボールドにする

履歴の表示をボールドにする

まずは着手履歴リストのbuttonタグにclassを追加します。

Game.js
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={'bold'} // class追加
        >
          {desc}
        </button>
      </li>
    );
  });
  // 以下変更無し

追加したclassに対応する記述をcssに追加します。

index.css
/* 追加 */
.bold {
  font-weight: bold;
}

kadai2-1.jpg

履歴の表示が全てボールドになります。

現在選択されているアイテムのみをボールドにする

最後に現在選択されているアイテムのみボールドに修正していきます。現在選択されているアイテムを判定するにはstemNumberが使えます。

Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          // 現在選択されているアイテムだけclassNameをboldにする
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });
  // 以下変更無し

kadai2-2.jpg

以上で課題2が終了です。

追加課題3 解説

課題3の内容は、Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換えるです。この課題は繰り返し文を利用して実装します。まずは一部だけfor文で書き換えます。

Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
      />
    );
  };

  const maxCol = 3;
  const rowBoard1 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 0 + col;
    rowBoard1.push(renderSquare(index));
  }

  return (
    <div>
      <div className="board-row">
        {rowBoard1}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};

export default Board;

rowBoard1の箇所を追加及び修正しました。しかしこの時点で警告が出ます。
kadai3-1.jpg
keyが必要なので追加します。

Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i} // key追加
      />
    );
  };
// 以下変更無し

これで警告も消えたので先程と同様にfor文で書き換えていきます。

Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const maxCol = 3;
  const rowBoard1 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 0 + col;
    rowBoard1.push(renderSquare(index));
  }

  const rowBoard2 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 1 + col;
    rowBoard2.push(renderSquare(index));
  }

  const rowBoard3 = [];
  for (let col = 0; col < maxCol; col++) {
    const index = maxCol * 2 + col;
    rowBoard3.push(renderSquare(index));
  }

  return (
    <div>
      <div className="board-row">
        {rowBoard1}
      </div>
      <div className="board-row">
        {rowBoard2}
      </div>
      <div className="board-row">
        {rowBoard3}
      </div>
    </div>
  );
};

export default Board;

明らかに冗長なのでさらにfor文を追加してネストさせます。

Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      rowBoard.push(renderSquare(index));
    }
    squareBoard.push(
      <div className="board-row">
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;

これでほぼ完成ですが、ここで再び警告が出ます。
kadai3-2.jpg
先程と同様にkeyを追加すると警告が消えます。

Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      rowBoard.push(renderSquare(index));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row} // key追加
      >
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;

以上で課題3が終了です。

追加課題4 解説

課題4の内容は、着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加するです。次の順番で実装していきます。

  1. 現在は昇順で表示されているので、降順で表示させる
  2. 昇順と降順を判定するためのstateを追加
  3. ボタンを追加し、ボタン押下で並べ替えができるようにする

降順で表示させる

降順で表示させるにはreverse()メソッドを用いてmovesの順序を入れ替えることで実現できます。

Game.js
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  moves.reverse(); // movesの順序入れ替え
// 以下変更無し

kadai4-1.jpg

昇順と降順を判定するためのstateを追加

Game.js
const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false); // state追加

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  // stateの値で昇順と降順を判定し条件分岐
  if (movesOrder) {
    moves.reverse();
  }
// 以下変更無し

ボタンを追加し、ボタン押下で並べ替えができるようにする

まずはボタンを追加します。

Game.js
  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        {/* ボタン追加 */}
        <button>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
// 以下変更無し

kadai4-2.jpg
ボタンを追加しましたが、まだ押下しても何も変化しません。最後にボタンを押下した時にstateの値が変わるようイベントを追加します。

Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        {/* イベント追加 */}
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

const calculateWinner = (squares) => {
  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 squares[a];
    }
  }
  return null;
};

export default Game;

以上で課題4が終了です。

追加課題5 解説

課題5の内容は、どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトするです。次の順番で実装していきます。

  1. calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする
  2. 3つのマスの情報を用いて該当箇所をハイライトする

calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする

calculateWinner関数の中身とそれを呼び出していた箇所を修正します。

Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares).winner || squares[i]) { // winnerを参照するよう修正
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares).winner; // winnerを参照するよう修正
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

const calculateWinner = (squares) => {
  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]
  ];

  // 勝者とマスの情報をもつオブジェクトを用意
  const result = {
    winner: null,
    winLine: []
  }

  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]) {
      result.winner = squares[a]; // 勝者をresultに代入
      result.winLine = result.winLine.concat(lines[i]); // 勝利に繋がったマスの情報をresultに代入
    }
  }
  return result; // resultを返す
};

export default Game;

3つのマスの情報を用いて該当箇所をハイライトする

calculateWinner関数から返るマスの情報をまずはBoardコンポーネントに渡します。

Game.js
  const current = history[stepNumber];
  const {winner, winLine} = calculateWinner(current.squares); // winnerとwinLineを分割代入
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }

  return (
    <div className="game">
      <div className="game-board">
        {/* propsにwinLineを追加 */}
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
          winLine={winLine}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
// 以下変更無し

これでBoardコンポーネントでwinLineが参照できるようになります。次にBoardコンポーネントを修正します。propsで受け取ったwinLineにindexが含まれるかどうか確認し、結果をSquareコンポーネントに渡します。

Board.js
const Board = (props) => {
  const renderSquare = (i, isHighlight) => { // isHighlightでハイライト有無の情報を受け取る
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
        // props追加
        isHighlight={isHighlight}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      const isHighlight = props.winLine.includes(index); // winLineにindexが含まれるかどうか確認
      rowBoard.push(renderSquare(index, isHighlight));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row}
      >
        {rowBoard}
      </div>
    );
  }
// 以下変更無し

これでSquareコンポーネントでハイライト有無の情報が参照できるようになります。その情報を用いてハイライトするようSquareコンポーネントを修正します。

Square.js
const Square = (props) => {
  const className = props.isHighlight ? 'square-highlight' : 'square';
  return (
    <button className={className} onClick={props.onClick}>
      {props.value}
    </button>
  );
};

export default Square;

最後に対応するcssを追加して完了です。

index.css
/* 追加 */
.square-highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

kadai5-1.jpg

追加課題6 解説

課題6の内容は、どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示するです。次の順番で実装していきます。

  1. calculateWinner関数で引き分けかどうかの情報も返すようにする
  2. 引き分けの場合、引き分けのメッセージを表示する

calculateWinner関数で引き分けかどうかの情報も返すようにする

Game.js
const calculateWinner = (squares) => {
  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]
  ];
  const result = {
    winner: null,
    winLine: [],
    isDraw: false // 追加
  }
  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]) {
      result.winner = squares[a];
      result.winLine = result.winLine.concat(lines[i]);
    }
  }
  // 勝者が決まっておらず、かつマスが全て埋まっている時が引き分けになる
  if (result.winner === null && !squares.includes(null)) {
    result.isDraw = true;
  }
  return result;
};

export default Game;

引き分けの場合、引き分けのメッセージを表示する

Game.js
  const current = history[stepNumber];
  const {winner, winLine, isDraw} = calculateWinner(current.squares); // isDrawも参照できるように書き換え
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  // 引き分けだった場合はstatusを変更
  if (isDraw) {
    status = 'Draw';
  }
// 以下変更無し

これで引き分けの場合はDrawのメッセージが表示されるようになります。

kadai6-1.jpg

まとめ

以上で全ての実装が終了です。
最後に最終結果のコードがこちらです。

index.js
import ReactDOM from 'react-dom';
import './index.css';
import Game from './Game.js';

ReactDOM.render(<Game />, document.getElementById("root"));
Game.js
import {useState} from 'react';
import Board from './Board';

const Game = () => {
  const [history, setHistory] = useState(
    [
      {
        squares: Array(9).fill(null),
        point: null
      }
    ]
  );
  const [stepNumber, setStepNumber] = useState(0);
  const [xIsNext, setXIsNext] = useState(true);
  const [movesOrder, setMovesOrder] = useState(false);

  const handleClick = (i) => {
    const copyHistory = history.slice(0, stepNumber + 1);
    const current = copyHistory[copyHistory.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares).winner || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? "X" : "O";
    setHistory(
      copyHistory.concat(
        [
          {
            squares: squares,
            point: i
          }
        ]
      )
    );
    setStepNumber(copyHistory.length);
    setXIsNext(!xIsNext);
  };

  const jumpTo = (step) => {
    setStepNumber(step);
    setXIsNext((step % 2) === 0);
  };

  const current = history[stepNumber];
  const {winner, winLine, isDraw} = calculateWinner(current.squares);
  const moves = history.map((step, move) => {
    const col = step.point % 3 + 1;
    const row = (step.point / 3 + 1) | 0;
    const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')';
    const desc = move ? goToMove : 'Go to game start';
    return (
      <li key={move}>
        <button
          onClick={() => jumpTo(move)}
          className={move === stepNumber ? 'bold' : ''}
        >
          {desc}
        </button>
      </li>
    );
  });

  if (movesOrder) {
    moves.reverse();
  }

  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }
  if (isDraw) {
    status = 'Draw';
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board
          squares={current.squares}
          onClick={i => handleClick(i)}
          winLine={winLine}
        />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <button onClick={() => {setMovesOrder(!movesOrder)}}>
          {'ASK⇔DESK'}
        </button>
        <ol>{moves}</ol>
      </div>
    </div>
  );
};

const calculateWinner = (squares) => {
  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]
  ];
  const result = {
    winner: null,
    winLine: [],
    isDraw: false
  }
  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]) {
      result.winner = squares[a];
      result.winLine = result.winLine.concat(lines[i]);
    }
  }
  if (result.winner === null && !squares.includes(null)) {
    result.isDraw = true;
  }
  return result;
};

export default Game;
Board.js
import Square from './Square';

const Board = (props) => {
  const renderSquare = (i, isHighlight) => {
    return (
      <Square
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
        key={'index-' + i}
        isHighlight={isHighlight}
      />
    );
  };

  const squareBoard = [];
  const maxRow = 3;
  const maxCol = 3;
  for (let row = 0; row < maxRow; row++) {
    const rowBoard = [];
    for (let col = 0; col < maxCol; col++) {
      const index = maxCol * row + col;
      const isHighlight = props.winLine.includes(index);
      rowBoard.push(renderSquare(index, isHighlight));
    }
    squareBoard.push(
      <div
        className="board-row"
        key={'row-' + row}
      >
        {rowBoard}
      </div>
    );
  }

  return (
    <div>
      {squareBoard}
    </div>
  );
};

export default Board;
Square.js
const Square = (props) => {
  const className = props.isHighlight ? 'square-highlight' : 'square';
  return (
    <button className={className} onClick={props.onClick}>
      {props.value}
    </button>
  );
};

export default Square;
index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square__highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.bold {
  font-weight: bold;
}

.square-highlight {
  background: yellow;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}
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