reactのチュートリアルで三目並べを作ったので、その応用として立体四目並べを作ってみました。
reactに慣れるという目的で作ったのでリファクタリングしてません。汚いコードです。
もし立体四目作ろうと思っているという方の参考になれば幸いです。
reactの三目並べチュートリアル
https://ja.react.dev/learn/tutorial-tic-tac-toe
これに変更を加えて行きます。
立体四目並べの説明
こういうの。平面(x,y軸)の四目並べではなく、立体(x,y,z軸)の四目並べ。
完成したもの
Xと○を交互に置いて行って、先に四目並べた方を判定して、勝ちとする。
判定は主に以下の4つ
左から3つ目の立体✖️斜めに関しては、見づらいのでイメージ図貼ります
手順
1.一般的な四目並べを作る
2.それぞれで独立したデータを持つボードを4枚用意する
3.前の層にマークがあれば、現在の層への操作を許可する
4.4枚目の層で、立体的に判定を行う
5.4つのboardのうち、いずれかで勝者が決まった場合、その後の操作を無効にする
6.Winnerが決まっていれば、操作をさせないようにする
一般的な四目並べを作る
立体四目並べを作るために、まず1枚のボード上で「平面の四目並べ」をつくる。
チュートリアルのコードを元に以下2点を修正していく。
・マスを3✖️3から4✖️4に増やす
・判定するマスを調整する
App.jsのコード
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 揃ったかの判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
それぞれで独立したデータを持つボードを4枚用意する
立体四目には4つの層があるため、4枚のボードを用意する必要がある。
チュートリアルのコードを元に以下2点を修正していく。
・ボードを1枚から4枚に増やす
・各ボードに独自のデータを持たせる
App.jsのコード
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
{/* <div className="status">{status}</div> */}
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history1, setHistory1] = useState([Array(9).fill(null)]); // 1枚目のボード用
const [history2, setHistory2] = useState([Array(9).fill(null)]); // 2枚目のボード用
const [history3, setHistory3] = useState([Array(9).fill(null)]); // 3枚目のボード用
const [history4, setHistory4] = useState([Array(9).fill(null)]); // 4枚目のボード用
const currentSquares1 = history1[history1.length - 1]; // 1枚目のボード用
const currentSquares2 = history2[history2.length - 1]; // 2枚目のボード用
const currentSquares3 = history3[history3.length - 1]; // 3枚目のボード用
const currentSquares4 = history4[history4.length - 1]; // 4枚目のボード用
// 1枚目のボード用
function handlePlay1(nextSquares) {
setHistory1([...history1, nextSquares]);
setXIsNext(!xIsNext);
}
// 2枚目のボード用
function handlePlay2(nextSquares) {
setHistory2([...history2, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay3(nextSquares) {
setHistory3([...history3, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay4(nextSquares) {
setHistory4([...history4, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
/>
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 揃ったかの判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
前の層にマークがあれば、現在の層への操作を許可する
以下の処理を追加します
・Boadに、1つ下の層のデータを渡す
・Boad内の処理で、iがクリックされたとき、一つ下の層のiがなければ、操作を無効とする
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
// 1つ目のボードに関しては最下層であるため、
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
prevSquares={currentSquares1} // 1つ下の層の状態を渡す
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
prevSquares={currentSquares2} // 1つ下の層の状態を渡す
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
prevSquares={currentSquares3} // 1つ下の層の状態を渡す
/>
Boad内の処理
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
// ========ここから追加========
if (prevSquares && !prevSquares[i]) {
// クリックされたマスの1つ前の層にマークがなければ無効
return;
}
// ========ここまで追加========
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
App.jsの全体のコード
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay, prevSquares }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
if (prevSquares && !prevSquares[i]) {
// クリックされたマスの1つ前の層にマークがなければ無効
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
{/* <div className="status">{status}</div> */}
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history1, setHistory1] = useState([Array(9).fill(null)]); // 1枚目のボード用
const [history2, setHistory2] = useState([Array(9).fill(null)]); // 2枚目のボード用
const [history3, setHistory3] = useState([Array(9).fill(null)]); // 3枚目のボード用
const [history4, setHistory4] = useState([Array(9).fill(null)]); // 4枚目のボード用
const currentSquares1 = history1[history1.length - 1]; // 1枚目のボード用
const currentSquares2 = history2[history2.length - 1]; // 2枚目のボード用
const currentSquares3 = history3[history3.length - 1]; // 3枚目のボード用
const currentSquares4 = history4[history4.length - 1]; // 4枚目のボード用
// 1枚目のボード用
function handlePlay1(nextSquares) {
setHistory1([...history1, nextSquares]);
setXIsNext(!xIsNext);
}
// 2枚目のボード用
function handlePlay2(nextSquares) {
setHistory2([...history2, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay3(nextSquares) {
setHistory3([...history3, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay4(nextSquares) {
setHistory4([...history4, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
prevSquares={currentSquares1} // 1枚目の状態を渡す
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
prevSquares={currentSquares2} // 2枚目の状態を渡す
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
prevSquares={currentSquares3} // 3枚目の状態を渡す
/>
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 揃ったかの判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
4枚目の層で、立体的に判定を行う
以下の処理を追加
・4層目にマークされたあと、立体的に4つ並んでいるかの判定を行う関数を追加
・board内で、追加した関数を呼び出す
// 立体的な判定を行う
function calculateWinner3D(squares, otherSquares) {
const lines = [
// 各層で横に揃うライン
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 各層で横に揃うライン(逆方向)
[3, 2, 1, 0],
[7, 6, 5, 4],
[11, 10, 9, 8],
[15, 14, 13, 12],
// 各層で縦に揃うライン
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 各層で縦に揃うライン(逆方向)
[12, 8, 4, 0],
[13, 9, 5, 1],
[14, 10, 6, 2],
[15, 11, 7, 3],
// 各層で斜めに揃うライン
[0, 5, 10, 15],
[3, 6, 9, 12],
// 各層で斜めに揃うライン(逆方向)
[15, 10, 5, 0],
[12, 9, 6, 3],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
// console.log("===確かめ===");
// console.log("4層目:squares[a]:", squares[a]);
// console.log("4層目:otherSquares[2][b]:", otherSquares[2][b]);
// console.log("4層目:otherSquares[1][c]:", otherSquares[1][c]);
// console.log("4層目:otherSquares[0][d]:", otherSquares[0][d]);
if (
squares[a] && // 4層目の現在のボード
squares[a] === otherSquares[2][b] && // 3層目
squares[a] === otherSquares[1][c] && // 2層目
squares[a] === otherSquares[0][d] // 1層目
) {
return squares[a];
}
}
// 各層の同じマスにマークがある判定
for (let i = 0; i < squares.length; i++) {
if (
squares[i] && // 4層目
squares[i] === otherSquares[0][i] && // 1層目
squares[i] === otherSquares[1][i] && // 2層目
squares[i] === otherSquares[2][i] // 3層目
) {
return squares[i];
}
}
return null;
}
App.js前コード
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay, prevSquares, otherSquares }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
if (prevSquares && !prevSquares[i]) {
// クリックされたマスの1つ前の層にマークがなければ無効
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
let winner = calculateWinner(squares);
if (!winner && otherSquares) {
// 平面の判定で勝者が決まらず、4層目の場合は立体の判定を行う
winner = calculateWinner3D(squares, otherSquares);
}
let status;
if (winner) {
status = "Winner: " + winner;
}
return (
<>
{status && <div className="status">判定: {status}</div>}
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history1, setHistory1] = useState([Array(9).fill(null)]); // 1枚目のボード用
const [history2, setHistory2] = useState([Array(9).fill(null)]); // 2枚目のボード用
const [history3, setHistory3] = useState([Array(9).fill(null)]); // 3枚目のボード用
const [history4, setHistory4] = useState([Array(9).fill(null)]); // 4枚目のボード用
const currentSquares1 = history1[history1.length - 1]; // 1枚目のボード用
const currentSquares2 = history2[history2.length - 1]; // 2枚目のボード用
const currentSquares3 = history3[history3.length - 1]; // 3枚目のボード用
const currentSquares4 = history4[history4.length - 1]; // 4枚目のボード用
// 1枚目のボード用
function handlePlay1(nextSquares) {
setHistory1([...history1, nextSquares]);
setXIsNext(!xIsNext);
}
// 2枚目のボード用
function handlePlay2(nextSquares) {
setHistory2([...history2, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay3(nextSquares) {
setHistory3([...history3, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay4(nextSquares) {
setHistory4([...history4, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
prevSquares={currentSquares1} // 1枚目の状態を渡す
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
prevSquares={currentSquares2} // 2枚目の状態を渡す
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
prevSquares={currentSquares3} // 3枚目の状態を渡す
otherSquares={[currentSquares1, currentSquares2, currentSquares3]}
/>
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 平面的な判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
// 立体的な判定を行う
function calculateWinner3D(squares, otherSquares) {
const lines = [
// 各層で横に揃うライン
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 各層で横に揃うライン(逆方向)
[3, 2, 1, 0],
[7, 6, 5, 4],
[11, 10, 9, 8],
[15, 14, 13, 12],
// 各層で縦に揃うライン
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 各層で縦に揃うライン(逆方向)
[12, 8, 4, 0],
[13, 9, 5, 1],
[14, 10, 6, 2],
[15, 11, 7, 3],
// 各層で斜めに揃うライン
[0, 5, 10, 15],
[3, 6, 9, 12],
// 各層で斜めに揃うライン(逆方向)
[15, 10, 5, 0],
[12, 9, 6, 3],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
// console.log("===確かめ===");
// console.log("4層目:squares[a]:", squares[a]);
// console.log("4層目:otherSquares[2][b]:", otherSquares[2][b]);
// console.log("4層目:otherSquares[1][c]:", otherSquares[1][c]);
// console.log("4層目:otherSquares[0][d]:", otherSquares[0][d]);
if (
squares[a] && // 4層目の現在のボード
squares[a] === otherSquares[2][b] && // 3層目
squares[a] === otherSquares[1][c] && // 2層目
squares[a] === otherSquares[0][d] // 1層目
) {
return squares[a];
}
}
// 各層の同じマスにマークがある判定
for (let i = 0; i < squares.length; i++) {
if (
squares[i] && // 4層目
squares[i] === otherSquares[0][i] && // 1層目
squares[i] === otherSquares[1][i] && // 2層目
squares[i] === otherSquares[2][i] // 3層目
) {
return squares[i];
}
}
return null;
}
現在の時点では、board内で勝者を出しているため、
4枚目の層で勝者が決まっても、1,2,3枚目の層では勝者の情報が渡されておらず、操作できてしまう。
4つのboardのうち、いずれかで勝者が決まった場合、その後の操作を無効にする
以下の処理を行う
・Game(親)コンポーネントに勝者の状態を追加する
・Game(親)コンポーネントに 勝者を更新する関数を作成
・Board(子)コンポーネントを呼び出す際に、勝者を更新する関数を渡す
・Board(子)コンポーネント内で、勝者が決まれば、親コンポーネントの勝者更新関数を呼び出す
これで親コンポーネントが、Winnerの管理をできるようになった。
App.js全体のコード
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({
xIsNext,
squares,
onPlay,
prevSquares,
otherSquares,
onUpdate,
}) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
if (prevSquares && !prevSquares[i]) {
// クリックされたマスの1つ前の層にマークがなければ無効
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
let winner = calculateWinner(squares);
if (!winner && otherSquares) {
// 平面の判定で勝者が決まらず、4層目の場合は立体の判定を行う
winner = calculateWinner3D(squares, otherSquares);
}
let status;
if (winner) {
status = "Winner: " + winner;
onUpdate(winner);
}
return (
<>
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history1, setHistory1] = useState([Array(9).fill(null)]); // 1枚目のボード用
const [history2, setHistory2] = useState([Array(9).fill(null)]); // 2枚目のボード用
const [history3, setHistory3] = useState([Array(9).fill(null)]); // 3枚目のボード用
const [history4, setHistory4] = useState([Array(9).fill(null)]); // 4枚目のボード用
const currentSquares1 = history1[history1.length - 1]; // 1枚目のボード用
const currentSquares2 = history2[history2.length - 1]; // 2枚目のボード用
const currentSquares3 = history3[history3.length - 1]; // 3枚目のボード用
const currentSquares4 = history4[history4.length - 1]; // 4枚目のボード用
// 勝者が決まった際、親コンポーネントで勝者を共有する
[winner, setWinner] = useState(null);
const updateWinner = (newWinner) => {
setWinner(newWinner);
};
// 1枚目のボード用
function handlePlay1(nextSquares) {
setHistory1([...history1, nextSquares]);
setXIsNext(!xIsNext);
}
// 2枚目のボード用
function handlePlay2(nextSquares) {
setHistory2([...history2, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay3(nextSquares) {
setHistory3([...history3, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay4(nextSquares) {
setHistory4([...history4, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
{winner && <div className="winner">判定: {winner}</div>}
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
onUpdate={updateWinner}
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
prevSquares={currentSquares1} // 1枚目の状態を渡す
onUpdate={updateWinner}
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
prevSquares={currentSquares2} // 2枚目の状態を渡す
onUpdate={updateWinner}
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
prevSquares={currentSquares3} // 3枚目の状態を渡す
otherSquares={[currentSquares1, currentSquares2, currentSquares3]}
onUpdate={updateWinner}
/>
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 平面的な判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
// 立体的な判定を行う
function calculateWinner3D(squares, otherSquares) {
const lines = [
// 各層で横に揃うライン
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 各層で横に揃うライン(逆方向)
[3, 2, 1, 0],
[7, 6, 5, 4],
[11, 10, 9, 8],
[15, 14, 13, 12],
// 各層で縦に揃うライン
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 各層で縦に揃うライン(逆方向)
[12, 8, 4, 0],
[13, 9, 5, 1],
[14, 10, 6, 2],
[15, 11, 7, 3],
// 各層で斜めに揃うライン
[0, 5, 10, 15],
[3, 6, 9, 12],
// 各層で斜めに揃うライン(逆方向)
[15, 10, 5, 0],
[12, 9, 6, 3],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
// console.log("===確かめ===");
// console.log("4層目:squares[a]:", squares[a]);
// console.log("4層目:otherSquares[2][b]:", otherSquares[2][b]);
// console.log("4層目:otherSquares[1][c]:", otherSquares[1][c]);
// console.log("4層目:otherSquares[0][d]:", otherSquares[0][d]);
if (
squares[a] && // 4層目の現在のボード
squares[a] === otherSquares[2][b] && // 3層目
squares[a] === otherSquares[1][c] && // 2層目
squares[a] === otherSquares[0][d] // 1層目
) {
return squares[a];
}
}
// 各層の同じマスにマークがある判定
for (let i = 0; i < squares.length; i++) {
if (
squares[i] && // 4層目
squares[i] === otherSquares[0][i] && // 1層目
squares[i] === otherSquares[1][i] && // 2層目
squares[i] === otherSquares[2][i] // 3層目
) {
return squares[i];
}
}
return null;
}
Winnerが決まっていれば、操作をさせないようにする
function Board({
xIsNext,
squares,
onPlay,
prevSquares,
otherSquares,
onUpdate,
// 追加
isWinner,
}) {
function handleClick(i) {
// ====ここから変更====
if (squares[i]) {
// すでにマークされた部分がクリックされたら何もしない
return;
}
if (isWinner != null) {
// 勝者が決まっている場合は何もしない
return;
}
// ====ここまで変更====
// ~省略~
}
gameコンポーネント内
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
onUpdate={updateWinner}
isWinner={winner} // ここ追加
/>
これでロジックは完成!
App.js全文
import { useState } from "react";
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({
xIsNext,
squares,
onPlay,
prevSquares,
otherSquares,
onUpdate,
isWinner,
}) {
function handleClick(i) {
if (squares[i]) {
// すでにマークされた部分がクリックされたら何もしない
return;
}
if (isWinner != null) {
// 勝者が決まっている場合は何もしない
return;
}
if (prevSquares && !prevSquares[i]) {
// クリックされたマスの1つ前の層にマークがなければ無効
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
let winner = calculateWinner(squares);
if (!winner && otherSquares) {
// 平面の判定で勝者が決まらず、4層目の場合は立体の判定を行う
winner = calculateWinner3D(squares, otherSquares);
}
let status;
if (winner) {
status = "Winner: " + winner;
onUpdate(winner);
}
return (
<>
<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)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
</div>
<div className="board-row">
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
</div>
<div className="board-row">
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
<Square value={squares[9]} onSquareClick={() => handleClick(9)} />
<Square value={squares[10]} onSquareClick={() => handleClick(10)} />
<Square value={squares[11]} onSquareClick={() => handleClick(11)} />
</div>
<div className="board-row">
<Square value={squares[12]} onSquareClick={() => handleClick(12)} />
<Square value={squares[13]} onSquareClick={() => handleClick(13)} />
<Square value={squares[14]} onSquareClick={() => handleClick(14)} />
<Square value={squares[15]} onSquareClick={() => handleClick(15)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history1, setHistory1] = useState([Array(9).fill(null)]); // 1枚目のボード用
const [history2, setHistory2] = useState([Array(9).fill(null)]); // 2枚目のボード用
const [history3, setHistory3] = useState([Array(9).fill(null)]); // 3枚目のボード用
const [history4, setHistory4] = useState([Array(9).fill(null)]); // 4枚目のボード用
const currentSquares1 = history1[history1.length - 1]; // 1枚目のボード用
const currentSquares2 = history2[history2.length - 1]; // 2枚目のボード用
const currentSquares3 = history3[history3.length - 1]; // 3枚目のボード用
const currentSquares4 = history4[history4.length - 1]; // 4枚目のボード用
// 勝者が決まった際、親コンポーネントで勝者を共有する
[winner, setWinner] = useState(null);
const updateWinner = (newWinner) => {
setWinner(newWinner);
};
// 1枚目のボード用
function handlePlay1(nextSquares) {
setHistory1([...history1, nextSquares]);
setXIsNext(!xIsNext);
}
// 2枚目のボード用
function handlePlay2(nextSquares) {
setHistory2([...history2, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay3(nextSquares) {
setHistory3([...history3, nextSquares]);
setXIsNext(!xIsNext);
}
// 3枚目のボード用
function handlePlay4(nextSquares) {
setHistory4([...history4, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
{winner && <div className="winner">判定: {winner}</div>}
<div>1枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares1}
onPlay={handlePlay1}
onUpdate={updateWinner}
isWinner={winner}
/>
<div>2枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares2}
onPlay={handlePlay2}
prevSquares={currentSquares1} // 1枚目の状態を渡す
onUpdate={updateWinner}
isWinner={winner}
/>
<div>3枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares3}
onPlay={handlePlay3}
prevSquares={currentSquares2} // 2枚目の状態を渡す
onUpdate={updateWinner}
isWinner={winner}
/>
<div>4枚目</div>
<Board
xIsNext={xIsNext}
squares={currentSquares4}
onPlay={handlePlay4}
prevSquares={currentSquares3} // 3枚目の状態を渡す
otherSquares={[currentSquares1, currentSquares2, currentSquares3]}
onUpdate={updateWinner}
isWinner={winner}
/>
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
// 平面的な判定を行う
function calculateWinner(squares) {
const lines = [
// 横に揃う
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 縦に揃う
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 斜めに揃う
[0, 5, 10, 15],
[3, 6, 9, 12],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] &&
squares[a] === squares[d]
) {
return squares[a];
}
}
return null;
}
// 立体的な判定を行う
function calculateWinner3D(squares, otherSquares) {
const lines = [
// 各層で横に揃うライン
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
// 各層で横に揃うライン(逆方向)
[3, 2, 1, 0],
[7, 6, 5, 4],
[11, 10, 9, 8],
[15, 14, 13, 12],
// 各層で縦に揃うライン
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
// 各層で縦に揃うライン(逆方向)
[12, 8, 4, 0],
[13, 9, 5, 1],
[14, 10, 6, 2],
[15, 11, 7, 3],
// 各層で斜めに揃うライン
[0, 5, 10, 15],
[3, 6, 9, 12],
// 各層で斜めに揃うライン(逆方向)
[15, 10, 5, 0],
[12, 9, 6, 3],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c, d] = lines[i];
// console.log("===確かめ===");
// console.log("4層目:squares[a]:", squares[a]);
// console.log("4層目:otherSquares[2][b]:", otherSquares[2][b]);
// console.log("4層目:otherSquares[1][c]:", otherSquares[1][c]);
// console.log("4層目:otherSquares[0][d]:", otherSquares[0][d]);
if (
squares[a] && // 4層目の現在のボード
squares[a] === otherSquares[2][b] && // 3層目
squares[a] === otherSquares[1][c] && // 2層目
squares[a] === otherSquares[0][d] // 1層目
) {
return squares[a];
}
}
// 各層の同じマスにマークがある判定
for (let i = 0; i < squares.length; i++) {
if (
squares[i] && // 4層目
squares[i] === otherSquares[0][i] && // 1層目
squares[i] === otherSquares[1][i] && // 2層目
squares[i] === otherSquares[2][i] // 3層目
) {
return squares[i];
}
}
return null;
}
これで、勝者がきまれば「判定:XもしくはO」と出て、これ以上の操作ができなくなります。
完成!
さいごに
立体四目を作ることで、
・親から子にデータを渡す
・子から親のデータを更新する
の理解が深まった。
チュートリアルで言われた通りに三目並べを作るのも楽しいけど
自分で考えて立体四目を作るとさらに楽しかったです。
reactの使い方について知るという目標は達成できました。
気が向いたら、リファクタリングするか、UIを見やすくするかしたいです
reactのお作法的に
「もっとこうするといいよ」なコメント、いただけると大変ありがたいです🙏 よろしくお願いします。