2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

reactで立体四目並べを作る

Posted at

reactのチュートリアルで三目並べを作ったので、その応用として立体四目並べを作ってみました。
reactに慣れるという目的で作ったのでリファクタリングしてません。汚いコードです。
もし立体四目作ろうと思っているという方の参考になれば幸いです。

reactの三目並べチュートリアル
https://ja.react.dev/learn/tutorial-tic-tac-toe
これに変更を加えて行きます。

この記事では↓ こういうものを作ります
スクリーンショット 2024-09-30 1.38.34.png

立体四目並べの説明

スクリーンショット 2024-09-30 0.57.45.png

こういうの。平面(x,y軸)の四目並べではなく、立体(x,y,z軸)の四目並べ。

完成したもの

Xと○を交互に置いて行って、先に四目並べた方を判定して、勝ちとする。
判定は主に以下の4つ
スクリーンショット 2024-09-30 1.33.16.png

左から3つ目の立体✖️斜めに関しては、見づらいのでイメージ図貼ります
スクリーンショット 2024-09-30 1.20.38.png

手順

1.一般的な四目並べを作る
2.それぞれで独立したデータを持つボードを4枚用意する
3.前の層にマークがあれば、現在の層への操作を許可する
4.4枚目の層で、立体的に判定を行う
5.4つのboardのうち、いずれかで勝者が決まった場合、その後の操作を無効にする
6.Winnerが決まっていれば、操作をさせないようにする

一般的な四目並べを作る

立体四目並べを作るために、まず1枚のボード上で「平面の四目並べ」をつくる。
チュートリアルのコードを元に以下2点を修正していく。
・マスを3✖️3から4✖️4に増やす
・判定するマスを調整する

スクリーンショット 2024-09-29 21.48.43.png

App.jsのコード
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枚のボードを用意する必要がある。
スクリーンショット 2024-09-29 22.27.36.png

チュートリアルのコードを元に以下2点を修正していく。
・ボードを1枚から4枚に増やす
・各ボードに独自のデータを持たせる

スクリーンショット 2024-09-29 22.23.28.png

App.jsのコード
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;
}

前の層にマークがあれば、現在の層への操作を許可する

スクリーンショット 2024-09-29 22.49.52.png

スクリーンショット 2024-09-29 22.53.38.png

以下の処理を追加します
・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内の処理

App.js
  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の全体のコード
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枚目の層で、立体的に判定を行う

スクリーンショット 2024-09-30 0.02.13.png

スクリーンショット 2024-09-30 0.06.49.png

以下の処理を追加
・4層目にマークされたあと、立体的に4つ並んでいるかの判定を行う関数を追加
・board内で、追加した関数を呼び出す

App.js
// 立体的な判定を行う
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前コード
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(子)コンポーネント内で、勝者が決まれば、親コンポーネントの勝者更新関数を呼び出す

スクリーンショット 2024-09-30 0.42.51.png

これで親コンポーネントが、Winnerの管理をできるようになった。

App.js全体のコード
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が決まっていれば、操作をさせないようにする

App.js
function Board({
  xIsNext,
  squares,
  onPlay,
  prevSquares,
  otherSquares,
  onUpdate,
  // 追加
  isWinner,
}) {
  function handleClick(i) {
    // ====ここから変更====
    if (squares[i]) {
      // すでにマークされた部分がクリックされたら何もしない
      return;
    }
    if (isWinner != null) {
      // 勝者が決まっている場合は何もしない
      return;
    }
    // ====ここまで変更====

    // ~省略~
  }

gameコンポーネント内

App.js
        <div>1枚目</div>
        <Board
          xIsNext={xIsNext}
          squares={currentSquares1}
          onPlay={handlePlay1}
          onUpdate={updateWinner}
          isWinner={winner} // ここ追加
        />

これでロジックは完成!

App.js全文
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;
}

スクリーンショット 2024-09-30 1.20.38.png

これで、勝者がきまれば「判定:XもしくはO」と出て、これ以上の操作ができなくなります。
完成!

さいごに

立体四目を作ることで、
・親から子にデータを渡す
・子から親のデータを更新する
の理解が深まった。

チュートリアルで言われた通りに三目並べを作るのも楽しいけど
自分で考えて立体四目を作るとさらに楽しかったです。

reactの使い方について知るという目標は達成できました。
気が向いたら、リファクタリングするか、UIを見やすくするかしたいです

reactのお作法的に
「もっとこうするといいよ」なコメント、いただけると大変ありがたいです🙏 よろしくお願いします。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?