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