コンテンツにスキップ

マインスイーパー(再帰処理)

1.概要

マインスイーパーは、隠れた地雷を避けながら、セルをクリックしてオープンにしていくゲームです。

このプログラムでは、空セルをクリックしたときに周辺のセルが次々と連鎖的にオープンされていく処理に、再帰関数(checkAround())を使って実現しています。

なお、再帰処理にあまり慣れていない場合には、下記のページを見てから以下のプログラム例を見ると良いでしょう。

マインスイーパーでの再帰処理の理解のために

以下にプログラム例として挙げたのは、プログラミング学習用の簡易版です。そのため、マップも固定で1種類しかありません。実際のゲームとして遊べるような完成版は「4.完成版」を参照してください。

2.プログラム例

//マインスイーパー  [簡易版/再帰利用版] 2022.10.2
int[][] cellValue = { //セルの中身。-1:爆弾 0:空 その他:隣接する爆弾数
{-1, 1, 0, 1, 1},
{1, 1, 0, 1, -1},
{1, 1, 0, 1, 1},
{-1, 2, 1, 0, 0},
{2, -1, 1, 0, 0}
};
int[][] cellState; // セルの状態。0:未オープン 1:オープン
boolean gameFlg; // true:ゲーム中 false:ゲーム中ではない
void setup() {
size(500, 500);
cellState = new int[5][5];
for (int i = 0; i<5; i++) {
for (int j = 0; j<5; j++) {
cellState[i][j]=0;
}
}
gameFlg = true;
PFont font = createFont("MS Gothic", 50);
textFont(font);
}
void draw() {
if (gameFlg == true) {
background(255);
drawCell(); // 盤面の描画
clearCheck(); // ゲームクリアのチェック
}
}
// 盤面の描画
void drawCell() {
fill(200);
for (int i = 0; i<5; i++) {
for (int j = 0; j<5; j++) {
if (cellState[i][j]==0) { //オープンされていないセル
fill(200);
rect(j*100, i*100, 100, 100);
} else if (cellState[i][j]==1) { //オープンされたセル
fill(255);
rect(j*100, i*100, 100, 100);
if (cellValue[i][j] != 0) { //中身が0の場合は、数字を表示しない
textSize(50);
fill(0);
text(cellValue[i][j], j*100+35, i*100+70);
}
} else if (cellState[i][j]==2) { //ゲームオーバーやゲームクリアでオープンされたセル(爆弾あり)
fill(255, 0, 0);
rect(j*100, i*100, 100, 100);
textSize(50);
fill(0);
text("", j*100+30, i*100+70);
}
}
}
}
//ゲームクリアのチェック(爆弾以外のセルを全て開けたかどうかのチェック)
void clearCheck() {
int count = 0;
for (int i = 0; i<5; i++) {
for (int j = 0; j<5; j++) {
if (cellState[i][j] != 0) {
count++;
}
}
}
if (5*5-count == 4) {
gameClear();
}
}
//マウスクリック処理
void mousePressed() {
int mj;
int mi;
mj = int(mouseX/100); //x座標から列番号を算出する
mi = int(mouseY/100); //y座標から行番号を算出する
if ((mi >= 0) && (mi < 5) && (mj >= 0) && (mj < 5)) { //盤面内であり、
if (cellState[mi][mj] == 0) { //そのセルがオープンされていない状態(==0)の場合のみ、以下を行う
if (cellValue[mi][mj]== -1) { //中身が-1は爆弾なので、ゲームオーバー
gameOver();
} else {
cellState[mi][mj] = 1;
//もし中身が0(空)だったら、その周辺セルも開ける必要があるので、checkAroundを実行する
if (cellValue[mi][mj]==0) {
checkAround(mi, mj);
}
}
}
}
}
void checkAround(int pi, int pj) {
for (int di = -1; di < 2; di++) {
for (int dj = -1; dj < 2; dj++) {
// 盤面外はチェックできないので次の方向の確認に移る。
if ((pi+ di < 0) || (pi+ di >= 5) || (pj + dj < 0) || (pj + dj >= 5)) {
continue;
}
// オープンされていないセル(==0)の場合のみ、以下の処理をする
if (cellState[pi+di][pj+dj] == 0) {
cellState[pi+di][pj+dj] = 1;
//もし中身が0(空)だったら、その隣接セルも開ける必要があるので、checkAroundを実行する
if (cellValue[pi+di][pj+dj]==0) {
checkAround(pi+di, pj+dj);
}
}
}
}
return; //再帰の終了条件は、周辺セル全てのチェックの完了(di,djループ完了)
}
//ゲームオーバー
void gameOver() {
for (int i = 0; i<5; i++) {
for (int j = 0; j<5; j++) {
if (cellValue[i][j] == -1) {
cellState[i][j]=2;
}
}
}
drawCell();
fill(0, 0, 255);
textSize(70);
text("GAME OVER", 100, 300);
gameFlg = false;
}
//ゲームクリア
void gameClear() {
for (int i = 0; i<5; i++) {
for (int j = 0; j<5; j++) {
if (cellValue[i][j] == -1) {
cellState[i][j]=2;
}
}
}
drawCell();
fill(0, 255, 0);
textSize(70);
text("GAME CLEAR", 100, 300);
gameFlg = false;
}
//debuged 2023.6.15

3.解説

(1) セルを連鎖的にオープンにしていく処理

  1. セルをクリックして、オープンにします。
  2. もしそのセルの中身(配列cellValue[][]の値)が0(空)であったら、そのセルを中心にした周囲の隣接セルもオープンにする必要があるので、関数checkAround()を実行します。
  3. 関数checkAround()では、周囲の隣接セルを順次チェックし、オープンにしていきます。もしその隣接セルの中身が0(空)だったら、そのセルを中心にした周囲の隣接セルもオープンにする必要があるので、関数checkAround()を再帰的に呼び出します。
  4. なお、再帰呼び出しは、そのセルがその時に初めてオープンにするセルの場合のみです。すでにオープンしている(すでにチェックしている)セルでは再帰呼び出しをしません。このためのセルのオープン状況は配列cellState[][]で管理しています。

(2)再帰処理について

本プログラムでは、いくつかの2次元配列がある中で再帰呼び出しをしているので、少々わかりづらくなっています。理解が難しかったら、別ページの「マインスイーパーでの再帰処理の理解のために」に簡素化した再帰処理の例を示していますので、これを理解してから本プログラムを見るとよいでしょう。

4.完成版

完成版は上記の簡易版と比べ以下のような改良点がありますが、これらを参考にして皆さんも独自の改良に挑戦してみてください。

完成版における改良点

  • ソースコードを編集すれば、セルの数や大きさ、爆弾の数を変更できます。

  • マップは毎回ランダムに自動生成されます。

    (周囲の爆弾の数も自動カウントされます。カウント処理は下記を参照。

    周囲のマスをチェックする小技

  • 最初にクリックしたセルは、必ず空セルになります。

  • 残りの爆弾数や経過時間が表示されます。

  • 右クリックでセルにマークを付けることができます。

  • フォルダ内に、主な関数の動きの説明文書があります。

img01.png

完成版サンプルコードダウンロード

minesweeper2_rec.zip