Port the '+' wizard-mode toggle-off (closes #11) #30
Reference in New Issue
Block a user
Delete Branch "fix/wizard-toggle-off"
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?
Closes #11.
C's
command.c317-338 has awhen '+'arm — in the main commandswitch, under
#ifdef MASTER— that toggles wizard mode. The port had no'+'anywhere, so the key fell throughdispatchKey's default toillcomand answered "illegal command '+'".
The password half of that arm was dropped deliberately (wizard mode is
ROGUE_WIZARDconfiguration) and is recorded in ARCHITECTURE.md §9.The leave half was lost silently, and it is a different decision: it does
not touch the password machinery. The substantive part of it is
turn_see(TRUE), not the flag — wizard sight draws every monster the herocannot see, so without the re-hide there is no way back to normal
visibility, and clearing the flag alone would leave the screen lying.
What changed
game/command.go: newwizardToggleCommand.After = false; in wizardmode it clears
Wizard, callsg.turnSee(true)and prints"not wizard any more"; otherwise"sorry".game/tables.go:'+'registered incommandHandlersbetween'^'andEscape— C's own switch order. Because C's arm is in the main switchrather than the
if (wizard) switch (ch)sub-switch thatwizardCommandports, it is reachable whether or not
wizardis set;commandHandlersis the faithful home, and the non-wizard case was a divergence too.
§9: a password check that no longer exists can never succeed, so the
else arm is exactly what C did on a wrong answer —
"sorry", with noprompt (nothing typed into one could change the outcome) and none of the
noscore/turn_see(FALSE)bookkeeping of C's unreachable success branch.game/wizard_test.go: two tests, both driving'+'throughg.dispatch.ARCHITECTURE.md§9: the password row now names the'+'enter armas the dropped part, and a new paragraph records that the leave arm is
ported in full.
TODO.md: Completed Steps entry.Next Stepdeliberately not rotated(out-of-band issue work).
C sources each claim was verified against
Read via
git show;c-masterwas not checked out or modified.origin/c-master:command.c317-338 — the#ifdef MASTER/when '+'/#endifblock, both arms, and its position immediately beforeotherwise:in the main switch (the wizard sub-switch starts at 369).The issue cited 316-336; cosmetic drift, everything else in it held.
origin/c-master:potions.c267-306 —turn_see. Theturn_offarmredraws
t_oldchat every monster the hero cannot see and clearsSEEMONST, and reaches norndcall (only the turn-on arm does).origin/c-master:rogue.h:691for the declaration.origin/c-master:wizard.c233-257 —passwd(): it prompts, reads aline, and compares
md_crypt(buf, "mT")againstPASSWD. An emptyanswer returns
FALSE, which is the branch this port is permanently on."not wizard any more"and
"sorry".Verification
make checkgreen on the branch:fmt-checkclean,golangci-lint0 issues,
make test(-timeout 30s -race -cover) ok for allpackages. The only linter output beyond that is the known
gomodguarddeprecation warning (#29).
GOLANGCI_LINT_CACHEin a fresh emptydirectory, not the shared host cache; the run named no path outside the
branch's own worktree.
make testre-run with the Go test cache bypassed, so the green is not acache hit.
turnSee(true)call failsTestWizardToggleOffRehidesSensedMonsterson all three visibilityassertions —
SenseMonstersstill set, cell still'P', cell still instandout — which is exactly what a flag-only test would have missed.
rndcall added, andTestSeedCompatItemTablespasses against the golden, which was not regenerated or edited.
.golangci.ymlunmodified (sha256 still021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb).mainand clean.
What is in this branch
One commit,
c95f98f, onfix/wizard-toggle-offoffmain@630038e.game/command.go— newwizardToggleCommand():After = falseunconditionally, as C's arm does first.g.Wizard = false, theng.turnSee(true), theng.msg("not wizard any more")— C's order exactly.g.msg("sorry").(command.c command)breadcrumb and statesthe dropped-password reduction in full.
game/tables.go—'+': (*RogueGame).wizardToggleCommandincommandHandlers, placed between'^'andEscape, which is where the armsits in C's switch.
ARCHITECTURE.md§9 — the "DES crypt wizard password" row now reads"DES crypt wizard password (
passwd(), the'+'enter arm)", and a newparagraph after the table records that the leave arm is ported in full as
wizardToggleCommand, thatturnSee(true)is why, and that'+'outsidewizard mode reduces to "sorry".
TODO.md— Completed Steps entry in the same commit.Next Stepwasdeliberately not rotated: this is out-of-band issue work, matching the
convention the last four issue entries use.
Where the issue was right, and the one place it drifted
Every substantive premise checked out. Two refinements:
command.c317-338 (#ifdef MASTER317,when '+':318,#endif338), not 316-336. Cosmetic.'+'is in C's main commandswitch, immediately before
otherwise:, not in theif (wizard) switch (ch)sub-switch (that one starts at line 369) whichwizardCommand/wizardDebugCommandport. UnderMASTER, C thereforereaches
'+'whether or notwizardis set. So the faithful home iscommandHandlers, notwizardCommand— and the non-wizard path was adivergence too, answering "illegal command '+'" where C answers "sorry".
The issue's DoD item 2 anticipated this; it is worth naming because
putting the handler in
wizardCommandwould have looked right and lefthalf the bug in place.
C sources each claim was verified against
All read with
git show;c-masterandmodern-roguewere not checked outor touched.
'+'arm exists, both halves, in main switchorigin/c-master:command.c317-338,otherwise:at 366origin/c-master:command.c369-405turn_seesemantics and norndon the off armorigin/c-master:potions.c267-306turn_seedeclarationorigin/c-master:rogue.h:691passwd()is a DES compare that can only failorigin/c-master:wizard.c233-257Message strings are byte-for-byte from
command.c:"not wizard any more"and
"sorry". They are asserted raw againstMessageLine.Huh, which iscaptured before
endmsg's display-time capitalization, so the test pins thecontract text rather than the rendered text.
How it was tested
game/wizard_test.gogains two tests, botht.Parallel(), in a file thatalready carries the approved
//nolint:testpackageheader.TestWizardToggleOffRehidesSensedMonsters— the substantive one. It spawnsa phantom next to the hero; a phantom carries
Invisiblestraight from themonster table, so
seeMonstis false for it and it is on screen onlybecause wizard sight put it there. The test asserts its preconditions before
asserting anything else (monster is genuinely unseeable, the map char under
it differs from its glyph,
turnSee(false)setSenseMonstersand drew'P'in standout at its cell), then drivesg.dispatch('+')and checks:Wizardcleared,SenseMonsterscleared, the cell restored to themonster's
OldCh, standout off, message"not wizard any more",Afterfalse.
TestWizardToggleWithoutWizardSaysSorry—'+'withWizardfalse: wizardmode stays off,
SenseMonstersis untouched, message"sorry",Afterfalse.
Mutation proof. Deleting the
g.turnSee(true)call makes the first testfail on all three visibility assertions and on none of the others:
That is the half a flag-only test would have missed, which is why the issue
insisted on it.
Gate
make checkgreen:fmt-checkclean,golangci-lint0 issues,make test(-timeout 30s -race -cover) ok on all three packages.GOLANGCI_LINT_CACHEpointed at a fresh empty directorycreated for this branch, not the shared host cache, so the 0 cannot be a
stale shared-cache artifact. Its output named no path outside this
branch's own worktree. The only extra output was the known
gomodguarddeprecation warning tracked as #29, untouched here.
make testwas also re-run with the Go test cache bypassed, so the greenis a real execution rather than a cache hit.
TestSeedCompatItemTablespasses againstgame/testdata/item_tables.goldenexactly as committed — the golden was neither edited nor regenerated, and
no
rndcall was added (theturn_offarm ofturn_seereaches none)..golangci.ymluntouched; sha256 still021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb.shared clone stayed on
mainand clean throughout.Review of PR #30 —
fix/wizard-toggle-off@c95f98f(basemain@630038e)Verdict: PASS.
Independent review; reviewer did not author the change. All C claims were
re-verified from scratch via
git show origin/c-master:...;c-masterandmodern-roguewere not checked out. Work was done in a throwaway worktree,which has been removed; the shared clone was left on
mainand clean.Adjudication of the author's correction to issue #11
The author is right, and the issue was wrong. Verified independently:
origin/c-master:rogue.h:52-53—#define when break;caseand#define otherwise break;default. Thewhenmacro claim holds.origin/c-master:command.c—#ifdef MASTERat 317,when '+':at318,
#endifat 338. The issue's 316-336 is off by one line at eachend. Cosmetic; the author's numbers are the correct ones.
when '^':at 300 precedes it,when ESCAPE:at 339 follows it,otherwise:is at 366, and theif (wizard) switch (ch)sub-switch thatwizardCommand/wizardDebugCommandport opens at 369 (inside
#ifdef MASTERat 368,#endifat 425).'+'is not in that sub-switch.
MASTER, C reaches'+'regardless of thewizardflag.commandHandlersis therefore the faithful home, and thenot-in-wizard-mode path was a live divergence too ("illegal command '+'" vs
C's "sorry"). Had the handler gone into
wizardCommandper the issue'sframing, half the bug would have survived while looking fixed.
One prose inaccuracy in the PR description and the issue comment, non-blocking
and not present in the repo: both say the arm sits "immediately before
otherwise:". It does not — six arms (ESCAPE,'m',')',']','=','@') sit between#endifat 338 andotherwise:at 366. The load-bearingclaim (main switch, not the wizard sub-switch) is unaffected, and the code,
the doc comment,
TODO.mdand ARCHITECTURE.md all describe the placementcorrectly ("between
'^'andEscape", which is exactly C's order).Itemized verification
command.c:324ismsg("not wizard any more")andcommand.c:336ismsg("sorry"). Both strings ingame/command.go:419,428match byte-for-byte, including the two-word"any more" and the lowercase "sorry". Both are asserted raw against
MessageLine.Huh, whichgame/io.go:57-60captures from the buffer inEnd()before the display-time capitalization atio.go:67-71— so thetests pin the contract text, not the rendered text. Correct choice.
command.c:322-324iswizard = FALSE;→turn_see(TRUE);→msg(...).game/command.go:424-428isg.Wizard = false→g.turnSee(true)→
g.msg(...). Identical, and the order matters:turnSeecallsseeMonst, so clearing the flag first is what makes the re-hide see thepost-wizard world.
turnSee(true)vsturn_see(TRUE).origin/c-master:potions.c:270-306against
game/potions.go:295-317: same loop over the monster list, samemovethencan_see = see_monst(mp), sameif (!can_see) addch(t_oldch)on the off arm, same
SEEMONSTclear after the loop. The re-hide is real.Pre-existing code, unchanged by this PR.
origin/c-master:wizard.c:233-258.passwd()prints"wizard's Password:", reads a line, returnsFALSEon an empty answer andotherwise compares
md_crypt(buf, "mT")againstPASSWD. C's wrong-answerpath then falls to
command.c:335-336, which prints"sorry"and doesnothing else — no state change, and
wizardwas already assignedFALSEfrom
passwd(). So the reduction to a bare"sorry"is exactly C'sreachable behaviour. C does prompt first; dropping the prompt is right,
because with no stored hash there is nothing the input could be compared
against and the outcome is fixed before the read. Dropping
noscore/turn_see(FALSE)is right for the same reason — both belong tothe unreachable success branch — and separately
noscoreis not lost fromthe port at all:
game/command.go:119-121(turnUpkeep) already setsg.NoScore = trueevery turn whileg.Wizard. The doc comment atgame/command.go:405-414states the reduction and its reasoning in full,with the
ROGUE_WIZARD/§9 pointer. Adequate.After = false.command.c:319setsafter = FALSEbefore theif (wizard), i.e. on both arms.game/command.go:416sets itunconditionally at the top. Matches;
'+'consumes no turn, so daemon andfuse sequencing is unchanged. Both tests assert it after presetting
g.After = true.game/tables.go:805adds'+': (*RogueGame).wizardToggleCommandbetween'^'andEscape— C's ownswitch order.
commandHandlersis a map, so the position is stylistic only.No key is shadowed or displaced:
'+'had no prior entry,wizardCommandand
wizardDebugCommandcontain no'+'case, and the only other'+'literals in non-test code are the
Doorglyph (game/types.go:48) and theblessing prompt (
game/wizard.go:84,96), neither of which is a command key.The diff touches no other handler.
g.turnSee(true)fromgame/command.go:427and runningGOFLAGS=-count=1 make test, the failureis exactly as claimed and confined to one test:
TestSeedCompatItemTables. Two furthermutations confirm the rest is non-vacuous: removing
g.After = falsefailswizard_test.go:498and:529in both tests; changing the strings to"not wizard anymore"/"Sorry"failswizard_test.go:494and:525.The visibility half is genuinely load-bearing — a flag-only test would have
passed all three mutations that matter. The preconditions
(
wizard_test.go:437-462) are asserted witht.Fatalbefore the act, sothe test cannot silently degrade into asserting nothing: it proves the
phantom is
Invisible, thatseeMonstis false for it, thatOldChdiffersfrom
Type, and thatturnSee(false)actually drew it in standout.potions.creachesrndonly on the turn-on arm(
rnd(26) + 'A', the hallucinating draw); theturn_offarm has norndcall, and the Go mirror keeps that split (
showSensedholds the turn-onarm). No
rndcall is added anywhere in the diff, nothing undergame/testdata/is touched, andTestSeedCompatItemTablespasses againstthe committed golden.
"DES crypt wizard password (
passwd(), the'+'enter arm)", which isprecisely the dropped half, and the new paragraph correctly states that the
leave arm is ported in full, why
turnSee(true)is the substantive part,and what the else arm reduces to. Table column alignment is intact (all rows
68 chars in column 1).
Gate
make checkgreen onc95f98fwithGOLANGCI_LINT_CACHEpointed at a freshempty directory outside the shared host cache:
fmt-checkclean (gofmt andprettier),
golangci-lint0 issues, tests ok for all three packages. Theonly extra output was the known
gomodguarddeprecation warning (#29), whichmatches the
main@630038ecalibration.make test(-timeout 30s -race -cover) re-run three times withGOFLAGS=-count=1: green each time, no data race..golangci.ymlnot in the diff; sha256 still021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb.git merge-treeagainstmain@630038e(current head) isconflict-free.
all; nothing red, and outside this PR's scope.
script/changes. No new//nolintdirectives; thetest file's
//nolint:testpackageheader is pre-existing and both new testscall
t.Parallel().Port the '+' wizard-mode toggle-off (closes #11). Authorand committer are
sneak <sneak@sneak.berlin>.the author identity; no attribution trailers.
git diff --checkclean; no scripted-edit artifacts.TODO.md: Completed Steps entry added in the same commit;Next Step("Broaden unit test coverage...") not rotated, as intended for out-of-band
issue work.
Non-blocking observations
otherwise:" phrasing in the PR body and in theissue #11 comment is wrong (see above). Repo content is unaffected; no
change requested.
wizardToggleCommandnever enters wizard mode, so the name is slightlybroader than the behaviour. It is the right name for C's command, the doc
comment resolves it immediately, and it is consistent with the neighbouring
identifyTrapCommand/wizardCommand. No change requested.Manager notes (the review is in its own comment above).
Verdict accepted: PASS. Labeling
merge-readyand merging directly —mainis unprotected here, so this does not go tosneakdespite thereviewer's suggestion.
My issue was wrong in a way that would have produced a half-fix. I said
'+'belongs inwizardCommand/wizardDebugCommand. It does not: it sits inC's main command switch (
when '^':at 300,when '+':at 318,when ESCAPE:at 339), while theif (wizard) switch (ch)sub-switch thatthose functions port does not open until 369.
The consequence is the part I would have missed entirely: under
MASTER, Creaches
'+'regardless of whetherwizardis set, so the non-wizard pathwas a second live divergence — Go answered "illegal command '+'" where C
answers "sorry". Implementing my version would have fixed the wizard arm,
looked correct, and left the other half broken with nothing to catch it.
The reviewer verified the correction from primary sources rather than
adjudicating between two agents' assertions, which is what made this
conclusive:
rogue.h:52-53confirms#define when break;case, and theline-by-line reading of
command.cplaces the block precisely. It alsocorrected the author's line numbers (317-338, not my 316-336).
Three mutations, not one. The author proved deleting
g.turnSee(true)fails the visibility test; the reviewer reproduced that and added two more —
removing
g.After = falsefails the turn-consumption assertions, and alteringthe message strings fails the text assertions. Each mutation fails only its
own assertions. That is a materially stronger claim than "the test passes",
and it is the standard I want on this repo after #26.
Two details worth recording because they show the check went past the obvious:
against
MessageLine.Huhas captured before the display capitalization ingame/io.go:67-71. Asserting post-capitalization would have silentlytolerated a change to the stored string.
noscoreis not lost. C's unreachable success branch sets it; thereviewer confirmed the port already sets
g.NoScore = trueevery turn whileg.WizardinturnUpkeep, so dropping that bookkeeping from the reductioncosts nothing. That is exactly the sort of "the reduction drops something
that matters" risk I wanted checked.
The one inaccuracy found is confined to the PR body and my issue comment,
not the repo: both say the arm sits "immediately before
otherwise:" when sixarms sit between. The code, doc comment,
TODO.md, andARCHITECTURE.md§9all describe the placement correctly, so nothing false lands in the tree.
Correctly non-blocking.
Follow-up filed as #31: nothing has ever checked C's command switch against
Go's dispatch table, and
'+'was found by accident rather than by audit. Amissing dispatch entry is uniquely invisible in a function-by-function port —
no dangling reference, no compile error, fails only when a player presses the
key. That issue asks for a regression test, not just a sweep.