From 8e7c57dcbfffd352a2ff3985a5b0d7b50c44a7ef Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 23:12:01 +0200 Subject: [PATCH] test: cover tests.streaksCurrentIsZeroAndNilOnAnEmptyHistory --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index e85c777..4204c47 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -10163,6 +10163,179 @@ function tests.exportCompendiumCsvListsEverySpeciesSeenOrNot() ok(contains(csv, "false,0"), "an undiscovered species still reports as unseen with a zero tally") end +function tests.streaksCurrentIsZeroAndNilOnAnEmptyHistory() + local length, outcome = Streaks.current(Streaks.new()) + eq(length, 0, "an empty history has a streak length of zero") + eq(outcome, nil, "an empty history has no outcome to report") +end + +function tests.streaksCurrentCountsOnlyTheUnbrokenRunAtTheEnd() + local history = Streaks.new() + Streaks.record(history, true) + Streaks.record(history, true) + Streaks.record(history, false) + Streaks.record(history, false) + Streaks.record(history, false) + local length, outcome = Streaks.current(history) + eq(length, 3, "three losses in a row at the tail end give a streak of three") + eq(outcome, false, "the current streak's outcome is a loss") + + Streaks.record(history, true) + length, outcome = Streaks.current(history) + eq(length, 1, "a single win right after a loss streak is itself a streak of exactly one") + eq(outcome, true, "the fresh win flips the current outcome to a win") +end + +function tests.streaksRecordTrimsHistoryPastItsCap() + local history = Streaks.new() + for i = 1, Streaks.HISTORY_CAP + 10 do + Streaks.record(history, i % 2 == 0) + end + eq(#history, Streaks.HISTORY_CAP, "history never grows past its fixed cap") +end + +function tests.streaksRecordDropsTheOldestEntryNotTheNewestOnceOverCap() + local history = Streaks.new() + for i = 1, Streaks.HISTORY_CAP do + Streaks.record(history, false) + end + eq(#history, Streaks.HISTORY_CAP, "filling exactly to the cap keeps every entry") + Streaks.record(history, true) + eq(#history, Streaks.HISTORY_CAP, "one more push still respects the same cap") + eq(history[#history], true, "the newest push is the one that survives at the tail") + eq(history[1], false, "the oldest entry was the one dropped to make room") +end + +function tests.streaksBestWinStreakFindsTheLongestWinRunAnywhereNotJustTheActiveOne() + local history = Streaks.new() + for _, won in ipairs({ true, true, true, false, true, false, true, true }) do + Streaks.record(history, won) + end + eq(Streaks.bestWinStreak(history), 3, "the best win streak is the earlier run of three, not the trailing two") + local currentLength, currentOutcome = Streaks.current(history) + eq(currentLength, 2, "the currently active streak is only two long") + eq(currentOutcome, true, "the currently active streak is a winning one") +end + +function tests.streaksBestWinStreakIsZeroWithNoWinsAtAll() + local history = Streaks.new() + Streaks.record(history, false) + Streaks.record(history, false) + eq(Streaks.bestWinStreak(history), 0, "a history of nothing but losses has a best win streak of zero") +end + +function tests.metaRecordRunFeedsStreaksAndRunHistorySurvivesASaveLoadRoundTrip() + local fresh = Meta.load("/tmp/warren_meta_does_not_exist_71234.sav") + eq(#fresh.runHistory, 0, "a freshly defaulted meta state has an empty run history") + local meta = Meta.new() + Meta.recordRun(meta, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = true }) + Meta.recordRun(meta, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = false }) + eq(#meta.runHistory, 2, "each recorded run appends exactly one entry to the history") + local currentLength, currentOutcome = Streaks.current(meta.runHistory) + eq(currentLength, 1, "the most recent recorded run is a fresh streak of one") + eq(currentOutcome, false, "the most recent recorded run was a loss") + local path = os.tmpname() + Meta.save(meta, path) + local restored = Meta.load(path) + os.remove(path) + eq(#restored.runHistory, 2, "run history survives a save/load round trip") + eq(restored.runHistory[1], true, "the oldest entry keeps its own outcome") + eq(restored.runHistory[2], false, "the newest entry keeps its own outcome") +end + +function tests.metaRecordRunUnlocksComebackOnlyAfterALossThenAWin() + local meta = Meta.new() + Meta.recordRun(meta, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = false }) + eq(meta.achievements.comeback, nil, "a lone loss cannot unlock a comeback") + Meta.recordRun(meta, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = true }) + eq(meta.achievements.comeback, true, "winning right after a loss unlocks the comeback") +end + +function tests.metaRecordRunDoesNotUnlockComebackOnAFirstEverWinOrAWinAfterAWin() + local isolated = Meta.new() + Meta.recordRun(isolated, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = true }) + eq(isolated.achievements.comeback, nil, "a first-ever run that wins has no prior loss to come back from") + + local streak = Meta.new() + Meta.recordRun(streak, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = true }) + Meta.recordRun(streak, { deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, won = true }) + eq(streak.achievements.comeback, nil, "a win right after another win is not a comeback") +end + +-- Nemesis (nemesis.lua): a cross-floor hunter born mid-run from a plain +-- monster that was actually hunting the player at the stairs, escalating +-- every time it follows without being killed first. + +function tests.nemesisIsEligibleRequiresAHuntingHostileAndExcludesEverySpecialCase() + local orc = Monsters.create(Monsters.byKey.orc, 1, 5, 5, 1) + ok(not Nemesis.isEligible(orc), "a monster that has not noticed the player yet is not eligible") + orc.awareness = Stealth.HUNTING + ok(Nemesis.isEligible(orc), "a hunting monster under a self-directed ai is eligible") + + local wretch = Monsters.create(Monsters.byKey.wretch, 2, 5, 5, 1) + wretch.awareness = Stealth.HUNTING + ok(not Nemesis.isEligible(wretch), "a permanent flee species never gives chase down a stairwell") + + local shopkeeper = Monsters.create(Monsters.byKey.shopkeeper, 3, 5, 5, 4) + shopkeeper.awareness = Stealth.HUNTING + ok(not Nemesis.isEligible(shopkeeper), "the shopkeeper's vend ai is never eligible") + + local boss = Bosses.create(Bosses.byKey.lich, 4, 5, 5) + boss.awareness = Stealth.HUNTING + ok(not Nemesis.isEligible(boss), "a boss already has its own escalation path") + + local elite = Monsters.create(Monsters.byKey.orc, 5, 5, 5, 3) + elite.awareness, elite.elite = Stealth.HUNTING, true + ok(not Nemesis.isEligible(elite), "an elite already has its own escalation path") + + local mimic = Mimics.reveal(6, 5, 5, 3) + mimic.awareness = Stealth.HUNTING + ok(not Nemesis.isEligible(mimic), "a revealed mimic already has its own escalation path") + + local shade = Graves.reveal(7, 5, 5, {}) + shade.awareness = Stealth.HUNTING + ok(not Nemesis.isEligible(shade), "a grave shade already has its own escalation path") + + local dead = Monsters.create(Monsters.byKey.rat, 8, 5, 5, 1) + dead.awareness, dead.hp = Stealth.HUNTING, 0 + ok(not Nemesis.isEligible(dead), "a dead monster cannot give chase") + + local already = Monsters.create(Monsters.byKey.rat, 9, 5, 5, 1) + already.awareness, already.nemesis = Stealth.HUNTING, true + ok(not Nemesis.isEligible(already), "an existing nemesis cannot spawn a second one") +end + +function tests.nemesisPickHunterReturnsTheNearestEligibleHunterBreakingTiesByListOrder() + local player = Entity.new({ x = 5, y = 5, glyph = "@", name = "you", kind = "player", hp = 20, attack = 5, defense = 1 }) + local level = arena(21, 11) + local far = Monsters.create(Monsters.byKey.orc, 1, 15, 5, 1) + far.awareness = Stealth.HUNTING + local dormant = Monsters.create(Monsters.byKey.orc, 2, 6, 5, 1) + local near = Monsters.create(Monsters.byKey.orc, 3, 7, 5, 1) + near.awareness = Stealth.HUNTING + level.entities = { far, dormant, near } + eq(Nemesis.pickHunter(level, player), near, "the nearest eligible hunter wins over a dormant closer monster and a farther one") + + local tie = Monsters.create(Monsters.byKey.orc, 4, 3, 5, 1) + tie.awareness = Stealth.HUNTING + level.entities = { near, tie } + eq(Nemesis.pickHunter(level, player), near, "an equidistant tie is broken by earlier list order") + + level.entities = { dormant } + eq(Nemesis.pickHunter(level, player), nil, "no eligible hunter on the floor returns nil") +end + +function tests.nemesisBeginFromCapturesSpeciesAndHpFractionAtGrudgeOne() + local orc = Monsters.create(Monsters.byKey.orc, 1, 5, 5, 1) + orc.hp = orc.maxHp // 2 + local pursuit = Nemesis.beginFrom(orc) + eq(pursuit.species, "orc", "the pursuit remembers which species is following") + eq(pursuit.grudge, 1, "a freshly begun pursuit starts at grudge one") + eq(pursuit.hpFraction, 0.5, "the pursuit captures the hunter's hp fraction at the moment it gave chase") +end + +function tests.nemesisEscalateRaisesGrudgeAndHealsTowardFullBothCapped() + local pursuit = { species = "orc", grudge = 1, hpFraction = 0.3 } end local order = {