ゲーム開発ラボ

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

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

前回は爆弾の配置と,爆弾数の表示,ゲームオーバーの定義を行いました
processing-p5.hateblo.jp

今回はセルに旗を立てられるようにします
下図が今回のアプリの様子です
f:id:filopodia:20201122232842g:plain

コードの全体は以下のとおりです

boolean gameOver = false;
int bomb_n = 10; // 爆弾の個数
int flag_n = 0; // 旗の個数

int col_num = 9; // 列数
int row_num = 9; // 行数
int field_w, field_h; // フィールドの幅と高さ
int window_h = 50; // 上部windowの高さ

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(300, 350);
  field_w = width; field_h = height-window_h;
  
  c_x = (field_w-(col_num+1)*margin)/col_num;
  c_y = (field_h-(row_num+1)*margin)/row_num;
  initiateField();
}

void draw(){
  background(220);
  
  // display window
  noStroke();
  fill(180);
  rect(0+margin, 0+margin, field_w-2*margin, window_h-margin);
  fill(0);
  int bomb_remain = bomb_n-flag_n;
  if(!gameOver){text("BOMB: "+ bomb_remain, 0.2*field_w, 0.5*window_h);}
  if(gameOver){
    textSize(14);text("Game Over", 0.5*field_w, 0.25*window_h); 
    textSize(10);text("Press Enter to Restart", 0.5*field_w, 0.75*window_h);
    textSize(12);
  }
  
  // display Field
  pushMatrix();
  translate(0, window_h);
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].display();
    }
  }
  popMatrix();
}

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

void keyPressed(){
  if(gameOver&&keyCode==ENTER){ // ゲームオーバー時にEnterクリックでゲーム再開
    gameOver=false;
    initiateField();
  }
}

void initiateField(){
  // Cellインスタンス生成
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j] = new Cell(i, j);
    }
  }
  // 爆弾を必要数生成
  int n = bomb_n;
  while(n>0){
    int xx = int(random(0,col_num-1));
    int yy = int(random(0,row_num-1));
    if(!field[xx][yy].isBomb){ // 爆弾がすでに配置されていないセルだけを選択
      field[xx][yy].isBomb = true;
      n--;
    }
  }
  // 近傍の爆弾数をカウント
  for(int i=0; i<col_num; i++){
    for(int j=0; j<col_num; j++){
      field[i][j].checkNeighboringBomb();
    }
  }  
}

class Cell{
  int x, y; // index
  int neighbor; // 近傍の爆弾数
  boolean isBomb; // 爆弾があるか
  boolean isOpen; // セルは開いているか
  boolean flag; // フラッグは立っているか

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

  // constructor
  Cell(int _x, int _y){
    x = _x;
    y = _y;
    isBomb = false;
    isOpen = false;
    flag = 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();
    }
    else{ // フラッグを表示
      if(flag){fill(0,0,255);text("♪", cx_min+0.5*c_x, cy_min+0.4*c_y);}
    }
    
    if(gameOver&&isBomb){ // ゲームオーバーの場合, 爆弾のセルを全て開く
      isOpen = true;
    }
  }
  
  void mouseClicked(){ // セル上でマウスクリックされたとき, isOpenにtrueを格納
    if(isMouseHover()&&!gameOver){ // ゲームオーバ時はクリック不可
      if(mouseButton==LEFT){     
        if(!flag){ // フラッグが立っていない場合のみクリック可能
          isOpen = true;
          if(isBomb){gameOver=true;} // 爆弾をクリックした場合, gameOverにtrueを格納
        }
      }
      else if(mouseButton==RIGHT){
        if(flag){flag=false; flag_n--;}
        else{flag=true; flag_n++;}
      }
    }
  }
  
  boolean isMouseHover(){ // マウスがセル上にホバーしているときはtrue
    // 上部windowの高さ分だけ, mouseYから減算
    if(cx_min<mouseX && mouseX<cx_max && cy_min<mouseY-window_h && mouseY-window_h<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;}}
  }
}


前回のコードから大きく変化した点をみていきましょう
まず,セル上に旗を立てるためにセルクラスのクラス変数としてflagを定義します
セル上で右クリックを一回押すとflag変数にtrueが格納されます
その状態でもう一度右クリックをするとflag変数にfalseが格納されます
なお,flag=trueのときは,セル上を左クリックしてもセルを開くことができません

  Cell(int _x, int _y){
    flag = false;
  }
  
  void mouseClicked(){
    if(isMouseHover()&&!gameOver){
      if(mouseButton==LEFT){ // 左クリック時の挙動
        if(!flag){ // フラグが立っていない場合のみクリック可能
          isOpen = true;
          if(isBomb){gameOver=true;}
        }
      }
      else if(mouseButton==RIGHT){ // 右クリック時の挙動
        if(flag){flag=false; flag_n--;} // すでにフラグが立っているときは, フラグを取り消す
        else{flag=true; flag_n++;} // フラグが立っていないときは, 新たにフラグを立てる
      }
    }
  }
}

下図は旗が立っているときの様子です
f:id:filopodia:20201122235020p:plain


前回のコードでは爆弾は確率的に生成していましたが, 今回は爆弾の数を固定するため, 次のようなコードの書き方に変更しています

int bomb_n = 10; // 爆弾の個数
void initiateField(){
  // 爆弾を必要数生成
  int n = bomb_n;
  while(n>0){
    int xx = int(random(0, col_num-1));
    int yy = int(random(0, row_num-1));
    if(!field[xx][yy].isBomb){ // 爆弾がすでに配置されていないセルだけを選択
      field[xx][yy].isBomb = true;
      n--;
    }
  }
 
}


マインスイーパでは, 画面上部に「のこり爆弾数」を表示することが一般的です
「のこり爆弾数」は「実際の爆弾数ー旗の数」で与えられます
旗の数を格納するグローバル変数flag_nと,爆弾の数を格納するグローバル変数bomb_nをそれぞれ定義し,その差分を画面上に表示します

int bomb_n = 10; // 爆弾の個数
int flag_n = 0; // 旗の個数
void draw(){
  if(!gameOver){text("BOMB: "+ bomb_remain, 0.2*field_w, 0.5*window_h);} // のこり爆弾数表示
  if(gameOver){ //ゲームオーバー画面表示
    textSize(14);text("Game Over", 0.5*field_w, 0.25*window_h); 
    textSize(10);text("Press Enter to Restart", 0.5*field_w, 0.75*window_h);
    textSize(12);
  }
}

次回はゲームクリア時の画面表示の実装をしたいと思います