Commit Graph

14 Commits

Author SHA1 Message Date
c95f98ffe5 Port the '+' wizard-mode toggle-off (closes #11)
C's command.c 317-338 has a `when '+'` arm in the main command switch,
under #ifdef MASTER, that toggles wizard mode. The port had no '+' at
all, so the key fell through dispatchKey's default to illcom and
answered "illegal command '+'".

The password half of that arm was dropped deliberately (wizard mode is
ROGUE_WIZARD configuration) and is recorded in ARCHITECTURE.md section
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) rather than the flag -- wizard sight draws every monster
the hero cannot see, so without the re-hide there is no way back to
normal visibility, and clearing the flag alone would leave the screen
lying.

New wizardToggleCommand, registered in commandHandlers between '^' and
Escape, which is C's own switch order. Because C's arm sits in the main
switch rather than the `if (wizard) switch (ch)` sub-switch that
wizardCommand ports, it is reachable whether or not wizard is set, so
the non-wizard case was a divergence too. 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", with 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 doc
comment and in section 9.

Two tests 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, then asserts the flag cleared, SenseMonsters cleared, the
cell restored to the map char under the monster with standout off, the
exact message text, and After false. Deleting the turnSee(true) call
fails it on all three visibility assertions. The other pins "sorry".

No RNG call is added: the turn_off arm of turn_see never reaches rnd.
TestSeedCompatItemTables is green against the untouched golden.
2026-08-09 08:22:05 +00:00
clawbot
3a01283358 fix: take the signal-time autosave on the game goroutine (closes #24)
The SIGHUP/SIGTERM handler gob-encoded the live game tree from the signal
goroutine while the game goroutine was mid-turn mutating it, and AutoSave
removed the save file before encoding — so the failure mode was not a
stale save but a deleted one followed by a possibly torn replacement,
with a window in which the player had neither. The suite has run under
-race since 2026-08-09 and was green because nothing had ever driven the
turn loop concurrently with a signal: evidence of untested, not of safe.

The handler no longer writes anything. AutoSaveOnSignal posts a request,
wakes the input read, and waits up to signalSaveTimeout for the game
goroutine to take it; the encode runs on the goroutine that owns the
state, at the three points where that goroutine can sit: between turns
(command), on waking from a blocked readchar, and while parked in the `!`
shell escape (runShellEscape, which now runs the shell on a helper
goroutine so a hangup during it still rescues the game).

Blocked on input is the case that matters — a dropped connection lands
while the player is thinking, so a flag checked only between turns would
never be looked at. Terminal.ReadChar therefore returns (byte, bool),
with ok false meaning "woken by Interrupt, no key", and term.Tcell posts
a tcell.EventInterrupt onto tcell's own event queue to unpark PollEvent.
readchar services the request and reads again, so no caller sees it.

Running the shell on a helper goroutine would also have moved
term.Tcell.ShellEscape's panic on a failed Screen.Resume onto it, and a
panic at the top of any goroutine terminates the process without running
the deferred calls of the others — including cmd/rogue/main.go's
`defer t.Fini()`. The tty would have been left raw on precisely the path
where the terminal is already broken, which is issue #12's failure on a
path this change created. runShellEscape therefore recovers the helper's
panic and re-raises it on the game goroutine, whose stack has the restore
in it, so "every path restores the terminal via Terminal.Fini before
exiting" stays true.

saveFile writes a temporary file in the save's own directory, fsyncs it
and renames it over the target instead of truncating in place, so a save
that fails — or never happens because the deadline ran out — leaves the
player's previous save whole.

What the handoff guarantees is stated exactly rather than flatteringly:
the encode runs on the state-owning goroutine, so the snapshot is
internally consistent and restorable, but it is not necessarily taken
between commands. Only the check at the top of command is; the other two
service points both sit inside a command call already under way. readchar
is reached from mid-command prompts (--More--, askOverwrite, getStr, the
direction and pack prompts) with the command's mutations already applied,
and runShellEscape is reached from shell, an ordinary '!' command handler
dispatched inside command, with that turn's DoDaemons(Before) and
DoFuses(Before) already fired and its AFTER pass not yet. Restoring
re-enters playit at the top of command, so either way the rest of that
command is lost and a fresh BEFORE pass runs on top of the one in the
snapshot.

The SIGINT/SIGQUIT no-save decision and the single-signal-read ordering
guarantee are untouched. pendingSaver reads the game out from under its
mutex rather than delegating with it held, because the delegated call now
blocks until the save is taken.
2026-08-09 07:50:23 +00:00
63d1e797e2 chore(lint): adopt canonical golangci-lint config
Replace .golangci.yml with the shared canonical config. The old
config's top-level linters-settings block was silently ignored under
the v2 schema, so the lll/funlen/cyclop/dupl thresholds now actually
apply. The four repo-specific disables (mnd, exhaustive, paralleltest,
testpackage) move out of the config into targeted in-code nolint
directives carrying their original approval dates, keeping the config
byte-identical to the canonical one.

Fixes surfaced by the stricter settings: t.Parallel() added to all 32
tests, 24 overlong lines wrapped or their comments tightened, tcell
control-code returns rewritten as character literals, dupl markers on
the identically-shaped item data tables, and two wsl_v5 defer cuddles.

No behavior changes. The repo has no golangci-lint version pin (no
Dockerfile or CI; make lint runs the host binary, currently v2.12.2),
so there was nothing to bump.
2026-08-07 20:45:02 +00:00
aa57349c34 Decompose command.go; command keys become a handler table (step 7)
The ordinary command keys move into gameData.commandHandlers (method
expressions and small literals); dispatchKey keeps only re-dispatching
prefixes (runCommand/fightCommand/repeatCommand/moveOnCommand) and the
wizard fallthrough. command() splits into playTurn/turnUpkeep/
readCommand/executeCommand/countPrefix/ringTurnEffects; search gains
searchSpot/searchFloor; help gains helpOne/helpAll/helpLines; call
gains callTarget/callPrelude; wizardCommand splits in two plus
wizardKit; uLevel and current flatten to early returns. command.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:15:27 +02:00
f432c8718c Rename getDir to promptDirection; rotate TODO (step 5 done) 2026-07-07 02:34:32 +02:00
6d798c56ed Rename item-subsystem methods (refactor step 5, items)
getItem→promptPackItem now returns (obj, ok) instead of a nil-signaling
pointer; invName→inventoryName; doPot→applyPotionFuse. C breadcrumbs
kept. Behavior unchanged; suite green.
2026-07-07 02:31:38 +02:00
6850c87ae7 Rename movement/world methods to idiomatic Go; remove all gotos
Refactor step 4. Renames (C breadcrumbs kept in doc comments):
doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms→digRooms, doPassages→digPassages, doMaze→digMaze,
chgStr→changeStrength, doRun→startRun, moveStuff→finishMove,
turnref→turnRefresh, moveMonst→moveMonster, doChase→chaseStep,
setOldch→setOldChar, cansee→canSee, roomin→roomIn, runto→runTo,
conn→connectRooms, putpass→putPassage, passnum→numberPassages,
numpass→numberPassage, rndPos→randomPos, rndRoom→randomRoom,
treasRoom→treasureRoom, accntMaze→accountMaze.

All goto/label flows are gone: moveHero uses a retry loop with the
PASSGO corner logic extracted into passageTurn; dispatch re-dispatches
via a loop; chaseStep re-checks via a loop; saveGame uses a labeled
prompt loop. Control flow and RNG call order are unchanged; suite
green.
2026-07-07 02:29:06 +02:00
a8feb6c05d Move all package-level vars into gameData; finish lint adoption
gochecknoglobals: all 37 package-level tables consolidated into the
gameData struct (game/tables.go), built by newGameData() and carried
on RogueGame as g.data (set in NewGame and Restore). ObjectKind
Glyph()/objectKindForGlyph are now switches; the table-reading subtype
Stringer methods are gone; isMagic is a RogueGame method.

goconst: repeated words named (potionName/scrollName/ringName/goldName
in object.go, wandName/staffName in sticks.go, ripWall in tables.go).

exhaustive, testpackage: disabled in .golangci.yml with sneak's
approval (2026-07-07).

Also reverts misspell's silent corruption of the "ther" scroll-name
syllable (it had become "there", changing generated scroll names vs C).

Remaining red: cyclop/gocognit/nestif until refactor step 7; mnd
awaits a ruling. TODO.md rotated; MEMORY.md lint notes updated.
2026-07-07 02:10:58 +02:00
5ba9fe8f66 Adopt house golangci-lint config; fix all approved-linter findings
.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:

- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
  the misspell autofix REVERTED where it rewrote authentic C game text
  ("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
  closes explicitly and removes corrupt saves, scoreboard writes are
  documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
  ErrScreenTooSmall); panics allowed for unrecoverable states per
  MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
  per-line nolints for provably-bounded conversions (randomMonsterLetter
  helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
  (recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
  (goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
  always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
  inventory filter untangled into matchesFilter, main.go split so
  defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods

Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
2026-07-07 00:03:45 +02:00
2eff377a73 Un-overload Object fields and pre-parse damage dice (refactor step 3)
Object.Arm carried four meanings in C (o_arm/o_charges/o_goldval plus
ring bonuses); it is now four fields: ArmorClass, Charges, GoldValue,
and Bonus. Stats.Arm becomes Stats.ArmorClass. The Charges()/GoldVal()
accessor pair is gone.

Damage dice strings ("1x4/1x2") are parsed once into DiceSpec — at
table definition for the bestiary and weapon tables, at creation for
items — instead of re-parsed with atoi on every swing as fight.c
roll_em did. ParseDice preserves the C parse semantics exactly,
including the junk-tolerant "%%%x0" bestiary placeholder and the
"000x0" flytrap reset (regression-tested in dice_test.go); the
flytrap's growing grip becomes DiceSpec{{VfHit, 1}}.

Save format bumps to 5.4.4-go3 (field renames and retypes would
silently zero under gob's match-by-name decoding).

No behavior change; full suite green.
2026-07-06 23:07:57 +02:00
b940cfc41f Give item categories and subtypes real types (refactor step 2)
ObjectKind separates what an item is from how it draws: Object.Type
(a byte that doubled as the map character) becomes Kind ObjectKind,
with Glyph() producing the display character and objectKindForGlyph
converting back at the map boundary. KindWand covers the C 'stick'
category. The magic-missile bolt uses KindGold, matching C's literal
o_type='*' trick, with a comment.

PotionKind, ScrollKind, RingKind, WandKind, WeaponKind, ArmorKind and
TrapKind are now iota enums with Stringer (names come from the base
info tables); Object gains typed accessors (obj.RingKind(), ...) and
Launch is typed WeaponKind. beTrapped returns TrapKind. The
getItem/inventory/whatis prompt filters take ObjectKind, with
KindCallable/KindRingOrStick replacing the C CALLABLE/R_OR_S
sentinels; wizard.c's type_name table is subsumed by
ObjectKind.String(). IsRing/IsWearing/initWeapon/doPot signatures are
typed accordingly.

The save format version becomes 5.4.4-go2: the gob field rename would
otherwise silently zero Kind when reading old saves.

No behavior change; full suite green.
2026-07-06 22:00:43 +02:00
e71a2ef3fd Rename constants to descriptive names (refactor step 1)
Pure rename, no behavior change; the full suite (RNG goldens,
generation invariants, scripted sessions, save round trip) is
unchanged and green.

- Creature flags: IsHuh→Confused, IsHalu→Hallucinating,
  CanHuh→CanConfuse, CanSee→CanSeeInvisible, IsRun→Awake,
  SeeMonst→SenseMonsters, IsCanc→Cancelled, IsLevit→Levitating,
  IsBlind/IsGreed/IsHaste/IsTarget/IsHeld/IsInvis/IsMean/IsRegen/
  IsFly/IsSlow → Blind/Greedy/Hasted/Targeted/Held/Invisible/Mean/
  Regenerates/Flying/Slowed, IsFound→Found
- Object flags: IsCursed→Cursed, IsKnow→Known, IsMissl→Missile,
  IsMany→Stackable, ObjIsFound→WasFound, IsProt→Protected
- Room flags: IsDark/IsGone/IsMaze → Dark/Gone/Maze; place flags:
  FPass→FPassage, FPNum→FPassNum, FTMask→FTrapMask
- Trap types: TDoor→TrapDoor ... TMyst→TrapMystery
- Item subtypes: P*→Potion* (PLSD→PotionLSD, PMFind→
  PotionDetectMonsters, ...), S*→Scroll* (SIDRorS→
  ScrollIdentifyRingOrStick, ...), R*→Ring* (RAddHit→RingDexterity,
  RNop→RingAdornment, ...), Ws*→Wand* (WsElect→WandLightning, ...),
  weapons (TwoSword→WeaponTwoHandedSword, Shiraken→WeaponShuriken,
  ...), armor (RingMail→ArmorRingMail, ...)
- Counts: Max{Potions,Scrolls,Rings,Sticks,Weapons,Armors} →
  Num{Potion,Scroll,Ring,Wand,Weapon,Armor}Types; NTraps→NumTrapTypes
- Level.NTraps field → TrapCount (also in the save snapshot)
- Original C constant names preserved as comment breadcrumbs in
  types.go so the lineage stays greppable

TODO.md: step 1 moved to Completed, step 2 (typed kinds) promoted to
Next Step.
2026-07-06 21:28:57 +02:00
c0b533ed2f Make the rgoue branch Go-only
The port is complete, so the C sources and their build system
(autoconf/automake inputs, Makefiles, Visual Studio project, doc
templates, RPM spec, C build/modernization notes) leave this branch;
they live on in master/modern-rogue for reference.

Also ports the last missing wizard command (things.c pr_list/pr_spec,
the '*' item-probability listing), rewrites README.md for the Go port,
and updates ARCHITECTURE.md's layout notes (game/ has no third-party
imports; tcell lives in term/).

Kept: LICENSE.TXT, rogue.png, rogue.desktop, ARCHITECTURE.md.
2026-07-06 20:19:33 +02:00
41fc1042fc go: port command loop, save/restore, tcell terminal, and the binary
- command.c in full: the command dispatcher with repeat counts, run
  prefixes, ctrl-run door-stop mode, fight-to-death targeting, and all
  wizard debug commands; search, help, identify, d_level/u_level, call,
  current
- main.c completed: Run()/playit() with the C double ROGUEOPTS parse;
  exit()-anywhere becomes a gameEnd panic recovered in Run
- save.c + state.c: gob SaveState snapshot with explicit pointer-to-
  index fixups for equipment, monster rooms, and chase targets (the
  rs_fix_thing role); save file deleted on restore as in C; SIGHUP/
  SIGTERM autosave hook
- wizard.c completed (create_obj, show_map), passages.c add_pass
- term/: tcell/v2 Terminal — the one third-party dependency — replacing
  curses and mdport's 900-line key decoder; shell escape via suspend
- cmd/rogue: flags -s/-d, SEED (wizard), ROGUEOPTS, ROGUE_WIZARD

Tests: full scripted sessions through Run (quit, descend stairs,
3200-move crash sweep, save-command round trip). Notable fixes found
by tests: gob silently drops embedded fields of unexported types, and
--More-- prompts swallow space-free input scripts.
2026-07-06 20:15:47 +02:00