test: cover tests.gravesEligibleForOnlyIncludesGravesAtOrDeeperThanTh...
This commit is contained in:
1 parent
91171be9e1
commit
8613a59377
1 file changed
+173
+173
@@ -8433,6 +8433,179 @@ function tests.gravesRecordAppendsNewestFirstAndTrimsToMaxRecorded()
|
|||||||
Graves.record(meta, { className = "fighter", depth = i, gold = i, essence = 0, turn = i })
|
Graves.record(meta, { className = "fighter", depth = i, gold = i, essence = 0, turn = i })
|
||||||
end
|
end
|
||||||
eq(#meta.graves, Graves.MAX_RECORDED, "recording past the cap trims the oldest graves off")
|
eq(#meta.graves, Graves.MAX_RECORDED, "recording past the cap trims the oldest graves off")
|
||||||
|
eq(meta.graves[1].depth, Graves.MAX_RECORDED + 5, "the most recently recorded grave sits at the front")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gravesEligibleForOnlyIncludesGravesAtOrDeeperThanTheQueriedDepth()
|
||||||
|
local graves = {
|
||||||
|
{ className = "a", depth = 2 },
|
||||||
|
{ className = "b", depth = 5 },
|
||||||
|
{ className = "c", depth = 5 },
|
||||||
|
}
|
||||||
|
local atFive = Graves.eligibleFor(graves, 5)
|
||||||
|
eq(#atFive, 2, "only the two graves dug at depth 5 or deeper are eligible for a depth-5 floor")
|
||||||
|
local atOne = Graves.eligibleFor(graves, 1)
|
||||||
|
eq(#atOne, 3, "every recorded grave is eligible for the entrance floor, however deep it was dug")
|
||||||
|
eq(#Graves.eligibleFor(nil, 3), 0, "a caller with no recorded graves at all gets an empty list, not an error")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gravesFloorItemBuildsAnInertGraveMarkerShape()
|
||||||
|
local grave = { className = "ranger", depth = 4, gold = 50, essence = 6, turn = 20 }
|
||||||
|
local item = Graves.floorItem(grave)
|
||||||
|
eq(item.kind, "grave", "a grave's floor item is its own kind, not a real item kind")
|
||||||
|
eq(item.grave, true, "a grave's floor item is tagged so pickupHere knows to wake it")
|
||||||
|
eq(item.graveClass, "ranger", "the marker carries the fallen's own class")
|
||||||
|
eq(item.graveGold, 50, "the marker carries the fallen's gold for Graves.reveal to read later")
|
||||||
|
eq(item.graveEssence, 6, "the marker carries the fallen's essence for Graves.reveal to read later")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gravesRevealScalesHpAndAttackWithCarriedWealthAndCaps()
|
||||||
|
local poor = Graves.reveal(1, 4, 6, { graveGold = 0, graveEssence = 0 })
|
||||||
|
eq(poor.hp, 10, "a penniless grave's shade has no hp bonus at all")
|
||||||
|
eq(poor.attack, 4, "a penniless grave's shade has no attack bonus at all")
|
||||||
|
eq(poor.faction, Factions.UNDEAD, "a shade is undead like every other restless dead in the bestiary")
|
||||||
|
|
||||||
|
local rich = Graves.reveal(2, 4, 6, { graveGold = 400, graveEssence = 200 })
|
||||||
|
eq(rich.hp, 30, "a rich grave's shade hp bonus caps at 20 rather than scaling without bound")
|
||||||
|
eq(rich.attack, 10, "a rich grave's shade attack bonus caps at 6 rather than scaling without bound")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gravesLootForReturnsHalfGoldAndAThirdEssenceRoundedDown()
|
||||||
|
local shade = Graves.reveal(1, 4, 6, { graveGold = 55, graveEssence = 10 })
|
||||||
|
local gold, essence = Graves.lootFor(shade)
|
||||||
|
eq(gold, 27, "grave loot pays out half the carried gold, rounded down")
|
||||||
|
eq(essence, 3, "grave loot pays out a third of the carried essence, rounded down")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.graveFieldsSurviveSerialization()
|
||||||
|
local shade = Graves.reveal(1, 4, 6, { graveClass = "ranger", graveGold = 88, graveEssence = 15 })
|
||||||
|
local restored = Entity.fromData(shade:toData())
|
||||||
|
eq(restored.grave, true, "a revealed shade's grave tag survives a save/load round trip")
|
||||||
|
eq(restored.graveClass, "ranger", "the fallen's class survives a save/load round trip")
|
||||||
|
eq(restored.graveGold, 88, "the grave's gold survives a save/load round trip")
|
||||||
|
eq(restored.graveEssence, 15, "the grave's essence survives a save/load round trip")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gamePickupHereWakesAGraveShadeAndAmbushesThePlayer()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 5, 5)
|
||||||
|
game.player.defense, game.player.baseDefense = 0, 0
|
||||||
|
local grave = { className = "rogue", depth = 3, gold = 0, essence = 0, turn = 10 }
|
||||||
|
game.level.floorItems = { { x = 5, y = 5, item = Graves.floorItem(grave) } }
|
||||||
|
local hpBefore = game.player.hp
|
||||||
|
game:pickupHere()
|
||||||
|
eq(#game.level.floorItems, 0, "the grave marker leaves the floor the instant it is disturbed")
|
||||||
|
eq(#game.level.entities, 1, "a disturbed grave joins the level as a real hostile monster")
|
||||||
|
eq(game.level.entities[1].grave, true, "the spawned monster is tagged as a grave shade")
|
||||||
|
eq(game.player.hp, hpBefore - 4, "the shade lands one free ambush hit for its full attack")
|
||||||
|
ok(contains(game.log:last(), "damage"), "the ambush hit is logged")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameKillingARevealedShadeGrantsItsFixedXpAndPaysOutGraveLoot()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local shade = Graves.reveal(1, 4, 6, { graveGold = 40, graveEssence = 9 })
|
||||||
|
shade.hp = 1
|
||||||
|
game.level.entities = { shade }
|
||||||
|
local essenceBefore, goldBefore = game.player.essence, game.player.gold
|
||||||
|
game:playerAttack(shade)
|
||||||
|
eq(game.player.essence, essenceBefore + Crafting.essenceFor(Graves.xpFor()) + 3,
|
||||||
|
"felling a shade grants essence scaled to its own fixed xp reward, plus a third of its grave essence")
|
||||||
|
eq(game.player.gold, goldBefore + 20, "felling a shade also pays out half its grave gold")
|
||||||
|
ok(contains(game.log:last(), "gold"), "the grave payout is logged")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gravesShadeLeavesNoCorpseLikeAnyOtherUndead()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local shade = Graves.reveal(1, 4, 6, { graveGold = 0, graveEssence = 0 })
|
||||||
|
shade.hp = 1
|
||||||
|
game.level.entities = { shade }
|
||||||
|
game:playerAttack(shade)
|
||||||
|
eq(#game.level.corpses, 0, "a felled shade leaves no corpse behind, same as any other undead")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameGraveEntryCapturesDepthGoldEssenceAndClassAtTheMomentOfDeath()
|
||||||
|
local game = Game.new(4, { class = "mage" })
|
||||||
|
game.deepestDepth = 6
|
||||||
|
game.player.gold = 77
|
||||||
|
game.player.essence = 14
|
||||||
|
game.turn = 120
|
||||||
|
local entry = game:graveEntry()
|
||||||
|
eq(entry.className, "mage", "the grave records the class the run actually played")
|
||||||
|
eq(entry.depth, 6, "the grave records the deepest depth reached, not just the current floor")
|
||||||
|
eq(entry.gold, 77, "the grave records exactly what gold was still on hand at death")
|
||||||
|
eq(entry.essence, 14, "the grave records exactly what essence was still on hand at death")
|
||||||
|
eq(entry.turn, 120, "the grave records the turn count at death")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameGraveEntryDefaultsToWandererForAClasslessRun()
|
||||||
|
local game = Game.new(4)
|
||||||
|
eq(game:graveEntry().className, "wanderer", "a run that never picked a class leaves a wanderer's grave")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.builderNeverPlacesAGraveWhenOptionsCarriesNone()
|
||||||
|
for seed = 1, 15 do
|
||||||
|
local level = Builder.build(5, Rng.new(seed), {})
|
||||||
|
for _, ground in ipairs(level.floorItems) do
|
||||||
|
ok(not ground.item.grave, "no grave appears without the caller supplying any")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.buildersOnlyRollAGraveOntoAFloorAtOrShallowerThanWhereItWasDug()
|
||||||
|
local shallowGrave = { className = "rogue", depth = 2, gold = 40, essence = 5, turn = 30 }
|
||||||
|
local found = false
|
||||||
|
for seed = 1, 40 do
|
||||||
|
local level = Builder.build(5, Rng.new(seed), { graves = { shallowGrave } })
|
||||||
|
for _, ground in ipairs(level.floorItems) do
|
||||||
|
if ground.item.grave then found = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ok(not found, "a grave dug at depth 2 never surfaces on a floor as deep as 5")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.buildersOccasionallyPlaceAnEligibleGraveFromSuppliedOptions()
|
||||||
|
local deepGrave = { className = "mage", depth = 6, gold = 80, essence = 12, turn = 90 }
|
||||||
|
local found
|
||||||
|
for seed = 1, 40 do
|
||||||
|
local level = Builder.build(3, Rng.new(seed), { graves = { deepGrave } })
|
||||||
|
for _, ground in ipairs(level.floorItems) do
|
||||||
|
if ground.item.grave then found = ground.item end
|
||||||
|
end
|
||||||
|
if found then break end
|
||||||
|
end
|
||||||
|
ok(found ~= nil, "a floor eventually rolls an eligible grave across a handful of seeds")
|
||||||
|
eq(found.graveClass, "mage", "the placed marker carries the fallen's own class")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.buildersInsertAGraveWithoutDisturbingAnyOtherPlacement()
|
||||||
|
local grave = { className = "fighter", depth = 8, gold = 60, essence = 9, turn = 50 }
|
||||||
|
local perturbed = false
|
||||||
|
local sawExtra = false
|
||||||
|
for seed = 1, 40 do
|
||||||
|
local plain = Builder.build(4, Rng.new(seed), {})
|
||||||
|
local withGrave = Builder.build(4, Rng.new(seed), { graves = { grave } })
|
||||||
|
if table.concat(plain.map.tiles) ~= table.concat(withGrave.map.tiles) then perturbed = true end
|
||||||
|
if #withGrave.entities ~= #plain.entities then perturbed = true end
|
||||||
|
if #withGrave.floorItems == #plain.floorItems + 1 then
|
||||||
|
sawExtra = true
|
||||||
|
local extra = withGrave.floorItems[#withGrave.floorItems]
|
||||||
|
ok(extra.item.grave, "the one extra floor item a supplied grave adds is the grave marker itself")
|
||||||
|
elseif #withGrave.floorItems ~= #plain.floorItems then
|
||||||
|
perturbed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ok(not perturbed, "supplying a grave never changes the map or any other placement, only adds at most its own marker")
|
||||||
|
ok(sawExtra, "across a handful of seeds, at least one actually places the grave marker")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- proficiency.lua: weapon mastery.
|
||||||
|
|
||||||
|
function tests.proficiencyKeyForReadsFistsWhenUnarmedAndTheWeaponsNameOnceEquipped()
|
||||||
|
eq(Proficiency.keyFor({}), Proficiency.UNARMED, "an entity with no weapon field masters bare fists")
|
||||||
|
eq(Proficiency.keyFor({ weapon = { name = "sword" } }), "sword", "an equipped weapon is keyed by its own name")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.proficiencyOnMeleeHitAccumulatesAndReturnsOnlyOnARankCrossing()
|
||||||
|
local entity = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user