React公式チュートリアル追加課題の実装に関してまとめます。前回の記事で関数コンポーネント化を行った後の続きです。まだの方は先に確認して頂ければと思います。
https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b
追加実装の流れ
- 公式チュートリアルのタイムトラベル機能実装まで完了(https://ja.reactjs.org/tutorial/tutorial.html#wrapping-up)
- モジュール化
- 関数コンポーネントに書き換え
- 追加課題の実装 ⇦この記事
追加課題1 解説
課題1の内容は、履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示するです。この課題を実装するには次の2つのことを実現する必要があります。
- 着手の位置をstateで保持する
- (col, row) のフォーマットで表示する
着手の位置や、それを表示する履歴情報を持っているのはGameコンポーネントです。では順番にGameコンポーネントに修正を加えます。
着手の位置をstateで保持する
ボードが要素9つの配列になってるので、そのインデックスを着手の位置として保持しようと思います。最終的に(col, row) のフォーマットで表示する必要があるものの、インデックスから計算で導き出すことができるので、インデックスのみをstateに保持させます。
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';
// 以下変更無し
ここまで修正を終えるとこのような結果になると思います。あとは表示を修正すれば完成です。
(col, row) のフォーマットで表示する
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関数を使うことも可能です。
以上で課題1は終了です。このような結果になります。
追加課題2 解説
課題2の内容は、着手履歴のリスト中で現在選択されているアイテムをボールドにするです。この課題を実装するには次の2つのことを実現する必要があります。
- 履歴の表示をボールドにする
- 現在選択されているアイテムのみをボールドにする
履歴の表示をボールドにする
まずは着手履歴リストのbuttonタグにclassを追加します。
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に追加します。
/* 追加 */
.bold {
font-weight: bold;
}
履歴の表示が全てボールドになります。
現在選択されているアイテムのみをボールドにする
最後に現在選択されているアイテムのみボールドに修正していきます。現在選択されているアイテムを判定するにはstemNumberが使えます。
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>
);
});
// 以下変更無し
以上で課題2が終了です。
追加課題3 解説
課題3の内容は、Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換えるです。この課題は繰り返し文を利用して実装します。まずは一部だけfor文で書き換えます。
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の箇所を追加及び修正しました。しかしこの時点で警告が出ます。
keyが必要なので追加します。
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文で書き換えていきます。
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文を追加してネストさせます。
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;
これでほぼ完成ですが、ここで再び警告が出ます。
先程と同様にkeyを追加すると警告が消えます。
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の内容は、着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加するです。次の順番で実装していきます。
- 現在は昇順で表示されているので、降順で表示させる
- 昇順と降順を判定するためのstateを追加
- ボタンを追加し、ボタン押下で並べ替えができるようにする
降順で表示させる
降順で表示させるにはreverse()メソッドを用いてmovesの順序を入れ替えることで実現できます。
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の順序入れ替え
// 以下変更無し
昇順と降順を判定するためのstateを追加
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();
}
// 以下変更無し
ボタンを追加し、ボタン押下で並べ替えができるようにする
まずはボタンを追加します。
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>
);
// 以下変更無し
ボタンを追加しましたが、まだ押下しても何も変化しません。最後にボタンを押下した時にstateの値が変わるようイベントを追加します。
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 つのマス目をハイライトするです。次の順番で実装していきます。
- calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする
- 3つのマスの情報を用いて該当箇所をハイライトする
calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする
calculateWinner関数の中身とそれを呼び出していた箇所を修正します。
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コンポーネントに渡します。
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コンポーネントに渡します。
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コンポーネントを修正します。
const Square = (props) => {
const className = props.isHighlight ? 'square-highlight' : 'square';
return (
<button className={className} onClick={props.onClick}>
{props.value}
</button>
);
};
export default Square;
最後に対応する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;
}
追加課題6 解説
課題6の内容は、どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示するです。次の順番で実装していきます。
- calculateWinner関数で引き分けかどうかの情報も返すようにする
- 引き分けの場合、引き分けのメッセージを表示する
calculateWinner関数で引き分けかどうかの情報も返すようにする
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;
引き分けの場合、引き分けのメッセージを表示する
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のメッセージが表示されるようになります。
まとめ
以上で全ての実装が終了です。
最後に最終結果のコードがこちらです。
import ReactDOM from 'react-dom';
import './index.css';
import Game from './Game.js';
ReactDOM.render(<Game />, document.getElementById("root"));
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;
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;
const Square = (props) => {
const className = props.isHighlight ? 'square-highlight' : 'square';
return (
<button className={className} onClick={props.onClick}>
{props.value}
</button>
);
};
export default Square;
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;
}