マインスイーパー(再帰処理)
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) セルを連鎖的にオープンにしていく処理
- セルをクリックして、オープンにします。
- もしそのセルの中身(配列cellValue[][]の値)が0(空)であったら、そのセルを中心にした周囲の隣接セルもオープンにする必要があるので、関数checkAround()を実行します。
- 関数checkAround()では、周囲の隣接セルを順次チェックし、オープンにしていきます。もしその隣接セルの中身が0(空)だったら、そのセルを中心にした周囲の隣接セルもオープンにする必要があるので、関数checkAround()を再帰的に呼び出します。
- なお、再帰呼び出しは、そのセルがその時に初めてオープンにするセルの場合のみです。すでにオープンしている(すでにチェックしている)セルでは再帰呼び出しをしません。このためのセルのオープン状況は配列cellState[][]で管理しています。
(2)再帰処理について
本プログラムでは、いくつかの2次元配列がある中で再帰呼び出しをしているので、少々わかりづらくなっています。理解が難しかったら、別ページの「マインスイーパーでの再帰処理の理解のために」に簡素化した再帰処理の例を示していますので、これを理解してから本プログラムを見るとよいでしょう。
4.完成版
完成版は上記の簡易版と比べ以下のような改良点がありますが、これらを参考にして皆さんも独自の改良に挑戦してみてください。
完成版における改良点
-
ソースコードを編集すれば、セルの数や大きさ、爆弾の数を変更できます。
-
マップは毎回ランダムに自動生成されます。
(周囲の爆弾の数も自動カウントされます。カウント処理は下記を参照。
-
最初にクリックしたセルは、必ず空セルになります。
-
残りの爆弾数や経過時間が表示されます。
-
右クリックでセルにマークを付けることができます。
-
フォルダ内に、主な関数の動きの説明文書があります。