From 6f260eb25f17b1c01ed687bfce4244161ce84a51 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 18:42:28 +0200 Subject: [PATCH] test: cover tests.gameCaptureAtRemovesTheCaughtMonsterFromTheLevelWit... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index df1d081..48d5c9e 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -12239,6 +12239,179 @@ function tests.captivesResolveCatchesABatteredTargetIntoACaptiveAndZeroesItsHp() eq(captive.value, Monsters.xpFor(rat), "the captive's value matches Captives.valueOf") eq(rat.hp, 0, "the caught monster's hp is zeroed") ok(not rat:isAlive(), "the caught monster is no longer alive") + ok(contains(lines[1], "bound"), "the log reports the net dropping true") +end + +function tests.gameCaptureAtRemovesTheCaughtMonsterFromTheLevelWithoutAwardingAKill() + local game = stagedGame(4, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + rat.hp = 1 + game.level.entities = { rat } + local before = #game.player.inventory + game.player.inventory[#game.player.inventory + 1] = Items.net() + local index = #game.player.inventory + game.rng = Rng.new(2) + local xpBefore = game.player.xp + local killsBefore = game.stats.kills + local success = game:captureAt(index, rat) + eq(success, true, "a successful net capture reports the item as thrown") + eq(#game.player.inventory, before, "the net leaves the inventory once thrown") + eq(#game.level.entities, 0, "the captured monster is swept from the level's entities") + eq(#game.level.captives, 1, "the captured monster becomes exactly one level captive") + eq(game.player.xp, xpBefore, "a capture grants no xp") + eq(game.stats.kills, killsBefore, "a capture is never counted as a kill") + eq(#game.level.corpses, 0, "a captured monster leaves no corpse behind") +end + +function tests.gameCaptureAtLeavesTheTargetAliveAndTheNetUnspentWhenOutOfRange() + local game = stagedGame(4, arena(21, 11), 2, 2) + local rat = Monsters.create(Monsters.byKey.rat, 1, 19, 9, 1) + rat.hp = 1 + game.level.entities = { rat } + game.player.inventory[#game.player.inventory + 1] = Items.net() + local index = #game.player.inventory + local before = #game.player.inventory + local success = game:captureAt(index, rat) + eq(success, false, "a capture attempt beyond range fails") + eq(#game.player.inventory, before, "a failed capture attempt does not consume the net") + ok(rat:isAlive(), "a failed capture attempt leaves the target alive") +end + +function tests.gameRansomCaptiveWarmsFactionStandingWithNoGoldChange() + local game = stagedGame(4, arena(21, 11), 5, 5) + game.level.captives = { { x = 5, y = 5, species = "rat", faction = Factions.VERMIN, name = "rat", value = 3 } } + local goldBefore = game.player.gold + local repBefore = Factions.reputationFor(game.player, Factions.VERMIN) + local success = game:ransomCaptive() + eq(success, true, "ransoming a held captive succeeds") + eq(#game.level.captives, 0, "ransoming removes the captive from the level") + eq(game.player.gold, goldBefore, "ransoming changes no gold") + eq(Factions.reputationFor(game.player, Factions.VERMIN), repBefore + Captives.RANSOM_REP_GAIN, + "ransoming warms the captive's own faction standing") +end + +function tests.gameSellCaptivePaysGoldAndCoolsFactionStanding() + local game = stagedGame(4, arena(21, 11), 5, 5) + game.level.captives = { { x = 5, y = 5, species = "rat", faction = Factions.VERMIN, name = "rat", value = 7 } } + local goldBefore = game.player.gold + local repBefore = Factions.reputationFor(game.player, Factions.VERMIN) + local success = game:sellCaptive() + eq(success, true, "selling a held captive succeeds") + eq(#game.level.captives, 0, "selling removes the captive from the level") + eq(game.player.gold, goldBefore + 7, "selling pays exactly the captive's own value in gold") + eq(Factions.reputationFor(game.player, Factions.VERMIN), repBefore - Captives.SELL_REP_LOSS, + "selling cools the captive's own faction standing") +end + +function tests.gameInterrogateCaptiveRevealsEveryHiddenDoorAndGrantsEssence() + local game = stagedGame(4, arena(21, 11), 5, 5) + game.level.secretDoors = { + { x = 8, y = 8, discovered = false }, + { x = 9, y = 9, discovered = false }, + { x = 10, y = 10, discovered = true }, + } + game.level.captives = { { x = 5, y = 5, species = "rat", faction = Factions.VERMIN, name = "rat", value = 8 } } + local essenceBefore = game.player.essence + local success = game:interrogateCaptive() + eq(success, true, "interrogating a held captive succeeds") + eq(#game.level.captives, 0, "interrogating removes the captive from the level") + ok(game.level.secretDoors[1].discovered, "interrogating reveals a previously hidden door") + ok(game.level.secretDoors[2].discovered, "interrogating reveals every previously hidden door, not just one") + eq(game.player.essence, essenceBefore + 4, "interrogating grants essence equal to half the captive's value") +end + +function tests.captiveFieldsSurviveALevelSerializationRoundTrip() + local level = arena(21, 11) + level.captives = { { x = 5, y = 5, species = "rat", faction = Factions.VERMIN, name = "rat", value = 3 } } + local restored = Level.fromData(level:toData()) + eq(#restored.captives, 1, "a held captive survives a level round trip") + eq(restored.captives[1].name, "rat", "the captive's name survives a round trip") + eq(restored.captives[1].faction, Factions.VERMIN, "the captive's faction survives a round trip") + eq(restored.captives[1].value, 3, "the captive's value survives a round trip") + eq(restored:captiveAt(5, 5), restored.captives[1], "Level:captiveAt finds the restored captive by position") +end + +function tests.autopilotNetsABatteredAdjacentMonsterInsteadOfFinishingItInMeleeWhenCarryingANet() + local game = stagedGame(4, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 5, 1) + rat.hp = 1 + game.level.entities = { rat } + game.player.inventory[#game.player.inventory + 1] = Items.net() + game.rng = Rng.new(2) + game:choosePlayerAction() + eq(#game.level.captives, 1, "the autopilot nets the battered adjacent monster instead of finishing it off") + eq(#game.level.entities, 0, "the netted monster is removed from the level's entities") +end + +function tests.autopilotSellsAHeldCaptiveOnSight() + local game = stagedGame(4, arena(21, 11), 5, 5) + game.level.captives = { { x = 5, y = 5, species = "rat", faction = Factions.VERMIN, name = "rat", value = 5 } } + local goldBefore = game.player.gold + game:choosePlayerAction() + eq(#game.level.captives, 0, "the autopilot cashes in a held captive on sight") + eq(game.player.gold, goldBefore + 5, "the autopilot sells rather than ransoms or interrogates on its own") +end + +function tests.builderOccasionallyPlacesANetFromDepthFive() + local level = Builder.build(5, Rng.new(10), {}) + local found + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.NET then found = ground.item end + end + ok(found ~= nil, "this seed's depth-5 floor places a net") +end + +function tests.netsStayOffFloorsAboveDepthFive() + for seed = 1, 8 do + for depth = 1, 4 do + local level = Builder.build(depth, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + ok(ground.item.kind ~= Items.NET, "no net appears above depth 5") + end + end + end +end + +function tests.hordeBuildArenaIsFullyEnclosedWithAClearCentreReachableFromEveryCorner() + local map, cx, cy = Horde.buildArena(Rng.new(1), Horde.WIDTH, Horde.HEIGHT) + for x = 1, Horde.WIDTH do + eq(map:get(x, 1), Tiles.WALL, "the top border is solid wall") + eq(map:get(x, Horde.HEIGHT), Tiles.WALL, "the bottom border is solid wall") + end + for y = 1, Horde.HEIGHT do + eq(map:get(1, y), Tiles.WALL, "the left border is solid wall") + eq(map:get(Horde.WIDTH, y), Tiles.WALL, "the right border is solid wall") + end + ok(map:isWalkable(cx, cy), "the centre cell the player starts on is always clear") + for _, anchor in ipairs(Horde.spawnAnchors(Horde.WIDTH, Horde.HEIGHT)) do + ok(Astar.find(map, cx, cy, anchor.x, anchor.y) ~= nil, + "every corner anchor is reachable from the centre") + end +end + +function tests.hordeBuildArenaIsDeterministicForAFixedSeedAndDiffersAcrossSeeds() + local mapA = Horde.buildArena(Rng.new(5), Horde.WIDTH, Horde.HEIGHT) + local mapB = Horde.buildArena(Rng.new(5), Horde.WIDTH, Horde.HEIGHT) + eq(table.concat(mapA.tiles), table.concat(mapB.tiles), "the same seed always carves the same pillars") + local mapC = Horde.buildArena(Rng.new(6), Horde.WIDTH, Horde.HEIGHT) + ok(table.concat(mapA.tiles) ~= table.concat(mapC.tiles), "a different seed carves a different arena") +end + +function tests.hordeSpawnPointsAreUniqueWalkableCellsAndAlwaysCoverAMaxWave() + local map = Horde.buildArena(Rng.new(3), Horde.WIDTH, Horde.HEIGHT) + local points = Horde.spawnPoints(map) + ok(#points >= Horde.MAX_COUNT, "the spawn queue always has room for a maximum-size wave") + local seen = {} + for _, point in ipairs(points) do + local key = point.x .. "," .. point.y + ok(not seen[key], "no spawn cell is ever offered twice") + seen[key] = true + ok(map:isWalkable(point.x, point.y), "every offered spawn cell is walkable") + end +end + +function tests.hordeCountForClimbsWithWaveThenCapsAtMaxCount() + eq(Horde.countFor(1), Horde.BASE_COUNT, "wave one spawns the base count") end local order = {