The Cantor gets its own portal; bullets stop strobing
ci / verify (push) Successful in 48s

A dungeon now names the boss arena it ends in, and the arena decides the boss.
Three entrances in the hub: Warden's Descent, The Choir Vault, Proving Grounds.

That replaces the seed coin-flip from the last commit, which was the wrong
call. Which boss you are about to fight is the single thing a player decides
before walking into a dungeon, and rolling it for them makes that decision
unavailable. Only the Proving Grounds still leaves its arena open, because a
harness you re-enter every couple of minutes wants whichever fight comes up
first -- and it is the one place where "either will do" is true.

test_every_boss_has_a_dungeon_that_reaches_it is the check that matters here:
adding a boss and forgetting to give it a way in is now a failing test rather
than a boss nobody meets. The smoke test asserts all three portals open over a
real socket -- bots pick theirs by account id, so a run exercises each.

Bullets no longer animate. The sheet's eight frames are a COLOUR cycle rather
than a shape change, so running it had every bullet on screen strobing through
a palette in unison, which with a few hundred in the air is exactly as hard to
look at as it sounds. Each bullet holds one frame and turns slowly instead,
offset by its own id so a ring of twenty does not rotate as one rigid wheel.
The rate is slow enough that nothing completes a full turn inside its own
lifetime, so it reads as drift rather than spin, and a test pins that against
the longest-lived bullet in the game. The renderer slices four textures at
startup now instead of thirty-two.

check.sh clean, 414 tests, SMOKE PASS (22 assertions), all four diagnostics
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 10:36:06 +02:00
parent 42568a40ad
commit 4a98cf4b0e
15 changed files with 251 additions and 100 deletions
+36 -20
View File
@@ -180,34 +180,50 @@ func test_each_arena_summons_its_own_boss() -> void:
assert_eq(Rooms.boss_for_arena(&"choir_vault"), Content.BOSS_CANTOR)
## Seed parity picks the arena, and the boss follows it. Deliberately NOT the
## depth: depth is a dev flag nothing in play raises, so keying the arena to it
## left the second boss unreachable in an actual game.
func test_a_generated_dungeon_gets_the_boss_its_arena_belongs_to() -> void:
var even := MapGen.generate(1234, 1)
var odd := MapGen.generate(1235, 1)
assert_eq(StringName(even["boss_id"]), Content.BOSS_WARDEN)
assert_eq(StringName(odd["boss_id"]), Content.BOSS_CANTOR)
assert_not_null(Content.boss(StringName(even["boss_id"])))
assert_not_null(Content.boss(StringName(odd["boss_id"])))
## The arena a dungeon names decides its boss, whatever the seed. Which boss you
## are about to fight is the one thing a player chooses before walking into a
## dungeon, so it must not be rolled for them.
func test_the_named_arena_decides_the_boss_whatever_the_seed() -> void:
for run in 12:
var seed_value := run * 7919 + 3
assert_eq(StringName(MapGen.generate(seed_value, 1, &"warden_hall")["boss_id"]),
Content.BOSS_WARDEN)
assert_eq(StringName(MapGen.generate(seed_value, 1, &"choir_vault")["boss_id"]),
Content.BOSS_CANTOR)
## Both bosses have to actually turn up. A run picks its seed at random, so
## this is the check that neither is effectively unreachable.
func test_both_bosses_are_reachable_at_the_depth_people_play() -> void:
## Only the test harness leaves its arena open, and then the seed decides. Both
## fights still have to turn up there, or half the harness is unreachable.
func test_an_unnamed_arena_still_produces_both_fights() -> void:
var seen := {}
for run in 40:
seen[StringName(MapGen.generate(run * 7919 + 3, 1)["boss_id"])] = true
for id in Content.ALL_BOSSES:
assert_true(seen.has(id), "%s never appears at depth 1" % id)
assert_true(seen.has(id), "%s never appears with the arena left open" % id)
func test_an_instance_spawns_the_boss_its_map_asked_for() -> void:
var inst := Instance.make_dungeon(2, 4321, 1)
assert_eq(inst.boss_id, Content.BOSS_CANTOR)
assert_eq(inst.world.boss.def.id, Content.BOSS_CANTOR)
assert_true(inst.world.boss.room.has_point(inst.world.boss.pos),
"and it starts inside its own arena")
## The real check that a boss is reachable in play: some dungeon in the hub ends
## with it. Adding a boss and forgetting to give it a way in is exactly the
## mistake this catches.
func test_every_boss_has_a_dungeon_that_reaches_it() -> void:
var reachable := {}
for dungeon_id in Dungeons.ORDER:
var arena := Dungeons.get_def(dungeon_id).arena
if arena.is_empty():
continue # the harness rolls; covered above
reachable[Rooms.boss_for_arena(arena)] = true
for id in Content.ALL_BOSSES:
assert_true(reachable.has(id), "no hub portal leads to %s" % id)
func test_an_instance_spawns_the_boss_its_dungeon_asked_for() -> void:
for pair in [[Dungeons.STANDARD, Content.BOSS_WARDEN],
[Dungeons.VAULT, Content.BOSS_CANTOR]]:
var inst := Instance.make_dungeon(2, 4321, 1, pair[0])
assert_eq(inst.boss_id, pair[1], "%s should end with %s" % pair)
assert_eq(inst.world.boss.def.id, pair[1])
assert_true(inst.world.boss.room.has_point(inst.world.boss.pos),
"and it starts inside its own arena")
## The Cantor exists to prove the boss format stretched to movement and