test: cover tests.streaksCurrentIsZeroAndNilOnAnEmptyHistory
This commit is contained in:
1 parent
fac05c737f
commit
8e7c57dcbf
1 file changed
+173
+173
@@ -10163,6 +10163,179 @@ function tests.exportCompendiumCsvListsEverySpeciesSeenOrNot()
|
|||||||
ok(contains(csv, "false,0"), "an undiscovered species still reports as unseen with a zero tally")
|
ok(contains(csv, "false,0"), "an undiscovered species still reports as unseen with a zero tally")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user