diff --git a/tests/run.lua b/tests/run.lua index 142cddb..0e59bbc 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -7741,6 +7741,179 @@ function tests.builderCanRollATomeOfTheShockBoltSpell() -- seed, and the nearest shock tome under the new list happens to sit at -- seed 174 - still well within "a reasonable seed range". local found + for seed = 1, 200 do + local level = Builder.build(6, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.TOME and ground.item.spell == "shock" then found = ground.item end + end + if found then break end + end + ok(found ~= nil, "the shock bolt tome is reachable dungeon loot within a reasonable seed range") + eq(found.name, "tome of shock", "an unnamed tome defaults its label to the spell key") +end + +function tests.entityWetFieldSurvivesSerialization() + local game = Game.new(3) + game.player.wet = 2 + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.wet, 2, "the player's soak counter survives a save/load round trip") +end + +function tests.diggingCanDigAllowsOnlyWallsAndRubble() + local level = arena(21, 11) + ok(Digging.canDig(level.map, 1, 5), "the arena's own border wall is diggable") + ok(not Digging.canDig(level.map, 5, 5), "open floor has nothing left to dig") + level.map:set(5, 5, Tiles.RUBBLE) + ok(Digging.canDig(level.map, 5, 5), "rubble left by a prior swing is still diggable") + level.map:set(5, 5, Tiles.VAULT_DOOR) + ok(not Digging.canDig(level.map, 5, 5), "a vault's own boundary never yields to a pick") + ok(not Digging.canDig(level.map, 0, 5), "a cell outside the map is never diggable") +end + +function tests.diggingSwingClearsAWallToRubbleOnAnOrdinarySwing() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(5, 5, Tiles.WALL) + game.rng = Rng.new(1) + local line = Digging.swing(game, game.player, 5, 5) + eq(game.level.map:get(5, 5), Tiles.RUBBLE, "an ordinary swing crumbles a wall to rubble, not clear") + ok(contains(line, "carve"), "the log line describes carving through rock") +end + +function tests.diggingSwingClearsRubbleToFloorOnASecondSwing() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(5, 5, Tiles.RUBBLE) + game.rng = Rng.new(1) + local line = Digging.swing(game, game.player, 5, 5) + eq(game.level.map:get(5, 5), Tiles.FLOOR, "a second swing clears rubble the rest of the way to open floor") + ok(contains(line, "clear"), "the log line describes clearing rubble away") +end + +function tests.diggingSwingCaveInHurtsTheDiggerAndLeavesTheTileUntouched() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(5, 5, Tiles.WALL) + game.rng = Rng.new(6) + local hpBefore = game.player.hp + local line = Digging.swing(game, game.player, 5, 5) + eq(game.level.map:get(5, 5), Tiles.WALL, "a cave-in wastes the swing; the wall is untouched") + ok(game.player.hp < hpBefore, "a cave-in deals real damage to the digger") + ok(contains(line, "buries you"), "the log line names the player directly, not by their entity name") +end + +function tests.diggingSwingOreStrikeClearsTheWallOutrightAndPaysEssenceToATrackingDigger() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(5, 5, Tiles.WALL) + game.rng = Rng.new(2) + local essenceBefore = game.player.essence + local line = Digging.swing(game, game.player, 5, 5) + eq(game.level.map:get(5, 5), Tiles.FLOOR, "a lucky ore strike clears the wall in a single stroke") + ok(game.player.essence > essenceBefore, "striking a vein pays essence to a digger that tracks it") + ok(contains(line, "essence"), "the log line reports the essence payout") +end + +function tests.diggingSwingOreStrikeSkipsTheEssenceRewardForADiggerThatDoesNotTrackIt() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(5, 5, Tiles.WALL) + local burrower = Entity.new({ id = 1, x = 4, y = 5, glyph = "b", name = "rock burrower", kind = "monster", hp = 12, attack = 4, defense = 2 }) + game.rng = Rng.new(2) + local line = Digging.swing(game, burrower, 5, 5) + eq(game.level.map:get(5, 5), Tiles.FLOOR, "the same lucky strike still clears the wall for a monster") + eq(burrower.essence, nil, "a monster never gains an essence field it never had") + ok(contains(line, "vein"), "the log line still names the vein even without a payout") +end + +function tests.diggingWearPickDepletesDurabilityAndBreaksAtZero() + local entity = { pick = Items.pick("test pick", 2) } + eq(Digging.wearPick(entity), nil, "the first swing just wears the edge down") + eq(entity.pick.uses, 1, "one use is spent") + local brokenLine = Digging.wearPick(entity) + ok(contains(brokenLine, "crumbles"), "the swing that empties the pick reports it breaking") + eq(entity.pick, nil, "a spent pick is discarded outright") +end + +function tests.diggingWearPickIsANoOpWithoutAPickEquipped() + local entity = {} + eq(Digging.wearPick(entity), nil, "wearing down a pick that was never equipped changes nothing") +end + +function tests.diggingDirectionsTowardPrefersTheLongerAxisFirst() + local dirs = Digging.directionsToward(10, 10, 15, 12) + eq(#dirs, 2, "a diagonal target yields both an x and a y candidate") + eq(dirs[1][1], 1, "the longer x delta is offered first") + eq(dirs[1][2], 0, "the first candidate is a pure x step") + eq(dirs[2][1], 0, "the second candidate is a pure y step") + eq(dirs[2][2], 1, "it steps toward the smaller but still positive y delta") + + local vertical = Digging.directionsToward(10, 10, 11, 5) + eq(vertical[1][1], 0, "when y dominates, the y step comes first") + eq(vertical[1][2], -1, "stepping toward a smaller y is a negative step") +end + +function tests.diggingDirectionsTowardOmitsAZeroDelta() + local dirs = Digging.directionsToward(10, 10, 15, 10) + eq(#dirs, 1, "a purely horizontal target yields only one candidate") + eq(dirs[1][1], 1, "the sole candidate steps toward positive x") + eq(dirs[1][2], 0, "the sole candidate carries no y component") + + local same = Digging.directionsToward(10, 10, 10, 10) + eq(#same, 0, "already standing on the target yields no candidate at all") +end + +function tests.itemsEquipPickSwapsAnyPriorPickBackIntoInventory() + local entity = { inventory = {}, pick = Items.pick("worn pick", 3) } + local fresh = Items.pick("fresh pick", 20) + Items.equipPick(fresh, entity) + eq(entity.pick, fresh, "the freshly equipped pick becomes the active one") + eq(#entity.inventory, 1, "the previously equipped pick drops back into the inventory") + eq(entity.inventory[1].name, "worn pick", "the displaced pick is the one that was equipped before") +end + +function tests.shopPicksHaveAPositiveCatalogPrice() + ok(Shop.priceOf(Items.pick()) > 0, "a miner's pick has a positive catalog price") +end + +function tests.gameDigFailsWithoutAPickEquipped() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(4, 6, Tiles.WALL) + local moved = game:dig(1, 0) + eq(moved, false, "digging without a pick equipped fails outright") + eq(game.level.map:get(4, 6), Tiles.WALL, "the wall is untouched without a pick") + ok(contains(game.log:last(), "nothing to dig with"), "the log explains why") +end + +function tests.gameDigFailsWhenNothingAheadIsDiggable() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.pick = Items.pick() + local moved = game:dig(1, 0) + eq(moved, false, "there is nothing to dig into open floor") + ok(contains(game.log:last(), "nothing here to dig"), "the log explains why") +end + +function tests.gameDigClearsAWallAndWearsThePickDown() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(4, 6, Tiles.WALL) + game.player.pick = Items.pick("miner's pick", 5) + game.rng = Rng.new(1) + local moved = game:dig(1, 0) + eq(moved, true, "a swing against a wall with a pick readied succeeds") + eq(game.level.map:get(4, 6), Tiles.RUBBLE, "the wall crumbles to rubble") + eq(game.player.pick.uses, 4, "the swing spends one use of the pick's durability") +end + +function tests.gameDigBreaksThePickOnItsLastUse() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.map:set(4, 6, Tiles.WALL) + game.player.pick = Items.pick("worn pick", 1) + game.rng = Rng.new(1) + game:dig(1, 0) + eq(game.player.pick, nil, "the pick's last use breaks it outright") + ok(contains(game.log:last(), "crumbles"), "the log reports the pick breaking") +end + +function tests.gameMonsterTunnelsThroughAWallToReachAndAttackThePlayer() + local level = arena(21, 11) end local order = {