extends GutTest ## Generator properties. The important one is reachability: a map where the ## boss cannot be walked to is a run that cannot be finished, and it would show ## up as a player wandering confused rather than as an error. func _flood_reachable(grid: MapGrid, from: Vector2i) -> Dictionary: var seen := {} var queue: Array[Vector2i] = [from] seen[from] = true while not queue.is_empty(): var t: Vector2i = queue.pop_back() for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]: var n: Vector2i = t + d if seen.has(n) or not grid.in_bounds(n.x, n.y): continue if grid.blocks_move(n.x, n.y): continue seen[n] = true queue.append(n) return seen func test_the_boss_is_always_reachable_from_the_spawn() -> void: # Many seeds, because "usually connected" is the failure mode that survives # a single-seed test and ruins one run in twenty. for s in range(1, 40): var m := MapGen.generate(s * 7919, 1 + (s % 5)) var grid: MapGrid = m["grid"] var spawn_tile := grid.to_tile(m["spawn"]) var boss_tile := grid.to_tile(m["boss_pos"]) var reachable := _flood_reachable(grid, spawn_tile) assert_true(reachable.has(boss_tile), "seed %d: the boss room is walled off from the spawn" % s) func test_generation_is_deterministic() -> void: var a := MapGen.generate(12345, 3) var b := MapGen.generate(12345, 3) assert_eq((a["grid"] as MapGrid).tiles, (b["grid"] as MapGrid).tiles, "same seed and depth must give the same map, or the server cannot " + "just send two numbers") assert_eq(a["spawn"], b["spawn"]) assert_eq(a["boss_pos"], b["boss_pos"]) func test_different_seeds_give_different_maps() -> void: var a := MapGen.generate(1, 2) var b := MapGen.generate(2, 2) assert_ne((a["grid"] as MapGrid).tiles, (b["grid"] as MapGrid).tiles) func test_maps_grow_with_depth() -> void: var shallow: MapGrid = MapGen.generate(99, 1)["grid"] var deep: MapGrid = MapGen.generate(99, 6)["grid"] assert_gt(deep.width, shallow.width) assert_gt(deep.height, shallow.height) func test_the_map_is_sealed_at_its_border() -> void: var grid: MapGrid = MapGen.generate(4242, 2)["grid"] for x in grid.width: assert_true(grid.blocks_move(x, 0), "top border leaks at x=%d" % x) assert_true(grid.blocks_move(x, grid.height - 1), "bottom border leaks at x=%d" % x) for y in grid.height: assert_true(grid.blocks_move(0, y), "left border leaks at y=%d" % y) assert_true(grid.blocks_move(grid.width - 1, y), "right border leaks at y=%d" % y) func test_spawn_and_boss_stand_on_open_ground() -> void: for s in range(1, 20): var m := MapGen.generate(s * 104729, 1 + (s % 4)) var grid: MapGrid = m["grid"] assert_false(grid.circle_blocked(m["spawn"], SimConfig.PLAYER_RADIUS), "seed %d: player spawns inside geometry" % s) assert_false(grid.circle_blocked(m["boss_pos"], 40.0), "seed %d: boss spawns inside geometry" % s) func test_rooms_do_not_overlap() -> void: var rooms: Array = MapGen.generate(777, 4)["rooms"] for i in rooms.size(): for j in range(i + 1, rooms.size()): assert_false((rooms[i] as Rect2i).intersects(rooms[j]), "rooms %d and %d overlap" % [i, j]) # --- Tunnels ---------------------------------------------------------------- func _inside_any(rooms: Array, x: int, y: int) -> bool: for r: Rect2i in rooms: if r.has_point(Vector2i(x, y)): return true return false ## Whether (x, y) is a corner of at least one 2x2 square of open floor. A ## passage one tile wide has no such square anywhere along it. func _in_open_square(g: MapGrid, x: int, y: int) -> bool: for dy in [-1, 0]: for dx in [-1, 0]: var ok := true for oy in 2: for ox in 2: if g.at(x + dx + ox, y + dy + oy) != MapGrid.Kind.FLOOR: ok = false if ok: return true return false func test_no_tunnel_is_a_single_file_squeeze() -> void: # A one-tile corridor in a bullet hell is a shooting gallery: there is no # side to step to. Every corridor tile has to sit in open ground at least # two wide in some direction. Rooms and the authored arena are left out -- # the arena's door is drawn one tile wide on purpose. for s in range(1, 40): var m := MapGen.generate(s * 7919, 1 + (s % 5)) var g: MapGrid = m["grid"] var rooms: Array = m["rooms"] var arena: Rect2i = m["boss_room"] var squeezes := 0 for y in g.height: for x in g.width: if g.at(x, y) != MapGrid.Kind.FLOOR: continue if _inside_any(rooms, x, y) or arena.has_point(Vector2i(x, y)): continue if not _in_open_square(g, x, y): squeezes += 1 assert_eq(squeezes, 0, "seed %d: %d corridor tile(s) in a passage one tile wide" % [s, squeezes]) func test_tunnel_widths_vary_and_never_go_below_two() -> void: var seen := {} for s in range(1, 40): var widths: PackedInt32Array = MapGen.generate(s * 7919, 1 + (s % 5))["corridor_widths"] assert_false(widths.is_empty(), "seed %d carved no tunnels at all" % s) for w in widths: assert_between(w, MapGen.CORRIDOR_MIN_WIDTH, MapGen.CORRIDOR_MAX_WIDTH) seen[w] = true assert_eq(seen.size(), MapGen.CORRIDOR_MAX_WIDTH - MapGen.CORRIDOR_MIN_WIDTH + 1, "every width in the range turns up somewhere; got %s" % [seen.keys()]) func test_the_arena_keeps_every_wall_it_was_drawn_with() -> void: # The corridor to the door used to aim at the door tile itself, which sits # ON the arena's west wall. An L that bent the other way ran its second leg # down that wall column, and 45% of generated arenas had a slit carved in # an authored room. A wide tunnel makes the same mistake wider. for arena in [&"warden_hall", &"choir_vault"]: var stamp := Rooms.warden_hall() if arena == &"warden_hall" else Rooms.choir_vault() for s in range(1, 60): var m := MapGen.generate(s * 7919, 1 + (s % 5), arena) var g: MapGrid = m["grid"] var room: Rect2i = m["boss_room"] var carved := 0 for y in stamp.size(): for x in stamp[y].length(): if stamp[y][x] != "#": continue if g.at(room.position.x + x, room.position.y + y) != MapGrid.Kind.WALL: carved += 1 assert_eq(carved, 0, "seed %d %s: %d of the arena's own wall tiles were carved open" % [s * 7919, arena, carved])