test: cover tests.gameSpringTrapConfuseEffectAppliesToAnyVictimNotJus...

This commit is contained in:
dualfroz committed 2026-09-08 17:13:56 +02:00
1 parent 9601bf2949
commit 62e694d522
1 file changed
+150
+150
View File
@@ -785,6 +785,137 @@ function tests.traps()
poisonGame.level.traps = { { x = 4, y = 6, effect = "poison", revealed = false } } poisonGame.level.traps = { { x = 4, y = 6, effect = "poison", revealed = false } }
poisonGame:movePlayer(1, 0) poisonGame:movePlayer(1, 0)
eq(Status.has(poisonGame.player, Status.POISON), true, "a poison trap poisons the stepper") eq(Status.has(poisonGame.player, Status.POISON), true, "a poison trap poisons the stepper")
local confuseGame = stagedGame(1, arena(21, 11), 3, 6)
confuseGame.level.traps = { { x = 4, y = 6, effect = "confuse", revealed = false } }
confuseGame:movePlayer(1, 0)
eq(Status.has(confuseGame.player, Status.CONFUSE), true, "a confuse trap confuses the stepper")
eq(confuseGame.level.traps[1].revealed, true, "a sprung confuse trap becomes revealed too")
local blindGame = stagedGame(1, arena(21, 11), 3, 6)
blindGame.level.traps = { { x = 4, y = 6, effect = "blind", revealed = false } }
blindGame:movePlayer(1, 0)
eq(Status.has(blindGame.player, Status.BLIND), true, "a blind trap blinds the stepper")
eq(blindGame.level.traps[1].revealed, true, "a sprung blind trap becomes revealed too")
eq(blindGame:effectiveRadius(), 1, "the freshly blinded stepper's own sight radius is clamped straight away")
local stunGame = stagedGame(1, arena(21, 11), 3, 6)
stunGame.level.traps = { { x = 4, y = 6, effect = "stun", revealed = false } }
stunGame:movePlayer(1, 0)
eq(Status.has(stunGame.player, Status.STUN), true, "a stun trap stuns the stepper")
eq(stunGame.level.traps[1].revealed, true, "a sprung stun trap becomes revealed too")
end
-- Game:springTrap is written to work against whatever `victim` it is handed,
-- not just self.player - the same generic shape a builder-placed trap needs
-- to also harm a monster that blunders into a player-planted trap kit.
function tests.gameSpringTrapConfuseEffectAppliesToAnyVictimNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 10, 6, 1)
game.level.entities = { rat }
local trap = { x = 10, y = 6, effect = "confuse", revealed = false }
game:springTrap(trap, rat)
eq(Status.has(rat, Status.CONFUSE), true, "a confuse trap sprung on a monster confuses that monster")
eq(Status.has(game.player, Status.CONFUSE), false, "the player is untouched by a trap sprung on someone else")
end
function tests.gameSpringTrapBlindEffectAppliesToAnyVictimNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 10, 6, 1)
game.level.entities = { rat }
local trap = { x = 10, y = 6, effect = "blind", revealed = false }
game:springTrap(trap, rat)
eq(Status.has(rat, Status.BLIND), true, "a blind trap sprung on a monster blinds that monster")
eq(Status.has(game.player, Status.BLIND), false, "the player is untouched by a trap sprung on someone else")
end
-- STUN previously had the opposite gap from BLIND (see abilities.lua's
-- tryShriek comment): a player's crafted weapon could already stun a
-- monster, but nothing on the environment/monster side could ever stun the
-- player. A trap is a fitting, low-risk source for that missing direction -
-- rare and avoidable/disarmable, unlike say an elite's on-hit rider firing
-- on every single blow it lands.
function tests.gameSpringTrapStunEffectAppliesToAnyVictimNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 10, 6, 1)
game.level.entities = { rat }
local trap = { x = 10, y = 6, effect = "stun", revealed = false }
game:springTrap(trap, rat)
eq(Status.has(rat, Status.STUN), true, "a stun trap sprung on a monster stuns that monster")
eq(Status.has(game.player, Status.STUN), false, "the player is untouched by a trap sprung on someone else")
end
-- A confused player's every attempted move rolls the same override chance a
-- confused monster's Ai.act does (status.lua's shared CONFUSE_NUMERATOR/
-- DENOMINATOR): in the middle of an open arena every one of the 8 compass
-- directions leads to open floor, so either outcome always still moves the
-- player somewhere and always spends the turn.
function tests.gameMovePlayerConfusionEitherFollowsTheChosenDirectionOrStumblesElsewhere()
local sawStraight, sawStumble = false, false
for seed = 1, 200 do
local game = stagedGame(seed, arena(21, 11), 10, 6)
Status.apply(game.player, Status.CONFUSE, 5, 1)
local consumesTurn = game:movePlayer(1, 0)
ok(consumesTurn, "a confused player's attempted move always spends the turn on open floor")
if game.player.x == 11 and game.player.y == 6 then
sawStraight = true
else
sawStumble = true
end
if sawStraight and sawStumble then break end
end
ok(sawStraight, "confusion sometimes lets the chosen direction through untouched")
ok(sawStumble, "confusion sometimes overrides the chosen direction")
end
-- The turn-spending contract that makes confusion different from an ordinary
-- rejected move: bumping a wall on purpose spends nothing (see
-- tests.doorsAndKeys's locked-door case and moveHandler's own "you cannot go
-- that way"), but a confused player who stumbles into a wall instead of their
-- own chosen direction still spent the turn on the stumble itself.
function tests.gameMovePlayerConfusionStumbleIntoAWallStillSpendsTheTurn()
local level = arena(21, 11)
-- Wall off every neighbour of (10, 6) except due east, so any redirect
-- other than the player's own chosen direction bumps a wall in place.
for _, cell in ipairs({ { 9, 5 }, { 10, 5 }, { 11, 5 }, { 9, 6 }, { 9, 7 }, { 10, 7 }, { 11, 7 } }) do
level.map:set(cell[1], cell[2], Tiles.WALL)
end
local sawBlockedStumble = false
for seed = 1, 500 do
local game = stagedGame(seed, level, 10, 6)
Status.apply(game.player, Status.CONFUSE, 5, 1)
local consumesTurn = game:movePlayer(1, 0)
ok(consumesTurn, "a confused move, straight or stumbled into this walled-off pocket, always spends the turn")
if game.player.x == 10 and game.player.y == 6 then
sawBlockedStumble = true
end
end
ok(sawBlockedStumble, "at least one seed in range stumbled into a wall instead of moving east")
end
-- Without Status.CONFUSE present at all, Game:movePlayer never consults the
-- rng for a redirect - the roll/pick machinery added for confusion must be a
-- true no-op for every unconfused move the engine already relied on.
function tests.gameMovePlayerWithoutConfusionNeverStumbles()
for seed = 1, 20 do
local game = stagedGame(seed, arena(21, 11), 10, 6)
game:movePlayer(1, 0)
eq(game.player.x, 11, "an unconfused move always lands exactly where it was aimed")
eq(game.player.y, 6, "an unconfused move never drifts off its own row")
end
end
-- Game:allyActsConfused is the companion/hireling/thrall counterpart of the
-- confusion machinery above - unlike Game:movePlayer and Ai.act, it is never
-- called at all unless a caller opts in, so an entity without Status.CONFUSE
-- must bail out before it ever touches the rng (companionsAct*/mercenaryAct*/
-- reanimationAct* below cover the roll actually firing).
function tests.gameAllyActsConfusedReturnsFalseImmediatelyWithoutTheStatus()
local game = stagedGame(1, arena(21, 11), 5, 5)
local golem = Companions.summon(game, "golem")
local rollsBefore = game.rng.state
eq(game:allyActsConfused(golem), false, "an ally without Status.CONFUSE never triggers a stumble")
eq(game.rng.state, rollsBefore, "checking an unconfused ally never advances the rng")
end end
function tests.lightAndDark() function tests.lightAndDark()
@@ -816,6 +947,19 @@ function tests.identification()
eq(Identify.label(a, healing), "potion of healing", "identifying reveals the true name") eq(Identify.label(a, healing), "potion of healing", "identifying reveals the true name")
end end
function tests.identifyGivesEveryPotionTypeItsOwnDistinctUnidentifiedLook()
local state = Identify.new(Rng.new(3))
local seen = {}
for _, entry in ipairs(Items.potionTypes) do
local label = Identify.label(state, Items.potionOf(entry.effect))
ok(seen[label] == nil, "no two potion types share an unidentified look (collided on " .. label .. ")")
seen[label] = true
end
local count = 0
for _ in pairs(seen) do count = count + 1 end
eq(count, #Items.potionTypes, "every potion type, including confuse, got its own look")
end
function tests.effects() function tests.effects()
local game = Game.new(31) local game = Game.new(31)
game.player.hp = 10 game.player.hp = 10
@@ -824,6 +968,12 @@ function tests.effects()
Effects.apply("poison", game, game.player, { power = 3 }) Effects.apply("poison", game, game.player, { power = 3 })
eq(Status.has(game.player, Status.POISON), true, "poison effect afflicts the target") eq(Status.has(game.player, Status.POISON), true, "poison effect afflicts the target")
Effects.apply("confuse", game, game.player, {})
eq(Status.has(game.player, Status.CONFUSE), true, "confuse effect afflicts the drinker just like poison")
Effects.apply("blind", game, game.player, {})
eq(Status.has(game.player, Status.BLIND), true, "blind effect afflicts the drinker just like confuse")
Effects.apply("mapping", game, game.player, {}) Effects.apply("mapping", game, game.player, {})
local map = game.level.map local map = game.level.map
local exploredAll = true local exploredAll = true