Unit-test the eight trap effects against the C reference (closes #14)
`trapHandlers` had eight entries and zero direct tests, on the one subsystem besides combat that can kill the hero outright. New `game/traps_test.go` covers all eight arms of `move.c be_trapped`, the prologue every trap runs through, and the `rust_armor` tail `T_RUST` calls. Test-only: no game code changes. Every expected value is transcribed from `origin/c-master` (`move.c`, `misc.c`, `fight.c`, `monsters.c`, `rogue.h`) and quoted in the file. No divergence from C was found. The trap set is `rogue.h` 192-200: there is no separate "poison dart" kind — `T_DART` is the poisoned dart — and `T_MYST`, the eleven-way `rnd(11)` message switch, is the eighth. Details the tests are built around: `BEARTIME`/`SLEEPTIME` are `spread(3)`/`spread(5)`, both of which reduce to `rnd(0)` and so cost no random number, which is asserted as well as their values; `T_ARROW` swings at `s_lvl - 1` and `T_DART` at `s_lvl + 1`; and the strength loss is gated on `!ISWEARING(R_SUSTSTR) && !save(VS_POISON)`, whose short circuit means the ring saves a random draw as well as the strength. Damage dice and swing arguments are checked by sweeps rather than single shots: `rnd(n)` is "raw value % n", so one draw cannot separate a d6 from a d5, and a forced hit or miss cannot see a wrong `at_lvl`. Both shapes were forced by mutation runs that the single-shot versions survived. The two death messages are deliberately uncovered: each is printed immediately before `death()`, which reaches `myExit` and `os.Exit`, so provoking either would kill the test binary. The hero is pinned with `fortify()` and the damage is checked by replaying C's arithmetic.
This commit is contained in:
67
TODO.md
67
TODO.md
@@ -70,6 +70,73 @@ of Completed Steps.
|
||||
reach only mean something against known walls and a known passage number. All
|
||||
27 mutations tried against the new tests were caught.
|
||||
|
||||
- 2026-08-09 Trap unit-test coverage (`test/traps-coverage`, closes #14):
|
||||
`trapHandlers` had eight entries and **zero** direct tests, on the one
|
||||
subsystem besides combat that can kill the hero outright. New
|
||||
`game/traps_test.go` (17 tests, 13 subtests) covers all eight arms of
|
||||
`move.c be_trapped`, the prologue every trap runs through, and the
|
||||
`rust_armor` tail `T_RUST` calls. Package coverage 56.2% -> 57.9% measured on
|
||||
`main` at `bf820e3`, the branch point, before the sticks tests landed. Every
|
||||
expected value is transcribed from `origin/c-master` and quoted in the file.
|
||||
**No divergence from C was found.**
|
||||
|
||||
The issue body's trap list was wrong and the correction is the first thing
|
||||
worth recording: there is no separate "poison dart" trap — `T_DART` **is**
|
||||
the poisoned dart, its death message being "a poisoned dart killed you" —
|
||||
and the list omitted `T_MYST`, the mystery trap, whose arm is an eleven-way
|
||||
`rnd(11)` message switch. `rogue.h` 192-200 is the authority
|
||||
(`T_DOOR`/`T_ARROW`/`T_SLEEP`/`T_BEAR`/`T_TELEP`/`T_DART`/`T_RUST`/`T_MYST`,
|
||||
`NTRAPS` 8) and the Go `TrapKind` iota matches it index-for-index.
|
||||
|
||||
Three C details the tests are built around. (1) `BEARTIME` and `SLEEPTIME`
|
||||
are `spread(3)` and `spread(5)` (`rogue.h` 108-109), and `spread` is
|
||||
`nm - nm/20 + rnd(nm/10)`; for both, `nm/10` is 0 and C's `rnd` short
|
||||
circuits a zero range without touching the generator, so each is an exact
|
||||
constant that costs **no** random number — and the tests assert the no-draw
|
||||
half as well as the value, because a stray draw desynchronises the
|
||||
seed-compatible stream. (2) `T_ARROW` swings at `s_lvl - 1` and `T_DART` at
|
||||
`s_lvl + 1`: opposite signs, which is exactly the kind of detail a
|
||||
transliterating port drops. (3) The strength loss is gated on
|
||||
`!ISWEARING(R_SUSTSTR) && !save(VS_POISON)`, and the `&&` is load-bearing —
|
||||
with the ring on, C never rolls the save, so the arm must spend two random
|
||||
numbers and not three.
|
||||
|
||||
Two shapes worth keeping, both forced by mutation results rather than
|
||||
foresight. Damage dice are checked by a **sweep**, not one shot: `rnd(n)` is
|
||||
"raw value % n", so a single draw agrees between a d6 and a d5 five times in
|
||||
six and leaves the generator identical either way — the first draft's
|
||||
single-trial arrow test passed with `roll(1,6)` mutated to `roll(1,5)`.
|
||||
Likewise the swing arguments are pinned by a 200-trial boundary sweep at a
|
||||
mid-range to-hit target: a forced hit and a forced miss cannot see a wrong
|
||||
`at_lvl` or a dropped `op_arm`, because both arms are reachable at any level
|
||||
and swing spends one `rnd(20)` regardless.
|
||||
|
||||
Mutation-proved, 31 mutations, each reverted. Two were **not** caught on the
|
||||
first pass and the tests were strengthened until they were, which is the
|
||||
useful part of the record. (a) Deleting `new_level()` from `T_DOOR` left the
|
||||
suite green: `be_trapped`'s own prologue stamps the trap glyph into the cell
|
||||
the hero fell through, so "the map changed" is true even with no new level
|
||||
dug. The test now counts differing cells — exactly one can change that way —
|
||||
and also requires the staircase to move and the hero to be re-placed. (b)
|
||||
The `roll(1,6)` case above. One line is recorded as **not isolable**:
|
||||
`T_TELEP`'s `mvaddch(tc, TRAP)`, whose C comment claims `look()` will not
|
||||
redraw the vacated square. It is in fact redundant in both C and this port —
|
||||
the prologue has already set `p_ch` to `TRAP`, `teleport()` opens by drawing
|
||||
`floor_at()` (which returns `chat(hero)`) over the departing square, and
|
||||
`leave_room` only blanks squares showing `FLOOR` — so no reachable
|
||||
configuration makes deleting it observable. That is stated in the test
|
||||
rather than papered over. The other 29 each failed their own test and only
|
||||
their own; two also moved `TestAutoSaveOnSignalRacesTurnLoop`, which drives
|
||||
real turns and is legitimately sensitive to `BEARTIME` and to armor rusting.
|
||||
|
||||
Deliberately uncovered: the two death messages, "an arrow killed you" and "a
|
||||
poisoned dart killed you". Each is printed immediately before `death()`,
|
||||
which reaches `myExit` and `os.Exit`, so provoking either would take the
|
||||
test binary with it; the hero is pinned with `fortify()` and the damage
|
||||
rolls are checked by replaying C's arithmetic instead of by letting HP reach
|
||||
zero. `Next Step` deliberately not rotated: #14 was an out-of-band gap found
|
||||
while surveying, not part of the rings/sticks/wizard step.
|
||||
|
||||
- 2026-08-09 Ring unit-test coverage (`test/rings-coverage`, closes #5): the
|
||||
first third of the standing coverage step. `game/rings.go` had **zero** tests
|
||||
— not one of the 32 in the suite touched wear, removal, hand choice, or the
|
||||
|
||||
1023
game/traps_test.go
Normal file
1023
game/traps_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user