Port the '+' wizard-mode toggle-off (closes #11) #30
@@ -1784,7 +1784,7 @@ exit. Those are the steps referenced above (e.g. "step 5", "step 7").
|
||||
| `md_readchar` escape decoding | tcell decodes keys | key-event translation table |
|
||||
| XOR save/score encryption | obscurity, not security | plain gob (file perms 0600) |
|
||||
| save-file symlink/hardlink checks | single-user era anti-cheat | none |
|
||||
| DES crypt wizard password | ditto | `ROGUE_WIZARD` env var |
|
||||
| DES crypt wizard password (`passwd()`, the `'+'` enter arm) | ditto | `ROGUE_WIZARD` env var |
|
||||
| load-average / user-count gating (`too_much`, `ucount`, CHECKTIME) | 1980s timesharing courtesy | none |
|
||||
| tty dsusp/ltc character juggling | tcell owns the tty | none |
|
||||
| shell escape (`!`) setuid dance | no privileges to drop | plain `os/exec` shell |
|
||||
@@ -1793,6 +1793,16 @@ exit. Those are the steps referenced above (e.g. "step 5", "step 7").
|
||||
| SIGINT → the interactive `quit()` prompt | see below | `Q`; SIGINT exits cleanly |
|
||||
| `auto_save` on SIGILL/TRAP/FPE/BUS/SEGV/SYS | see below | none |
|
||||
|
||||
Only half of C's `'+'` command (`command.c` 317-338) goes with the password row.
|
||||
The leave arm is ported in full as `wizardToggleCommand`: it clears the wizard
|
||||
flag, calls `turnSee(true)` — C's `turn_see(TRUE)`, without which there is no
|
||||
way back out of wizard sight once it is on — and prints "not wizard any more".
|
||||
What `passwd()` takes with it is the enter arm. A password check that no longer
|
||||
exists is a password check that can never succeed, so `'+'` outside wizard mode
|
||||
reduces to what C did when the answer was wrong: the message "sorry", with no
|
||||
prompt, since nothing typed into one could change the outcome, and none of the
|
||||
`noscore`/`turn_see(FALSE)` bookkeeping of C's success branch.
|
||||
|
||||
The three signal rows warrant more than a table cell.
|
||||
|
||||
**SIGTSTP / `tstp()`.** Not handled, deliberately. Ctrl-Z cannot reach the game
|
||||
|
||||
38
TODO.md
38
TODO.md
@@ -34,6 +34,44 @@ wizard commands).
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-09 The `'+'` wizard-mode toggle (`fix/wizard-toggle-off`, closes #11):
|
||||
C's `command.c` 317-338 has a `when '+'` arm that leaves wizard mode —
|
||||
`wizard = FALSE`, `turn_see(TRUE)`, `msg("not wizard any more")` — and the
|
||||
port had no `'+'` anywhere, so the key fell through `dispatchKey`'s default to
|
||||
`illcom` and answered "illegal command '+'". The password half of that arm was
|
||||
dropped on purpose (wizard mode is `ROGUE_WIZARD` configuration) and is in
|
||||
ARCHITECTURE.md §9; the leave half was lost silently and is not the same
|
||||
decision — it does not touch the password machinery at all. **The substantive
|
||||
part is `turn_see(TRUE)`**, not the flag: wizard sight draws every monster the
|
||||
hero cannot see, so without the re-hide there is no way back to normal
|
||||
visibility once wizard mode is on, and clearing the flag alone would have left
|
||||
the screen lying. New `wizardToggleCommand` in `game/command.go`, registered
|
||||
in `commandHandlers` between `'^'` and `Escape` — C's own switch order, and
|
||||
note that C's arm sits in the **main** command switch under `#ifdef MASTER`,
|
||||
not in the `if (wizard) switch (ch)` sub-switch that `wizardCommand` ports, so
|
||||
it is reachable whether or not `wizard` is set. That makes the non-wizard case
|
||||
a divergence too, and it resolves the way the dropped `passwd()` forces: a
|
||||
password check that no longer exists can never succeed, so the else arm is
|
||||
what C did on a wrong answer, the message "sorry" — no prompt, since nothing
|
||||
typed into one could change the outcome, and none of the
|
||||
`noscore`/`turn_see(FALSE)` bookkeeping of C's unreachable success branch. The
|
||||
choice is stated in the function's doc comment and in §9, whose password row
|
||||
now names the `'+'` enter arm and whose new paragraph records that the leave
|
||||
arm is ported in full. Two tests in `game/wizard_test.go` drive `'+'` through
|
||||
`g.dispatch`: the wizard one spawns a phantom (`ISINVIS` straight from the
|
||||
monster table, so `seeMonst` is false and it is on screen only because wizard
|
||||
sight put it there), asserts the precondition — monster glyph drawn in
|
||||
standout at its cell, `SenseMonsters` set — and then asserts the flag cleared,
|
||||
`SenseMonsters` cleared, the cell back to the map char under the monster with
|
||||
standout off, the exact message, and `After` false; the non-wizard one pins
|
||||
"sorry" and that `'+'` is no longer an illegal command. Mutation-proved:
|
||||
deleting the `turnSee(true)` call fails the test on all three visibility
|
||||
assertions, which is the half a flag-only test would have missed. No RNG call
|
||||
is added — the `turn_off` arm of `turn_see` never reaches `rnd`, only the
|
||||
turn-on arm does — and `TestSeedCompatItemTables` stays green against the
|
||||
untouched golden. `Next Step` deliberately not rotated: out-of-band issue
|
||||
work.
|
||||
|
||||
- 2026-08-09 Cleanups deferred from the PR #26 review (`cleanup/pr26-followups`,
|
||||
closes #27): four items, no behaviour change. (1) The `sig-leave` entry below
|
||||
still argued, in the present tense, that declining to save on SIGINT/SIGQUIT
|
||||
|
||||
@@ -398,6 +398,36 @@ func (g *RogueGame) identifyTrapCommand() {
|
||||
}
|
||||
}
|
||||
|
||||
// wizardToggleCommand handles '+': leave wizard mode (command.c
|
||||
// command). C's arm lives in the main command switch under
|
||||
// #ifdef MASTER, not in the wizard sub-switch, so it is reachable
|
||||
// whether or not wizard is set.
|
||||
//
|
||||
// The entry half is deliberately not ported. C ran wizard = passwd(),
|
||||
// which compared a DES-crypted answer against a compiled-in password;
|
||||
// this port drops that machinery and makes wizard mode configuration
|
||||
// instead (ROGUE_WIZARD, ARCHITECTURE.md §9). A password check that is
|
||||
// gone is a password check that can never succeed, so the else arm
|
||||
// reduces to exactly what C did when the answer was wrong: wizard stays
|
||||
// off and the game says "sorry". No prompt is shown, since nothing typed
|
||||
// into it could change the outcome, and the noscore/turn_see(FALSE)
|
||||
// bookkeeping of C's success branch is unreachable and so is absent.
|
||||
func (g *RogueGame) wizardToggleCommand() {
|
||||
g.After = false
|
||||
|
||||
if !g.Wizard {
|
||||
g.msg("sorry")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Wizard = false
|
||||
// Re-hide the monsters wizard sight was showing: without this there
|
||||
// is no way back to normal visibility.
|
||||
g.turnSee(true)
|
||||
g.msg("not wizard any more")
|
||||
}
|
||||
|
||||
// wizardCommand handles the MASTER debug commands (command.c).
|
||||
func (g *RogueGame) wizardCommand(ch byte) {
|
||||
p := &g.Player
|
||||
|
||||
@@ -802,6 +802,7 @@ func newGameData() *gameData {
|
||||
g.After = false // "legal" illegal command
|
||||
},
|
||||
'^': (*RogueGame).identifyTrapCommand,
|
||||
'+': (*RogueGame).wizardToggleCommand,
|
||||
Escape: func(g *RogueGame) {
|
||||
g.DoorStop = false
|
||||
g.Count = 0
|
||||
|
||||
@@ -424,3 +424,108 @@ func TestWhichLimitCoversEveryIndexedTable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWizardToggleOffRehidesSensedMonsters drives '+' through command
|
||||
// dispatch in wizard mode (command.c 317-338). Clearing the flag is the
|
||||
// cheap half; the substantive half is turn_see(TRUE) — leaving wizard
|
||||
// mode has to put the screen back, or there is no way out of wizard
|
||||
// sight once it is on.
|
||||
func TestWizardToggleOffRehidesSensedMonsters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 11)
|
||||
g.Wizard = true
|
||||
|
||||
// A phantom carries ISINVIS straight from the monster table, so
|
||||
// seeMonst is false for it and it is on screen only because wizard
|
||||
// sight put it there.
|
||||
tp := spawnAdjacent(g, 'P')
|
||||
if !tp.On(Invisible) {
|
||||
t.Fatal("phantom is not invisible; this test needs an unseeable monster")
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
t.Fatal("monster is ordinarily visible; wizard sight would reveal nothing")
|
||||
}
|
||||
|
||||
if tp.OldCh == tp.Type {
|
||||
t.Fatalf("map char under the monster is also %q; the redraw "+
|
||||
"assertion would prove nothing", tp.Type)
|
||||
}
|
||||
|
||||
g.turnSee(false)
|
||||
|
||||
if !g.Player.On(SenseMonsters) {
|
||||
t.Fatal("turnSee(false) did not set SenseMonsters")
|
||||
}
|
||||
|
||||
if ch := g.mvinch(tp.Pos.Y, tp.Pos.X); ch != tp.Type {
|
||||
t.Fatalf("wizard sight did not draw the monster: cell is %q, want %q",
|
||||
ch, tp.Type)
|
||||
}
|
||||
|
||||
if !g.scr.Std.at(tp.Pos.Y, tp.Pos.X).standout {
|
||||
t.Fatal("wizard-sighted monster was not drawn in standout")
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
g.After = true
|
||||
|
||||
g.dispatch('+')
|
||||
|
||||
if g.Wizard {
|
||||
t.Error("'+' did not clear the wizard flag")
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
t.Error("'+' left SenseMonsters set: turn_see(TRUE) was not performed")
|
||||
}
|
||||
|
||||
if ch := g.mvinch(tp.Pos.Y, tp.Pos.X); ch != tp.OldCh {
|
||||
t.Errorf("monster still on screen after leaving wizard mode: cell is "+
|
||||
"%q, want the map char under it, %q", ch, tp.OldCh)
|
||||
}
|
||||
|
||||
if g.scr.Std.at(tp.Pos.Y, tp.Pos.X).standout {
|
||||
t.Error("cell left in standout after leaving wizard mode")
|
||||
}
|
||||
|
||||
if g.Msgs.Huh != "not wizard any more" {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, "not wizard any more")
|
||||
}
|
||||
|
||||
if g.After {
|
||||
t.Error("'+' consumed a turn; C sets after = FALSE")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWizardToggleWithoutWizardSaysSorry pins the other arm. C ran
|
||||
// wizard = passwd() and said "sorry" when the answer was wrong; the
|
||||
// password machinery is dropped, so that is the only outcome left. What
|
||||
// it must not be any more is "illegal command '+'".
|
||||
func TestWizardToggleWithoutWizardSaysSorry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 12)
|
||||
g.Wizard = false
|
||||
g.Msgs.Mpos = 0
|
||||
g.After = true
|
||||
|
||||
g.dispatch('+')
|
||||
|
||||
if g.Wizard {
|
||||
t.Error("'+' entered wizard mode with no password check to pass")
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
t.Error("'+' turned on monster sense outside wizard mode")
|
||||
}
|
||||
|
||||
if g.Msgs.Huh != "sorry" {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, "sorry")
|
||||
}
|
||||
|
||||
if g.After {
|
||||
t.Error("'+' consumed a turn; C sets after = FALSE")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user