Unit test coverage: traps (all 8 trapHandlers entries untested) #14
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The
trapHandlersdispatch table has 8 entries and zero of them aredirectly tested. Trap effects live in
game/move.go:332-441.Traps are one of the few subsystems that can kill the player outright
(teleport into a monster pack, dart poison, trapdoor fall), so a divergence
here is both easy to introduce and hard to notice.
For comparison, the other effect tables all have at least token coverage:
quaffHandlersreadHandlerszapHandlershitHandlersdaemonHandlerstrapHandlersThis unit was not part of the
TODO.md"rings, sticks, wizard commands" NextStep — it is an additional gap found while surveying, and it is arguably more
consequential than the wizard commands.
Definition of done
game/traps_test.go(or additions to an existing move/trap testfile) covering all 8 trap kinds, each asserting its characteristic
effect:
springTrapdiscovery behavior: a previously-hidden trap becomes visibleand is recorded on the level.
os.Exit, which would kill the test binary. Use the existingfortify()-style HP-pinning pattern from the playtest tests.make checkfully green.TODO.mdupdated in the same commit.(closes #N).Implementation requirements
(
git show origin/c-master:move.c, thebe_trappedswitch). Do NOT checkout or modify
origin/c-master. Do not write tests that merely restatecurrent Go behavior — the point is catching divergence from C.
in a test-coverage commit, and do not encode the bug as expected behavior.
t.Parallel()and the//nolint:testpackageheader.maketargets only. Do NOT modify.golangci.yml. No game behavior changes.Implementation plan
Branch
test/traps-coverageoffmain@bf820e3. New white-boxgame/traps_test.go; no game code touched.First: a correction to the issue body's trap list
The definition-of-done paraphrases the trap set as "trapdoor, teleport,
dart, sleeping gas, bear trap, arrow, poison dart, rust". Read against
origin/c-masterthat is not the set.rogue.h192-200 defines exactlyeight:
There is no separate "poison dart" trap —
T_DARTis the poison dart(its death message is "a poisoned dart killed you") — and the list omits
T_MYST, the mystery trap, whosebe_trappedarm is an eleven-wayrnd(11)message switch. The GoTrapKindiota (game/types.go202-209) matches C index-for-index, and
trapHandlers(
game/tables.go835-844) is keyed by it. So the work coversT_MYSTin place of the phantom second dart. Flagging it here rather than
silently reinterpreting the issue.
What gets asserted, per C
move.c be_trappedPrologue, before the switch, tested on its own:
on(player, ISLEVIT)returns
T_RUSTwith no side effects; otherwiserunning = FALSE,count = FALSE,pp->p_ch = TRAP,pp->p_flags |= F_SEEN— thatlast pair is the discovery behaviour in item 2 — and the return value is
the trap kind.
T_DOOR—level++,new_level(), "you fell into a trap!". Depthincremented and the map actually regenerated (snapshot of
Level.Places+Stairsbefore/after).T_BEAR—no_move += BEARTIME, "you are caught in a bear trap".rogue.h108 is#define BEARTIME spread(3), so the delta is checkedby replaying
spread(3)from a snapshotted generator state, not byaccepting a range.
T_SLEEP—no_command += SLEEPTIME(spread(5), same replay),player.t_flags &= ~ISRUN(GoAwake, same bit0o020000), and themist message.
T_ARROW— both arms ofswing(pstats.s_lvl - 1, pstats.s_arm, 1),forced deterministically by picking level/AC so the roll cannot
matter. Hit: HP down by a replayed
roll(1,6), "oh no! An arrow shotyou". Miss: an
ARROWobject exists on the floor at the hero, witho_count == 1, and "an arrow shoots past you".T_TELEP— hero relocated to a walkable square, and the trap glyphwritten to the old square via
mvaddch(the comment in C is thepoint of the case), plus
teleport()'s ownno_move = 0.T_DART— both arms ofswing(pstats.s_lvl + 1, ...). Miss: "a smalldart whizzes by your ear and vanishes" and no HP change. Hit: HP down
by a replayed
roll(1,4), "a small dart just hit you in theshoulder", and the
!ISWEARING(R_SUSTSTR) && !save(VS_POISON)gate on
chg_str(-1)— including a sustain-strength ring pinningstrength.
T_RUST— "a gush of water hits you on the head", thenrust_armor(cur_armor): with no armor the gush is the last messageand nothing changes; with rustable armor
o_arm++.T_MYST— the wholernd(11)switch, checked by snapshotting thegenerator, running the trap, and recomputing C's expected string from
the identical state (including the
rainbow[rnd(cNCOLORS)]seconddraw in cases 1, 4, 6 and 10). That pins the message set, the case
numbering, the
rnd(11)bound, and the exact number of RNG draws eacharm spends — which matters, because a stray draw desynchronises the
seed-compatible stream.
Plus a table-completeness check that all eight
TrapKindvalues have ahandler, so a new C trap arm cannot be silently dropped.
Practicalities
Message text is compared against constants transcribed from C, via
Msgs.Huh. Death paths (death('a'),death('d')) reachmyExit→os.Exit, so HP is pinned high in thefortify()style; the two deathmessages are the one thing left unassertable in-process and will be
called out as such. Fixed seed throughout,
t.Parallel(), the approved//nolint:testpackageheader, scripted input carries an abort tail(space then ESCAPE) so a reprompt loop fails fast instead of hanging on
the headless terminal's filler input.
Every handler will be mutated away one at a time to prove the tests are
non-vacuous, and the results reported.
TODO.mdgets a Completed Stepsentry in the same commit;
Next Stepis not rotated, since this isout-of-band issue work.
Done in #37 (branch
test/traps-coverage, rebased ontomainatc0741ad).New
game/traps_test.go: 17 tests, 13 subtests, covering all eight armsof
move.c be_trapped, the prologue every trap runs through(
ISLEVITearly return;running/countreset;p_ch = TRAPandp_flags |= F_SEEN, which is item 2 of the definition of done), and therust_armortailT_RUSTcalls. No game code changed. Every expectedvalue is transcribed from
origin/c-masterand quoted in the file.No divergence from C was found.
One correction to the issue body, raised before writing any code and
repeated here for the record: the eight-trap list in the definition of
done is wrong.
rogue.h192-200 hasT_DOOR,T_ARROW,T_SLEEP,T_BEAR,T_TELEP,T_DART,T_RUST,T_MYST. There is no separate"poison dart" trap —
T_DARTis the poisoned dart — and the listomitted
T_MYST, the mystery trap, whose arm is an eleven-wayrnd(11)message switch.
T_MYSTis covered in place of the phantom second dart.The per-trap table of C effect and message against what is asserted, the
full mutation log, and the uncovered items are all in the PR body. The
three things worth surfacing here:
strengthened until they were. Deleting
new_level()fromT_DOORleftthe suite green, because
be_trapped's own prologue stamps the trapglyph into the cell the hero fell through, so "the map changed" is true
with no new level dug; the test now counts differing cells. And
roll(1,6)->roll(1,5)on the arrow left it green, becausernd(n)is "raw value % n" — one draw agrees five times in six and leaves the
generator identical — so the damage checks are sweeps now. 31 mutations
total, each reverted; 29 failed their own test and only their own, all
fast rather than by timeout.
you" and "a poisoned dart killed you" are each printed immediately
before
death(), which reachesmyExitandos.Exit, so provokingeither would take the test binary with it. The hero is pinned with
fortify()and the damage is checked by replaying C's arithmeticrather than by letting HP reach zero.
T_TELEP'smvaddch(tc, TRAP)is not isolable, and that is aproperty of C rather than a port defect. Its comment claims
look()will not redraw the vacated square, but the prologue has already set
p_chtoTRAPandteleport()opens by drawingfloor_at()— whichreturns
chat(hero)— over the departing square, whileleave_roomonly blanks squares showing
FLOOR. The glyph is there before the lineruns. Stated in the test rather than papered over.
Verification:
make checkfully green after the rebase, withGOFLAGS=-count=1so the run is not served from cache(
ok git.eeqj.de/sneak/rgoue/game 2.326s);golangci-lint0 issues on aprivate empty cache with no lock collision and no path outside the
worktree;
.golangci.ymlsha256 unchanged; nothing undergame/testdata/touched andTestSeedCompatItemTablesgreen. Packagecoverage 56.2% -> 57.9% measured on the branch point
bf820e3.