最新ダウンロード [応用編・立体迷路ゲームDL]

No001 迷路作成プログラムの製作

コンパイラ : VC++ 6.0
プログラム種類 : Windowsコンソールアプリケーション

はじめに
私が最初に作った本格的なプログラムが高校生のころに作った迷路ゲームでした。
迷路の中に実際にいるような立体表示で、迷路のなかを歩き回って出口にたどりつくという単純でありがちなゲームでしたが、 迷路を作成するロジックも自分でいろいろ研究して考えたものです。 そこで、第1回はこの迷路を作るプログラムを製作しようと思います。

どんな迷路をつくるか
図1に示すような迷路を作成します。迷路の特徴は
・全体が長方形の平面、プログラムではサイズを指定できるようにします。
・通路と壁の幅は同じ長さ。厚さが2つ分以上の通路や壁は存在しません。
・通路のどの地点からどの地点まででも到達するルートが1つだけあります。
 言い換えれば、スタートとゴールを通路上のどの地点にも設定できます。

図1 作成する迷路の例

迷路の作り方
私が自分で名付けた方法ですが、穴掘り法を使用します。 簡単に説明しますと、通路を掘り進みながら迷路を作成していきます。
具体的には以下に示す手順で作成します。
1. 座標(1,1)に最初の穴(通路)空け、ここが穴掘りのスタート地点となります。
  (迷路全体の左上隅を座標(0,0)とします。)
2. 上下左右で穴掘り可能な方向を探します。
  穴掘り可能の条件:2マス進んだところがマップ内でまだ通路になっていない場所であること。
2−1. 穴掘り可能な方向があった場合、その方向に2マス掘り進みます。
(複数ある場合はランダムに選択します。) 手順2へ
2−2. 穴掘り可能な方向がなかった場合、そこまで掘り進んできたルートを2マスだけ逆戻りします。
逆戻りできない場合(スタート地点)迷路作成終了。 それ以外は手順2へ。

手順は非常に簡単です。本当にこれだけで迷路が作れるでしょうか。
それでは次からは実際にプログラムを作っていきます。

プログラムの仕様
使用する開発ツールはVC++6.0で、言語はC++を使用します。
プログラムはWindowsのコマンドラインアプリケーションとして作成します。
プログラムの簡単な仕様を表1に示します。

表1 プログラム仕様
プログラム名称 IMaze
入力 迷路全体(マップ)のサイズをコマンドライン引数で受け取ります。
IMaze <横サイズ> <縦サイズ>
横サイズ・縦サイズはともに3以上。
出力 迷路のイメージをキャラクタ表現で標準出力(画面)に出力します。
壁  ・・・ '#'(シャープ)
通路 ・・・ '.'(ピリオド)
行末 ・・・ 'CR'+'LF'

入出力部分の作成
最初に、入力(引数の受け取り)と出力(迷路の表示)の機能のみを実現したプログラムを作成します。 入出力部分を最初に作るのは、結果を見ながらプログラミングが出来てデバッグに便利だからです。
実際に迷路作成を行うのは CMaze クラスで、迷路全体(マップ)のサイズを渡してオブジェクトを生成すると 同時に迷路も作成します。迷路の内容呼び出しには Get メンバ関数を位置を指定して呼び出すことによって 指定位置のキャラクタを受け取ります。迷路作成は Make メンバ関数で行っていますが、現時点では マップの初期化(壁キャラクタで全体を埋める)しか行っていないため、プログラムを実行しても 壁キャラクタだけの迷路が表示されます。
ここからは最新のソースと実行ファイルもアップしますので、興味のある方はダウロードしてください。

CMaze クラスの定義
既にスケルトンは出来ていますが、ここで CMaze (迷路作成)クラスに構築すべきメンバを上げてみることに します。表2がそのメンバリストです。
表2 CMaze クラスメンバ
int m_sizeX 迷路横サイズ
int m_sizeY 迷路縦サイズ
char* m_mapMaze 迷路マップ情報
迷路全体をキャラクタ表現で保持する。
キャラクタの総数は(迷路横サイズ)×(迷路縦サイズ)。
const static char CHAR_WALL '#'  壁キャラクタ
const static char CHAR_WAY '.'  通路キャラクタ
const static char CHAR_MARK 'M'  足跡キャラクタ(逆戻りのためのマーカーに使用します)
const static int aryStepX[4] 移動方向からX座標を更新するための配列
const static int aryStepY[4] 移動方向からY座標を更新するための配列
CMaze(int sizeX=3, int sizeY=3) コンストラクタ
迷路サイズを指定してオブジェクトを構築します。
char Get(int, int) マップの指定位置のキャラクタを返します。
void Put(int, int, char) マップの指定位置にキャラクタを設定します。
int Make() 迷路作成メイン
int GetNextStep(int* const
, int, int)
指定位置で穴掘り可能な方向を全てみつけて配列に設定し、見つかった数をかえします。
int SearchMark(int, int) 指定位置から逆戻りするために足跡マーカーの方向を見つけます。

CMaze クラスの実装
CMaze クラスの主なメンバについて解説します。

  
const int CMaze::aryStepX[4] = {0, 0, -1, 1};
const int CMaze::aryStepY[4] = {-1, 1, 0, 0};
    
この二つの定数配列は、上下左右のマスへの相対座標を格納しています。
例えば、上のマスへはX座標は変化無しでY座標は−1(Y軸は下方向を+としています)ですので
aryStepX[0] = 0 aryStepY[0] = -1 となっています。
上下左右のマス判定や移動に使用します。


int CMaze::GetNextStep(int* const aryWork, int posX, int posY)
{
    int i, cntWay=0;
    for (i = 0; i < 4; i++){
        int posNextX = posX + aryStepX[i] * 2;
        int posNextY = posY + aryStepY[i] * 2;
        if (posNextX < 1)    continue;
        if (posNextY < 1)    continue;
        if (posNextX >= m_sizeX-1)   continue;
        if (posNextY >= m_sizeY-1)   continue;
        if (Get(posNextX, posNextY) != CHAR_WALL)   continue;
        aryWork[cntWay] = i;
        cntWay++;
    }
    return cntWay;
}
    
このメンバ関数は、穴掘り可能な方向を全てみつけて配列に設定し、見つかった数をかえします。
上下左右のマスへの相対位置を格納したテーブルを利用することにより、別々に判定することなく 繰り返し処理で全ての方向を判定することができます。
相対位置を2倍しているのは、壁の厚みを考慮して2マス先を判定するためです。
リターン値は見つかった数で、0ならば穴掘り可能な方向はないということです。

int CMaze::SearchMark(int posX, int posY) 
{ 
    int i; 
    for (i = 0; i < 4; i++){ 
        int posNextX = posX + aryStepX[i]; 
        int posNextY = posY + aryStepY[i]; 
        if (posNextX < 1)    continue; 
        if (posNextY < 1)    continue; 
        if (posNextX >= m_sizeX-1)   continue; 
        if (posNextY >= m_sizeY-1)   continue; 
        if (Get(posNextX, posNextY) == CHAR_MARK)   return i; 
    } 
    return 4; 
}
    
GetNextStep と同様に繰り返し処理で上下左右位置のマーカーを探しています。
マーカーが複数見つかることはありえないので、見つかった時点でその方向をリターン値にして 終了します。マーカーが見つからなかった場合はリターン値が4になります。

int CMaze::Make()
{
    //              マップ全体を壁キャラクタで初期化
    int i;
    for (i = 0; i < m_sizeX*m_sizeY; i++){
        m_mapMaze[i] = CHAR_WALL;
    }
    //              乱数の初期化
    srand((unsigned int)time(NULL));
    //              穴掘り人配置
    int posWorkerX = 1;
    int posWorkerY = 1;
    Put(posWorkerX, posWorkerY, CHAR_MARK);
    //              全ての穴掘り完了まで
    while(true) {
        //              穴掘り可能方向の取得
        int iStep, aryStepIndex[4];
        iStep = GetNextStep(aryStepIndex, posWorkerX, posWorkerY);
        if (iStep == 0){
            //              穴掘り不可の場合後戻り
            Put(posWorkerX, posWorkerY, CHAR_WAY);
            iStep = SearchMark(posWorkerX, posWorkerY);
            if (iStep >= 4){
                //              後戻りができない(穴掘り完了)
                break;
            }
            //              穴掘り人を2マス後戻りさせる
            posWorkerX += aryStepX[iStep];
            posWorkerY += aryStepY[iStep];
            Put(posWorkerX, posWorkerY, CHAR_WAY);
            posWorkerX += aryStepX[iStep];
            posWorkerY += aryStepY[iStep];
        }
        else {
            //              穴掘り可能の場合乱数で方向を決定
            int iSelect = aryStepIndex[rand() % iStep];
            //              穴掘り人を2マス進める
            posWorkerX += aryStepX[iSelect];
            posWorkerY += aryStepY[iSelect];
            Put(posWorkerX, posWorkerY, CHAR_MARK);
            posWorkerX += aryStepX[iSelect];
            posWorkerY += aryStepY[iSelect];
            Put(posWorkerX, posWorkerY, CHAR_MARK);
        }
    }

    return 0;
}
    
迷路作成の本体部分です。基本的に、前述の”迷路の作り方”で説明した手順をソースコードに しているだけなので、読み比べるとなにをしているか理解しやすいと思います。
穴掘りの方向決定には乱数を使用しています。

以上でプログラムは完成です。

プログラムの実行
コンソールアプリケーションなので、DOSモードまたはDOS窓で実行します。

実行例
D:\IMaze>imaze 41 15
Maze size : 41 x 15
#########################################
#.#.......#.....#.......#.......#.......#
#.#.#####.###.#.###.###.###.#.#.#.#####.#
#.#.#.....#...#.....#.#...#.#.#.#.#...#.#
#.#.#.#####.#########.###.###.#.###.#.#.#
#.#.#.#...#.....#.........#...#.....#...#
#.#.#.###.#.###.###.#######.###########.#
#...#...#.....#...#...#.....#.....#.....#
#######.###.#####.###.#.###.#####.#.#####
#.....#...#.#.....#...#.#...#.....#.#...#
###.#.###.#.#.#####.#####.###.#####.#.###
#...#.#...#.#...#...#...#.#.#...#...#...#
#.#####.#######.#.###.#.#.#.#.#.#.#####.#
#...............#.....#...#...#.........#
#########################################
    

最後に
今回は特にバグらしいものも見つからなかったので、これで一応完成ということになります。
ただ、キャラクタ表現に多少不満が残ります。HTMLのテーブルタグで体裁を整えて表示を しようかと考え、IMaze の出力をCSV形式にしてそれをHTMLに変換するという方法を考えましたが、 いずれにしろキャラクタ表現ではそれほど期待できないでしょう。
そこでセルの色設定で迷路を表現するテーブルタグを出力するようにしました。最新のプログラムソース と実行ファイルはページ一番下の「最新バージョン」にあります。
CSVファイルをHTMLテーブルタグに変換するアイデアは捨てがたいので、次回の公開プログラミングで 取り上げることにします。