Cover game/rings.go with C-verified unit tests (closes #5)
game/rings.go had no test coverage at all: not one of the suite's tests touched wearing a ring, taking one off, choosing a hand, or the ring contribution to the hunger clock. New game/rings_test.go covers ringOn, pickRingHand, ringOff, gethand, ringEat and ringNum, plus the ring arm of things.c dropcheck (dropRing), which is what actually removes a worn ring. 17 tests, 44 subtests; package coverage 53.7% -> 56.2%. No game code changes. Every expected value is transcribed from the C reference on origin/c-master (rings.c, rogue.h, things.c) and quoted in the file, rather than from what the port currently returns. No divergence from C was found. ringEat is the reason this matters most: it feeds daemons.c's hunger clock, so a wrong entry is a slow, silent drift in when the hero starves. All fourteen ring kinds are pinned to C's uses[] table, both hands. The three C subtleties are handled explicitly: a negative uses[] entry is a one-in-n chance of a single unit and not a literal cost; R_DIGEST then flips the sign, so slow digestion returns 0 or -1; and ring_num's switch closes with the otherwise macro (rogue.h 53: break;default), so its four labels fall through to one sprintf and every other kind returns "" from a default arm. The chance rings are checked by snapshotting the generator, calling ringEat, and replaying C's own expression from the identical state, which pins the one-in-n denominator, the sign flip and the fact that exactly one rnd call is spent; a frequency check over 4000 trials backs it. The non-negative entries assert the opposite, that the generator is untouched, because C never reaches rnd on that path and a stray call there would desynchronise the game's RNG stream from C's. Scripted hand answers carry an abort tail (a space for the reprompt's --More--, then ESCAPE) so that a port which stopped accepting a key fails on its assertion instead of looping forever on the headless terminal's filler input. The "only one hand free" cases script the wrong hand key on purpose: a port that prompted anyway would consume it and land the ring on the wrong side. Mutation-proved with 23 mutations, each reverted, each failing its own test and only its own. All fourteen kinds are exercised; the eleven with no wear-time effect in C are documented at the foot of the file as deliberately not given a wear/remove test, and ring_off's unreachable "not wearing such a ring" arm is documented as unreachable.
This commit is contained in:
71
TODO.md
71
TODO.md
@@ -29,11 +29,78 @@ Refactor ground rules:
|
||||
|
||||
# Next Step
|
||||
|
||||
Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
|
||||
wizard commands).
|
||||
Broaden unit test coverage where playtesting finds thin spots — sticks (#6) and
|
||||
wizard commands (#7). Rings, the first third of this step, is done; see the top
|
||||
of Completed Steps.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 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
|
||||
ring contribution to the hunger clock. New `game/rings_test.go` (17 tests, 44
|
||||
subtests) covers `ringOn`, `pickRingHand`, `ringOff`, `gethand`, `ringEat` and
|
||||
`ringNum`, plus the ring arm of `things.c dropcheck` (`dropRing`), which is
|
||||
what actually takes a ring off. Package coverage 53.7% -> 56.2%. `Next Step`
|
||||
narrowed rather than rotated: #6 and #7 are the other two thirds.
|
||||
|
||||
Every expected value is transcribed from `origin/c-master` (`rings.c`,
|
||||
`rogue.h`, `things.c`), never from what the port returns, and the C is
|
||||
quoted in the file. **No divergence from C was found**, which is the result
|
||||
and is worth recording as a negative: `ringEat` is the one function here
|
||||
whose being wrong would be invisible — it feeds `daemons.c`'s hunger clock,
|
||||
so a bad entry is a slow drift in when the hero starves rather than anything
|
||||
a playtest would notice — and it now has all fourteen ring kinds pinned to
|
||||
C's table.
|
||||
|
||||
Three C details the tests were written around. (1) `ring_eat`'s `uses[]`
|
||||
holds negatives, and a negative is **not** a cost: C computes
|
||||
`eat = (rnd(-eat) == 0)`, a one-in-n chance of a single unit. (2) `R_DIGEST`
|
||||
then flips the sign, so slow digestion returns 0 or **-1** and is the only
|
||||
ring that gives food back. (3) `ring_num`'s switch closes with the
|
||||
`otherwise` macro, which `rogue.h` 53 defines as `break;default` — so its
|
||||
four labels fall through to one `sprintf` and every other kind returns `""`
|
||||
from a default arm, not by falling off the end. The `RingKind` iota matches
|
||||
C's `R_` numbering index-for-index, so a `uses[]` index and a `RingKind` are
|
||||
the same number; `R_ADDHIT` is `RingDexterity` and `R_ADDDAM` is
|
||||
`RingIncreaseDamage`.
|
||||
|
||||
The chance rings are checked two ways at once. Each call snapshots the
|
||||
generator, runs `ringEat`, and replays C's own expression from the identical
|
||||
state — which pins the one-in-n denominator, the sign flip, and the fact
|
||||
that exactly one `rnd` call is spent — and a frequency check over 4000
|
||||
trials backs it with a number a human can read. The non-negative entries
|
||||
assert the reverse: the generator must be **untouched**, because C never
|
||||
reaches `rnd` on that path and a stray call there would desynchronise the
|
||||
whole game's RNG stream from C's and cost seed compatibility. That assertion
|
||||
is what caught the one real bug in this work, which was in the test and not
|
||||
the game: `g.Rng` is a pointer, so the first draft's snapshots aliased
|
||||
instead of copying.
|
||||
|
||||
Two shapes worth keeping. Scripted hand answers carry an abort tail (a space
|
||||
for the reprompt's `--More--`, then ESCAPE): without it a port that stopped
|
||||
accepting a key would loop forever on the headless terminal's filler input
|
||||
and the test would die of the 30s timeout instead of failing on its
|
||||
assertion — which is exactly what the first draft did, and it was only
|
||||
visible because the mutation run was inspected rather than trusted. And the
|
||||
"only one hand free" case scripts the _wrong_ hand key deliberately: a port
|
||||
that asked anyway consumes it and lands the ring on the wrong side, so the
|
||||
test fails on a hand rather than on a hang.
|
||||
|
||||
Mutation-proved, 23 mutations, each reverted: breaking `pickRingHand`'s
|
||||
ask/auto/reject arms, `ring_on`'s type guard, `is_current` guard and all
|
||||
three effect arms, `ring_off`'s no-rings message, hand selection and ESCAPE
|
||||
abort, `gethand`'s uppercase keys, ESCAPE and reprompt, `dropRing`'s hand
|
||||
clearing and both effect arms, `dropcheck`'s cursed gate, three `ringUses`
|
||||
entries, the `R_DIGEST` sign flip, the one-in-n roll, the empty-hand zero,
|
||||
and `ring_num`'s `ISKNOW` guard, label set and `RING`-vs-`WEAPON`
|
||||
formatting. Each failed its own test and only its own; stripping all three
|
||||
`ring_on` effect arms failed 3 of 3. All fourteen ring kinds are exercised;
|
||||
the eleven with no wear-time effect in C are documented at the foot of the
|
||||
file as deliberately not given a wear/remove test, with the files their
|
||||
powers actually live in, and `ring_off`'s unreachable "not wearing such a
|
||||
ring" arm is documented as unreachable rather than left looking untested.
|
||||
|
||||
- 2026-08-09 Command dispatch audit (`audit/command-switch-coverage`, closes
|
||||
#31): checked every case label in C's `command.c` against this port's
|
||||
dispatch, and left the audit behind as a standing test
|
||||
|
||||
Reference in New Issue
Block a user