javascriptで不思議のダンジョン自動生成

Roguelike(Rogue, NetHack,トルネコ,etc)のダンジョンを自動生成するプログラムです。
javascriptにもだいぶ慣れてきたので、やってみました。
http://eva-lu-ator.net/~gemma/geocities/jscont/dungeon.html

昔に書いた解説はこちら。 http://racanhack.sourceforge.jp/rhdoc/index.html
Gauche で書いた例はこちら。 id:Gemma:20070617


以下、解説。
まずは、[40][80]の2次元配列を用意して、初期化して、表示してみましょう。
実例: http://eva-lu-ator.net/~gemma/geocities/jscont/dungeon_a.html

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>Dungeon</title>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <script src="./lib/prototype.js" type="text/javascript"></script>
  <script type="text/javascript" language="javascript">
  // <![CDATA[

var dungeon_width = 80;
var dungeon_height = 40;

dungeon = new Array(dungeon_height);
for (j=0; j < dungeon_height; j++) {
   dungeon[j] = new Array(dungeon_width)
   for (i=0; i < dungeon_width; i++) {
     dungeon[j][i] = '.';
   }
}

function main() {
  dungeon.each(function(x) {
    var elt = document.createElement('code');
    elt.appendChild(document.createTextNode(x.join('')));
    $('main').appendChild(elt);
    $('main').appendChild(document.createElement('br'));
  });
}

window.onload = main;
  // ]]>
  </script>
</head>
<body>
<div id="main"></div>
</body>
</html>

では次に区画分け。

実例: http://eva-lu-ator.net/~gemma/geocities/jscont/dungeon_b.html
肝はsplit関数で、

function split(rect) {
  if ((rect.x1 - rect.x0 < margin * 2) ||
      (rect.y1 - rect.y0 < margin * 2) ||
      (random_range(0,5) === 0)) {
  //以上が終了条件
    return [rect];
  } else {
    if (random_range(0,2) === 0) {
      //縦に分割
      var a = random_range(rect.y0 + margin, rect.y1 - margin);
      return [new rectangle(rect.x0, rect.y0, rect.x1, a),
	      new rectangle(rect.x0, a, rect.x1, rect.y1)].map(split).flatten();
    } else {
      //横に分割
      var a = random_range(rect.x0 + margin, rect.x1 - margin);
      return [new rectangle(rect.x0, rect.y0, a, rect.y1),
	      new rectangle(a, rect.y0, rect.x1, rect.y1)].map(split).flatten();
    }
  }
}

となっています。
map(split).flatten(); で、どんどん区画を再帰で細かくしていって最後のflattenで1次元の配列にしています。

最終的には、それをこんなふうに、つないでやるわけです。

これで、「どの部屋も必ず繋がっている」ことが保証されます。
この保証さえあれば後は自由に、通路を足したり、隠し部屋を作ったり、できます。

つなぐ処理がmake_corriderです。

function make_corrider(partitions, rooms) {
  var connect_list = new Array();
  
  for (var i = 0; i < partitions.length - 1; i++)
    connect_list.push([i, i + 1]);
    
  var connect = function(p0,p1,r0,r1) {
    if (p0.y1 == p1.y0) {
      var a = random_range(r0.x0, r0.x1);
      var b = random_range(r1.x0, r1.x1);
      fill(new rectangle(a, half(p0.y0 + p0.y1), a, p0.y1));
      fill(new rectangle(b, half(p1.y0 + p1.y1), b, p1.y0));
      fill(new rectangle(a, p0.y1, b, p1.y0));
    } else if (p0.x1 == p1.x0) {
      var a = random_range(r0.y0, r0.y1);
      var b = random_range(r1.y0, r1.y1);
      fill(new rectangle(half(r0.x0 + r0.x1), a, p0.x1, a));
      fill(new rectangle(half(r1.x0 + r1.x1), b, p1.x0, b));
      fill(new rectangle(p0.x1, a, p1.x0, b));
    }
  };
  
  connect_list.each(function(x){
                         connect(partitions[x[0]],partitions[x[1]],rooms[x[0]],rooms[x[1]])});
}

分割の方向が縦か横かを判断してから、それに応じた通路を書いてやります。通路の形は、2回曲がり角があるような、Z字、N字です。
実例: http://eva-lu-ator.net/~gemma/geocities/jscont/dungeon_c.html

これで、「どの部屋も必ず繋がっている」ことが保証されています。
しかし、ただの一本道になって迷路としてつまらないので、通路を足してやります。
そこでmake_corriderのconnect_listに、座標が隣り合っている区画のペアを、ランダムに追加してやります。

  for (var i = 0; i < partitions.length; i++)
    for (var j = i + 1; j < partitions.length; j++)
      if (random_range(0,5) == 0)
	if (partitions[i].x0 == partitions[j].x1 ||
	    partitions[i].x1 == partitions[j].x0 ||
	    partitions[i].y0 == partitions[j].y1 ||
	    partitions[i].y1 == partitions[j].y0)
	  connect_list.push([i,j]);

これで完成です。
実例: http://eva-lu-ator.net/~gemma/geocities/jscont/dungeon.html