game/wizard.go (230 lines, 8 functions) has no test coverage. The wizard
commands are the debug surface used to set up game states, so a break here
silently costs future debugging and playtesting — and nothing would catch it.
Third of three units splitting the TODO.md Next Step into commit-sized
pieces. Completing this one closes out that step.
Untested surface
Function
What it does
createObj()
wizard item creation from a type prompt
createWeaponArmor(obj)
fill in weapon/armor specifics
createRing(obj)
fill in ring specifics
showMap()
reveal the whole level
whatis(insist, kind)
identify an item
whatisPick(insist, kind)
the item-selection half of whatis
setKnow(obj, info)
mark an item type as known
teleport()
wizard teleport
Note whatis/whatisPick/setKnow are not purely debug — setKnow mutates
the per-game "discovered" tables that drive item naming in normal play, so a
defect leaks into ordinary gameplay.
Definition of done
A new game/wizard_test.go covering, at minimum:
createObj produces an object of the requested kind, correctly placed.
createWeaponArmor and createRing populate their kind-specific fields
(damage dice / armor class / ring bonus).
showMap marks the level's squares seen — assert against a generated
level, not a hand-built stub.
whatis on an unidentified item marks the type known and changes the
item's displayed name.
setKnow mutates the correct per-game table entry (and, critically, that
it does not leak across games — build two games and confirm independence).
teleport relocates the hero to a valid floor square, and updates
whatever room/position bookkeeping the move requires.
Wizard mode is entered the way the real code does (see the ROGUE_WIZARD
handling), not by poking a flag, unless poking is the only option — in
which case say so in a comment.
make check fully green.
TODO.md updated in the same commit — with #5 and #6 done, the
"broaden unit test coverage (rings, sticks, wizard commands)" step is
complete and should be rotated into Completed Steps, promoting the next
Future Step.
Commit title ends with (closes #N).
Implementation requirements
Cross-check against the C reference (git show origin/c-master:wizard.c).
Do NOT check out or modify origin/c-master. Do not write tests that merely
restate current Go behavior.
If you find a real divergence from C, stop and report it rather than
fixing gameplay in a test-coverage commit.
Tests must call t.Parallel(); keep the lint run at 0 issues.
White-box tests need the //nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
header.
make targets only. Do NOT modify .golangci.yml. No game behavior changes.
Watch for os.Exit paths — wizard commands that can end the game must not
terminate the test binary.
Never mention Claude or Anthropic anywhere.
Depends on
Best done after #5 and #6 so the TODO.md step rotation is a single clean
edit, but it is independently implementable if picked up first (in which case
narrow the step instead of closing it).
## Problem
`game/wizard.go` (230 lines, 8 functions) has **no test coverage**. The wizard
commands are the debug surface used to set up game states, so a break here
silently costs future debugging and playtesting — and nothing would catch it.
Third of three units splitting the `TODO.md` Next Step into commit-sized
pieces. Completing this one closes out that step.
## Untested surface
| Function | What it does |
| --- | --- |
| `createObj()` | wizard item creation from a type prompt |
| `createWeaponArmor(obj)` | fill in weapon/armor specifics |
| `createRing(obj)` | fill in ring specifics |
| `showMap()` | reveal the whole level |
| `whatis(insist, kind)` | identify an item |
| `whatisPick(insist, kind)` | the item-selection half of `whatis` |
| `setKnow(obj, info)` | mark an item type as known |
| `teleport()` | wizard teleport |
Note `whatis`/`whatisPick`/`setKnow` are not purely debug — `setKnow` mutates
the per-game "discovered" tables that drive item naming in normal play, so a
defect leaks into ordinary gameplay.
## Definition of done
1. A new `game/wizard_test.go` covering, at minimum:
- `createObj` produces an object of the requested kind, correctly placed.
- `createWeaponArmor` and `createRing` populate their kind-specific fields
(damage dice / armor class / ring bonus).
- `showMap` marks the level's squares seen — assert against a generated
level, not a hand-built stub.
- `whatis` on an unidentified item marks the type known and changes the
item's displayed name.
- `setKnow` mutates the correct per-game table entry (and, critically, that
it does not leak across games — build two games and confirm independence).
- `teleport` relocates the hero to a valid floor square, and updates
whatever room/position bookkeeping the move requires.
2. Wizard mode is entered the way the real code does (see the `ROGUE_WIZARD`
handling), not by poking a flag, unless poking is the only option — in
which case say so in a comment.
3. `make check` fully green.
4. `TODO.md` updated in the same commit — with #5 and #6 done, the
"broaden unit test coverage (rings, sticks, wizard commands)" step is
complete and should be rotated into Completed Steps, promoting the next
Future Step.
5. Commit title ends with ` (closes #N)`.
## Implementation requirements
- Cross-check against the C reference (`git show origin/c-master:wizard.c`).
Do NOT check out or modify `origin/c-master`. Do not write tests that merely
restate current Go behavior.
- If you find a real divergence from C, **stop and report it** rather than
fixing gameplay in a test-coverage commit.
- Tests must call `t.Parallel()`; keep the lint run at 0 issues.
- White-box tests need the
`//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)`
header.
- `make` targets only. Do NOT modify `.golangci.yml`. No game behavior changes.
- Watch for `os.Exit` paths — wizard commands that can end the game must not
terminate the test binary.
- Never mention Claude or Anthropic anywhere.
## Depends on
Best done after #5 and #6 so the `TODO.md` step rotation is a single clean
edit, but it is independently implementable if picked up first (in which case
narrow the step instead of closing it).
Branch test/wizard-coverage off main @ bf820e3. Everything below is
transcribed from git show origin/c-master:wizard.c (plus command.c for the ^I kit and rogue.h for the R_* numbering and F_* place flags), not from
what the port currently returns.
Additions go into the existing game/wizard_test.go (created by #20, extended by #30) so the mustNotPanic / malformed
harness is reused rather than duplicated. Nothing already there is touched.
createObj — the existing tests already pin kind/Which selection and the
bounds guard. Adding: the object is actually filed in the pack with a pack
letter and Count == 1, Group == 0 (C sets both before the kind switch),
and the gold arm ('*') sets GoldValue from the typed number, C's get_num(&obj->o_goldval, stdscr).
createWeaponArmor — the sign convention is the substantive thing here and
it is inverted between the two arms in C: a cursed weapon does o_hplus -= rnd(3)+1 and a blessed one +=, while cursed armor does o_arm += rnd(3)+1 and blessed -= (lower AC is better). Plus ISCURSED
on '-', the init_weapon damage dice for the weapon arm, and o_arm = a_class[o_which] as the armor baseline.
createRing — o_arm = (bless == '-' ? -1 : rnd(2)+1) for the four bonus
rings (R_PROTECT, R_ADDSTR, R_ADDHIT, R_ADDDAM); ISCURSED and no prompt at all for R_AGGR/R_TELEPORT; nothing for every other
kind. The "no prompt" half is asserted by checking the scripted input
cursor did not advance, which is what would catch a stray readchar
desynchronising every later answer.
showMap — against a generated level (fixed seed, depth deep enough that
the generator actually produces non-F_REAL squares), asserting every cell
in y 1..NumLines-2 of the hw window carries the map char, and that
rows 0 and NumLines-1 are left alone, since C's loop excludes them.
whatis / whatisPick — an unidentified item becomes Know in the right
per-game table and its inventoryName changes; the insist loop's three
C arms (n_objs == 0 gives up, NULL gives "you must identify something",
wrong kind gives "you must identify a %s"); the empty-pack early return.
setKnow — right table, right index, Guess cleared, ISKNOW set, and
two independent games confirming no cross-game leak.
teleport — lands on a square stepOk accepts, Player.Room agrees with roomIn(pos) afterwards, the vacated square is redrawn with floorAt()
and the new one with PLAYER, and the Flytrap unhold (ISHELD cleared, vf_hit = 0, monster 'F' damage reset to 000x0) which is the one part
of the function that touches ordinary play.
wizardKit (game/command.go) — nine raise_level() calls, the
(+1,+1) two-handed sword as cur_weapon, plate mail at o_arm == -5 with ISKNOW as cur_armor.
Wizard mode is entered through the real path: Params{Wizard: true} is the
field cmd/rogue/main.go fills from ROGUE_WIZARD, so no flag poking is
needed. Every scripted input sequence gets an abort tail, because testTerm.ReadChar returns filler forever once the script runs out and a
re-prompting loop would spin to the 30s timeout instead of failing. Each
substantive assertion will be mutation-checked (break the behaviour, confirm
that test and only that test fails, revert) and the results posted.
Two corrections to the issue before I start
DoD item 1 says showMap "marks the level's squares seen". It does not,
in either language: C's show_map writes into the hw window and calls show_win, and the Go port does the same. No F_SEEN is set and no Place is modified — the whole function is display-only. The test will
assert what it does do (renders the whole map into hw) against a
generated level as asked, but it cannot assert squares becoming seen.
DoD item 4's rotation assumes #6 has landed. As of bf820e3 it has
not, so per the issue's own "in which case narrow the step instead of
closing it" I will narrow Next Step to sticks only unless #6 merges
before I commit.
One divergence found while reading the C (not fixing it here)
show_map's standout handling is asymmetric in C and symmetric in the port:
real = flat(y, x);
if (!(real & F_REAL))
wstandout(hw);
wmove(hw, y, x);
waddch(hw, chat(y, x));
if (!real) /* <- whole flag word, not the F_REAL bit */
wstandend(hw);
new_level.c seeds every square with p_flags = F_REAL, so real is only
ever zero where F_REAL was cleared from a word that held nothing else. putpass clears it on a square that already has F_PASS (secret passage),
and the trap loop clears it on a square it then ORs a non-zero rnd(NTRAPS)
into — in both cases real != 0, so C turns standout on and never off,
and the remainder of the map renders in reverse video. game/wizard.go uses isReal for both tests, so it highlights only the individual square.
The port's behaviour is the sane one and this is a wizard-only display quirk,
so I am not touching it in a test-coverage commit and not filing it as a
defect. What I will not do either is write a test asserting the symmetric
behaviour as if it were C-verified: the showMap test asserts the map
characters unconditionally and the standout attribute only up to the first
non-real square, with the quirk quoted in a comment. Flagging it here so it
can be decided on deliberately.
## Implementation plan
Branch `test/wizard-coverage` off `main` @ `bf820e3`. Everything below is
transcribed from `git show origin/c-master:wizard.c` (plus `command.c` for the
`^I` kit and `rogue.h` for the `R_*` numbering and `F_*` place flags), not from
what the port currently returns.
Additions go into the existing `game/wizard_test.go` (created by
https://git.eeqj.de/sneak/rgoue/pulls/20, extended by
https://git.eeqj.de/sneak/rgoue/pulls/30) so the `mustNotPanic` / `malformed`
harness is reused rather than duplicated. Nothing already there is touched.
1. `createObj` — the existing tests already pin kind/`Which` selection and the
bounds guard. Adding: the object is actually filed in the pack with a pack
letter and `Count == 1`, `Group == 0` (C sets both before the kind switch),
and the gold arm (`'*'`) sets `GoldValue` from the typed number, C's
`get_num(&obj->o_goldval, stdscr)`.
2. `createWeaponArmor` — the sign convention is the substantive thing here and
it is inverted between the two arms in C: a cursed weapon does
`o_hplus -= rnd(3)+1` and a blessed one `+=`, while cursed **armor** does
`o_arm += rnd(3)+1` and blessed `-=` (lower AC is better). Plus `ISCURSED`
on `'-'`, the `init_weapon` damage dice for the weapon arm, and
`o_arm = a_class[o_which]` as the armor baseline.
3. `createRing` — `o_arm = (bless == '-' ? -1 : rnd(2)+1)` for the four bonus
rings (`R_PROTECT`, `R_ADDSTR`, `R_ADDHIT`, `R_ADDDAM`); `ISCURSED` and
**no prompt at all** for `R_AGGR`/`R_TELEPORT`; nothing for every other
kind. The "no prompt" half is asserted by checking the scripted input
cursor did not advance, which is what would catch a stray `readchar`
desynchronising every later answer.
4. `showMap` — against a generated level (fixed seed, depth deep enough that
the generator actually produces non-`F_REAL` squares), asserting every cell
in `y` 1..`NumLines-2` of the `hw` window carries the map char, and that
rows 0 and `NumLines-1` are left alone, since C's loop excludes them.
5. `whatis` / `whatisPick` — an unidentified item becomes `Know` in the right
per-game table and its `inventoryName` changes; the `insist` loop's three
C arms (`n_objs == 0` gives up, `NULL` gives "you must identify something",
wrong kind gives "you must identify a %s"); the empty-pack early return.
6. `setKnow` — right table, right index, `Guess` cleared, `ISKNOW` set, and
two independent games confirming no cross-game leak.
7. `teleport` — lands on a square `stepOk` accepts, `Player.Room` agrees with
`roomIn(pos)` afterwards, the vacated square is redrawn with `floorAt()`
and the new one with `PLAYER`, and the Flytrap unhold (`ISHELD` cleared,
`vf_hit = 0`, monster `'F'` damage reset to `000x0`) which is the one part
of the function that touches ordinary play.
8. `wizardKit` (`game/command.go`) — nine `raise_level()` calls, the
(+1,+1) two-handed sword as `cur_weapon`, plate mail at `o_arm == -5` with
`ISKNOW` as `cur_armor`.
Wizard mode is entered through the real path: `Params{Wizard: true}` is the
field `cmd/rogue/main.go` fills from `ROGUE_WIZARD`, so no flag poking is
needed. Every scripted input sequence gets an abort tail, because
`testTerm.ReadChar` returns filler forever once the script runs out and a
re-prompting loop would spin to the 30s timeout instead of failing. Each
substantive assertion will be mutation-checked (break the behaviour, confirm
that test and only that test fails, revert) and the results posted.
## Two corrections to the issue before I start
- **DoD item 1 says `showMap` "marks the level's squares seen".** It does not,
in either language: C's `show_map` writes into the `hw` window and calls
`show_win`, and the Go port does the same. No `F_SEEN` is set and no
`Place` is modified — the whole function is display-only. The test will
assert what it does do (renders the whole map into `hw`) against a
generated level as asked, but it cannot assert squares becoming seen.
- **DoD item 4's rotation** assumes #6 has landed. As of `bf820e3` it has
not, so per the issue's own "in which case narrow the step instead of
closing it" I will narrow `Next Step` to sticks only unless #6 merges
before I commit.
## One divergence found while reading the C (not fixing it here)
`show_map`'s standout handling is asymmetric in C and symmetric in the port:
real = flat(y, x);
if (!(real & F_REAL))
wstandout(hw);
wmove(hw, y, x);
waddch(hw, chat(y, x));
if (!real) /* <- whole flag word, not the F_REAL bit */
wstandend(hw);
`new_level.c` seeds every square with `p_flags = F_REAL`, so `real` is only
ever zero where `F_REAL` was cleared from a word that held nothing else.
`putpass` clears it on a square that already has `F_PASS` (secret passage),
and the trap loop clears it on a square it then ORs a non-zero `rnd(NTRAPS)`
into — in both cases `real != 0`, so C turns standout **on and never off**,
and the remainder of the map renders in reverse video. `game/wizard.go` uses
`isReal` for both tests, so it highlights only the individual square.
The port's behaviour is the sane one and this is a wizard-only display quirk,
so I am not touching it in a test-coverage commit and not filing it as a
defect. What I will not do either is write a test asserting the symmetric
behaviour as if it were C-verified: the `showMap` test asserts the map
characters unconditionally and the standout attribute only up to the first
non-real square, with the quirk quoted in a comment. Flagging it here so it
can be decided on deliberately.
Implemented in #38 (branch test/wizard-coverage, commit 495c462). One commit, tests plus the TODO.md rotation; no game behaviour changed.
What was built. Additions to the existing game/wizard_test.go covering
all eight functions of game/wizard.go plus wizardKit from game/command.go: createObj's shared tail (o_group = 0, o_count = 1, add_pack filing) and its GOLD arm; createWeaponArmor's dice and the
inverted blessing sign between the weapon and armor arms; createRing's three
case groups; showMap's rendering and loop bounds; whatis's per-kind table
dispatch, its weapon/armor arm and its empty-pack return; all three arms of whatisPick's insist loop; setKnow's entry, guess-clearing and
cross-game independence; and teleport's destination, room bookkeeping,
redraw, run-state reset and Flytrap unhold. Wizard mode is entered through Params{Wizard: true} — the field cmd/rogue/main.go fills from ROGUE_WIZARD — so nothing pokes the flag.
How it was verified.make check fully green; golangci-lint 0 issues on
a private cache with no lock collision and no path outside the worktree;
package coverage 60.6% -> 62.4% measured against the branch base with and
without the new file. Every expected value transcribed from wizard.c, command.c, extern.c, weapons.c, rogue.h, new_level.c and passages.c via git show origin/c-master:. 30 mutations run one at a
time, 30 caught, each with the intended test failing and nothing else; 29
fail in under a second, and the one that fails by timeout does so because the
arm it deletes is the insist loop's only exit.
On the two corrections raised in the plan.
showMap marking squares seen: confirmed it does not, so that part of the
definition of done could not be met as written. The loop bounds are pinned
instead by planting a marker in the rows C's loop excludes — needed because
those rows are blank on a real level, so "untouched" was otherwise
unfalsifiable (a mutation caught that weakness in my own first draft).
The rotation: #6 merged while this was
in flight, so main moved to c0741ad. The branch was moved onto it and
re-verified, and the step is therefore rotated rather than narrowed,
with the release step promoted into Next Step.
One further C finding, beyond the show_map standout divergence already
reported above. A wizard-created "cursed" weapon is not cursed, in C or in
the port: create_obj sets ISCURSED and init_weapon then assigns weap->o_flags = iwp->iw_flags straight over it, leaving only the o_hplus
penalty. The port is faithful; the test pins the whole flag word, and deleting
the ISCURSED line leaves every weapon test green, which is the evidence the
line is dead for weapons. Recorded, not changed.
Implemented in https://git.eeqj.de/sneak/rgoue/pulls/38 (branch
`test/wizard-coverage`, commit `495c462`). One commit, tests plus the
`TODO.md` rotation; no game behaviour changed.
**What was built.** Additions to the existing `game/wizard_test.go` covering
all eight functions of `game/wizard.go` plus `wizardKit` from
`game/command.go`: `createObj`'s shared tail (`o_group = 0`, `o_count = 1`,
`add_pack` filing) and its GOLD arm; `createWeaponArmor`'s dice and the
inverted blessing sign between the weapon and armor arms; `createRing`'s three
case groups; `showMap`'s rendering and loop bounds; `whatis`'s per-kind table
dispatch, its weapon/armor arm and its empty-pack return; all three arms of
`whatisPick`'s `insist` loop; `setKnow`'s entry, guess-clearing and
cross-game independence; and `teleport`'s destination, room bookkeeping,
redraw, run-state reset and Flytrap unhold. Wizard mode is entered through
`Params{Wizard: true}` — the field `cmd/rogue/main.go` fills from
`ROGUE_WIZARD` — so nothing pokes the flag.
**How it was verified.** `make check` fully green; `golangci-lint` 0 issues on
a private cache with no lock collision and no path outside the worktree;
package coverage 60.6% -> 62.4% measured against the branch base with and
without the new file. Every expected value transcribed from `wizard.c`,
`command.c`, `extern.c`, `weapons.c`, `rogue.h`, `new_level.c` and
`passages.c` via `git show origin/c-master:`. **30 mutations run one at a
time, 30 caught**, each with the intended test failing and nothing else; 29
fail in under a second, and the one that fails by timeout does so because the
arm it deletes is the `insist` loop's only exit.
**On the two corrections raised in the plan.**
- `showMap` marking squares seen: confirmed it does not, so that part of the
definition of done could not be met as written. The loop bounds are pinned
instead by planting a marker in the rows C's loop excludes — needed because
those rows are blank on a real level, so "untouched" was otherwise
unfalsifiable (a mutation caught that weakness in my own first draft).
- The rotation: https://git.eeqj.de/sneak/rgoue/issues/6 merged while this was
in flight, so `main` moved to `c0741ad`. The branch was moved onto it and
re-verified, and the step is therefore **rotated** rather than narrowed,
with the release step promoted into `Next Step`.
**One further C finding, beyond the `show_map` standout divergence already
reported above.** A wizard-created "cursed" weapon is not cursed, in C or in
the port: `create_obj` sets `ISCURSED` and `init_weapon` then *assigns*
`weap->o_flags = iwp->iw_flags` straight over it, leaving only the `o_hplus`
penalty. The port is faithful; the test pins the whole flag word, and deleting
the `ISCURSED` line leaves every weapon test green, which is the evidence the
line is dead for weapons. Recorded, not changed.
Correction: my show_map C analysis above was wrong
This corrects the two comments above
(#7 (comment) and #7 (comment)), which are left in
place. It was caught in review of #38 and
is now fixed in the commit message, TODO.md, the PR body and the showMap
test's comment at 6f409bd.
The divergence is real and unchanged. C tests !(real & F_REAL) before the
draw and !real — the whole flag word — after; game/wizard.go tests isReal
on both sides. What I got wrong was the enumeration of squares that lose F_REAL, and therefore the consequence.
I claimed two sites and said every such square keeps other bits, so real is
never zero and C latches standout on at the first secret square and never turns
it off, reversing the rest of the map. There are three sites — grepping ~F_REAL across every .c file finds exactly three hits — and two of them
leave the flag word at zero:
passages.c putpass() — pp->p_flags |= F_PASS runs first, so clearing F_REAL leaves 0x80. Non-zero. This one I had right.
passages.c door(), the secret-door arm — I omitted it entirely. It
clears F_REAL on a room-wall exit whose flags are still exactly F_REAL: new_level seeds that, rooms.c writes no p_flags at all (only p_ch),
and conn() calls door() before the digging loop, which starts at spos
but increments before its first putpass. Result: p_flags == 0
exactly. Its per-square gate is rnd(5) == 0 against putpass's rnd(40) == 0, and game/passages.go's door reproduces it, so these
squares occur on generated levels in both languages.
The new_level.c trap loop — *sp &= ~F_REAL; *sp |= rnd(NTRAPS);. rnd() is range == 0 ? 0 : abs((int) RN) % range (main.c) and NTRAPS
is 8, so it yields 0..7, and T_DOOR is 00 (rogue.h). An unsprung
trapdoor square is therefore also exactly zero — be_trapped is what later
ORs F_SEEN in. My "a non-zero rnd(NTRAPS)" was wrong one time in eight.
Corrected behaviour: C's wstandenddoes fire, at secret doors and at
unsprung trapdoors. What C gets wrong is leaking the attribute forward from
a secret passage (or a non-trapdoor trap) until the scan reaches one of those
zero-flag squares. That is intermittent bands of reverse video, not a
permanently reversed map — a materially milder defect than I described, which
matters because that severity is the input to #39.
Also withdrawn: the commit message's "a square whose whole flag word is zero,
which no secret square has". Two of the three sites produce exactly that.
How I verified it, first-hand rather than from the review. All reads via git show origin/c-master:<file>, nothing checked out.
Site enumeration: ~F_REAL over every .c file in origin/c-master returns
three hits and no others — new_level.c, and passages.c twice.
Site 1: read putpass() in full; the |= F_PASS precedes the gate.
Site 2: read door() in full, plus conn() for the call order and rooms.c
for what touches p_flags (nothing — the only p_flags reference there is a
read of F_PASS in the maze code, and door() returns early for ISMAZE
rooms anyway). Compared against game/passages.go's door, which matches
line for line.
Site 3: read new_level()'s trap loop, rnd() in main.c, and NTRAPS / T_DOOR in rogue.h; and be_trapped() in move.c for where F_SEEN
arrives afterwards. game/newlevel.go does the same Clear(FReal) / |= PlaceFlags(g.rnd(NumTrapTypes)).
No test or game code changed. The showMap test was already written to pin
only what both languages agree on, so the corrected analysis does not disturb
it; the only test-file edit is the comment above it, plus renaming TestShowMapLeavesTheRowsCOmits to TestShowMapLoopBoundsMatchC (the old name
parsed as "C Omits"). make check green again after the edits.
## Correction: my `show_map` C analysis above was wrong
This corrects the two comments above
(https://git.eeqj.de/sneak/rgoue/issues/7#issuecomment-50493 and
https://git.eeqj.de/sneak/rgoue/issues/7#issuecomment-50707), which are left in
place. It was caught in review of https://git.eeqj.de/sneak/rgoue/pulls/38 and
is now fixed in the commit message, `TODO.md`, the PR body and the `showMap`
test's comment at `6f409bd`.
**The divergence is real and unchanged.** C tests `!(real & F_REAL)` before the
draw and `!real` — the whole flag word — after; `game/wizard.go` tests `isReal`
on both sides. What I got wrong was the enumeration of squares that lose
`F_REAL`, and therefore the consequence.
I claimed two sites and said every such square keeps other bits, so `real` is
never zero and C latches standout on at the first secret square and never turns
it off, reversing the rest of the map. There are **three** sites — grepping
`~F_REAL` across every `.c` file finds exactly three hits — and two of them
leave the flag word at zero:
1. **`passages.c putpass()`** — `pp->p_flags |= F_PASS` runs first, so clearing
`F_REAL` leaves `0x80`. Non-zero. This one I had right.
2. **`passages.c door()`, the secret-door arm** — I omitted it entirely. It
clears `F_REAL` on a room-wall exit whose flags are still exactly `F_REAL`:
`new_level` seeds that, `rooms.c` writes no `p_flags` at all (only `p_ch`),
and `conn()` calls `door()` before the digging loop, which starts at `spos`
but increments before its first `putpass`. Result: **`p_flags == 0`
exactly.** Its per-square gate is `rnd(5) == 0` against `putpass`'s
`rnd(40) == 0`, and `game/passages.go`'s `door` reproduces it, so these
squares occur on generated levels in both languages.
3. **The `new_level.c` trap loop** — `*sp &= ~F_REAL; *sp |= rnd(NTRAPS);`.
`rnd()` is `range == 0 ? 0 : abs((int) RN) % range` (`main.c`) and `NTRAPS`
is 8, so it yields **0..7**, and `T_DOOR` is `00` (`rogue.h`). An unsprung
trapdoor square is therefore also exactly zero — `be_trapped` is what later
ORs `F_SEEN` in. My "a non-zero `rnd(NTRAPS)`" was wrong one time in eight.
**Corrected behaviour:** C's `wstandend` *does* fire, at secret doors and at
unsprung trapdoors. What C gets wrong is leaking the attribute **forward** from
a secret passage (or a non-trapdoor trap) until the scan reaches one of those
zero-flag squares. That is **intermittent bands of reverse video, not a
permanently reversed map** — a materially milder defect than I described, which
matters because that severity is the input to
https://git.eeqj.de/sneak/rgoue/issues/39.
Also withdrawn: the commit message's "a square whose whole flag word is zero,
which no secret square has". Two of the three sites produce exactly that.
**How I verified it, first-hand rather than from the review.** All reads via
`git show origin/c-master:<file>`, nothing checked out.
- Site enumeration: `~F_REAL` over every `.c` file in `origin/c-master` returns
three hits and no others — `new_level.c`, and `passages.c` twice.
- Site 1: read `putpass()` in full; the `|= F_PASS` precedes the gate.
- Site 2: read `door()` in full, plus `conn()` for the call order and `rooms.c`
for what touches `p_flags` (nothing — the only `p_flags` reference there is a
read of `F_PASS` in the maze code, and `door()` returns early for `ISMAZE`
rooms anyway). Compared against `game/passages.go`'s `door`, which matches
line for line.
- Site 3: read `new_level()`'s trap loop, `rnd()` in `main.c`, and `NTRAPS` /
`T_DOOR` in `rogue.h`; and `be_trapped()` in `move.c` for where `F_SEEN`
arrives afterwards. `game/newlevel.go` does the same
`Clear(FReal)` / `|= PlaceFlags(g.rnd(NumTrapTypes))`.
**No test or game code changed.** The `showMap` test was already written to pin
only what both languages agree on, so the corrected analysis does not disturb
it; the only test-file edit is the comment above it, plus renaming
`TestShowMapLeavesTheRowsCOmits` to `TestShowMapLoopBoundsMatchC` (the old name
parsed as "C Omits"). `make check` green again after the edits.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
game/wizard.go(230 lines, 8 functions) has no test coverage. The wizardcommands are the debug surface used to set up game states, so a break here
silently costs future debugging and playtesting — and nothing would catch it.
Third of three units splitting the
TODO.mdNext Step into commit-sizedpieces. Completing this one closes out that step.
Untested surface
createObj()createWeaponArmor(obj)createRing(obj)showMap()whatis(insist, kind)whatisPick(insist, kind)whatissetKnow(obj, info)teleport()Note
whatis/whatisPick/setKnoware not purely debug —setKnowmutatesthe per-game "discovered" tables that drive item naming in normal play, so a
defect leaks into ordinary gameplay.
Definition of done
game/wizard_test.gocovering, at minimum:createObjproduces an object of the requested kind, correctly placed.createWeaponArmorandcreateRingpopulate their kind-specific fields(damage dice / armor class / ring bonus).
showMapmarks the level's squares seen — assert against a generatedlevel, not a hand-built stub.
whatison an unidentified item marks the type known and changes theitem's displayed name.
setKnowmutates the correct per-game table entry (and, critically, thatit does not leak across games — build two games and confirm independence).
teleportrelocates the hero to a valid floor square, and updateswhatever room/position bookkeeping the move requires.
ROGUE_WIZARDhandling), not by poking a flag, unless poking is the only option — in
which case say so in a comment.
make checkfully green.TODO.mdupdated in the same commit — with #5 and #6 done, the"broaden unit test coverage (rings, sticks, wizard commands)" step is
complete and should be rotated into Completed Steps, promoting the next
Future Step.
(closes #N).Implementation requirements
git show origin/c-master:wizard.c).Do NOT check out or modify
origin/c-master. Do not write tests that merelyrestate current Go behavior.
fixing gameplay in a test-coverage commit.
t.Parallel(); keep the lint run at 0 issues.//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)header.
maketargets only. Do NOT modify.golangci.yml. No game behavior changes.os.Exitpaths — wizard commands that can end the game must notterminate the test binary.
Depends on
Best done after #5 and #6 so the
TODO.mdstep rotation is a single cleanedit, but it is independently implementable if picked up first (in which case
narrow the step instead of closing it).
Implementation plan
Branch
test/wizard-coverageoffmain@bf820e3. Everything below istranscribed from
git show origin/c-master:wizard.c(pluscommand.cfor the^Ikit androgue.hfor theR_*numbering andF_*place flags), not fromwhat the port currently returns.
Additions go into the existing
game/wizard_test.go(created by#20, extended by
#30) so the
mustNotPanic/malformedharness is reused rather than duplicated. Nothing already there is touched.
createObj— the existing tests already pin kind/Whichselection and thebounds guard. Adding: the object is actually filed in the pack with a pack
letter and
Count == 1,Group == 0(C sets both before the kind switch),and the gold arm (
'*') setsGoldValuefrom the typed number, C'sget_num(&obj->o_goldval, stdscr).createWeaponArmor— the sign convention is the substantive thing here andit is inverted between the two arms in C: a cursed weapon does
o_hplus -= rnd(3)+1and a blessed one+=, while cursed armor doeso_arm += rnd(3)+1and blessed-=(lower AC is better). PlusISCURSEDon
'-', theinit_weapondamage dice for the weapon arm, ando_arm = a_class[o_which]as the armor baseline.createRing—o_arm = (bless == '-' ? -1 : rnd(2)+1)for the four bonusrings (
R_PROTECT,R_ADDSTR,R_ADDHIT,R_ADDDAM);ISCURSEDandno prompt at all for
R_AGGR/R_TELEPORT; nothing for every otherkind. The "no prompt" half is asserted by checking the scripted input
cursor did not advance, which is what would catch a stray
readchardesynchronising every later answer.
showMap— against a generated level (fixed seed, depth deep enough thatthe generator actually produces non-
F_REALsquares), asserting every cellin
y1..NumLines-2of thehwwindow carries the map char, and thatrows 0 and
NumLines-1are left alone, since C's loop excludes them.whatis/whatisPick— an unidentified item becomesKnowin the rightper-game table and its
inventoryNamechanges; theinsistloop's threeC arms (
n_objs == 0gives up,NULLgives "you must identify something",wrong kind gives "you must identify a %s"); the empty-pack early return.
setKnow— right table, right index,Guesscleared,ISKNOWset, andtwo independent games confirming no cross-game leak.
teleport— lands on a squarestepOkaccepts,Player.Roomagrees withroomIn(pos)afterwards, the vacated square is redrawn withfloorAt()and the new one with
PLAYER, and the Flytrap unhold (ISHELDcleared,vf_hit = 0, monster'F'damage reset to000x0) which is the one partof the function that touches ordinary play.
wizardKit(game/command.go) — nineraise_level()calls, the(+1,+1) two-handed sword as
cur_weapon, plate mail ato_arm == -5withISKNOWascur_armor.Wizard mode is entered through the real path:
Params{Wizard: true}is thefield
cmd/rogue/main.gofills fromROGUE_WIZARD, so no flag poking isneeded. Every scripted input sequence gets an abort tail, because
testTerm.ReadCharreturns filler forever once the script runs out and are-prompting loop would spin to the 30s timeout instead of failing. Each
substantive assertion will be mutation-checked (break the behaviour, confirm
that test and only that test fails, revert) and the results posted.
Two corrections to the issue before I start
showMap"marks the level's squares seen". It does not,in either language: C's
show_mapwrites into thehwwindow and callsshow_win, and the Go port does the same. NoF_SEENis set and noPlaceis modified — the whole function is display-only. The test willassert what it does do (renders the whole map into
hw) against agenerated level as asked, but it cannot assert squares becoming seen.
bf820e3it hasnot, so per the issue's own "in which case narrow the step instead of
closing it" I will narrow
Next Stepto sticks only unless #6 mergesbefore I commit.
One divergence found while reading the C (not fixing it here)
show_map's standout handling is asymmetric in C and symmetric in the port:new_level.cseeds every square withp_flags = F_REAL, sorealis onlyever zero where
F_REALwas cleared from a word that held nothing else.putpassclears it on a square that already hasF_PASS(secret passage),and the trap loop clears it on a square it then ORs a non-zero
rnd(NTRAPS)into — in both cases
real != 0, so C turns standout on and never off,and the remainder of the map renders in reverse video.
game/wizard.gousesisRealfor both tests, so it highlights only the individual square.The port's behaviour is the sane one and this is a wizard-only display quirk,
so I am not touching it in a test-coverage commit and not filing it as a
defect. What I will not do either is write a test asserting the symmetric
behaviour as if it were C-verified: the
showMaptest asserts the mapcharacters unconditionally and the standout attribute only up to the first
non-real square, with the quirk quoted in a comment. Flagging it here so it
can be decided on deliberately.
Implemented in #38 (branch
test/wizard-coverage, commit495c462). One commit, tests plus theTODO.mdrotation; no game behaviour changed.What was built. Additions to the existing
game/wizard_test.gocoveringall eight functions of
game/wizard.gopluswizardKitfromgame/command.go:createObj's shared tail (o_group = 0,o_count = 1,add_packfiling) and its GOLD arm;createWeaponArmor's dice and theinverted blessing sign between the weapon and armor arms;
createRing's threecase groups;
showMap's rendering and loop bounds;whatis's per-kind tabledispatch, its weapon/armor arm and its empty-pack return; all three arms of
whatisPick'sinsistloop;setKnow's entry, guess-clearing andcross-game independence; and
teleport's destination, room bookkeeping,redraw, run-state reset and Flytrap unhold. Wizard mode is entered through
Params{Wizard: true}— the fieldcmd/rogue/main.gofills fromROGUE_WIZARD— so nothing pokes the flag.How it was verified.
make checkfully green;golangci-lint0 issues ona private cache with no lock collision and no path outside the worktree;
package coverage 60.6% -> 62.4% measured against the branch base with and
without the new file. Every expected value transcribed from
wizard.c,command.c,extern.c,weapons.c,rogue.h,new_level.candpassages.cviagit show origin/c-master:. 30 mutations run one at atime, 30 caught, each with the intended test failing and nothing else; 29
fail in under a second, and the one that fails by timeout does so because the
arm it deletes is the
insistloop's only exit.On the two corrections raised in the plan.
showMapmarking squares seen: confirmed it does not, so that part of thedefinition of done could not be met as written. The loop bounds are pinned
instead by planting a marker in the rows C's loop excludes — needed because
those rows are blank on a real level, so "untouched" was otherwise
unfalsifiable (a mutation caught that weakness in my own first draft).
in flight, so
mainmoved toc0741ad. The branch was moved onto it andre-verified, and the step is therefore rotated rather than narrowed,
with the release step promoted into
Next Step.One further C finding, beyond the
show_mapstandout divergence alreadyreported above. A wizard-created "cursed" weapon is not cursed, in C or in
the port:
create_objsetsISCURSEDandinit_weaponthen assignsweap->o_flags = iwp->iw_flagsstraight over it, leaving only theo_hpluspenalty. The port is faithful; the test pins the whole flag word, and deleting
the
ISCURSEDline leaves every weapon test green, which is the evidence theline is dead for weapons. Recorded, not changed.
clawbot referenced this issue2026-08-09 17:43:36 +02:00
Correction: my
show_mapC analysis above was wrongThis corrects the two comments above
(#7 (comment) and
#7 (comment)), which are left in
place. It was caught in review of #38 and
is now fixed in the commit message,
TODO.md, the PR body and theshowMaptest's comment at
6f409bd.The divergence is real and unchanged. C tests
!(real & F_REAL)before thedraw and
!real— the whole flag word — after;game/wizard.gotestsisRealon both sides. What I got wrong was the enumeration of squares that lose
F_REAL, and therefore the consequence.I claimed two sites and said every such square keeps other bits, so
realisnever zero and C latches standout on at the first secret square and never turns
it off, reversing the rest of the map. There are three sites — grepping
~F_REALacross every.cfile finds exactly three hits — and two of themleave the flag word at zero:
passages.c putpass()—pp->p_flags |= F_PASSruns first, so clearingF_REALleaves0x80. Non-zero. This one I had right.passages.c door(), the secret-door arm — I omitted it entirely. Itclears
F_REALon a room-wall exit whose flags are still exactlyF_REAL:new_levelseeds that,rooms.cwrites nop_flagsat all (onlyp_ch),and
conn()callsdoor()before the digging loop, which starts atsposbut increments before its first
putpass. Result:p_flags == 0exactly. Its per-square gate is
rnd(5) == 0againstputpass'srnd(40) == 0, andgame/passages.go'sdoorreproduces it, so thesesquares occur on generated levels in both languages.
new_level.ctrap loop —*sp &= ~F_REAL; *sp |= rnd(NTRAPS);.rnd()isrange == 0 ? 0 : abs((int) RN) % range(main.c) andNTRAPSis 8, so it yields 0..7, and
T_DOORis00(rogue.h). An unsprungtrapdoor square is therefore also exactly zero —
be_trappedis what laterORs
F_SEENin. My "a non-zerornd(NTRAPS)" was wrong one time in eight.Corrected behaviour: C's
wstandenddoes fire, at secret doors and atunsprung trapdoors. What C gets wrong is leaking the attribute forward from
a secret passage (or a non-trapdoor trap) until the scan reaches one of those
zero-flag squares. That is intermittent bands of reverse video, not a
permanently reversed map — a materially milder defect than I described, which
matters because that severity is the input to
#39.
Also withdrawn: the commit message's "a square whose whole flag word is zero,
which no secret square has". Two of the three sites produce exactly that.
How I verified it, first-hand rather than from the review. All reads via
git show origin/c-master:<file>, nothing checked out.~F_REALover every.cfile inorigin/c-masterreturnsthree hits and no others —
new_level.c, andpassages.ctwice.putpass()in full; the|= F_PASSprecedes the gate.door()in full, plusconn()for the call order androoms.cfor what touches
p_flags(nothing — the onlyp_flagsreference there is aread of
F_PASSin the maze code, anddoor()returns early forISMAZErooms anyway). Compared against
game/passages.go'sdoor, which matchesline for line.
new_level()'s trap loop,rnd()inmain.c, andNTRAPS/T_DOORinrogue.h; andbe_trapped()inmove.cfor whereF_SEENarrives afterwards.
game/newlevel.godoes the sameClear(FReal)/|= PlaceFlags(g.rnd(NumTrapTypes)).No test or game code changed. The
showMaptest was already written to pinonly what both languages agree on, so the corrected analysis does not disturb
it; the only test-file edit is the comment above it, plus renaming
TestShowMapLeavesTheRowsCOmitstoTestShowMapLoopBoundsMatchC(the old nameparsed as "C Omits").
make checkgreen again after the edits.