1日目

はじめに

これまでに学んだC++言語の機能を使ってプログラムを作成します. ここでは,簡単な迷路作成プログラムをC++のクラスを使って実現します.

迷路作成アプリケーションの設計

迷路を自動的に作成するアプリケーションに必要な要素を考えましょう. まず,迷路の完成形をイメージします. ここでは,下に示すような2次元の迷路を考えてみましょう.
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    S □ □ □ □ □ □ □ ■
    ■ □ ■ ■ ■ ■ ■ □ ■
    ■ □ ■ □ ■ □ □ □ ■
    ■ □ □ □ ■ ■ ■ □ ■
    ■ □ ■ □ ■ □ ■ □ ■
    ■ □ ■ □ ■ □ ■ □ ■
    ■ □ ■ □ □ □ ■ □ G
    ■ ■ ■ ■ ■ ■ ■ ■ ■        
    
この図では黒塗り四角(■)が壁,白塗り四角(□)が通路,Sはスタート地点,Gがゴール地点を意味しています.
迷路を表示するために必要な要素
迷路の構造に必要な要件
実現したい迷路アプリケーションの要件
更に
実現したい迷路アプリケーションの要件

迷路データの格納

まず,迷路を構成するためのデータを格納するクラスについて考えていきましょう. ここでは,迷路データ格納のためのクラスとしてMazeMapクラスを考えていきます.

MazeMap.h

#ifndef MAZEMAP_H
#define MAZEMAP_H

/**
 * @class MazeMap
 * @breif 迷路情報保持用クラス
 */
class MazeMap {
public:
    /** 「壁」の表示 */
    static const char wall = 'O';
    /** 「通路」の表示 */
    static const char passage = ' ';
    /** 「スタート地点」の表示 */
    static const char start = 'S';
    /** 「ゴール地点」の表示 */
    static const char goal = 'G';

    /**
     * コンストラクタ
     * @param w 迷路幅の基底
     * @param h 迷路高の基底
     * @attention 実際の迷路幅は2w+1,迷路高は2h+1     
     */
    MazeMap(int w, int h);

    /**
     *  デストラクタ
     */
    virtual ~MazeMap();

    /**
     * 迷路幅用ゲッタ
     * @return 迷路幅
     */
    int getWidth();

    /**
     * 迷路高用ゲッタ
     * @return 迷路高
     */
    int getHeight();

    /**
     * 指定座標が迷路の範囲内に存在するかをチェック
     * @param x x座標
     * @param y y座標
     * @retval true 迷路の範囲内
     * @retval false 迷路の範囲外
     */
    bool isInside(int x, int y);

    /**
     * 指定座標が壁かどうかをチェック
     * @param x x座標
     * @param y y座標
     * @retval true 壁
     * @retval false 壁でない
     * @attention 指定座標が迷路範囲外であればfalseを返す
     */
    bool isWall(int x, int y);

    /**
     * 指定座標のマップ情報のセッタ
     * @param x x座標
     * @param y y座標
     * @param c 要素
     * @retval true 指定座標が迷路の範囲内
     * @retval false 指定座標が迷路の範囲外
     * @attention 要素を指定しない場合,通路とする
     */
    bool set(int x, int y, char c = passage);

    /**
     * 指定座標のマップ情報のゲッタ
     * @param x x座標
     * @param y y座標
     * @return マップ情報
     * @attention 迷路範囲外の座標を指定した場合,'\0'を返す
     */
    char get(int x, int y);

    /**
     * マップ情報の初期化
     * @attention 全ての要素を「壁」にする
     */
    void clear();
private:
    /** 迷路幅 */
    int width;
    /** 迷路高 */
    int height;
    /** 迷路情報 */
    char **data;
};

#endif /* MAZEMAP_H */

MazeMap.cpp

#include "MazeMap.h"

MazeMap::MazeMap(int w, int h) {
    width = 2 * w + 1;
    height = 2 * h + 1;
    data = new char *[width];
    for (int i = 0; i < width; ++i) {
        data[i] = new char[height];
    }
}

MazeMap::~MazeMap() {
    for (int i = 0; i < width; ++i) {
        delete[] data[i];
    }
    delete[] data;
}


int MazeMap::getWidth() {
    /* ここを実装してください */
}

int MazeMap::getHeight() {
    /* ここを実装してください */
}

bool MazeMap::isInside(int x, int y) {
    /* ここを実装してください */
}

bool MazeMap::isWall(int x, int y) {
    /* ここを実装してください */
}

bool MazeMap::set(int x, int y, char c) {
    /* ここを実装してください */
}

char MazeMap::get(int x, int y) {
    /* ここを実装してください */
}

void MazeMap::clear() {
    /* ここを実装してください */
}

メンバ変数

迷路を構成するためのデータとして を保持する必要があり,これをメンバ変数として格納します. 迷路幅と迷路高はそれぞれint型変数として保持し,1マス分の壁や通路の情報はchar型のデータとして表現することにします. マップ情報を格納する2次元配列は動的にメモリを確保することを想定し,char型のポインタのポインタとして宣言しています.
迷路幅,迷路高とマップ情報は,プログラムの保守のためにカプセル化しましょう. 例えば,widthをpublicとして宣言しておくと,MazeMapのインスタンスから直接.widthとしてメンバ変数にアクセスできてしまいます. このような場合,もし,MazeMapクラスのwidthを別の変数名に変更した時,該当する箇所をすべて変更しなければならず,プログラムの保守に大きな影響を及ぼす可能性があります. また,メンバ変数の外部からの予期せぬ変更によりプログラムが正常に動作しなくなる可能性があります.
カプセル化
プログラムコードとプログラムコードが扱うデータを一体化して,外部の干渉や誤用から両者を保護するための仕組みの事を指します. プログラムコードが扱うデータは原則,非公開(private)にして,ゲッタとセッタ(アクセッサ)を用いてデータアクセスを行う事が好ましいです.
さらに,壁と通路のそれぞれを表す文字を定数として定義しておきます. ここでは,壁のデータとして,'O'とし,通路のデータとして,' '(空白)と定義することにします. さらに,スタート地点とゴール地点の文字定数も定義しています.
wall, passage, start, goalはdefineでも定数定義出来ますが,defineだとグルーバルな定数となってしまいます(型不定のdefineを用いた定数定義はC++では推奨されません). C++で定数定義する場合,constを利用したほうが良いです. クラス内でのみ利用できればよいような定数を定義する場合,static変数とて初期化しなければなりません. これらは,外部から変更が不可能なのでpublicなメンバ変数としても大丈夫です.

コンストラクタ

次に,コンストラクタについて考えましょう.
MazeMapクラスをインスタンス化(実体化)する際に,迷路幅と迷路高のサイズを指定して,一度インスタンス化した後は迷路のサイズは変更しないものとします. コンストラクタが呼び出された際は,引数として渡されたサイズ情報をメンバ変数に格納し,指定されたサイズに必要なマップ情報の領域を動的に確保するようにします. ここで,サイズ情報に関して,迷路幅と迷路高を奇数にするために,実際の迷路のサイズは,コンストラクタ引数として受け取った行数と列数をそれぞれ2倍して1足したものとします. 例えば,幅と高さのサイズとして(2,3)を与えた場合,迷路のサイズとしては(5,7)になります.

デストラクタ

次に,デストラクタについて考えましょう.
今回のプログラムでは,明示的にデストラクタを呼び出すことはありませんが,コンストラクタの中で動的にメモリを確保している場合は,デストラクタでそれを必ず解放する事を忘れないようにしましょう.
デストラクタが明示的に定義されていない場合,デフォルトとなるデストラクタが呼び出されることになりますが,デフォルトのデストラクタはコンストラクタの中で動的に確保したメモリの解放までは面倒をみてくれません. そのため,オブジェクトをnewしたり deleteしたりすることを頻繁に繰り返すと,メモリリークする可能性が上がり,最悪の場合,実行に必要なメモリを確保できずに,プログラムの実行が継続できなくなることもあります.

メンバ関数

最後に,メンバ関数について考えましょう.
必要になりそうなメンバ関数としては以下のものが考えられます.
isWall()は迷路の範囲外の場合,「壁でない」として判定しましょう.

set()は第3引数が省略された場合,「通路」としてセットするようにディフォルト引数で設定します.
デフォルト引数
関数の呼び出し時に指定されていない引数があるとき,対応する仮引数にディフォルト値を与えることができます. ディフォルト仮引数は複数設定できますが,ディフォルト値を持たないすべての仮引数よりも右側に記述する必要があります.
set()を呼び出すとき,マップ情報に文字定数を直接セットせずに,引数として与えられている文字変数をセットします. もし,下の悪い例のようにしてしまうと,「壁」の定数定義を変更したときに,該当する箇所をすべて変更しなければならなくなってしまいます.

clear()が呼ばれると,マップの全ての要素が「壁」になるようにします.
MazeMapクラスの未実装の7つのメンバ関数を実装しましょう. 以下のテストプログラムを用いて,作成したプログラムの動作を確認してみましょう.

main.cpp

#include <iostream>
#include "MazeMap.h"

/**
 * 迷路の表示
 * @param mazeMap mazeMapインスタンスの参照
 */
void printMap(MazeMap &mazeMap) {
    for (int y = 0; y < mazeMap.getHeight(); ++y) {
        for (int x = 0; x < mazeMap.getWidth(); ++x) {
            std::cout << mazeMap.get(x, y); // get()を使って表示
        }
        std::cout << std::endl;
    }
    std::cout << std::endl;
}

/**
 * 指定座標を通路に設定
 * @param mazeMap MazeMapインスタンスの参照
 * @param x x座標
 * @param y y座標
 */
void testSet(MazeMap &mazeMap, int x, int y) {
    std::cout << "set(" << x << ", " << y << ") = " << (mazeMap.set(x, y) ? "success" : "failure") << std::endl;
}

/**
 * 指定座標が迷路範囲内かどうかをチェックし,結果を出力
 * @param mazeMap mazeMapインスタンスの参照
 * @param x x座標
 * @param y y座標
 */
void testIsInside(MazeMap &mazeMap, int x, int y) {
    std::cout << "(" << x << ", " << y << ") = " << (mazeMap.isInside(x, y) ? "inside" : "outside") << std::endl;
}

/**
 * 指定座標が壁かどうかをチェックし,結果を出力
 * @param mazeMap mazeMapインスタンスの参照
 * @param x x座標
 * @param y y座標
 */
void testIsWall(MazeMap &mazeMap, int x, int y) {
    std::cout << "(" << x << ", " << y << ") " << (mazeMap.isWall(x, y) ? "= " : "!= ") << "wall" << std::endl;
}

int main() {
    MazeMap mazeMap(2, 1); // サイズをいろいろ変えてみる

    // マップのサイズを表示
    // (マップのサイズはコンストラクタに渡した値を 2倍して1足した値になる)
    std::cout << "width=" << mazeMap.getWidth() << std::endl;
    std::cout << "height=" << mazeMap.getHeight() << std::endl;

    // 迷路データクリア (全て壁 'O'になる)
    mazeMap.clear();

    // set()の動作チェック
    std::cout << "##### Test for set #####" << std::endl;
    testSet(mazeMap, 0, 1); // successになるはず
    testSet(mazeMap, 1, 1); // successになるはず 
    testSet(mazeMap, 2, 1); // successになるはず 
    testSet(mazeMap, 2, 2); // successになるはず 
    testSet(mazeMap, 6, 4); // failureになるはず 
    testSet(mazeMap, -1, 0); // failureになるはず 

    // 中身を表示 (意図した通りにセットされているかをチェック)
    std::cout << "##### Print Map #####" << std::endl;
    printMap(mazeMap);

    // isInside()の動作チェック
    std::cout << "##### Test for isInside #####" << std::endl;
    testIsInside(mazeMap, 0, 0); // inside になるはず
    testIsInside(mazeMap, mazeMap.getWidth() - 1, 0); // inside になるはず
    testIsInside(mazeMap, 0, mazeMap.getHeight() - 1); // inside になるはず
    testIsInside(mazeMap, -1, -1); // outsideになるはず
    testIsInside(mazeMap, mazeMap.getWidth() + 1, 0); // outsideになるはず
    testIsInside(mazeMap, 0, mazeMap.getHeight() + 1); // outsideになるはず
    testIsInside(mazeMap, mazeMap.getWidth(), 0); // outsideになるはず
    testIsInside(mazeMap, 0, mazeMap.getHeight()); // outsideになるはず

    // isWall()の動作チェック
    std::cout << "##### Test for isWall #####" << std::endl;
    testIsWall(mazeMap, 0, 0); // wall になるはず
    testIsWall(mazeMap, 1, 0); // wall になるはず
    testIsWall(mazeMap, -1, -1); // not wall になるはず (範囲外は壁ではない)
    testIsWall(mazeMap, 1, 1); // not wall になるはず
    testIsWall(mazeMap, 2, 1); // not wall になるはず
    std::cout << std::endl;

    // 再び迷路データクリア (全て壁 'O'になる)
    mazeMap.clear();
    // 中身を表示 (きちんとクリアされているかをチェック)
    std::cout << "##### Print Map #####" << std::endl;
    printMap(mazeMap);
    return 0;
}

出力例

width=5
height=3
##### Test for set #####
set(0, 1) = success
set(1, 1) = success
set(2, 1) = success
set(2, 2) = success
set(6, 4) = failure
set(-1, 0) = failure
##### Print Map #####
OOOOO
   OO
OO OO

##### Test for isInside #####
(0, 0) = inside
(4, 0) = inside
(0, 2) = inside
(-1, -1) = outside
(6, 0) = outside
(0, 4) = outside
(5, 0) = outside
(0, 3) = outside
##### Test for isWall #####
(0, 0) = wall
(1, 0) = wall
(-1, -1) != wall
(1, 1) != wall
(2, 1) != wall

##### Print Map #####
OOOOO
OOOOO
OOOOO

迷路の生成

穴掘り法

迷路作成のアルゴリズムの一つである穴掘り法について説明します. ここでは,迷路幅と迷路高が両方とも奇数とします. ここでの例では,迷路幅と迷路高を同じく9としますが,必ずしも同じである必要はありません. 左上を原点(0,0)として,座標の要素が両方偶数の座標は必ず「壁」となり,座標の要素が両方奇数の座標は必ず「通路」となります. 以降,座標の要素が両方偶数の座標を偶数座標,座標の要素が両方奇数の座標を奇数座標と呼びます.
最初に全てのマスを「壁」として初期化します. 以降の例では,■を「壁」,□を「通路」として表します.
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
奇数座標からランダムに1つ選択します. 下図では,左上を原点(0,0)として右に3,下に3の座標(3,3)が選択されたものとします. (「○」は現在の注目点を意味します)
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ○ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
次に穴を掘り進める方向をランダムに選択し,その方向に2マス先のマスを調べて,まだ「通路」でなければ「通路」にして2マス分を通路にします. 以下の例では現在の注目点から下に「通路」を延ばそうして,下に2マス先を調べて,まだ「通路」になっていなかったので,下に2マス分延ばした状態です. (同時に注目点も移動させます)
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ■ ■ ■ ■ 
    ■ ■ ■ □ ■ ■ ■ ■ ■ 
    ■ ■ ■ ○ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
移動先のマスで先ほどと同様に,次に掘り進める方向をランダムに選択し,選択した方向の2マス先を調べ,「通路」でない場合にはそちらの方向に「通路」を延ばしていくという操作を繰り返します. もし,ランダムに選択した方向の2マス先が「通路」であった場合は,「通路」を延ばす作業はここで一旦終了します. 以下の図において,左に「通路」を延ばそうとした場合,左に2マス先は既に「通路」になっているので,これ以上「通路」を延ばせません.
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ○ ■ ■ ■
    ■ ■ ■ □ ■ □ ■ ■ ■
    ■ ■ ■ □ □ □ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
「通路」を延ばせなくなった場合は,新たに奇数座標をランダムに選択し,そのマスを起点として「通路」を延ばす作業を行います. 以上の作業を,「通路」を延ばせるマスが無くなるまで繰り返します. 以下の図では,どの奇数座標をとっても,通路を延ばすことができないので終了となります.
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ □ □ □ □ □ □ □ ■
    ■ □ ■ ■ ■ ■ ■ □ ■
    ■ □ ■ □ ■ □ □ □ ■
    ■ □ ■ □ ■ ■ ■ □ ■
    ■ □ ■ □ ■ □ ■ □ ■
    ■ □ ■ □ ■ □ ■ □ ■
    ■ □ □ □ □ □ ■ □ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■

穴掘り法(簡易版)

穴掘り法のアルゴリズムを実装する場合,「通路」を延ばせなくなった際に,「通路」を延ばすことができるマスの中から1つのマスをランダムに選択する作業が少し面倒です. そこで,この処理を簡略化したアルゴリズムを考えてみましょう. 「通路」を延ばしていく際に延ばせる方向がある限り延ばしていき,どの方向にも「通路」が延ばせなくなった場合には,これまで進んできた「通路」上で一つ前のマスに戻って他の方向に「通路」が延ばせないかを調べていくという方法を考えてみましょう. 穴掘り法のアルゴリズムの場合では,以下の状態で左に「通路」を延ばそうとして延ばせなかった場合は,新たな起点から「通路」を延ばすようにしていましたが,簡易アルゴリズムでは,他の方向を調べて「通路」を延ばせるようであれば,そちらに「通路」を延ばしていきます.
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ■ ■ ■ ■
    ■ ■ ■ □ ■ ○ ■ ■ ■
    ■ ■ ■ □ ■ □ ■ ■ ■
    ■ ■ ■ □ □ □ ■ ■ ■
    ■ ■ ■ ■ ■ ■ ■ ■ ■
(注目点○では下と左には「通路」を延ばせませんが,右と上にはまだ「通路」を延ばせます.)
どの方向にも「通路」を延ばせなくなった場合は,これまでたどってきた「通路」を(2マスずつ)戻りながら「通路」を延ばせる方向がないかを探していきます. もし「通路」を延ばせるマスが見つかれば,そこから「通路」を延ばしていきます. この作業を繰り返していき,迷路作成を開始した最初のマスまで戻ってきたら作業は終了となります. つまり,(意味的には)深さ優先探索で2次元平面上に「通路」を延ばしているわけですが,こうすることで,端から順に迷路が作られるようになり,深さ優先探索での処理が完了した後には,「通路」を延ばすことが出来るマスが存在しないことが保証されます. ただし,このアルゴリズムでは「通路」上での枝分かれが発生しない単調な迷路になりがちであるため,迷路としては簡単なものになってしまいます.
穴掘り法(簡易版)の参考資料

迷路作成クラス

ここでは,「穴掘り法」の簡易アルゴリズムで迷路を作成するクラスをMazeMakerSimpleと名付けます. メンバ変数としては,MazeMapクラスのインスタンスのポインタを保持しており,MazeMakerSimpleクラスのインスタンスを生成する際に呼ばれるコンストラクタに引数として渡しています. (コンストラクタの役目は,MazeMapクラスのインスタンスへのポインタをメンバ変数に格納しておくだけです.) メンバ関数としては,publicなものとしてgenerate(),privateなものとしてcheckDirection()とcheckPoint()があります. 迷路作成作業は,generate()を呼び出すことで実行されます. generate()は,関数中で,checkPoint()とcheckDirection()を呼び出しながら迷路を作成していきます. checkPoint()では,現在の位置からランダムに「通路」を延ばす方向を決定し,時計周りあるいは反時計周りに,checkDirection()を呼び出すことで,2マス先が「壁」かどうかをチェックしながら「通路」を延ばして行きます. checkDirection()では,引数により指定された方向の2マス先が「壁」かどうかを調べ,「壁」であったら(すなわち「通路」でなかったら)「通路」を2マス分延ばします. その後,checkPoint()を呼び出すことで,「通路」を延ばした先のマスで延ばせるだけの「通路」を延ばす処理を行います. checkPoint()とcheckDirection()の2つのメンバ関数がそれぞれお互いを呼び出し合いながら,深さ優先で「通路」を延ばす処理を行なっています.

MazeMakerSimple.h

#ifndef MAZEMAKERSIMPLE_H
#define MAZEMAKERSIMPLE_H

#include <cstdlib>
#include "MazeMap.h"

class MazeMakerSimple {
public:
    /**
     * コンストラクタ
     * @param mazeMap MazeMapインスタンス
     */
    MazeMakerSimple(MazeMap *mazeMap);
    /**
     * 迷路生成
     */
    void generate();
private:
    /** MazeMapインスタンス */
    MazeMap *mazeMap;
    /**
     * 座標(x,y)において dで指定された方向をチェック
     * @param x x座標
     * @param y y座標
     * @param d 方向を示すインデックス
     */
    void checkDirection(int x, int y, int d);
    /**
     * 座標(x,y)において全方向をチェック
     * @param x x座標
     * @param y y座標
     */
    void checkPoint(int x, int y);
};

#endif /* MAZEMAKERSIMPLE_H */

MazeMakerSimple.cpp

#include "MazeMakerSimple.h"

MazeMakerSimple::MazeMakerSimple(MazeMap *mazeMap) {
    this->mazeMap = mazeMap;
}

void MazeMakerSimple::generate() {
    // 迷路マップ情報をクリア
    mazeMap->clear();

    // ランダムに迷路作成開始地点を選択(ただし,奇数座標)
    int x = rand() % (mazeMap->getWidth() / 2) * 2 + 1;
    int y = rand() % (mazeMap->getHeight() / 2) * 2 + 1;

    // ランダムに選んだ (x,y)の座標から迷路の作成開始
    checkPoint(x, y);

    // 最後に左上にスタート地点、右下にゴール地点を置いて終了
    mazeMap->set(0, 1, mazeMap->start);
    mazeMap->set(mazeMap->getWidth() - 1, mazeMap->getHeight() - 2, mazeMap->goal);
}

void MazeMakerSimple::checkDirection(int x, int y, int d) {
    // 4つの方向を示す構造体    

    static const struct pos {
        int x, y;
    } steps[] = {
        {0, +1}, // 下
        {+1, 0}, // 右
        {0, -1}, // 上    
        {-1, 0} // 左
    };

    // 引数dの値により,steps[]で示される方向の1つを決定
    int dx = steps[d & 3].x;
    int dy = steps[d & 3].y;

    if (mazeMap->isWall(x + 2 * dx, y + 2 * dy)) { // 指定された方向の2マス先が壁であれば
        mazeMap->set(x + dx, y + dy); // その方向の1マス先を通路にセット
        mazeMap->set(x + 2 * dx, y + 2 * dy); // その方向の2マス先を通路にセット
        // 注目点を2マス先に移して,チェックを継続
        checkPoint(x + 2 * dx, y + 2 * dy);
    }
}

void MazeMakerSimple::checkPoint(int x, int y) {
    if (!mazeMap->isInside(x, y)) { // 迷路の範囲外なら
        return;
    }
    // 指定座標を通路にセット
    mazeMap->set(x, y);

    // 探索する方向を乱数で設定
    int d = rand();
    // 再探索の順番を設定(時計回り or 反時計回り)
    int dd = (rand() & 1) * 2 - 1; // -1 or +1 

    // 4方向に通路が延ばせるか順番にチェック
    for (int i = 0; i < 4; ++i) {
        checkDirection(x, y, d);
        d += dd;
    }
}