Unit test coverage: wizard commands (game/wizard.go has zero tests) #7
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.