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
|
reach only mean something against known walls and a known passage number. All
|
||||||
27 mutations tried against the new tests were caught.
|
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
|
- 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
|
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
|
— 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