迷路を生成する

記事の作成日 : 2020/02/16

突然ですが、迷路を生成したくなったので、生成してみようと思います。
使用するアルゴリズムは、 棒倒し型です。

あと一つ、highlight.js を追加しました。ソースコードが見やすくなったと思います。

完成例

13

先にソースコード

    
        // いろいろな関数のなかでも使うグローバルなやつ を宣言
        let map = "",
            wall = "■",
            air = "□",
            wh = 0;

        // マップを読み込む
        function read(x, y) {
            return map[x + y * wh];
        }

        //マップを読み込む(安全版)
        function SafeRead(x, y) {
            if (x < 0 || y < 0 || x >= wh || y >= wh) {
                return "e";
            }
            return read(x, y);
        }

        // マップに書き込む
        function write(data, x, y) {
            map = stringsel(map, x + y * wh, data);
        }

        // string sel write
        function stringsel(str, sel, data) {
            var startString = "";
            var endString = "";

            if (sel != 0) {
                startString = str.substring(0, sel);
            }
            if (sel != str.length) {
                endString = str.substring(sel + 1, str.length);
            }
            if (sel == 0) {
                return data + endString;
            }
            if (sel == str.length) {
                return startString + data;
            }
            return startString + data + endString;
        }

        // 囲いを作成する関数
        function create_box() {
            let i;
            for (i = 0; i < wh; i++) {
                write(wall, i, 0);
                write(wall, 0, i);
                write(wall, i, wh - 1);
                write(wall, wh - 1, i);
            }
        }

        // 棒を作成する関数
        function create_stick() {
            let x, y, setx = 1,
                sety = 1;

            for (y = 2; y < wh - 1; y++) {
                for (x = 2; x < wh - 1; x++) {
                    console.log(x + "," + y + " / " + setx + "," + sety);
                    if (setx == true && sety == true) {
                        write(wall, x, y);
                    }
                    setx = !setx;
                }
                sety = !sety;
            }
        }

        // 迷路を作成する(棒を倒す)関数
        function create_maze() {
            let x, y, setx = 1,
                sety = 1,
                cnt = 0,
                r, l, u, d, total, target;

            for (y = 2; y < wh - 1; y++) {
                for (x = 2; x < wh - 1; x++) {
                    if (setx == true && sety == true) {
                        write(wall, x, y);
                        if (cnt == 0) {
                            total = "";
                            r = SafeRead(x + 1, y);
                            l = SafeRead(x - 1, y);
                            u = SafeRead(x, y - 1);
                            d = SafeRead(x, y + 1);
                            if (r == air) {
                                total += "r"
                            }
                            if (l == air) {
                                total += "l"
                            }
                            if (u == air) {
                                total += "u"
                            }
                            if (d == air) {
                                total += "d"
                            }

                            target = total[Math.round(Math.random() * (total.length - 1))];

                            if (target == "r") {
                                write(wall, x + 1, y);
                            }
                            if (target == "l") {
                                write(wall, x - 1, y);
                            }
                            if (target == "u") {
                                write(wall, x, y - 1);
                            }
                            if (target == "d") {
                                write(wall, x, y + 1);
                            }
                        }


                        if (cnt == 1) {
                            total = "";
                            r = SafeRead(x + 1, y);
                            l = SafeRead(x - 1, y);
                            d = SafeRead(x, y + 1);
                            if (r == air) {
                                total += "r"
                            }
                            if (l == air) {
                                total += "l"
                            }
                            if (d == air) {
                                total += "d"
                            }

                            target = total[Math.round(Math.random() * (total.length - 1))];

                            if (target == "r") {
                                write(wall, x + 1, y);
                            }
                            if (target == "l") {
                                write(wall, x - 1, y);
                            }
                            if (target == "d") {
                                write(wall, x, y + 1);
                            }
                        }


                    }
                    setx = !setx;
                }
                sety = !sety;
                cnt = 1;
            }
        }

        // 結果表示のための変換
        function convert() {
            let mapcv = "",
                ch;
            for (let i = 0; i < (wh * wh); i++) {
                ch = map[i];
                if (ch == air) {
                    ch = "<span style='color:rgb(0,0,0,0);'>" + wall + "";
                }
                mapcv += ch;
                if (i % wh == wh - 1) {
                    mapcv += "<br>"; //String.fromCharCode(10);
                }
            }
            return mapcv;
        }


        // これが「生成」ボタンが押されて実行される関数です。
        function create() {
            // マップサイズ設定
            wh = document.getElementById("wh").value;
            // 文字の決定
            wall = "■";
            air = "□";
            // マップ作成
            map = air.repeat(wh * wh);

            //囲い作成
            create_box();

            // 棒を作成
            create_stick();

            // 迷路を作成(棒を倒す)
            create_maze();

            // 結果を表示
            document.getElementById("map").innerHTML = convert(); // convert()で人が見られる形に修正してから出力

        }
    
    

迷路の仕組み

棒倒しアルゴリズムを用いて、迷路を作成しました。
簡単なアルゴリズムで、考えた人は天才だと思います。

棒倒しのルール

その1

必ずマップの広さは、奇数であること。
マップの最小は5です。

その2

マップの周りに、囲いを作り、隣接しないように(斜めもダメ)、点(立っている棒)を置く。
◆◆◆◆◆◆◆
◆◇◇◇◇◇◆
◆◇◇◆
◆◇◇◇◇◇◆
◆◇◇◆
◆◇◇◇◇◇◆
◆◆◆◆◆◆◆

その3

1段目の棒は左右上下に倒してもいいが、隣の棒が倒れている方向には倒れないこと。
◆◆◆◆◆◆◆
◆◇◇◇◇◆
◆◇◆◆◇◆
◆◇◇◇◇◇◆
◆◇◆◇◆◇◆
◆◇◇◇◇◇◆
◆◆◆◆◆◆◆

その4

2段目からは、上方向には倒れないことと、隣の棒が倒れている方向には倒れないこと。
◆◆◆◆◆◆◆
◆◇◇◇◇◆
◆◇◆◆◇◆
◆◇◇◇◇◇◆
◆◇◆◇◆
◆◇◇◇◇◆
◆◆◆◆◆◆◆

怪しい例

隣の棒と倒れる方向が重なってはいけません。
◆◆◆◆◆◆◆
◆◇◇◇◇◆
◆◇◆◆◇◆
◆◇◇◇◇◇◆
◆◇◆◆◇◆
◆◇◇◇◇◇◆
◆◆◆◆◆◆◆


先に変数を宣言

先に変数を宣言しておきます。これらは、ほかの関数に使われます。
    
     // いろいろな関数のなかでも使うグローバルなやつ を宣言
     let map = "",
        wall = "■",
        air = "□",
        wh = 0;

    /*
        mapは、迷路のデータが入る予定です。
        wall は壁のデータです。
        air は通路のデータです。が、表示する場合には、透明な■(wall の文字 の 透明)に置換されて、マップが崩れるのを防ぎます。
        wh は迷路の大きさが入ります。
    */
    
    

メイン関数create() を作成

create() はボタンが押された時に実行されるようにします。
    
    function create() {
        /* 
            マップサイズ設定
            <input type="range" id="wh" min="5" max="99" value="13">
            ここから ID"wh" の値を取得します。

            横に数値を付けるには、inputの中にもう一つ下記のコードを配置します。
            oninput="document.getElementById('ここにIDを入れる').innerHTML=this.value"

            HTMLでは、<span id="ここに同じIDを入れる"> を追加します。
        */
        wh = document.getElementById("wh").value;

        /*
            文字の決定
            wall は壁です。
            air は通路です。が文字がずれてしまうので、表示には使っていません。
        */
        wall = "■";
        air = "□";

        // マップ作成 埋め立てる。
        map = air.repeat(wh * wh);

        //囲い作成 下のほうで内容の説明
        create_box();

        // 棒を作成 下のほうで内容の説明
        create_stick();

        // 迷路を作成(棒を倒す)下のほうで内容の説明
        create_maze();

        // 結果を表示
        document.getElementById("map").innerHTML = convert(); // convert()で人が見られる形に修正してから出力

    }
    
    

マップを座標としてデータを取得する関数read()、SafeRead() を作成

マップデータをそのまま取り扱うと非常に不便です。
座標として扱うことで非常に楽になります。
    
    // マップを読み込む 簡単な式。whを使用することに注意。
    function read(x, y) {
        return map[x + y * wh]; // マップデータ1つを取り出す。
    }

    //マップを読み込む(安全版) 0 と wh がリミッターになり、規格外の数値が入っても、エラーにならずに結果に「e」が返ってきます。
    function SafeRead(x, y) {
        if (x < 0 || y < 0 || x >= wh || y >= wh) {
            return "e";
        }
        return read(x, y);
    }
    
    

文字列の中の1文字を変える関数stringsel() を作成

マップの一部に書き込むための関数です。
(本当は、一文字だけ書き換える方法がわからなかったので作った関数です。)
    
    // string sel write
    /*
        マップで使うときは、
        str に map を。
        sel に 編集位置(数値)を。
        data に データを(文字列を上書き)。
    */
    function stringsel(str, sel, data) {
        var startString = "";
        var endString = "";

        if (sel != 0) {
            startString = str.substring(0, sel);
        }
        if (sel != str.length) {
            endString = str.substring(sel + 1, str.length);
        }
        if (sel == 0) {
            return data + endString;
        }
        if (sel == str.length) {
            return startString + data;
        }
        return startString + data + endString;
    }
    
    

囲い作成関数create_box() の作成

マップ上に囲いを作成するための関数を作成します。
    
    // 囲いを作成する関数
    function create_box() {
        let i;
        for (i = 0; i < wh; i++) {
            write(wall, i, 0);
            write(wall, 0, i);
            write(wall, i, wh - 1);
            write(wall, wh - 1, i);
        }
    }
    
    

棒を設置する関数create_stick() を作成

マップ上に棒(点、または壁)を設置します。
これには、左右上下、斜めに隣接しないように配置するようにします。
    
    // 棒を作成する関数
    function create_stick() {
        let x, y, setx = 1,
            sety = 1;

        for (y = 2; y < wh - 1; y++) {
            for (x = 2; x < wh - 1; x++) {
                console.log(x + "," + y + " / " + setx + "," + sety);
                if (setx == true && sety == true) {
                    write(wall, x, y);
                }
                setx = !setx;
            }
            sety = !sety;
        }
    }
    
    

迷路を作成する関数create_maze() を作成

棒を倒して迷路を作成します。
    
    // 迷路を作成する(棒を倒す)関数
        function create_maze() {
            let x, y, setx = 1,
                sety = 1,
                cnt = 0,
                r, l, u, d, total, target;

            for (y = 2; y < wh - 1; y++) {
                for (x = 2; x < wh - 1; x++) {
                    if (setx == true && sety == true) {
                        write(wall, x, y);
                        if (cnt == 0) {
                            total = "";
                            r = SafeRead(x + 1, y);
                            l = SafeRead(x - 1, y);
                            u = SafeRead(x, y - 1);
                            d = SafeRead(x, y + 1);
                            if (r == air) {
                                total += "r"
                            }
                            if (l == air) {
                                total += "l"
                            }
                            if (u == air) {
                                total += "u"
                            }
                            if (d == air) {
                                total += "d"
                            }

                            target = total[Math.round(Math.random() * (total.length - 1))];

                            if (target == "r") {
                                write(wall, x + 1, y);
                            }
                            if (target == "l") {
                                write(wall, x - 1, y);
                            }
                            if (target == "u") {
                                write(wall, x, y - 1);
                            }
                            if (target == "d") {
                                write(wall, x, y + 1);
                            }
                        }


                        if (cnt == 1) {
                            total = "";
                            r = SafeRead(x + 1, y);
                            l = SafeRead(x - 1, y);
                            d = SafeRead(x, y + 1);
                            if (r == air) {
                                total += "r"
                            }
                            if (l == air) {
                                total += "l"
                            }
                            if (d == air) {
                                total += "d"
                            }

                            target = total[Math.round(Math.random() * (total.length - 1))];

                            if (target == "r") {
                                write(wall, x + 1, y);
                            }
                            if (target == "l") {
                                write(wall, x - 1, y);
                            }
                            if (target == "d") {
                                write(wall, x, y + 1);
                            }
                        }


                    }
                    setx = !setx;
                }
                sety = !sety;
                cnt = 1;
            }
        }
    
    

マップを人のわかりやすいように変換する関数convert() を作成

人にマップをわかりやすくする関数を作成します。
    
    // 結果表示のための変換
    function convert() {
        let mapcv = "",
            ch;
        for (let i = 0; i < (wh * wh); i++) {
            ch = map[i];
            if (ch == air) {
                ch = "<span style='color:rgb(0,0,0,0);'>" + wall + "";
            }
            mapcv += ch;
            if (i % wh == wh - 1) {
                mapcv += "<br>";
            }
        }
        return mapcv;
    }
    
    

最後、HTMLのコード

    
    <h2>完成例</h2>
    <input type="range" id="wh" min="5" max="99" step="2" value="13" oninput="document.getElementById('sizeview').innerHTML=this.value">
    <span id="sizeview">13</span>
    <input type="button" value="生成" onclick="create()">
    <div id="map" style="letter-spacing: -6px; line-height: 0.6em;"></div>
    
    


迷路作成に必要な知識は、
  1. 変数
  2. HTML( span div input などなど )
  3. 関数 (function)
  4. Math.random() Math.round()
  5. 加算処理(変数への)
  6. For文
  7. If文
  8. Document.getElementById (.value & .innerHTML)
  9. 理論式 ( < > =< => == != などなど )
このくらいです。

<<前 : for文を使ってみる >>次 : 理論式?不等式?表