ポケダンのダンジョンを模倣したい
公開日:
2022/11/05
ポケダンのダンジョンを模倣したい

こんにちは

konoです。今回の白鷺祭ではオセロAIのようなものを作ろうと頑張っていましたが、どうしてもアルゴリズムの実装がうまくいかなかったので急遽代わりのものを作ることになりました。うーんどうしようかなーーそうだ!

迫りくるマッチョから逃げるゲームを作ろう!

まずフィールド作りから始めることにしました。すぐ終わるやろと思っていたわけですがフィールドを作っていたら白鷺祭の日になっていましたね。ほにゅ...?
 ゲームを完成させることはできませんでしたが、フィールドをつくっただけでもきっとえらいです。というわけでこの記事は、後々作る(かもしれない)ゲームのためのフィールド作成プログラムを書いたよ、というものです。

ポケダンを目標に

 フィールドを作るにあたって参考にしたのがポケモン不思議のダンジョン、略してポケダンシリーズです。   ポケダンではダンジョンがランダムに生成されます。同じ名前のダンジョンを訪れても毎回フィールドの形が違うんですね。こんなランダムなダンジョン生成に憧れて、似たようなものを作ろうと頑張りました。

手順

 ポケダンのようなプレイするたびにマップやダンジョンが新たに作られるゲームは「ローグライクゲーム」と言うらしいです。ローグライクゲームでは普通、
① 大きい長方形を複数の長方形に分ける
② 分けられた長方形の中に部屋を作る
③ 部屋をつなぐ道を作る

という手順で地形が作られるらしいです。むずそーー。

プログラムを書く

うおおおおおおおおおおおおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおおおおおおおお
!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!


書いた

書きました。できたものがこちらになります。

ポケダンには及ばないですがかなり様になっているのではないでしょうか。

中身

プログラムはPython3.9で書きました。Unityとかなんにもわかりませんので。可読性が低くバグの潰し方が汚いソースコードですが、まあ素人の書くコードなんてこんなものでしょう。 うごけばええねん。 去年作ったブラックジャックのプログラムの汚さと比べれば100倍マシです。

ソースコード
from copy import deepcopy
from random import randint, shuffle
from time import sleep


def create_field(H, W):
    field = [[0, H, 0, W]]
    return field


def divide_area(area_edge, area_index):
    # 分割する領域
    edges = area_edge.pop(area_index)
    # 分割できないくらい領域が小さい時はそのまま返す
    if min(edges[1] - edges[0], edges[3] - edges[2]) < 9 or max(edges[1] - edges[0], edges[3] - edges[2]) < 15:
        area_edge.append(edges)
        return area_edge
    # 分割する
    divided_area_1 = deepcopy(edges)
    divided_area_2 = deepcopy(edges)
    if edges[1] - edges[0] > edges[3] - edges[2]:
        new_edge = randint(edges[0] + 7, edges[1] - 8)
        divided_area_1[1] = new_edge
        divided_area_2[0] = new_edge + 1
    else:
        new_edge = randint(edges[2] + 7, edges[3] - 8)
        divided_area_1[3] = new_edge
        divided_area_2[2] = new_edge + 1
    area_edge.append(divided_area_1)
    area_edge.append(divided_area_2)
    return area_edge


def divide_field(area_edge, number):
    count = 0
    while len(area_edge) < number:
        count += 1
        if count == 3000:
            return 0
        area_edge = divide_area(area_edge=area_edge, area_index=randint(0, len(area_edge) - 1))
    area_edge.sort()
    return area_edge


def create_rooms(area_edge):
    count_1 = 0
    while True:
        # 無限ループ対策その1
        count_1 += 1
        if count_1 == 2000:
            return 0

        room_edge = []
        for area in area_edge:
            count_2 = 0
            while True:
                # 無限ループ対策その2
                count_2 += 1
                if count_2 == 2000:
                    return 0

                room = [0, 0, 0, 0]
                room[0] = randint(area[0] + 1, area[1] - 6)
                room[1] = randint(room[0] + 3, min(area[1] - 3, room[0] + 10))
                room[2] = randint(area[2] + 1, area[3] - 6)
                room[3] = randint(room[2] + 3, min(area[3] - 3, room[2] + 10))
                '''
                if (room[1] - room[0] < (area[1] - area[0]) * 0.6) and \
                        (room[3] - room[2] < (area[3] - area[2]) * 0.6):
                    continue
                if (room[1] - room[0] < (area[1] - area[0]) * 0.35) or \
                        (room[3] - room[2] < (area[3] - area[2]) * 0.35):
                    continue
                if not (0.55 <= (room[1] - room[0]) / (room[3] - room[2]) <= 1.8):
                    p = randint(1, 100)
                    if p <= 100:
                        continue
                '''
                break
            room_edge.append(room)

        flag = True
        for i in range(4):
            edge_set = set()
            for r in room_edge:
                edge_set.add(r[i])
            if len(edge_set) != len(room_edge):
                flag = False
        if flag:
            break

    room_edge.sort()
    return room_edge


def edge_to_grid(room_edge, H, W):
    r = room_edge
    keep_out = keep_out_set(room_edge)
    grid = [[""] * W for _ in range(H)]
    for i in range(H):
        for j in range(W):
            ok = False
            for k in range(len(r)):
                if r[k][0] <= i <= r[k][1] and r[k][2] <= j <= r[k][3]:
                    ok = True
                    grid[i][j] = "."
                    break
            if not ok:
                grid[i][j] = "#"
            if (i, j) in keep_out:
                grid[i][j] = "x"
    return grid


def edge_coordinate(room_edge):
    edge_list = [[[] for j in range(4)] for i in range(len(room_edge))]
    # 部屋i
    for i in range(len(room_edge)):
        r = room_edge[i]
        for h in range(r[0], r[1] + 1):
            if h == r[0]:
                for w in range(r[2], r[3] + 1):
                    edge_list[i][0].append((h, w))
                edge_list[i][2].append((h, r[2]))
                edge_list[i][3].append((h, r[3]))
            elif h == r[1]:
                for w in range(r[2], r[3] + 1):
                    edge_list[i][1].append((h, w))
                edge_list[i][2].append((h, r[2]))
                edge_list[i][3].append((h, r[3]))
            else:
                edge_list[i][2].append((h, r[2]))
                edge_list[i][3].append((h, r[3]))
    return edge_list


def edge_set(room_edge_coordinate):
    coordination_set = set()
    for room_i in room_edge_coordinate:
        for udlr in room_i:
            for yx in udlr:
                coordination_set.add(yx)
    return coordination_set


def keep_out_set(room_edge):
    keep_out = set()
    for room_i in room_edge:
        keep_out.add((room_i[0] - 1, room_i[2] - 1))
        keep_out.add((room_i[0] - 1, room_i[3] + 1))
        keep_out.add((room_i[1] + 1, room_i[2] - 1))
        keep_out.add((room_i[1] + 1, room_i[3] + 1))
    return keep_out


def divide_keep_out(room_edge):
    keep_out = [[] for _ in range(4)]
    for room_i in room_edge:
        keep_out[0].append((room_i[0] - 1, room_i[2] - 1))
        keep_out[1].append((room_i[0] - 1, room_i[3] + 1))
        keep_out[2].append((room_i[1] + 1, room_i[2] - 1))
        keep_out[3].append((room_i[1] + 1, room_i[3] + 1))
    return keep_out


def print_field(field_grid):
    H = len(field_grid)
    W = len(field_grid[0])
    print("   ", end="")
    for j in range(W):
        if j <= 9:
            print(j, end=" ")
        else:
            print(j, end="")
    print()
    for i in range(H):
        if i <= 9:
            print(i, end="  ")
        else:
            print(i, end=" ")
        for j in range(W):
            print(field_grid[i][j], end=" ")
        print()
    print()


def DFS_connect(field_grid, now, past):
    H = len(field_grid)
    W = len(field_grid[0])
    if field_grid[now[0]][now[1]] != ".":
        dydx = ((-1, 0), (1, 0), (0, -1), (0, 1))
        can_go = []
        for dy, dx in dydx:
            next = (now[0] + dy, now[1] + dx)
            if (not 0 <= next[0] <= H - 1) or (not 0 <= next[1] <= W - 1):
                return field_grid
            if (field_grid[next[0]][next[1]] == "|" or field_grid[next[0]][next[1]] == ".") and next != past:
                can_go.append(next)
        for next in can_go:
            DFS_connect(field_grid, next, now)

    if field_grid[now[0]][now[1]] == ".":
        field_grid[past[0]][past[1]] = "."
        return field_grid
    else:
        return field_grid


def arrange_grid(field_grid):
    for h in range(len(field_grid)):
        for w in range(len(field_grid[0])):
            if field_grid[h][w] != ".":
                field_grid[h][w] = "#"
    return field_grid


def add_straight_road(field_grid, room_edge_coordinate):
    H = len(field_grid)
    W = len(field_grid[0])
    r = room_edge_coordinate
    direction = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    front = [[(-1, -1), (-1, 1)], [(1, 1), (1, -1)], [(1, -1), (-1, -1)], [(1, 1), (-1, 1)]]
    for i in range(len(r)):
        # 部屋iから道をつくっていく
        for d in range(4):  # 4方向について
            now = list(r[i][d][randint(0, len(r[i]) - 1)])
            # もし道をつくることで道が太くなるなら飛ばす
            flag = False
            for dy, dx in front[d]:
                if field_grid[now[0] + dy][now[1] + dx] == "." or field_grid[now[0] + dy][now[1] + dx] == "|":
                    flag = True
                    break
            if flag:
                continue
            dy, dx = direction[d]
            start = now
            dfs_start = (now[0] + dy, now[1] + dx)  # 後のDFSで使う
            while True:
                next = [now[0] + dy, now[1] + dx]
                # 範囲外ならbreak
                if (not 0 <= next[0] <= H - 1) or (not 0 <= next[1] <= W - 1):
                    break
                # 通れないならbreak
                if field_grid[next[0]][next[1]] == "x":
                    break
                # どこかと繋がったら道を作ってからbreak
                if field_grid[next[0]][next[1]] == "." or field_grid[next[0]][next[1]] == "|":
                    # DFSで繋ぐ
                    try:
                        field_grid = DFS_connect(field_grid, now=dfs_start, past=start)
                    except RecursionError:
                        return 0
                    break
                field_grid[next[0]][next[1]] = "|"
                now = [next[0], next[1]]
    field_grid = arrange_grid(field_grid)
    return field_grid


def narrow_road(field_grid, keep_out, room_edge_set):
    H = len(field_grid)
    W = len(field_grid[0])
    direction = [[(-1, 0), (0, -1)], [(-1, 0), (0, 1)], [(1, 0), (0, -1)], [(1, 0), (0, 1)]]
    front = [[[(-1, -1), (-1, 1)], [(-1, -1), (1, -1)]],
             [[(-1, -1), (-1, 1)], [(-1, 1), (1, 1)]],
             [[(1, -1), (1, 1)], [(-1, -1), (1, -1)]],
             [[(1, -1), (1, 1)], [(-1, 1), (1, 1)]]]
    for c in range(4):
        # 4つの角について
        # 左上、右上、左下、右下の順
        for corner in keep_out[c]:
            # ある方向にあるkeepoutのマスすべてについて
            # 左上、右上、左下、右下の順
            for d in range(2):
                # keepoutのマスの、調べなければならない方向2つについて
                # direction[c] は [(-1, 0), (0, -1)]
                # direction[c][d] は (-1, 0)
                dy, dx = direction[c][d][0], direction[c][d][1]
                now_y, now_x = corner[0], corner[1]
                if field_grid[now_y + dy][now_x + dx] != ".":  # 異常なし
                    continue
                else:  # バグの可能性
                    while True:
                        # front[c][d] は [(-1, -1), (-1, 1)]
                        if not (0 <= now_y + front[c][d][0][0] <= H - 1 and 0 <= now_x + front[c][d][0][1] <= W - 1):
                            break
                        if not (0 <= now_y + front[c][d][1][0] <= H - 1 and 0 <= now_x + front[c][d][1][1] <= W - 1):
                            break
                        look_1 = field_grid[now_y + front[c][d][0][0]][now_x + front[c][d][0][1]]
                        look_2 = field_grid[now_y + dy][now_x + dx]
                        look_3 = field_grid[now_y + front[c][d][1][0]][now_x + front[c][d][1][1]]
                        if (now_y + dy, now_x + dx) in room_edge_set:
                            break
                        elif look_2 != ".":
                            break
                        elif look_1 == look_3 == ".":
                            now_y, now_x = now_y + dy, now_x + dx
                            continue
                        elif look_1 == look_3 == "#":
                            break
                        else:
                            field_grid[now_y + dy][now_x + dx] = "#"
                            now_y, now_x = now_y + dy, now_x + dx
                            if not (0 <= now_y <= H - 1 and 0 <= now_x <= W - 1):
                                break
    return field_grid


def road_start_point(field_grid, room_edge_coordinate):
    dydx = ((-1, 0), (1, 0), (0, -1), (0, 1))
    start_point = list()
    r = room_edge_coordinate
    for i in range(len(room_edge_coordinate)):  # 部屋番号
        for j in range(4):  # 方向
            dy, dx = dydx[j]
            for y, x in r[i][j]:  # (y, x) は部屋の縁の座標
                if field_grid[y + dy][x + dx] == ".":
                    start_point.append((y + dy, x + dx, y, x))
    return start_point


def DFS_make_up(field_grid, now, past, room_edge_set):
    dydx = ((-1, 0), (1, 0), (0, -1), (0, 1))
    can_go = []
    for dy, dx in dydx:
        next = (now[0] + dy, now[1] + dx)
        if field_grid[next[0]][next[1]] == ".":
            if next != past:
                can_go.append(next)
    if len(can_go) == 0:
        next = (now[0] + (now[0] - past[0]), now[1] + (now[1] - past[1]))
        field_grid[next[0]][next[1]] = "."
        can_go.append(next)
    for next in can_go:
        if next in room_edge_set:
            return field_grid
        DFS_make_up(field_grid, next, now, room_edge_set)
    return field_grid


def make_up_lacking_part(field_grid, road_start, room_edge_set):
    try:
        for y_s, x_s, y_r, x_r in road_start:  # 道の根本を全部見ていく
            field_grid = DFS_make_up(field_grid, (y_s, x_s), (y_r, x_r), room_edge_set)
    except RecursionError:
        return 0
    return field_grid


# 最終的に、これだけ実行してマップを作れるようにする
def generate():
    while True:
        # まず大枠をつくる
        while True:
            H = randint(28, 32)
            W = randint(32, 40)
            # 枠づくり
            edge = create_field(H, W)
            # いくつに分割するかをきめる
            number_of_area = [8] * 100
            number = number_of_area[randint(0, 99)]
            # 分割
            area_edges = divide_field(edge, number)
            if area_edges == 0:
                continue
            room_edges = create_rooms(area_edges)
            if room_edges == 0:
                continue
            else:
                break
        field_grid = edge_to_grid(room_edges, H, W)
        # 次に部屋をつなぐ道をつける
        shuffle(room_edges)
        room_edge_coordinate = edge_coordinate(room_edges)
        field_grid = add_straight_road(field_grid, room_edge_coordinate)
        if field_grid == 0:
            continue
        room_edge_set = edge_set(room_edge_coordinate)
        # バグって太い道を細くする
        field_grid = narrow_road(field_grid, divide_keep_out(room_edges), room_edge_set)
        # バグって欠けた道を修復する
        road_start = road_start_point(field_grid, room_edge_coordinate)
        field_grid = make_up_lacking_part(field_grid, road_start, room_edge_set)
        if field_grid == 0:
            continue
        # 完成!!!!!!!!!
        break
    for i in field_grid:
        print(*i)
    print("\n")


while True:
    generate()
    sleep(1.8)

がんばったところ

色々あるのですがなんと言っても部屋をつなぐ道づくりです。ごめん頑張ったので長くなります。どうやって道をつなげようか調べていた時に見つけたサイトでは、

① 繋ぐ2つの部屋を決める
② 2部屋から道を伸ばし、
③ 部屋の属する領域同士の境界線上でつなぎ合わせる

という方法が書かれていました。文ではわかりづらいですが、上のイラストのような部屋のつなぎ方ですね。
 確かにこの方法でできる道はとてもポケダンらしい見た目なのですが、自分はちょっとこの方法で上手くいく気がしなかったので、以下の方法で道を作りました。

① ひとつの部屋を選ぶ
② その部屋から上下左右4方向に道(仮)を伸ばす
③ 道(仮)が他の部屋に当たれば道(仮)をちゃんとした道にする、
	 他の部屋に当たらなかったらとりあえず道(仮)を放置
④ 他の部屋からも4方向に道(仮)を伸ばし、他の部屋や他の部屋からの道(仮)に当たれば
	 道(仮)をちゃんとした道にして部屋同士をつなげる
⑤ ④を残りのすべての部屋について施す

④以降で道をつなげる時、「深さ優先探索」といわれるアルゴリズムを用いたのですが、僕はこのアルゴリズムがとっても苦手です。記事の最初に「アルゴリズムの実装ができなくてオセロAI作りを断念した」と書きましたが、そのアルゴリズムであるアルファベータ法は再帰(関数の定義に同じ関数を利用すること)を利用して強い手を探すというものです。
 僕は再帰がめちゃくちゃ嫌いでできるだけ避けて生きてきたのですが、今回必要になった深さ優先探索も不幸なことに再帰を利用するアルゴリズムです。再帰から逃げてきたのに再帰に回り込まれてしまいました。仕方ないので泣く泣く深さ優先探索と向き合います。
 なんとか⑤まで終わらせると、今度は怒涛のバグ潰しが待っていました。  まずなんか道が太くなってました。なんやかんやで原因をつきとめ、どうにもならんので太い道を作らないことを諦め、太くなった道を細くする方向で頑張りました。


 頑張った結果、なんか道がちぎれてました。どうしようか考えましたが

深さ優先探索で補正するのが一番よさそうでした。


 このバグ10回に1回も起きやんし、正直もう放置でよくないですか?そう言う悪魔を必死に抑えつつもう一度深さ優先探索を書きました。もう自分でも何を書いているのか分からなかったですがなんか上手く動いたのでOKです。

改善したい点

ひとつとてもわかりやすいバグがあります。上の動画を注意深く見ていた人は気づいたかもしれません。  このマップ生成方法だと、なんと他のどの部屋にも道が繋がっていない部屋、つまり孤立した部屋がごく稀にできてしまうんですね。マッチョから逃げるゲームを作るときにこのバグが残ったままだと、主人公のスポーン地点がその孤立部屋だった場合、どれだけ放置してもそこにマッチョが来ることがないという状況になってしまいます。
 解決策は浮かんでいますがこの記事を書いている現在11月5日(土)の朝5時半ということで、バグを直してまた実行の様子をキャプチャしてうんぬんかんぬんする元気は僕にはありません。ゆるして。

 もうひとつ絶対にどうにかしたい部分が、最初に示した全体の手順

 ① 大きい長方形を複数の長方形に分ける
 ② 分けられた長方形の中に部屋を作る
 ③ 部屋をつなぐ道を作る

の①と②の部分、つまり部屋づくりですね。今回、部屋の個数と大きさをランダムで良い感じに決まるようにしたかったのですが、大きさを良い感じにすることができませんでした。(部屋の数を良い感じのランダムにすることはできましたが、上のコードでは見栄えが良くなるように一旦部屋の数を8個に固定しています。)
 動画を見てもらえればわかる通り、部屋の大きさが全体的に小さいです。一応部屋の大きさをランダムにすることはできていますがその自由度が低いんですね。なんとかしたいです。

感想

バグを直したところからまた別のバグが発生するというあるある(?)を体験できたのが楽しかったですね。今回書いたプログラムは400行くらいで僕にとってはとても大変だったのですが、ガチでプログラミングをする人は何千行何万行、多い時は何十万行のプログラムを書くこともあるらしくて戦慄しています。化け物か?
 あと、変数名や関数名が同じようなものばかりになってどの変数に何が格納されているのかわからないことが多々ありました。読みやすいプログラムを書くにはどうしないといけないのか勉強していかないといけませんね。
 作るものが途中で変更になりましたが、なんとか展示時間までに作品を形にできてよかったです。最後に、魅力的な作品がたくさんある中、文字ばっかりで長ったらしいこれを読んでくださり、ありがとうございました。

一緒に読まれている記事
記事がありません。