1 Commits

Author SHA1 Message Date
6f409bda9e Cover the wizard commands with C-verified unit tests (closes #7)
game/wizard.go had no tests of its own. It is not purely a debug
surface: set_know writes the per-game discovered tables that name items
in ordinary play, and teleport is what the teleport ring calls every
fiftieth turn, so a defect in either leaks into a normal game.

Adds coverage for createObj (pack filing and the gold arm),
createWeaponArmor, createRing, showMap, whatis, whatisPick, setKnow,
teleport and command.go's wizardKit. Package coverage 60.6% -> 62.4%.
Expected values are transcribed from wizard.c, command.c, extern.c,
weapons.c and rogue.h rather than read off the port; wizard mode is
entered through Params.Wizard, the field main.go fills from
ROGUE_WIZARD, so no test pokes the flag.

Two notes from the C. A wizard-created "cursed" weapon is not cursed in
either language: init_weapon assigns o_flags over the ISCURSED bit
create_obj had just set, leaving only the o_hplus penalty, and the test
pins the whole flag word to the init_dam[] row to say so. And show_map
turns standout on for a square missing F_REAL but off only for a square
whose whole flag word is zero. Exactly three sites clear F_REAL:
putpass, which sets F_PASS first and so leaves 0x80; door's secret-door
arm, on a room-wall exit still holding exactly F_REAL, leaving zero;
and new_level's trap loop, whose rnd(NTRAPS) is 0..7, so the T_DOOR
(00) case leaves zero as well. C's wstandend therefore does fire, at
secret doors and unsprung trapdoors; what it gets wrong is leaking the
attribute forward from a secret passage or a non-trapdoor trap until it
reaches one of those - intermittent bands of reverse video, not a
permanently reversed map. That display-only difference is reported on
the issue and left alone here, and the test asserts standout only up to
the first secret square.

No game behavior is changed.
2026-08-09 16:01:07 +00:00
2 changed files with 45 additions and 28 deletions

51
TODO.md
View File

@@ -55,29 +55,40 @@ is finished.
from the port leaves every weapon test green — which is the evidence that from the port leaves every weapon test green — which is the evidence that
the line is dead for weapons. The armor arm has no such clobber and does the line is dead for weapons. The armor arm has no such clobber and does
keep the curse. (2) **`show_map`'s standout is asymmetric in C and symmetric keep the curse. (2) **`show_map`'s standout is asymmetric in C and symmetric
here.** C tests `!(real & F_REAL)` before drawing and `!real` after, and here.** C tests `!(real & F_REAL)` before drawing and `!real` — the whole
since `new_level` seeds every square with `p_flags = F_REAL` the squares flag word — after. `new_level` seeds every square with `p_flags = F_REAL`,
that lose it keep other bits (`F_PASS` from `putpass`, a non-zero and exactly three sites clear that bit. `passages.c putpass` sets `F_PASS`
`rnd(NTRAPS)` from the trap loop), so C turns standout on at the first first, so its secret passage is left at `0x80`. `passages.c door`'s
secret square and never off — the rest of the map renders reversed. secret-door arm clears it on a room-wall exit whose flags are still exactly
`game/wizard.go` tests `isReal` both times and highlights the one square. `F_REAL` (`rooms.c` writes no `p_flags` at all), leaving `p_flags == 0`; its
That is a display-only difference in a wizard-only command and was reported per-square gate is `rnd(5) == 0` against `putpass`'s `rnd(40) == 0`, and
on the issue rather than changed here; the test asserts the map characters `game/passages.go`'s `door` reproduces it. `new_level`'s trap loop then ORs
unconditionally but the standout attribute only up to the first secret in `rnd(NTRAPS)`, which is `abs((int) RN) % 8` and so yields `0..7`, and
square, so it pins nothing that C contradicts. `T_DOOR` is `00` — an unsprung trapdoor square is also exactly zero
(`be_trapped` is what later ORs `F_SEEN` into it). So C _does_ turn standout
off again, at secret doors and unsprung trapdoors; what it gets wrong is
leaking the attribute forward from a secret passage or a non-trapdoor trap
until it reaches one of those. Intermittent bands of reverse video, not a
permanently reversed map. `game/wizard.go` tests `isReal` both times and
highlights the one square. That is a display-only difference in a
wizard-only command and was reported on the issue rather than changed here;
the test asserts the map characters unconditionally but the standout
attribute only up to the first secret square, so it pins nothing that C
contradicts.
Two things the tests had to be built around. The `insist` arm of `whatis` is Two things the tests had to be built around. The `insist` arm of `whatis` is
a loop whose only exits are picking a matching item and `n_objs == 0`, so a a loop whose only exits are picking a matching item and `n_objs == 0`, so a
script that runs dry hangs instead of failing — every sequence here ends in script that runs dry hangs instead of failing — every sequence that can
an abort tail, the `n_objs == 0` exit is reached the way a player reaches it re-prompt ends in an abort tail, the `n_objs == 0` exit is reached the way a
(`*` for a list with nothing appropriate in the pack) rather than by poking player reaches it (`*` for a list with nothing appropriate in the pack)
the counter, and the one mutation that deletes that exit is the only one of rather than by poking the counter, and the one mutation that deletes that
the 30 that fails by timeout instead of fast, necessarily so. And `show_map` exit is the only one of the 30 that fails by timeout instead of fast,
does **not** mark squares seen — it writes into `hw` and touches no `PLACE` necessarily so. And `show_map` does **not** mark squares seen — it writes
at all — so the issue's wording for it could not be tested as written; the into `hw` and touches no `PLACE` at all — so the issue's wording for it
loop bounds are asserted instead by planting a marker in the rows C's loop could not be tested as written; the loop bounds are asserted instead by
excludes, since those rows are blank on a real level and copying blanks over planting a marker in the rows C's loop excludes, since those rows are blank
blanks would have made the bound unfalsifiable. on a real level and copying blanks over blanks would have made the bound
unfalsifiable.
- 2026-08-09 Wands and staffs under test (`test/sticks-coverage`, closes #6): - 2026-08-09 Wands and staffs under test (`test/sticks-coverage`, closes #6):
the second of the three thin spots the Next Step names. `game/sticks.go` was the second of the three thin spots the Next Step names. `game/sticks.go` was

View File

@@ -928,12 +928,18 @@ func TestCreateRingOtherKindsAreLeftAlone(t *testing.T) {
// ... // ...
// if (!real) wstandend(hw); /* whole word, not the bit */ // if (!real) wstandend(hw); /* whole word, not the bit */
// //
// new_level.c seeds every square with p_flags = F_REAL, and the squares // new_level.c seeds every square with p_flags = F_REAL, and exactly three
// that lose F_REAL keep other bits (F_PASS from putpass, a non-zero // sites clear that bit. putpass sets F_PASS first, so a secret passage is
// rnd(NTRAPS) from the trap loop), so C turns standout on at the first // left at 0x80. door's secret-door arm clears it on a room-wall exit whose
// secret square and never turns it off again. game/wizard.go tests isReal // flags are still exactly F_REAL, leaving p_flags == 0. And the trap loop
// both times and highlights the single square. That divergence is // ORs in rnd(NTRAPS), which is 0..7, so the T_DOOR (00) case is zero too
// reported on issue #7 rather than settled here, so this test asserts only // until be_trapped ORs F_SEEN in. So C's wstandend does fire, at secret
// doors and unsprung trapdoors; what it gets wrong is leaking standout
// forward from a secret passage or a non-trapdoor trap until it reaches
// one of those — intermittent bands, not a permanently reversed map.
// game/wizard.go tests isReal both times and highlights the single square.
// That divergence is reported on issue #7 rather than settled here, so
// this test asserts only
// what both agree on: the characters everywhere, standout on every // what both agree on: the characters everywhere, standout on every
// non-real square, and no standout on real squares before the first // non-real square, and no standout on real squares before the first
// non-real one. // non-real one.
@@ -979,10 +985,10 @@ func TestShowMapRendersTheWholeLevel(t *testing.T) {
} }
} }
// TestShowMapLeavesTheRowsCOmits pins the loop bounds. C starts at y = 1 // TestShowMapLoopBoundsMatchC pins the loop bounds. C starts at y = 1
// and stops before NUMLINES-1, so the top line stays free for show_win's // and stops before NUMLINES-1, so the top line stays free for show_win's
// prompt and the status line is never overwritten. // prompt and the status line is never overwritten.
func TestShowMapLeavesTheRowsCOmits(t *testing.T) { func TestShowMapLoopBoundsMatchC(t *testing.T) {
t.Parallel() t.Parallel()
g := mkWizard(t, 32, 10) g := mkWizard(t, 32, 10)