diff --git a/tests/run.lua b/tests/run.lua index d5f1041..56a432c 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -12760,6 +12760,179 @@ function tests.gameEnterRiftAndExitRiftBothCarryALivingCompanionAcrossTheCrossin eq(strandedInPocket, 0, "the companion is not left stranded on the pocket floor either") end +function tests.choosePlayerActionEntersAWellHealedRiftUnderfootButSkipsItWhenBadlyHurt() + local hurt = stagedGame(1, arena(21, 11), 5, 5) + hurt.level.rifts = { { x = 5, y = 5 } } + hurt.player.inventory[#hurt.player.inventory + 1] = Items.riftstone() + hurt.player.hp = math.floor(hurt.player.maxHp * 0.5) + hurt:choosePlayerAction() + eq(hurt.rift or false, false, "the autopilot never spends a shard while badly hurt") + + local healthy = stagedGame(1, arena(21, 11), 5, 5) + healthy.level.rifts = { { x = 5, y = 5 } } + healthy.player.inventory[#healthy.player.inventory + 1] = Items.riftstone() + healthy:choosePlayerAction() + eq(healthy.rift, true, "the autopilot crosses a rift underfoot on sight while at good health") +end + +function tests.choosePlayerActionStepsThroughAndThenExitsARiftUnderAutopilot() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.rifts = { { x = 5, y = 5 } } + game.player.inventory[#game.player.inventory + 1] = Items.riftstone() + game:choosePlayerAction() + ok(game.rift, "the autopilot has crossed into the pocket floor") + game.player.x, game.player.y = game.level.downX, game.level.downY + game:choosePlayerAction() + eq(game.rift, false, "standing at the pocket floor's own exit, the autopilot immediately steps back out") +end + +function tests.renderDrawsARiftGlyphWhenVisible() + local rendered = stagedGame(1, arena(21, 11), 3, 6) + rendered.level.rifts = { { x = 5, y = 6 } } + rendered:computeVisible() + ok(contains(rendered:render(), "Q"), "a rift on a visible cell renders as its glyph") +end + +function tests.gameSaveLoadRoundTripsAnActiveRiftCrossing() + local game = Game.new(9) + game.level.rifts = { { x = game.player.x, y = game.player.y } } + game.player.inventory[#game.player.inventory + 1] = Items.riftstone() + ok(game:enterRift(), "the staged rift crossing succeeds") + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.rift, true, "the rift flag survives a save/load round trip") + eq(restored.depth, game.depth, "depth survives the round trip") + eq(table.concat(restored.level.map.tiles), table.concat(game.level.map.tiles), + "the active pocket floor's map survives the round trip") + eq(restored.riftReturn.x, game.riftReturn.x, "the remembered return position survives the round trip") + eq(restored.riftReturn.y, game.riftReturn.y, "the remembered return position survives the round trip") + eq(restored.riftReturn.depth, game.riftReturn.depth, "the remembered return depth survives the round trip") + restored.player.x, restored.player.y = restored.level.downX, restored.level.downY + ok(restored:exitRift(), "exiting still works correctly after a save/load round trip") + eq(restored.rift, false, "the rift flag clears after exiting post-load") +end + +function tests.gameSaveLoadRoundTripsAnActiveHordeArena() + local game = Game.new(11) + game:startHorde() + -- Clear wave one so the round trip has to prove more than just "wave one + -- survives" - the wave counter must actually be the one Horde.tick + -- advanced to, not always reset back to its Horde.newState() default. + for _, entity in ipairs(game.level.entities) do entity.hp = 0 end + game:cleanupDead() + game:endPlayerTurn() + eq(game.horde.wave, 2, "the staged run has advanced to wave two before saving") + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + ok(restored.horde, "an active horde run survives a save/load round trip at all") + eq(restored.horde.wave, 2, "the exact wave in progress survives the round trip") + eq(restored.level.generator, "horde", "the restored level is still tagged as the arena") + eq(table.concat(restored.level.map.tiles), table.concat(game.level.map.tiles), + "the arena's own map survives the round trip rather than falling back to the abandoned floor") + eq(#restored.level.entities, #game.level.entities, "wave two's monster count survives the round trip") + eq(restored.player.x, game.player.x, "the player's arena position survives the round trip") + eq(restored.player.y, game.player.y, "the player's arena position survives the round trip") + for _, entity in ipairs(restored.level.entities) do entity.hp = 0 end + restored:cleanupDead() + restored:endPlayerTurn() + eq(restored.horde.wave, 3, "the restored run's Horde.tick still advances waves normally after loading") +end + +function tests.gameSaveLoadOfAnOrdinaryRunNeverGainsAHordeField() + local game = Game.new(3) + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.horde, nil, "a run that never entered the arena has no horde field after a round trip") + eq(restored.level.generator, game.level.generator, + "an ordinary floor's own generator survives the round trip unaffected by the horde snapshot fields") +end + +-- commands.lua: the typed-command parser/dispatcher layered on top of the +-- same granular Game: methods choosePlayerAction already drives, and +-- Game:manualStep, its integration into the turn cycle (see game.lua's +-- Game:beginTurn/autoStep for the shared pre-turn head both share). + +function tests.commandsParseSplitsAVerbAndArgsLowercasedAndTrimmed() + local parsed = Commands.parse(" ThRoW 3 ") + eq(parsed.verb, "throw", "the verb is lowercased") + eq(parsed.args[1], "3", "the remaining token becomes an arg") + eq(Commands.parse(""), nil, "an empty line parses to nil") + eq(Commands.parse(" "), nil, "an all-whitespace line parses to nil") +end + +function tests.commandsApplyRejectsAnUnknownVerbWithoutSpendingATurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + local consumesTurn, message = Commands.apply(game, "dance") + eq(consumesTurn, false, "an unrecognized verb spends no turn") + ok(contains(message, "unknown command"), "the message names the bad verb") +end + +function tests.commandsApplyMovingIntoAWallFailsWithoutSpendingATurn() + local game = stagedGame(1, arena(21, 11), 2, 2) + local consumesTurn, message = Commands.apply(game, "north") + eq(consumesTurn, false, "walking into the arena's own boundary wall spends no turn") + eq(message, "you cannot go that way", "the failure explains itself") + eq(game.player.x, 2, "the player never actually moves on a rejected step") + eq(game.player.y, 2, "the player never actually moves on a rejected step") +end + +function tests.commandsApplyMovementBumpsIntoAnAdjacentHostileAsAnAttack() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 5, 1) + rat.hp = 20 + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "east") + eq(consumesTurn, true, "bumping into a hostile spends the turn") + eq(game.player.x, 5, "the player does not step onto the monster's own tile") + ok(rat.hp < 20, "the bump attack actually lands through Game:movePlayer") +end + +function tests.commandsApplyAttackAliasesMovementIntoAnExplicitDirection() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 5, 1) + rat.hp = 20 + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "attack east") + eq(consumesTurn, true, "attack spends the turn") + ok(rat.hp < 20, "attack lands the same bump attack a plain direction would") + local rejected, message = Commands.apply(game, "attack sideways") + eq(rejected, false, "attack with an unrecognized direction word spends no turn") + eq(message, "attack which direction?", "the failure names what is missing") +end + +function tests.commandsApplyWaitAlwaysSucceedsAndSpendsTheTurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + local consumesTurn, message = Commands.apply(game, "wait") + eq(consumesTurn, true, "waiting always spends the turn") + eq(message, "you wait", "waiting reports itself") +end + +function tests.commandsApplyPickupSweepsFloorItemsAtThePlayersFeetAndSpendsTheTurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.floorItems = { { x = 5, y = 5, item = Items.gold(15) } } + local goldBefore = game.player.gold + local consumesTurn = Commands.apply(game, "pickup") + eq(consumesTurn, true, "pickup always spends the turn") + eq(game.player.gold, goldBefore + 15, "gold sitting on the player's own tile is swept up") + eq(#game.level.floorItems, 0, "the picked-up item leaves the floor") +end + +function tests.commandsApplyUseDrinksAPotionByInventoryIndexAndSpendsTheTurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + local before = #game.player.inventory + local consumesTurn = Commands.apply(game, "use 1") + eq(consumesTurn, true, "using a valid inventory item spends the turn") + eq(#game.player.inventory, before - 1, "the consumed potion leaves the inventory") +end + +end + local order = { "rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine", "astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects",