diff --git a/tests/run.lua b/tests/run.lua index d76d7fa..a10690c 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -9644,6 +9644,179 @@ function tests.gameOpenChestFallsBackToALockpickWhenNoKeyIsCarried() for _, item in ipairs(game.player.inventory) do if item.kind == Items.LOCKPICK then stillHasPick = true end end + ok(not stillHasPick, "the lockpick is spent gambling against the lock with no key on hand") +end + +function tests.gameOpenChestRefusesWhenNothingIsHereOrItIsAlreadyEmpty() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.chests = {} + eq(game:openChest(), false, "opening with no chest underfoot fails") + local chest = { x = 3, y = 6, locked = false, trapped = false, opened = true, loot = {} } + game.level.chests = { chest } + eq(game:openChest(), false, "opening an already-emptied chest fails") +end + +function tests.chestsFieldsSurviveSerialization() + local level = arena(21, 11) + local chest = Chests.new(Rng.new(4), 3) + chest.x, chest.y = 5, 5 + chest.locked = true + chest.trapped = true + level.chests = { chest } + local restored = Level.fromData(level:toData()) + eq(#restored.chests, 1, "the chest list survives a round trip") + eq(restored.chests[1].locked, true, "a chest's locked flag survives a round trip") + eq(restored.chests[1].trapped, true, "a chest's trapped flag survives a round trip") + eq(restored.chests[1].opened, false, "a chest's opened flag survives a round trip") + eq(#restored.chests[1].loot, #chest.loot, "a chest's loot list survives a round trip intact") + eq(restored.chests[1].loot[1].kind, chest.loot[1].kind, "a chest's loot items keep their own kind") +end + +function tests.renderShowsAClosedChestGlyphAndAnOpenedOneOnceEmptied() + local game = stagedGame(1, arena(21, 11), 3, 6) + local chest = { x = 4, y = 6, locked = true, trapped = false, opened = false, loot = { Items.gold(5) } } + game.level.chests = { chest } + game:computeVisible() + local frame = game:render() + ok(contains(frame, "B"), "a closed chest renders as its own glyph") + chest.opened = true + chest.loot = {} + frame = game:render() + ok(contains(frame, "b"), "an emptied chest renders as its own distinct glyph") +end + +function tests.autopilotOpensAnUnlockedChestOnSightButSkipsALockedOneWithNoKeyOrPick() + local game = stagedGame(1, arena(21, 11), 3, 6) + local chest = { x = 3, y = 6, locked = false, trapped = false, opened = false, loot = { Items.gold(5) } } + game.level.chests = { chest } + game:choosePlayerAction() + eq(chest.opened, true, "the autopilot opens a free chest the instant it stands on one") + + local lockedGame = stagedGame(1, arena(21, 11), 3, 6) + local locked = { x = 3, y = 6, locked = true, trapped = false, opened = false, loot = { Items.gold(5) } } + lockedGame.level.chests = { locked } + local beforeX, beforeY = lockedGame.player.x, lockedGame.player.y + lockedGame:choosePlayerAction() + eq(locked.opened, false, "the autopilot never gambles a lock it has neither a key nor a lockpick for") + ok(lockedGame.player.x ~= beforeX or lockedGame.player.y ~= beforeY, + "the autopilot still makes progress toward the stairs instead of parking on an unopenable chest") +end + +function tests.morgueScoreWeighsEveryStatByItsOwnFixedWeight() + local w = Morgue.SCORE_WEIGHTS + local report = { + deepestDepth = 4, kills = 12, bossKills = 1, questsCompleted = 2, artifactsFound = 1, + vaultsOpened = 1, setPiecesFound = 3, chestsOpened = 5, playerLevel = 6, gold = 40, won = false, + } + local expected = 4 * w.depth + 12 * w.kill + 1 * w.bossKill + 2 * w.quest + 1 * w.artifact + + 1 * w.vault + 3 * w.setPiece + 5 * w.chest + 6 * w.playerLevel + 40 + eq(Morgue.score(report), expected, "score sums every weighted stat plus flat gold") +end + +function tests.morgueScoreAddsTheWinBonusOnlyWhenWon() + local base = { + deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, + vaultsOpened = 0, setPiecesFound = 0, chestsOpened = 0, playerLevel = 1, gold = 0, won = false, + } + local lost = Morgue.score(base) + base.won = true + local won = Morgue.score(base) + eq(won - lost, Morgue.SCORE_WEIGHTS.win, "winning adds exactly the win bonus and nothing else") +end + +function tests.morgueEquipmentListsOnlyOccupiedSlotsInFixedOrder() + local game = Game.new(9) + eq(#Morgue.equipment(game.player), 0, "a fresh player with nothing worn has no equipment lines") + game.player.armor = Items.armor("plate", 4) + game.player.weapon = Items.weapon("sword", 3) + local lines = Morgue.equipment(game.player) + eq(#lines, 2, "only the two worn slots produce a line") + eq(lines[1], "weapon: sword", "weapon is listed before armor regardless of assignment order") + eq(lines[2], "armor: plate", "armor keeps its own slot label") +end + +function tests.morgueEquipmentPrefixesARevealedCurseButNotAHiddenOne() + local game = Game.new(9) + local ring = Items.ring("ring of woe", 0, 0) + Curses.afflict(ring, Curses.pool.ring[1]) + game.player.ring = ring + eq(Morgue.equipment(game.player)[1], "ring: ring of woe", "an unrevealed curse reads like an ordinary item") + Curses.reveal(ring) + eq(Morgue.equipment(game.player)[1], "ring: cursed ring of woe", "a revealed curse is prefixed") +end + +function tests.morgueKillTallySortsByCountThenByCatalogOrderOnTies() + local compendium = Compendium.new() + Compendium.noteKill(compendium, "goblin") + Compendium.noteKill(compendium, "rat") + Compendium.noteKill(compendium, "rat") + Compendium.noteKill(compendium, "bat") + local rows = Morgue.killTally(compendium) + eq(#rows, 3, "only species with at least one kill appear") + eq(rows[1].key, "rat", "the heaviest kill count sorts first") + eq(rows[1].kills, 2, "rat's tally is exactly two") + eq(rows[2].key, "bat", "a tie between bat and goblin breaks toward bat's earlier catalog slot") + eq(rows[3].key, "goblin", "goblin follows bat on the same tied count") +end + +function tests.morgueBuildMirrorsGameStatsPlayerAndCompendium() + local game = Game.new(21) + game.stats.kills = 3 + game.stats.bossKills = 1 + game.deepestDepth = 5 + game.turn = 120 + game.player.gold = 77 + Compendium.noteKill(game.compendium, "orc") + local report = Morgue.build(game) + eq(report.kills, 3, "build copies the live kill tally") + eq(report.bossKills, 1, "build copies the live boss kill tally") + eq(report.deepestDepth, 5, "build copies the deepest depth reached") + eq(report.turn, 120, "build copies the current turn") + eq(report.gold, 77, "build copies the player's gold") + eq(report.alive, true, "a living player's report says so") + eq(#report.killTally, 1, "the compendium's own kill feeds straight into the tally") + eq(report.killTally[1].key, "orc", "the tallied species matches what was actually killed") +end + +function tests.morgueFormatRendersOutcomeGearKillsAndScoreInOrder() + local game = Game.new(3) + game.player.weapon = Items.weapon("axe", 2) + Compendium.noteKill(game.compendium, "kobold") + game.player.hp = 0 + local text = Morgue.format(Morgue.build(game)) + ok(contains(text, "died in the dark"), "a dead player's obituary names the outcome") + ok(contains(text, "weapon: axe"), "the report lists the worn weapon") + ok(contains(text, "kobold"), "the report lists a slain species") + local scoreLine = "score: " .. Morgue.score(Morgue.build(game)) + ok(contains(text, scoreLine), "the report ends with the matching numeric score") + local scorePos = text:find("score:", 1, true) + local killsPos = text:find("kills:", 1, true) + ok(killsPos < scorePos, "the kill tally is printed before the score line") +end + +function tests.morgueFormatNamesTheWinOutcomeWhenWon() + local game = Game.new(3) + game.won = true + local text = Morgue.format(Morgue.build(game)) + ok(contains(text, "Victorious the wanderer"), "a won run's heading calls the player victorious") + ok(contains(text, "escaped the warren with the Amulet"), "a won run's outcome line names the escape") +end + +function tests.morgueFormatOmitsEquipmentAndKillsSectionsWhenEmpty() + local game = Game.new(3) + local text = Morgue.format(Morgue.build(game)) + ok(not contains(text, "equipment:"), "an unarmed player's report has no equipment section") + ok(not contains(text, "kills:"), "a report with no recorded kills has no kill section") + ok(contains(text, "still drawing breath"), "a living player's report says so") +end + +function tests.leaderboardRecordOnAnEmptyBoardLandsAtRankOne() + local board = Leaderboard.new() + local rank = Leaderboard.record(board, { score = 100 }) + eq(rank, 1, "the first entry ever recorded ranks first") + eq(#board, 1, "the board now holds exactly one entry") +end + end local order = {