【PHP】Lesson5-☆2:マインスイーパでクラスとメソッドの使い方をマスターしよう

レッスン5で学習した内容を応用して簡単なゲームを作成しましょう。
Lesson1:基礎文法編
Lesson2:制御構造編
Lesson3:関数編
Lesson4:データ構造編
Lesson5:クラス編
・Lesson5-1:クラスを定義しよう
・Lesson5-2:コンストラクタを理解しよう
・Lesson5-3:アクセス修飾子とカプセル化を理解しよう
・Lesson5-4:クラスメンバを理解しよう
・Lesson5-5:クラスの継承を理解しよう
・Lesson5-6:メソッドのオーバーライドを理解しよう
・Lesson5-7:抽象クラスを理解しよう
・Lesson5-8:インターフェースを理解しよう
・Lesson5-9:トレイトを理解しよう
・確認問題5-1:モンスター捕獲ゲームを作ろう
・確認問題5-2:マインスイーパを作ろう ◁今回はココ
PHPで作るゲームコード一覧はこちら
演習問題|クラスとメソッドでマインスイーパを実装しよう

コマンドラインでプレイできるマインスイーパゲームを作成しましょう。
このプログラムでは、10×10のマス目に10個の爆弾がランダムに配置され、ユーザーが爆弾の位置を避けながら、すべての安全なマスを開くとゲームクリアになります。
プログラムはクラスを使用し、ゲーム状態や盤面を管理します。
また、ゲームクリアやゲームオーバーの判定を行い、結果を表示してください。
この演習の要件
以下の要件に従ってコードを完成させてください。
- クラスの作成: Minesweeper クラスを作成し、ゲームロジックを実装すること。
- 定数の設定: グリッドサイズを 10×10、爆弾数を 10 個に設定すること。
- 盤面の初期化: グリッドを0で埋め、ランダムに爆弾を配置すること。
- 数字の計算: 選択したマスの周囲の爆弾数をカウントして表示すること。
- 再帰的開放: クリックしたマスが0の場合、隣接するマスを自動的に開くこと。
- ゲーム終了条件:
- 爆弾を開いた場合はゲームオーバーと表示すること。
- 爆弾以外のマスをすべて開いた場合はゲームクリアと表示すること。
- 表示形式:
PHP_EOL
を使って盤面を表示し、座標を指定して操作できるようにすること。 - 入力処理: プレイヤーが指定する座標を受け取り、そのマスを開くようにすること。
ただし、以下のような実行結果となること。
0 1 2 3 4 5 6 7 8 9
0 . . . . . . . . . .
1 . . . . . . . . . .
2 . . . . . . . . . .
3 . . . . . . . . . .
4 . . . . . . . . . .
5 . . . . . . . . . .
6 . . . . . . . . . .
7 . . . . . . . . . .
8 . . . . . . . . . .
9 . . . . . . . . . .
座標を入力 (例: 3 4): 3 4
0 1 2 3 4 5 6 7 8 9
0 . . . . . . . . . .
1 . . . . . . . . . .
2 . . . 1 1 1 . . . .
3 . . . 1 B 1 . . . .
4 . . . 1 1 1 . . . .
5 . . . . . . . . . .
6 . . . . . . . . . .
7 . . . . . . . . . .
8 . . . . . . . . . .
9 . . . . . . . . . .
ゲームオーバー!
ゲーム終了。
解き方のヒント
1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。
- ヒント1【コードの構成を見る】
-
正解のコードは上から順に以下のような構成となっています。
1. クラスと変数の定義:Minesweeper クラス
- 目的:マインスイーパゲーム全体を管理するクラス。
- 定数の定義:
SIZE
:グリッドサイズを10×10に固定。BOMBS
:爆弾の数を10に設定。
- 変数の定義:
$grid
:グリッドの状態を記録する配列。$revealed
:マスの開示状態を記録する配列。$gameOver
:ゲーム終了状態を管理するフラグ。
2. 初期化処理:initializeGame メソッド
- 目的:ゲーム開始時の初期化を行う。
- 処理内容:
- グリッドを作成し、爆弾を配置する。
- 各マスの開示状態をすべて「未開示」に設定する。
- ゲームオーバーフラグを「false」に設定する。
3. グリッドの作成:initializeGrid メソッド
- 目的:爆弾を配置する前のグリッドを初期化する。
- 処理内容:
- すべてのマスに 0 を設定(爆弾なしを示す)。
4. 爆弾の配置:placeBombs メソッド
- 目的:ランダムに10個の爆弾を配置する。
- 処理内容:
- 爆弾が10個配置されるまで繰り返す。
- ランダムな座標を生成し、既に爆弾がある場合は再試行する。
- 爆弾を配置した場合、周囲のマスの数字を更新する。
5. 周囲の数字更新:updateNumbers メソッド
- 目的:爆弾が配置されたマスの周囲に爆弾の数を表示する。
- 処理内容:
- 周囲のマスを調べ、爆弾でなければ数値をインクリメントする。
- 8方向をループして確認する。
6. マスの開示:revealCell メソッド
- 目的:選択されたマスを開示する。
- 処理内容:
- ゲームオーバーや既に開示済みのマスは無視する。
- 爆弾を開いた場合はゲームオーバーにする。
- 周囲に爆弾がない場合は再帰的に周囲のマスを開示する。
7. ゲームのクリア判定:checkClear メソッド
- 目的:すべての安全なマスが開示されているか確認する。
- 処理内容:
- 開示されたマスの数と爆弾以外のマスの合計数を比較する。
8. グリッド表示:displayGrid メソッド
- 目的:現在のグリッド状態を表示する。
- 処理内容:
- 開示済みのマスは数字または爆弾を表示する。
- 未開示のマスは「.」で表現する。
9. ゲームループとユーザー入力:メインループ
- 目的:ゲームを進行する。
- 処理内容:
- グリッドを表示する。
- ゲームオーバーやクリア判定を行う。
- ユーザーに座標を入力させてマスを開く。
- 無効な入力の場合はエラーメッセージを表示して再入力させる。
10. ゲーム終了時の処理
目的:クリアまたはゲームオーバーの結果を表示する。
- ヒント2【穴埋め問題にする】
-
以下のコードをコピーし、コメントに従ってコードを完成させて下さい。
クリックして開く<?php
class Minesweeper {
private const SIZE = 10; // グリッドサイズ
private const BOMBS = 10; // 爆弾の数
private array $grid; // ゲームグリッド
private array $revealed; // 開示状況
private bool $gameOver; // ゲームオーバーフラグ
public function __construct() {
/* 【穴埋め問題1】
ここでゲーム初期化のためにinitializeGameメソッドを呼び出してください。
*/
}
// ゲーム初期化
private function initializeGame(): void {
/* 【穴埋め問題2】
ここでグリッドの初期化のためにinitializeGridメソッドを呼び出し、その結果を$this->gridに代入してください。
*/
/* 【穴埋め問題3】
ここで爆弾を配置するためにplaceBombsメソッドを呼び出してください。
*/
/* 【穴埋め問題4】
ここで$this->revealedを10x10のfalseで埋めた2次元配列として初期化してください。
*/
/* 【穴埋め問題5】
ここで$this->gameOverをfalseに初期化してください。
*/
}
// グリッド初期化
private function initializeGrid(): array {
/* 【穴埋め問題6】
ここで10x10の0で埋めた2次元配列を返すコードを書いてください。
*/
}
// 爆弾配置
private function placeBombs(): void {
$bombCount = 0;
while ($bombCount < self::BOMBS) {
$x = rand(0, self::SIZE - 1);
$y = rand(0, self::SIZE - 1);
/* 【穴埋め問題7】
ここで選択されたセルに爆弾がない場合にのみ、爆弾を配置し、$bombCountを増やすコードを書いてください。
*/
/* 【穴埋め問題8】
ここでupdateNumbersメソッドを呼び出し、爆弾の位置を渡して数字を更新するコードを書いてください。
*/
}
}
}
このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。
演習問題の答え合わせ
この問題の正解コードとその解説は以下の通りです。
クリックして開いて確認してください。
- 正解コード
-
クリックして開く
<?php
class Minesweeper {
private const SIZE = 10; // グリッドサイズ
private const BOMBS = 10; // 爆弾の数
private array $grid; // ゲームグリッド
private array $revealed; // 開示状況
private bool $gameOver; // ゲームオーバーフラグ
public function __construct() {
$this->initializeGame();
}
// ゲーム初期化
private function initializeGame(): void {
$this->grid = $this->initializeGrid();
$this->placeBombs();
$this->revealed = array_fill(0, self::SIZE, array_fill(0, self::SIZE, false));
$this->gameOver = false;
}
// グリッド初期化
private function initializeGrid(): array {
return array_fill(0, self::SIZE, array_fill(0, self::SIZE, 0));
}
// 爆弾配置
private function placeBombs(): void {
$bombCount = 0;
while ($bombCount < self::BOMBS) {
$x = rand(0, self::SIZE - 1);
$y = rand(0, self::SIZE - 1);
if ($this->grid[$x][$y] !== 'B') {
$this->grid[$x][$y] = 'B';
$bombCount++;
$this->updateNumbers($x, $y);
}
}
}
// 数字更新
private function updateNumbers(int $bombX, int $bombY): void {
for ($dx = -1; $dx <= 1; $dx++) {
for ($dy = -1; $dy <= 1; $dy++) {
$nx = $bombX + $dx;
$ny = $bombY + $dy;
if ($nx >= 0 && $nx < self::SIZE && $ny >= 0 && $ny < self::SIZE && $this->grid[$nx][$ny] !== 'B') {
$this->grid[$nx][$ny]++;
}
}
}
}
// マスを開く
public function revealCell(int $x, int $y): void {
if ($this->gameOver || $this->revealed[$x][$y]) {
return;
}
$this->revealed[$x][$y] = true;
if ($this->grid[$x][$y] === 'B') {
$this->gameOver = true;
return;
}
// 周囲に爆弾がなければ再帰的に開く
if ($this->grid[$x][$y] == 0) {
for ($dx = -1; $dx <= 1; $dx++) {
for ($dy = -1; $dy <= 1; $dy++) {
$nx = $x + $dx;
$ny = $y + $dy;
if ($nx >= 0 && $nx < self::SIZE && $ny >= 0 && $ny < self::SIZE) {
$this->revealCell($nx, $ny);
}
}
}
}
}
// ゲームクリア判定
public function checkClear(): bool {
$revealedCount = 0;
foreach ($this->revealed as $row) {
$revealedCount += array_sum($row);
}
$totalCells = self::SIZE * self::SIZE;
return ($revealedCount + self::BOMBS) === $totalCells;
}
// グリッド表示
public function displayGrid(): void {
echo " ";
for ($i = 0; $i < self::SIZE; $i++) {
echo "$i ";
}
echo PHP_EOL;
for ($i = 0; $i < self::SIZE; $i++) {
echo "$i ";
for ($j = 0; $j < self::SIZE; $j++) {
if ($this->revealed[$i][$j]) {
$value = $this->grid[$i][$j];
echo ($value === 'B' ? 'B' : $value) . " ";
} else {
echo ". ";
}
}
echo PHP_EOL;
}
}
// ゲーム状態取得
public function isGameOver(): bool {
return $this->gameOver;
}
}
// ゲーム開始
$game = new Minesweeper();
while (true) {
// グリッド表示
$game->displayGrid();
// ゲームオーバー判定
if ($game->isGameOver()) {
echo "ゲームオーバー!" . PHP_EOL;
break;
}
// ゲームクリア判定
if ($game->checkClear()) {
echo "ゲームクリア!" . PHP_EOL;
break;
}
// ユーザー入力
echo "座標を入力 (例: 3 4): ";
$input = trim(fgets(STDIN));
[$x, $y] = explode(" ", $input);
if (!is_numeric($x) || !is_numeric($y) || $x < 0 || $x >= 10 || $y < 0 || $y >= 10) {
echo "無効な入力です。もう一度入力してください。" . PHP_EOL;
continue;
}
// セルを開く
$game->revealCell((int)$x, (int)$y);
}
echo "ゲーム終了。" . PHP_EOL;
- 正解コードの解説
-
コードをブロックごとに分割して解説します。
クラスとプロパティの定義
クリックして開くclass Minesweeper {
private const SIZE = 10;
private const BOMBS = 10;
private array $grid;
private array $revealed;
private bool $gameOver;
}
- クラス定義:
class Minesweeper {}
: Minesweeperという名前のクラスを定義します。このクラスはゲーム全体を管理します。
- 定数の宣言:
private const SIZE = 10;
とprivate const BOMBS = 10;
: グリッドサイズを10×10、爆弾の数を10個と固定します。
- プロパティの宣言:
$grid
: ゲーム盤面を表す2次元配列です。$revealed
: 各マスが開かれているかどうかを記録する2次元配列です。$gameOver
: ゲームが終了したかどうかを判定するフラグです。
ゲームの初期化
クリックして開くpublic function __construct() {
$this->initializeGame();
}
コンストラクタ:
- クラスのインスタンスを作成すると自動的に呼び出される特別なメソッドです。
initializeGame()
メソッドを呼んでゲームの初期設定を行います。
ゲームの盤面を準備
クリックして開くprivate function initializeGrid(): array {
return array_fill(0, self::SIZE, array_fill(0, self::SIZE, 0));
}
盤面の初期化:
array_fill
: 配列を特定の値で埋めます。この場合は、10×10のグリッドをすべて0で埋めています。- 爆弾が配置される前の状態を表します。
爆弾の配置
クリックして開くprivate function placeBombs(): void {
$bombCount = 0;
while ($bombCount < self::BOMBS) {
$x = rand(0, self::SIZE - 1);
$y = rand(0, self::SIZE - 1);
if ($this->grid[$x][$y] !== 'B') {
$this->grid[$x][$y] = 'B';
$bombCount++;
$this->updateNumbers($x, $y);
}
}
}
- 爆弾のランダム配置:
rand(0, self::SIZE - 1)
: ランダムな座標を生成します。- 既に爆弾がある場合はスキップし、新しい場所に爆弾を配置します。
- 数字の更新:
updateNumbers
: 爆弾の周囲の数字を増やして、近くに爆弾があることを示します。
マスの開放処理
クリックして開くpublic function revealCell(int $x, int $y): void {
if ($this->gameOver || $this->revealed[$x][$y]) {
return;
}
$this->revealed[$x][$y] = true;
if ($this->grid[$x][$y] === 'B') {
$this->gameOver = true;
return;
}
if ($this->grid[$x][$y] == 0) {
for ($dx = -1; $dx <= 1; $dx++) {
for ($dy = -1; $dy <= 1; $dy++) {
$nx = $x + $dx;
$ny = $y + $dy;
if ($nx >= 0 && $nx < self::SIZE && $ny >= 0 && $ny < self::SIZE) {
$this->revealCell($nx, $ny);
}
}
}
}
}
- マスの開放:
$this->revealed[$x][$y] = true;
: マスを開いたことを記録します。 - ゲームオーバー判定:爆弾「B」を開いた場合は、
$this->gameOver = true
でゲーム終了です。 - 再帰的開放:マスの数字が0の場合は、隣接するマスも自動的に開く再帰処理を行います。
ゲームクリア判定
クリックして開くpublic function checkClear(): bool {
$revealedCount = 0;
foreach ($this->revealed as $row) {
$revealedCount += array_sum($row);
}
return ($revealedCount + self::BOMBS) === (self::SIZE * self::SIZE);
}
クリア条件:開かれたマスの数と爆弾の数を合計して、全マス数と一致すればゲームクリアです。
盤面表示
クリックして開くpublic function displayGrid(): void {
echo " ";
for ($i = 0; $i < self::SIZE; $i++) {
echo "$i ";
}
echo PHP_EOL;
for ($i = 0; $i < self::SIZE; $i++) {
echo "$i ";
for ($j = 0; $j < self::SIZE; $j++) {
if ($this->revealed[$i][$j]) {
$value = $this->grid[$i][$j];
echo ($value === 'B' ? 'B' : $value) . " ";
} else {
echo ". ";
}
}
echo PHP_EOL;
}
}
盤面の可視化:爆弾は「B」で表示し、未開放のマスは「.」で表示します。
- クラス定義:
まとめ
このコードでは、クラスとメソッドを使ってマインスイーパのゲームロジックを管理する方法を学びました。
特に、再帰的なマスの開放処理やゲームクリア判定など、実際のゲーム設計に必要な概念を学ぶことができます。
これを理解することで、さらに高度なゲームやアプリケーションの開発に役立つスキルを身につけることができます。
ぜひ応用してみてください!