Files
transcience/tests/unit/test_map_gen.gd
T
claude 7af439341d
ci / verify (push) Successful in 46s
Stage 1: tile maps, walls, fog of war, aggro, scrolling camera
Replaces the fixed centred arena with per-world tile geometry, which is the
foundation the remaining features sit on.

- MapGrid: tile grid with three independent flags -- blocks movement, bullets,
  sight -- so a pit stops feet but not bullets or eyes, and a barricade stops
  feet and bullets but not eyes. Circle collision with per-axis sliding, and
  Bresenham line of sight shared by fog and aggro.
- MapGen: rooms and corridors generated from (seed, depth), with hand-authored
  boss arenas from Rooms stamped in first so a corridor can never carve through
  a designed fight. Reachability from spawn to boss is asserted over 40 seeds --
  "usually connected" is the failure mode that ruins one run in twenty.
- Dungeons are populated at creation, per room, instead of gating on waves. You
  explore and choose your fights; the run ends when the boss dies, not when the
  map is swept. Boss rooms have no lock, so walking out is always available.
- Aggro: enemies need range AND line of sight, so a dungeon stays quiet until
  engaged and cover actually protects.
- Hard fog, scrolling camera, and terrain rendering.

Maps are streamed per peer in chunks around that peer's player, and the seed is
deliberately NOT sent -- a client holding it could regenerate the whole dungeon,
which is a map hack for free. Stream radius (900u) is wider than view radius
(460u) because the client predicts movement against walls and simulates bullets
that die on them; the accepted cost is a cheater seeing a little further than
the fog, never the floor plan.

Partial map knowledge means wall deaths must be announced rather than derived.
A test caught the subtle half of that: out-of-bounds tiles read as WALL by
design, so checking geometry before bounds reported every bullet leaving the map
as a wall kill.

128 tests (was 103); check.sh, test.sh and smoke.sh pass. 0.26 ms/tick with 4
players and a boss, ~65x headroom.
2026-09-03 20:49:27 +02:00

86 lines
3.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])