From d16250b6f91ae8f49de2488c55566a8c116c49d7 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 19:18:42 +0200 Subject: [PATCH] test: cover tests.autopilotRepairsBrokenGearAutomaticallyWhenAffordable --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 61a848f..f36d937 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -9125,6 +9125,179 @@ function tests.gameRepairEquippedFailsAndLogsWhenGoldIsShort() local success = game:repairEquipped("weapon") eq(success, false, "an empty purse cannot repair") eq(game.player.weapon.durability, 0, "the weapon stays broken") + ok(contains(game.log:last(), "cannot repair"), "the failure is logged") +end + +function tests.autopilotRepairsBrokenGearAutomaticallyWhenAffordable() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipWeapon(Items.weapon("sword", 4), game.player) + game.player.weapon.durability = 0 + game.player.gold = 1000 + game.level.entities = {} + game:choosePlayerAction() + eq(game.player.weapon.durability, Durability.MAX, + "the autopilot repairs a broken weapon on sight once it can afford to") +end + +function tests.durabilityFieldsSurviveSerialization() + local game = Game.new(3) + Items.equipWeapon(Items.weapon("sword", 4), game.player) + game.player.weapon.durability = 7 + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.weapon.durability, 7, "a weapon's own durability survives a save/load round trip") + eq(restored.player.weapon.maxDurability, Durability.MAX, "a weapon's max durability survives a save/load round trip") +end + +function tests.renderPanelShowsWornGearOnlyOnceItHasTakenDamage() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipWeapon(Items.weapon("sword", 4), game.player) + ok(not contains(Render.panel(game), "sword ("), "a pristine weapon's label carries no wear indicator") + game.player.weapon.durability = 10 + ok(contains(Render.panel(game), "sword (10/30)"), "a dinged weapon's label reports its remaining durability") + game.player.weapon.durability = 0 + ok(contains(Render.panel(game), "sword (broken)"), "a broken weapon's label says so plainly") +end + +-- disarm.lua: passive trap sense and deliberate disarming. + +function tests.disarmSweepDetectsAHiddenTrapWithinOneStepEventuallyAcrossAHandfulOfSeeds() + local detected = false + for seed = 1, 100 do + local level = { traps = { { x = 5, y = 5, effect = "dart", revealed = false } } } + if #Disarm.sweep(level, Rng.new(seed), 5, 6) > 0 then detected = true break end + end + ok(detected, "an adjacent hidden trap is eventually detected across a handful of seeds") +end + +function tests.disarmSweepNeverDetectsATrapTwoStepsAway() + for seed = 1, 200 do + local level = { traps = { { x = 5, y = 5, effect = "dart", revealed = false } } } + eq(#Disarm.sweep(level, Rng.new(seed), 7, 7), 0, + "a trap two steps away is never detected regardless of the roll") + end +end + +function tests.disarmSweepNeverRedetectsAnAlreadyDetectedOrRevealedTrap() + local level = { traps = { + { x = 5, y = 5, effect = "dart", revealed = false, detected = true }, + { x = 5, y = 6, effect = "dart", revealed = true }, + } } + for seed = 1, 50 do + eq(#Disarm.sweep(level, Rng.new(seed), 5, 5), 0, + "neither an already-detected nor an already-sprung trap is swept again") + end +end + +function tests.disarmOddsForClimbWithLevelButNeverReachTheDenominator() + local num1, den1 = Disarm.oddsFor({ level = 1 }) + eq(num1, Disarm.DISARM_BASE_NUMERATOR, "a fresh level-one player rolls the catalog's base odds") + local num2, den2 = Disarm.oddsFor({ level = 100 }) + eq(den2, den1, "the denominator never changes with level") + ok(num2 < den2, "even an extremely high level never reaches a guaranteed disarm") + eq(num2, Disarm.DISARM_BASE_DENOMINATOR - 1, "the numerator caps a single point shy of the denominator") +end + +function tests.disarmAttemptOnSuccessRemovesTheTrapAndPaysSalvage() + local succeeded = false + for seed = 1, 300 do + local game = stagedGame(seed, arena(21, 11), 3, 6) + local trap = { x = 4, y = 6, effect = "dart", revealed = false, detected = true } + game.level.traps = { trap } + local essenceBefore = game.player.essence + local success, line = Disarm.attempt(game, trap) + if success then + eq(#game.level.traps, 0, "a successfully disarmed trap is removed from the level") + ok(game.player.essence > essenceBefore, "a successful disarm pays out salvage essence") + ok(contains(line, "disarm"), "the success is reported") + succeeded = true + break + end + end + ok(succeeded, "disarming eventually succeeds across a handful of seeds") +end + +function tests.disarmAttemptOnFailureSpringsTheTrapOnThePlayer() + local failed = false + for seed = 1, 300 do + local game = stagedGame(seed, arena(21, 11), 3, 6) + local trap = { x = 4, y = 6, effect = "dart", revealed = false, detected = true } + game.level.traps = { trap } + local hpBefore = game.player.hp + local success = Disarm.attempt(game, trap) + if not success then + eq(trap.revealed, true, "a failed disarm springs the trap, revealing it") + ok(game.player.hp < hpBefore, "a failed disarm's spring wounds the player exactly like a footstep would") + failed = true + break + end + end + ok(failed, "disarming eventually fails across a handful of seeds") +end + +function tests.gameDisarmTrapRefusesAnUndetectedOrAlreadySprungTrap() + local game = stagedGame(1, arena(21, 11), 3, 6) + local undetected = { x = 4, y = 6, effect = "dart", revealed = false } + game.level.traps = { undetected } + eq(game:disarmTrap(undetected), false, "an undetected trap refuses disarming outright") + eq(#game.level.traps, 1, "the untouched trap is still there") + + local sprung = { x = 5, y = 6, effect = "dart", revealed = true, detected = true } + eq(game:disarmTrap(sprung), false, "an already-sprung trap refuses disarming outright") +end + +function tests.gameDisarmTrapSucceedsOrSpringsExactlyLikeDisarmAttempt() + local sawSuccess, sawFailure = false, false + for seed = 1, 300 do + if sawSuccess and sawFailure then break end + local game = stagedGame(seed, arena(21, 11), 3, 6) + local trap = { x = 4, y = 6, effect = "dart", revealed = false, detected = true } + game.level.traps = { trap } + local hpBefore = game.player.hp + if game:disarmTrap(trap) then + eq(#game.level.traps, 0, "a successful disarm through the game wrapper removes the trap") + sawSuccess = true + else + eq(trap.revealed, true, "a failed disarm through the game wrapper springs the trap") + ok(game.player.hp <= hpBefore, "a failed disarm through the game wrapper can wound the player") + sawFailure = true + end + end + ok(sawSuccess, "the game wrapper eventually succeeds across a handful of seeds") + ok(sawFailure, "the game wrapper eventually fails across a handful of seeds") +end + +function tests.gameEndPlayerTurnEventuallyDetectsAnAdjacentTrapAcrossAHandfulOfSeeds() + local noticed = false + for seed = 1, 100 do + local game = stagedGame(1, arena(21, 11), 3, 6) + game.rng = Rng.new(seed) + game.level.traps = { { x = 4, y = 6, effect = "dart", revealed = false } } + game:endPlayerTurn() + if game.level.traps[1].detected then + local reported = false + for _, line in ipairs(game.log:recent(6)) do + if contains(line, "notice") then reported = true end + end + ok(reported, "a freshly detected trap is reported somewhere in the turn's log lines") + noticed = true + break + end + end + ok(noticed, "an adjacent hidden trap is eventually detected during a normal turn across a handful of seeds") +end + +function tests.trapDetectedFieldSurvivesSerialization() + local game = Game.new(3) + game.level.traps = { { x = 1, y = 1, effect = "dart", revealed = false, detected = true } } + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.level.traps[1].detected, true, "a trap's detected flag survives a save/load round trip") +end end local order = {