Unit-test the eight trap effects against the C reference (closes #14) #37
86
TODO.md
86
TODO.md
@@ -70,6 +70,92 @@ 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` (19 tests, 15 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, 33 mutations, each reverted, and every one of them is now
|
||||
caught. Three 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.
|
||||
|
||||
(c) **`be_trapped` takes a coordinate, and which coordinate decides whether
|
||||
`T_TELEP`'s `mvaddch(tc, TRAP)` does anything.** Deleting that line first
|
||||
left the suite green, and the first draft wrote that off as an unavoidable
|
||||
redundancy — wrongly, because the test only exercised one of the two call
|
||||
sites. `move.go` 105-108 (`case Floor`) springs a trap under the hero and
|
||||
passes `p.Pos`; there `tc` **is** the hero's square, the prologue has
|
||||
already set its `p_ch` to `TRAP`, and `teleport()` opens by drawing
|
||||
`floor_at()` — which returns `chat(hero)` — over it, so the glyph is on
|
||||
screen before the line runs. But `move.go` 94-98 (`case Trap`), the ordinary
|
||||
walk onto a hidden trap, passes `nh`, the square being stepped **onto**,
|
||||
with the hero still on the previous square: `teleport()`'s opening `mvaddch`
|
||||
paints the old square, `leave_room` writes blanks and never `TRAP`, and
|
||||
nothing calls `look()` afterwards because the `case Trap` arm returns before
|
||||
`finishMove` for a teleporter. There `mvaddch(tc, TRAP)` is the only writer,
|
||||
exactly as C's comment says.
|
||||
`TestTrapTeleportDrawsTheTrapOnTheSquareSteppedOnto` springs the trap at a
|
||||
floor square next to the hero and pins it: unmutated the screen at `tc`
|
||||
reads `^`, with the line deleted it reads `.`.
|
||||
|
||||
The other 30 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. They are the only two: `rust_armor`'s `|| ISWEARING(R_SUSTARM)`
|
||||
operand and its `if (!to_death)` suppression of the rust-vanishes message,
|
||||
the last predicates that had no assertion, are pinned by
|
||||
`TestTrapRustHonoursTheRingAndTheToDeathFlag`. This entry does **not**
|
||||
rotate `Next Step`: #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
|
||||
|
||||
1140
game/traps_test.go
Normal file
1140
game/traps_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user