From e7b64c1e92a7f88b190ed974654e94623e4dc73d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 21:00:13 +0200 Subject: [PATCH] test: cover tests.amuletSpawnsOnFinalFloorOnly --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 2694f66..bad457e 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -2034,6 +2034,179 @@ function tests.autoplayDescends() eq(again.turn, run.turn, "the autopilot takes the same number of turns") end +function tests.amuletSpawnsOnFinalFloorOnly() + local finalLevel = Builder.build(Builder.MAX_DEPTH, Rng.new(999), {}) + local finalCount = 0 + for _, ground in ipairs(finalLevel.floorItems) do + if ground.item.kind == Items.AMULET then finalCount = finalCount + 1 end + end + eq(finalCount, 1, "exactly one amulet is placed on the final floor") + + local shallowLevel = Builder.build(Builder.MAX_DEPTH - 1, Rng.new(999), {}) + local shallowCount = 0 + for _, ground in ipairs(shallowLevel.floorItems) do + if ground.item.kind == Items.AMULET then shallowCount = shallowCount + 1 end + end + eq(shallowCount, 0, "no amulet appears above the final floor") +end + +function tests.descendIsBlockedAtTheFinalFloor() + local game = Game.new(555) + game.depth = game.maxDepth + game.level = game.dungeon:get(game.maxDepth) + game.player.x, game.player.y = game.level.downX, game.level.downY + local result = game:descend() + eq(result, false, "descend fails once at the final floor") + eq(game.depth, game.maxDepth, "depth is unchanged after a blocked descend") + ok(contains(game.log:last(), "rock"), "a message explains why the descent is blocked") +end + +function tests.ascendWithoutAmuletDoesNotWin() + local game = Game.new(7) + game.player.x, game.player.y = game.level.upX, game.level.upY + local result = game:ascend() + eq(result, true, "climbing out at depth 1 is still a valid move") + eq(game.won, false, "climbing out without the amulet does not win the game") +end + +function tests.ascendWithAmuletWinsTheGame() + local game = Game.new(7) + game.player.inventory[#game.player.inventory + 1] = Items.amulet() + game.player.x, game.player.y = game.level.upX, game.level.upY + local result = game:ascend() + eq(result, true, "climbing out at depth 1 with the amulet succeeds") + eq(game.won, true, "carrying the amulet to the surface wins the game") + ok(contains(game.log:last(), "win"), "the victory is logged") +end + +function tests.wonGameHaltsTheAutopilot() + local game = Game.new(7) + game.won = true + local turnBefore = game.turn + game:autoStep() + eq(game.turn, turnBefore, "autoStep is a no-op once the game is already won") +end + +function tests.autopilotFetchesTheAmuletAndWins() + local function playout(seed) + local game = Game.new(seed, { maxDepth = 3 }) + for _ = 1, 900 do + if not game.player:isAlive() or game.won then break end + game:autoStep() + end + return game + end + local run = playout(67) + ok(run.player:isAlive(), "the autopilot survives the round trip on this seed") + eq(run.won, true, "the autopilot retrieves the amulet and escapes") + eq(run.depth, 1, "winning leaves the player back at depth 1") + ok(run:hasAmulet(), "the amulet is still in the winning player's inventory") + local again = playout(67) + eq(again.turn, run.turn, "the winning playthrough takes the same number of turns on the same seed") +end + +function tests.shopPriceCatalogScalesWithPower() + local base = Shop.priceOf(Items.weapon("dagger", 2)) + local strong = Shop.priceOf(Items.weapon("battle axe", 5)) + ok(strong > base, "a more powerful weapon costs more at the shop") + eq(Shop.priceOf(Items.potionOf("heal")), 15, "a catalogued potion has its fixed price") + eq(Shop.sellValue(Items.potionOf("heal")), 7, "selling nets half the catalog price, rounded down") +end + +function tests.shopStockRollsAffordableWares() + local wares = Shop.stock(Rng.new(7)) + ok(#wares >= 3 and #wares <= 5, "a shop rolls three to five wares") + for _, item in ipairs(wares) do + ok(Shop.priceOf(item) > 0, "every ware has a positive price") + end +end + +function tests.goldPickupAddsToWalletNotInventory() + local game = stagedGame(1, arena(21, 11), 3, 3) + local before = #game.player.inventory + local goldBefore = game.player.gold + game.level.floorItems = { { x = 4, y = 3, item = Items.gold(35) } } + game:movePlayer(1, 0) + eq(game.player.gold, goldBefore + 35, "walking onto gold adds it to the wallet") + eq(#game.player.inventory, before, "gold never occupies an inventory slot") + eq(#game.level.floorItems, 0, "the gold pile is removed from the floor") +end + +function tests.affordableWareIsBoughtAutomaticallyOnPickup() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.player.gold = 50 + local before = #game.player.inventory + game.level.floorItems = { { x = 4, y = 3, item = Items.potionOf("heal"), price = 15, shopId = keeper.id } } + game:movePlayer(1, 0) + eq(game.player.gold, 35, "the ware's stamped price is deducted from the wallet") + eq(#game.player.inventory, before + 1, "the bought ware joins the inventory") + eq(#game.level.floorItems, 0, "the purchased ware leaves the ground") +end + +function tests.unaffordableWareStaysOnTheGround() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.player.gold = 5 + local before = #game.player.inventory + game.level.floorItems = { { x = 4, y = 3, item = Items.potionOf("heal"), price = 15, shopId = keeper.id } } + game:movePlayer(1, 0) + eq(game.player.gold, 5, "an unaffordable ware is never bought, so no gold changes hands") + eq(#game.player.inventory, before, "the ware never reaches the inventory") + eq(#game.level.floorItems, 1, "the unbought ware is left in place") +end + +function tests.deadKeeperMakesWaresFreeToLoot() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + keeper.hp = 0 + game.level.entities = { keeper } + game.player.gold = 0 + local before = #game.player.inventory + game.level.floorItems = { { x = 4, y = 3, item = Items.potionOf("heal"), price = 15, shopId = keeper.id } } + game:movePlayer(1, 0) + eq(game.player.gold, 0, "looting a dead keeper's stock costs nothing") + eq(#game.player.inventory, before + 1, "the ware is picked up normally once its keeper is dead") +end + +function tests.stealingFromShopTurnsTheKeeperHostile() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.player.gold = 0 + local before = #game.player.inventory + game.level.floorItems = { { x = 4, y = 3, item = Items.scroll("identify"), price = 18, shopId = keeper.id } } + local stolen = game:stealFromShop(1) + eq(stolen, true, "stealing succeeds against a live keeper's ware") + eq(game.player.gold, 0, "theft costs no gold") + eq(#game.player.inventory, before + 1, "the stolen item joins the inventory") + eq(keeper.hostile, true, "the keeper turns hostile after the theft") + ok(contains(game.log:last(), "steal"), "the theft is logged") +end + +function tests.hostileShopkeeperChasesLikeAnyMonster() + local game = stagedGame(1, arena(21, 11), 3, 6) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 8, 6, 4) + keeper.hostile = true + game.level.entities = { keeper } + Ai.act(game, keeper) + eq(keeper.x, 7, "a hostile shopkeeper closes in on the player like a chaser") +end + +function tests.passiveShopkeeperIgnoresTheAdjacentPlayer() + local game = stagedGame(1, arena(21, 11), 3, 6) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 4, 6, 4) + game.level.entities = { keeper } + local hpBefore = game.player.hp + Ai.act(game, keeper) + eq(keeper.x, 4, "an un-robbed shopkeeper does not move") + eq(game.player.hp, hpBefore, "an un-robbed shopkeeper does not attack even when adjacent") +end + +end + local order = { "rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine", "astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects",