Files
transcience/tests/unit/test_map_gen.gd
T
claudeandClaude Opus 5.5 83c3b920fb Varied-width tunnels, and stop carving slits in the boss arenas
Every corridor was one tile wide, which in a bullet hell is a shooting
gallery: there is no side to step to, so anything fired down it lands.
Each leg of each L now rolls its own width between 2 and 4, so a tunnel
can open up or pinch at its bend rather than every connection being the
same pipe. Carving stops one tile short of the map's outer ring, which
keeps the world sealed however close to the edge a wide tunnel runs.

Found while doing it: 45% of generated arenas had their own walls carved
open. The last corridor aimed at the door tile, which sits ON the
arena's west wall, and an L that bent the other way ran its second leg
straight down that column -- 181 of 398 arenas, measured. It now aims at
the tile outside the door and always arrives along the door's own row,
and the arena is excluded from every carve, so a wide tunnel stops at
the wall instead of eating into it.

Three tests: no corridor tile sits in a passage one tile wide, every
width in the range turns up and none falls below two, and every arena
keeps every wall tile it was drawn with. The overshoot at a bend is
cosmetic -- the legs always overlap on the inner side, so removing it
bevels the outer corner and narrows nothing -- and the comment now says
so rather than claiming otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:51:19 +02:00

168 lines
6.1 KiB
GDScript

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])