マインスイーパーのとある処理について(文章まとまってなくてすみません)

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
ゆうとぴあ

マインスイーパーのとある処理について(文章まとまってなくてすみません)

#1

投稿記事 by ゆうとぴあ » 3年前

[0]はじめに

この掲示板を使うのは初めてで、色々と不器用ですがよろしくお願いします。かなり長い文面となっています。不明点はどんどん質問し返してください。どうしても解決したい内容なので、ご協力お願いします。

[1] 質問というかお願い(?)

現在C言語でマインスイーパー を作成しています。

<何を回答して欲しいのか>

この後長々と仕様やらなんやらについて説明しますが、最終的に以下のことを回答していただければ良いので、回答に必要ないと思った箇所はさらっと目を通すだけで良いかと思います。

↓回答して欲しいこと↓
一括オープンのコードを書いて欲しい!

*一括オープンとは?
マインスイーパーをプレイしたことがある方ならわかると思いますが、周囲に何もないマスを開くとその周囲のマスもどんどん開いていく(数字マスが現れるまで開く)という仕様のことです。なお、一括オープンは私が名付けた仮の名前です。

↓回答の際に注意して欲しい事↓
・変数は基本的に自由に設定して使ってください。
・外部ライブラリの使用はなるべく避けてください。
・ポインタや構造体はまだ理解が浅いので、なるべく使用は避けていただきたいですが、仕方ない場合は使用してください。

以下、さらっと見てください。

<現在の自作マインスイーパー の仕様>

*コマンドプロンプトで動くだけ(ウィンドウが開くなどの凝ったものではない)。

*マインスイーパー のゲーム自体の仕様(例えば、旗を立てたところは開かないなど)はGoogleで無料で遊べるマインスイーパー(+α)を目指している。←スマホでGoogleを開き、マインスイーパー と検索したら一番上に出てくる緑を基調としたやつのことです。

*プレイヤーは盤面の行数と列数や地雷数を自由に設定できる(上限下限あり)。

*プレイヤーは基本的にキーボードのテンキーで数値を入力して行や列を選択したり、行動を選択する。

*マインスイーパーの3つの状態は2次元配列(行×列)で管理している。←3つの状態とは開いているか否か、地雷があるか否か、旗があるか否か(基本的に否の場合は0、そうで無いときは1)。それぞれの配列はopen、bomb、flagとしている。

*マスの描写は■や△などを使用しており、for文を二重にすることによって行×列の盤面を描写している(その際先ほどの3つの状態を使用する)。

>他にもいろいろと仕様がありますが質問に関係無さそうな仕様は省略してあります。(知りたい仕様などがあれば質問し返してください)

<今作りたい仕様(仮に一括オープンと名付ける)>

簡単に言うと、開いたマスの周辺に何もなければ、数字マスが現れるまで開き続けるという仕様。数字マスという状態は配列で管理しておらず、周りの地雷の数を盤面の描写時に数えて地雷が1つ以上隣接していれば数字描写している。

少し詳しく言うと、マスを開いたとき、周囲8マスに地雷がなければ旗以外のマスのopenに1を代入する。また、周囲8マスそれぞれに対しても周囲8マスを調べ、地雷がなければ…というように開く仕様。

<一括オープンを実現するためにどんなコードを書いているか>

本当はソースコードを見せるのが良いと思うのですが、これが初のコーディングなのでハッキリ言って見づらく、混乱を招くようなコメントも多々あるため、回答者さんからの依頼がある場合のみ載せますが、なかなか読みづらいということだけ把握をお願いします。

ところで、今書いているコードの全体的な構造を大まかにお伝えします(実行される順に書いてます)。

注)一部の変数を除いてグローバル変数を使っています(ポインタに抵抗があったので)。なお、関数の引数はローカル変数です。

・再帰関数とカウント関数は自作関数です。

メイン関数{
・マインスイーパーのカスタムを行う
・3つの状態の初期化

・プレイヤーの行動(行と列、行動を選択)
・とりあえず、プレイヤーが選択したマスのopenに1を代入したり、flagに1を代入したりする。
・その他のマスの状態の変更。再帰関数を呼び出すが、引数にはプレイヤーが選択した行と列を使用する(→再帰関数へ)
・各マスの状態に基づいてマスを描写する
*繰り返し(*へ戻る)
}

再帰関数{
・プレイヤーが開いたマスの周りの地雷を数える。カウント関数を呼び出すが、引数には再帰関数の引数を使う(→カウント関数へ)
・カウント関数の結果に基づいて処理を決定する(周囲の地雷が0個の場合は一括オープンのコードを実行する。そうで無いときは何もしない)。
・周囲8マスのそれぞれの行と列を用いて再帰関数を呼び出す。引数にはもちろんその行と列を使う(→再帰関数へ)
}

カウント関数{
・隣接する周囲8マスの地雷を数える
}

<何故悩んでいるのか>

一言で言うと一括オープンの部分がうまくいきません。どううまくいかないのかというと、開いて欲しいところが開かない(現在のプログラムでは周囲8マスのうち左上しか調べてくれない←全部調べてくれるコードを書いたつもりだったが…)、処理が無限ループするなどです。もうかれこれ2週間くらい悩んでます。

<結局何を回答して欲しいのか>
一括オープンのコードを書いて欲しい!というのが願いです。もうかなり時間を費やしてしまったので、答えというか解決例のようなものを見たいです。もちろん、書いていただいたコードは何がどうなっているのか理解した上で使う予定です。

[2]環境など

<環境>
・Windows10
・gccもしくは学習用C言語開発環境(苦しんで覚えるC言語というサイトのものです)
・コーディングにはVScodeを使っています。

<C言語の理解度等>
・C言語の入門の内容のうち、ポインタと構造体以外は一応使った事があります。
・ライブラリは標準ライブラリしか使っていません。

アバター
あたっしゅ
記事: 663
登録日時: 13年前
住所: 東京23区
連絡を取る:

Re: マインスイーパーのとある処理について(文章まとまってなくてすみません)

#2

投稿記事 by あたっしゅ » 3年前

>*マインスイーパー のゲーム自体の仕様(例えば、旗を立てたところは開かないなど)は
>Googleで無料で遊べるマインスイーパー(+α)を目指している。←スマホでGoogleを開き、
>マインスイーパー と検索したら一番上に出てくる緑を基調としたやつのことです。

今の Windows 10 には、マインスイーパーついてないのか ? と思って調べたら、Microsoft Store に、あった。


>本当はソースコードを見せるのが良いと思うのですが、これが初のコーディングなので
>ハッキリ言って見づらく、混乱を招くようなコメントも多々あるため、回答者さんからの
>依頼がある場合のみ載せますが、なかなか読みづらいということだけ把握をお願いします。

はい、ソースコードを見せてください。
VTuber:
東上☆海美☆(とうじょう・うみみ)
http://atassyu.php.xdomain.jp/vtuber/index.html
レスがついていないものを優先して、レスするみみ。時々、見当外れなレスしみみ。

中の人:
手提鞄あたッしュ、[MrAtassyu] 手提鞄屋魚有店
http://ameblo.jp/mratassyu/
Pixiv: 666303
Windows, Mac, Linux, Haiku, Raspbery Pi, Jetson Nano, 電子ブロック 持ち。

ゆうとぴあ

Re: マインスイーパーのとある処理について(文章まとまってなくてすみません)

#3

投稿記事 by ゆうとぴあ » 3年前

このコードは質問時より少し手を加えたものです(目指す仕様に変更はありません)。
他人に見せる目的で書いたコードではないので(他にも理由はあるけど...)、見ての通り決して読みやすいものではありません。

また、このコードを実行すると一括オープンの際に
1.開くべきマスが開かないことがある
2.数字マスが開かない
という、現時点のプログラムでは望まない動作をします。
ひとまずここを何とかしたいところです。

あと、一括オープンのあたりのプログラムは理解しながら書いたというより、望まない動作をやめさせるために付けたり消したりしながら書いたので、過不足があると思います(旗が立っている場合などの動作はまだ組み込んでいません。混乱するので。)

コード:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 20

int rec();
int count();

int act,area,b,bombs,countbomb,num,x,y,selectx,selecty,checkx,checky,k,l,open[N][N],bomb[N][N],flag[N][N];
int rand_X,rand_Y;

int main(void){

    int i,j;
   countbomb=0;
   srand(time(NULL));

   //b=爆弾の割合、爆弾の数 bombs=爆弾のメニュー bomb=配列

   //配列は[0][0]から(システム的なものは基本0から)、ユーザーが入力するものは1から始める

    //広さ選択始め
    while(1){

        printf("広さを決めてください\n1:初級(10×10)\n2:中級(15×15)\n3:上級(20×20)\n4:カスタム\n....");
        scanf("%d",&area);

        if(area!=1 && area!=2 && area!=3 && area!=4){
            printf("error1\n");
        }
        else break;

    }

    if(area==1){//初級
        x=10;
        y=10;
    }
    else if(area==2){//中級
        x=15;
        y=15;
    }
    else if(area==3){//上級
        x=20;
        y=20;
    }
    else{//カスタム

        while(1){

            printf("行数を入れてください....");
            scanf("%d",&x);

            //5*5最小
            if(x<5 || x>N){
                printf("error2\n");
            }
            else break;

        }

        while(1){
        
            printf("列数を入れてください....");
            scanf("%d",&y);

            //5*5最小
            if(y<5 || x>N){
                printf("error3\n");
            }
            else break;

        }

    }
    //広さ選択終わり

    //爆弾数選択始め
    while(1){

        printf("爆弾数を決めてください ()内は爆弾の入る確率\n1:初級(15%)\n2:中級(20%)\n3:上級(25%)\n4:カスタム\n....");
        scanf("%d",&bombs);

        if(bombs!=1 && bombs!=2 && bombs!=3 && bombs!=4){
            printf("error4\n");
        }
        else break;

    }

    if(bombs==4){
        while(1){
            
            printf("爆弾数(%%)を入れてください....");
            scanf("%d",&b);

            if(b<=0 || b>50){//爆弾の割合上限
                printf("error5\n");
            }
            else break;

        }
    }
    //爆弾数選択終わり

    //初期化など
    for(i=0;i<x;i++){
        for(j=0;j<y;j++){
            printf("■");
            open[i][j]=0;
            flag[i][j]=0;
            bomb[i][j]=0;
        }
        printf("\n");
    }

    //新たな爆弾初期化法
    i=0;
    if(bombs==1) b=x*y*0.15;
    else if(bombs==2) b=x*y*0.2;
    else if(bombs==3) b=x*y*0.25;
    else b=x*y*(float)b/100;

    while(i<b){//iがbになった瞬間、爆弾はb個セットされた状態になるので、i=bになったらwhileをぬける。
        rand_X=rand()%x;
        rand_Y=rand()%y;
        if(bomb[rand_X][rand_Y]==0){
            bomb[rand_X][rand_Y]=1;
            i++;
        }
    }

    for(i=0;i<x;i++){
        for(j=0;j<y;j++){
            if(bomb[i][j]==1) countbomb++;//爆弾を数える
        }
    }
    printf("残りの爆弾=%d\n",countbomb);

    while(1){//マインスイーパー動作部

        //行動選択、行列選択のエラー復帰用
        while(1){

            //開くマス選択始め
            while(1){

                printf("開く行を入力....");
                scanf("%d",&selectx);

                selectx--;//ユーザーは配列の0行目を1行目として入力する
        
                if(selectx<0 || selectx>x-1){
                    printf("error6\n");
                }
                else break;

            }

            while(1){

                printf("開く列を入力....");
                scanf("%d",&selecty);

                selecty--;//ユーザーは配列の0列目を1列目として入力する
        
                if(selecty<0 || selecty>y-1){//x=5としたとき0~4行まで作られる。5行目は存在しない
                    printf("error7\n");
                }
                else break;

            }

            //行動選択 act=1:開く 2:旗
            while(1){
            
                printf("行動選択;開く:1,フラグ:2");
                scanf("%d",&act);

                if(act!=1 && act!=2){
                    printf("error8\n");
                }
                else break;

            }

            if(act==1 && open[selectx][selecty]==1){//開いているマスを開くと選びなおし
                printf("error9\n");
            }
            else if(act==1 && flag[selectx][selecty]==1){//フラグマスは開けない
                printf("error10\n");
            }
            else break;
        }
        //開くマス選択終わり

        //マスの値制御
        if(act==1) open[selectx][selecty]=1;//選択されたマスを開く
        else{
            if(flag[selectx][selecty]==1) flag[selectx][selecty]=0;//フラグが立っていれば除去
            else flag[selectx][selecty]=1;
        }

        count(selectx,selecty);
        if(num==0){
            for(i=selectx-1;i<=selectx+1;i++){
                for(j=selecty-1;j<=selecty+1;j++){
                    rec(i,j);
                }
            }
        }

        //表示部始め
        countbomb=0;

        for(i=0;i<x;i++){
            for(j=0;j<y;j++){
                num=0;//numはマスごとに加算するのでここで初期化

                //既にゲームオーバーでこのマスに爆弾があるとき...
                if(open[selectx][selecty]==1 && bomb[selectx][selecty]==1 && bomb[i][j]==1) printf("×");
                //ゲームオーバーじゃないとき、ゲームオーバーだけど爆弾マスではないとき
                else{
                    if(open[i][j]==1){//マスが開いてるとき
                        
                        //周りの爆弾を調べる
                        count(i,j);
                        
                        //数字または空白を表示
                        if(num==1) printf("1");
                        else if(num==2) printf("2");
                        else if(num==3) printf("3");
                        else if(num==4) printf("4");
                        else if(num==5) printf("5");
                        else if(num==6) printf("6");
                        else if(num==7) printf("7");
                        else if(num==8) printf("8");
                        
                        else{//空白マスの時
                            if(open[selectx][selecty]==1 && bomb[selectx][selecty]==1) printf("□");//ゲームオーバーのときは空白を表示するだけ
                            else{//ゲームオーバーでないかつ空白の時、周りも開く
                                
                                if(open[i][j]==1) printf("□");
                            }
                        }
                    }
                    else if(flag[i][j]==1) printf("△");//マスが閉じていて、フラグがあるとき
                    else printf("■");//その他
                }                
                if(bomb[i][j]==1) countbomb++;//爆弾を数える
            }
            printf("\n");
        }

        printf("残りの爆弾=%d\n",countbomb);
        //表示部終わり

        //ゲームの終了
        if(open[selectx][selecty]==1 && bomb[selectx][selecty]==1){
            printf("GAME OVER\n");
            return 0;
        }
    }
    //動作部終わり
    return 0;
}

int rec(int checkx,int checky){

    num=0;

    count(checkx,checky);

    if(checkx==selectx && checky==selecty) return 0;
    if(checkx<0 || checkx>=x || checky<0 || checky>=y) return 0;
    if(num!=0) return 0;
    if(bomb[checkx][checky]==1 || open[checkx][checky]==1) return 0;
    open[checkx][checky]=1;

    for(k=checkx-1;k<=checkx+1;k++){
        for(l=checky-1;l<=checky+1;l++){
            rec(k,l);
        }
    }

    return 0;
}

int count(int countx,int county){

    int i,j;

    for(i=countx-1;i<=countx+1;i++){
        for(j=county-1;j<=county+1;j++){
            if(i<0 || i>=x || j<0 || j>=y) continue;
            if(i==countx && j==county) continue;
            if(bomb[i][j]==1) num++;
        }
    }

    return 0;
}

アバター
あたっしゅ
記事: 663
登録日時: 13年前
住所: 東京23区
連絡を取る:

Re: マインスイーパーのとある処理について(文章まとまってなくてすみません)

#4

投稿記事 by あたっしゅ » 3年前

.cpp にしたので、プロトタイプ宣言を以下のように修正して

コード:

int rec(int checkx,int checky);
int count(int countx,int county);
とりあえず、自分の環境で、ビルドできました。
VTuber:
東上☆海美☆(とうじょう・うみみ)
http://atassyu.php.xdomain.jp/vtuber/index.html
レスがついていないものを優先して、レスするみみ。時々、見当外れなレスしみみ。

中の人:
手提鞄あたッしュ、[MrAtassyu] 手提鞄屋魚有店
http://ameblo.jp/mratassyu/
Pixiv: 666303
Windows, Mac, Linux, Haiku, Raspbery Pi, Jetson Nano, 電子ブロック 持ち。

アバター
usao
記事: 1887
登録日時: 11年前

Re: マインスイーパーのとある処理について(文章まとまってなくてすみません)

#5

投稿記事 by usao » 3年前

”塗りつぶし アルゴリズム”とかでググればいいんじゃない?

(どうせ理解して使う前提なら,コードよりも処理内容の話を見つけた方が早かろう)

アバター
usao
記事: 1887
登録日時: 11年前

Re: マインスイーパーのとある処理について(文章まとまってなくてすみません)

#6

投稿記事 by usao » 3年前

すげーてきとーに再帰っぽい物を示すとしたらこんな感じかな?
(自分が作るなら再帰では書かないと思うけど)

コード:

//(x,y)のマスを開く処理.
//ユーザが指定した座標を引数として呼ばれる想定.
//(戻り値は処理結果を示す何か)
OpenResultValue Open( int x, int y )
{
  //入力位置に対する何やかんやの判定処理
  if( (x,y)が範囲外だったら ){  return その旨を示す値;  }
  if( (x,y)が既にオープン済みだったら ){  return その旨を示す値;  }
  if( (x,y)が爆弾だったら ){  return ゲームオーバーを示す値;  }
  ...
  
  //(x,y)を開く.
  配列[y][x] = このマスを開いたことを示す値;
  
  //(x,y)の周囲にある爆弾の個数.0~8であろう.
  int nAdjacentBomb = ...;

  //このマスの周囲に爆弾が無い場合には自動で周囲のマスを開く
  if( nAdjacentBomb == 0 )
  {
    //4隣接なら周囲4マス.8隣接なら8個書けばいいっしょ.
    Open( x, y-1 );
    Open( x-1, y );  Open( x+1, y );
    Open( x, y+1 );
  }
  
  return 正常にマスを開いたという処理結果を示す値か何か;
}

返信

“C言語何でも質問掲示板” へ戻る