test: cover tests.leaderboardRecordKeepsTheBoardSortedBestScoreFirst
This commit is contained in:
1 parent
b0648df4e0
commit
f68d0299e3
1 file changed
+173
+173
@@ -9817,6 +9817,179 @@ function tests.leaderboardRecordOnAnEmptyBoardLandsAtRankOne()
|
|||||||
eq(#board, 1, "the board now holds exactly one entry")
|
eq(#board, 1, "the board now holds exactly one entry")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardRecordKeepsTheBoardSortedBestScoreFirst()
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
Leaderboard.record(board, { score = 50, tag = "a" })
|
||||||
|
Leaderboard.record(board, { score = 90, tag = "b" })
|
||||||
|
Leaderboard.record(board, { score = 70, tag = "c" })
|
||||||
|
eq(board[1].tag, "b", "the highest score sorts to the very top")
|
||||||
|
eq(board[2].tag, "c", "the middling score lands second")
|
||||||
|
eq(board[3].tag, "a", "the lowest score lands last")
|
||||||
|
local rank = Leaderboard.record(board, { score = 80, tag = "d" })
|
||||||
|
eq(rank, 2, "a fresh entry ranks by where it actually lands, not where it was inserted")
|
||||||
|
eq(board[2].tag, "d", "the new entry displaces the weaker one down a slot")
|
||||||
|
eq(board[3].tag, "c", "everything below the insertion point shifts down by one")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardRecordDropsAnEntryThatDoesNotCrackTheCap()
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
for i = 1, Leaderboard.CAPACITY do
|
||||||
|
Leaderboard.record(board, { score = 1000 - i })
|
||||||
|
end
|
||||||
|
eq(#board, Leaderboard.CAPACITY, "the board fills to exactly its capacity")
|
||||||
|
local rank = Leaderboard.record(board, { score = 1 })
|
||||||
|
eq(rank, nil, "a score worse than everything already on the board does not crack it")
|
||||||
|
eq(#board, Leaderboard.CAPACITY, "the board stays at capacity rather than growing")
|
||||||
|
eq(board[Leaderboard.CAPACITY].score, 1000 - Leaderboard.CAPACITY, "the weakest surviving entry is unchanged")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardRecordBumpsTheWeakestEntryOffOnceFull()
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
for i = 1, Leaderboard.CAPACITY do
|
||||||
|
Leaderboard.record(board, { score = 1000 - i })
|
||||||
|
end
|
||||||
|
local worst = board[Leaderboard.CAPACITY].score
|
||||||
|
local rank = Leaderboard.record(board, { score = 1500 })
|
||||||
|
ok(rank ~= nil, "a score that beats the current worst entry cracks the board")
|
||||||
|
eq(#board, Leaderboard.CAPACITY, "the board stays at capacity after bumping someone off")
|
||||||
|
for _, entry in ipairs(board) do
|
||||||
|
ok(entry.score ~= worst, "the previous weakest entry no longer appears on the board")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardTopReturnsAtMostCountEntriesInOrder()
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
Leaderboard.record(board, { score = 30 })
|
||||||
|
Leaderboard.record(board, { score = 10 })
|
||||||
|
Leaderboard.record(board, { score = 20 })
|
||||||
|
local top2 = Leaderboard.top(board, 2)
|
||||||
|
eq(#top2, 2, "top(2) returns exactly two entries")
|
||||||
|
eq(top2[1].score, 30, "the first slot is the best score")
|
||||||
|
eq(top2[2].score, 20, "the second slot is the next best score")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardTopDefaultsToTheFullCapacityWhenNoCountIsGiven()
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
for i = 1, Leaderboard.CAPACITY + 3 do
|
||||||
|
Leaderboard.record(board, { score = i })
|
||||||
|
end
|
||||||
|
local top = Leaderboard.top(board)
|
||||||
|
eq(#top, Leaderboard.CAPACITY, "top() with no count returns the board's full capacity, not everything ever recorded")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.leaderboardBestScoreReadsTheTopEntryOrZeroWhenEmpty()
|
||||||
|
eq(Leaderboard.bestScore(Leaderboard.new()), 0, "an empty board's best score is zero")
|
||||||
|
local board = Leaderboard.new()
|
||||||
|
Leaderboard.record(board, { score = 42 })
|
||||||
|
eq(Leaderboard.bestScore(board), 42, "the best score matches the single recorded entry")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.metaLeaderboardFieldDefaultsToEmptyAndSurvivesASaveLoadRoundTrip()
|
||||||
|
local fresh = Meta.load("/tmp/warren_meta_does_not_exist_55511.sav")
|
||||||
|
eq(#fresh.leaderboard, 0, "a freshly defaulted meta state has an empty leaderboard")
|
||||||
|
local meta = Meta.new()
|
||||||
|
Leaderboard.record(meta.leaderboard, { score = 250, className = "mage", depth = 4, turn = 88, won = false, seed = 7 })
|
||||||
|
local path = os.tmpname()
|
||||||
|
Meta.save(meta, path)
|
||||||
|
local restored = Meta.load(path)
|
||||||
|
os.remove(path)
|
||||||
|
eq(#restored.leaderboard, 1, "a recorded leaderboard entry survives a save/load round trip")
|
||||||
|
eq(restored.leaderboard[1].score, 250, "a restored entry keeps its own score")
|
||||||
|
eq(restored.leaderboard[1].className, "mage", "a restored entry keeps its own class")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.replayRunIsDeterministicAndMatchesAFreshGamesOwnReport()
|
||||||
|
local seed, steps = 55, 60
|
||||||
|
local game = Game.new(seed)
|
||||||
|
for _ = 1, steps do
|
||||||
|
if not game.player:isAlive() then break end
|
||||||
|
game:autoStep()
|
||||||
|
end
|
||||||
|
local expected = Morgue.build(game)
|
||||||
|
local replayed = Replay.run(seed, nil, steps)
|
||||||
|
local matches, mismatchField = Replay.matches(expected, replayed)
|
||||||
|
ok(matches, "replaying the same seed and step count reproduces every compared field (first mismatch: "
|
||||||
|
.. tostring(mismatchField) .. ")")
|
||||||
|
eq(replayed.turn, expected.turn, "the replay reaches the exact same turn")
|
||||||
|
eq(replayed.deepestDepth, expected.deepestDepth, "the replay reaches the exact same depth")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.replayVerifyConfirmsAGenuineReportAndReturnsItsScore()
|
||||||
|
local seed, steps = 55, 60
|
||||||
|
local game = Game.new(seed)
|
||||||
|
for _ = 1, steps do
|
||||||
|
if not game.player:isAlive() then break end
|
||||||
|
game:autoStep()
|
||||||
|
end
|
||||||
|
local report = Morgue.build(game)
|
||||||
|
local matches, mismatchField, score = Replay.verify(seed, nil, steps, report)
|
||||||
|
ok(matches, "verify confirms a report that genuinely came from this seed and step count")
|
||||||
|
eq(mismatchField, nil, "a matching verify names no mismatched field")
|
||||||
|
eq(score, Morgue.score(report), "verify's returned score matches the report's own score")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.replayVerifyFlagsATamperedReportAndStillReturnsTheRealScore()
|
||||||
|
local seed, steps = 55, 60
|
||||||
|
local game = Game.new(seed)
|
||||||
|
for _ = 1, steps do
|
||||||
|
if not game.player:isAlive() then break end
|
||||||
|
game:autoStep()
|
||||||
|
end
|
||||||
|
local report = Morgue.build(game)
|
||||||
|
local tampered = {}
|
||||||
|
for k, v in pairs(report) do tampered[k] = v end
|
||||||
|
tampered.gold = report.gold + 9999
|
||||||
|
local matches, mismatchField, score = Replay.verify(seed, nil, steps, tampered)
|
||||||
|
eq(matches, false, "a tampered gold total fails verification")
|
||||||
|
eq(mismatchField, "gold", "verify names gold as the field that diverged")
|
||||||
|
eq(score, Morgue.score(report), "the replay still reports the real, untampered score")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.replayComparedFieldsCoversEveryFieldMorgueScoreActuallyReads()
|
||||||
|
local weighed = {
|
||||||
|
"deepestDepth", "kills", "bossKills", "questsCompleted", "artifactsFound",
|
||||||
|
"vaultsOpened", "setPiecesFound", "chestsOpened", "playerLevel", "gold", "won",
|
||||||
|
}
|
||||||
|
for _, field in ipairs(weighed) do
|
||||||
|
local found = false
|
||||||
|
for _, compared in ipairs(Replay.COMPARED_FIELDS) do
|
||||||
|
if compared == field then found = true end
|
||||||
|
end
|
||||||
|
ok(found, "Replay.COMPARED_FIELDS includes '" .. field .. "', every field Morgue.score weighs")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.replayMatchesReportsTheFirstDivergingFieldInComparedFieldsOrder()
|
||||||
|
local base = {
|
||||||
|
won = false, alive = true, deepestDepth = 3, turn = 100, gold = 10, essence = 2,
|
||||||
|
kills = 1, bossKills = 0, questsCompleted = 0, artifactsFound = 0,
|
||||||
|
vaultsOpened = 0, setPiecesFound = 0, chestsOpened = 0, playerLevel = 1,
|
||||||
|
}
|
||||||
|
local altered = {}
|
||||||
|
for k, v in pairs(base) do altered[k] = v end
|
||||||
|
altered.turn = 999
|
||||||
|
local matches, field = Replay.matches(base, altered)
|
||||||
|
eq(matches, false, "a divergent turn count fails the match")
|
||||||
|
eq(field, "turn", "the mismatch is reported against the turn field specifically")
|
||||||
|
eq(Replay.matches(base, base), true, "an identical report matches itself")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.challengesForDaySameDayAlwaysReturnsTheSamePresetAndSeed()
|
||||||
|
local presetA, seedA = Challenges.forDay(7)
|
||||||
|
local presetB, seedB = Challenges.forDay(7)
|
||||||
|
eq(presetA, presetB, "the same day index always names the identical preset table")
|
||||||
|
eq(seedA, seedB, "the same day index always yields the identical seed")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.challengesForDayCyclesThroughTheCatalogInOrder()
|
||||||
|
for i = 0, #Challenges.CATALOG - 1 do
|
||||||
|
local preset = Challenges.forDay(i)
|
||||||
|
eq(preset, Challenges.CATALOG[i + 1], "day " .. i .. " maps to catalog slot " .. (i + 1))
|
||||||
|
end
|
||||||
|
local wrapped = Challenges.forDay(#Challenges.CATALOG)
|
||||||
|
eq(wrapped, Challenges.CATALOG[1], "the cycle wraps back to the first preset after one full lap")
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user