Files
transcience/tests/unit/test_dungeons.gd
T
claude 4a98cf4b0e
ci / verify (push) Successful in 48s
The Cantor gets its own portal; bullets stop strobing
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>
2026-09-07 10:36:06 +02:00

199 lines
8.3 KiB
GDScript

extends GutTest
## The dungeon flavours. The Proving Grounds exists so a manual pass over loot,
## inventory and all four boss phases takes minutes rather than a quarter of an
## hour -- which is only true if it stays genuinely identical to the real run
## apart from the numbers it scales.
func test_every_registered_id_has_a_definition() -> void:
for id in Dungeons.ORDER:
assert_not_null(Dungeons.get_def(id), "%s is in ORDER with no definition" % id)
func test_the_wire_index_round_trips() -> void:
for id in Dungeons.ORDER:
assert_eq(Dungeons.by_index(Dungeons.index_of(id)), id)
## An id off the wire has to resolve to something playable. Falling back to the
## standard run is the safe direction; returning null would crash a peer for
## sending a byte this build does not recognise.
func test_an_unknown_id_falls_back_to_the_real_run() -> void:
assert_null(Dungeons.get_def(&"atlantis"))
assert_eq(Dungeons.get_or_default(&"atlantis").id, Dungeons.STANDARD)
assert_eq(Dungeons.by_index(99), Dungeons.default_id())
assert_eq(Dungeons.by_index(-1), Dungeons.default_id())
## The baseline has to be exactly the content as authored, or "identical but
## easier" is measured against something that is itself already scaled.
func test_the_standard_run_scales_nothing() -> void:
var d := Dungeons.standard()
assert_eq(d.enemy_hp_mult, 1.0)
assert_eq(d.boss_hp_mult, 1.0)
assert_eq(d.loot_chance_mult, 1.0)
assert_eq(d.apply_to_enemy(Content.drifter()).max_hp, Content.drifter().max_hp)
assert_eq(d.apply_to_boss(Content.warden()).max_hp, Content.warden().max_hp)
assert_eq(d.apply_to_enemy(Content.drifter()).loot[0].chance,
Content.drifter().loot[0].chance)
func test_the_proving_grounds_is_easier_in_every_direction() -> void:
var easy := Dungeons.proving_grounds()
assert_lt(easy.apply_to_enemy(Content.drifter()).max_hp, Content.drifter().max_hp)
assert_lt(easy.apply_to_boss(Content.warden()).max_hp, Content.warden().max_hp)
assert_gt(easy.apply_to_enemy(Content.drifter()).loot[0].chance,
Content.drifter().loot[0].chance)
## Nothing may be scaled out of existence: a one-shot enemy is still a fight,
## an enemy with zero health is a crash waiting for a divide.
func test_scaling_never_produces_a_creature_with_no_health() -> void:
var brutal := DungeonDef.new()
brutal.enemy_hp_mult = 0.0
brutal.boss_hp_mult = 0.0
assert_gte(brutal.apply_to_enemy(Content.drifter()).max_hp, 1)
assert_gte(brutal.apply_to_boss(Content.warden()).max_hp, 1)
## A generous multiplier must not push a chance past certain, or a "chance"
## stops being one and the roll becomes dead code.
func test_boosted_drop_chances_are_clamped_at_certain() -> void:
var generous := DungeonDef.new()
generous.loot_chance_mult = 500.0
for entry in generous.apply_to_enemy(Content.drifter()).loot:
assert_lte(entry.chance, 1.0)
# And a guaranteed drop stays exactly guaranteed rather than overflowing.
for entry in generous.apply_to_boss(Content.warden()).loot:
assert_eq(entry.chance, 1.0)
## Scaling mutates the def it is given. That is only safe because every
## Content.* call constructs a fresh one -- if that ever stops being true, one
## easy dungeon would permanently nerf every hard one in the process.
func test_scaling_one_dungeon_does_not_leak_into_the_next() -> void:
Dungeons.proving_grounds().apply_to_boss(Content.warden())
assert_eq(Dungeons.standard().apply_to_boss(Content.warden()).max_hp,
Content.warden().max_hp,
"a standard run built afterwards must still have a full-health boss")
# --- Instances --------------------------------------------------------------
## Every dungeon in the hub names its arena, so a player knows what they are
## walking into. Only the harness leaves it to the seed.
func test_only_the_test_harness_leaves_its_boss_to_chance() -> void:
for id in Dungeons.ORDER:
var d := Dungeons.get_def(id)
if id == Dungeons.PROVING:
assert_true(d.arena.is_empty(), "the harness takes either fight")
else:
assert_false(d.arena.is_empty(), "%s must name its arena" % id)
assert_not_null(Content.boss(Rooms.boss_for_arena(d.arena)))
func test_each_real_dungeon_leads_somewhere_different() -> void:
var arenas := {}
for id in Dungeons.ORDER:
var arena := Dungeons.get_def(id).arena
if arena.is_empty():
continue
assert_false(arenas.has(arena), "two portals lead to the same fight")
arenas[arena] = true
## The seed is even on purpose. The harness rolls its arena, and only a run
## that landed in the SAME arena is comparable -- a different one is a different
## size, which moves the rooms and therefore the enemy count.
func test_a_proving_run_is_built_weaker_than_a_standard_one() -> void:
var same_arena := 4242
assert_eq(posmod(same_arena, 2), 0, "setup: this seed has to roll warden_hall")
var hard := Instance.make_dungeon(2, same_arena, 1, Dungeons.STANDARD)
var easy := Instance.make_dungeon(3, same_arena, 1, Dungeons.PROVING)
assert_lt(easy.world.boss.hp, hard.world.boss.hp)
assert_eq(easy.world.map.width, hard.world.map.width,
"same seed and depth must still give the same size of map")
assert_eq(easy.world.enemies.size(), hard.world.enemies.size(),
"and the same number of enemies in the same rooms")
var easy_hp := 0
var hard_hp := 0
for e in easy.world.enemies.values():
easy_hp += e.hp
for e in hard.world.enemies.values():
hard_hp += e.hp
assert_lt(easy_hp, hard_hp)
func test_a_dungeon_defaults_to_the_real_run() -> void:
assert_eq(Instance.make_dungeon(2, 1, 1).dungeon_id, Dungeons.default_id())
## Walking into the Proving Grounds must never drop you into a standard run that
## happens to still be forming, however conveniently timed.
func test_a_forming_run_only_accepts_its_own_kind() -> void:
var inst := Instance.make_dungeon(2, 1, 1, Dungeons.PROVING)
assert_true(inst.accepts_new_party_member(Dungeons.PROVING))
assert_false(inst.accepts_new_party_member(Dungeons.STANDARD))
func test_the_hub_has_one_entrance_per_dungeon() -> void:
var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID)
assert_eq(lobby.world.portals.size(), Dungeons.ORDER.size())
var seen := {}
for portal in lobby.world.portals:
seen[portal.dungeon] = true
for id in Dungeons.ORDER:
assert_true(seen.has(id), "%s has no way in" % id)
## Two entrances whose catchment areas overlap would make which dungeon you
## enter depend on list order rather than on where you are standing.
func test_the_entrances_are_far_enough_apart_to_be_unambiguous() -> void:
var portals := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID).world.portals
for i in portals.size():
for j in range(i + 1, portals.size()):
assert_gt(portals[i].pos.distance_to(portals[j].pos),
SimConfig.PORTAL_RADIUS * 2.0,
"portals %d and %d overlap" % [i, j])
func test_standing_on_an_entrance_resolves_to_that_dungeon() -> void:
var world := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID).world
for portal in world.portals:
var found := world.portal_at(portal.pos)
assert_not_null(found)
assert_eq(found.dungeon, portal.dungeon)
func test_standing_between_them_is_still_out_of_reach_of_both() -> void:
var world := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID).world
if world.portals.size() < 2:
pass_test("only one entrance; nothing to stand between")
return
var midpoint := (world.portals[0].pos + world.portals[1].pos) * 0.5
assert_null(world.portal_at(midpoint),
"the gap between entrances has to be a place where nothing happens")
## The event carries which dungeon, because the server must never take that from
## anything the client says.
func test_using_an_entrance_reports_which_dungeon_it_opens() -> void:
var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID)
lobby.add_peer(1, "tester")
var target: SimPortal = lobby.world.portals[Dungeons.ORDER.size() - 1]
lobby.world.players[1].pos = target.pos
var frames: Array[InputFrame] = [InputFrame.make(
lobby.world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
lobby.world.queue_input(1, frames)
lobby.step()
var used := lobby.world.events.filter(
func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.PORTAL_USED)
assert_eq(used.size(), 1)
assert_eq(StringName(used[0]["dungeon"]), target.dungeon)
func test_a_dungeon_world_has_no_entrances_of_its_own() -> void:
var inst := Instance.make_dungeon(2, 1, 1, Dungeons.PROVING)
assert_eq(inst.world.portals.size(), 0,
"there is no way further down yet, and no way to re-enter from inside")