ゲーム開発ラボ

視覚的に楽しいアプリやゲーム開発をしながら,Javaやjavascriptを楽しく学んでいきます

マインスイーパ ゲームを作成する-2

前回はキャンバスにセルを表示する機能を実装しました
processing-p5.hateblo.jp

今回はセルに爆弾を配置すると同時に, 周りのセルの爆弾数を計算し,その数を表示する機能を追加します
以下が今回作成するアプリの様子です
f:id:filopodia:20201122152245g:plain
今回のアプリでは爆弾のセルを開いてしまうと,フィールド上のすべての爆弾の位置が示されます(ゲームオーバーとなる)
ゲーム内経過時間や旗などは,現段階ではまだ実装していません

コードは以下のようになりました

boolean gameOver = false;
int col_num = 8; // 列数
int row_num = 8; // 行数
float p_bomb = 0.15; // 爆弾の確率
Cell[][] field = new Cell[col_num][row_num];
float c_x, c_y; // セルの幅
float margin = 4; // セル間の余白

void setup(){  
  PFont font = createFont("4x4kanafont.ttf", 12, true);
  textFont(font);
  textAlign(CENTER, CENTER);
  frameRate(30);
  size(200, 200);
  background(255);
  c_x = (width-(col_num+1)*margin)/col_num;
  c_y = (height-(row_num+1)*margin)/row_num;
  
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      boolean bb=false;
      if(random(1)<p_bomb){bb=true;} // p_bombの確率で爆弾生成
      field[i][j] = new Cell(i, j, bb);
    }
  }
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].checkNeighboringBomb();
    }
  }
}

void draw(){
  background(220);
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].display();
    }
  }
}

void mouseClicked(){
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].mouseClicked();
    }
  }  
}

class Cell{
  int x, y; // index
  int neighbor; // 近傍の爆弾数
  boolean isBomb;
  boolean isOpen;

  float cx_min, cx_max, cy_min, cy_max; // セルの4辺の座標 

  // constructor
  Cell(int _x, int _y, boolean _isBomb){
    x = _x;
    y = _y;
    isBomb = _isBomb;
    isOpen = false;
    
    cx_min = x*(c_x+margin)+margin; // セルの左端
    cx_max = (x+1)*(c_x+margin); // セルの右端
    cy_min = y*(c_y+margin)+margin; // セルの上端
    cy_max = (y+1)*(c_y+margin); // セルの下端 
  }
  
  // method
  void display(){
    stroke(50);
    if(isOpen){noStroke(); fill(180);}
    else if(isMouseHover()){fill(200);}
    else {fill(240);}
    rect(x*(c_x+margin)+margin, y*(c_y+margin)+margin, c_x, c_y);
    
    if(isOpen){ // 爆弾や数値を表示
      if(isBomb){fill(255,0,0);text("★", cx_min+0.5*c_x, cy_min+0.4*c_y);}
      else if(neighbor>0){fill(0);text(neighbor, cx_min+0.5*c_x, cy_min+0.4*c_y);}
      openNeighboringCells();
    }
    
    if(gameOver&&isBomb){ // ゲームオーバーの場合, 爆弾のセルを全て開く
      isOpen = true;
    }
  }
  
  void mouseClicked(){ // セル上でマウスクリックされたとき, isOpenにtrueを格納
    if(isMouseHover()){
      isOpen = true;
      if(isBomb){gameOver=true;} // 爆弾をクリックした場合, gameOverにtrueを格納
    }
  }
  
  boolean isMouseHover(){ // マウスがセル上にホバーしているときはtrue    
    if(cx_min<mouseX && mouseX<cx_max && cy_min<mouseY && mouseY<cy_max){
      return true;
    }
    else{return false;}
  }
    
  void checkNeighboringBomb(){ // 近傍の爆弾数を数える
    int num = 0;
    if(x>0){ // 左列
      if(y>0){if(field[x-1][y-1].isBomb){num++;}}
      if(y<row_num-1){if(field[x-1][y+1].isBomb){num++;}}
      if(field[x-1][y].isBomb){num++;}
    }
    if(x<col_num-1){ // 右列
      if(y>0){if(field[x+1][y-1].isBomb){num++;}}
      if(y<row_num-1){if(field[x+1][y+1].isBomb){num++;}}
      if(field[x+1][y].isBomb){num++;}
    }
    if(y>0){if(field[x][y-1].isBomb){num++;}}
    if(y<row_num-1){if(field[x][y+1].isBomb){num++;}}
    
    if(isBomb){num=-1;} // 自分が爆弾の場合, -1を格納
    neighbor = num;
  }
  
  void openNeighboringCells(){ // 近傍のセルが空欄の場合,セルを解放する
    if(x>0){ // 左列
      if(y>0){if(field[x-1][y-1].neighbor==0){field[x-1][y-1].isOpen=true;}}
      if(y<row_num-1){if(field[x-1][y+1].neighbor==0){field[x-1][y+1].isOpen=true;}}
      if(field[x-1][y].neighbor==0){field[x-1][y].isOpen=true;}
    }
    if(x<col_num-1){ // 右列
      if(y>0){if(field[x+1][y-1].neighbor==0){field[x+1][y-1].isOpen=true;}}
      if(y<row_num-1){if(field[x+1][y+1].neighbor==0){field[x+1][y+1].isOpen=true;}}
      if(field[x+1][y].neighbor==0){field[x+1][y].isOpen=true;}
    }
    if(y>0){if(field[x][y-1].neighbor==0){field[x][y-1].isOpen=true;}}
    if(y<row_num-1){if(field[x][y+1].neighbor==0){field[x][y+1].isOpen=true;}}
  }
}


上のコードを詳しくみていきましょう

まず,キャンバス上に文字を表示するためにsetup()関数中でPFontクラスのオブジェクトを生成しています
そしてセルクラスのdisplay()関数では,セルをクリックして開いたときに爆弾あるいは数字を表示するよう記述しています
セル内に爆弾が存在するとき(isBomb=trueのとき)は★を表示し, 近隣のセルに爆弾が存在するときはその個数を表示します

void setup(){  
  PFont font = createFont("4x4kanafont.ttf", 12, true);
  textFont(font);
  textAlign(CENTER, CENTER);
}

class Cell{
  void display(){    
    if(isOpen){ // 爆弾や数値を表示
      if(isBomb){fill(255,0,0);text("★", cx_min+0.5*c_x, cy_min+0.4*c_y);}
      else if(neighbor>0){fill(0);text(neighbor, cx_min+0.5*c_x, cy_min+0.4*c_y);}
      openNeighboringCells();
    }
}


セル内に爆弾が存在するかどうかは, クラス変数 isBombで定義しています
また,セルに爆弾を配置するか否かは確率的に決定されており, 確率の高さはグローバル変数p_bombで指定しています

float p_bomb = 0.15; // 爆弾の確率

void setup(){    
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      boolean bb=false;
      if(random(1)<p_bomb){bb=true;} // p_bombの確率で爆弾生成
      field[i][j] = new Cell(i, j, bb);
    }
  }
}

class Cell{
  boolean isBomb;

  // constructor
  Cell(int _x, int _y, boolean _isBomb){
    x = _x;
    y = _y;
    isBomb = _isBomb;
    isOpen = false;
  }
}


次にセルの近傍のセルに存在する爆弾の数を, クラス変数neighborとして定義しています
爆弾の個数を数えるのはクラス関数 checkNeighboringBomb()で行なっています

void setup(){    
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].checkNeighboringBomb();
    }
  }
}

class Cell{
 void checkNeighboringBomb(){ // 近傍の爆弾数を数える
    int num = 0;
    if(x>0){ // 左列
      if(y>0){if(field[x-1][y-1].isBomb){num++;}}
      if(y<row_num-1){if(field[x-1][y+1].isBomb){num++;}}
      if(field[x-1][y].isBomb){num++;}
    }
    if(x<col_num-1){ // 右列
      if(y>0){if(field[x+1][y-1].isBomb){num++;}}
      if(y<row_num-1){if(field[x+1][y+1].isBomb){num++;}}
      if(field[x+1][y].isBomb){num++;}
    }
    if(y>0){if(field[x][y-1].isBomb){num++;}}
    if(y<row_num-1){if(field[x][y+1].isBomb){num++;}}
    
    if(isBomb){num=-1;} // 自分が爆弾の場合, -1を格納
    neighbor = num;
  }
}


もし爆弾のセルを開いてしまった場合には, グローバル変数gameOverにtrueが格納されます
gameOverがtrueのときは爆弾のセルはすべてisOpen=trueとなるようにクラス関数display()中に記述しています

boolean gameOver = false;

class Cell{
  void display(){
    if(gameOver&&isBomb){ // ゲームオーバーの場合, 爆弾のセルを全て開く
      isOpen = true;
    }
  }
  void mouseClicked(){ // セル上でマウスクリックされたとき, isOpenにtrueを格納
    if(isMouseHover()){
      isOpen = true;
      if(isBomb){gameOver=true;} // 爆弾をクリックしてしまった場合, gameOverにtrueを格納
    }
  }
}


マインスイーパーでは, セル内に爆弾も数字も存在しない場合(近傍に爆弾が存在しない場合), セルが連鎖的に開かれます
この動作は, クラス関数openNeighboringCells()で定義しています
なお, openNeighboringCells()はクラス関数display()中で呼び出しています

class Cell{
  void display(){
    if(isOpen){ // 爆弾や数値を表示
      if(isBomb){fill(255,0,0);text("★", cx_min+0.5*c_x, cy_min+0.4*c_y);}
      else if(neighbor>0){fill(0);text(neighbor, cx_min+0.5*c_x, cy_min+0.4*c_y);}
      openNeighboringCells(); // 開いたセルが空欄の場合, 周囲の空欄セルを連鎖的に開く
  }

  void openNeighboringCells(){ // 近傍のセルが空欄の場合,セルを解放する
    if(x>0){ // 左列
      if(y>0){if(field[x-1][y-1].neighbor==0){field[x-1][y-1].isOpen=true;}}
      if(y<row_num-1){if(field[x-1][y+1].neighbor==0){field[x-1][y+1].isOpen=true;}}
      if(field[x-1][y].neighbor==0){field[x-1][y].isOpen=true;}
    }
    if(x<col_num-1){ // 右列
      if(y>0){if(field[x+1][y-1].neighbor==0){field[x+1][y-1].isOpen=true;}}
      if(y<row_num-1){if(field[x+1][y+1].neighbor==0){field[x+1][y+1].isOpen=true;}}
      if(field[x+1][y].neighbor==0){field[x+1][y].isOpen=true;}
    }
    if(y>0){if(field[x][y-1].neighbor==0){field[x][y-1].isOpen=true;}}
    if(y<row_num-1){if(field[x][y+1].neighbor==0){field[x][y+1].isOpen=true;}}
  }
}