64 Commits

Author SHA1 Message Date
8ce238dd62 Merge seed-compat (item-table cross-validation vs C reference) 2026-07-24 03:05:39 +07:00
c30da22e43 Rotate TODO to coverage-broadening step (seed-compat item tables done) 2026-07-24 03:05:39 +07:00
e595b87718 Cross-validate item appearance tables against the C reference
Instrumented the C game on modern-rogue with a DUMP mode (patch in
testdata/c_seedcompat.patch) that forces the RNG seed and prints the
per-seed item appearance tables — potion colors, scroll names, ring
stones, wand/staff materials — in the normal init order, before initscr
so no terminal is needed. Captured its output for four seeds as
testdata/item_tables.golden.

TestSeedCompatItemTables regenerates the same tables from the Go port
via New(Params{Seed, Wizard: true}) and checks they match the golden
byte for byte. They do, for all four seeds — proving the LCG and its
consumption order through the whole init sequence (init_probs →
init_player → init_names → init_colors → init_stones → init_materials,
including init_player's arrow rnd(8)+rnd(15)) agree with C exactly.

testdata/README.md documents how to regenerate the golden.
2026-07-24 03:05:01 +07:00
11223caa7c Merge playtest-hardening (deep playthrough + crash sweep tests) 2026-07-23 08:59:03 +07:00
e7e1bc3c40 Rotate TODO to seed-verification step (playtest hardening done) 2026-07-23 08:59:03 +07:00
061da11877 Add deep-playthrough and turn-loop crash-sweep tests (playtest hardening)
Two death-safe regression drives that exercise the full turn loop within the
step-8 os.Exit constraint (a fortify() helper pins HP/food/exp and clears the
freeze/stuck counters each turn, so no death exits the test binary; fixed seeds
keep them deterministic):

- TestDeepPlaythrough: quaff/read/zap through command dispatch, then descend the
  staircase to depth 8 with a save/restore at depth 4 — a crash sweep of deep
  level generation, item effects, and mid-game save/restore. It asserts the
  consumables identify themselves (the commands really ran) and the descent and
  restore land where expected.
- TestTurnLoopCrashSweep: mash movement/search/rest for 200 turns on four seeds,
  exercising combat, monster AI, and traps.

Neither surfaced a panic. Space-separated command scripts answer the --More--
prompts, as wait_for consumes input up to a space.
2026-07-23 08:58:05 +07:00
0e6ed41351 Merge docs-refresh (ARCHITECTURE.md Part 2 + rename table) 2026-07-23 08:40:55 +07:00
b431af8b74 Rotate TODO to playtest step (docs refresh done) 2026-07-23 08:40:55 +07:00
cb1e302102 Refresh ARCHITECTURE.md Part 2 for the post-refactor design
Part 2 was written as a design sketch before the port was implemented and
refactored, so much of it described the planned code rather than the final
code. Updated the RogueGame/Stats/Object/Flags/Level sketches to the current
names and types (typed ObjectKind, DiceSpec, split o_arm fields, step-1 flag
names, TrapCount, Level list methods); rewrote §4.7 to say the static tables
now live on the per-game gameData struct (no package globals); noted the
daemon and effect handler tables (step 7), the MessageLine extraction (step 6),
the Terminal interface, the flat gob SaveState, and the New(Params) +
os.Exit-on-game-over design (step 8). Added §7.1, a C-name → Go-name rename
table, and a README note about the make targets. Docs only.
2026-07-23 08:40:15 +07:00
bcdfaf4ab4 Merge refactor/constructor-style (refactor step 8: New/Params + os.Exit) 2026-07-23 08:02:40 +07:00
a7d27ef65f Rotate TODO to docs-refresh step (step 8 done)
Step 8 complete: New(Params) constructor and os.Exit game-over. The
77-column wrap sweep was dropped per sneak. Docs refresh is now Next
Step.
2026-07-23 08:02:35 +07:00
194ce1dd16 Exit the process on game-over instead of unwinding a panic
One game run is one process, so game-over ends the process directly,
as the C game did with exit(). myExit now restores the terminal
(via the new Terminal.Fini) and calls os.Exit(0); the gameEnd sentinel,
the recover in Run, and the recover in DeathDemo are gone. Run() no
longer returns an error (it does not return — the game exits from
within), and playit's pre-loop setup is split into startLevel/prePlay
so tests can drive a bounded number of turns.

Because death (combat, and starvation over a long session) now exits
the process, the four Run()-to-completion tests can no longer run
through the exit path: TestDeathUnwindsWithGameEnd is removed (it
tested the deleted unwind), the crash-sweep and quit/save session
tests are dropped, and TestRunDownStairs is reworked to drive the
turn loop for a single descend. Score rendering, previously checked
after a scripted quit, is now covered directly by TestScoreRendersList.
Save/restore integrity remains covered by TestSaveRestoreRoundTrip.
2026-07-23 06:44:39 +07:00
cd0ba6c8ee Rename constructor to game.New(game.Params) per styleguide
NewGame(Config) becomes New(Params), and Restore takes Params too, so
the package's primary type gets the canonical New() constructor with a
named-field Params struct (styleguide points 139, 159). cmd/rogue and
all tests updated; ARCHITECTURE.md constructor references corrected.
Pure rename, suite green.
2026-07-23 05:39:31 +07:00
8241cf4bee Merge add-make-targets (dev Makefile + markdown formatting) 2026-07-22 22:20:45 +07:00
35b538e888 Add dev Makefile; format all markdown with prettier
Adds a minimal Makefile wrapping the toolchain the way sneak's other
repos expose it:

- fmt        gofmt -w plus prettier (4-space tabs, proseWrap: always)
- fmt-check  fail if any Go or Markdown file is unformatted
- lint       golangci-lint run ./...
- test       go test ./...
- check      fmt-check + lint + test (the local pre-commit gate)

Running make fmt normalizes the four existing Markdown docs to the
shared prettier style (80-column proseWrap: always, aligned tables) —
a one-time reflow with no content change. The repo remains exempt from
the rest of the policy scaffold (no Dockerfile, CI, or REPO_POLICIES).
2026-07-22 22:20:34 +07:00
88f18fc635 Merge refactor/effects-dispatch (refactor step 7) 2026-07-22 22:18:11 +07:00
ad098f9d99 Rotate TODO to step 8; note complexity linters clean (step 7 done)
Refactor step 7 is complete: the whole golangci-lint run is 0 issues.
MEMORY.md updated to reflect the complexity linters are enabled and
clean.
2026-07-22 22:18:11 +07:00
5b7e258195 Decompose test functions to clear complexity findings (step 7)
TestSaveRestoreRoundTrip, TestNewGameRandomizesAppearances, and
TestNewLevelInvariants split their assertion blocks into t.Helper()
sub-checks. The lint run is now completely clean (0 issues).
2026-07-22 22:11:10 +07:00
5c14a829aa Decompose tcell ReadChar key translation (refactor step 7)
ReadChar's key switch moves into translateKey with namedKey split into
motionKey/editingKey halves. term is complexity-clean. Behavior
unchanged.
2026-07-22 22:08:59 +07:00
b68836dde0 Decompose wizard.go createObj and whatis (refactor step 7)
createObj gains createWeaponArmor/createRing; whatis gains whatisPick
for its prompt loop. wizard.go is complexity-clean. Behavior unchanged.
2026-07-22 22:07:31 +07:00
730d91d160 Decompose ObjectKind.String and doMotion (refactor step 7)
String's tail moves into stringRest; doMotion's erase step moves into
eraseFlight and the inverted loop drops a nesting level. Behavior
unchanged.
2026-07-22 22:06:48 +07:00
71713d68b7 Decompose io.go End and status (refactor step 7)
End's --More-- handling moves into promptMore; status's redraw-skip
check moves into statusUnchanged. io.go is complexity-clean. Behavior
unchanged.
2026-07-22 22:05:06 +07:00
444bc30f2c Decompose ringOn and totalWinner (refactor step 7)
ringOn gains pickRingHand and chooseTerse messages; totalWinner's
appraisal switch becomes objectWorth with loreWorth/ringWorth/
wandWorth. Behavior unchanged.
2026-07-22 22:03:31 +07:00
ff7ee95395 Finish score/wakeMonster tidy (scoreSlot; drop unused return) 2026-07-22 22:02:02 +07:00
8895db530d Decompose score and wakeMonster (refactor step 7)
score splits into scoreInsert/scoreLines/showScores; wakeMonster gains
meanWakes/medusaCatches/medusaGaze predicates and effect. Behavior and
RNG call order unchanged.
2026-07-22 22:01:03 +07:00
3e1c30c787 Flatten digWallGap 2026-07-22 21:59:44 +07:00
432ea4f019 Decompose rooms.go (refactor step 7)
digRooms splits into digRoom/placeGoneRoom/placeMazeRoom/
placeNormalRoom/roomGold/roomMonster; dig gains digPick/digWallGap;
findFloorImpl gains floorChar; enterRoom and leaveRoom gain per-cell
helpers. rooms.go is complexity-clean. Behavior and RNG call order
unchanged.
2026-07-22 21:59:24 +07:00
bac9e361bc Decompose daemons.go; daemon dispatch becomes a table (step 7)
runDaemon's switch becomes gameData.daemonHandlers (the C d_func
function pointers restored as method expressions); stomach splits into
stomachFaint/stomachDigest; visuals gains visualMonsters. daemons.go
is complexity-clean. Behavior and RNG call order unchanged.
2026-07-22 21:57:22 +07:00
9083967ed3 Split applyHeader turn-state half; drop inline stat err 2026-07-22 21:55:04 +07:00
80484bcd31 Finish save.go split: header/player halves, drop inline err 2026-07-22 21:54:41 +07:00
43b4fbe746 Decompose save.go (refactor step 7)
saveGame splits into askDefaultSave/saveFileName/saveCheckOverwrite/
askOverwrite around a saveAnswer tri-state; snapshot gains destRefFor;
applySnapshot gains applyMonsters/applyDests. save.go is
complexity-clean. Behavior unchanged.
2026-07-22 21:53:02 +07:00
389db14bbf Finish addLine split (addLinePageBreak) 2026-07-22 21:51:06 +07:00
e1f065e783 Decompose things.go (refactor step 7)
inventoryName splits into nameScroll/nameFood/nameWeapon/nameArmor/
describeWorn/fixNameCase; newThing gains newFoodThing/newWeaponThing/
newArmorThing/newRingThing; dropCheck gains dropRing; addLine splits
into addLineSlow/addLinePaged/addLineOverlay. things.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-22 21:50:44 +07:00
0b798c9c82 Tidy pack.go split: drop unused lp param and named returns 2026-07-07 03:31:53 +02:00
0274460e62 Decompose pack.go (refactor step 7)
addPack splits into pickupScareScroll and packInsert with
packScanKind/packScanWhich/packMatch/packMatchGroup for the C
linked-list walk; promptPackItem gains repeatLastItem and
promptItemPurpose; inventory's empty-handed messages flatten via
chooseTerse. pack.go is complexity-clean. Behavior unchanged.
2026-07-07 03:31:23 +02:00
c6dae3cf3d Finish getStr split (getStrResult) 2026-07-07 03:28:30 +02:00
5849dddcf0 Decompose options.go (refactor step 7)
ParseOpts splits into parseOptName/parseOptValue/parseInvType; getStr
gains endsInput/getStrErase/getStrEdit. options.go is complexity-clean.
Behavior unchanged.
2026-07-07 03:27:56 +02:00
cc2efb86e8 Decompose passages.go (refactor step 7)
digPassages gains pickNeighbor; connectRooms splits into
connOrient/connPlanDown/connPlanRight/connEnd/digCorridor around a
corridorPlan struct; addPass gains addPassSpot; the shared door/
secret-door predicate becomes hiddenExit. passages.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:25:46 +02:00
ea68df32f0 Decompose move.go; traps become a handler table (refactor step 7)
The be_trapped switch becomes gameData.trapHandlers with one trap*
method per trap kind (mystery messages split in two); moveHero splits
into moveTarget/moveResolve/moveEnter/moveOnto/offMap; passageTurn
gains per-axis passageTurnVertical/Horizontal. move.go is
complexity-clean. Behavior and RNG call order unchanged.
2026-07-07 03:22:54 +02:00
fec79b939a Decompose chase.go (refactor step 7)
chase splits into chaseBestSpot/chaseTry/scareScrollAt with a
chaseSearch state struct; chaseStep gains chaseRooms, chaseGoal,
dragonBreath/dragonShoots, and chaseTakeObject; runners gains
runnerTurn; findDest gains objectClaimed. chase.go is complexity-clean.
Behavior and RNG call order unchanged.
2026-07-07 03:19:31 +02: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
4a248eb392 Decompose look and promptDirection (refactor step 7)
look's nine-square scan splits into lookAround/lookCell with a
lookScan state struct and guard helpers (lookSkips,
lookForeignPassage, lookDiagonalBlocked, lookCellChar, lookShow,
lookRunCheck, atRunEdge); promptDirection gains deltaFor and
confuseDirection. misc.go is complexity-clean. Behavior and RNG call
order unchanged.
2026-07-07 03:03:50 +02:00
a20f500655 Decompose fight.go: hit-handler table, weapon/armor helpers (step 7)
The monster special-power switch in attack becomes
gameData.hitHandlers (indexed by monster letter); attack splits into
monsterHit/monsterMiss; fight gains revealXeroc and heroHits;
rollAttacks gains weaponAttack, wieldedRingBonus, and defenderArmor;
killed gains killedSpecial. fight.go is complexity-clean. Behavior and
RNG call order unchanged.
2026-07-07 02:58:44 +02:00
ebe477ba28 Decompose remaining effects-file hot spots (refactor step 7)
drain gains drainReaches; zapSpeed gains hasteTarget/slowTarget;
readHoldMonster gains holdMonstersNear; readCreateMonster gains
createMonsterSpot; revealSpot splits into revealChar/revealWall/
revealSolid/revealFloor; turnSee gains showSensed. potions.go,
scrolls.go, and sticks.go are complexity-clean. Behavior and RNG call
order unchanged.
2026-07-07 02:53:09 +02:00
1a25beead8 Decompose fireBolt (refactor step 7)
The fire_bolt loop splits into boltDirChar, boltBounces,
boltStrikesMonster, and boltStrikesHero; loop state (hitHero/changed/
used) stays in fireBolt. Effect order and RNG calls unchanged.
2026-07-07 02:49:37 +02:00
8e2915f60d Convert doZap to a per-wand handler table (refactor step 7)
The do_zap switch becomes gameData.zapHandlers, indexed by WandKind;
the shared monster-ray preamble is zapRayMonster/zapVictim, teleport
away/to and the three bolt wands share handlers, and a false return
aborts the zap without spending a charge (drain life on a too-weak
hero, as in C). Effect order and RNG call sequence unchanged.
2026-07-07 02:48:01 +02:00
3047f729aa Convert readScroll to a per-scroll handler table (refactor step 7)
The read_scroll switch becomes gameData.readHandlers, indexed by
ScrollKind; the five identify scrolls share one handler. The magic
mapping cell logic is extracted into revealSpot. Effect order and RNG
call sequence unchanged.
2026-07-07 02:46:09 +02:00
cc025eb808 Convert quaff to a per-potion handler table (refactor step 7)
The quaff switch becomes gameData.quaffHandlers, a method-expression
table indexed by PotionKind; each case body moved verbatim into a
quaff* method. Effect order and RNG call sequence unchanged.
2026-07-07 02:44:33 +02:00
acef593288 Merge refactor/god-object-extraction (refactor step 6) 2026-07-07 02:42:18 +02:00
0b56ac8019 Extract MessageLine, Player pack ops, and Level list management
Refactor step 6. MessageLine (was MsgLine) owns the msg/addmsg/endmsg
machinery, wired to its screen, pre---More-- redraw, and input via
attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so
the ~400 call sites are unchanged. Player gains nextPackChar and
removeFromPack (the state half of pack.c leave_pack); leavePack keeps
only the LastPick repeat-command tracking. Level gains ObjectAt
(misc.c find_obj) and AddObject/RemoveObject/AddMonster/RemoveMonster,
replacing direct attach/detach calls on the level lists. Inventory and
pickup UI flows stay on RogueGame: display and orchestration, not
state surgery. Behavior and RNG order unchanged; suite green.
2026-07-07 02:42:18 +02:00
a094f7c6c3 Merge refactor/fix-nonamedreturns 2026-07-07 02:35:18 +02:00
0caaa14198 Drop unused named return on moveMonster (nonamedreturns) 2026-07-07 02:35:18 +02:00
d3ef07cfa7 Merge refactor/item-combat-ui-renames (refactor step 5) 2026-07-07 02:34:32 +02:00
f432c8718c Rename getDir to promptDirection; rotate TODO (step 5 done) 2026-07-07 02:34:32 +02:00
ae79fd5e84 Rename combat methods; -1 status codes become named bool results
Refactor step 5, combat: rollEm→rollAttacks; attack, moveMonster, and
chaseStep return (removed bool) instead of the C -1/0 int codes.
Behavior unchanged; suite green.
2026-07-07 02:33:15 +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
0554f5d4f1 Merge refactor/movement-renames (refactor step 4) 2026-07-07 02:29:06 +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
65a1cd68b8 Merge refactor/drop-unused-nolints 2026-07-07 02:17:25 +02:00
525465a68b Drop two unused gosec nolint directives in chooseSeed
The 0x7fffffff mask makes both int32 conversions provably safe, so
G115 never fired; nolintlint flags the directives as unused.
2026-07-07 02:17:25 +02:00
a49d857970 Merge refactor/lint-mnd-disable (mnd disabled per approval) 2026-07-07 02:16:38 +02:00
32067eb318 Disable mnd with sneak's approval (2026-07-07)
The 289 findings are C-faithful gameplay literals (probability rolls,
damage spreads, screen coordinates); naming them would invent constants
the C never had and hurt greppability against the reference sources.
2026-07-07 02:16:38 +02:00
d6aa74d9f1 Merge refactor/no-package-globals (all globals into gameData, lint adoption done) 2026-07-07 02:11:04 +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
52 changed files with 7891 additions and 5383 deletions

View File

@@ -16,6 +16,10 @@ linters:
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
# Repo-specific exceptions approved by sneak (2026-07-06) # Repo-specific exceptions approved by sneak (2026-07-06)
- paralleltest # Requires t.Parallel() in every test - paralleltest # Requires t.Parallel() in every test
# Approved by sneak 2026-07-07
- testpackage # Tests use internal package game to reach unexported state
- exhaustive # C-faithful switches handle only the cases C handled
- mnd # C-faithful gameplay literals; naming them hurts C-greppability
linters-settings: linters-settings:
lll: lll:

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,41 @@
# Project Memory # Project Memory
Working notes for agents on this repo. Read this alongside TODO.md Working notes for agents on this repo. Read this alongside TODO.md (which holds
(which holds the step queue and workflow) before starting work. the step queue and workflow) before starting work.
## Error handling ## Error handling
Panicking on bad/unexpected errors is allowed and preferred over Panicking on bad/unexpected errors is allowed and preferred over threading
threading unlikely error returns through game code — e.g. write-side unlikely error returns through game code — e.g. write-side Close/encode failures
Close/encode failures where continuing would mean corrupt state. The where continuing would mean corrupt state. The game already unwinds C's exit()
game already unwinds C's exit() calls via a gameEnd panic recovered in calls via a gameEnd panic recovered in Run. Return errors where a caller
Run. Return errors where a caller genuinely handles them (save-file genuinely handles them (save-file prompts, restore validation). Reserve
prompts, restore validation). Reserve deliberate `_ =` discards for deliberate `_ =` discards for true best-effort paths (scorefile writes,
true best-effort paths (scorefile writes, signal-time autosave), always signal-time autosave), always with a comment saying why.
with a comment saying why.
## Linting ## Linting
The .golangci.yml is the house standard and may only be modified with The .golangci.yml is the house standard and may only be modified with sneak's
sneak's explicit permission. To disable a linter, ask, explaining what explicit permission. To disable a linter, ask, explaining what the linter does;
the linter does; he approves specific exceptions, which are recorded he approves specific exceptions, which are recorded in the config's
in the config's "Repo-specific exceptions" block with the approval "Repo-specific exceptions" block with the approval date. Approved so far:
date. Approved so far: paralleltest (2026-07-06). Line-level //nolint paralleltest (2026-07-06); testpackage, exhaustive, and mnd (2026-07-07). The
with a reason is used sparingly for C-faithfulness (e.g. the authentic complexity linters (cyclop, gocognit, nestif) are enabled and clean as of
"missle" message spellings) and provably-safe gosec conversions; each refactor step 7 (2026-07-07): the whole golangci-lint run is 0 issues, so keep
it that way — decompose new hot spots rather than reaching for a nolint.
Line-level //nolint with a reason is used sparingly for C-faithfulness (e.g. the
authentic "missle" message spellings) and provably-safe gosec conversions; each
needs a justifying comment. needs a justifying comment.
## Faithfulness ## Faithfulness
Behavior must not change during the idiomatic-Go refactor unless a Behavior must not change during the idiomatic-Go refactor unless a TODO step
TODO step says so. The 80x24 seed-compatible gameplay, message text says so. The 80x24 seed-compatible gameplay, message text (including original
(including original typos), RNG call order, and C quirks (documented typos), RNG call order, and C quirks (documented in tests like
in tests like TestHoldScrollGreedyMonsterQuirk) are contract. Doc TestHoldScrollGreedyMonsterQuirk) are contract. Doc comments keep their "(file.c
comments keep their "(file.c func_name)" breadcrumbs. func_name)" breadcrumbs.
## Debugging ## Debugging
Write real, committed test files with t.Logf output and run plain Write real, committed test files with t.Logf output and run plain `go test -v`;
`go test -v`; no throwaway scratch scripts. Successful debug probes no throwaway scratch scripts. Successful debug probes become regression tests.
become regression tests.

35
Makefile Normal file
View File

@@ -0,0 +1,35 @@
# Development convenience targets. This repo is exempt from the standard
# policy scaffold (no Dockerfile, CI, or REPO_POLICIES.md); this Makefile
# is only a thin wrapper around the Go toolchain, golangci-lint, and
# prettier so `make fmt` / `make check` behave the same as in sneak's
# other repos.
GO_PKGS := ./...
MD_FILES := $(shell git ls-files '*.md')
PRETTIER := prettier --tab-width 4 --prose-wrap always
.PHONY: check fmt fmt-check lint test
# Format, lint, and test — the full local pre-commit gate.
check: fmt-check lint test
# Format Go and Markdown in place.
fmt:
gofmt -w .
$(PRETTIER) --write $(MD_FILES)
# Fail if any Go or Markdown file is not formatted.
fmt-check:
@unformatted="$$(gofmt -l .)"; \
if [ -n "$$unformatted" ]; then \
echo "gofmt needed on:"; echo "$$unformatted"; exit 1; \
fi
$(PRETTIER) --check $(MD_FILES)
# Run the house linter (config in .golangci.yml).
lint:
golangci-lint run $(GO_PKGS)
# Run the test suite.
test:
go test $(GO_PKGS)

View File

@@ -2,19 +2,19 @@
[![License](https://img.shields.io/badge/license-BSD-blue.svg)](LICENSE.TXT) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](LICENSE.TXT)
**Rogue** is the original dungeon-crawling adventure game that spawned an **Rogue** is the original dungeon-crawling adventure game that spawned an entire
entire genre. This branch is a faithful Go port of Rogue 5.4.4: explore genre. This branch is a faithful Go port of Rogue 5.4.4: explore procedurally
procedurally generated dungeons, fight monsters, collect treasure, and generated dungeons, fight monsters, collect treasure, and attempt to retrieve
attempt to retrieve the Amulet of Yendor. the Amulet of Yendor.
**Original authors:** Michael Toy, Ken Arnold, and Glenn Wichman **Original authors:** Michael Toy, Ken Arnold, and Glenn Wichman (19801983,
(19801983, 1985, 1999). 1985, 1999).
The port is function-by-function faithful to the classic C sources — same The port is function-by-function faithful to the classic C sources — same
dungeon generation (seed-compatible RNG), same combat math, same item dungeon generation (seed-compatible RNG), same combat math, same item tables,
tables, same messages. The C reference implementation lives on the same messages. The C reference implementation lives on the `master` and
`master` and `modern-rogue` branches; [ARCHITECTURE.md](ARCHITECTURE.md) `modern-rogue` branches; [ARCHITECTURE.md](ARCHITECTURE.md) documents both the
documents both the original program structure and the design of this port. original program structure and the design of this port.
## Building and running ## Building and running
@@ -40,13 +40,12 @@ go build ./cmd/rogue
Press `?` in game for the full list. Press `?` in game for the full list.
- **arrows** or **h/j/k/l/y/u/b/n** — move (shift to run, ctrl to run - **arrows** or **h/j/k/l/y/u/b/n** — move (shift to run, ctrl to run until
until adjacent) adjacent)
- **`.`** rest, **`s`** search for hidden doors and traps - **`.`** rest, **`s`** search for hidden doors and traps
- **`i`** inventory, **`,`** pick up, **`d`** drop - **`i`** inventory, **`,`** pick up, **`d`** drop
- **`q`** quaff potion, **`r`** read scroll, **`e`** eat food - **`q`** quaff potion, **`r`** read scroll, **`e`** eat food
- **`w`** wield weapon, **`W`** wear armor, **`P`**/**`R`** put on / - **`w`** wield weapon, **`W`** wear armor, **`P`**/**`R`** put on / remove ring
remove ring
- **`t`** throw, **`z`** zap a wand, **`f`**/**`F`** fight - **`t`** throw, **`z`** zap a wand, **`f`**/**`F`** fight
- **`>`**/**`<`** take the stairs - **`>`**/**`<`** take the stairs
- **`S`** save, **`Q`** quit - **`S`** save, **`Q`** quit
@@ -61,8 +60,8 @@ export ROGUEOPTS="name=YourName,terse,jump,fruit=mango"
ROGUE_WIZARD=1 SEED=12345 ./rogue ROGUE_WIZARD=1 SEED=12345 ./rogue
``` ```
The scoreboard is kept in `~/.rogue.scores`. Save files are Go gob The scoreboard is kept in `~/.rogue.scores`. Save files are Go gob snapshots
snapshots and, as in the original, are deleted when restored. and, as in the original, are deleted when restored.
## Code layout ## Code layout
@@ -73,13 +72,17 @@ term/ tcell-backed terminal, replacing curses
cmd/rogue/ the executable cmd/rogue/ the executable
``` ```
The engine package is fully headless-testable: `go test ./game/` runs The engine package is fully headless-testable: `go test ./game/` runs scripted
scripted game sessions, dungeon-generation golden checks, and an RNG command sequences, dungeon-generation golden checks, and an RNG compatibility
compatibility test against the original C generator. test against the original C generator.
For development, the `Makefile` wraps the toolchain: `make fmt` (gofmt +
prettier), `make lint` (golangci-lint), `make test`, and `make check` (all
three).
## License ## License
BSD-style; see [LICENSE.TXT](LICENSE.TXT). BSD-style; see [LICENSE.TXT](LICENSE.TXT).
Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman.
Wichman. All rights reserved. All rights reserved.

278
TODO.md
View File

@@ -1,130 +1,188 @@
# Workflow # Workflow
* branch (from `main`) - branch (from `main`)
* do the work in Next Step - do the work in Next Step
* move Next Step to the top of Completed Steps - move Next Step to the top of Completed Steps
* move the top item of Future Steps into Next Step - move the top item of Future Steps into Next Step
* commit (`TODO.md` changes in the same commit as the work) - commit (`TODO.md` changes in the same commit as the work)
* merge to `main` if the branch is not protected, otherwise open a PR - merge to `main` if the branch is not protected, otherwise open a PR
* push - push
# Status # Status
pre-1.0 pre-1.0
The port on main is complete and faithful (function-by-function from The port on main is complete and faithful (function-by-function from Rogue 5.4.4
Rogue 5.4.4 C; reference sources on c-master/modern-rogue). Current C; reference sources on c-master/modern-rogue). Current phase: refactor from a
phase: refactor from a transliterated port into idiomatic Go — one transliterated port into idiomatic Go — one feature branch per step below,
feature branch per step below, descriptive naming, real types, house descriptive naming, real types, house style per
style per ~/dev/prompts/prompts/CODE_STYLEGUIDE_GO.md. ~/dev/prompts/prompts/CODE_STYLEGUIDE_GO.md.
Refactor ground rules: Refactor ground rules:
- Behavior must not change unless a step says so. The full test suite - Behavior must not change unless a step says so. The full test suite (scripted
(scripted sessions, generation invariants, C-compatible RNG goldens) sessions, generation invariants, C-compatible RNG goldens) gates every step;
gates every step; 80x24 seed-compatible gameplay stays intact. 80x24 seed-compatible gameplay stays intact.
- Renames keep the C lineage greppable: doc comments retain their - Renames keep the C lineage greppable: doc comments retain their "(file.c
"(file.c func_name)" breadcrumbs, and the docs refresh step adds a func_name)" breadcrumbs, and the docs refresh step adds a C-name → Go-name
C-name → Go-name table to ARCHITECTURE.md. table to ARCHITECTURE.md.
# Next Step # Next Step
Finish adopting the house Go linting standards. Done so far (branch Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
refactor/lint-adoption): .golangci.yml copied verbatim from the wizard commands).
prompts repo (plus the sneak-approved paralleltest exception,
2026-07-06); ~1,500 findings fixed (autofix formatting sweep, errcheck/
err113/noinlineerr error handling, forbidigo, funcorder, recvcheck
pointer receivers, goprintffuncname renames msg helpers to *f, revive
doc comments, gocritic switch rewrites, gosec real fixes plus justified
nolints, unparam signature tightening, C-faithful "missle" spellings
restored after misspell autofix changed game text). Remaining findings
are all in linters awaiting sneak's exception decision: mnd (288),
gochecknoglobals (37), cyclop (36), nestif (30), gocognit (23),
exhaustive (22), goconst (16), testpackage (9). Blocked on that
decision; either disable with approval or scope the fixes.
# Completed Steps # Completed Steps
- 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue - 2026-07-24 Seed compatibility — item tables (seed-compat): instrumented the C
(go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string). reference on modern-rogue with a DUMP mode (testdata/c_seedcompat.patch) that
- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split forces the RNG seed and prints the per-seed item appearance tables (potion
into ArmorClass/Charges/GoldValue/Bonus (rings); Stats.Arm → colors, scroll names, ring stones, wand/staff materials) before initscr, and
ArmorClass; damage strings parsed once into DiceSpec at table captured its output for four seeds as testdata/item_tables.golden.
definition (ParseDice keeps C roll_em parse semantics, incl. "%%%x0" TestSeedCompatItemTables regenerates the same tables from the Go port and they
and "000x0" edge cases, regression-tested); save format 5.4.4-go3. match byte for byte — proving the LCG and its consumption order through the
- 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc): whole init sequence agree with C. The remaining "same dungeon (map)" half
ObjectKind separates item category from map glyph (Object.Type byte would need the harder headless-curses C dump (new_level draws to curses);
→ Kind ObjectKind with Glyph()); PotionKind/ScrollKind/RingKind/ deferred — the item-table match already validates RNG-order faithfulness
WandKind/WeaponKind/ArmorKind/TrapKind typed iota enums with through init, and the Go generation goldens guard determinism thereafter.
Stringer; typed accessors on Object; getItem/inventory/whatis
filters take ObjectKind (KindCallable/KindRingOrStick replace - 2026-07-23 Playtest hardening (playtest-hardening): added two death-safe
CALLABLE/R_OR_S); save format bumped to 5.4.4-go2. Suite green. crash-sweep drives through the real turn loop, within the step-8 os.Exit
- 2026-07-06 Refactor step 1 (refactor/descriptive-constants): renamed constraint (a fortify() helper pins HP/food/exp and clears the freeze/stuck
all flag bits, trap types, item subtype constants, and Max* counts to counters each turn so no death exits the test binary; fixed seeds keep them
descriptive names (IsHuh→Confused, SeeMonst→SenseMonsters, deterministic). TestDeepPlaythrough uses quaff/read/zap through command
WsHasteM→WandHasteMonster, MaxSticks→NumWandTypes, ...); dispatch, then descends to depth 8 with a save/restore at depth 4;
Level.NTraps→TrapCount; C names kept as comment breadcrumbs. Pure TestTurnLoopCrashSweep mashes movement/search/rest for 200 turns on four
rename, suite green. seeds. Neither surfaced a panic. The interactive "play several games at a real
tcell terminal" portion needs a human at an 80x24 terminal and is left to the
maintainer; the binary's non-interactive paths (`-s` scores) were
smoke-tested.
- 2026-07-23 Docs refresh (docs-refresh): rewrote ARCHITECTURE.md Part 2 (the
pre-implementation design sketch) to match the final code — current type/field
names (ObjectKind, DiceSpec, split o_arm, step-1 flag names, TrapCount, Level
list methods), the static tables now on the per-game gameData struct, the
daemon/effect handler tables, the MessageLine extraction, the Terminal
interface, the flat gob SaveState, and the New(Params) + os.Exit design. Added
§7.1, a C-name → Go-name rename table, and a README note on the make targets.
- 2026-07-23 Refactor step 8 (refactor/constructor-style): constructor and exit
pass. NewGame(Config) → New(Params) and Restore takes Params, so the package's
primary type gets the canonical New() constructor with a named-field Params
struct (styleguide 139/159). The gameEnd panic unwind is gone: one game run is
one process, so myExit restores the terminal (new Terminal.Fini) and calls
os.Exit(0), and Run() no longer returns; the four Run()-to-completion tests
were reworked/dropped since death (combat or starvation) now exits the process
(TestScoreRendersList and TestRunDownStairs preserve what is still drivable;
save/restore stays covered by TestSaveRestoreRoundTrip). The 77-column wrap
sweep was dropped per sneak (2026-07-23): line lengths left as-is (lll caps at
88 and passes).
- 2026-07-07 Refactor step 7 (refactor/effects-dispatch): effects dispatch
tables plus a full decomposition sweep — the quaff / readScroll / doZap
switches, the attack monster-power switch, the be_trapped switch, the daemon
d_func switch, and the command-key switch all became handler tables on
gameData (quaffHandlers, readHandlers, zapHandlers, hitHandlers, trapHandlers,
daemonHandlers, commandHandlers), one small named method per case. Every
remaining cyclop/gocognit/nestif hot spot was split into named helpers across
fight, misc (look), command, chase, move, passages, options, pack, things,
save, daemons, rooms, score, monsters, rings, rip, io, object, weapons,
wizard, and term/tcell, plus three test functions. Effect order and RNG call
sequence preserved throughout; the whole golangci-lint run is now 0 issues.
- 2026-07-07 Refactor step 6 (refactor/god-object-extraction): MessageLine (was
MsgLine) owns the msg/addmsg/endmsg machinery, wired to its screen/look/input
needs via attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so
call sites are unchanged. Player owns pack bookkeeping (nextPackChar,
removeFromPack — the state half of leave_pack; leavePack keeps only LastPick
tracking). Level owns object/monster list management and lookup (ObjectAt
replaces findObj; AddObject/RemoveObject/AddMonster/RemoveMonster replace
direct attachObj/detachObj/attachMon/detachMon on level lists).
Inventory/pickup UI flows stay on RogueGame deliberately: they are display and
turn orchestration, not state surgery.
- 2026-07-07 Refactor step 5 (refactor/item-combat-ui-renames, three commits,
one subsystem each): items — getItem→promptPackItem now returning (obj, ok),
invName→inventoryName, doPot→applyPotionFuse; combat — rollEm→rollAttacks,
attack/moveMonster/chaseStep return (removed bool) instead of C -1/0 int
codes; UI — getDir→promptDirection. C breadcrumbs kept; suite green.
- 2026-07-07 Refactor step 4 (refactor/movement-renames): movement/world renames
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms/doPassages/doMaze→digRooms/digPassages/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 replaced with loops (moveHero retry loop + extracted
passageTurn, dispatch re-dispatch loop, chaseStep passage loop, saveGame
labeled prompt loop); C breadcrumbs kept in doc comments.
- 2026-07-07 Lint adoption finished (refactor/no-package-globals): all 37
package-level vars moved into `gameData` (built by `newGameData`, hung on
RogueGame as `g.data`, set in NewGame and Restore); ObjectKind
Glyph()/objectKindForGlyph became switches; the table-reading subtype
Stringers were removed; isMagic became a RogueGame method; goconst fixed with
named word constants (potionName, goldName, staffName, ripWall, ...);
testpackage and exhaustive disabled in .golangci.yml with sneak's approval
(2026-07-07); misspell's corruption of the "ther" scroll syllable reverted.
mnd disabled with sneak's approval (2026-07-07, follow-up commit). Remaining
red: cyclop (36), nestif (30), gocognit (23) stay until step 7 fixes them per
sneak's ruling.
- 2026-07-06 Lint adoption bulk (refactor/lint-adoption, 5ba9fe8): .golangci.yml
copied verbatim from the prompts repo (plus the sneak-approved paralleltest
exception, 2026-07-06); ~1,500 findings fixed (autofix formatting sweep,
errcheck/err113/noinlineerr error handling, forbidigo, funcorder, recvcheck
pointer receivers, goprintffuncname renames msg helpers to *f, revive doc
comments, gocritic switch rewrites, gosec real fixes plus justified nolints,
unparam signature tightening, C-faithful "missle" spellings restored after
misspell autofix changed game text).
- 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue (go.mod, term/
and cmd/ imports, ARCHITECTURE.md, version string).
- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split into
ArmorClass/Charges/GoldValue/Bonus (rings); Stats.Arm → ArmorClass; damage
strings parsed once into DiceSpec at table definition (ParseDice keeps C
roll_em parse semantics, incl. "%%%x0" and "000x0" edge cases,
regression-tested); save format 5.4.4-go3.
- 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc): ObjectKind
separates item category from map glyph (Object.Type byte → Kind ObjectKind
with Glyph()); PotionKind/ScrollKind/RingKind/
WandKind/WeaponKind/ArmorKind/TrapKind typed iota enums with Stringer; typed
accessors on Object; getItem/inventory/whatis filters take ObjectKind
(KindCallable/KindRingOrStick replace CALLABLE/R_OR_S); save format bumped to
5.4.4-go2. Suite green.
- 2026-07-06 Refactor step 1 (refactor/descriptive-constants): renamed all flag
bits, trap types, item subtype constants, and Max* counts to descriptive names
(IsHuh→Confused, SeeMonst→SenseMonsters, WsHasteM→WandHasteMonster,
MaxSticks→NumWandTypes, ...); Level.NTraps→TrapCount; C names kept as comment
breadcrumbs. Pure rename, suite green.
- 2026-07-06 Made the rgoue branch Go-only: removed C sources and the - 2026-07-06 Made the rgoue branch Go-only: removed C sources and the
autoconf/VS build system (they remain on master and modern-rogue), autoconf/VS build system (they remain on master and modern-rogue), ported the
ported the last wizard command (item-probability listing), rewrote last wizard command (item-probability listing), rewrote README.md for the Go
README.md for the Go port (c0b533e) port (c0b533e)
- 2026-07-06 Ported the command loop, save/restore, the tcell terminal - 2026-07-06 Ported the command loop, save/restore, the tcell terminal layer,
layer, and the playable binary at cmd/rogue (41fc104) and the playable binary at cmd/rogue (41fc104)
- 2026-07-06 Ported item effects: potions, scrolls, options, call_it - 2026-07-06 Ported item effects: potions, scrolls, options, call_it (cdf9bf7)
(cdf9bf7) - 2026-07-06 Ported combat, the chase driver, traps, zapping, death and scores
- 2026-07-06 Ported combat, the chase driver, traps, zapping, death and (3c5add8)
scores (3c5add8) - 2026-07-06 Ported dungeon generation, base items, the pack, and monster
- 2026-07-06 Ported dungeon generation, base items, the pack, and creation (a69ef7d)
monster creation (a69ef7d) - 2026-07-06 Ported the foundation: types, seed-compatible RNG, item tables,
- 2026-07-06 Ported the foundation: types, seed-compatible RNG, item daemon scheduler (7fa2048)
tables, daemon scheduler (7fa2048) - 2026-07-06 Wrote ARCHITECTURE.md Parts 1 and 2: complete map of the C program
- 2026-07-06 Wrote ARCHITECTURE.md Parts 1 and 2: complete map of the C and the Go port design (91eeee0, 45dba95)
program and the Go port design (91eeee0, 45dba95) - Fork base: Davidslv/rogue C 5.4.4 with modernization fixes (C23 prototypes,
- Fork base: Davidslv/rogue C 5.4.4 with modernization fixes (C23 ncurses compat), preserved on master/modern-rogue
prototypes, ncurses compat), preserved on master/modern-rogue
# Future Steps # Future Steps
1. Refactor step 4: method renames, movement/world subsystem 1. Tag a release once a full game (Amulet retrieval and score entry) completes
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep, without defects.
doRooms/doPassages/doMaze→digRooms/digPassages/digMaze, 2. Full-terminal-size support (deferred by explicit decision 2026-07-06):
chgStr→changeStrength, ...); remove the goto/label flows in doMove, per-game dungeon dimensions instead of the 80x24 constants; open design
dispatch, and saveGame in favor of loops and helpers. questions are resize policy, gameplay tuning at larger sizes, and a --classic
2. Refactor step 5: method renames, items/combat/UI subsystems 80x24 mode.
(invName→inventoryName, rollEm→rollAttacks, doPot→applyPotionFuse, 3. Note: this repo is exempt from the standard policy scaffold. A minimal dev
getItem→promptPackItem returning (obj, ok), getDir→promptDirection); Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
int status codes (attack returning -1) become named results. Two or 2026-07-07 request, but do not add a Dockerfile, CI config, or
three commits, one subsystem each. REPO_POLICIES.md.
3. Refactor step 6: extract types from the god object — MessageLine
owns the msg/addmsg/endmsg machinery; pack/inventory operations move
onto *Player; monster/object list management and map queries
consolidate onto *Level; RogueGame keeps turn orchestration and
cross-system effects only.
4. Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
switches become per-kind handler tables of small named methods,
keeping effect order and RNG call sequence identical.
5. Refactor step 8: constructor and style pass per the house
styleguide — game.New(game.Params{...}) replacing NewGame(Config);
replace the gameEnd panic unwind with error-based turn results where
feasible; 77-column wrap sweep.
6. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
post-refactor names; add the C name → Go name rename table.
7. Playtest hardening pass: play several full games with the tcell
binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests.
8. Verify the seed-compatibility claim against the C reference on
c-master: same seed, same dungeon, same item tables, for several
seeds.
9. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands).
10. Tag a release once a full game (Amulet retrieval and score entry)
completes without defects.
11. Full-terminal-size support (deferred by explicit decision
2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode.
12. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -21,18 +21,20 @@ func main() {
os.Exit(run()) os.Exit(run())
} }
// run carries the real main so that deferred terminal restoration runs // run does the real work and returns an exit code. It only returns on a
// before the process exits (os.Exit skips defers). // startup error; once the game starts, it ends by exiting the process
// from within (game.myExit restores the terminal first). The deferred
// Fini covers the early-return paths.
func run() int { func run() int {
scores := flag.Bool("s", false, "print the scoreboard and exit") scores := flag.Bool("s", false, "print the scoreboard and exit")
deathDemo := flag.Bool("d", false, "die a random death (demo)") deathDemo := flag.Bool("d", false, "die a random death (demo)")
flag.Parse() flag.Parse()
cfg := loadConfig() params := loadParams()
if *scores { if *scores {
game.NewGame(cfg).ShowScores() game.New(params).ShowScores()
return 0 return 0
} }
@@ -45,46 +47,39 @@ func run() int {
} }
defer t.Fini() defer t.Fini()
cfg.Term = t params.Term = t
var g *game.RogueGame var g *game.RogueGame
if args := flag.Args(); len(args) == 1 && !*deathDemo { if args := flag.Args(); len(args) == 1 && !*deathDemo {
// restore a saved game // restore a saved game
g, err = game.Restore(args[0], cfg) g, err = game.Restore(args[0], params)
if err != nil { if err != nil {
t.Fini() fmt.Fprintln(os.Stderr, err) // deferred Fini restores the terminal
fmt.Fprintln(os.Stderr, err)
return 1 return 1
} }
} else { } else {
g = game.NewGame(cfg) g = game.New(params)
} }
if *deathDemo { if *deathDemo {
g.DeathDemo() g.DeathDemo() // does not return: death exits the process
return 0 return 0
} }
installAutosave(g, t) installAutosave(g, t)
runErr := g.Run() g.Run() // does not return: the game ends by exiting the process
if runErr != nil {
t.Fini()
fmt.Fprintln(os.Stderr, runErr)
return 1
}
return 0 return 0
} }
// loadConfig gathers the game configuration from the environment: home // loadParams gathers the game parameters from the environment: home
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed // directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
// (main.c's startup). // (main.c's startup).
func loadConfig() game.Config { func loadParams() game.Params {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
name := "" name := ""
@@ -96,7 +91,7 @@ func loadConfig() game.Config {
wizard := os.Getenv("ROGUE_WIZARD") != "" wizard := os.Getenv("ROGUE_WIZARD") != ""
return game.Config{ return game.Params{
Seed: chooseSeed(wizard), Seed: chooseSeed(wizard),
Name: name, Name: name,
RogueOpts: os.Getenv("ROGUEOPTS"), RogueOpts: os.Getenv("ROGUEOPTS"),
@@ -133,6 +128,6 @@ func chooseSeed(wizard bool) int32 {
// The C game computed `lowtime + getpid()` in int; the truncation to // The C game computed `lowtime + getpid()` in int; the truncation to
// 32 bits is the same wraparound the C int arithmetic performed. // 32 bits is the same wraparound the C int arithmetic performed.
return int32(time.Now().Unix()&0x7fffffff) + //nolint:gosec // G115: deliberate wrap return int32(time.Now().Unix()&0x7fffffff) +
int32(os.Getpid()&0x7fffffff) //nolint:gosec // G115: deliberate wrap int32(os.Getpid()&0x7fffffff)
} }

View File

@@ -6,8 +6,8 @@ package game
func (g *RogueGame) wear() { func (g *RogueGame) wear() {
p := &g.Player p := &g.Player
obj := g.getItem("wear", KindArmor) obj, ok := g.promptPackItem("wear", KindArmor)
if obj == nil { if !ok {
return return
} }
@@ -32,7 +32,7 @@ func (g *RogueGame) wear() {
g.wasteTime() g.wasteTime()
obj.Flags.Set(Known) obj.Flags.Set(Known)
sp := g.invName(obj, true) sp := g.inventoryName(obj, true)
p.CurArmor = obj p.CurArmor = obj
if !g.Options.Terse { if !g.Options.Terse {
@@ -70,7 +70,7 @@ func (g *RogueGame) takeOff() {
g.addmsgf("you used to be") g.addmsgf("you used to be")
} }
g.msg(" wearing %c) %s", obj.PackCh, g.invName(obj, true)) g.msg(" wearing %c) %s", obj.PackCh, g.inventoryName(obj, true))
} }
// wasteTime does nothing but let other things happen (armor.c waste_time). // wasteTime does nothing but let other things happen (armor.c waste_time).

View File

@@ -9,26 +9,7 @@ const dragonShot = 5
func (g *RogueGame) runners(int) { func (g *RogueGame) runners(int) {
list := append([]*Monster(nil), g.Level.Monsters...) list := append([]*Monster(nil), g.Level.Monsters...)
for _, tp := range list { for _, tp := range list {
if !tp.On(Held) && tp.On(Awake) { g.runnerTurn(tp)
origPos := tp.Pos
wastarget := tp.On(Targeted)
if g.moveMonst(tp) == -1 {
continue
}
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if g.moveMonst(tp) == -1 {
continue
}
}
if wastarget && origPos != tp.Pos {
tp.Flags.Clear(Targeted)
g.ToDeath = false
}
}
} }
if g.HasHit { if g.HasHit {
@@ -37,24 +18,52 @@ func (g *RogueGame) runners(int) {
} }
} }
// moveMonst executes a single turn of running for a monster (chase.c // runnerTurn gives one monster its motion for the turn; flying monsters
// move_monst). Returns -1 if the monster died or left the level. // far from the hero move twice (the loop body of chase.c runners).
func (g *RogueGame) moveMonst(tp *Monster) int { func (g *RogueGame) runnerTurn(tp *Monster) {
if tp.On(Held) || !tp.On(Awake) {
return
}
origPos := tp.Pos
wastarget := tp.On(Targeted)
if removed := g.moveMonster(tp); removed {
return
}
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if removed := g.moveMonster(tp); removed {
return
}
}
if wastarget && origPos != tp.Pos {
tp.Flags.Clear(Targeted)
g.ToDeath = false
}
}
// moveMonster executes a single turn of running for a monster (chase.c
// move_monst). The result reports that the monster died or left the
// level (the C -1 return).
func (g *RogueGame) moveMonster(tp *Monster) bool {
if !tp.On(Slowed) || tp.Turn { if !tp.On(Slowed) || tp.Turn {
if g.doChase(tp) == -1 { if g.chaseStep(tp) {
return -1 return true
} }
} }
if tp.On(Hasted) { if tp.On(Hasted) {
if g.doChase(tp) == -1 { if g.chaseStep(tp) {
return -1 return true
} }
} }
tp.Turn = !tp.Turn tp.Turn = !tp.Turn
return 0 return false
} }
// relocate makes the monster's new location be the specified one, updating // relocate makes the monster's new location be the specified one, updating
@@ -62,8 +71,8 @@ func (g *RogueGame) moveMonst(tp *Monster) int {
func (g *RogueGame) relocate(th *Monster, newLoc Coord) { func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
if newLoc != th.Pos { if newLoc != th.Pos {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh) g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
th.Room = g.roomin(newLoc) th.Room = g.roomIn(newLoc)
g.setOldch(th, newLoc) g.setOldChar(th, newLoc)
oroom := th.Room oroom := th.Room
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil) g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
@@ -86,12 +95,48 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
} }
} }
// doChase makes one thing chase another (chase.c do_chase). Returns -1 if // chaseStep makes one thing chase another (chase.c do_chase). The
// the chaser died in the attempt. // result reports that the chaser died or left the level in the attempt
func (g *RogueGame) doChase(th *Monster) int { // (the C -1 return).
p := &g.Player func (g *RogueGame) chaseStep(th *Monster) bool {
stoprun := false // true means we are there stoprun := false // true means we are there
mindist := 32767
rer, ree, door := g.chaseRooms(th)
this, shot := g.chaseGoal(th, rer, ree, door, 32767)
if shot {
return false
}
// This now contains what we want to run to this time so we run to it.
// If we hit it we either want to fight it or stop running
if g.chase(th, this) {
if th.Type == 'F' {
return false
}
} else {
switch this {
case g.Player.Pos:
return g.attack(th)
case *th.Dest:
g.chaseTakeObject(th)
stoprun = th.Type != 'F'
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake)
}
return false
}
// chaseRooms finds the rooms of the chaser and its desire; doors do not
// count as inside rooms here (the setup of chase.c do_chase).
func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) {
p := &g.Player
rer := th.Room // find room of chaser rer := th.Room // find room of chaser
if th.On(Greedy) && rer.GoldVal == 0 { if th.On(Greedy) && rer.GoldVal == 0 {
@@ -102,17 +147,28 @@ func (g *RogueGame) doChase(th *Monster) int {
if th.Dest == &p.Pos { if th.Dest == &p.Pos {
ree = p.Room ree = p.Room
} else { } else {
ree = g.roomin(*th.Dest) ree = g.roomIn(*th.Dest)
} }
// We don't count doors as inside rooms for this routine
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
return rer, ree, g.Level.Char(th.Pos.Y, th.Pos.X) == Door
}
// chaseGoal picks the spot the chaser runs toward this turn: the
// nearest exit toward its desire when it is in a different room, or the
// desire itself. shot means a dragon breathed flame instead of moving
// (the goal loop of chase.c do_chase).
func (g *RogueGame) chaseGoal(th *Monster, rer, ree *Room, door bool, mindist int) (Coord, bool) {
var this Coord var this Coord
over: for {
// If the object of our desire is in a different room, and we are not // If the object of our desire is in a different room, and we are
// in a corridor, run to the door nearest to our goal. // not in a corridor, run to the door nearest to our goal.
if rer != ree { if rer == ree {
this = *th.Dest
return this, g.dragonBreath(th)
}
for i := range rer.Exits { for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i]) curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist { if curdist < mindist {
@@ -121,21 +177,25 @@ over:
} }
} }
if door { if !door {
return this, false
}
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum] rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
door = false door = false
// the C goto over: redo with the passage as room
goto over
} }
} else { }
this = *th.Dest
// For dragons check and see if (a) the hero is on a straight line // dragonBreath checks whether a dragon shoots flame at the hero instead
// from it, and (b) that it is within shooting distance, but // of moving, and shoots it (the D block of chase.c do_chase).
// outside of striking range. func (g *RogueGame) dragonBreath(th *Monster) bool {
if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X || if th.Type != 'D' || !g.dragonShoots(th) {
abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) && return false
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength && }
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
p := &g.Player
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y) g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
g.Delta.X = sign(p.Pos.X - th.Pos.X) g.Delta.X = sign(p.Pos.X - th.Pos.X)
@@ -153,18 +213,30 @@ over:
g.Kamikaze = false g.Kamikaze = false
} }
return 0 return true
}
// dragonShoots decides whether the dragon takes the shot: the hero is
// on a straight line from it, within shooting distance but outside
// striking range, it is not cancelled, and the shot roll comes up
// (chase.c do_chase).
func (g *RogueGame) dragonShoots(th *Monster) bool {
p := &g.Player
if th.Pos.Y != p.Pos.Y && th.Pos.X != p.Pos.X &&
abs(th.Pos.Y-p.Pos.Y) != abs(th.Pos.X-p.Pos.X) {
return false
} }
}
// This now contains what we want to run to this time so we run to it. return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
// If we hit it we either want to fight it or stop running !th.On(Cancelled) && g.rnd(dragonShot) == 0
if !g.chase(th, this) { }
if this == p.Pos {
return g.attack(th) // chaseTakeObject has the monster pick up the object it was running to
} else if this == *th.Dest { // (the dest arm of chase.c do_chase).
func (g *RogueGame) chaseTakeObject(th *Monster) {
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos { if th.Dest == &obj.Pos {
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
attachObj(&th.Pack, obj) attachObj(&th.Pack, obj)
if th.Room.Flags.Has(Gone) { if th.Room.Flags.Has(Gone) {
@@ -178,34 +250,12 @@ over:
break break
} }
} }
if th.Type != 'F' {
stoprun = true
}
}
} else {
if th.Type == 'F' {
return 0
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake)
}
return 0
} }
// chase finds the spot for the chaser to move closer to the chasee // chase finds the spot for the chaser to move closer to the chasee
// (chase.c chase). Returns true if we want to keep on chasing later, false // (chase.c chase). Returns true if we want to keep on chasing later, false
// if we reach the goal. The chosen spot lands in g.chRet. // if we reach the goal. The chosen spot lands in g.chRet.
func (g *RogueGame) chase(tp *Monster, ee Coord) bool { func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
p := &g.Player
er := tp.Pos
plcnt := 1
var curdist int var curdist int
// If the thing is confused, let it move randomly. Invisible Stalkers // If the thing is confused, let it move randomly. Invisible Stalkers
@@ -214,18 +264,34 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) || if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
(tp.Type == 'B' && g.rnd(2) == 0) { (tp.Type == 'B' && g.rnd(2) == 0) {
// get a valid random move // get a valid random move
g.chRet = g.rndmove(&tp.Creature) g.chRet = g.randomStep(&tp.Creature)
curdist = distCp(g.chRet, ee) curdist = distCp(g.chRet, ee)
// Small chance that it will become un-confused // Small chance that it will become un-confused
if g.rnd(20) == 0 { if g.rnd(20) == 0 {
tp.Flags.Clear(Confused) tp.Flags.Clear(Confused)
} }
} else { } else {
// Otherwise, find the empty spot next to the chaser that is curdist = g.chaseBestSpot(tp, ee)
// closest to the chasee. This will eventually hold where we move }
// to get closer. If we can't find an empty spot, we stay where we
// are. return curdist != 0 && g.chRet != g.Player.Pos
curdist = distCp(er, ee) }
// chaseSearch is the scan state while chase looks for the step that
// gets a monster closest to its chasee.
type chaseSearch struct {
er Coord // where the chaser is
ee Coord // where it wants to go
curdist int
plcnt int
}
// chaseBestSpot finds the empty spot next to the chaser that is closest
// to the chasee, leaving it in g.chRet; if there is none, the chaser
// stays where it is (the search half of chase.c chase).
func (g *RogueGame) chaseBestSpot(tp *Monster, ee Coord) int {
er := tp.Pos
s := chaseSearch{er: er, ee: ee, curdist: distCp(er, ee), plcnt: 1}
g.chRet = er g.chRet = er
ey := er.Y + 1 ey := er.Y + 1
@@ -244,58 +310,63 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
} }
for y := er.Y - 1; y <= ey; y++ { for y := er.Y - 1; y <= ey; y++ {
g.chaseTry(&s, y, x)
}
}
return s.curdist
}
// chaseTry scores one candidate square, reservoir-sampling among ties
// (the scan body of chase.c chase).
func (g *RogueGame) chaseTry(s *chaseSearch, y, x int) {
tryp := Coord{X: x, Y: y} tryp := Coord{X: x, Y: y}
if !g.diagOk(er, tryp) { if !g.diagOk(s.er, tryp) {
continue return
} }
ch := g.Level.VisibleChar(y, x) ch := g.Level.VisibleChar(y, x)
if stepOk(ch) { if !stepOk(ch) {
// If it is a scroll, it might be a scare monster return
// scroll so we need to look it up to see what type
// it is.
if ch == Scroll {
var found *Object
for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X {
found = obj
break
}
}
if found != nil && found.ScrollKind() == ScrollScareMonster {
continue
} }
// If it is a scroll, it might be a scare monster scroll so we need
// to look it up to see what type it is.
if ch == Scroll && g.scareScrollAt(y, x) {
return
} }
// It can also be a Xeroc, which we shouldn't step on // It can also be a Xeroc, which we shouldn't step on
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' { if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
continue return
} }
// If we didn't find any scrolls at this place or it // If we didn't find any scrolls at this place or it wasn't a scare
// wasn't a scare scroll, then this place counts // scroll, then this place counts
thisdist := distance(y, x, ee.Y, ee.X) thisdist := distance(y, x, s.ee.Y, s.ee.X)
if thisdist < curdist { if thisdist < s.curdist {
plcnt = 1 s.plcnt = 1
g.chRet = tryp g.chRet = tryp
curdist = thisdist s.curdist = thisdist
} else if thisdist == curdist { } else if thisdist == s.curdist {
if plcnt++; g.rnd(plcnt) == 0 { if s.plcnt++; g.rnd(s.plcnt) == 0 {
g.chRet = tryp g.chRet = tryp
curdist = thisdist s.curdist = thisdist
} }
} }
}
}
}
}
return curdist != 0 && g.chRet != p.Pos
} }
// setOldch sets the oldch character for the monster (chase.c set_oldch). // scareScrollAt reports whether the object lying at (y, x) is a scare
func (g *RogueGame) setOldch(tp *Monster, cp Coord) { // monster scroll (chase.c chase).
func (g *RogueGame) scareScrollAt(y, x int) bool {
for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X {
return obj.ScrollKind() == ScrollScareMonster
}
}
return false
}
// setOldChar sets the oldch character for the monster (chase.c set_oldch).
func (g *RogueGame) setOldChar(tp *Monster, cp Coord) {
if tp.Pos == cp { if tp.Pos == cp {
return return
} }
@@ -341,8 +412,8 @@ func (g *RogueGame) seeMonst(mp *Monster) bool {
return !mp.Room.Flags.Has(Dark) return !mp.Room.Flags.Has(Dark)
} }
// runto sets a monster running after the hero (chase.c runto). // runTo sets a monster running after the hero (chase.c runto).
func (g *RogueGame) runto(runner Coord) { func (g *RogueGame) runTo(runner Coord) {
tp := g.Level.MonsterAt(runner.Y, runner.X) tp := g.Level.MonsterAt(runner.Y, runner.X)
if tp == nil { if tp == nil {
return return
@@ -353,9 +424,9 @@ func (g *RogueGame) runto(runner Coord) {
tp.Dest = g.findDest(tp) tp.Dest = g.findDest(tp)
} }
// roomin finds what room some coordinates are in; nil means they aren't in // roomIn finds what room some coordinates are in; nil means they aren't in
// any room (chase.c roomin). // any room (chase.c roomin).
func (g *RogueGame) roomin(cp Coord) *Room { func (g *RogueGame) roomIn(cp Coord) *Room {
fp := *g.Level.FlagsAt(cp.Y, cp.X) fp := *g.Level.FlagsAt(cp.Y, cp.X)
if fp.Has(FPassage) { if fp.Has(FPassage) {
return &g.Level.Passages[fp&FPassNum] return &g.Level.Passages[fp&FPassNum]
@@ -387,9 +458,9 @@ func (g *RogueGame) diagOk(sp, ep Coord) bool {
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X)) return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
} }
// cansee returns true if the hero can see a certain coordinate (chase.c // canSee returns true if the hero can see a certain coordinate (chase.c
// cansee). // cansee).
func (g *RogueGame) cansee(y, x int) bool { func (g *RogueGame) canSee(y, x int) bool {
p := &g.Player p := &g.Player
if p.On(Blind) { if p.On(Blind) {
return false return false
@@ -408,7 +479,7 @@ func (g *RogueGame) cansee(y, x int) bool {
} }
// We can only see if the hero is in the same room as the coordinate // We can only see if the hero is in the same room as the coordinate
// and the room is lit, or if it is close. // and the room is lit, or if it is close.
rer := g.roomin(Coord{X: x, Y: y}) rer := g.roomIn(Coord{X: x, Y: y})
return rer == p.Room && !rer.Flags.Has(Dark) return rer == p.Room && !rer.Flags.Has(Dark)
} }
@@ -426,22 +497,23 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
continue continue
} }
if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob { if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob &&
claimed := false !g.objectClaimed(obj) {
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
claimed = true
break
}
}
if !claimed {
return &obj.Pos return &obj.Pos
} }
} }
}
return &g.Player.Pos return &g.Player.Pos
} }
// objectClaimed reports whether some monster already runs toward this
// object (chase.c find_dest).
func (g *RogueGame) objectClaimed(obj *Object) bool {
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
return true
}
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,47 @@ func (p *Player) IsWearing(ring RingKind) bool {
return p.IsRing(Left, ring) || p.IsRing(Right, ring) return p.IsRing(Left, ring) || p.IsRing(Right, ring)
} }
// nextPackChar claims and returns the next unused pack character (pack.c
// pack_char).
func (p *Player) nextPackChar() byte {
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
}
// removeFromPack takes an item out of the pack: the whole entry, or one
// of a stack when all is false (the bookkeeping half of pack.c
// leave_pack). It returns the object that left the pack — a copy when
// newobj asks for a split.
func (p *Player) removeFromPack(obj *Object, newobj, all bool) *Object {
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// attachMon pushes a monster onto the front of a list (list.c attach). // attachMon pushes a monster onto the front of a list (list.c attach).
func attachMon(list *[]*Monster, item *Monster) { func attachMon(list *[]*Monster, item *Monster) {
*list = append([]*Monster{item}, *list...) *list = append([]*Monster{item}, *list...)

View File

@@ -5,38 +5,14 @@ package game
// runDaemon invokes the callback named by id (the call through d_func in C). // runDaemon invokes the callback named by id (the call through d_func in C).
func (g *RogueGame) runDaemon(id DaemonID, arg int) { func (g *RogueGame) runDaemon(id DaemonID, arg int) {
switch id { h := g.data.daemonHandlers[id]
case DRollwand: if h == nil {
g.rollwand(arg) // Handlers are added to the table as their subsystems are
case DDoctor: // ported; reaching one that isn't there is a porting bug.
g.doctor(arg)
case DStomach:
g.stomach(arg)
case DRunners:
g.runners(arg)
case DSwander:
g.swander(arg)
case DNohaste:
g.nohaste(arg)
case DUnconfuse:
g.unconfuse(arg)
case DUnsee:
g.unsee(arg)
case DSight:
g.sight(arg)
case DVisuals:
g.visuals(arg)
case DComeDown:
g.comeDown(arg)
case DLand:
g.land(arg)
case DTurnSee:
g.turnSee(arg != 0)
default:
// Callbacks are added to this switch as their subsystems are
// ported; reaching one that isn't here is a porting bug.
panic("daemon not yet ported") panic("daemon not yet ported")
} }
h(g, arg)
} }
// doctor is the healing daemon that restores hit points after rest // doctor is the healing daemon that restores hit points after rest
@@ -141,6 +117,24 @@ func (g *RogueGame) stomach(int) {
origHungry := p.HungryState origHungry := p.HungryState
if p.FoodLeft <= 0 { if p.FoodLeft <= 0 {
g.stomachFaint()
} else {
g.stomachDigest()
}
if p.HungryState != origHungry {
p.Flags.Clear(Awake)
g.Running = false
g.ToDeath = false
g.Count = 0
}
}
// stomachFaint starves and possibly faints an empty-stomached hero (the
// no-food arm of daemons.c stomach).
func (g *RogueGame) stomachFaint() {
p := &g.Player
if p.FoodLeft--; p.FoodLeft < -StarveTime { if p.FoodLeft--; p.FoodLeft < -StarveTime {
g.death('s') g.death('s')
} }
@@ -159,7 +153,12 @@ func (g *RogueGame) stomach(int) {
} }
g.msg("%s", g.chooseStr("You freak out", "You faint")) g.msg("%s", g.chooseStr("You freak out", "You faint"))
} else { }
// stomachDigest burns food and reports growing hunger (the fed arm of
// daemons.c stomach).
func (g *RogueGame) stomachDigest() {
p := &g.Player
oldfood := p.FoodLeft oldfood := p.FoodLeft
amulet := 0 amulet := 0
@@ -185,15 +184,6 @@ func (g *RogueGame) stomach(int) {
"you are starting to get hungry")) "you are starting to get hungry"))
} }
} }
}
if p.HungryState != origHungry {
p.Flags.Clear(Awake)
g.Running = false
g.ToDeath = false
g.Count = 0
}
} }
// comeDown takes the hero down off her acid trip (daemons.c come_down). // comeDown takes the hero down off her acid trip (daemons.c come_down).
@@ -212,7 +202,7 @@ func (g *RogueGame) comeDown(int) {
// undo the things // undo the things
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Kind.Glyph()) g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Kind.Glyph())
} }
} }
@@ -223,7 +213,7 @@ func (g *RogueGame) comeDown(int) {
for _, tp := range g.Level.Monsters { for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X) g.move(tp.Pos.Y, tp.Pos.X)
if g.cansee(tp.Pos.Y, tp.Pos.X) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
if !tp.On(Invisible) || p.On(CanSeeInvisible) { if !tp.On(Invisible) || p.On(CanSeeInvisible) {
g.addch(tp.Disguise) g.addch(tp.Disguise)
} else { } else {
@@ -242,24 +232,29 @@ func (g *RogueGame) comeDown(int) {
// visuals changes the characters for the player while hallucinating // visuals changes the characters for the player while hallucinating
// (daemons.c visuals). // (daemons.c visuals).
func (g *RogueGame) visuals(int) { func (g *RogueGame) visuals(int) {
p := &g.Player
if !g.After || (g.Running && g.Options.Jump) { if !g.After || (g.Running && g.Options.Jump) {
return return
} }
// change the things // change the things
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing()) g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing())
} }
} }
// change the stairs // change the stairs
if !g.SeenStairs && g.cansee(g.Level.Stairs.Y, g.Level.Stairs.X) { if !g.SeenStairs && g.canSee(g.Level.Stairs.Y, g.Level.Stairs.X) {
g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing()) g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing())
} }
// change the monsters // change the monsters
seemonst := p.On(SenseMonsters) g.visualMonsters()
}
// visualMonsters redraws the monsters through the hallucination (the
// monster loop of daemons.c visuals).
func (g *RogueGame) visualMonsters() {
seemonst := g.Player.On(SenseMonsters)
for _, tp := range g.Level.Monsters { for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X) g.move(tp.Pos.Y, tp.Pos.X)

View File

@@ -32,15 +32,16 @@ func TestParseDice(t *testing.T) {
// The bestiary and weapon tables must parse to at least one attack each so // The bestiary and weapon tables must parse to at least one attack each so
// every creature and weapon actually swings. // every creature and weapon actually swings.
func TestTablesHaveDice(t *testing.T) { func TestTablesHaveDice(t *testing.T) {
for i, m := range monsterTable { data := newGameData()
for i, m := range data.monsterTable {
if len(m.Stats.Dmg) == 0 { if len(m.Stats.Dmg) == 0 {
t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name) t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name)
} }
} }
for w, iw := range initWeaps { for w, iw := range data.initWeaps {
if len(iw.dam) == 0 || len(iw.hrl) == 0 { if len(iw.dam) == 0 || len(iw.hrl) == 0 {
t.Errorf("weapon %v has empty dice", WeaponKind(w)) t.Errorf("weapon %d has empty dice", w)
} }
} }
} }

View File

@@ -7,10 +7,10 @@ import "testing"
func mkGameInput(t *testing.T) *RogueGame { func mkGameInput(t *testing.T) *RogueGame {
t.Helper() t.Helper()
g := NewGame(Config{Seed: 5, Term: &testTerm{}}) g := New(Params{Seed: 5, Term: &testTerm{}})
g.NewLevel() g.NewLevel()
g.Oldpos = g.Player.Pos g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos) g.Oldrp = g.roomIn(g.Player.Pos)
return g return g
} }
@@ -183,7 +183,7 @@ func TestZapSlowMonster(t *testing.T) {
} }
func TestParseOpts(t *testing.T) { func TestParseOpts(t *testing.T) {
g := NewGame(Config{Seed: 1}) g := New(Params{Seed: 1})
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow") g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")
if !g.Options.Terse { if !g.Options.Terse {

View File

@@ -4,43 +4,6 @@ import "strconv"
// fight.c — all the fighting gets done here. // fight.c — all the fighting gets done here.
// hNames are the strings for hitting; the first four are used when the
// player strikes, the second four for monsters (fight.c h_names).
var hNames = [8]string{
" scored an excellent hit on ",
" hit ",
" have injured ",
" swing and hit ",
" scored an excellent hit on ",
" hit ",
" has injured ",
" swings and hits ",
}
// mNames are the strings for missing (fight.c m_names).
var mNames = [8]string{
" miss",
" swing and miss",
" barely miss",
" don't hit",
" misses",
" swings and misses",
" barely misses",
" doesn't hit",
}
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
var strPlus = [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
}
// addDam adjusts damage done due to strength (fight.c add_dam).
var addDam = [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
}
// setMname returns the monster name for the given monster (fight.c // setMname returns the monster name for the given monster (fight.c
// set_mname). // set_mname).
func (g *RogueGame) setMname(tp *Monster) string { func (g *RogueGame) setMname(tp *Monster) string {
@@ -84,9 +47,38 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
// place. // place.
g.Count = 0 g.Count = 0
g.Quiet = 0 g.Quiet = 0
g.runto(mp) g.runTo(mp)
// Let him know it was really a xeroc (if it was one).
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(Blind) { if g.revealXeroc(tp) && !thrown {
return false
}
mname := g.setMname(tp)
g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
g.heroHits(tp, mname, weap, thrown)
return true
}
if thrown {
g.bounce(weap, mname, g.Options.Terse)
} else {
g.miss("", mname, g.Options.Terse)
}
return false
}
// revealXeroc lets him know it was really a xeroc (if it was one); it
// reports whether one was unmasked (the X block of fight.c fight).
func (g *RogueGame) revealXeroc(tp *Monster) bool {
p := &g.Player
if tp.Type != 'X' || tp.Disguise == 'X' || p.On(Blind) {
return false
}
tp.Disguise = 'X' tp.Disguise = 'X'
if p.On(Hallucinating) { if p.On(Hallucinating) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter()) g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
@@ -95,17 +87,14 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!", g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!")) "wait! That's a xeroc!"))
if !thrown { return true
return false }
}
}
mname := g.setMname(tp) // heroHits lands the hero's blow on a monster: messages, the confusing
didHit := false // touch, and the kill check (the hit arm of fight.c fight).
func (g *RogueGame) heroHits(tp *Monster, mname string, weap *Object, thrown bool) {
g.HasHit = g.Options.Terse && !g.ToDeath p := &g.Player
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) { confused := false
didHit = false
if thrown { if thrown {
g.thunk(weap, mname, g.Options.Terse) g.thunk(weap, mname, g.Options.Terse)
@@ -114,7 +103,7 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
} }
if p.On(CanConfuse) { if p.On(CanConfuse) {
didHit = true confused = true
tp.Flags.Set(Confused) tp.Flags.Set(Confused)
p.Flags.Clear(CanConfuse) p.Flags.Clear(CanConfuse)
@@ -125,25 +114,15 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
if tp.Stats.HP <= 0 { if tp.Stats.HP <= 0 {
g.killed(tp, true) g.killed(tp, true)
} else if didHit && !p.On(Blind) { } else if confused && !p.On(Blind) {
g.msg("%s appears confused", mname) g.msg("%s appears confused", mname)
} }
didHit = true
} else {
if thrown {
g.bounce(weap, mname, g.Options.Terse)
} else {
g.miss("", mname, g.Options.Terse)
}
}
return didHit
} }
// attack has the monster attack the player (fight.c attack). Returns -1 if // attack has the monster attack the player (fight.c attack). The result
// the monster removed itself from the level during its own attack. // reports that the monster took itself off the level during its own
func (g *RogueGame) attack(mp *Monster) int { // attack (the C -1 return).
func (g *RogueGame) attack(mp *Monster) bool {
p := &g.Player p := &g.Player
// Since this is an attack, stop running and any healing that was // Since this is an attack, stop running and any healing that was
// going on at the time. // going on at the time.
@@ -167,7 +146,27 @@ func (g *RogueGame) attack(mp *Monster) int {
oldhp := p.Stats.HP oldhp := p.Stats.HP
removed := false removed := false
if g.rollEm(&mp.Creature, &p.Creature, nil, false) { if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
removed = g.monsterHit(mp, mname, oldhp)
} else {
g.monsterMiss(mp, mname)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
}
g.Count = 0
g.status()
return removed
}
// monsterHit lands a monster's blow on the hero: messages, death and
// to-death bookkeeping, then the monster's special power (the hit arm
// of fight.c attack). It reports whether the monster removed itself.
func (g *RogueGame) monsterHit(mp *Monster, mname string, oldhp int) bool {
p := &g.Player
if mp.Type != 'I' { if mp.Type != 'I' {
if g.HasHit { if g.HasHit {
g.addmsgf(". ") g.addmsgf(". ")
@@ -193,13 +192,53 @@ func (g *RogueGame) attack(mp *Monster) int {
} }
if !mp.On(Cancelled) { if !mp.On(Cancelled) {
switch mp.Type { if h := g.data.hitHandlers[mp.Type-'A']; h != nil {
case 'A': return h(g, mp, mname)
}
}
return false
}
// monsterMiss handles a monster's whiffed swing (the miss arm of
// fight.c attack); ice monsters miss silently.
func (g *RogueGame) monsterMiss(mp *Monster, mname string) {
if mp.Type == 'I' {
return
}
p := &g.Player
if g.HasHit {
g.addmsgf(". ")
g.HasHit = false
}
if mp.Type == 'F' {
p.Stats.HP -= p.VfHit
if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ...
}
}
g.miss(mname, "", false)
}
// The monster special-power handlers, dispatched through
// gameData.hitHandlers when an uncancelled monster's hit lands. Each is
// one case of the C attack switch; a true return means the monster
// removed itself from the level.
func (g *RogueGame) hitAquator(*Monster, string) bool {
// If an aquator hits, you can lose armor class. // If an aquator hits, you can lose armor class.
g.rustArmor(p.CurArmor) g.rustArmor(g.Player.CurArmor)
case 'I':
return false
}
func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool {
// The ice monster freezes you // The ice monster freezes you
p.Flags.Clear(Awake) g.Player.Flags.Clear(Awake)
if g.NoCommand == 0 { if g.NoCommand == 0 {
g.addmsgf("you are frozen") g.addmsgf("you are frozen")
@@ -215,34 +254,41 @@ func (g *RogueGame) attack(mp *Monster) int {
if g.NoCommand > BoreLevel { if g.NoCommand > BoreLevel {
g.death('h') g.death('h')
} }
case 'R':
// Rattlesnakes have poisonous bites
if !g.save(VsPoison) {
if !p.IsWearing(RingSustainStrength) {
g.chgStr(-1)
if !g.Options.Terse { return false
g.msg("you feel a bite in your leg and now feel weaker") }
} else {
g.msg("a bite has weakened you") func (g *RogueGame) hitRattlesnake(*Monster, string) bool {
// Rattlesnakes have poisonous bites
if g.save(VsPoison) {
return false
} }
if !g.Player.IsWearing(RingSustainStrength) {
g.changeStrength(-1)
g.msg("%s", g.chooseTerse("a bite has weakened you",
"you feel a bite in your leg and now feel weaker"))
} else if !g.ToDeath { } else if !g.ToDeath {
if !g.Options.Terse { g.msg("%s", g.chooseTerse("bite has no effect",
g.msg("a bite momentarily weakens you") "a bite momentarily weakens you"))
} else {
g.msg("bite has no effect")
} }
}
} return false
case 'W', 'V': }
// Wraiths might drain energy levels, and Vampires can
// steal max_hp func (g *RogueGame) hitLifeDrainer(mp *Monster, _ string) bool {
// Wraiths might drain energy levels, and Vampires can steal max_hp
p := &g.Player
chance := 30 chance := 30
if mp.Type == 'W' { if mp.Type == 'W' {
chance = 15 chance = 15
} }
if g.rnd(100) < chance { if g.rnd(100) >= chance {
return false
}
var fewer int var fewer int
if mp.Type == 'W' { if mp.Type == 'W' {
@@ -254,7 +300,7 @@ func (g *RogueGame) attack(mp *Monster) int {
p.Stats.Exp = 0 p.Stats.Exp = 0
p.Stats.Lvl = 1 p.Stats.Lvl = 1
} else { } else {
p.Stats.Exp = eLevels[p.Stats.Lvl-1] + 1 p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
} }
fewer = g.roll(1, 10) fewer = g.roll(1, 10)
@@ -274,9 +320,13 @@ func (g *RogueGame) attack(mp *Monster) int {
} }
g.msg("you suddenly feel weaker") g.msg("you suddenly feel weaker")
}
case 'F': return false
}
func (g *RogueGame) hitFlytrap(*Monster, string) bool {
// Venus Flytrap stops the poor guy from moving // Venus Flytrap stops the poor guy from moving
p := &g.Player
p.Flags.Set(Held) p.Flags.Set(Held)
p.VfHit++ p.VfHit++
@@ -284,8 +334,13 @@ func (g *RogueGame) attack(mp *Monster) int {
if p.Stats.HP--; p.Stats.HP <= 0 { if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F') g.death('F')
} }
case 'L':
return false
}
func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool {
// Leprechaun steals some gold // Leprechaun steals some gold
p := &g.Player
lastpurse := p.Purse lastpurse := p.Purse
p.Purse -= g.goldCalc() p.Purse -= g.goldCalc()
@@ -299,14 +354,18 @@ func (g *RogueGame) attack(mp *Monster) int {
g.removeMon(mp.Pos, mp, false) g.removeMon(mp.Pos, mp, false)
removed = true
if p.Purse != lastpurse { if p.Purse != lastpurse {
g.msg("your purse feels lighter") g.msg("your purse feels lighter")
} }
case 'N':
// Nymphs steal a magic item; look through the pack and return true
// pick out one we like. }
func (g *RogueGame) hitNymph(mp *Monster, _ string) bool {
// Nymphs steal a magic item; look through the pack and pick out one
// we like.
p := &g.Player
var steal *Object var steal *Object
nobj := 0 nobj := 0
@@ -314,51 +373,22 @@ func (g *RogueGame) attack(mp *Monster) int {
for _, obj := range p.Pack { for _, obj := range p.Pack {
if obj != p.CurArmor && obj != p.CurWeapon && if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] && obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
obj.isMagic() { g.isMagic(obj) {
if nobj++; g.rnd(nobj) == 0 { if nobj++; g.rnd(nobj) == 0 {
steal = obj steal = obj
} }
} }
} }
if steal != nil { if steal == nil {
return false
}
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false) g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
removed = true
g.leavePack(steal, false, false) g.leavePack(steal, false, false)
g.msg("she stole %s!", g.invName(steal, true)) g.msg("she stole %s!", g.inventoryName(steal, true))
}
}
}
} else if mp.Type != 'I' {
if g.HasHit {
g.addmsgf(". ")
g.HasHit = false
}
if mp.Type == 'F' { return true
p.Stats.HP -= p.VfHit
if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ...
}
}
g.miss(mname, "", false)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
}
g.Count = 0
g.status()
if removed {
return -1
}
return 0
} }
// swing returns true if the swing hits (fight.c swing). // swing returns true if the swing hits (fight.c swing).
@@ -369,9 +399,8 @@ func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
return res+wplus >= need return res+wplus >= need
} }
// rollEm rolls several attacks (fight.c roll_em). // rollAttacks rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool { func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player
att := &thatt.Stats att := &thatt.Stats
def := &thdef.Stats def := &thdef.Stats
@@ -383,10 +412,65 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
if weap == nil { if weap == nil {
attacks = att.Dmg attacks = att.Dmg
} else { } else {
hplus = weap.HPlus attacks, hplus, dplus = g.weaponAttack(weap, hurl)
}
// If the creature being attacked is not running (asleep or held) then
// the attacker gets a plus four bonus to hit.
if !thdef.Flags.Has(Awake) {
hplus += 4
}
dplus = weap.DPlus defArm := g.defenderArmor(thdef)
didHit := false
for _, atk := range attacks {
if g.swing(att.Lvl, defArm, hplus+g.data.strPlus[att.Str]) {
proll := g.roll(atk.Count, atk.Sides)
damage := dplus + proll + g.data.addDam[att.Str]
if damage > 0 {
def.HP -= damage
}
didHit = true
}
}
return didHit
}
// weaponAttack picks the dice and to-hit/damage bonuses a weapon swings
// with: ring bonuses when wielded, and launcher pairing for hurled
// missiles (the weapon preamble of fight.c roll_em).
func (g *RogueGame) weaponAttack(weap *Object, hurl bool) (DiceSpec, int, int) {
p := &g.Player
hplus := weap.HPlus
dplus := weap.DPlus
if weap == p.CurWeapon { if weap == p.CurWeapon {
hplus, dplus = g.wieldedRingBonus(hplus, dplus)
}
attacks := weap.Damage
if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch {
attacks = weap.HurlDmg
hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 {
attacks = weap.HurlDmg
}
}
return attacks, hplus, dplus
}
// wieldedRingBonus folds damage and dexterity ring bonuses into the
// wielded weapon's to-hit/damage pluses (fight.c roll_em).
func (g *RogueGame) wieldedRingBonus(hplus, dplus int) (int, int) {
p := &g.Player
if p.IsRing(Left, RingIncreaseDamage) { if p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Bonus dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) { } else if p.IsRing(Left, RingDexterity) {
@@ -398,25 +482,16 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
} else if p.IsRing(Right, RingDexterity) { } else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus hplus += p.CurRing[Right].Bonus
} }
}
attacks = weap.Damage return hplus, dplus
if hurl { }
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch { // defenderArmor computes the defender's effective armor class: worn
attacks = weap.HurlDmg // armor and protection rings when the hero defends (the def_arm
hplus += p.CurWeapon.HPlus // computation of fight.c roll_em).
dplus += p.CurWeapon.DPlus func (g *RogueGame) defenderArmor(thdef *Creature) int {
} else if weap.Launch < 0 { p := &g.Player
attacks = weap.HurlDmg def := &thdef.Stats
}
}
}
// If the creature being attacked is not running (asleep or held) then
// the attacker gets a plus four bonus to hit.
if !thdef.Flags.Has(Awake) {
hplus += 4
}
defArm := def.ArmorClass defArm := def.ArmorClass
if def == &p.Stats { if def == &p.Stats {
@@ -433,22 +508,7 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
} }
} }
didHit := false return defArm
for _, atk := range attacks {
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) {
proll := g.roll(atk.Count, atk.Sides)
damage := dplus + proll + addDam[att.Str]
if damage > 0 {
def.HP -= damage
}
didHit = true
}
}
return didHit
} }
// cAtoi parses a leading integer like C atoi: trailing non-digits are // cAtoi parses a leading integer like C atoi: trailing non-digits are
@@ -515,7 +575,7 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
i += 4 i += 4
} }
s = hNames[i] s = g.data.hNames[i]
} }
g.addmsgf("%s", s) g.addmsgf("%s", s)
@@ -546,7 +606,7 @@ func (g *RogueGame) miss(er, ee string, noend bool) {
i += 4 i += 4
} }
g.addmsgf("%s", mNames[i]) g.addmsgf("%s", g.data.mNames[i])
if !g.Options.Terse { if !g.Options.Terse {
g.addmsgf(" %s", prname(ee, false)) g.addmsgf(" %s", prname(ee, false))
@@ -590,7 +650,7 @@ func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
g.Level.SetMonsterAt(mp.Y, mp.X, nil) g.Level.SetMonsterAt(mp.Y, mp.X, nil)
g.mvaddch(mp.Y, mp.X, tp.OldCh) g.mvaddch(mp.Y, mp.X, tp.OldCh)
detachMon(&g.Level.Monsters, tp) g.Level.RemoveMonster(tp)
if tp.On(Targeted) { if tp.On(Targeted) {
g.Kamikaze = false g.Kamikaze = false
@@ -607,30 +667,7 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
p := &g.Player p := &g.Player
p.Stats.Exp += tp.Stats.Exp p.Stats.Exp += tp.Stats.Exp
// If the monster was a venus flytrap, un-hold him g.killedSpecial(tp)
switch tp.Type {
case 'F':
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
tp.Room.Gold = pos
}
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Kind = KindGold
gold.GoldValue = g.goldCalc()
if g.save(VsMagic) {
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}
}
// Get rid of the monster. // Get rid of the monster.
mname := g.setMname(tp) mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true) g.removeMon(tp.Pos, tp, true)
@@ -657,6 +694,37 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
} }
} }
// killedSpecial handles deaths with side effects: a flytrap releases its
// grip and a leprechaun drops its gold (the switch of fight.c killed).
func (g *RogueGame) killedSpecial(tp *Monster) {
p := &g.Player
// If the monster was a venus flytrap, un-hold him
switch tp.Type {
case 'F':
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
tp.Room.Gold = pos
}
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Kind = KindGold
gold.GoldValue = g.goldCalc()
if g.save(VsMagic) {
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}
}
}
// flushType flushes typeahead for the fight_flush option (mach_dep.c // flushType flushes typeahead for the fight_flush option (mach_dep.c
// flush_type / curses flushinp). // flush_type / curses flushinp).
func (g *RogueGame) flushType() { func (g *RogueGame) flushType() {

View File

@@ -7,10 +7,10 @@ import "testing"
func mkGame(t *testing.T, seed int32) *RogueGame { func mkGame(t *testing.T, seed int32) *RogueGame {
t.Helper() t.Helper()
g := NewGame(Config{Seed: seed, Term: &testTerm{}}) g := New(Params{Seed: seed, Term: &testTerm{}})
g.NewLevel() g.NewLevel()
g.Oldpos = g.Player.Pos g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos) g.Oldrp = g.roomIn(g.Player.Pos)
return g return g
} }
@@ -32,7 +32,7 @@ func TestRollEmParsesMultiAttackDice(t *testing.T) {
// With attacker level 20 vs armor 10, swing always hits // With attacker level 20 vs armor 10, swing always hits
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of // (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of
// 1x4 + str bonus 1 each must deal between 6 and 15 damage. // 1x4 + str bonus 1 each must deal between 6 and 15 damage.
if !g.rollEm(att, def, nil, false) { if !g.rollAttacks(att, def, nil, false) {
t.Fatal("attack with guaranteed swing missed") t.Fatal("attack with guaranteed swing missed")
} }
@@ -79,24 +79,6 @@ func TestAttackHurtsPlayer(t *testing.T) {
} }
} }
func TestDeathUnwindsWithGameEnd(t *testing.T) {
g := mkGame(t, 11)
defer func() {
r := recover()
if _, ok := r.(gameEnd); !ok {
t.Fatalf("death did not unwind with gameEnd, got %v", r)
}
if g.Playing {
t.Error("still playing after death")
}
}()
g.Options.Tombstone = false
g.death('K')
}
func TestRunnersChaseHero(t *testing.T) { func TestRunnersChaseHero(t *testing.T) {
g := mkGame(t, 3) g := mkGame(t, 3)
// Place a hobgoblin a few squares away in the hero's room and set it // Place a hobgoblin a few squares away in the hero's room and set it

View File

@@ -31,9 +31,9 @@ type Options struct {
InvType int // inven: inventory style (InvOver/InvSlow/InvClear) InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
} }
// Config carries everything needed to construct a game. // Params carries everything needed to construct a game.
type Config struct { type Params struct {
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env) Seed int32 // dungeon number; caller derives it (time+pid or SEED)
Name string // player name (overridden by ROGUEOPTS name=) Name string // player name (overridden by ROGUEOPTS name=)
RogueOpts string // the ROGUEOPTS environment string RogueOpts string // the ROGUEOPTS environment string
Home string // home directory (save file default location) Home string // home directory (save file default location)
@@ -44,7 +44,7 @@ type Config struct {
// RogueGame is one complete game of Rogue: every piece of state that was a // RogueGame is one complete game of Rogue: every piece of state that was a
// global (or file-scope static) in the C sources, plus the terminal it is // global (or file-scope static) in the C sources, plus the terminal it is
// played on. Construct with NewGame, then call Run. // played on. Construct with New, then call Run.
// //
// The struct grows with the port; fields appear in the phase that ports the // The struct grows with the port; fields appear in the phase that ports the
// code owning them. // code owning them.
@@ -110,7 +110,7 @@ type RogueGame struct {
// screen / messages // screen / messages
scr *Screen scr *Screen
Msgs MsgLine Msgs MessageLine
statusCache statusCache statusCache statusCache
invPage invPage // things.c discovery-list pagination statics invPage invPage // things.c discovery-list pagination statics
@@ -138,23 +138,27 @@ type RogueGame struct {
rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C
restored bool // game came from a save file; Run skips setup restored bool // game came from a save file; Run skips setup
// data is the game's copy of the static tables (extern.c and friends).
data *gameData
} }
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item // New builds a game from params, seeds the RNG, and randomizes the item
// appearance tables (the front half of main.c main(); the player roll-up // appearance tables (the front half of main.c main(); the player roll-up
// and first level arrive with later porting phases). // and first level arrive with later porting phases).
func NewGame(cfg Config) *RogueGame { func New(params Params) *RogueGame {
g := &RogueGame{ g := &RogueGame{
Rng: &Rng{Seed: cfg.Seed}, data: newGameData(),
Dnum: int(cfg.Seed), Rng: &Rng{Seed: params.Seed},
Whoami: cfg.Name, Dnum: int(params.Seed),
Whoami: params.Name,
Fruit: "slime-mold", Fruit: "slime-mold",
Home: cfg.Home, Home: params.Home,
Wizard: cfg.Wizard, Wizard: params.Wizard,
NoScore: cfg.Wizard, NoScore: params.Wizard,
Playing: true, Playing: true,
Depth: 1, Depth: 1,
ScorePath: cfg.ScorePath, ScorePath: params.ScorePath,
LastScore: -1, LastScore: -1,
} }
g.Options = Options{ g.Options = Options{
@@ -164,19 +168,20 @@ func NewGame(cfg Config) *RogueGame {
} }
g.InvDescribe = true g.InvDescribe = true
g.Msgs.SaveMsg = true g.Msgs.SaveMsg = true
g.scr = NewScreen(cfg.Term) g.scr = NewScreen(params.Term)
g.FileName = cfg.Home + "/rogue.save" g.Msgs.attach(g.scr, g.look, g.readchar)
g.FileName = params.Home + "/rogue.save"
g.rogueOpts = cfg.RogueOpts g.rogueOpts = params.RogueOpts
if cfg.Wizard { if params.Wizard {
g.Player.Flags.Set(SenseMonsters) g.Player.Flags.Set(SenseMonsters)
} }
if cfg.RogueOpts != "" { if params.RogueOpts != "" {
g.ParseOpts(cfg.RogueOpts) g.ParseOpts(params.RogueOpts)
} }
g.Monsters = monsterTable g.Monsters = g.data.monsterTable
g.Items.Group = 2 // weapons.c: int group = 2 g.Items.Group = 2 // weapons.c: int group = 2
for i := range g.Level.Passages { for i := range g.Level.Passages {
@@ -194,37 +199,45 @@ func NewGame(cfg Config) *RogueGame {
} }
// Run plays the game to its end: the back half of main.c main() plus // Run plays the game to its end: the back half of main.c main() plus
// playit(). It returns after death, victory, quitting, or saving. // playit(). It does not return — the game ends by exiting the process
func (g *RogueGame) Run() error { // (see myExit); one game run is one process.
// A gameEnd panic is the port's my_exit(): recovering it here makes func (g *RogueGame) Run() {
// Run return normally (zero values), restoring the terminal via the g.startLevel()
// caller's defers. g.playit()
defer func() { }
if r := recover(); r != nil {
if _, ok := r.(gameEnd); ok { // startLevel draws the first level and starts the standing daemons and
return // normal game over / save exit // fuses for a fresh game; a restored game brings its own (the back half
// of main.c main()).
func (g *RogueGame) startLevel() {
if g.restored {
return
} }
panic(r)
}
}()
if !g.restored {
g.NewLevel() // draw current level g.NewLevel() // draw current level
// Start up daemons and fuses // Start up daemons and fuses
g.StartDaemon(DRunners, 0, After) g.StartDaemon(DRunners, 0, After)
g.StartDaemon(DDoctor, 0, After) g.StartDaemon(DDoctor, 0, After)
g.Fuse(DSwander, 0, wanderTime(g), After) g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After) g.StartDaemon(DStomach, 0, After)
}
g.playit()
return nil
} }
// playit is the main loop of the program (main.c playit). // playit is the main loop of the program (main.c playit).
func (g *RogueGame) playit() { func (g *RogueGame) playit() {
g.prePlay()
for g.Playing {
g.command() // command execution
}
g.endit()
}
// prePlay does the option and position setup at the top of playit,
// before the command loop (main.c playit). It is split out so tests can
// drive a bounded number of turns; the loop itself never returns,
// because game-over exits the process.
func (g *RogueGame) prePlay() {
// set up defaults for modern terminals: curses' md_hasclreol() is // set up defaults for modern terminals: curses' md_hasclreol() is
// always true, so the C default inventory style applies // always true, so the C default inventory style applies
if !g.restored { if !g.restored {
@@ -237,13 +250,7 @@ func (g *RogueGame) playit() {
} }
g.Oldpos = g.Player.Pos g.Oldpos = g.Player.Pos
g.Oldrp = g.roomIn(g.Player.Pos)
g.Oldrp = g.roomin(g.Player.Pos)
for g.Playing {
g.command() // command execution
}
g.endit()
} }
// endit exits the game (main.c endit). // endit exits the game (main.c endit).

View File

@@ -8,7 +8,7 @@ import "strings"
// initPlayer rolls her up (init.c init_player). // initPlayer rolls her up (init.c init_player).
func (g *RogueGame) initPlayer() { func (g *RogueGame) initPlayer() {
p := &g.Player p := &g.Player
p.MaxStats = initStats p.MaxStats = g.data.initStats
p.Stats = p.MaxStats p.Stats = p.MaxStats
p.FoodLeft = HungerTime p.FoodLeft = HungerTime
// Give him some food // Give him some food
@@ -20,7 +20,7 @@ func (g *RogueGame) initPlayer() {
obj = newObject() obj = newObject()
obj.Kind = KindArmor obj.Kind = KindArmor
obj.Which = int(ArmorRingMail) obj.Which = int(ArmorRingMail)
obj.ArmorClass = aClass[ArmorRingMail] - 1 obj.ArmorClass = g.data.aClass[ArmorRingMail] - 1
obj.Flags.Set(Known) obj.Flags.Set(Known)
obj.Count = 1 obj.Count = 1
p.CurArmor = obj p.CurArmor = obj
@@ -50,19 +50,19 @@ func (g *RogueGame) initPlayer() {
// initColors initializes the potion color scheme for this game // initColors initializes the potion color scheme for this game
// (init.c init_colors). // (init.c init_colors).
func (g *RogueGame) initColors() { func (g *RogueGame) initColors() {
used := make([]bool, len(rainbow)) used := make([]bool, len(g.data.rainbow))
for i := range NumPotionTypes { for i := range NumPotionTypes {
var j int var j int
for { for {
j = g.rnd(len(rainbow)) j = g.rnd(len(g.data.rainbow))
if !used[j] { if !used[j] {
break break
} }
} }
used[j] = true used[j] = true
g.Items.PotColors[i] = rainbow[j] g.Items.PotColors[i] = g.data.rainbow[j]
} }
} }
@@ -75,7 +75,7 @@ func (g *RogueGame) initNames() {
for ; nwords > 0; nwords-- { for ; nwords > 0; nwords-- {
nsyl := g.rnd(3) + 1 nsyl := g.rnd(3) + 1
for ; nsyl > 0; nsyl-- { for ; nsyl > 0; nsyl-- {
sp := sylls[g.rnd(len(sylls))] sp := g.data.sylls[g.rnd(len(g.data.sylls))]
if cp.Len()+len(sp) > MaxNameLen { if cp.Len()+len(sp) > MaxNameLen {
break break
} }
@@ -93,47 +93,47 @@ func (g *RogueGame) initNames() {
// initStones initializes the ring stone setting scheme for this game // initStones initializes the ring stone setting scheme for this game
// (init.c init_stones). // (init.c init_stones).
func (g *RogueGame) initStones() { func (g *RogueGame) initStones() {
used := make([]bool, len(stoneTable)) used := make([]bool, len(g.data.stoneTable))
for i := range NumRingTypes { for i := range NumRingTypes {
var j int var j int
for { for {
j = g.rnd(len(stoneTable)) j = g.rnd(len(g.data.stoneTable))
if !used[j] { if !used[j] {
break break
} }
} }
used[j] = true used[j] = true
g.Items.RingStones[i] = stoneTable[j].Name g.Items.RingStones[i] = g.data.stoneTable[j].Name
g.Items.Rings[i].Worth += stoneTable[j].Value g.Items.Rings[i].Worth += g.data.stoneTable[j].Value
} }
} }
// initMaterials initializes the construction materials for wands and staffs // initMaterials initializes the construction materials for wands and staffs
// (init.c init_materials). // (init.c init_materials).
func (g *RogueGame) initMaterials() { func (g *RogueGame) initMaterials() {
used := make([]bool, len(woods)) used := make([]bool, len(g.data.woods))
metused := make([]bool, len(metals)) metused := make([]bool, len(g.data.metals))
for i := range NumWandTypes { for i := range NumWandTypes {
var str string var str string
for { for {
if g.rnd(2) == 0 { if g.rnd(2) == 0 {
j := g.rnd(len(metals)) j := g.rnd(len(g.data.metals))
if !metused[j] { if !metused[j] {
g.Items.WandType[i] = "wand" g.Items.WandType[i] = wandName
str = metals[j] str = g.data.metals[j]
metused[j] = true metused[j] = true
break break
} }
} else { } else {
j := g.rnd(len(woods)) j := g.rnd(len(g.data.woods))
if !used[j] { if !used[j] {
g.Items.WandType[i] = "staff" g.Items.WandType[i] = staffName
str = woods[j] str = g.data.woods[j]
used[j] = true used[j] = true
break break
@@ -156,13 +156,13 @@ func sumProbs(info []ObjInfo) {
// initProbs copies the base tables into the game and initializes the // initProbs copies the base tables into the game and initializes the
// probabilities for the various items (init.c init_probs). // probabilities for the various items (init.c init_probs).
func (g *RogueGame) initProbs() { func (g *RogueGame) initProbs() {
g.Items.Things = baseThings g.Items.Things = g.data.baseThings
g.Items.Potions = basePotInfo g.Items.Potions = g.data.basePotInfo
g.Items.Scrolls = baseScrInfo g.Items.Scrolls = g.data.baseScrInfo
g.Items.Rings = baseRingInfo g.Items.Rings = g.data.baseRingInfo
g.Items.Sticks = baseWsInfo g.Items.Sticks = g.data.baseWsInfo
g.Items.Weapons = baseWeapInfo g.Items.Weapons = g.data.baseWeapInfo
g.Items.Armors = baseArmInfo g.Items.Armors = g.data.baseArmInfo
sumProbs(g.Items.Things[:]) sumProbs(g.Items.Things[:])
sumProbs(g.Items.Potions[:]) sumProbs(g.Items.Potions[:])
@@ -177,7 +177,7 @@ func (g *RogueGame) initProbs() {
// hallucinating (init.c pick_color). // hallucinating (init.c pick_color).
func (g *RogueGame) pickColor(col string) string { func (g *RogueGame) pickColor(col string) string {
if g.Player.On(Hallucinating) { if g.Player.On(Hallucinating) {
return rainbow[g.rnd(len(rainbow))] return g.data.rainbow[g.rnd(len(g.data.rainbow))]
} }
return col return col

View File

@@ -10,9 +10,11 @@ import (
// maxMsg is io.c MAXMSG: how much message fits before --More--. // maxMsg is io.c MAXMSG: how much message fits before --More--.
const maxMsg = NumCols - len("--More--") - 1 const maxMsg = NumCols - len("--More--") - 1
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus // MessageLine is the io.c message machinery: the static msgbuf/newpos
// the related globals (mpos, huh, and the message-behavior flags). // pair plus the related globals (mpos, huh, and the message-behavior
type MsgLine struct { // flags). It owns the top line of the screen; attach wires in the
// display and input it needs.
type MessageLine struct {
buf strings.Builder // msgbuf buf strings.Builder // msgbuf
newpos int newpos int
Mpos int // where cursor is on top line Mpos int // where cursor is on top line
@@ -20,51 +22,82 @@ type MsgLine struct {
SaveMsg bool // remember last msg SaveMsg bool // remember last msg
LowerMsg bool // messages should start w/lower case LowerMsg bool // messages should start w/lower case
MsgEsc bool // check for ESC from msg's --More-- MsgEsc bool // check for ESC from msg's --More--
scr *Screen // the top line lives on scr.Std
look func(wakeup bool) // redraw before a --More-- (misc.c look)
readChar func() byte // input for --More-- prompts
} }
// Msg displays a message at the top of the screen (io.c msg). It returns // Msg displays a message at the top of the screen (io.c msg). It returns
// Escape if the player escaped out of a --More--, ^Escape otherwise (the C // Escape if the player escaped out of a --More--, ^Escape otherwise (the
// convention: callers compare against ESCAPE). // C convention: callers compare against ESCAPE).
func (g *RogueGame) msg(format string, a ...any) int { func (m *MessageLine) Msg(format string, a ...any) int {
// if the string is "", just clear the line // if the string is "", just clear the line
if format == "" { if format == "" {
g.move(0, 0) m.scr.Std.Move(0, 0)
g.clrtoeol() m.scr.Std.Clrtoeol()
g.Msgs.Mpos = 0 m.Mpos = 0
return ^Escape return ^Escape
} }
// otherwise add to the message and flush it out // otherwise add to the message and flush it out
g.doaddf(format, a...) m.doaddf(format, a...)
return g.endmsg() return m.End()
} }
// addmsgf adds things to the current message (io.c addmsg). // Addf adds things to the current message (io.c addmsg).
func (g *RogueGame) addmsgf(format string, a ...any) { func (m *MessageLine) Addf(format string, a ...any) {
g.doaddf(format, a...) m.doaddf(format, a...)
} }
// endmsg displays a new msg, giving the player a chance to see the previous // End displays a new msg, giving the player a chance to see the previous
// one if it is up there with the --More-- (io.c endmsg). // one if it is up there with the --More-- (io.c endmsg).
func (g *RogueGame) endmsg() int { func (m *MessageLine) End() int {
m := &g.Msgs
if m.SaveMsg { if m.SaveMsg {
m.Huh = m.buf.String() m.Huh = m.buf.String()
} }
if m.Mpos != 0 { if m.Mpos != 0 && m.promptMore() == Escape {
g.look(false) return Escape
g.mvaddstr(0, m.Mpos, "--More--") }
g.refresh() // All messages should start with uppercase, except ones that start
// with a pack addressing character
out := m.buf.String()
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg &&
(len(out) <= 1 || out[1] != ')') {
out = string(toUpper(out[0])) + out[1:]
}
m.scr.Std.MvAddStr(0, 0, out)
m.scr.Std.Clrtoeol()
m.Mpos = m.newpos
m.newpos = 0
m.buf.Reset()
m.scr.Refresh()
return ^Escape
}
// promptMore shows the --More-- prompt and waits for the reader to
// acknowledge; Escape means the player bailed out (the Mpos block of
// io.c endmsg).
func (m *MessageLine) promptMore() int {
m.look(false)
m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
m.scr.Refresh()
if !m.MsgEsc { if !m.MsgEsc {
g.waitFor(' ') m.waitForSpace()
} else {
return ^Escape
}
for { for {
ch := g.readchar() ch := m.readChar()
if ch == ' ' { if ch == ' ' {
break return ^Escape
} }
if ch == Escape { if ch == Escape {
@@ -75,40 +108,51 @@ func (g *RogueGame) endmsg() int {
return Escape return Escape
} }
} }
}
// attach wires the message line to its display and input; NewGame and
// Restore call it once the screen and game exist.
func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) {
m.scr = scr
m.look = look
m.readChar = readChar
}
// waitForSpace absorbs input until the player types a space: the
// --More-- acknowledgement (io.c wait_for).
func (m *MessageLine) waitForSpace() {
for {
if m.readChar() == ' ' {
return
} }
} }
// All messages should start with uppercase, except ones that start
// with a pack addressing character
out := m.buf.String()
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg &&
(len(out) <= 1 || out[1] != ')') {
out = string(toUpper(out[0])) + out[1:]
}
g.mvaddstr(0, 0, out)
g.clrtoeol()
m.Mpos = m.newpos
m.newpos = 0
m.buf.Reset()
g.refresh()
return ^Escape
} }
// doaddf performs an add onto the message buffer (io.c doadd). // doaddf performs an add onto the message buffer (io.c doadd).
func (g *RogueGame) doaddf(format string, a ...any) { func (m *MessageLine) doaddf(format string, a ...any) {
m := &g.Msgs
s := fmt.Sprintf(format, a...) s := fmt.Sprintf(format, a...)
if len(s)+m.newpos >= maxMsg { if len(s)+m.newpos >= maxMsg {
g.endmsg() m.End()
} }
m.buf.WriteString(s) m.buf.WriteString(s)
m.newpos = m.buf.Len() m.newpos = m.buf.Len()
} }
// msg, addmsgf, and endmsg are the game-side shorthands for the message
// line; the machinery lives on MessageLine.
func (g *RogueGame) msg(format string, a ...any) int {
return g.Msgs.Msg(format, a...)
}
func (g *RogueGame) addmsgf(format string, a ...any) {
g.Msgs.Addf(format, a...)
}
func (g *RogueGame) endmsg() {
g.Msgs.End()
}
// stepOk returns true if it is ok to step on ch (io.c step_ok). // stepOk returns true if it is ok to step on ch (io.c step_ok).
func stepOk(ch byte) bool { func stepOk(ch byte) bool {
switch ch { switch ch {
@@ -146,8 +190,6 @@ type statusCache struct {
init bool init bool
} }
var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"}
// status displays the important stats line, keeping the cursor where it was // status displays the important stats line, keeping the cursor where it was
// (io.c status). // (io.c status).
func (g *RogueGame) status() { func (g *RogueGame) status() {
@@ -160,9 +202,7 @@ func (g *RogueGame) status() {
temp = p.CurArmor.ArmorClass temp = p.CurArmor.ArmorClass
} }
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp && if g.statusUnchanged(temp) {
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
return return
} }
@@ -192,7 +232,7 @@ func (g *RogueGame) status() {
"Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s", "Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s",
g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP, g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP,
p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp, p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp,
hungerStateName[p.HungryState]) g.data.hungerStateName[p.HungryState])
if g.StatMsg { if g.StatMsg {
g.move(0, 0) g.move(0, 0)
g.msg("%s", line) g.msg("%s", line)
@@ -205,6 +245,18 @@ func (g *RogueGame) status() {
g.move(oy, ox) g.move(oy, ox)
} }
// statusUnchanged reports whether the status line still shows current
// values, so it need not be redrawn (the shadow-variable check of io.c
// status). temp is the effective armor class.
func (g *RogueGame) statusUnchanged(temp int) bool {
s := &g.statusCache
p := &g.Player
return s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg
}
// waitFor sits around until the guy types the right key (io.c wait_for). // waitFor sits around until the guy types the right key (io.c wait_for).
func (g *RogueGame) waitFor(ch byte) { func (g *RogueGame) waitFor(ch byte) {
if ch == '\n' { if ch == '\n' {

View File

@@ -51,6 +51,29 @@ func (l *Level) VisibleChar(y, x int) byte {
return l.Char(y, x) return l.Char(y, x)
} }
// ObjectAt finds the unclaimed object at (y, x) (misc.c find_obj).
func (l *Level) ObjectAt(y, x int) *Object {
for _, obj := range l.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
}
// AddObject puts an object on the level (list.c attach on lvl_obj).
func (l *Level) AddObject(obj *Object) { attachObj(&l.Objects, obj) }
// RemoveObject takes an object off the level (list.c detach on lvl_obj).
func (l *Level) RemoveObject(obj *Object) { detachObj(&l.Objects, obj) }
// AddMonster puts a monster on the level (list.c attach on mlist).
func (l *Level) AddMonster(m *Monster) { attachMon(&l.Monsters, m) }
// RemoveMonster takes a monster off the level (list.c detach on mlist).
func (l *Level) RemoveMonster(m *Monster) { detachMon(&l.Monsters, m) }
// goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth. // goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth.
func (g *RogueGame) goldCalc() int { func (g *RogueGame) goldCalc() int {
return g.rnd(50+10*g.Depth) + 2 return g.rnd(50+10*g.Depth) + 2

View File

@@ -4,11 +4,23 @@ package game
// and small utilities. call_it arrives with the scroll/potion phase (it // and small utilities. call_it arrives with the scroll/potion phase (it
// needs the get_str line editor). // needs the get_str line editor).
// lookScan carries the state of one look() glance while it examines the
// nine squares around the hero.
type lookScan struct {
hero Coord
pch byte // map character under the hero
pfl PlaceFlags // map flags under the hero
wakeup bool
doorStop bool // door-stop checking applies (mid-run)
sy, sx, ey, ex int
sumhero, diffhero int
passcount int
}
// look takes a quick glance all around the player (misc.c look). // look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) { func (g *RogueGame) look(wakeup bool) {
p := &g.Player p := &g.Player
hero := p.Pos hero := p.Pos
passcount := 0
rp := p.Room rp := p.Room
if g.Oldpos != hero { if g.Oldpos != hero {
@@ -17,85 +29,126 @@ func (g *RogueGame) look(wakeup bool) {
g.Oldrp = rp g.Oldrp = rp
} }
ey := hero.Y + 1 s := lookScan{
ex := hero.X + 1 hero: hero,
sx := hero.X - 1 wakeup: wakeup,
sy := hero.Y - 1 sy: hero.Y - 1,
sx: hero.X - 1,
ey: hero.Y + 1,
ex: hero.X + 1,
}
sumhero, diffhero := 0, 0 s.doorStop = g.DoorStop && !g.Firstmove
if g.DoorStop && !g.Firstmove && g.Running { if s.doorStop && g.Running {
sumhero = hero.Y + hero.X s.sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X s.diffhero = hero.Y - hero.X
} }
pp := g.Level.At(hero.Y, hero.X) pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch s.pch = pp.Ch
pfl := pp.Flags s.pfl = pp.Flags
for y := sy; y <= ey; y++ { g.lookAround(&s)
if s.doorStop && s.passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
}
// lookAround runs the nine-square scan of look().
func (g *RogueGame) lookAround(s *lookScan) {
for y := s.sy; y <= s.ey; y++ {
if y <= 0 || y >= NumLines-1 { if y <= 0 || y >= NumLines-1 {
continue continue
} }
for x := sx; x <= ex; x++ { for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols { if x < 0 || x >= NumCols {
continue continue
} }
if !p.On(Blind) { g.lookCell(s, y, x)
if y == hero.Y && x == hero.X {
continue
} }
} }
}
// lookCell examines one square around the hero: visibility rules, trip
// and monster rendering, drawing, and run-stop checks (the loop body of
// misc.c look).
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
if g.lookSkips(s, pp, y, x) {
ch := pp.Ch return
if ch == ' ' { // nothing need be done with a ' '
continue
}
fp := &pp.Flags
if pch != Door && ch != Door {
if (pfl & FPassage) != (*fp & FPassage) {
continue
}
}
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
continue
}
} }
tp := pp.Monst tp := pp.Monst
switch { ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
case tp == nil: if skip {
ch = g.tripCh(y, x, ch) return
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
continue if !g.lookShow(s, tp, ch, y, x) {
default: return
if wakeup {
g.wakeMonster(y, x)
} }
if g.seeMonst(tp) { if s.doorStop && g.Running {
if p.On(Hallucinating) { g.lookRunCheck(s, ch, y, x)
ch = g.randomMonsterLetter()
} else {
ch = tp.Disguise
}
} }
}
// lookSkips reports whether look ignores this square entirely: the
// hero's own square when sighted, blank rock, passage squares of
// another network, and diagonals the hero could not step to (the guard
// chain of the misc.c look loop).
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
return true
} }
if p.On(Blind) && (y != hero.Y || x != hero.X) { if pp.Ch == ' ' { // nothing need be done with a ' '
continue return true
}
return lookForeignPassage(s, pp.Flags, pp.Ch) ||
g.lookDiagonalBlocked(s, pp.Flags, pp.Ch, y, x)
}
// lookForeignPassage hides passage squares belonging to a different
// passage network than the hero's (misc.c look).
func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
if s.pch != Door && ch != Door {
return (s.pfl & FPassage) != (fp & FPassage)
}
return false
}
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
// not actually step to (misc.c look).
func (g *RogueGame) lookDiagonalBlocked(s *lookScan, fp PlaceFlags, ch byte, y, x int) bool {
if !fp.Has(FPassage) && ch != Door {
return false
}
if !s.pfl.Has(FPassage) && s.pch != Door {
return false
}
return s.hero.X != x && s.hero.Y != y &&
!stepOk(g.Level.Char(y, s.hero.X)) && !stepOk(g.Level.Char(s.hero.Y, x))
}
// lookShow draws the square if it changed; it reports false when a
// blind hero cannot see it at all (the draw part of the look loop).
func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
p := &g.Player
if p.On(Blind) && (y != s.hero.Y || x != s.hero.X) {
return false
} }
g.move(y, x) g.move(y, x)
@@ -108,66 +161,88 @@ func (g *RogueGame) look(wakeup bool) {
g.addch(ch) g.addch(ch)
} }
if g.DoorStop && !g.Firstmove && g.Running { return true
switch g.RunCh { }
case 'h':
if x == ex { // lookCellChar picks what the square shows: trip rendering for empty
continue // squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop).
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
p := &g.Player
switch {
case tp == nil:
return g.tripCh(y, x, ch), false
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
case 'j':
if y == sy { return ch, true
continue default:
if s.wakeup {
g.wakeMonster(y, x)
} }
case 'k':
if y == ey { if g.seeMonst(tp) {
continue if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
} }
case 'l':
if x == sx { return tp.Disguise, false
continue
} }
case 'y':
if (y+x)-sumhero >= 1 { return ch, false
continue
}
case 'u':
if (y-x)-diffhero >= 1 {
continue
}
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
} }
}
// lookRunCheck decides whether what this square shows should stop a run
// (the DoorStop tail of the misc.c look loop). Squares on the running
// edge are ignored.
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
if s.atRunEdge(g.RunCh, y, x) {
return
} }
switch ch { switch ch {
case Door: case Door:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
g.Running = false g.Running = false
} }
case Passage: case Passage:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
passcount++ s.passcount++
} }
case Floor, '|', '-', ' ': case Floor, '|', '-', ' ':
default: default:
g.Running = false g.Running = false
} }
} }
}
// atRunEdge reports whether (y, x) sits on the leading edge of the run
// direction, where door-stop checking does not apply (the first RunCh
// switch of the misc.c look loop).
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
switch runCh {
case 'h':
return x == s.ex
case 'j':
return y == s.sy
case 'k':
return y == s.ey
case 'l':
return x == s.sx
case 'y':
return (y+x)-s.sumhero >= 1
case 'u':
return (y-x)-s.diffhero >= 1
case 'n':
return (y+x)-s.sumhero <= -1
case 'b':
return (y-x)-s.diffhero <= -1
} }
if g.DoorStop && !g.Firstmove && passcount > 1 { return false
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
} }
// tripCh returns the character for this space, taking into account whether // tripCh returns the character for this space, taking into account whether
@@ -223,21 +298,10 @@ func (g *RogueGame) showFloor() bool {
return true return true
} }
// findObj finds the unclaimed object at (y, x) (misc.c find_obj).
func (g *RogueGame) findObj(y, x int) *Object {
for _, obj := range g.Level.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
}
// eat lets her try to eat something (misc.c eat). // eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() { func (g *RogueGame) eat() {
obj := g.getItem("eat", KindFood) obj, ok := g.promptPackItem("eat", KindFood)
if obj == nil { if !ok {
return return
} }
@@ -286,8 +350,8 @@ func (g *RogueGame) checkLevel() {
p := &g.Player p := &g.Player
var i int var i int
for i = 0; eLevels[i] != 0; i++ { for i = 0; g.data.eLevels[i] != 0; i++ {
if eLevels[i] > p.Stats.Exp { if g.data.eLevels[i] > p.Stats.Exp {
break break
} }
} }
@@ -305,9 +369,9 @@ func (g *RogueGame) checkLevel() {
} }
} }
// chgStr modifies the player's strength, keeping track of the highest it // changeStrength modifies the player's strength, keeping track of the
// has been (misc.c chg_str). // highest it has been (misc.c chg_str).
func (g *RogueGame) chgStr(amt int) { func (g *RogueGame) changeStrength(amt int) {
if amt == 0 { if amt == 0 {
return return
} }
@@ -362,10 +426,10 @@ func (g *RogueGame) addHaste(potion bool) bool {
// aggravate aggravates all the monsters on this level (misc.c aggravate). // aggravate aggravates all the monsters on this level (misc.c aggravate).
func (g *RogueGame) aggravate() { func (g *RogueGame) aggravate() {
// runto() can splice the monster list while we walk it, so iterate a copy. // runTo() can splice the monster list while we walk it, so iterate a copy.
monsters := append([]*Monster(nil), g.Level.Monsters...) monsters := append([]*Monster(nil), g.Level.Monsters...)
for _, mp := range monsters { for _, mp := range monsters {
g.runto(mp.Pos) g.runTo(mp.Pos)
} }
} }
@@ -406,9 +470,9 @@ func (g *RogueGame) isCurrent(obj *Object) bool {
return false return false
} }
// getDir sets up the direction coordinate for use in various "prefix" // promptDirection sets up the direction coordinate for use in various
// commands (misc.c get_dir). // "prefix" commands (misc.c get_dir).
func (g *RogueGame) getDir() bool { func (g *RogueGame) promptDirection() bool {
if g.Again && g.LastDir != 0 { if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt g.Delta = g.lastDelt
g.DirCh = g.LastDir g.DirCh = g.LastDir
@@ -420,40 +484,22 @@ func (g *RogueGame) getDir() bool {
} }
for { for {
gotit := true g.DirCh = g.readchar()
if g.DirCh == Escape {
switch g.DirCh = g.readchar(); g.DirCh {
case 'h', 'H':
g.Delta = Coord{X: -1, Y: 0}
case 'j', 'J':
g.Delta = Coord{X: 0, Y: 1}
case 'k', 'K':
g.Delta = Coord{X: 0, Y: -1}
case 'l', 'L':
g.Delta = Coord{X: 1, Y: 0}
case 'y', 'Y':
g.Delta = Coord{X: -1, Y: -1}
case 'u', 'U':
g.Delta = Coord{X: 1, Y: -1}
case 'b', 'B':
g.Delta = Coord{X: -1, Y: 1}
case 'n', 'N':
g.Delta = Coord{X: 1, Y: 1}
case Escape:
g.LastDir = 0 g.LastDir = 0
g.resetLast() g.resetLast()
return false return false
default:
g.Msgs.Mpos = 0
g.msg("%s", prompt)
gotit = false
} }
if gotit { if d, ok := deltaFor(g.DirCh); ok {
g.Delta = d
break break
} }
g.Msgs.Mpos = 0
g.msg("%s", prompt)
} }
g.DirCh = toLower(g.DirCh) g.DirCh = toLower(g.DirCh)
@@ -462,14 +508,7 @@ func (g *RogueGame) getDir() bool {
} }
if g.Player.On(Confused) && g.rnd(5) == 0 { if g.Player.On(Confused) && g.rnd(5) == 0 {
for { g.confuseDirection()
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
break
}
}
} }
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -477,6 +516,44 @@ func (g *RogueGame) getDir() bool {
return true return true
} }
// confuseDirection randomizes the chosen direction for a confused hero
// (the ISHUH tail of misc.c get_dir).
func (g *RogueGame) confuseDirection() {
for {
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
return
}
}
}
// deltaFor maps a direction key to its movement delta; ok is false for
// keys that are not directions (the switch of misc.c get_dir).
func deltaFor(ch byte) (Coord, bool) {
switch ch {
case 'h', 'H':
return Coord{X: -1, Y: 0}, true
case 'j', 'J':
return Coord{X: 0, Y: 1}, true
case 'k', 'K':
return Coord{X: 0, Y: -1}, true
case 'l', 'L':
return Coord{X: 1, Y: 0}, true
case 'y', 'Y':
return Coord{X: -1, Y: -1}, true
case 'u', 'U':
return Coord{X: 1, Y: -1}, true
case 'b', 'B':
return Coord{X: -1, Y: 1}, true
case 'n', 'N':
return Coord{X: 1, Y: 1}, true
}
return Coord{}, false
}
// callIt calls an object something after use (misc.c call_it). // callIt calls an object something after use (misc.c call_it).
func (g *RogueGame) callIt(info *ObjInfo) { func (g *RogueGame) callIt(info *ObjInfo) {
if info.Know { if info.Know {
@@ -493,22 +570,17 @@ func (g *RogueGame) callIt(info *ObjInfo) {
} }
} }
// thingList is misc.c rnd_thing()'s static table.
var thingList = []byte{
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
}
// rndThing picks a random thing appropriate for this level (misc.c // rndThing picks a random thing appropriate for this level (misc.c
// rnd_thing). // rnd_thing).
func (g *RogueGame) rndThing() byte { func (g *RogueGame) rndThing() byte {
var i int var i int
if g.Depth >= AmuletLevel { if g.Depth >= AmuletLevel {
i = g.rnd(len(thingList)) i = g.rnd(len(g.data.thingList))
} else { } else {
i = g.rnd(len(thingList) - 1) i = g.rnd(len(g.data.thingList) - 1)
} }
return thingList[i] return g.data.thingList[i]
} }
// chooseStr picks the first or second string depending on whether the // chooseStr picks the first or second string depending on whether the

View File

@@ -2,24 +2,12 @@ package game
// monsters.c — monster creation and saving throws. // monsters.c — monster creation and saving throws.
// lvlMons and wandMons list monsters in rough order of vorpalness; zero
// entries in wandMons never wander (monsters.c).
var lvlMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
}
var wandMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
}
// randMonster picks a monster to show up; the lower the level, the meaner // randMonster picks a monster to show up; the lower the level, the meaner
// the monster (monsters.c randmonster). // the monster (monsters.c randmonster).
func (g *RogueGame) randMonster(wander bool) byte { func (g *RogueGame) randMonster(wander bool) byte {
mons := &lvlMons mons := &g.data.lvlMons
if wander { if wander {
mons = &wandMons mons = &g.data.wandMons
} }
for { for {
@@ -43,13 +31,13 @@ func (g *RogueGame) randMonster(wander bool) byte {
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) { func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
levAdd := max(g.Depth-AmuletLevel, 0) levAdd := max(g.Depth-AmuletLevel, 0)
attachMon(&g.Level.Monsters, tp) g.Level.AddMonster(tp)
tp.Type = typ tp.Type = typ
tp.Disguise = typ tp.Disguise = typ
tp.Pos = cp tp.Pos = cp
g.move(cp.Y, cp.X) g.move(cp.Y, cp.X)
tp.OldCh = g.inch() tp.OldCh = g.inch()
tp.Room = g.roomin(cp) tp.Room = g.roomIn(cp)
g.Level.SetMonsterAt(cp.Y, cp.X, tp) g.Level.SetMonsterAt(cp.Y, cp.X, tp)
mp := &g.Monsters[tp.Type-'A'] mp := &g.Monsters[tp.Type-'A']
tp.Stats.Lvl = mp.Stats.Lvl + levAdd tp.Stats.Lvl = mp.Stats.Lvl + levAdd
@@ -69,7 +57,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.Pack = nil tp.Pack = nil
if g.Player.IsWearing(RingAggravateMonsters) { if g.Player.IsWearing(RingAggravateMonsters) {
g.runto(cp) g.runTo(cp)
} }
if typ == 'X' { if typ == 'X' {
@@ -104,7 +92,7 @@ func (g *RogueGame) wanderer() {
var cp Coord var cp Coord
for { for {
cp, _ = g.findFloor(true) cp, _ = g.findFloor(true)
if g.roomin(cp) != g.Player.Room { if g.roomIn(cp) != g.Player.Room {
break break
} }
} }
@@ -123,12 +111,12 @@ func (g *RogueGame) wanderer() {
g.standend() g.standend()
} }
g.runto(tp.Pos) g.runTo(tp.Pos)
} }
// wakeMonster is what to do when the hero steps next to a monster // wakeMonster is what to do when the hero steps next to a monster
// (monsters.c wake_monster). // (monsters.c wake_monster).
func (g *RogueGame) wakeMonster(y, x int) *Monster { func (g *RogueGame) wakeMonster(y, x int) {
p := &g.Player p := &g.Player
tp := g.Level.MonsterAt(y, x) tp := g.Level.MonsterAt(y, x)
@@ -136,22 +124,63 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
panic("can't find monster in wake_monster") panic("can't find monster in wake_monster")
} }
ch := tp.Type
// Every time he sees a mean monster, it might start chasing him // Every time he sees a mean monster, it might start chasing him
if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) && if g.meanWakes(tp) {
!p.IsWearing(RingStealth) && !p.On(Levitating) {
tp.Dest = &p.Pos tp.Dest = &p.Pos
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
} }
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) && if g.medusaCatches(tp) {
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) { g.medusaGaze(tp, y, x)
}
// Let greedy ones guard gold
if tp.On(Greedy) && !tp.On(Awake) {
tp.Flags.Set(Awake)
if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold
} else {
tp.Dest = &p.Pos
}
}
}
// meanWakes decides whether a sleeping mean monster starts the chase
// (monsters.c wake_monster). The waking roll happens for any sleeping
// monster, as in C.
func (g *RogueGame) meanWakes(tp *Monster) bool {
p := &g.Player
return !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
!p.IsWearing(RingStealth) && !p.On(Levitating)
}
// medusaCatches reports an uncovered, awake medusa the hero can see
// (monsters.c wake_monster).
func (g *RogueGame) medusaCatches(tp *Monster) bool {
p := &g.Player
return tp.Type == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake)
}
// medusaGaze confuses the hero when the medusa's gaze lands (the M
// block of monsters.c wake_monster).
func (g *RogueGame) medusaGaze(tp *Monster, y, x int) {
p := &g.Player
rp := p.Room rp := p.Room
if (rp != nil && !rp.Flags.Has(Dark)) || if (rp == nil || rp.Flags.Has(Dark)) &&
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { distance(y, x, p.Pos.Y, p.Pos.X) >= LampDist {
return
}
tp.Flags.Set(Found) tp.Flags.Set(Found)
if !g.save(VsMagic) { if g.save(VsMagic) {
return
}
if p.On(Confused) { if p.On(Confused) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration)) g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else { } else {
@@ -168,21 +197,6 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
} }
g.msg("s gaze has confused you") g.msg("s gaze has confused you")
}
}
}
// Let greedy ones guard gold
if tp.On(Greedy) && !tp.On(Awake) {
tp.Flags.Set(Awake)
if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold
} else {
tp.Dest = &p.Pos
}
}
return tp
} }
// givePack gives a pack to a monster if it deserves one (monsters.c // givePack gives a pack to a monster if it deserves one (monsters.c

View File

@@ -2,16 +2,17 @@ package game
// move.c — hero movement commands. // move.c — hero movement commands.
// doRun starts the hero running (move.c do_run). // startRun starts the hero running (move.c do_run).
func (g *RogueGame) doRun(ch byte) { func (g *RogueGame) startRun(ch byte) {
g.Running = true g.Running = true
g.After = false g.After = false
g.RunCh = ch g.RunCh = ch
} }
// doMove checks that a move is legal and handles the consequences — // moveHero checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move). // fighting, picking up, etc. (move.c do_move). The C `goto over`
func (g *RogueGame) doMove(dy, dx int) { // re-check after a passage turn is the retry loop.
func (g *RogueGame) moveHero(dy, dx int) {
p := &g.Player p := &g.Player
g.Firstmove = false g.Firstmove = false
@@ -24,7 +25,7 @@ func (g *RogueGame) doMove(dy, dx int) {
// Do a confused move (maybe) // Do a confused move (maybe)
var nh Coord var nh Coord
if p.On(Confused) && g.rnd(5) != 0 { if p.On(Confused) && g.rnd(5) != 0 {
nh = g.rndmove(&p.Creature) nh = g.randomStep(&p.Creature)
if nh == p.Pos { if nh == p.Pos {
g.After = false g.After = false
g.Running = false g.Running = false
@@ -36,22 +37,103 @@ func (g *RogueGame) doMove(dy, dx int) {
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx} nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
} }
over: for {
// Check if he tried to move off the screen or make an illegal diagonal ch, fl, stop := g.moveTarget(nh)
// move, and stop him if he did. if stop {
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1 return
}
var ( turned, ndy, ndx := g.moveResolve(nh, ch, fl, dy, dx)
ch byte if !turned {
fl PlaceFlags return
) }
// the C goto over: re-check the turned move
dy, dx = ndy, ndx
g.turnRefresh()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
}
}
// moveResolve acts on the square the hero stepped at: a wall may turn a
// passage runner (reported with the new deltas); anything else completes
// or refuses the move (the switch of move.c do_move).
func (g *RogueGame) moveResolve(nh Coord, ch byte, fl PlaceFlags, dy, dx int) (bool, int, int) {
switch ch {
case ' ', '|', '-':
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
return true, ndy, ndx
}
g.Running = false
g.After = false
default:
g.moveEnter(nh, fl, ch)
}
return false, 0, 0
}
// moveEnter completes a step onto a walkable square: doors, traps,
// passages, floor, and things (the entry arms of the move.c do_move
// switch).
func (g *RogueGame) moveEnter(nh Coord, fl PlaceFlags, ch byte) {
p := &g.Player
switch ch {
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
g.enterRoom(nh)
}
case Trap:
tr := g.springTrap(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
case Passage:
// when you're in a corridor, you don't know if you're in a maze
// room or not, and there ain't no way to find out if you're
// leaving a maze room, so it is necessary to always recalculate
// proom.
p.Room = g.roomIn(p.Pos)
case Floor:
if !fl.Has(FReal) {
g.springTrap(p.Pos)
}
default:
g.moveOnto(nh, fl, ch)
return
}
g.finishMove(nh, fl)
}
// offMap reports coordinates outside the walkable map (move.c do_move).
func offMap(nh Coord) bool {
return nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
}
// moveTarget inspects the square the hero is stepping onto: bounds and
// diagonal legality, hidden traps underfoot, and being held. stop means
// the move is refused (the checks of move.c do_move).
func (g *RogueGame) moveTarget(nh Coord) (byte, PlaceFlags, bool) {
p := &g.Player
// Check if he tried to move off the screen or make an illegal
// diagonal move, and stop him if he did.
if offMap(nh) {
return ' ', 0, false // fall into the wall case
}
if !hitBound {
if !g.diagOk(p.Pos, nh) { if !g.diagOk(p.Pos, nh) {
g.After = false g.After = false
g.Running = false g.Running = false
return return 0, 0, true
} }
if g.Running && p.Pos == nh { if g.Running && p.Pos == nh {
@@ -59,9 +141,9 @@ over:
g.Running = false g.Running = false
} }
fl = *g.Level.FlagsAt(nh.Y, nh.X) fl := *g.Level.FlagsAt(nh.Y, nh.X)
ch = g.Level.VisibleChar(nh.Y, nh.X) ch := g.Level.VisibleChar(nh.Y, nh.X)
if !fl.Has(FReal) && ch == Floor { if !fl.Has(FReal) && ch == Floor {
if !p.On(Levitating) { if !p.On(Levitating) {
ch = Trap ch = Trap
@@ -71,96 +153,16 @@ over:
} else if p.On(Held) && ch != 'F' { } else if p.On(Held) && ch != 'F' {
g.msg("you are being held") g.msg("you are being held")
return return 0, 0, true
}
} }
if hitBound { return ch, fl, false
ch = ' ' // fall into the wall case below }
}
switch ch { // moveOnto handles stepping at a monster or onto an item (the default
case ' ', '|', '-': // arm of the move.c do_move switch).
if g.Options.PassGo && g.Running && p.Room.Flags.Has(Gone) && func (g *RogueGame) moveOnto(nh Coord, fl PlaceFlags, ch byte) {
!p.On(Blind) { p := &g.Player
var b1, b2 bool
switch g.RunCh {
case 'h', 'l':
b1 = p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
b2 = p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
if b1 != b2 {
if b1 {
g.RunCh = 'k'
dy = -1
} else {
g.RunCh = 'j'
dy = 1
}
dx = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
case 'j', 'k':
b1 = p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
b2 = p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
if b1 != b2 {
if b1 {
g.RunCh = 'h'
dx = -1
} else {
g.RunCh = 'l'
dx = 1
}
dy = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
}
}
g.Running = false
g.After = false
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
g.enterRoom(nh)
}
g.moveStuff(nh, fl)
case Trap:
tr := g.beTrapped(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
g.moveStuff(nh, fl)
case Passage:
// when you're in a corridor, you don't know if you're in a maze
// room or not, and there ain't no way to find out if you're
// leaving a maze room, so it is necessary to always recalculate
// proom.
p.Room = g.roomin(p.Pos)
g.moveStuff(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.beTrapped(p.Pos)
}
g.moveStuff(nh, fl)
default:
if ch == Stairs { if ch == Stairs {
g.SeenStairs = true g.SeenStairs = true
} }
@@ -173,13 +175,82 @@ over:
g.Take = ch g.Take = ch
} }
g.moveStuff(nh, fl) g.finishMove(nh, fl)
}
} }
} }
// moveStuff is the move_stuff label in do_move: complete the step. // passageTurn checks whether a runner in a gone-room passage should turn
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) { // the corner instead of stopping at a wall (the PASSGO block of move.c
// do_move). It reports whether to turn and the new deltas, updating RunCh.
func (g *RogueGame) passageTurn(dy, dx int) (bool, int, int) {
p := &g.Player
if !g.Options.PassGo || !g.Running || !p.Room.Flags.Has(Gone) ||
p.On(Blind) {
return false, dy, dx
}
switch g.RunCh {
case 'h', 'l':
if turn, ndy := g.passageTurnVertical(); turn {
return true, ndy, 0
}
case 'j', 'k':
if turn, ndx := g.passageTurnHorizontal(); turn {
return true, 0, ndx
}
}
return false, dy, dx
}
// passageTurnVertical decides whether a horizontal runner turns up or
// down at a corner (move.c do_move).
func (g *RogueGame) passageTurnVertical() (bool, int) {
p := &g.Player
b1 := p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
b2 := p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
if b1 == b2 {
return false, 0
}
if b1 {
g.RunCh = 'k'
return true, -1
}
g.RunCh = 'j'
return true, 1
}
// passageTurnHorizontal decides whether a vertical runner turns left or
// right at a corner (move.c do_move).
func (g *RogueGame) passageTurnHorizontal() (bool, int) {
p := &g.Player
b1 := p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
b2 := p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
if b1 == b2 {
return false, 0
}
if b1 {
g.RunCh = 'h'
return true, -1
}
g.RunCh = 'l'
return true, 1
}
// finishMove is the move_stuff label in do_move: complete the step.
func (g *RogueGame) finishMove(nh Coord, fl PlaceFlags) {
p := &g.Player p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
@@ -198,8 +269,9 @@ func (g *RogueGame) turnOk(y, x int) bool {
return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage) return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage)
} }
// turnref decides whether to refresh at a passage turning (move.c turnref). // turnRefresh decides whether to refresh at a passage turning (move.c
func (g *RogueGame) turnref() { // turnref).
func (g *RogueGame) turnRefresh() {
p := &g.Player p := &g.Player
pp := g.Level.At(p.Pos.Y, p.Pos.X) pp := g.Level.At(p.Pos.Y, p.Pos.X)
@@ -228,8 +300,8 @@ func (g *RogueGame) doorOpen(rp *Room) {
} }
} }
// beTrapped makes him pay for stepping on a trap (move.c be_trapped). // springTrap makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) beTrapped(tc Coord) TrapKind { func (g *RogueGame) springTrap(tc Coord) TrapKind {
p := &g.Player p := &g.Player
if p.On(Levitating) { if p.On(Levitating) {
return TrapRust // anything that's not a door or teleport return TrapRust // anything that's not a door or teleport
@@ -242,30 +314,54 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
tr := TrapKind(pp.Flags & FTrapMask) tr := TrapKind(pp.Flags & FTrapMask)
pp.Flags.Set(FSeen) pp.Flags.Set(FSeen)
switch tr { if h := g.data.trapHandlers[tr]; h != nil {
case TrapDoor: h(g, tc)
}
g.flushType()
return tr
}
// The per-trap effect handlers, dispatched through
// gameData.trapHandlers. Each is one case of the C be_trapped switch.
func (g *RogueGame) trapFall(Coord) {
g.Depth++ g.Depth++
g.NewLevel() g.NewLevel()
g.msg("you fell into a trap!") g.msg("you fell into a trap!")
case TrapBear: }
func (g *RogueGame) trapBear(Coord) {
g.NoMove += g.spread(3) // BEARTIME g.NoMove += g.spread(3) // BEARTIME
g.msg("you are caught in a bear trap") g.msg("you are caught in a bear trap")
case TrapMystery: }
switch g.rnd(11) {
func (g *RogueGame) trapMystery(Coord) {
which := g.rnd(11)
switch which {
case 0: case 0:
g.msg("you are suddenly in a parallel dimension") g.msg("you are suddenly in a parallel dimension")
case 1: case 1:
g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(rainbow))]) g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 2: case 2:
g.msg("you feel a sting in the side of your neck") g.msg("you feel a sting in the side of your neck")
case 3: case 3:
g.msg("multi-colored lines swirl around you, then fade") g.msg("multi-colored lines swirl around you, then fade")
case 4: case 4:
g.msg("a %s light flashes in your eyes", rainbow[g.rnd(len(rainbow))]) g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 5: case 5:
g.msg("a spike shoots past your ear!") g.msg("a spike shoots past your ear!")
default:
g.trapMysteryMore(which)
}
}
// trapMysteryMore holds the back half of the mystery-trap messages.
func (g *RogueGame) trapMysteryMore(which int) {
switch which {
case 6: case 6:
g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(rainbow))]) g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 7: case 7:
g.msg("you suddenly feel very thirsty") g.msg("you suddenly feel very thirsty")
case 8: case 8:
@@ -273,14 +369,19 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
case 9: case 9:
g.msg("time now seems to be going slower") g.msg("time now seems to be going slower")
case 10: case 10:
g.msg("you pack turns %s!", rainbow[g.rnd(len(rainbow))]) g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
} }
case TrapSleep: }
func (g *RogueGame) trapSleep(Coord) {
g.NoCommand += g.spread(5) // SLEEPTIME g.NoCommand += g.spread(5) // SLEEPTIME
p.Flags.Clear(Awake) g.Player.Flags.Clear(Awake)
g.msg("a strange white mist envelops you and you fall asleep") g.msg("a strange white mist envelops you and you fall asleep")
case TrapArrow: }
func (g *RogueGame) trapArrow(Coord) {
p := &g.Player
if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) { if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) {
p.Stats.HP -= g.roll(1, 6) p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
@@ -297,15 +398,23 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
g.fall(arrow, false) g.fall(arrow, false)
g.msg("an arrow shoots past you") g.msg("an arrow shoots past you")
} }
case TrapTeleport: }
func (g *RogueGame) trapTeleport(tc Coord) {
// since the hero's leaving, look() won't put a TRAP down for us, // since the hero's leaving, look() won't put a TRAP down for us,
// so we have to do it ourself // so we have to do it ourself
g.teleport() g.teleport()
g.mvaddch(tc.Y, tc.X, Trap) g.mvaddch(tc.Y, tc.X, Trap)
case TrapDart: }
func (g *RogueGame) trapDart(Coord) {
p := &g.Player
if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) { if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) {
g.msg("a small dart whizzes by your ear and vanishes") g.msg("a small dart whizzes by your ear and vanishes")
} else {
return
}
p.Stats.HP -= g.roll(1, 4) p.Stats.HP -= g.roll(1, 4)
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
g.msg("a poisoned dart killed you") g.msg("a poisoned dart killed you")
@@ -313,24 +422,20 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
} }
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) { if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
g.chgStr(-1) g.changeStrength(-1)
} }
g.msg("a small dart just hit you in the shoulder") g.msg("a small dart just hit you in the shoulder")
}
case TrapRust:
g.msg("a gush of water hits you on the head")
g.rustArmor(p.CurArmor)
}
g.flushType()
return tr
} }
// rndmove moves in a random direction if the monster/person is confused func (g *RogueGame) trapRust(Coord) {
// (move.c rndmove). g.msg("a gush of water hits you on the head")
func (g *RogueGame) rndmove(who *Creature) Coord { g.rustArmor(g.Player.CurArmor)
}
// randomStep moves in a random direction if the monster/person is
// confused (move.c rndmove).
func (g *RogueGame) randomStep(who *Creature) Coord {
ret := Coord{ ret := Coord{
Y: who.Pos.Y + g.rnd(3) - 1, Y: who.Pos.Y + g.rnd(3) - 1,
X: who.Pos.X + g.rnd(3) - 1, X: who.Pos.X + g.rnd(3) - 1,

View File

@@ -27,8 +27,8 @@ func (g *RogueGame) NewLevel() {
// go with them (the garbage collector is our free_list). // go with them (the garbage collector is our free_list).
g.Level.Monsters = nil g.Level.Monsters = nil
g.Level.Objects = nil g.Level.Objects = nil
g.doRooms() // Draw rooms g.digRooms() // Draw rooms
g.doPassages() // Draw passages g.digPassages() // Draw passages
p.NoFood++ p.NoFood++
@@ -61,7 +61,7 @@ func (g *RogueGame) NewLevel() {
g.SeenStairs = false g.SeenStairs = false
for _, tp := range g.Level.Monsters { for _, tp := range g.Level.Monsters {
tp.Room = g.roomin(tp.Pos) tp.Room = g.roomIn(tp.Pos)
} }
hero, _ := g.findFloor(true) hero, _ := g.findFloor(true)
@@ -78,8 +78,8 @@ func (g *RogueGame) NewLevel() {
} }
} }
// rndRoom picks a room that is really there (new_level.c rnd_room). // randomRoom picks a room that is really there (new_level.c rnd_room).
func (g *RogueGame) rndRoom() int { func (g *RogueGame) randomRoom() int {
for { for {
rm := g.rnd(MaxRooms) rm := g.rnd(MaxRooms)
if !g.Level.Rooms[rm].Flags.Has(Gone) { if !g.Level.Rooms[rm].Flags.Has(Gone) {
@@ -98,14 +98,14 @@ func (g *RogueGame) putThings() {
} }
// check for treasure rooms, and if so, put it in. // check for treasure rooms, and if so, put it in.
if g.rnd(treasRoomChance) == 0 { if g.rnd(treasRoomChance) == 0 {
g.treasRoom() g.treasureRoom()
} }
// Do MAXOBJ attempts to put things on a level // Do MAXOBJ attempts to put things on a level
for range MaxObj { for range MaxObj {
if g.rnd(100) < 36 { if g.rnd(100) < 36 {
// Pick a new object and link it in the list // Pick a new object and link it in the list
obj := g.newThing() obj := g.newThing()
attachObj(&g.Level.Objects, obj) g.Level.AddObject(obj)
// Put it somewhere // Put it somewhere
obj.Pos, _ = g.findFloor(false) obj.Pos, _ = g.findFloor(false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph()) g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
@@ -115,7 +115,7 @@ func (g *RogueGame) putThings() {
// yet, put it somewhere on the ground // yet, put it somewhere on the ground
if g.Depth >= AmuletLevel && !g.HasAmulet { if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject() obj := newObject()
attachObj(&g.Level.Objects, obj) g.Level.AddObject(obj)
obj.Damage = dice("0x0") obj.Damage = dice("0x0")
obj.HurlDmg = dice("0x0") obj.HurlDmg = dice("0x0")
obj.ArmorClass = 11 obj.ArmorClass = 11
@@ -126,9 +126,9 @@ func (g *RogueGame) putThings() {
} }
} }
// treasRoom adds a treasure room (new_level.c treas_room). // treasureRoom adds a treasure room (new_level.c treas_room).
func (g *RogueGame) treasRoom() { func (g *RogueGame) treasureRoom() {
rp := &g.Level.Rooms[g.rndRoom()] rp := &g.Level.Rooms[g.randomRoom()]
spots := min((rp.Max.Y-2)*(rp.Max.X-2)-minTreas, maxTreas-minTreas) spots := min((rp.Max.Y-2)*(rp.Max.X-2)-minTreas, maxTreas-minTreas)
@@ -137,7 +137,7 @@ func (g *RogueGame) treasRoom() {
mp, _ := g.findFloorIn(rp, 2*maxTries, false) mp, _ := g.findFloorIn(rp, 2*maxTries, false)
tp := g.newThing() tp := g.newThing()
tp.Pos = mp tp.Pos = mp
attachObj(&g.Level.Objects, tp) g.Level.AddObject(tp)
g.Level.SetChar(mp.Y, mp.X, tp.Kind.Glyph()) g.Level.SetChar(mp.Y, mp.X, tp.Kind.Glyph())
} }

View File

@@ -8,7 +8,7 @@ import (
func genLevel(t *testing.T, seed int32) *RogueGame { func genLevel(t *testing.T, seed int32) *RogueGame {
t.Helper() t.Helper()
g := NewGame(Config{Seed: seed}) g := New(Params{Seed: seed})
g.NewLevel() g.NewLevel()
return g return g
@@ -38,6 +38,19 @@ func TestNewLevelInvariants(t *testing.T) {
for _, seed := range []int32{1, 12345, 2026, 99999} { for _, seed := range []int32{1, 12345, 2026, 99999} {
g := genLevel(t, seed) g := genLevel(t, seed)
checkHeroPlacement(t, g, seed)
checkRoomsDrawn(t, g, seed)
checkMonstersPlaced(t, g, seed)
checkObjectsPlaced(t, g, seed)
checkStartingKit(t, g, seed)
}
}
// checkHeroPlacement verifies the staircase and hero landed on valid,
// unoccupied cells.
func checkHeroPlacement(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// The staircase is somewhere real. // The staircase is somewhere real.
st := g.Level.Stairs st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs { if g.Level.Char(st.Y, st.X) != Stairs {
@@ -58,8 +71,12 @@ func TestNewLevelInvariants(t *testing.T) {
if g.Player.Room == nil { if g.Player.Room == nil {
t.Errorf("seed %d: hero not in any room", seed) t.Errorf("seed %d: hero not in any room", seed)
} }
}
// checkRoomsDrawn verifies rooms and floor/passages appear on the map.
func checkRoomsDrawn(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Some rooms exist and are drawn.
m := renderMap(g) m := renderMap(g)
if !strings.Contains(m, "|") || !strings.Contains(m, "-") { if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
t.Errorf("seed %d: no room walls drawn", seed) t.Errorf("seed %d: no room walls drawn", seed)
@@ -68,8 +85,13 @@ func TestNewLevelInvariants(t *testing.T) {
if !strings.Contains(m, ".") && !strings.Contains(m, "#") { if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
t.Errorf("seed %d: no floor or passages drawn", seed) t.Errorf("seed %d: no floor or passages drawn", seed)
} }
}
// checkMonstersPlaced verifies every monster is indexed on the map and
// placed in a room.
func checkMonstersPlaced(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Every monster is indexed on the map and placed in a room.
for _, mon := range g.Level.Monsters { for _, mon := range g.Level.Monsters {
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon { if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
t.Errorf("seed %d: monster %c not indexed at its position", t.Errorf("seed %d: monster %c not indexed at its position",
@@ -80,9 +102,14 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: monster %c has no room", seed, mon.Type) t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
} }
} }
}
// checkObjectsPlaced verifies every level object sits on a cell
// displaying its type (items can share cells only with monsters standing
// on them).
func checkObjectsPlaced(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// Every level object sits on a cell displaying its type (items can
// share cells only with monsters standing on them).
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch != obj.Kind.Glyph() && if ch != obj.Kind.Glyph() &&
@@ -91,8 +118,13 @@ func TestNewLevelInvariants(t *testing.T) {
seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch) seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch)
} }
} }
}
// checkStartingKit verifies the player has her starting kit: food, armor,
// mace, bow, arrows.
func checkStartingKit(t *testing.T, g *RogueGame, seed int32) {
t.Helper()
// The player has her starting kit: food, armor, mace, bow, arrows.
if len(g.Player.Pack) != 5 { if len(g.Player.Pack) != 5 {
t.Errorf("seed %d: starting pack has %d items, want 5", t.Errorf("seed %d: starting pack has %d items, want 5",
seed, len(g.Player.Pack)) seed, len(g.Player.Pack))
@@ -107,7 +139,6 @@ func TestNewLevelInvariants(t *testing.T) {
g.Player.CurArmor.ArmorKind() != ArmorRingMail { g.Player.CurArmor.ArmorKind() != ArmorRingMail {
t.Errorf("seed %d: not wearing the starting ring mail", seed) t.Errorf("seed %d: not wearing the starting ring mail", seed)
} }
}
} }
func TestNewLevelDeterministic(t *testing.T) { func TestNewLevelDeterministic(t *testing.T) {
@@ -128,7 +159,7 @@ func TestNewLevelDeterministic(t *testing.T) {
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep. // mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
func TestDeeperLevels(t *testing.T) { func TestDeeperLevels(t *testing.T) {
for _, seed := range []int32{7, 42, 1000, 31337} { for _, seed := range []int32{7, 42, 1000, 31337} {
g := NewGame(Config{Seed: seed}) g := New(Params{Seed: seed})
for depth := 1; depth <= 30; depth++ { for depth := 1; depth <= 30; depth++ {
g.Depth = depth g.Depth = depth
g.NewLevel() g.NewLevel()

View File

@@ -26,36 +26,49 @@ const (
KindRingOrStick ObjectKind = -2 KindRingOrStick ObjectKind = -2
) )
// kindGlyphs maps each kind to the character Rogue draws for it. // Category words shared by ObjectKind.String, the discovery list, and the
var kindGlyphs = [...]byte{ // ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the
KindNone: ' ', // glyph byte constants.)
KindPotion: Potion, const (
KindScroll: Scroll, potionName = "potion"
KindFood: Food, scrollName = "scroll"
KindWeapon: Weapon, ringName = "ring"
KindArmor: Armor, goldName = "gold"
KindRing: Ring, )
KindWand: Stick,
KindAmulet: Amulet,
KindGold: Gold,
}
// Glyph returns the map/display character for this kind of object. // Glyph returns the map/display character for this kind of object.
func (k ObjectKind) Glyph() byte { func (k ObjectKind) Glyph() byte {
if k < 0 || int(k) >= len(kindGlyphs) { switch k {
return ' ' case KindPotion:
return Potion
case KindScroll:
return Scroll
case KindFood:
return Food
case KindWeapon:
return Weapon
case KindArmor:
return Armor
case KindRing:
return Ring
case KindWand:
return Stick
case KindAmulet:
return Amulet
case KindGold:
return Gold
} }
return kindGlyphs[k] return ' '
} }
// String names the category the way the C type_name() did. // String names the category the way the C type_name() did.
func (k ObjectKind) String() string { func (k ObjectKind) String() string {
switch k { switch k {
case KindPotion: case KindPotion:
return "potion" return potionName
case KindScroll: case KindScroll:
return "scroll" return scrollName
case KindFood: case KindFood:
return "food" return "food"
case KindWeapon: case KindWeapon:
@@ -63,27 +76,34 @@ func (k ObjectKind) String() string {
case KindArmor: case KindArmor:
return "suit of armor" return "suit of armor"
case KindRing: case KindRing:
return "ring" return ringName
case KindWand: default:
return "wand or staff" return k.stringRest()
case KindAmulet:
return "amulet"
case KindGold:
return "gold"
case KindRingOrStick:
return "ring, wand or staff"
} }
return "bizarre thing"
} }
// objectKindForGlyph is the reverse of Glyph: what category of item does a // objectKindForGlyph is the reverse of Glyph: what category of item does a
// map character denote. Returns KindNone for non-item characters. // map character denote. Returns KindNone for non-item characters.
func objectKindForGlyph(ch byte) ObjectKind { func objectKindForGlyph(ch byte) ObjectKind {
for k, g := range kindGlyphs { switch ch {
if g == ch && ObjectKind(k) != KindNone { case Potion:
return ObjectKind(k) return KindPotion
} case Scroll:
return KindScroll
case Food:
return KindFood
case Weapon:
return KindWeapon
case Armor:
return KindArmor
case Ring:
return KindRing
case Stick:
return KindWand
case Amulet:
return KindAmulet
case Gold:
return KindGold
} }
return KindNone return KindNone
@@ -95,6 +115,23 @@ func (k ObjectKind) MergesInPack() bool {
return k == KindPotion || k == KindScroll || k == KindFood return k == KindPotion || k == KindScroll || k == KindFood
} }
// stringRest names the remaining kinds, including the ring-or-stick
// prompt pseudo-kind (the tail of the C type_name switch).
func (k ObjectKind) stringRest() string {
switch k {
case KindWand:
return "wand or staff"
case KindAmulet:
return "amulet"
case KindGold:
return goldName
case KindRingOrStick:
return "ring, wand or staff"
}
return "bizarre thing"
}
// Object is the _o arm of the C THING union: anything that can lie on the // Object is the _o arm of the C THING union: anything that can lie on the
// floor or ride in a pack. // floor or ride in a pack.
type Object struct { type Object struct {

View File

@@ -101,7 +101,7 @@ func (g *RogueGame) putOpt(op *optDesc) {
case optBool, optSeeFloor: case optBool, optSeeFloor:
hw.AddStr(boolStr(*op.boolP)) hw.AddStr(boolStr(*op.boolP))
case optInvT: case optInvT:
hw.AddStr(invTName[*op.intP]) hw.AddStr(g.data.invTName[*op.intP])
case optStr: case optStr:
hw.AddStr(*op.strP) hw.AddStr(*op.strP)
} }
@@ -205,55 +205,11 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
) )
for { for {
c = g.readchar() c = g.readchar()
if c == '\n' || c == '\r' || c == Escape { if endsInput(c) || (len(buf) == 0 && c == '-' && !onStd) {
break break
} }
if c == 8 || c == 0x7f { // erase character buf = g.getStrEdit(win, buf, c, oy, ox)
if len(buf) > 0 {
buf = buf[:len(buf)-1]
win.Move(oy, ox+len(displayStr(buf)))
}
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
if c == CTRL('U') { // kill character
buf = buf[:0]
win.Move(oy, ox)
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
if len(buf) == 0 {
if c == '-' && !onStd {
break
}
if c == '~' {
buf = append(buf, g.Home...)
win.AddStr(g.Home)
win.Clrtoeol()
g.scr.RefreshWin(win)
continue
}
}
if len(buf) >= MaxInp || (!isPrint(c) && c != ' ') {
continue // C beeps here
}
buf = append(buf, c)
win.AddStr(unctrl(c))
win.Clrtoeol()
g.scr.RefreshWin(win)
} }
if len(buf) > 0 { // only change option if something has been typed if len(buf) > 0 { // only change option if something has been typed
@@ -267,6 +223,12 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
g.Msgs.Mpos += len(buf) g.Msgs.Mpos += len(buf)
} }
return getStrResult(c)
}
// getStrResult maps the terminating key to the C return code (options.c
// get_str).
func getStrResult(c byte) int {
switch c { switch c {
case '-': case '-':
return Minus return Minus
@@ -277,6 +239,52 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
} }
} }
// endsInput reports the keys that finish line input (options.c get_str).
func endsInput(c byte) bool {
return c == '\n' || c == '\r' || c == Escape
}
// getStrErase deletes the last character of the buffer (options.c
// get_str).
func getStrErase(win *Window, buf []byte, oy, ox int) []byte {
if len(buf) > 0 {
buf = buf[:len(buf)-1]
win.Move(oy, ox+len(displayStr(buf)))
}
win.Clrtoeol()
return buf
}
// getStrEdit applies one key to the line editor's buffer: erase, kill,
// home expansion, or a typed character (options.c get_str).
func (g *RogueGame) getStrEdit(win *Window, buf []byte, c byte, oy, ox int) []byte {
switch {
case c == 8 || c == 0x7f:
buf = getStrErase(win, buf, oy, ox)
case c == CTRL('U'): // kill character
buf = buf[:0]
win.Move(oy, ox)
win.Clrtoeol()
case len(buf) == 0 && c == '~':
buf = append(buf, g.Home...)
win.AddStr(g.Home)
win.Clrtoeol()
case len(buf) >= MaxInp || (!isPrint(c) && c != ' '):
return buf // C beeps here
default:
buf = append(buf, c)
win.AddStr(unctrl(c))
win.Clrtoeol()
}
g.scr.RefreshWin(win)
return buf
}
// displayStr renders a buffer the way the input echo did. // displayStr renders a buffer the way the input echo did.
func displayStr(buf []byte) string { func displayStr(buf []byte) string {
var sb strings.Builder var sb strings.Builder
@@ -291,7 +299,7 @@ func displayStr(buf []byte) string {
func (g *RogueGame) getInvT(ip *int) int { func (g *RogueGame) getInvT(ip *int) int {
win := g.scr.Hw win := g.scr.Hw
oy, ox := win.GetYX() oy, ox := win.GetYX()
win.AddStr(invTName[*ip]) win.AddStr(g.data.invTName[*ip])
for { for {
win.Move(oy, ox) win.Move(oy, ox)
@@ -319,7 +327,7 @@ func (g *RogueGame) getInvT(ip *int) int {
break break
} }
win.MvPrintwf(oy, ox, "%s\n", invTName[*ip]) win.MvPrintwf(oy, ox, "%s\n", g.data.invTName[*ip])
return Norm return Norm
} }
@@ -337,20 +345,49 @@ func (g *RogueGame) ParseOpts(str string) {
i++ i++
} }
name := str[:i] rest := g.parseOptName(optlist, str[:i], str[i:])
rest := str[i:] // skip to start of next option name
matched := false for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
}
// parseOptName applies one named option, returning the unconsumed
// remainder: "name" turns a boolean on, "noname" turns it off, and
// string options consume a value (the option scan of options.c
// parse_opts).
func (g *RogueGame) parseOptName(optlist []optDesc, name, rest string) string {
for oi := range optlist { for oi := range optlist {
op := &optlist[oi] op := &optlist[oi]
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
if strings.HasPrefix(op.name, name) && name != "" { if strings.HasPrefix(op.name, name) && name != "" {
matched = true
if isBoolOpt { if isBoolOpt {
*op.boolP = true *op.boolP = true
} else {
return rest
}
return g.parseOptValue(op, rest)
}
if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
*op.boolP = false
return rest
}
}
return rest
}
// parseOptValue consumes an option's "=value" from rest, storing it,
// and returns the remainder (the string arm of options.c parse_opts).
func (g *RogueGame) parseOptValue(op *optDesc, rest string) string {
// Skip to start of string value // Skip to start of string value
for rest != "" && rest[0] == '=' { for rest != "" && rest[0] == '=' {
rest = rest[1:] rest = rest[1:]
@@ -374,45 +411,31 @@ func (g *RogueGame) ParseOpts(str string) {
} }
word := val[:end] word := val[:end]
rest = val[end:]
if op.kind == optInvT { if op.kind == optInvT {
// check for type of inventory g.parseInvType(op, word)
w := word } else {
if w != "" { *op.strP = prefix + strucpy(word)
w = string(toUpper(w[0])) + w[1:]
} }
for ti, tn := range invTName { return val[end:]
if strings.HasPrefix(tn, w) { }
// parseInvType matches an inventory-style name by prefix (options.c
// parse_opts).
func (g *RogueGame) parseInvType(op *optDesc, word string) {
// check for type of inventory
if word != "" {
word = string(toUpper(word[0])) + word[1:]
}
for ti, tn := range g.data.invTName {
if strings.HasPrefix(tn, word) {
*op.intP = ti *op.intP = ti
break break
} }
} }
} else {
*op.strP = prefix + strucpy(word)
}
}
break
} else if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
matched = true
*op.boolP = false
break
}
}
_ = matched
// skip to start of next option name
for rest != "" && !isAlpha(rest[0]) {
rest = rest[1:]
}
str = rest
}
} }
// strucpy copies a string keeping only printable characters, capped at // strucpy copies a string keeping only printable characters, capped at

View File

@@ -9,7 +9,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
fromFloor := false fromFloor := false
if obj == nil { if obj == nil {
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil { if obj = g.Level.ObjectAt(p.Pos.Y, p.Pos.X); obj == nil {
return return
} }
@@ -17,107 +17,15 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
} }
// Check for and deal with scare monster scrolls // Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) { if g.pickupScareScroll(obj) {
detachObj(&g.Level.Objects, obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
return return
} }
if len(p.Pack) == 0 { obj, ok := g.packInsert(obj, fromFloor)
p.Pack = append(p.Pack, obj) if !ok {
obj.PackCh = g.packChar()
p.Inpack++
} else {
// Walk the pack looking for the insertion point, keeping items of
// one type together and merging stackable/grouped items — a direct
// translation of the C linked-list walk. lp is the index to insert
// after; -1 after a merge means no insertion.
lp := -1
merged := false
for i := 0; i < len(p.Pack); i++ {
if p.Pack[i].Kind != obj.Kind {
lp = i
continue
}
// found the group of our type: scan for matching subtype
for p.Pack[i].Kind == obj.Kind && p.Pack[i].Which != obj.Which {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which {
switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) {
return return
} }
op.Count++
obj = op
lp = -1
merged = true
case obj.Group != 0:
lp = i
for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op = p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return
}
obj = op
lp = -1
merged = true
}
default:
lp = i
}
}
break
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return
}
obj.PackCh = g.packChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
}
obj.Flags.Set(WasFound) obj.Flags.Set(WasFound)
// If this was the object of something's desire, that monster will get // If this was the object of something's desire, that monster will get
@@ -137,10 +45,167 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
g.addmsgf("you now have ") g.addmsgf("you now have ")
} }
g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh) g.msg("%s (%c)", g.inventoryName(obj, !g.Options.Terse), obj.PackCh)
} }
} }
// pickupScareScroll crumbles a found scare monster scroll when it is
// picked up again; it reports whether it did (pack.c add_pack).
func (g *RogueGame) pickupScareScroll(obj *Object) bool {
if obj.Kind != KindScroll || obj.ScrollKind() != ScrollScareMonster ||
!obj.Flags.Has(WasFound) {
return false
}
p := &g.Player
g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
return true
}
// packInsert places the object in the pack, keeping items of one type
// together and merging stackable and grouped items — a translation of
// the C linked-list walk in pack.c add_pack. It returns the pack entry
// the object ended up as; ok is false when the pack has no room.
func (g *RogueGame) packInsert(obj *Object, fromFloor bool) (*Object, bool) {
p := &g.Player
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = p.nextPackChar()
p.Inpack++
return obj, true
}
merged := false
// lp is the index to insert after; -1 after a merge means no
// insertion.
i, lp := packScanKind(p.Pack, obj.Kind)
if i < len(p.Pack) {
i, lp = packScanWhich(p.Pack, obj, i, lp)
if op := p.Pack[i]; op.Kind == obj.Kind && op.Which == obj.Which {
var ok bool
obj, lp, merged, ok = g.packMatch(obj, op, i, fromFloor)
if !ok {
return nil, false
}
}
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return nil, false
}
obj.PackCh = p.nextPackChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
return obj, true
}
// packScanKind scans to the first pack entry of this kind, returning
// its index (or the pack length) and the entry to insert after (the
// outer scan of pack.c add_pack).
func packScanKind(pack []*Object, kind ObjectKind) (int, int) {
lp := -1
i := 0
for ; i < len(pack); i++ {
if pack[i].Kind == kind {
break
}
lp = i
}
return i, lp
}
// packScanWhich scans within the kind group for the matching subtype
// (the inner scan of pack.c add_pack).
func packScanWhich(pack []*Object, obj *Object, i, lp int) (int, int) {
for pack[i].Kind == obj.Kind && pack[i].Which != obj.Which {
lp = i
if i+1 >= len(pack) {
break
}
i++
}
return i, lp
}
// packMatch merges the object with a matching pack entry when possible:
// stackables merge counts, grouped missiles rejoin their bundle, and
// anything else marks the insertion point (the matched-subtype switch
// of pack.c add_pack). It returns the resulting entry, the insert-after
// index, whether a merge happened, and ok false when the pack is full.
func (g *RogueGame) packMatch(obj, op *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) {
return nil, 0, false, false
}
op.Count++
return op, -1, true, true
case obj.Group != 0:
return g.packMatchGroup(obj, i, fromFloor)
default:
return obj, i, false, true
}
}
// packMatchGroup rejoins a grouped missile bundle with its group entry
// (the o_group arm of pack.c add_pack).
func (g *RogueGame) packMatchGroup(obj *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
p := &g.Player
lp := i
for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return nil, 0, false, false
}
return op, -1, true, true
}
return obj, lp, false, true
}
// packRoom sees if there's room in the pack; if not, prints an appropriate // packRoom sees if there's room in the pack; if not, prints an appropriate
// message (pack.c pack_room). // message (pack.c pack_room).
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool { func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
@@ -168,7 +233,7 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
} }
if fromFloor { if fromFloor {
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) { if p.Room.Flags.Has(Gone) {
@@ -181,46 +246,17 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
return true return true
} }
// leavePack takes an item out of the pack (pack.c leave_pack). // leavePack takes an item out of the pack (pack.c leave_pack), keeping
// the repeat-command bookkeeping; the pack surgery is
// Player.removeFromPack.
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object { func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all { if obj.Count > 1 && !all {
g.LastPick = obj g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else { } else {
g.LastPick = nil g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
} }
return nobj return g.Player.removeFromPack(obj, newobj, all)
}
// packChar returns the next unused pack character (pack.c pack_char).
func (g *RogueGame) packChar() byte {
p := &g.Player
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
} }
// inventory lists what is in the pack; returns true if there is something // inventory lists what is in the pack; returns true if there is something
@@ -236,7 +272,7 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.NObjs++ g.NObjs++
g.Msgs.MsgEsc = true g.Msgs.MsgEsc = true
line := string(item.PackCh) + ") " + g.invName(item, false) line := string(item.PackCh) + ") " + g.inventoryName(item, false)
if g.addLine("%s", line) == Escape { if g.addLine("%s", line) == Escape {
g.Msgs.MsgEsc = false g.Msgs.MsgEsc = false
g.msg("") g.msg("")
@@ -248,18 +284,11 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
} }
if g.NObjs == 0 { if g.NObjs == 0 {
if g.Options.Terse {
if kind == KindNone { if kind == KindNone {
g.msg("empty handed") g.msg("%s", g.chooseTerse("empty handed", "you are empty handed"))
} else { } else {
g.msg("nothing appropriate") g.msg("%s", g.chooseTerse("nothing appropriate",
} "you don't have anything appropriate"))
} else {
if kind == KindNone {
g.msg("you are empty handed")
} else {
g.msg("you don't have anything appropriate")
}
} }
return false return false
@@ -277,7 +306,7 @@ func (g *RogueGame) pickUp(ch byte) {
return return
} }
obj := g.findObj(p.Pos.Y, p.Pos.X) obj := g.Level.ObjectAt(p.Pos.Y, p.Pos.X)
if g.MoveOn { if g.MoveOn {
g.moveMsg(obj) g.moveMsg(obj)
@@ -291,7 +320,7 @@ func (g *RogueGame) pickUp(ch byte) {
} }
g.money(obj.GoldValue) g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
p.Room.GoldVal = 0 p.Room.GoldVal = 0
default: default:
@@ -323,7 +352,7 @@ func (g *RogueGame) moveMsg(obj *Object) {
g.addmsgf("you ") g.addmsgf("you ")
} }
g.msg("moved onto %s", g.invName(obj, true)) g.msg("moved onto %s", g.inventoryName(obj, true))
} }
// pickyInven allows the player to inventory a single item (pack.c // pickyInven allows the player to inventory a single item (pack.c
@@ -337,7 +366,7 @@ func (g *RogueGame) pickyInven() {
} }
if len(p.Pack) == 1 { if len(p.Pack) == 1 {
g.msg("a) %s", g.invName(p.Pack[0], false)) g.msg("a) %s", g.inventoryName(p.Pack[0], false))
return return
} }
@@ -354,7 +383,7 @@ func (g *RogueGame) pickyInven() {
for _, obj := range p.Pack { for _, obj := range p.Pack {
if mch == obj.PackCh { if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false)) g.msg("%c) %s", mch, g.inventoryName(obj, false))
return return
} }
@@ -363,26 +392,72 @@ func (g *RogueGame) pickyInven() {
g.msg("'%s' not in pack", unctrl(mch)) g.msg("'%s' not in pack", unctrl(mch))
} }
// getItem picks something out of a pack for a purpose (pack.c get_item). // promptPackItem picks something out of a pack for a purpose (pack.c
func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object { // get_item); the second result reports whether the player chose an
// item.
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (*Object, bool) {
p := &g.Player p := &g.Player
if len(p.Pack) == 0 { if len(p.Pack) == 0 {
g.msg("you aren't carrying anything") g.msg("you aren't carrying anything")
return nil return nil, false
} }
if g.Again { if g.Again {
return g.repeatLastItem()
}
for {
g.promptItemPurpose(purpose)
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
if ch == Escape {
g.resetLast()
g.After = false
g.msg("")
return nil, false
}
g.NObjs = 1 // normal case: person types one char
if ch == '*' {
g.Msgs.Mpos = 0
if !g.inventory(p.Pack, kind) {
g.After = false
return nil, false
}
continue
}
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj, true
}
}
g.msg("'%s' is not a valid item", unctrl(ch))
}
}
// repeatLastItem replays the previous selection for the repeat command
// (pack.c get_item).
func (g *RogueGame) repeatLastItem() (*Object, bool) {
if g.LastPick != nil { if g.LastPick != nil {
return g.LastPick return g.LastPick, true
} }
g.msg("you ran out") g.msg("you ran out")
return nil return nil, false
} }
for { // promptItemPurpose prints the "which object do you want to ...?"
// prompt (pack.c get_item).
func (g *RogueGame) promptItemPurpose(purpose string) {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsgf("which object do you want to ") g.addmsgf("which object do you want to ")
} }
@@ -394,37 +469,6 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
} }
g.msg("? (* for list): ") g.msg("? (* for list): ")
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
if ch == Escape {
g.resetLast()
g.After = false
g.msg("")
return nil
}
g.NObjs = 1 // normal case: person types one char
if ch == '*' {
g.Msgs.Mpos = 0
if !g.inventory(p.Pack, kind) {
g.After = false
return nil
}
continue
}
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj
}
}
g.msg("'%s' is not a valid item", unctrl(ch))
}
} }
// money adds or subtracts gold from the pack (pack.c money). // money adds or subtracts gold from the pack (pack.c money).

View File

@@ -2,21 +2,8 @@ package game
// passages.c — draw the connecting passages. // passages.c — draw the connecting passages.
// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages. // digPassages draws all the passages on a level (passages.c do_passages).
var rdesConn = [MaxRooms][MaxRooms]bool{ func (g *RogueGame) digPassages() {
{false, true, false, true, false, false, false, false, false},
{true, false, true, false, true, false, false, false, false},
{false, true, false, false, false, true, false, false, false},
{true, false, false, false, true, false, true, false, false},
{false, true, false, true, false, true, false, true, false},
{false, false, true, false, true, false, false, false, true},
{false, false, false, true, false, false, false, true, false},
{false, false, false, false, true, false, true, false, true},
{false, false, false, false, false, true, false, true, false},
}
// doPassages draws all the passages on a level (passages.c do_passages).
func (g *RogueGame) doPassages() {
var ( var (
isconn [MaxRooms][MaxRooms]bool isconn [MaxRooms][MaxRooms]bool
ingraph [MaxRooms]bool ingraph [MaxRooms]bool
@@ -28,223 +15,253 @@ func (g *RogueGame) doPassages() {
r1 := g.rnd(MaxRooms) r1 := g.rnd(MaxRooms)
ingraph[r1] = true ingraph[r1] = true
for { for roomcount < MaxRooms {
// find a room to connect with // find a room to connect with
j := 0 r2 := g.pickNeighbor(r1, func(i int) bool { return !ingraph[i] })
r2 := -1 if r2 < 0 {
// if no adjacent rooms are outside the graph, pick a new
for i := range MaxRooms { // room to look from
if rdesConn[r1][i] && !ingraph[i] {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
if j == 0 {
// if no adjacent rooms are outside the graph, pick a new room
// to look from
for { for {
r1 = g.rnd(MaxRooms) r1 = g.rnd(MaxRooms)
if ingraph[r1] { if ingraph[r1] {
break break
} }
} }
} else {
continue
}
// otherwise, connect new room to the graph, and draw a tunnel // otherwise, connect new room to the graph, and draw a tunnel
// to it // to it
ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded ingraph[r2] = true
g.conn(r1, r2) g.connectRooms(r1, r2)
isconn[r1][r2] = true isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded isconn[r2][r1] = true
roomcount++ roomcount++
} }
if roomcount >= MaxRooms {
break
}
}
// attempt to add passages to the graph a random number of times so that // attempt to add passages to the graph a random number of times so that
// there isn't always just one unique passage through it. // there isn't always just one unique passage through it.
for roomcount = g.rnd(5); roomcount > 0; roomcount-- { for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
r1 = g.rnd(MaxRooms) // a random room to look from r1 = g.rnd(MaxRooms) // a random room to look from
// find an adjacent room not already connected // find an adjacent room not already connected; if there is one,
// connect it and look for the next added passage
r2 := g.pickNeighbor(r1, func(i int) bool { return !isconn[r1][i] })
if r2 >= 0 {
g.connectRooms(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true
}
}
g.numberPassages()
}
// pickNeighbor reservoir-picks an adjacent room for which ok holds, or
// -1 when there is none (passages.c do_passages).
func (g *RogueGame) pickNeighbor(r1 int, ok func(int) bool) int {
j := 0 j := 0
r2 := -1 r2 := -1
for i := range MaxRooms { for i := range MaxRooms {
if rdesConn[r1][i] && !isconn[r1][i] { if g.data.rdesConn[r1][i] && ok(i) {
if j++; g.rnd(j) == 0 { if j++; g.rnd(j) == 0 {
r2 = i r2 = i
} }
} }
} }
// if there is one, connect it and look for the next added passage
if j != 0 {
g.conn(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
}
}
g.passnum() return r2
} }
// conn draws a corridor from a room in a certain direction (passages.c // corridorPlan is the movement setup connectRooms computes before it
// conn). // digs (the local variables of passages.c conn).
func (g *RogueGame) conn(r1, r2 int) { type corridorPlan struct {
var ( rpf, rpt *Room // the rooms being joined
rm int del Coord // direction of move
direc byte turnDelta Coord // direction to turn
) spos, epos Coord // start and end of move
distance, turnDistance int // how far to move and to turn
}
if r1 < r2 { // connectRooms draws a corridor from a room in a certain direction
rm = r1 // (passages.c conn).
if r1+1 == r2 { func (g *RogueGame) connectRooms(r1, r2 int) {
direc = 'r' rm, direc := connOrient(r1, r2)
} else {
direc = 'd'
}
} else {
rm = r2
if r2+1 == r1 {
direc = 'r'
} else {
direc = 'd'
}
}
rpf := &g.Level.Rooms[rm]
// Set up the movement variables, in two cases: first drawing one down.
var (
rpt *Room
del, turnDelta, spos, epos Coord
distance, turnDistance int
)
var plan corridorPlan
if direc == 'd' { if direc == 'd' {
rmt := rm + 3 // room # of dest plan = g.connPlanDown(rm)
rpt = &g.Level.Rooms[rmt] // room pointer of dest
del = Coord{X: 0, Y: 1} // direction of move
spos = rpf.Pos // start of move
epos = rpt.Pos // end of move
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
for {
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break
}
}
}
distance = abs(spos.Y-epos.Y) - 1 // distance to move
turnDelta.Y = 0 // direction to turn
if spos.X < epos.X {
turnDelta.X = 1
} else { } else {
turnDelta.X = -1 plan = g.connPlanRight(rm)
} }
turnDistance = abs(spos.X - epos.X) // how far to turn turnSpot := g.rnd(plan.distance-1) + 1 // where turn starts
} else { // setup for moving right
rmt := rm + 1
rpt = &g.Level.Rooms[rmt]
del = Coord{X: 1, Y: 0}
spos = rpf.Pos
epos = rpt.Pos
if !rpf.Flags.Has(Gone) {
for {
spos.X = rpf.Pos.X + rpf.Max.X - 1
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break
}
}
}
distance = abs(spos.X-epos.X) - 1
if spos.Y < epos.Y {
turnDelta.Y = 1
} else {
turnDelta.Y = -1
}
turnDelta.X = 0
turnDistance = abs(spos.Y - epos.Y)
}
turnSpot := g.rnd(distance-1) + 1 // where turn starts
// Draw in the doors on either side of the passage or just put #'s if // Draw in the doors on either side of the passage or just put #'s if
// the rooms are gone. // the rooms are gone.
if !rpf.Flags.Has(Gone) { g.connEnd(plan.rpf, plan.spos)
g.door(rpf, spos) g.connEnd(plan.rpt, plan.epos)
} else {
g.putpass(spos) g.digCorridor(plan, turnSpot)
}
// connOrient picks the upper-left room of the pair and the digging
// direction: right for horizontal neighbors, down otherwise (passages.c
// conn).
func connOrient(r1, r2 int) (int, byte) {
rm := min(r1, r2)
if abs(r1-r2) == 1 {
return rm, 'r'
}
return rm, 'd'
}
// connPlanDown sets up the movement variables for a corridor drawn
// downward (passages.c conn).
func (g *RogueGame) connPlanDown(rm int) corridorPlan {
rpf := &g.Level.Rooms[rm]
rpt := &g.Level.Rooms[rm+3] // room pointer of dest
plan := corridorPlan{
rpf: rpf,
rpt: rpt,
del: Coord{X: 0, Y: 1}, // direction of move
spos: rpf.Pos, // start of move
epos: rpt.Pos, // end of move
}
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
for {
plan.spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
plan.spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !rpf.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
break
}
}
} }
if !rpt.Flags.Has(Gone) { if !rpt.Flags.Has(Gone) {
g.door(rpt, epos) for {
} else { plan.epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
g.putpass(epos) if !rpt.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
break
} }
// Get ready to move... }
curr := spos }
plan.distance = abs(plan.spos.Y-plan.epos.Y) - 1 // distance to move
plan.turnDelta.Y = 0 // direction to turn
if plan.spos.X < plan.epos.X {
plan.turnDelta.X = 1
} else {
plan.turnDelta.X = -1
}
plan.turnDistance = abs(plan.spos.X - plan.epos.X) // how far to turn
return plan
}
// connPlanRight sets up the movement variables for a corridor drawn to
// the right (passages.c conn).
func (g *RogueGame) connPlanRight(rm int) corridorPlan {
rpf := &g.Level.Rooms[rm]
rpt := &g.Level.Rooms[rm+1]
plan := corridorPlan{
rpf: rpf,
rpt: rpt,
del: Coord{X: 1, Y: 0},
spos: rpf.Pos,
epos: rpt.Pos,
}
if !rpf.Flags.Has(Gone) {
for {
plan.spos.X = rpf.Pos.X + rpf.Max.X - 1
plan.spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !rpf.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
break
}
}
}
if !rpt.Flags.Has(Gone) {
for {
plan.epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !rpt.Flags.Has(Maze) ||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
break
}
}
}
plan.distance = abs(plan.spos.X-plan.epos.X) - 1
if plan.spos.Y < plan.epos.Y {
plan.turnDelta.Y = 1
} else {
plan.turnDelta.Y = -1
}
plan.turnDelta.X = 0
plan.turnDistance = abs(plan.spos.Y - plan.epos.Y)
return plan
}
// connEnd draws a corridor end: a door on a real room, a passage square
// on a gone one (passages.c conn).
func (g *RogueGame) connEnd(rp *Room, pos Coord) {
if !rp.Flags.Has(Gone) {
g.door(rp, pos)
} else {
g.putPassage(pos)
}
}
// digCorridor digs from spos to epos, turning at turnSpot (the digging
// loop of passages.c conn).
func (g *RogueGame) digCorridor(plan corridorPlan, turnSpot int) {
curr := plan.spos
distance := plan.distance
turnDistance := plan.turnDistance
for distance > 0 { for distance > 0 {
// Move to new position // Move to new position
curr.X += del.X curr.X += plan.del.X
curr.Y += del.Y curr.Y += plan.del.Y
// Check if we are at the turn place, if so do the turn // Check if we are at the turn place, if so do the turn
if distance == turnSpot { if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- { for ; turnDistance > 0; turnDistance-- {
g.putpass(curr) g.putPassage(curr)
curr.X += turnDelta.X curr.X += plan.turnDelta.X
curr.Y += turnDelta.Y curr.Y += plan.turnDelta.Y
} }
} }
// Continue digging along // Continue digging along
g.putpass(curr) g.putPassage(curr)
distance-- distance--
} }
curr.X += del.X curr.X += plan.del.X
curr.Y += del.Y curr.Y += plan.del.Y
if curr != epos { if curr != plan.epos {
g.msg("warning, connectivity problem on this level") g.msg("warning, connectivity problem on this level")
} }
} }
// putpass adds a passage character or secret passage here (passages.c // putPassage adds a passage character or secret passage here (passages.c
// putpass). // putpass).
func (g *RogueGame) putpass(cp Coord) { func (g *RogueGame) putPassage(cp Coord) {
pp := g.Level.At(cp.Y, cp.X) pp := g.Level.At(cp.Y, cp.X)
pp.Flags.Set(FPassage) pp.Flags.Set(FPassage)
@@ -283,9 +300,18 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
func (g *RogueGame) addPass() { func (g *RogueGame) addPass() {
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := range NumCols { for x := range NumCols {
pp := g.Level.At(y, x) g.addPassSpot(g.Level.At(y, x), y, x)
if pp.Flags.Has(FPassage) || pp.Ch == Door || }
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) { }
}
// addPassSpot shows one passage or door square for the wizard (the loop
// body of passages.c add_pass).
func (g *RogueGame) addPassSpot(pp *Place, y, x int) {
if !pp.Flags.Has(FPassage) && !hiddenExit(pp.Flags, pp.Ch) {
return
}
ch := pp.Ch ch := pp.Ch
if pp.Flags.Has(FPassage) { if pp.Flags.Has(FPassage) {
ch = Passage ch = Passage
@@ -310,13 +336,16 @@ func (g *RogueGame) addPass() {
g.standend() g.standend()
} }
}
}
}
} }
// passnum assigns a number to each passageway (passages.c passnum). // hiddenExit reports a door, or a secret door still drawn as a wall
func (g *RogueGame) passnum() { // (passages.c add_pass / numpass).
func hiddenExit(fp PlaceFlags, ch byte) bool {
return ch == Door || (!fp.Has(FReal) && (ch == '|' || ch == '-'))
}
// numberPassages assigns a number to each passageway (passages.c passnum).
func (g *RogueGame) numberPassages() {
g.pnum = 0 g.pnum = 0
g.newpnum = false g.newpnum = false
@@ -328,14 +357,14 @@ func (g *RogueGame) passnum() {
rp := &g.Level.Rooms[i] rp := &g.Level.Rooms[i]
for j := range rp.Exits { for j := range rp.Exits {
g.newpnum = true g.newpnum = true
g.numpass(rp.Exits[j].Y, rp.Exits[j].X) g.numberPassage(rp.Exits[j].Y, rp.Exits[j].X)
} }
} }
} }
// numpass numbers a passageway square and its brethren (passages.c // numberPassage numbers a passageway square and its brethren (passages.c
// numpass). // numpass).
func (g *RogueGame) numpass(y, x int) { func (g *RogueGame) numberPassage(y, x int) {
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 { if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
return return
} }
@@ -351,8 +380,7 @@ func (g *RogueGame) numpass(y, x int) {
} }
// check to see if it is a door or secret door, i.e., a new exit, or a // check to see if it is a door or secret door, i.e., a new exit, or a
// numerable type of place // numerable type of place
if ch := g.Level.Char(y, x); ch == Door || if hiddenExit(*fp, g.Level.Char(y, x)) {
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
rp := &g.Level.Passages[g.pnum] rp := &g.Level.Passages[g.pnum]
rp.Exits = append(rp.Exits, Coord{Y: y, X: x}) rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
} else if !fp.Has(FPassage) { } else if !fp.Has(FPassage) {
@@ -361,10 +389,10 @@ func (g *RogueGame) numpass(y, x int) {
*fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13 *fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13
// recurse on the surrounding places // recurse on the surrounding places
g.numpass(y+1, x) g.numberPassage(y+1, x)
g.numpass(y-1, x) g.numberPassage(y-1, x)
g.numpass(y, x+1) g.numberPassage(y, x+1)
g.numpass(y, x-1) g.numberPassage(y, x-1)
} }
// abs is C abs() for ints. // abs is C abs() for ints.

View File

@@ -13,30 +13,12 @@ type pact struct {
straight string straight string
} }
// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic
// (it names the fruit) and is computed in doPot.
var pActions = [NumPotionTypes]pact{
PotionConfusion: {Confused, DUnconfuse, HuhDuration,
"what a tripy feeling!",
"wait, what's going on here. Huh? What? Who?"},
PotionLSD: {Hallucinating, DComeDown, SeeDuration,
"Oh, wow! Everything seems so cosmic!",
"Oh, wow! Everything seems so cosmic!"},
PotionSeeInvisible: {CanSeeInvisible, DUnsee, SeeDuration, "", ""},
PotionBlindness: {Blind, DSight, SeeDuration,
"oh, bummer! Everything is dark! Help!",
"a cloak of darkness falls around you"},
PotionLevitation: {Levitating, DLand, HealTime,
"oh, wow! You're floating in the air!",
"you start to float in the air"},
}
// quaff drinks a potion from the pack (potions.c quaff). // quaff drinks a potion from the pack (potions.c quaff).
func (g *RogueGame) quaff() { func (g *RogueGame) quaff() {
p := &g.Player p := &g.Player
obj := g.getItem("quaff", KindPotion) obj, ok := g.promptPackItem("quaff", KindPotion)
// Make certain that it is something that we want to drink // Make certain that it is something that we want to drink
if obj == nil { if !ok {
return return
} }
@@ -59,19 +41,36 @@ func (g *RogueGame) quaff() {
g.leavePack(obj, false, false) g.leavePack(obj, false, false)
switch obj.PotionKind() { if h := g.data.quaffHandlers[obj.PotionKind()]; h != nil {
case PotionConfusion: h(g, trip)
g.doPot(PotionConfusion, !trip) }
case PotionPoison:
g.status()
// Throw the item away
g.callIt(&g.Items.Potions[obj.Which])
}
// The per-potion effect handlers, dispatched through
// gameData.quaffHandlers. Each is one case of the C quaff switch.
func (g *RogueGame) quaffConfusion(trip bool) {
g.applyPotionFuse(PotionConfusion, !trip)
}
func (g *RogueGame) quaffPoison(bool) {
g.Items.Potions[PotionPoison].Know = true g.Items.Potions[PotionPoison].Know = true
if p.IsWearing(RingSustainStrength) { if g.Player.IsWearing(RingSustainStrength) {
g.msg("you feel momentarily sick") g.msg("you feel momentarily sick")
} else { } else {
g.chgStr(-(g.rnd(3) + 1)) g.changeStrength(-(g.rnd(3) + 1))
g.msg("you feel very sick now") g.msg("you feel very sick now")
g.comeDown(0) g.comeDown(0)
} }
case PotionHealing: }
func (g *RogueGame) quaffHealing(bool) {
p := &g.Player
g.Items.Potions[PotionHealing].Know = true g.Items.Potions[PotionHealing].Know = true
if p.Stats.HP += g.roll(p.Stats.Lvl, 4); p.Stats.HP > p.Stats.MaxHP { if p.Stats.HP += g.roll(p.Stats.Lvl, 4); p.Stats.HP > p.Stats.MaxHP {
p.Stats.MaxHP++ p.Stats.MaxHP++
@@ -80,19 +79,25 @@ func (g *RogueGame) quaff() {
g.sight(0) g.sight(0)
g.msg("you begin to feel better") g.msg("you begin to feel better")
case PotionGainStrength: }
func (g *RogueGame) quaffGainStrength(bool) {
g.Items.Potions[PotionGainStrength].Know = true g.Items.Potions[PotionGainStrength].Know = true
g.chgStr(1) g.changeStrength(1)
g.msg("you feel stronger, now. What bulging muscles!") g.msg("you feel stronger, now. What bulging muscles!")
case PotionDetectMonsters: }
p.Flags.Set(SenseMonsters)
func (g *RogueGame) quaffDetectMonsters(bool) {
g.Player.Flags.Set(SenseMonsters)
g.Fuse(DTurnSee, 1, HuhDuration, After) g.Fuse(DTurnSee, 1, HuhDuration, After)
if !g.turnSee(false) { if !g.turnSee(false) {
g.msg("you have a %s feeling for a moment, then it passes", g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange")) g.chooseStr("normal", "strange"))
} }
case PotionDetectMagic: }
func (g *RogueGame) quaffDetectMagic(bool) {
// Potion of magic detection. Show the potions and scrolls // Potion of magic detection. Show the potions and scrolls
show := false show := false
@@ -100,7 +105,7 @@ func (g *RogueGame) quaff() {
g.scr.Hw.Clear() g.scr.Hw.Clear()
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if tp.isMagic() { if g.isMagic(tp) {
show = true show = true
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic) g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
@@ -110,7 +115,7 @@ func (g *RogueGame) quaff() {
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
for _, tp := range mp.Pack { for _, tp := range mp.Pack {
if tp.isMagic() { if g.isMagic(tp) {
show = true show = true
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic) g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
@@ -126,7 +131,10 @@ func (g *RogueGame) quaff() {
g.msg("you have a %s feeling for a moment, then it passes", g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange")) g.chooseStr("normal", "strange"))
} }
case PotionLSD: }
func (g *RogueGame) quaffLSD(trip bool) {
p := &g.Player
if !trip { if !trip {
if p.On(SenseMonsters) { if p.On(SenseMonsters) {
g.turnSee(false) g.turnSee(false)
@@ -136,22 +144,30 @@ func (g *RogueGame) quaff() {
g.SeenStairs = g.seenStairs() g.SeenStairs = g.seenStairs()
} }
g.doPot(PotionLSD, true) g.applyPotionFuse(PotionLSD, true)
case PotionSeeInvisible: }
show := p.On(CanSeeInvisible)
g.doPot(PotionSeeInvisible, false) func (g *RogueGame) quaffSeeInvisible(bool) {
show := g.Player.On(CanSeeInvisible)
g.applyPotionFuse(PotionSeeInvisible, false)
if !show { if !show {
g.invisOn() g.invisOn()
} }
g.sight(0) g.sight(0)
case PotionRaiseLevel: }
func (g *RogueGame) quaffRaiseLevel(bool) {
g.Items.Potions[PotionRaiseLevel].Know = true g.Items.Potions[PotionRaiseLevel].Know = true
g.msg("you suddenly feel much more skillful") g.msg("you suddenly feel much more skillful")
g.raiseLevel() g.raiseLevel()
case PotionExtraHealing: }
func (g *RogueGame) quaffExtraHealing(bool) {
p := &g.Player
g.Items.Potions[PotionExtraHealing].Know = true g.Items.Potions[PotionExtraHealing].Know = true
if p.Stats.HP += g.roll(p.Stats.Lvl, 8); p.Stats.HP > p.Stats.MaxHP { if p.Stats.HP += g.roll(p.Stats.Lvl, 8); p.Stats.HP > p.Stats.MaxHP {
if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 { if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 {
@@ -165,14 +181,19 @@ func (g *RogueGame) quaff() {
g.sight(0) g.sight(0)
g.comeDown(0) g.comeDown(0)
g.msg("you begin to feel much better") g.msg("you begin to feel much better")
case PotionHaste: }
func (g *RogueGame) quaffHaste(bool) {
g.Items.Potions[PotionHaste].Know = true g.Items.Potions[PotionHaste].Know = true
g.After = false g.After = false
if g.addHaste(true) { if g.addHaste(true) {
g.msg("you feel yourself moving much faster") g.msg("you feel yourself moving much faster")
} }
case PotionRestoreStrength: }
func (g *RogueGame) quaffRestoreStrength(bool) {
p := &g.Player
if p.IsRing(Left, RingAddStrength) { if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus) addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
} }
@@ -194,28 +215,27 @@ func (g *RogueGame) quaff() {
} }
g.msg("hey, this tastes great. It make you feel warm all over") g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness: }
g.doPot(PotionBlindness, true)
case PotionLevitation:
g.doPot(PotionLevitation, true)
}
g.status() func (g *RogueGame) quaffBlindness(bool) {
// Throw the item away g.applyPotionFuse(PotionBlindness, true)
g.callIt(&g.Items.Potions[obj.Which]) }
func (g *RogueGame) quaffLevitation(bool) {
g.applyPotionFuse(PotionLevitation, true)
} }
// raiseLevel: the guy just magically went up a level (potions.c // raiseLevel: the guy just magically went up a level (potions.c
// raise_level). // raise_level).
func (g *RogueGame) raiseLevel() { func (g *RogueGame) raiseLevel() {
g.Player.Stats.Exp = eLevels[g.Player.Stats.Lvl-1] + 1 g.Player.Stats.Exp = g.data.eLevels[g.Player.Stats.Lvl-1] + 1
g.checkLevel() g.checkLevel()
} }
// doPot does a potion with standard setup: it uses a fuse and turns on a // applyPotionFuse does a potion with standard setup: it uses a fuse and
// flag (potions.c do_pot). // turns on a flag (potions.c do_pot).
func (g *RogueGame) doPot(kind PotionKind, knowit bool) { func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
pp := &pActions[kind] pp := &g.data.pActions[kind]
if !g.Items.Potions[kind].Know { if !g.Items.Potions[kind].Know {
g.Items.Potions[kind].Know = knowit g.Items.Potions[kind].Know = knowit
} }
@@ -240,10 +260,10 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
} }
// isMagic reports whether an object radiates magic (potions.c is_magic). // isMagic reports whether an object radiates magic (potions.c is_magic).
func (o *Object) isMagic() bool { func (g *RogueGame) isMagic(o *Object) bool {
switch o.Kind { switch o.Kind {
case KindArmor: case KindArmor:
return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which] return o.Flags.Has(Protected) || o.ArmorClass != g.data.aClass[o.Which]
case KindWeapon: case KindWeapon:
return o.HPlus != 0 || o.DPlus != 0 return o.HPlus != 0 || o.DPlus != 0
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet: case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:
@@ -278,7 +298,24 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
if !canSee { if !canSee {
g.addch(mp.OldCh) g.addch(mp.OldCh)
} }
} else if g.showSensed(mp, canSee) {
addNew = true
}
}
if turnOff {
g.Player.Flags.Clear(SenseMonsters)
} else { } else {
g.Player.Flags.Set(SenseMonsters)
}
return addNew
}
// showSensed draws one monster for monster sense, standout when it is
// otherwise invisible; it reports whether the monster was newly revealed
// (the turn-on arm of the C turn_see loop).
func (g *RogueGame) showSensed(mp *Monster, canSee bool) bool {
if !canSee { if !canSee {
g.standout() g.standout()
} }
@@ -292,18 +329,10 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
if !canSee { if !canSee {
g.standend() g.standend()
addNew = true return true
}
}
} }
if turnOff { return false
g.Player.Flags.Clear(SenseMonsters)
} else {
g.Player.Flags.Set(SenseMonsters)
}
return addNew
} }
// seenStairs reports whether the player has seen the stairs (potions.c // seenStairs reports whether the player has seen the stairs (potions.c

View File

@@ -7,18 +7,15 @@ import "fmt"
// ringOn puts a ring on a hand (rings.c ring_on). // ringOn puts a ring on a hand (rings.c ring_on).
func (g *RogueGame) ringOn() { func (g *RogueGame) ringOn() {
p := &g.Player p := &g.Player
obj := g.getItem("put on", KindRing) obj, ok := g.promptPackItem("put on", KindRing)
// Make certain that it is something that we want to wear // Make certain that it is something that we want to wear
if obj == nil { if !ok {
return return
} }
if obj.Kind != KindRing { if obj.Kind != KindRing {
if !g.Options.Terse { g.msg("%s", g.chooseTerse("not a ring",
g.msg("it would be difficult to wrap that around a finger") "it would be difficult to wrap that around a finger"))
} else {
g.msg("not a ring")
}
return return
} }
@@ -28,24 +25,8 @@ func (g *RogueGame) ringOn() {
return return
} }
var ring int ring := g.pickRingHand()
if ring < 0 {
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if ring = g.gethand(); ring < 0 {
return
}
case p.CurRing[Left] == nil:
ring = Left
case p.CurRing[Right] == nil:
ring = Right
default:
if !g.Options.Terse {
g.msg("you already have a ring on each hand")
} else {
g.msg("wearing two")
}
return return
} }
@@ -54,7 +35,7 @@ func (g *RogueGame) ringOn() {
// Calculate the effect it has on the poor guy. // Calculate the effect it has on the poor guy.
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(obj.Bonus) g.changeStrength(obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.invisOn() g.invisOn()
case RingAggravateMonsters: case RingAggravateMonsters:
@@ -65,7 +46,27 @@ func (g *RogueGame) ringOn() {
g.addmsgf("you are now wearing ") g.addmsgf("you are now wearing ")
} }
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh) g.msg("%s (%c)", g.inventoryName(obj, true), obj.PackCh)
}
// pickRingHand chooses the hand for a new ring, asking when both are
// free; negative aborts (rings.c ring_on).
func (g *RogueGame) pickRingHand() int {
p := &g.Player
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
return g.gethand()
case p.CurRing[Left] == nil:
return Left
case p.CurRing[Right] == nil:
return Right
default:
g.msg("%s", g.chooseTerse("wearing two",
"you already have a ring on each hand"))
return -1
}
} }
// ringOff takes off a ring (rings.c ring_off). // ringOff takes off a ring (rings.c ring_off).
@@ -103,7 +104,7 @@ func (g *RogueGame) ringOff() {
} }
if g.dropCheck(obj) { if g.dropCheck(obj) {
g.msg("was wearing %s(%c)", g.invName(obj, true), obj.PackCh) g.msg("was wearing %s(%c)", g.inventoryName(obj, true), obj.PackCh)
} }
} }
@@ -139,25 +140,6 @@ func (g *RogueGame) gethand() int {
} }
} }
// ringUses is the rings.c ring_eat static uses[] table: how much food each
// ring type uses up per turn (negative = a 1-in-n chance of 1).
var ringUses = [NumRingTypes]int{
1, // R_PROTECT
1, // R_ADDSTR
1, // R_SUSTSTR
-3, // R_SEARCH
-5, // R_SEEINVIS
0, // R_NOP
0, // R_AGGR
-3, // R_ADDHIT
-3, // R_ADDDAM
2, // R_REGEN
-2, // R_DIGEST
0, // R_TELEPORT
1, // R_STEALTH
1, // R_SUSTARM
}
// ringEat reports how much food the ring on the given hand uses up // ringEat reports how much food the ring on the given hand uses up
// (rings.c ring_eat). // (rings.c ring_eat).
func (g *RogueGame) ringEat(hand int) int { func (g *RogueGame) ringEat(hand int) int {
@@ -166,7 +148,7 @@ func (g *RogueGame) ringEat(hand int) int {
return 0 return 0
} }
eat := ringUses[ring.RingKind()] eat := g.data.ringUses[ring.RingKind()]
if eat < 0 { if eat < 0 {
if g.rnd(-eat) == 0 { if g.rnd(-eat) == 0 {
eat = 1 eat = 1

View File

@@ -2,39 +2,20 @@ package game
import ( import (
"fmt" "fmt"
"os"
"time" "time"
) )
// rip.c — the fun ends: death or a total win. // rip.c — the fun ends: death or a total win.
// //
// The C functions here call exit(); the port panics with a gameEnd sentinel // The C functions here call exit(). One game run is one process, so the
// that Run recovers, so the terminal is restored by normal unwinding. // port does the same: myExit restores the terminal and exits directly.
// gameEnd is the sentinel carried by the panic that replaces my_exit(). // myExit leaves the process properly (main.c my_exit): it restores the
type gameEnd struct{} // terminal and ends the process. Every C caller exited with status 0.
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
// Every C caller exited with status 0; abnormal exits panic for real.
func (g *RogueGame) myExit() { func (g *RogueGame) myExit() {
g.Playing = false g.scr.Fini()
os.Exit(0)
panic(gameEnd{})
}
var ripArt = []string{
" __________",
" / \\",
" / REST \\",
" / IN \\",
" / PEACE \\",
" / \\",
" | |",
" | |",
" | killed by a |",
" | |",
" | 1980 |",
" *| * * * | *",
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
} }
// death does something really fun when he dies (rip.c death). // death does something really fun when he dies (rip.c death).
@@ -56,7 +37,7 @@ func (g *RogueGame) death(monst byte) {
} else { } else {
year := time.Now().Year() year := time.Now().Year()
for i, line := range ripArt { for i, line := range g.data.ripArt {
g.scr.Std.MvAddStr(8+i, 0, line) g.scr.Std.MvAddStr(8+i, 0, line)
} }
@@ -131,8 +112,25 @@ func (g *RogueGame) totalWinner() {
line := 1 line := 1
for _, obj := range p.Pack { for _, obj := range p.Pack {
worth := 0 worth := g.objectWorth(obj)
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.inventoryName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit()
}
// objectWorth appraises one pack item on the way out, marking it known
// (the switch of rip.c total_winner).
func (g *RogueGame) objectWorth(obj *Object) int {
it := &g.Items it := &g.Items
worth := 0
switch obj.Kind { switch obj.Kind {
case KindFood: case KindFood:
@@ -144,29 +142,45 @@ func (g *RogueGame) totalWinner() {
case KindArmor: case KindArmor:
worth = it.Armors[obj.Which].Worth worth = it.Armors[obj.Which].Worth
worth += (9 - obj.ArmorClass) * 100 worth += (9 - obj.ArmorClass) * 100
worth += 10 * (aClass[obj.Which] - obj.ArmorClass) worth += 10 * (g.data.aClass[obj.Which] - obj.ArmorClass)
obj.Flags.Set(Known) obj.Flags.Set(Known)
case KindScroll: case KindScroll:
op := &it.Scrolls[obj.Which] worth = loreWorth(&it.Scrolls[obj.Which], obj.Count)
worth = op.Worth * obj.Count
if !op.Know {
worth /= 2
}
op.Know = true
case KindPotion: case KindPotion:
op := &it.Potions[obj.Which] worth = loreWorth(&it.Potions[obj.Which], obj.Count)
case KindRing:
worth = g.ringWorth(obj)
case KindWand:
worth = g.wandWorth(obj)
case KindAmulet:
worth = 1000
}
worth = op.Worth * obj.Count if worth < 0 {
worth = 0
}
return worth
}
// loreWorth appraises a scroll or potion, halved when unidentified, and
// identifies it (rip.c total_winner).
func loreWorth(op *ObjInfo, count int) int {
worth := op.Worth * count
if !op.Know { if !op.Know {
worth /= 2 worth /= 2
} }
op.Know = true op.Know = true
case KindRing:
op := &it.Rings[obj.Which] return worth
worth = op.Worth }
// ringWorth appraises a ring: bonus rings gain by their bonus, cursed
// ones are junk (rip.c total_winner).
func (g *RogueGame) ringWorth(obj *Object) int {
op := &g.Items.Rings[obj.Which]
worth := op.Worth
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage || if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity { obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
@@ -184,9 +198,15 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known) obj.Flags.Set(Known)
op.Know = true op.Know = true
case KindWand:
op := &it.Sticks[obj.Which] return worth
worth = op.Worth }
// wandWorth appraises a wand or staff by its charges (rip.c
// total_winner).
func (g *RogueGame) wandWorth(obj *Object) int {
op := &g.Items.Sticks[obj.Which]
worth := op.Worth
worth += 20 * obj.Charges worth += 20 * obj.Charges
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
@@ -196,33 +216,8 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known) obj.Flags.Set(Known)
op.Know = true op.Know = true
case KindAmulet:
worth = 1000
}
if worth < 0 { return worth
worth = 0
}
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.invName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit()
}
// killnameTable is the rip.c nlist[]: special death causes.
var killnameTable = []helpEntry{
{'a', "arrow", true},
{'b', "bolt", true},
{'d', "dart", true},
{'h', "hypothermia", false},
{'s', "starvation", false},
} }
// killname converts a code to a monster name (rip.c killname). // killname converts a code to a monster name (rip.c killname).
@@ -239,7 +234,7 @@ func (g *RogueGame) killname(monst byte, doart bool) string {
sp = "Wally the Wonder Badger" sp = "Wally the Wonder Badger"
article = false article = false
for _, hp := range killnameTable { for _, hp := range g.data.killnameTable {
if hp.Ch == monst { if hp.Ch == monst {
sp = hp.Desc sp = hp.Desc
article = hp.Print article = hp.Print
@@ -257,18 +252,9 @@ func (g *RogueGame) killname(monst byte, doart bool) string {
} }
// DeathDemo implements the -d command line option (main.c): burn some // DeathDemo implements the -d command line option (main.c): burn some
// random numbers to break patterns, then die a random death. // random numbers to break patterns, then die a random death. It does not
// return — death exits the process.
func (g *RogueGame) DeathDemo() { func (g *RogueGame) DeathDemo() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(gameEnd); ok {
return
}
panic(r)
}
}()
dnum := g.rnd(100) dnum := g.rnd(100)
for dnum--; dnum > 0; dnum-- { for dnum--; dnum > 0; dnum-- {
g.rnd(100) g.rnd(100)

View File

@@ -17,9 +17,9 @@ type mazeState struct {
const goldGrp = 1 const goldGrp = 1
// doRooms creates rooms and corridors with a connectivity graph (rooms.c // digRooms creates rooms and corridors with a connectivity graph (rooms.c
// do_rooms). // do_rooms).
func (g *RogueGame) doRooms() { func (g *RogueGame) digRooms() {
var bsze Coord // maximum room size var bsze Coord // maximum room size
bsze.X = NumCols / 3 bsze.X = NumCols / 3
@@ -34,29 +34,25 @@ func (g *RogueGame) doRooms() {
// Put the gone rooms, if any, on the level // Put the gone rooms, if any, on the level
leftOut := g.rnd(4) leftOut := g.rnd(4)
for range leftOut { for range leftOut {
g.Level.Rooms[g.rndRoom()].Flags.Set(Gone) g.Level.Rooms[g.randomRoom()].Flags.Set(Gone)
} }
// dig and populate all the rooms on the level // dig and populate all the rooms on the level
for i := range g.Level.Rooms { for i := range g.Level.Rooms {
g.digRoom(i, bsze)
}
}
// digRoom digs and populates one room (the loop body of rooms.c
// do_rooms).
func (g *RogueGame) digRoom(i int, bsze Coord) {
rp := &g.Level.Rooms[i] rp := &g.Level.Rooms[i]
// Find upper left corner of box that this room goes in // Find upper left corner of box that this room goes in
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y} top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
if rp.Flags.Has(Gone) { if rp.Flags.Has(Gone) {
// Place a gone room. Make certain that there is a blank line g.placeGoneRoom(rp, top, bsze)
// for passage drawing.
for {
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
rp.Max.X = -NumCols
rp.Max.Y = -NumLines return
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
break
}
}
continue
} }
// set room type // set room type
if g.rnd(10) < g.Depth-1 { if g.rnd(10) < g.Depth-1 {
@@ -68,6 +64,33 @@ func (g *RogueGame) doRooms() {
} }
// Find a place and size for a random room // Find a place and size for a random room
if rp.Flags.Has(Maze) { if rp.Flags.Has(Maze) {
placeMazeRoom(rp, top, bsze)
} else {
g.placeNormalRoom(rp, top, bsze)
}
g.drawRoom(rp)
g.roomGold(rp)
g.roomMonster(rp)
}
// placeGoneRoom places a gone room, making certain that there is a
// blank line for passage drawing (rooms.c do_rooms).
func (g *RogueGame) placeGoneRoom(rp *Room, top, bsze Coord) {
for {
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
rp.Max.X = -NumCols
rp.Max.Y = -NumLines
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
return
}
}
}
// placeMazeRoom sizes a maze room to fill its box (rooms.c do_rooms).
func placeMazeRoom(rp *Room, top, bsze Coord) {
rp.Max.X = bsze.X - 1 rp.Max.X = bsze.X - 1
rp.Max.Y = bsze.Y - 1 rp.Max.Y = bsze.Y - 1
@@ -79,7 +102,11 @@ func (g *RogueGame) doRooms() {
rp.Pos.Y++ rp.Pos.Y++
rp.Max.Y-- rp.Max.Y--
} }
} else { }
// placeNormalRoom rolls a place and size for an ordinary room (rooms.c
// do_rooms).
func (g *RogueGame) placeNormalRoom(rp *Room, top, bsze Coord) {
for { for {
rp.Max.X = g.rnd(bsze.X-4) + 4 rp.Max.X = g.rnd(bsze.X-4) + 4
rp.Max.Y = g.rnd(bsze.Y-4) + 4 rp.Max.Y = g.rnd(bsze.Y-4) + 4
@@ -87,14 +114,17 @@ func (g *RogueGame) doRooms() {
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y) rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
if rp.Pos.Y != 0 { if rp.Pos.Y != 0 {
break return
} }
} }
}
// roomGold maybe puts a gold pile in the room (rooms.c do_rooms).
func (g *RogueGame) roomGold(rp *Room) {
if g.rnd(2) != 0 || (g.HasAmulet && g.Depth < g.MaxDepth) {
return
} }
g.drawRoom(rp)
// Put the gold in
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
gold := newObject() gold := newObject()
rp.GoldVal = g.goldCalc() rp.GoldVal = g.goldCalc()
gold.GoldValue = rp.GoldVal gold.GoldValue = rp.GoldVal
@@ -105,9 +135,12 @@ func (g *RogueGame) doRooms() {
gold.Flags = Stackable gold.Flags = Stackable
gold.Group = goldGrp gold.Group = goldGrp
gold.Kind = KindGold gold.Kind = KindGold
attachObj(&g.Level.Objects, gold) g.Level.AddObject(gold)
} }
// Put the monster in
// roomMonster maybe puts a monster in the room; gold attracts them
// (rooms.c do_rooms).
func (g *RogueGame) roomMonster(rp *Room) {
prob := 25 prob := 25
if rp.GoldVal > 0 { if rp.GoldVal > 0 {
prob = 80 prob = 80
@@ -119,14 +152,13 @@ func (g *RogueGame) doRooms() {
g.newMonster(tp, g.randMonster(false), mp) g.newMonster(tp, g.randMonster(false), mp)
g.givePack(tp) g.givePack(tp)
} }
}
} }
// drawRoom draws a box around a room and lays down the floor for normal // drawRoom draws a box around a room and lays down the floor for normal
// rooms; for maze rooms, draws the maze (rooms.c draw_room). // rooms; for maze rooms, draws the maze (rooms.c draw_room).
func (g *RogueGame) drawRoom(rp *Room) { func (g *RogueGame) drawRoom(rp *Room) {
if rp.Flags.Has(Maze) { if rp.Flags.Has(Maze) {
g.doMaze(rp) g.digMaze(rp)
return return
} }
@@ -158,8 +190,8 @@ func (g *RogueGame) horiz(rp *Room, starty int) {
} }
} }
// doMaze digs a maze (rooms.c do_maze). // digMaze digs a maze (rooms.c do_maze).
func (g *RogueGame) doMaze(rp *Room) { func (g *RogueGame) digMaze(rp *Room) {
m := &g.maze m := &g.maze
for y := range m.maze { for y := range m.maze {
for x := range m.maze[y] { for x := range m.maze[y] {
@@ -175,16 +207,35 @@ func (g *RogueGame) doMaze(rp *Room) {
starty := (g.rnd(rp.Max.Y) / 2) * 2 starty := (g.rnd(rp.Max.Y) / 2) * 2
startx := (g.rnd(rp.Max.X) / 2) * 2 startx := (g.rnd(rp.Max.X) / 2) * 2
pos := Coord{Y: starty + m.starty, X: startx + m.startx} pos := Coord{Y: starty + m.starty, X: startx + m.startx}
g.putpass(pos) g.putPassage(pos)
g.dig(starty, startx) g.dig(starty, startx)
} }
// dig digs out from around where we are now, if possible (rooms.c dig). // dig digs out from around where we are now, if possible (rooms.c dig).
func (g *RogueGame) dig(y, x int) { func (g *RogueGame) dig(y, x int) {
m := &g.maze m := &g.maze
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
for { for {
nexty, nextx, ok := g.digPick(y, x)
if !ok {
return
}
g.accountMaze(y, x, nexty, nextx)
g.accountMaze(nexty, nextx, y, x)
g.putPassage(digWallGap(m, y, x, nexty, nextx))
g.putPassage(Coord{Y: nexty + m.starty, X: nextx + m.startx})
g.dig(nexty, nextx)
}
}
// digPick reservoir-picks the next unvisited maze cell; ok is false
// when the digger is boxed in (the candidate scan of rooms.c dig).
func (g *RogueGame) digPick(y, x int) (int, int, bool) {
m := &g.maze
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
cnt := 0 cnt := 0
var nexty, nextx int var nexty, nextx int
@@ -207,40 +258,31 @@ func (g *RogueGame) dig(y, x int) {
} }
} }
if cnt == 0 { return nexty, nextx, cnt != 0
return
}
g.accntMaze(y, x, nexty, nextx)
g.accntMaze(nexty, nextx, y, x)
var pos Coord
if nexty == y {
pos.Y = y + m.starty
if nextx-x < 0 {
pos.X = nextx + m.startx + 1
} else {
pos.X = nextx + m.startx - 1
}
} else {
pos.X = x + m.startx
if nexty-y < 0 {
pos.Y = nexty + m.starty + 1
} else {
pos.Y = nexty + m.starty - 1
}
}
g.putpass(pos)
pos.Y = nexty + m.starty
pos.X = nextx + m.startx
g.putpass(pos)
g.dig(nexty, nextx)
}
} }
// accntMaze accounts for maze exits (rooms.c accnt_maze). // digWallGap picks the wall square to knock out between two maze cells
func (g *RogueGame) accntMaze(y, x, ny, nx int) { // (rooms.c dig).
func digWallGap(m *mazeState, y, x, nexty, nextx int) Coord {
if nexty == y {
pos := Coord{Y: y + m.starty, X: nextx + m.startx - 1}
if nextx-x < 0 {
pos.X = nextx + m.startx + 1
}
return pos
}
pos := Coord{X: x + m.startx, Y: nexty + m.starty - 1}
if nexty-y < 0 {
pos.Y = nexty + m.starty + 1
}
return pos
}
// accountMaze accounts for maze exits (rooms.c accnt_maze).
func (g *RogueGame) accountMaze(y, x, ny, nx int) {
sp := &g.maze.maze[y][x] sp := &g.maze.maze[y][x]
for i := range sp.nexits { for i := range sp.nexits {
if sp.exits[i].Y == ny && sp.exits[i].X == nx { if sp.exits[i].Y == ny && sp.exits[i].X == nx {
@@ -255,8 +297,8 @@ func (g *RogueGame) accntMaze(y, x, ny, nx int) {
} }
} }
// rndPos picks a random spot in a room (rooms.c rnd_pos). // randomPos picks a random spot in a room (rooms.c rnd_pos).
func (g *RogueGame) rndPos(rp *Room) Coord { func (g *RogueGame) randomPos(rp *Room) Coord {
var cp Coord var cp Coord
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1 cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
@@ -278,13 +320,20 @@ func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
return g.findFloorImpl(rp, limit, monst, false) return g.findFloorImpl(rp, limit, monst, false)
} }
// floorChar is what an empty spot looks like in a room: passage in a
// maze, floor otherwise (rooms.c find_floor).
func floorChar(rp *Room) byte {
if rp.Flags.Has(Maze) {
return Passage
}
return Floor
}
func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) { func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) {
var compchar byte var compchar byte
if !pickroom { if !pickroom {
compchar = Floor compchar = floorChar(rp)
if rp.Flags.Has(Maze) {
compchar = Passage
}
} }
cnt := limit cnt := limit
@@ -296,15 +345,11 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
} }
if pickroom { if pickroom {
rp = &g.Level.Rooms[g.rndRoom()] rp = &g.Level.Rooms[g.randomRoom()]
compchar = floorChar(rp)
compchar = Floor
if rp.Flags.Has(Maze) {
compchar = Passage
}
} }
cp := g.rndPos(rp) cp := g.randomPos(rp)
pp := g.Level.At(cp.Y, cp.X) pp := g.Level.At(cp.Y, cp.X)
if monst { if monst {
@@ -321,7 +366,7 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
// enter_room). // enter_room).
func (g *RogueGame) enterRoom(cp Coord) { func (g *RogueGame) enterRoom(cp Coord) {
p := &g.Player p := &g.Player
rp := g.roomin(cp) rp := g.roomIn(cp)
p.Room = rp p.Room = rp
g.doorOpen(rp) g.doorOpen(rp)
@@ -330,6 +375,15 @@ func (g *RogueGame) enterRoom(cp Coord) {
g.move(y, rp.Pos.X) g.move(y, rp.Pos.X)
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ { for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
g.enterRoomCell(y, x)
}
}
}
}
// enterRoomCell draws one square of a room being lit on entry (the loop
// body of rooms.c enter_room).
func (g *RogueGame) enterRoomCell(y, x int) {
tp := g.Level.MonsterAt(y, x) tp := g.Level.MonsterAt(y, x)
ch := g.Level.Char(y, x) ch := g.Level.Char(y, x)
@@ -339,23 +393,24 @@ func (g *RogueGame) enterRoom(cp Coord) {
} else { } else {
g.move(y, x+1) g.move(y, x+1)
} }
} else {
return
}
tp.OldCh = ch tp.OldCh = ch
if !g.seeMonst(tp) { if g.seeMonst(tp) {
if p.On(SenseMonsters) { g.addch(tp.Disguise)
return
}
if g.Player.On(SenseMonsters) {
g.standout() g.standout()
g.addch(tp.Disguise) g.addch(tp.Disguise)
g.standend() g.standend()
} else { } else {
g.addch(ch) g.addch(ch)
} }
} else {
g.addch(tp.Disguise)
}
}
}
}
}
} }
// leaveRoom is the code for when we exit a room (rooms.c leave_room). // leaveRoom is the code for when we exit a room (rooms.c leave_room).
@@ -381,6 +436,16 @@ func (g *RogueGame) leaveRoom(cp Coord) {
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPassNum] p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPassNum]
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ { for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ { for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
g.leaveRoomCell(floor, y, x)
}
}
g.doorOpen(rp)
}
// leaveRoomCell hides one square of a room being left (the loop body of
// rooms.c leave_room).
func (g *RogueGame) leaveRoomCell(floor byte, y, x int) {
g.move(y, x) g.move(y, x)
switch ch := g.inch(); ch { switch ch := g.inch(); ch {
@@ -391,13 +456,16 @@ func (g *RogueGame) leaveRoom(cp Coord) {
default: default:
// to check for monster, we have to strip out the standout // to check for monster, we have to strip out the standout
// bit (our Window returns the bare character already) // bit (our Window returns the bare character already)
if isUpper(ch) { if !isUpper(ch) {
if p.On(SenseMonsters) { return
}
if g.Player.On(SenseMonsters) {
g.standout() g.standout()
g.addch(ch) g.addch(ch)
g.standend() g.standend()
break return
} }
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
@@ -407,9 +475,4 @@ func (g *RogueGame) leaveRoom(cp Coord) {
g.addch(floor) g.addch(floor)
} }
} }
}
}
}
g.doorOpen(rp)
} }

View File

@@ -1,27 +1,75 @@
package game package game
import ( import (
"path/filepath"
"strings" "strings"
"testing" "testing"
) )
// TestRunScriptedSession drives a complete game through Run(): a few // fortify makes the hero effectively immortal for a crash-sweep drive:
// moves, a rest, an inventory, then Q-quit answered yes. // game-over now calls myExit and os.Exit(0) (step 8), which would kill the
func TestRunScriptedSession(t *testing.T) { // test binary, so every death vector is neutralized. Re-applied each turn
tt := &testTerm{input: []byte("hjkl.i Qy")} // because combat, digestion, freezing, and level drain chip away at these.
func fortify(g *RogueGame) {
p := &g.Player
p.Stats.HP = 30000 // survive combat, arrow/dart traps, bolts
p.Stats.MaxHP = 30000 // survive vampire max-hp drain
p.Stats.Exp = 30000 // survive wraith level drain (death when exp hits 0)
p.FoodLeft = 30000 // never starve
g.NoCommand = 0 // never freeze to death (ice monster / sleep trap)
g.NoMove = 0 // never stay stuck in a bear trap
}
g := NewGame(Config{Seed: 99, Term: tt}) // driveTurns runs the game's per-turn loop up to n times, doing the same
// first-level and pre-play setup Run() does. Run() itself no longer
// returns — game-over exits the process — so tests drive command()
// directly, with short scripts that avoid quitting, saving, or playing
// long enough to starve, any of which would exit the test binary.
func driveTurns(t *testing.T, g *RogueGame, n int) {
t.Helper()
err := g.Run() g.startLevel()
if err != nil { g.prePlay()
t.Fatalf("Run: %v", err)
for range n {
g.command()
} }
}
if g.Playing { // TestRunDownStairs stands the hero on the staircase and descends via the
t.Error("still playing after quit") // '>' command through the real turn loop, then checks the level changed.
func TestRunDownStairs(t *testing.T) {
// '>' is a free action (After=false), so it is followed by a paying
// rest ('.') to end the command() call; without a paying action the
// turn loop would spin forever on the auto-fed prompt input.
tt := &testTerm{input: []byte(">.")}
g := New(Params{Seed: 7, Term: tt})
g.NewLevel()
g.Player.Pos = g.Level.Stairs // stand on the stairs
g.restored = true // keep startLevel from regenerating
g.Daemons = DaemonList{} // and give it a fresh daemon table
g.StartDaemon(DRunners, 0, After)
g.StartDaemon(DDoctor, 0, After)
g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After)
driveTurns(t, g, 1)
if g.Depth != 2 {
t.Errorf("depth = %d after descending, want 2", g.Depth)
} }
// After quitting, the scoreboard is the last thing shown (in C it went }
// to stdout after endwin; here it is drawn on the screen).
// TestScoreRendersList checks that the scoreboard is drawn on the screen
// (in C it went to stdout after endwin; here it stays on the screen). The
// quit and death paths that normally show it now exit the process, so the
// display is exercised through score() directly.
func TestScoreRendersList(t *testing.T) {
g := New(Params{Seed: 1, Term: &testTerm{}})
g.Player.Purse = 100
g.score(g.Player.Purse, 1, 0) // flags 1 = quit; posts the top-ten list
found := false found := false
for y := range NumLines { for y := range NumLines {
@@ -31,96 +79,126 @@ func TestRunScriptedSession(t *testing.T) {
} }
if !found { if !found {
t.Error("score list not on screen after quit") t.Error("score list not on screen")
} }
} }
// TestRunManyTurns mashes movement keys for a while as a crash sweep of // TestDeepPlaythrough drives a fortified hero through the real command loop:
// the whole turn loop (daemons, hunger, monsters, combat), ending with a // quaff/read/zap on the first level, then descend through the staircase to
// quit. The input alternates directions so the hero bumps around rooms. // depth 8, saving and restoring mid-way. It is a crash sweep of the turn
func TestRunManyTurns(t *testing.T) { // engine, deep level generation, item effects, and mid-game save/restore.
// Spaces between commands double as answers to any --More-- prompts; // The hero is fortified so no death exits the process (step 8), and the fixed
// without them a single prompt would swallow the rest of the script // seed keeps it deterministic.
// (wait_for eats everything that isn't a space). func TestDeepPlaythrough(t *testing.T) {
moves := []byte("h j k l y u b n s .") g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}})
script := make([]byte, 0, len(moves)*200+7) g.startLevel()
g.prePlay()
fortify(g)
// Stock and use one of each consumable through the command dispatch.
pot := give(g, &Object{Kind: KindPotion, Which: int(PotionHealing)})
scr := give(g, &Object{Kind: KindScroll, Which: int(ScrollMagicMapping)})
wand := newObject()
wand.Kind = KindWand
wand.Which = int(WandLight)
wand.Charges = 5
zap := give(g, wand)
setInput(t, g, 'q', pot) // quaff healing
g.command()
setInput(t, g, 'r', scr) // read magic mapping
g.command()
setInput(t, g, 'z', 'h', zap) // zap the light wand west
g.command()
fortify(g)
// Each consumable identifies itself on use, confirming the q/r/z
// commands actually ran through dispatch (not aborted on a bad prompt).
if !g.Items.Potions[PotionHealing].Know {
t.Error("quaff command did not identify the healing potion")
}
if !g.Items.Scrolls[ScrollMagicMapping].Know {
t.Error("read command did not identify the magic-mapping scroll")
}
if !g.Items.Sticks[WandLight].Know {
t.Error("zap command did not identify the light wand")
}
const wantDepth = 8
for g.Depth < wantDepth {
g.Player.Pos = g.Level.Stairs // stand on the stairs
setInput(t, g, '>', '.') // '>' descends (free), '.' pays the turn
g.command()
fortify(g)
if g.Depth == 4 {
g = saveAndRestore(t, g)
fortify(g)
}
}
if g.Depth != wantDepth {
t.Errorf("depth = %d after descending, want %d", g.Depth, wantDepth)
}
if g.Player.Stats.HP <= 0 {
t.Error("hero died during the playthrough")
}
}
// TestTurnLoopCrashSweep mashes movement, search, and rest through the real
// turn loop for many turns on several seeds, exercising combat, monster AI,
// and traps. The hero is fortified each turn so nothing exits the process,
// and the fixed seeds keep it deterministic; the point is to surface panics.
func TestTurnLoopCrashSweep(t *testing.T) {
// A generous mix of movement, search, and rest. The spaces between
// commands double as answers to any --More-- prompt (wait_for eats
// everything up to a space); without them one prompt would swallow the
// rest of the script. The script is long enough that the bounded drive
// never exhausts it (which would spin on the auto-fed prompt input).
script := []byte(strings.Repeat("h j k l y u b n s . ", 400))
for _, seed := range []int32{1, 99, 2026, 31337} {
g := New(Params{Seed: seed, Term: &testTerm{input: script}})
g.startLevel()
g.prePlay()
for range 200 { for range 200 {
script = append(script, moves...) fortify(g)
g.command()
} }
script = append(script, " Q y Qy"...) if g.Player.Stats.HP <= 0 {
tt := &testTerm{input: script} t.Errorf("seed %d: hero died despite fortify", seed)
g := NewGame(Config{Seed: 31337, Term: tt})
err := g.Run()
if err != nil {
t.Fatalf("Run: %v", err)
} }
if g.Playing {
t.Error("session did not end")
} }
} }
// TestRunDownStairs walks the hero onto the stairs by teleporting there in // saveAndRestore snapshots the game to a file, restores it, checks the key
// wizard style, then descends and keeps playing. // state survived, and returns the restored game ready to keep playing.
func TestRunDownStairs(t *testing.T) { func saveAndRestore(t *testing.T, g *RogueGame) *RogueGame {
tt := &testTerm{input: []byte(">..Qy")} t.Helper()
g := NewGame(Config{Seed: 7, Term: tt})
g.NewLevel()
g.Player.Pos = g.Level.Stairs // stand on the stairs
g.restored = true // keep Run from regenerating the level
g.Daemons = DaemonList{} // and give it a fresh daemon table
g.StartDaemon(DRunners, 0, After)
g.StartDaemon(DDoctor, 0, After)
g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After)
err := g.Run() path := filepath.Join(t.TempDir(), "deep.save")
if err != nil {
t.Fatalf("Run: %v", err) saveErr := g.saveFile(path)
if saveErr != nil {
t.Fatalf("saveFile: %v", saveErr)
} }
if g.Depth != 2 { h, err := Restore(path, Params{Wizard: true, Term: &testTerm{}})
t.Errorf("depth = %d after descending, want 2", g.Depth)
}
}
// TestSaveCommandRoundTrip saves via the 'S' command (as a player would)
// and restores the game.
func TestSaveCommandRoundTrip(t *testing.T) {
// The C get_str caps input at MAXINP=50 characters, so the save path
// must be short: work from the temp directory.
t.Chdir(t.TempDir())
path := "cmd.save"
// 'S' with no default file name goes straight to the name prompt.
script := "S" + path + "\n"
tt := &testTerm{input: []byte(script)}
g := NewGame(Config{Seed: 55, Term: tt})
g.FileName = "" // force the name prompt
runErr := g.Run()
if runErr != nil {
t.Fatalf("Run: %v", runErr)
}
h, err := Restore(path, Config{Term: &testTerm{}})
if err != nil { if err != nil {
t.Fatalf("Restore: %v", err) t.Fatalf("Restore: %v", err)
} }
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse { if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
t.Error("restored game does not match saved game") t.Errorf("restored game diverged: depth %d/%d purse %d/%d",
h.Depth, g.Depth, h.Player.Purse, g.Player.Purse)
} }
// The restored game must be playable.
setInput(t, h, []byte("..Qy")...)
restoredErr := h.Run() return h
if restoredErr != nil {
t.Fatalf("restored Run: %v", restoredErr)
}
} }

View File

@@ -177,8 +177,48 @@ func (g *RogueGame) packIdx(obj *Object) int {
// snapshot captures the complete game state. // snapshot captures the complete game state.
func (g *RogueGame) snapshot() *SaveState { func (g *RogueGame) snapshot() *SaveState {
p := &g.Player st := g.snapshotHeader()
st := &SaveState{
// the map, sans monster pointers (rebuilt on load)
st.Places = make([]savedPlace, len(g.Level.Places))
for i := range g.Level.Places {
st.Places[i] = savedPlace{
Ch: g.Level.Places[i].Ch,
Flags: g.Level.Places[i].Flags,
}
}
// level objects by value; remember their pointers for dest encoding
objAt := make(map[*Object]int, len(g.Level.Objects))
for i, o := range g.Level.Objects {
st.Objects = append(st.Objects, *o)
objAt[o] = i
}
st.Player = g.snapshotPlayer()
// monsters, with chase targets as (kind, index) references
for _, m := range g.Level.Monsters {
sc := savedCreature{
Pos: m.Pos, Turn: m.Turn, Type: m.Type, Disguise: m.Disguise,
OldCh: m.OldCh, Flags: m.Flags, Stats: m.Stats,
RoomIdx: g.roomIdx(m.Room),
}
for _, o := range m.Pack {
sc.Pack = append(sc.Pack, *o)
}
st.Monsters = append(st.Monsters, sc)
st.Dests = append(st.Dests, g.destRefFor(m, objAt))
}
return st
}
// snapshotHeader captures the scalar game state (the field list of
// state.c rs_save_file).
func (g *RogueGame) snapshotHeader() *SaveState {
return &SaveState{
Version: saveFormatVersion, Version: saveFormatVersion,
Seed: g.Rng.Seed, Seed: g.Rng.Seed,
Dnum: g.Dnum, Dnum: g.Dnum,
@@ -226,25 +266,14 @@ func (g *RogueGame) snapshot() *SaveState {
AllScore: g.AllScore, AllScore: g.AllScore,
Screen: g.scr.Std.Contents(), Screen: g.scr.Std.Contents(),
} }
}
// the map, sans monster pointers (rebuilt on load) // snapshotPlayer captures the player, equipment as pack indices (the
st.Places = make([]savedPlace, len(g.Level.Places)) // player half of snapshot).
for i := range g.Level.Places { func (g *RogueGame) snapshotPlayer() savedPlayer {
st.Places[i] = savedPlace{ p := &g.Player
Ch: g.Level.Places[i].Ch,
Flags: g.Level.Places[i].Flags,
}
}
// level objects by value; remember their pointers for dest encoding sp := savedPlayer{
objAt := make(map[*Object]int, len(g.Level.Objects))
for i, o := range g.Level.Objects {
st.Objects = append(st.Objects, *o)
objAt[o] = i
}
// the player
st.Player = savedPlayer{
Body: savedCreature{ Body: savedCreature{
Pos: p.Pos, Turn: p.Turn, Type: p.Type, Disguise: p.Disguise, Pos: p.Pos, Turn: p.Turn, Type: p.Type, Disguise: p.Disguise,
OldCh: p.OldCh, Flags: p.Flags, Stats: p.Stats, OldCh: p.OldCh, Flags: p.Flags, Stats: p.Stats,
@@ -260,61 +289,72 @@ func (g *RogueGame) snapshot() *SaveState {
MaxStats: p.MaxStats, VfHit: p.VfHit, MaxStats: p.MaxStats, VfHit: p.VfHit,
} }
for _, o := range p.Pack { for _, o := range p.Pack {
st.Player.Body.Pack = append(st.Player.Body.Pack, *o) sp.Body.Pack = append(sp.Body.Pack, *o)
} }
// monsters, with chase targets as (kind, index) references return sp
for _, m := range g.Level.Monsters { }
sc := savedCreature{
Pos: m.Pos, Turn: m.Turn, Type: m.Type, Disguise: m.Disguise,
OldCh: m.OldCh, Flags: m.Flags, Stats: m.Stats,
RoomIdx: g.roomIdx(m.Room),
}
for _, o := range m.Pack {
sc.Pack = append(sc.Pack, *o)
}
st.Monsters = append(st.Monsters, sc)
ref := destRef{}
// destRefFor encodes a monster's chase target as a (kind, index)
// reference: the hero, another monster, a level object, or room gold
// (state.c rs_write_thing).
func (g *RogueGame) destRefFor(m *Monster, objAt map[*Object]int) destRef {
switch { switch {
case m.Dest == nil: case m.Dest == nil:
case m.Dest == &p.Pos: return destRef{}
ref = destRef{Kind: 1} case m.Dest == &g.Player.Pos:
default: return destRef{Kind: 1}
}
for mi, om := range g.Level.Monsters { for mi, om := range g.Level.Monsters {
if m.Dest == &om.Pos { if m.Dest == &om.Pos {
ref = destRef{Kind: 2, Idx: mi} return destRef{Kind: 2, Idx: mi}
} }
} }
if ref.Kind == 0 {
for _, oo := range g.Level.Objects { for _, oo := range g.Level.Objects {
if m.Dest == &oo.Pos { if m.Dest == &oo.Pos {
ref = destRef{Kind: 3, Idx: objAt[oo]} return destRef{Kind: 3, Idx: objAt[oo]}
}
} }
} }
if ref.Kind == 0 {
for ri := range g.Level.Rooms { for ri := range g.Level.Rooms {
if m.Dest == &g.Level.Rooms[ri].Gold { if m.Dest == &g.Level.Rooms[ri].Gold {
ref = destRef{Kind: 4, Idx: ri} return destRef{Kind: 4, Idx: ri}
}
}
} }
} }
st.Dests = append(st.Dests, ref) return destRef{}
}
return st
} }
// applySnapshot rebuilds live game state from a snapshot. // applySnapshot rebuilds live game state from a snapshot.
func (g *RogueGame) applySnapshot(st *SaveState) { func (g *RogueGame) applySnapshot(st *SaveState) {
p := &g.Player g.applyHeader(st)
for i := range g.Level.Places {
g.Level.Places[i] = Place{
Ch: st.Places[i].Ch,
Flags: st.Places[i].Flags,
}
}
// level objects
g.Level.Objects = nil
for i := range st.Objects {
o := st.Objects[i]
g.Level.Objects = append(g.Level.Objects, &o)
}
g.applyPlayer(st)
g.applyMonsters(st)
g.applyDests(st)
g.scr.Std.SetContents(st.Screen)
}
// applyHeader restores the scalar game state (the field list of
// applySnapshot).
func (g *RogueGame) applyHeader(st *SaveState) {
g.Rng.Seed = st.Seed g.Rng.Seed = st.Seed
g.Dnum = st.Dnum g.Dnum = st.Dnum
g.Whoami = st.Whoami g.Whoami = st.Whoami
@@ -329,6 +369,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Passages = st.Passages g.Level.Passages = st.Passages
g.Level.Stairs = st.Stairs g.Level.Stairs = st.Stairs
g.Level.TrapCount = st.TrapCount g.Level.TrapCount = st.TrapCount
g.applyTurnState(st)
}
// applyTurnState restores the in-turn command state (the second half of
// applyHeader).
func (g *RogueGame) applyTurnState(st *SaveState) {
g.After = st.After g.After = st.After
g.Again = st.Again g.Again = st.Again
g.NoScore = st.NoScoreF g.NoScore = st.NoScoreF
@@ -359,22 +405,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.LastScore = st.LastScore g.LastScore = st.LastScore
g.AllScore = st.AllScore g.AllScore = st.AllScore
g.Playing = true g.Playing = true
}
for i := range g.Level.Places { // applyPlayer restores the player, resolving equipment pack indices
g.Level.Places[i] = Place{ // (the player half of applySnapshot).
Ch: st.Places[i].Ch, func (g *RogueGame) applyPlayer(st *SaveState) {
Flags: st.Places[i].Flags, p := &g.Player
}
}
// level objects
g.Level.Objects = nil
for i := range st.Objects {
o := st.Objects[i]
g.Level.Objects = append(g.Level.Objects, &o)
}
// the player
sp := &st.Player sp := &st.Player
p.Pos = sp.Body.Pos p.Pos = sp.Body.Pos
p.Turn = sp.Body.Turn p.Turn = sp.Body.Turn
@@ -411,8 +447,11 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.NoFood = sp.NoFood p.NoFood = sp.NoFood
p.MaxStats = sp.MaxStats p.MaxStats = sp.MaxStats
p.VfHit = sp.VfHit p.VfHit = sp.VfHit
}
// monsters, their map index, and their chase targets // applyMonsters rebuilds the monster list and its map index from a
// snapshot (the monster half of applySnapshot).
func (g *RogueGame) applyMonsters(st *SaveState) {
g.Level.Monsters = nil g.Level.Monsters = nil
for i := range st.Monsters { for i := range st.Monsters {
sc := &st.Monsters[i] sc := &st.Monsters[i]
@@ -430,13 +469,17 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Monsters = append(g.Level.Monsters, m) g.Level.Monsters = append(g.Level.Monsters, m)
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m) g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
} }
}
// applyDests re-aims the monsters' chase targets from their (kind,
// index) references (the fixup half of applySnapshot).
func (g *RogueGame) applyDests(st *SaveState) {
for i, ref := range st.Dests { for i, ref := range st.Dests {
m := g.Level.Monsters[i] m := g.Level.Monsters[i]
switch ref.Kind { switch ref.Kind {
case 1: case 1:
m.Dest = &p.Pos m.Dest = &g.Player.Pos
case 2: case 2:
m.Dest = &g.Level.Monsters[ref.Idx].Pos m.Dest = &g.Level.Monsters[ref.Idx].Pos
case 3: case 3:
@@ -445,91 +488,51 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
m.Dest = &g.Level.Rooms[ref.Idx].Gold m.Dest = &g.Level.Rooms[ref.Idx].Gold
} }
} }
g.scr.Std.SetContents(st.Screen)
} }
// saveGame implements the "save game" command (save.c save_game). The C // saveAnswer is a yes/no/escape prompt result in the save-game flow.
// goto over/gotfile flow becomes the useDefault flag. type saveAnswer int
// The saveGame prompt outcomes.
const (
saveYes saveAnswer = iota
saveNo
saveAbort
)
// saveGame implements the "save game" command (save.c save_game). The
// labeled prompt loop stands in for the C goto over/gotfile flow.
func (g *RogueGame) saveGame() { func (g *RogueGame) saveGame() {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
over: prompt:
for {
useDefault := false useDefault := false
if g.FileName != "" { if g.FileName != "" {
var c byte a := g.askDefaultSave()
if a == saveAbort {
for {
g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0
if c == Escape {
g.msg("")
return return
} }
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' { useDefault = a == saveYes
break
}
g.msg("please answer Y or N")
}
if c == 'y' || c == 'Y' {
g.addstr("Yes\n")
g.refresh()
useDefault = true
}
} }
for { for {
var buf string buf, ok := g.saveFileName(useDefault)
if useDefault { if !ok {
buf = g.FileName return
}
useDefault = false useDefault = false
} else {
g.Msgs.Mpos = 0
g.msg("file name: ")
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
a := g.saveCheckOverwrite(buf)
if a == saveAbort {
return return
} }
g.Msgs.Mpos = 0 if a == saveNo {
} continue prompt // the C goto over: start again
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr == nil {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
c := g.readchar()
if c == Escape {
g.msg("")
return
}
if c == 'y' || c == 'Y' {
break
}
if c == 'n' || c == 'N' {
goto over
}
g.msg("Please answer Y or N")
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
} }
g.FileName = buf g.FileName = buf
@@ -541,12 +544,104 @@ over:
continue continue
} }
break break prompt
}
} }
g.myExit() g.myExit()
} }
// askDefaultSave asks whether to save to the current file name (save.c
// save_game).
func (g *RogueGame) askDefaultSave() saveAnswer {
for {
g.msg("save file (%s)? ", g.FileName)
c := g.readchar()
g.Msgs.Mpos = 0
switch c {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
g.addstr("Yes\n")
g.refresh()
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("please answer Y or N")
}
}
// saveFileName picks the save path: the default, or a prompted one; ok
// is false when the player quit the prompt (save.c save_game).
func (g *RogueGame) saveFileName(useDefault bool) (string, bool) {
if useDefault {
return g.FileName, true
}
g.Msgs.Mpos = 0
g.msg("file name: ")
buf := ""
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
return "", false
}
g.Msgs.Mpos = 0
return buf, true
}
// saveCheckOverwrite guards an existing file: saveNo restarts the whole
// prompt, saveAbort quits (save.c save_game).
func (g *RogueGame) saveCheckOverwrite(buf string) saveAnswer {
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr != nil {
return saveYes
}
answer := g.askOverwrite()
if answer != saveYes {
return answer
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
return saveYes
}
// askOverwrite asks whether to overwrite the existing file (save.c
// save_game).
func (g *RogueGame) askOverwrite() saveAnswer {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
switch g.readchar() {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("Please answer Y or N")
}
}
// saveFile writes the saved game (save.c save_file). A failed write means // saveFile writes the saved game (save.c save_file). A failed write means
// a corrupt save, so the file is removed before reporting the error. // a corrupt save, so the file is removed before reporting the error.
func (g *RogueGame) saveFile(path string) error { func (g *RogueGame) saveFile(path string) error {
@@ -586,7 +681,7 @@ var ErrSaveOutOfDate = errors.New("sorry, saved game is out of date")
// Restore restores a saved game from a file (save.c restore). The file is // Restore restores a saved game from a file (save.c restore). The file is
// deleted, as in C, to defeat restarting from the same save. // deleted, as in C, to defeat restarting from the same save.
func Restore(path string, cfg Config) (*RogueGame, error) { func Restore(path string, params Params) (*RogueGame, error) {
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
if err != nil { if err != nil {
return nil, err return nil, err
@@ -606,14 +701,16 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
} }
g := &RogueGame{ g := &RogueGame{
data: newGameData(),
Rng: &Rng{}, Rng: &Rng{},
Playing: true, Playing: true,
ScorePath: cfg.ScorePath, ScorePath: params.ScorePath,
FileName: path, FileName: path,
rogueOpts: cfg.RogueOpts, rogueOpts: params.RogueOpts,
restored: true, restored: true,
} }
g.scr = NewScreen(cfg.Term) g.scr = NewScreen(params.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.applySnapshot(&st) g.applySnapshot(&st)
// defeat multiple restarting from the same place // defeat multiple restarting from the same place

View File

@@ -29,7 +29,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Fatalf("saveFile: %v", saveErr) t.Fatalf("saveFile: %v", saveErr)
} }
h, err := Restore(path, Config{Term: &testTerm{}}) h, err := Restore(path, Params{Term: &testTerm{}})
if err != nil { if err != nil {
t.Fatalf("Restore: %v", err) t.Fatalf("Restore: %v", err)
} }
@@ -39,6 +39,15 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Error("save file not deleted on restore (C anti-restart rule)") t.Error("save file not deleted on restore (C anti-restart rule)")
} }
checkRestoredState(t, g, h)
checkRestoredMonsters(t, g, h)
checkEquipmentAliasing(t, g, h)
}
// checkRestoredState verifies the scalar state survived the round trip.
func checkRestoredState(t *testing.T, g, h *RogueGame) {
t.Helper()
if h.Player.Purse != 123 || h.Player.FoodLeft != 777 { if h.Player.Purse != 123 || h.Player.FoodLeft != 777 {
t.Errorf("player state lost: purse=%d food=%d", t.Errorf("player state lost: purse=%d food=%d",
h.Player.Purse, h.Player.FoodLeft) h.Player.Purse, h.Player.FoodLeft)
@@ -67,13 +76,22 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
if renderMap(h) != renderMap(g) { if renderMap(h) != renderMap(g) {
t.Error("restored level map differs") t.Error("restored level map differs")
} }
}
// checkRestoredMonsters verifies the monster list and its pointer fixups
// survived the round trip.
func checkRestoredMonsters(t *testing.T, g, h *RogueGame) {
t.Helper()
if len(h.Level.Monsters) != len(g.Level.Monsters) { if len(h.Level.Monsters) != len(g.Level.Monsters) {
t.Fatalf("monster count %d != %d", t.Fatalf("monster count %d != %d",
len(h.Level.Monsters), len(g.Level.Monsters)) len(h.Level.Monsters), len(g.Level.Monsters))
} }
if len(g.Level.Monsters) > 0 { if len(g.Level.Monsters) == 0 {
return
}
m := h.Level.Monsters[0] m := h.Level.Monsters[0]
if m.Dest != &h.Player.Pos { if m.Dest != &h.Player.Pos {
t.Error("monster chase target not re-aliased to the hero") t.Error("monster chase target not re-aliased to the hero")
@@ -86,9 +104,13 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
if m.Room == nil { if m.Room == nil {
t.Error("monster room pointer not rebuilt") t.Error("monster room pointer not rebuilt")
} }
} }
// Equipment aliasing: the wielded mace must be the same *Object as the
// one in the pack. // checkEquipmentAliasing verifies the wielded mace is the same *Object as
// the one in the pack.
func checkEquipmentAliasing(t *testing.T, g, h *RogueGame) {
t.Helper()
st := g.snapshot() st := g.snapshot()
t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d", t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d",
st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing, st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing,
@@ -132,7 +154,7 @@ func TestRestoreRejectsWrongVersion(t *testing.T) {
t.Fatal(closeErr) t.Fatal(closeErr)
} }
_, restoreErr := Restore(path, Config{}) _, restoreErr := Restore(path, Params{})
if restoreErr == nil { if restoreErr == nil {
t.Error("restore accepted an out-of-date save") t.Error("restore accepted an out-of-date save")
} }

View File

@@ -25,13 +25,6 @@ type ScoreEnt struct {
Time int64 Time int64
} }
var scoreReasons = [4]string{
"killed",
"quit",
"A total winner",
"killed with Amulet",
}
// rdScore reads the scoreboard file (save.c rd_score). // rdScore reads the scoreboard file (save.c rd_score).
func (g *RogueGame) rdScore() []ScoreEnt { func (g *RogueGame) rdScore() []ScoreEnt {
topTen := make([]ScoreEnt, numScores) topTen := make([]ScoreEnt, numScores)
@@ -114,26 +107,30 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
topTen := g.rdScore() topTen := g.rdScore()
// Insert her in list if need be // Insert her in list if need be
ins := -1 ins := -1
if !g.NoScore && flags >= 0 { if !g.NoScore && flags >= 0 {
ins = g.scoreInsert(topTen, amount, flags, monst)
}
lines, highlight := g.scoreLines(topTen, ins)
g.showScores(lines, highlight)
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
}
// scoreInsert slots the new score into the top ten, honoring the
// one-score-per-losing-uid rule; -1 means it did not place (the
// insertion half of rip.c score).
func (g *RogueGame) scoreInsert(topTen []ScoreEnt, amount, flags int, monst byte) int {
uid := os.Getuid() uid := os.Getuid()
scp := len(topTen) scp := g.scoreSlot(topTen, amount, flags, uid)
for i := range topTen { if scp >= len(topTen) {
if amount > topTen[i].Score { return -1
scp = i
break
} else if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid
scp = len(topTen)
break
}
} }
if scp < len(topTen) {
sc2 := len(topTen) - 1 sc2 := len(topTen) - 1
if flags != 2 && !g.AllScore { if flags != 2 && !g.AllScore {
for i := scp; i < len(topTen); i++ { for i := scp; i < len(topTen); i++ {
@@ -164,11 +161,32 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
Level: lvl, Level: lvl,
Time: time.Now().Unix(), Time: time.Now().Unix(),
} }
ins = scp
return scp
}
// scoreSlot finds where the new score lands: len(topTen) when it does
// not place, or when this uid already holds a losing score (the scan of
// rip.c score).
func (g *RogueGame) scoreSlot(topTen []ScoreEnt, amount, flags, uid int) int {
for i := range topTen {
if amount > topTen[i].Score {
return i
}
if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid
return len(topTen)
} }
} }
// Build the list display return len(topTen)
}
// scoreLines formats the scoreboard, noting which display line holds
// the freshly inserted score (the display half of rip.c score).
func (g *RogueGame) scoreLines(topTen []ScoreEnt, ins int) ([]string, int) {
label := "Rogueists" label := "Rogueists"
if g.AllScore { if g.AllScore {
label = "Scores" label = "Scores"
@@ -187,7 +205,7 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
} }
line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1, line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1,
scp.Score, scp.Name, scoreReasons[scp.Flags], scp.Level) scp.Score, scp.Name, g.data.scoreReasons[scp.Flags], scp.Level)
if scp.Flags == 0 || scp.Flags == 3 { if scp.Flags == 0 || scp.Flags == 3 {
line += " by " + g.killname(scp.Monster, true) line += " by " + g.killname(scp.Monster, true)
} }
@@ -201,7 +219,20 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
lines = append(lines, line) lines = append(lines, line)
} }
if g.scr != nil && g.scr.term != nil { return lines, highlight
}
// showScores prints the scoreboard on the screen when there is one,
// else to standard output (rip.c score).
func (g *RogueGame) showScores(lines []string, highlight int) {
if g.scr == nil || g.scr.term == nil {
for _, line := range lines {
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
}
return
}
g.clear() g.clear()
for i, line := range lines { for i, line := range lines {
@@ -217,16 +248,6 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
} }
g.refresh() g.refresh()
} else {
for _, line := range lines {
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
}
}
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
} }
// ShowScores implements the -s command line option: print the scoreboard // ShowScores implements the -s command line option: print the scoreboard

View File

@@ -16,6 +16,9 @@ type Terminal interface {
// ReadChar blocks for the next key, translated to Rogue's input bytes // ReadChar blocks for the next key, translated to Rogue's input bytes
// (arrows become hjkl, control keys their C0 codes). // (arrows become hjkl, control keys their C0 codes).
ReadChar() byte ReadChar() byte
// Fini restores the device to its pre-game state (curses endwin). The
// game calls it on its way out, since one game run is one process.
Fini()
} }
// cell is one screen position. // cell is one screen position.
@@ -213,6 +216,13 @@ func (s *Screen) Refresh() {
} }
} }
// Fini restores the terminal device, if there is one (curses endwin).
func (s *Screen) Fini() {
if s.term != nil {
s.term.Fini()
}
}
// RefreshWin pushes an arbitrary window to the device (curses wrefresh). // RefreshWin pushes an arbitrary window to the device (curses wrefresh).
func (s *Screen) RefreshWin(w *Window) { func (s *Screen) RefreshWin(w *Window) {
if s.term != nil { if s.term != nil {

View File

@@ -2,23 +2,13 @@ package game
// scrolls.c — read a scroll and let it happen. // scrolls.c — read a scroll and let it happen.
// idType maps identify scrolls to the kind of item they identify
// (scrolls.c static id_type).
var idType = [ScrollIdentifyRingOrStick + 1]ObjectKind{
ScrollIdentifyPotion: KindPotion,
ScrollIdentifyScroll: KindScroll,
ScrollIdentifyWeapon: KindWeapon,
ScrollIdentifyArmor: KindArmor,
ScrollIdentifyRingOrStick: KindRingOrStick,
}
// readScroll reads a scroll from the pack and does the appropriate thing // readScroll reads a scroll from the pack and does the appropriate thing
// (scrolls.c read_scroll). // (scrolls.c read_scroll).
func (g *RogueGame) readScroll() { func (g *RogueGame) readScroll() {
p := &g.Player p := &g.Player
obj := g.getItem("read", KindScroll) obj, ok := g.promptPackItem("read", KindScroll)
if obj == nil { if !ok {
return return
} }
@@ -38,20 +28,63 @@ func (g *RogueGame) readScroll() {
// Get rid of the thing // Get rid of the thing
g.leavePack(obj, false, false) g.leavePack(obj, false, false)
switch obj.ScrollKind() { if h := g.data.readHandlers[obj.ScrollKind()]; h != nil {
case ScrollMonsterConfusion: h(g, obj)
}
g.look(true) // put the result of the scroll on the screen
g.status()
g.callIt(&g.Items.Scrolls[obj.Which])
}
// The per-scroll effect handlers, dispatched through
// gameData.readHandlers. Each is one case of the C read_scroll switch.
func (g *RogueGame) readMonsterConfusion(*Object) {
// Scroll of monster confusion. Give him that power. // Scroll of monster confusion. Give him that power.
p.Flags.Set(CanConfuse) g.Player.Flags.Set(CanConfuse)
g.msg("your hands begin to glow %s", g.pickColor("red")) g.msg("your hands begin to glow %s", g.pickColor("red"))
case ScrollEnchantArmor: }
func (g *RogueGame) readEnchantArmor(*Object) {
p := &g.Player
if p.CurArmor != nil { if p.CurArmor != nil {
p.CurArmor.ArmorClass-- p.CurArmor.ArmorClass--
p.CurArmor.Flags.Clear(Cursed) p.CurArmor.Flags.Clear(Cursed)
g.msg("your armor glows %s for a moment", g.pickColor("silver")) g.msg("your armor glows %s for a moment", g.pickColor("silver"))
} }
case ScrollHoldMonster: }
func (g *RogueGame) readHoldMonster(*Object) {
// Hold monster scroll. Stop all monsters within two spaces from // Hold monster scroll. Stop all monsters within two spaces from
// chasing after the hero. // chasing after the hero.
held := g.holdMonstersNear()
if held > 0 {
g.addmsgf("the monster")
if held > 1 {
g.addmsgf("s around you")
}
g.addmsgf(" freeze")
if held == 1 {
g.addmsgf("s")
}
g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true
} else {
g.msg("you feel a strange sense of loss")
}
}
// holdMonstersNear freezes every awake monster within two spaces of the
// hero and reports how many froze (the scan of the C S_HOLD case).
func (g *RogueGame) holdMonstersNear() int {
p := &g.Player
held := 0 held := 0
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ { for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
@@ -73,34 +106,35 @@ func (g *RogueGame) readScroll() {
} }
} }
if held > 0 { return held
g.addmsgf("the monster") }
if held > 1 { func (g *RogueGame) readSleep(*Object) {
g.addmsgf("s around you")
}
g.addmsgf(" freeze")
if held == 1 {
g.addmsgf("s")
}
g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true
} else {
g.msg("you feel a strange sense of loss")
}
case ScrollSleep:
// Scroll which makes you fall asleep // Scroll which makes you fall asleep
g.Items.Scrolls[ScrollSleep].Know = true g.Items.Scrolls[ScrollSleep].Know = true
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
p.Flags.Clear(Awake) g.Player.Flags.Clear(Awake)
g.msg("you fall asleep") g.msg("you fall asleep")
case ScrollCreateMonster: }
func (g *RogueGame) readCreateMonster(*Object) {
// Create a monster: first look in a circle around him, next try // Create a monster: first look in a circle around him, next try
// his room, otherwise give up // his room, otherwise give up
mp, ok := g.createMonsterSpot()
if !ok {
g.msg("you hear a faint cry of anguish in the distance")
} else {
tp := &Monster{}
g.newMonster(tp, g.randMonster(false), mp)
}
}
// createMonsterSpot reservoir-samples a legal spot around the hero for a
// created monster; ok is false when every neighbor is blocked (the scan
// of the C S_CREATE case).
func (g *RogueGame) createMonsterSpot() (Coord, bool) {
p := &g.Player
i := 0 i := 0
var mp Coord var mp Coord
@@ -114,7 +148,7 @@ func (g *RogueGame) readScroll() {
// Or anything else nasty // Or anything else nasty
if ch := g.Level.VisibleChar(y, x); stepOk(ch) { if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
if ch == Scroll { if ch == Scroll {
if fo := g.findObj(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster { if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
continue continue
} }
} }
@@ -126,59 +160,63 @@ func (g *RogueGame) readScroll() {
} }
} }
if i == 0 { return mp, i != 0
g.msg("you hear a faint cry of anguish in the distance") }
} else {
tp := &Monster{} func (g *RogueGame) readIdentify(obj *Object) {
g.newMonster(tp, g.randMonster(false), mp)
}
case ScrollIdentifyPotion, ScrollIdentifyScroll, ScrollIdentifyWeapon, ScrollIdentifyArmor, ScrollIdentifyRingOrStick:
// Identify, let him figure something out // Identify, let him figure something out
g.Items.Scrolls[obj.Which].Know = true g.Items.Scrolls[obj.Which].Know = true
g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name) g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name)
g.whatis(true, idType[obj.ScrollKind()]) g.whatis(true, g.data.idType[obj.ScrollKind()])
case ScrollMagicMapping: }
func (g *RogueGame) readMagicMapping(*Object) {
// Scroll of magic mapping. // Scroll of magic mapping.
g.Items.Scrolls[ScrollMagicMapping].Know = true g.Items.Scrolls[ScrollMagicMapping].Know = true
g.msg("oh, now this scroll has a map on it") g.msg("oh, now this scroll has a map on it")
// take all the things we want to keep hidden out of the window // take all the things we want to keep hidden out of the window
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := range NumCols { for x := range NumCols {
g.revealSpot(y, x)
}
}
}
// revealSpot uncovers one map cell for magic mapping and draws it (the
// loop body of the C SCR_MAP case).
func (g *RogueGame) revealSpot(y, x int) {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
ch := revealChar(pp)
if ch != ' ' {
if tp := pp.Monst; tp != nil {
tp.OldCh = ch
if !g.Player.On(SenseMonsters) {
g.mvaddch(y, x, ch)
}
} else {
g.mvaddch(y, x, ch)
}
}
}
// revealChar decides what magic mapping shows at a cell, making secret
// doors, hidden passages, and hidden traps real as a side effect; ' '
// means show nothing (the switch of the C SCR_MAP loop).
func revealChar(pp *Place) byte {
ch := pp.Ch ch := pp.Ch
pass := false pass := false
switch ch { switch ch {
case Door, Stairs: case Door, Stairs:
case '-', '|': case '-', '|':
if !pp.Flags.Has(FReal) { ch = revealWall(pp)
ch = Door
pp.Ch = Door
pp.Flags.Set(FReal)
}
case ' ': case ' ':
if pp.Flags.Has(FReal) { pass = revealSolid(pp)
// def: hidden things in walls stay hidden
if pp.Flags.Has(FPassage) {
pass = true
} else {
ch = ' '
}
} else {
pp.Flags.Set(FReal)
pp.Ch = Passage
pass = true
}
case Passage: case Passage:
pass = true pass = true
case Floor: case Floor:
if pp.Flags.Has(FReal) { ch = revealFloor(pp)
ch = ' '
} else {
ch = Trap
pp.Ch = Trap
pp.Flags.Set(FSeen | FReal)
}
default: default:
if pp.Flags.Has(FPassage) { if pp.Flags.Has(FPassage) {
pass = true pass = true
@@ -197,19 +235,50 @@ func (g *RogueGame) readScroll() {
ch = Passage ch = Passage
} }
if ch != ' ' { return ch
if tp := pp.Monst; tp != nil { }
tp.OldCh = ch
if !p.On(SenseMonsters) { // revealWall handles a wall cell for magic mapping: a secret door
g.mvaddch(y, x, ch) // becomes a real door (the '-'/'|' arm).
func revealWall(pp *Place) byte {
if !pp.Flags.Has(FReal) {
pp.Ch = Door
pp.Flags.Set(FReal)
return Door
} }
} else {
g.mvaddch(y, x, ch) return pp.Ch
}
// revealSolid handles a blank cell for magic mapping, reporting whether
// it is passage: hidden passages become real; hidden things in walls
// stay hidden (the ' ' arm).
func revealSolid(pp *Place) bool {
if pp.Flags.Has(FReal) {
return pp.Flags.Has(FPassage)
} }
pp.Flags.Set(FReal)
pp.Ch = Passage
return true
}
// revealFloor handles a floor cell for magic mapping: a hidden trap
// becomes a real, seen trap; real floor shows nothing (the FLOOR arm).
func revealFloor(pp *Place) byte {
if pp.Flags.Has(FReal) {
return ' '
} }
}
} pp.Ch = Trap
case ScrollFoodDetection: pp.Flags.Set(FSeen | FReal)
return Trap
}
func (g *RogueGame) readFoodDetection(*Object) {
// Food detection // Food detection
found := false found := false
@@ -229,8 +298,11 @@ func (g *RogueGame) readScroll() {
} else { } else {
g.msg("your nose tingles") g.msg("your nose tingles")
} }
case ScrollTeleportation: }
func (g *RogueGame) readTeleportation(*Object) {
// Scroll of teleportation: make him disappear and reappear // Scroll of teleportation: make him disappear and reappear
p := &g.Player
curRoom := p.Room curRoom := p.Room
g.teleport() g.teleport()
@@ -238,7 +310,10 @@ func (g *RogueGame) readScroll() {
if curRoom != p.Room { if curRoom != p.Room {
g.Items.Scrolls[ScrollTeleportation].Know = true g.Items.Scrolls[ScrollTeleportation].Know = true
} }
case ScrollEnchantWeapon: }
func (g *RogueGame) readEnchantWeapon(*Object) {
p := &g.Player
if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon { if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon {
g.msg("you feel a strange sense of loss") g.msg("you feel a strange sense of loss")
} else { } else {
@@ -253,23 +328,34 @@ func (g *RogueGame) readScroll() {
g.msg("your %s glows %s for a moment", g.msg("your %s glows %s for a moment",
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue")) g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
} }
case ScrollScareMonster: }
func (g *RogueGame) readScareMonster(*Object) {
// Reading it is a mistake and produces laughter at her poor boo // Reading it is a mistake and produces laughter at her poor boo
// boo. // boo.
g.msg("you hear maniacal laughter in the distance") g.msg("you hear maniacal laughter in the distance")
case ScrollRemoveCurse: }
func (g *RogueGame) readRemoveCurse(*Object) {
p := &g.Player
uncurse(p.CurArmor) uncurse(p.CurArmor)
uncurse(p.CurWeapon) uncurse(p.CurWeapon)
uncurse(p.CurRing[Left]) uncurse(p.CurRing[Left])
uncurse(p.CurRing[Right]) uncurse(p.CurRing[Right])
g.msg("%s", g.chooseStr("you feel in touch with the Universal Onenes", g.msg("%s", g.chooseStr("you feel in touch with the Universal Onenes",
"you feel as if somebody is watching over you")) "you feel as if somebody is watching over you"))
case ScrollAggravateMonsters: }
func (g *RogueGame) readAggravateMonsters(*Object) {
// This scroll aggravates all the monsters on the current level // This scroll aggravates all the monsters on the current level
// and sets them running towards the hero // and sets them running towards the hero
g.aggravate() g.aggravate()
g.msg("you hear a high pitched humming noise") g.msg("you hear a high pitched humming noise")
case ScrollProtectArmor: }
func (g *RogueGame) readProtectArmor(*Object) {
p := &g.Player
if p.CurArmor != nil { if p.CurArmor != nil {
p.CurArmor.Flags.Set(Protected) p.CurArmor.Flags.Set(Protected)
g.msg("your armor is covered by a shimmering %s shield", g.msg("your armor is covered by a shimmering %s shield",
@@ -277,12 +363,6 @@ func (g *RogueGame) readScroll() {
} else { } else {
g.msg("you feel a strange sense of loss") g.msg("you feel a strange sense of loss")
} }
}
g.look(true) // put the result of the scroll on the screen
g.status()
g.callIt(&g.Items.Scrolls[obj.Which])
} }
// uncurse uncurses an item (scrolls.c uncurse). // uncurse uncurses an item (scrolls.c uncurse).

98
game/seedcompat_test.go Normal file
View File

@@ -0,0 +1,98 @@
package game
import (
"fmt"
"os"
"strings"
"testing"
)
// dumpItemTables formats a game's per-seed item appearance tables in the
// same layout the instrumented C reference prints: the potion colors,
// scroll names, ring stones, and wand/staff materials, each generated by
// consuming the RNG in a fixed order during New().
func dumpItemTables(seed int32, g *RogueGame) string {
var b strings.Builder
fmt.Fprintf(&b, "SEED %d\n", seed)
fmt.Fprintln(&b, "POTIONS")
for _, c := range g.Items.PotColors {
fmt.Fprintln(&b, c)
}
fmt.Fprintln(&b, "SCROLLS")
for _, s := range g.Items.ScrNames {
fmt.Fprintln(&b, s)
}
fmt.Fprintln(&b, "RINGS")
for _, s := range g.Items.RingStones {
fmt.Fprintln(&b, s)
}
fmt.Fprintln(&b, "STICKS")
for i := range g.Items.WandType {
fmt.Fprintf(&b, "%s %s\n", g.Items.WandType[i], g.Items.WandMade[i])
}
return b.String()
}
// TestSeedCompatItemTables proves the port's seed-compatibility claim: for
// the same seed, the Go game generates the exact per-seed item appearance
// tables as the C reference on modern-rogue. That requires the LCG and its
// consumption order through the whole init sequence (init_probs →
// init_player → init_names → init_colors → init_stones → init_materials) to
// match C byte for byte. The golden is captured from an instrumented build
// of the C game (testdata/README.md).
func TestSeedCompatItemTables(t *testing.T) {
golden, err := os.ReadFile("testdata/item_tables.golden")
if err != nil {
t.Fatalf("read golden: %v", err)
}
// These must match the seeds the golden was generated from
// (testdata/README.md).
seeds := []int32{1, 42, 12345, 99999}
var got strings.Builder
for _, seed := range seeds {
g := New(Params{Seed: seed, Wizard: true})
got.WriteString(dumpItemTables(seed, g))
}
if got.String() != string(golden) {
t.Errorf("Go item tables diverge from the C reference at %s",
firstDiff(string(golden), got.String()))
}
}
// firstDiff returns a description of the first line where want and got
// differ, for a readable failure.
func firstDiff(want, got string) string {
wl := strings.Split(want, "\n")
gl := strings.Split(got, "\n")
for i := 0; i < len(wl) || i < len(gl); i++ {
w, g := "", ""
if i < len(wl) {
w = wl[i]
}
if i < len(gl) {
g = gl[i]
}
if w != g {
return fmt.Sprintf("line %d: C=%q Go=%q", i+1, w, g)
}
}
return "no line difference (trailing content?)"
}

View File

@@ -4,12 +4,16 @@ import "fmt"
// sticks.c — zap wands and staffs. // sticks.c — zap wands and staffs.
// The two ws_type strings a stick can be made as.
const (
wandName = "wand"
staffName = "staff"
)
// doZap performs a zap with a wand (sticks.c do_zap). // doZap performs a zap with a wand (sticks.c do_zap).
func (g *RogueGame) doZap() { func (g *RogueGame) doZap() {
p := &g.Player obj, ok := g.promptPackItem("zap with", KindWand)
if !ok {
obj := g.getItem("zap with", KindWand)
if obj == nil {
return return
} }
@@ -26,9 +30,51 @@ func (g *RogueGame) doZap() {
return return
} }
switch obj.WandKind() { if h := g.data.zapHandlers[obj.WandKind()]; h != nil {
case WandLight: if !h(g, obj) {
return // the zap aborted; no charge is used
}
}
obj.Charges--
}
// zapRayMonster walks the zap ray from the hero to the first blocking
// spot and returns the monster standing there, if any (the shared
// preamble of the C monster-affecting zap cases).
func (g *RogueGame) zapRayMonster() *Monster {
p := &g.Player
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
}
return g.Level.MonsterAt(y, x)
}
// zapVictim is zapRayMonster plus the flytrap release the C code does
// before the invisibility-family effects.
func (g *RogueGame) zapVictim() *Monster {
tp := g.zapRayMonster()
if tp != nil && tp.Type == 'F' {
g.Player.Flags.Clear(Held)
}
return tp
}
// The per-wand effect handlers, dispatched through gameData.zapHandlers.
// Each is one case of the C do_zap switch; returning false aborts the
// zap without using a charge.
func (g *RogueGame) zapLight(*Object) bool {
// Reddy Kilowatt wand. Light up the room // Reddy Kilowatt wand. Light up the room
p := &g.Player
g.Items.Sticks[WandLight].Know = true g.Items.Sticks[WandLight].Know = true
if p.Room.Flags.Has(Gone) { if p.Room.Flags.Has(Gone) {
g.msg("the corridor glows and then fades") g.msg("the corridor glows and then fades")
@@ -44,42 +90,47 @@ func (g *RogueGame) doZap() {
g.endmsg() g.endmsg()
} }
case WandDrainLife:
return true
}
func (g *RogueGame) zapDrainLife(*Object) bool {
// take away 1/2 of hero's hit points, then take it away evenly // take away 1/2 of hero's hit points, then take it away evenly
// from the monsters in the room (or next to hero if he is in a // from the monsters in the room (or next to hero if he is in a
// passage) // passage)
if p.Stats.HP < 2 { if g.Player.Stats.HP < 2 {
g.msg("you are too weak to use it") g.msg("you are too weak to use it")
return return false
} }
g.drain() g.drain()
case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation:
y := p.Pos.Y
x := p.Pos.X return true
for stepOk(g.Level.VisibleChar(y, x)) { }
y += g.Delta.Y
x += g.Delta.X
}
if tp := g.Level.MonsterAt(y, x); tp != nil { func (g *RogueGame) zapInvisibility(*Object) bool {
monster := tp.Type if tp := g.zapVictim(); tp != nil {
if monster == 'F' {
p.Flags.Clear(Held)
}
switch obj.WandKind() {
case WandInvisibility:
tp.Flags.Set(Invisible) tp.Flags.Set(Invisible)
if g.cansee(y, x) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(y, x, tp.OldCh) g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.OldCh)
} }
case WandPolymorph: }
return true
}
func (g *RogueGame) zapPolymorph(*Object) bool {
tp := g.zapVictim()
if tp == nil {
return true
}
y, x := tp.Pos.Y, tp.Pos.X
pp := tp.Pack pp := tp.Pack
detachMon(&g.Level.Monsters, tp) g.Level.RemoveMonster(tp)
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.mvaddch(y, x, g.Level.Char(y, x)) g.mvaddch(y, x, g.Level.Char(y, x))
@@ -88,7 +139,7 @@ func (g *RogueGame) doZap() {
oldch := tp.OldCh oldch := tp.OldCh
g.Delta.Y = y g.Delta.Y = y
g.Delta.X = x g.Delta.X = x
monster = g.randomMonsterLetter() monster := g.randomMonsterLetter()
g.newMonster(tp, monster, g.Delta) g.newMonster(tp, monster, g.Delta)
if g.seeMonst(tp) { if g.seeMonst(tp) {
@@ -101,15 +152,32 @@ func (g *RogueGame) doZap() {
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.Items.Sticks[WandPolymorph].Know = true g.Items.Sticks[WandPolymorph].Know = true
} }
case WandCancellation:
return true
}
func (g *RogueGame) zapCancellation(*Object) bool {
if tp := g.zapVictim(); tp != nil {
tp.Flags.Set(Cancelled) tp.Flags.Set(Cancelled)
tp.Flags.Clear(Invisible | CanConfuse) tp.Flags.Clear(Invisible | CanConfuse)
tp.Disguise = tp.Type tp.Disguise = tp.Type
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.mvaddch(y, x, tp.Disguise) g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Disguise)
} }
case WandTeleportAway, WandTeleportTo: }
return true
}
func (g *RogueGame) zapTeleport(obj *Object) bool {
p := &g.Player
tp := g.zapVictim()
if tp == nil {
return true
}
var newPos Coord var newPos Coord
if obj.WandKind() == WandTeleportAway { if obj.WandKind() == WandTeleportAway {
@@ -127,9 +195,13 @@ func (g *RogueGame) doZap() {
tp.Dest = &p.Pos tp.Dest = &p.Pos
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
g.relocate(tp, newPos) g.relocate(tp, newPos)
}
} return true
case WandMagicMissile: }
func (g *RogueGame) zapMagicMissile(*Object) bool {
p := &g.Player
g.Items.Sticks[WandMagicMissile].Know = true g.Items.Sticks[WandMagicMissile].Know = true
bolt := newObject() bolt := newObject()
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
@@ -152,23 +224,42 @@ func (g *RogueGame) doZap() {
} else { } else {
g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling
} }
case WandHasteMonster, WandSlowMonster:
y := p.Pos.Y
x := p.Pos.X return true
for stepOk(g.Level.VisibleChar(y, x)) { }
y += g.Delta.Y
x += g.Delta.X func (g *RogueGame) zapSpeed(obj *Object) bool {
tp := g.zapRayMonster()
if tp == nil {
return true
} }
if tp := g.Level.MonsterAt(y, x); tp != nil {
if obj.WandKind() == WandHasteMonster { if obj.WandKind() == WandHasteMonster {
hasteTarget(tp)
} else {
slowTarget(tp)
}
g.Delta.Y = tp.Pos.Y
g.Delta.X = tp.Pos.X
g.runTo(g.Delta)
return true
}
// hasteTarget cancels a slow or applies a haste (the WS_HASTE_M arm of
// do_zap).
func hasteTarget(tp *Monster) {
if tp.On(Slowed) { if tp.On(Slowed) {
tp.Flags.Clear(Slowed) tp.Flags.Clear(Slowed)
} else { } else {
tp.Flags.Set(Hasted) tp.Flags.Set(Hasted)
} }
} else { }
// slowTarget cancels a haste or applies a slow (the WS_SLOW_M arm of
// do_zap).
func slowTarget(tp *Monster) {
if tp.On(Hasted) { if tp.On(Hasted) {
tp.Flags.Clear(Hasted) tp.Flags.Clear(Hasted)
} else { } else {
@@ -176,13 +267,9 @@ func (g *RogueGame) doZap() {
} }
tp.Turn = true tp.Turn = true
} }
g.Delta.Y = y func (g *RogueGame) zapBolt(obj *Object) bool {
g.Delta.X = x
g.runto(g.Delta)
}
case WandLightning, WandFire, WandCold:
var name string var name string
switch obj.WandKind() { switch obj.WandKind() {
@@ -194,12 +281,10 @@ func (g *RogueGame) doZap() {
name = "ice" name = "ice"
} }
g.fireBolt(p.Pos, &g.Delta, name) g.fireBolt(g.Player.Pos, &g.Delta, name)
g.Items.Sticks[obj.Which].Know = true g.Items.Sticks[obj.Which].Know = true
case WandNothing:
}
obj.Charges-- return true
} }
// drain does the drain-hit-points-from-player schtick (sticks.c drain). // drain does the drain-hit-points-from-player schtick (sticks.c drain).
@@ -216,9 +301,7 @@ func (g *RogueGame) drain() {
var drainee []*Monster var drainee []*Monster
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
if mp.Room == p.Room || mp.Room == corp || if g.drainReaches(mp, corp, inpass) {
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPassNum] == p.Room) {
drainee = append(drainee, mp) drainee = append(drainee, mp)
} }
} }
@@ -237,11 +320,23 @@ func (g *RogueGame) drain() {
if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 { if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 {
g.killed(mp, g.seeMonst(mp)) g.killed(mp, g.seeMonst(mp))
} else { } else {
g.runto(mp.Pos) g.runTo(mp.Pos)
} }
} }
} }
// drainReaches reports whether the drain-life wand reaches this monster:
// the hero's room, the passage behind the door he stands on, or — when
// he is in a passage — a door of that same passage (the drainee
// condition of sticks.c drain).
func (g *RogueGame) drainReaches(mp *Monster, corp *Room, inpass bool) bool {
p := &g.Player
return mp.Room == p.Room || mp.Room == corp ||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPassNum] == p.Room)
}
// fireBolt fires a bolt in a given direction from a specific starting // fireBolt fires a bolt in a given direction from a specific starting
// place (sticks.c fire_bolt). // place (sticks.c fire_bolt).
func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) { func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
@@ -256,21 +351,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
bolt.DPlus = 0 bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name g.Items.Weapons[WeaponFlame].Name = name
var dirch byte dirch := boltDirChar(*dir)
switch dir.Y + dir.X {
case 0:
dirch = '/'
case 1, -1:
if dir.Y == 0 {
dirch = '-'
} else {
dirch = '|'
}
case 2, -2:
dirch = '\\'
}
pos := start pos := start
hitHero := !fromHero hitHero := !fromHero
used := false used := false
@@ -281,22 +362,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
pos.Y += dir.Y pos.Y += dir.Y
pos.X += dir.X pos.X += dir.X
spotpos = append(spotpos, pos) spotpos = append(spotpos, pos)
ch := g.Level.VisibleChar(pos.Y, pos.X) ch := g.Level.VisibleChar(pos.Y, pos.X)
bounce := false if boltBounces(ch, p.Pos, pos) {
switch ch {
case Door:
// this code is necessary if the hero is on a door and he
// fires at the wall the door is in, it would otherwise loop
// infinitely
if p.Pos != pos {
bounce = true
}
case '|', '-', ' ':
bounce = true
}
if bounce {
if !changed { if !changed {
hitHero = !hitHero hitHero = !hitHero
} }
@@ -314,11 +382,62 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil { if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
hitHero = true hitHero = true
changed = !changed changed = !changed
used = g.boltStrikesMonster(tp, bolt, pos, ch, name, fromHero)
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
used = g.boltStrikesHero(start, name, fromHero)
}
g.mvaddch(pos.Y, pos.X, dirch)
g.refresh()
}
// erase the bolt trail
for _, c2 := range spotpos {
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
}
}
// boltDirChar picks the character a traveling bolt is drawn with for its
// direction (the dirch switch of sticks.c fire_bolt).
func boltDirChar(dir Coord) byte {
switch dir.Y + dir.X {
case 0:
return '/'
case 1, -1:
if dir.Y == 0 {
return '-'
}
return '|'
case 2, -2:
return '\\'
}
return 0 // unreachable for the eight legal directions, as in C
}
// boltBounces reports whether a bolt bounces off this spot: walls, and
// any door except the one the hero stands on (which would otherwise loop
// infinitely, per the C comment in fire_bolt).
func boltBounces(ch byte, heroPos, pos Coord) bool {
switch ch {
case Door:
return heroPos != pos
case '|', '-', ' ':
return true
}
return false
}
// boltStrikesMonster resolves a bolt arriving on a monster's square (the
// monster arm of the fire_bolt loop). It reports whether the bolt was
// used up.
func (g *RogueGame) boltStrikesMonster(tp *Monster, bolt *Object, pos Coord, ch byte, name string, fromHero bool) bool {
tp.OldCh = g.Level.Char(pos.Y, pos.X) tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) { if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos bolt.Pos = pos
used = true
if tp.Type == 'D' && name == "flame" { if tp.Type == 'D' && name == "flame" {
g.addmsgf("the flame bounces") g.addmsgf("the flame bounces")
@@ -331,9 +450,13 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
} else { } else {
g.hitMonster(pos, bolt) g.hitMonster(pos, bolt)
} }
} else if ch != 'M' || tp.Disguise == 'M' {
return true
}
if ch != 'M' || tp.Disguise == 'M' {
if fromHero { if fromHero {
g.runto(pos) g.runTo(pos)
} }
if g.Options.Terse { if g.Options.Terse {
@@ -342,11 +465,20 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
g.msg("the %s whizzes past %s", name, g.setMname(tp)) g.msg("the %s whizzes past %s", name, g.setMname(tp))
} }
} }
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
if !g.save(VsMagic) { return false
}
// boltStrikesHero resolves a bolt arriving on the hero (the hero arm of
// the fire_bolt loop). It reports whether the bolt was used up.
func (g *RogueGame) boltStrikesHero(start Coord, name string, fromHero bool) bool {
p := &g.Player
if g.save(VsMagic) {
g.msg("the %s whizzes by you", name)
return false
}
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 { if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
if fromHero { if fromHero {
g.death('b') g.death('b')
@@ -355,30 +487,18 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
} }
} }
used = true
if g.Options.Terse { if g.Options.Terse {
g.msg("the %s hits", name) g.msg("the %s hits", name)
} else { } else {
g.msg("you are hit by the %s", name) g.msg("you are hit by the %s", name)
} }
} else {
g.msg("the %s whizzes by you", name)
}
}
g.mvaddch(pos.Y, pos.X, dirch) return true
g.refresh()
}
// erase the bolt trail
for _, c2 := range spotpos {
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
}
} }
// fixStick sets up a new wand or staff (sticks.c fix_stick). // fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) { func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" { if g.Items.WandType[cur.Which] == staffName {
cur.Damage = dice("2x3") cur.Damage = dice("2x3")
} else { } else {
cur.Damage = dice("1x1") cur.Damage = dice("1x1")

View File

@@ -1,15 +1,174 @@
package game package game
// This file is the immutable data from extern.c and init.c: tables that are // tables.go — the static data tables the C game kept as file-scope globals
// never written after program start. Per-game mutable copies (the ObjInfo // (extern.c, init.c, and assorted per-file statics). The port gathers them
// tables, whose probabilities are re-summed and whose Know/Guess fields // into gameData: every RogueGame carries its own copy, so the package holds
// change during play) are cloned into RogueGame.Items by NewGame. // no package-level state. Per-game mutable copies (the ObjInfo tables,
// whose probabilities are re-summed and whose Know/Guess fields change
// during play) are cloned into RogueGame.Items by NewGame.
// initStats is the C INIT_STATS: the player's starting statistics. // helpEntry is rogue.h struct h_list.
var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12} type helpEntry struct {
Ch byte
Desc string
Print bool
}
// aClass is extern.c a_class[]: armor class for each armor type. // gameData is the bundle of static tables, built by newGameData and hung on
var aClass = [NumArmorTypes]int{ // RogueGame as g.data. Nothing in it mutates during play: the one table the
// C code writes to (the venus flytrap damage hack) operates on the game's
// Monsters copy, not on this template.
type gameData struct {
// initStats is the C INIT_STATS: the player's starting statistics.
initStats Stats
// aClass is extern.c a_class[]: armor class for each armor type.
aClass [NumArmorTypes]int
// eLevels is extern.c e_levels[]: experience thresholds per level; the
// zero terminates the table as in C.
eLevels []int
// trName is extern.c tr_name[]: names of the traps.
trName [NumTrapTypes]string
// invTName is extern.c inv_t_name[]: the inventory style names.
invTName []string
// monsterTable is extern.c monsters[26]: all monster kinds, indexed by
// letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C)
// is rolled from the level at creation time.
monsterTable [26]MonsterKind
// Base ObjInfo tables (extern.c). These are templates: NewGame copies
// them into ItemLore before initProbs converts Prob to cumulative form.
baseThings [NumThings]ObjInfo
baseArmInfo [NumArmorTypes]ObjInfo
basePotInfo [NumPotionTypes]ObjInfo
baseRingInfo [NumRingTypes]ObjInfo
baseScrInfo [NumScrollTypes]ObjInfo
baseWeapInfo [NumWeaponTypes + 1]ObjInfo
baseWsInfo [NumWandTypes]ObjInfo
// rainbow is init.c rainbow[]: the possible potion colors.
rainbow []string
// sylls is init.c sylls[]: syllables for generated scroll names.
sylls []string
// stoneTable is init.c stones[]: ring stones and their worth.
stoneTable []Stone
// woods is init.c wood[]: what staffs are made of.
woods []string
// metals is init.c metal[]: what wands are made of.
metals []string
// helpStr is extern.c helpstr[]: the '?' command help text.
helpStr []helpEntry
// hNames are the strings for hitting; the first four are used when the
// player strikes, the second four for monsters (fight.c h_names).
hNames [8]string
// mNames are the strings for missing (fight.c m_names).
mNames [8]string
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
strPlus [32]int
// addDam adjusts damage done due to strength (fight.c add_dam).
addDam [32]int
// lvlMons and wandMons list monsters in rough order of vorpalness;
// zero entries in wandMons never wander (monsters.c).
lvlMons [26]byte
wandMons [26]byte
// ringUses is the rings.c ring_eat static uses[] table: how much food
// each ring type uses up per turn (negative = a 1-in-n chance of 1).
ringUses [NumRingTypes]int
// initWeaps is the weapons.c init_dam[] table.
initWeaps [NumWeaponTypes]weaponSetup
// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic
// (it names the fruit) and is computed in applyPotionFuse.
pActions [NumPotionTypes]pact
// idType maps identify scrolls to the kind of item they identify
// (scrolls.c static id_type).
idType [ScrollIdentifyRingOrStick + 1]ObjectKind
// rdesConn is the hardcoded 3x3 room adjacency matrix from
// passages.c do_passages.
rdesConn [MaxRooms][MaxRooms]bool
// thingList is misc.c rnd_thing()'s static table.
thingList []byte
// identList is command.c's static ident_list.
identList []helpEntry
// hungerStateName is io.c state_name[].
hungerStateName [4]string
// ripArt is the rip.c rip[] tombstone art.
ripArt []string
// killnameTable is the rip.c nlist[]: special death causes.
killnameTable []helpEntry
// scoreReasons is the rip.c reason[] scoreboard strings.
scoreReasons [4]string
// quaffHandlers dispatches each potion kind to its effect method:
// the cases of the potions.c quaff switch. The bool is trip — was
// the hero hallucinating when the potion went down.
quaffHandlers [NumPotionTypes]func(g *RogueGame, trip bool)
// readHandlers dispatches each scroll kind to its effect method:
// the cases of the scrolls.c read_scroll switch. The handler gets
// the scroll being read (the identify family needs its subtype).
readHandlers [NumScrollTypes]func(g *RogueGame, obj *Object)
// zapHandlers dispatches each wand kind to its effect method: the
// cases of the sticks.c do_zap switch. A false return aborts the
// zap without using a charge (drain life on a too-weak hero).
zapHandlers [NumWandTypes]func(g *RogueGame, obj *Object) bool
// hitHandlers dispatches a monster's special power when its hit
// lands, indexed by monster letter - 'A': the cases of the fight.c
// attack switch. A true return means the monster removed itself.
hitHandlers [26]func(g *RogueGame, mp *Monster, mname string) bool
// commandHandlers dispatches the ordinary command keys: the simple
// cases of the big command.c switch. Keys that re-dispatch (runs,
// fight, repeat, move-on) and wizard keys stay in dispatchKey.
commandHandlers map[byte]func(g *RogueGame)
// trapHandlers dispatches each sprung trap to its effect method:
// the cases of the move.c be_trapped switch.
trapHandlers [NumTrapTypes]func(g *RogueGame, tc Coord)
// daemonHandlers dispatches DaemonIDs to their callbacks: the C
// d_func function pointers (daemons.c / daemon.c).
daemonHandlers [DTurnSee + 1]func(g *RogueGame, arg int)
}
// ripWall is the repeated blank wall line of the tombstone art.
const ripWall = " | |"
// newGameData builds the static tables. Each game gets a fresh copy, which
// keeps the package free of globals.
//
//nolint:funlen,maintidx // a single composite literal holding every C data table
func newGameData() *gameData {
return &gameData{
initStats: Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12},
aClass: [NumArmorTypes]int{
8, // LEATHER 8, // LEATHER
7, // RING_MAIL 7, // RING_MAIL
7, // STUDDED_LEATHER 7, // STUDDED_LEATHER
@@ -18,17 +177,14 @@ var aClass = [NumArmorTypes]int{
4, // SPLINT_MAIL 4, // SPLINT_MAIL
4, // BANDED_MAIL 4, // BANDED_MAIL
3, // PLATE_MAIL 3, // PLATE_MAIL
} },
// eLevels is extern.c e_levels[]: experience thresholds per level; the eLevels: []int{
// zero terminates the table as in C.
var eLevels = []int{
10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000, 10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000,
50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0, 50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0,
} },
// trName is extern.c tr_name[]: names of the traps. trName: [NumTrapTypes]string{
var trName = [NumTrapTypes]string{
"a trapdoor", "a trapdoor",
"an arrow trap", "an arrow trap",
"a sleeping gas trap", "a sleeping gas trap",
@@ -37,15 +193,11 @@ var trName = [NumTrapTypes]string{
"a poison dart trap", "a poison dart trap",
"a rust trap", "a rust trap",
"a mysterious trap", "a mysterious trap",
} },
// invTName is extern.c inv_t_name[]: the inventory style names. invTName: []string{"Overwrite", "Slow", "Clear"},
var invTName = []string{"Overwrite", "Slow", "Clear"}
// monsterTable is extern.c monsters[26]: all monster kinds, indexed by monsterTable: [26]MonsterKind{
// letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C) is
// rolled from the level at creation time.
var monsterTable = [26]MonsterKind{
/* Name CARRY FLAGS str exp lvl arm hp dmg */ /* Name CARRY FLAGS str exp lvl arm hp dmg */
{"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}}, {"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}},
{"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}}, {"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}},
@@ -73,12 +225,9 @@ var monsterTable = [26]MonsterKind{
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}}, {"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}}, {"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}}, {"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
} },
// Base ObjInfo tables (extern.c). These are templates: NewGame copies them baseThings: [NumThings]ObjInfo{
// into ItemLore before initProbs converts Prob to cumulative form.
var baseThings = [NumThings]ObjInfo{
{Prob: 26}, // potion {Prob: 26}, // potion
{Prob: 36}, // scroll {Prob: 36}, // scroll
{Prob: 16}, // food {Prob: 16}, // food
@@ -86,9 +235,9 @@ var baseThings = [NumThings]ObjInfo{
{Prob: 7}, // armor {Prob: 7}, // armor
{Prob: 4}, // ring {Prob: 4}, // ring
{Prob: 4}, // stick {Prob: 4}, // stick
} },
var baseArmInfo = [NumArmorTypes]ObjInfo{ baseArmInfo: [NumArmorTypes]ObjInfo{
{Name: "leather armor", Prob: 20, Worth: 20}, {Name: "leather armor", Prob: 20, Worth: 20},
{Name: "ring mail", Prob: 15, Worth: 25}, {Name: "ring mail", Prob: 15, Worth: 25},
{Name: "studded leather armor", Prob: 15, Worth: 20}, {Name: "studded leather armor", Prob: 15, Worth: 20},
@@ -97,9 +246,9 @@ var baseArmInfo = [NumArmorTypes]ObjInfo{
{Name: "splint mail", Prob: 10, Worth: 80}, {Name: "splint mail", Prob: 10, Worth: 80},
{Name: "banded mail", Prob: 10, Worth: 90}, {Name: "banded mail", Prob: 10, Worth: 90},
{Name: "plate mail", Prob: 5, Worth: 150}, {Name: "plate mail", Prob: 5, Worth: 150},
} },
var basePotInfo = [NumPotionTypes]ObjInfo{ basePotInfo: [NumPotionTypes]ObjInfo{
{Name: "confusion", Prob: 7, Worth: 5}, {Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", Prob: 8, Worth: 5}, {Name: "hallucination", Prob: 8, Worth: 5},
{Name: "poison", Prob: 8, Worth: 5}, {Name: "poison", Prob: 8, Worth: 5},
@@ -114,9 +263,9 @@ var basePotInfo = [NumPotionTypes]ObjInfo{
{Name: "restore strength", Prob: 13, Worth: 130}, {Name: "restore strength", Prob: 13, Worth: 130},
{Name: "blindness", Prob: 5, Worth: 5}, {Name: "blindness", Prob: 5, Worth: 5},
{Name: "levitation", Prob: 6, Worth: 75}, {Name: "levitation", Prob: 6, Worth: 75},
} },
var baseRingInfo = [NumRingTypes]ObjInfo{ baseRingInfo: [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400}, {Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", Prob: 9, Worth: 400}, {Name: "add strength", Prob: 9, Worth: 400},
{Name: "sustain strength", Prob: 5, Worth: 280}, {Name: "sustain strength", Prob: 5, Worth: 280},
@@ -131,9 +280,9 @@ var baseRingInfo = [NumRingTypes]ObjInfo{
{Name: "teleportation", Prob: 5, Worth: 30}, {Name: "teleportation", Prob: 5, Worth: 30},
{Name: "stealth", Prob: 7, Worth: 470}, {Name: "stealth", Prob: 7, Worth: 470},
{Name: "maintain armor", Prob: 5, Worth: 380}, {Name: "maintain armor", Prob: 5, Worth: 380},
} },
var baseScrInfo = [NumScrollTypes]ObjInfo{ baseScrInfo: [NumScrollTypes]ObjInfo{
{Name: "monster confusion", Prob: 7, Worth: 140}, {Name: "monster confusion", Prob: 7, Worth: 140},
{Name: "magic mapping", Prob: 4, Worth: 150}, {Name: "magic mapping", Prob: 4, Worth: 150},
{Name: "hold monster", Prob: 2, Worth: 180}, {Name: "hold monster", Prob: 2, Worth: 180},
@@ -152,9 +301,9 @@ var baseScrInfo = [NumScrollTypes]ObjInfo{
{Name: "remove curse", Prob: 7, Worth: 105}, {Name: "remove curse", Prob: 7, Worth: 105},
{Name: "aggravate monsters", Prob: 3, Worth: 20}, {Name: "aggravate monsters", Prob: 3, Worth: 20},
{Name: "protect armor", Prob: 2, Worth: 250}, {Name: "protect armor", Prob: 2, Worth: 250},
} },
var baseWeapInfo = [NumWeaponTypes + 1]ObjInfo{ baseWeapInfo: [NumWeaponTypes + 1]ObjInfo{
{Name: "mace", Prob: 11, Worth: 8}, {Name: "mace", Prob: 11, Worth: 8},
{Name: "long sword", Prob: 11, Worth: 15}, {Name: "long sword", Prob: 11, Worth: 15},
{Name: "short bow", Prob: 12, Worth: 15}, {Name: "short bow", Prob: 12, Worth: 15},
@@ -165,9 +314,9 @@ var baseWeapInfo = [NumWeaponTypes + 1]ObjInfo{
{Name: "shuriken", Prob: 12, Worth: 5}, {Name: "shuriken", Prob: 12, Worth: 5},
{Name: "spear", Prob: 12, Worth: 5}, {Name: "spear", Prob: 12, Worth: 5},
{}, // DO NOT REMOVE: fake entry for dragon's breath {}, // DO NOT REMOVE: fake entry for dragon's breath
} },
var baseWsInfo = [NumWandTypes]ObjInfo{ baseWsInfo: [NumWandTypes]ObjInfo{
{Name: "light", Prob: 12, Worth: 250}, {Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5}, {Name: "invisibility", Prob: 6, Worth: 5},
{Name: "lightning", Prob: 3, Worth: 330}, {Name: "lightning", Prob: 3, Worth: 330},
@@ -182,18 +331,17 @@ var baseWsInfo = [NumWandTypes]ObjInfo{
{Name: "teleport away", Prob: 6, Worth: 340}, {Name: "teleport away", Prob: 6, Worth: 340},
{Name: "teleport to", Prob: 6, Worth: 50}, {Name: "teleport to", Prob: 6, Worth: 50},
{Name: "cancellation", Prob: 5, Worth: 280}, {Name: "cancellation", Prob: 5, Worth: 280},
} },
// rainbow is init.c rainbow[]: the possible potion colors. rainbow: []string{
var rainbow = []string{
"amber", "aquamarine", "black", "blue", "brown", "clear", "crimson", "amber", "aquamarine", "black", "blue", "brown", "clear", "crimson",
"cyan", "ecru", "gold", "green", "grey", "magenta", "orange", "pink", "cyan", "ecru", "gold", "green", "grey", "magenta", "orange", "pink",
"plaid", "purple", "red", "silver", "tan", "tangerine", "topaz", "plaid", "purple", "red", "silver", "tan", "tangerine", "topaz",
"turquoise", "vermilion", "violet", "white", "yellow", "turquoise", "vermilion", "violet", "white", "yellow",
} },
// sylls is init.c sylls[]: syllables for generated scroll names. //nolint:misspell // "ther" is a C scroll syllable, kept faithfully
var sylls = []string{ sylls: []string{
"a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash", "a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash",
"bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp", "bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp",
"con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el", "con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el",
@@ -206,14 +354,13 @@ var sylls = []string{
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa", "prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik", "san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem", "sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
"there", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un", "ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg", "uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim", "wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
"zok", "zon", "zum", "zok", "zon", "zum",
} },
// stoneTable is init.c stones[]: ring stones and their worth. stoneTable: []Stone{
var stoneTable = []Stone{
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50}, {"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300}, {"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
{"germanium", 225}, {"granite", 5}, {"garnet", 50}, {"germanium", 225}, {"granite", 5}, {"garnet", 50},
@@ -223,35 +370,25 @@ var stoneTable = []Stone{
{"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200}, {"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200},
{"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70}, {"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70},
{"taaffeite", 300}, {"zircon", 80}, {"taaffeite", 300}, {"zircon", 80},
} },
// woods is init.c wood[]: what staffs are made of. woods: []string{
var woods = []string{
"avocado wood", "balsa", "bamboo", "banyan", "birch", "cedar", "avocado wood", "balsa", "bamboo", "banyan", "birch", "cedar",
"cherry", "cinnibar", "cypress", "dogwood", "driftwood", "ebony", "cherry", "cinnibar", "cypress", "dogwood", "driftwood", "ebony",
"elm", "eucalyptus", "fall", "hemlock", "holly", "ironwood", "elm", "eucalyptus", "fall", "hemlock", "holly", "ironwood",
"kukui wood", "mahogany", "manzanita", "maple", "oaken", "kukui wood", "mahogany", "manzanita", "maple", "oaken",
"persimmon wood", "pecan", "pine", "poplar", "redwood", "rosewood", "persimmon wood", "pecan", "pine", "poplar", "redwood", "rosewood",
"spruce", "teak", "walnut", "zebrawood", "spruce", "teak", "walnut", "zebrawood",
} },
// metals is init.c metal[]: what wands are made of. metals: []string{
var metals = []string{
"aluminum", "beryllium", "bone", "brass", "bronze", "copper", "aluminum", "beryllium", "bone", "brass", "bronze", "copper",
"electrum", "gold", "iron", "lead", "magnesium", "mercury", "electrum", "gold", "iron", "lead", "magnesium", "mercury",
"nickel", "pewter", "platinum", "steel", "silver", "silicon", "nickel", "pewter", "platinum", "steel", "silver", "silicon",
"tin", "titanium", "tungsten", "zinc", "tin", "titanium", "tungsten", "zinc",
} },
// helpEntry is rogue.h struct h_list. helpStr: []helpEntry{
type helpEntry struct {
Ch byte
Desc string
Print bool
}
// helpStr is extern.c helpstr[]: the '?' command help text.
var helpStr = []helpEntry{
{'?', " prints help", true}, {'?', " prints help", true},
{'/', " identify object", true}, {'/', " identify object", true},
{'h', " left", true}, {'h', " left", true},
@@ -317,6 +454,401 @@ var helpStr = []helpEntry{
{'!', " shell escape", true}, {'!', " shell escape", true},
{'F', "<dir> fight till either of you dies", true}, {'F', "<dir> fight till either of you dies", true},
{'v', " print version number", true}, {'v', " print version number", true},
},
hNames: [8]string{
" scored an excellent hit on ",
" hit ",
" have injured ",
" swing and hit ",
" scored an excellent hit on ",
" hit ",
" has injured ",
" swings and hits ",
},
mNames: [8]string{
" miss",
" swing and miss",
" barely miss",
" don't hit",
" misses",
" swings and misses",
" barely misses",
" doesn't hit",
},
strPlus: [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
},
addDam: [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
},
lvlMons: [26]byte{
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
},
wandMons: [26]byte{
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
},
ringUses: [NumRingTypes]int{
1, // R_PROTECT
1, // R_ADDSTR
1, // R_SUSTSTR
-3, // R_SEARCH
-5, // R_SEEINVIS
0, // R_NOP
0, // R_AGGR
-3, // R_ADDHIT
-3, // R_ADDDAM
2, // R_REGEN
-2, // R_DIGEST
0, // R_TELEPORT
1, // R_STEALTH
1, // R_SUSTARM
},
initWeaps: [NumWeaponTypes]weaponSetup{
{dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
{dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
{dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
{dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
{dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
{dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
{dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
{dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
{dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
},
pActions: [NumPotionTypes]pact{
PotionConfusion: {Confused, DUnconfuse, HuhDuration,
"what a tripy feeling!",
"wait, what's going on here. Huh? What? Who?"},
PotionLSD: {Hallucinating, DComeDown, SeeDuration,
"Oh, wow! Everything seems so cosmic!",
"Oh, wow! Everything seems so cosmic!"},
PotionSeeInvisible: {CanSeeInvisible, DUnsee, SeeDuration, "", ""},
PotionBlindness: {Blind, DSight, SeeDuration,
"oh, bummer! Everything is dark! Help!",
"a cloak of darkness falls around you"},
PotionLevitation: {Levitating, DLand, HealTime,
"oh, wow! You're floating in the air!",
"you start to float in the air"},
},
idType: [ScrollIdentifyRingOrStick + 1]ObjectKind{
ScrollIdentifyPotion: KindPotion,
ScrollIdentifyScroll: KindScroll,
ScrollIdentifyWeapon: KindWeapon,
ScrollIdentifyArmor: KindArmor,
ScrollIdentifyRingOrStick: KindRingOrStick,
},
rdesConn: [MaxRooms][MaxRooms]bool{
{false, true, false, true, false, false, false, false, false},
{true, false, true, false, true, false, false, false, false},
{false, true, false, false, false, true, false, false, false},
{true, false, false, false, true, false, true, false, false},
{false, true, false, true, false, true, false, true, false},
{false, false, true, false, true, false, false, false, true},
{false, false, false, true, false, false, false, true, false},
{false, false, false, false, true, false, true, false, true},
{false, false, false, false, false, true, false, true, false},
},
thingList: []byte{
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
},
identList: []helpEntry{
{'|', "wall of a room", false},
{'-', "wall of a room", false},
{Gold, goldName, false},
{Stairs, "a staircase", false},
{Door, "door", false},
{Floor, "room floor", false},
{PlayerCh, "you", false},
{Passage, "passage", false},
{Trap, "trap", false},
{Potion, potionName, false},
{Scroll, scrollName, false},
{Food, "food", false},
{Weapon, "weapon", false},
{' ', "solid rock", false},
{Armor, "armor", false},
{Amulet, "the Amulet of Yendor", false},
{Ring, ringName, false},
{Stick, "wand or staff", false},
},
hungerStateName: [4]string{"", "Hungry", "Weak", "Faint"},
ripArt: []string{
" __________",
" / \\",
" / REST \\",
" / IN \\",
" / PEACE \\",
" / \\",
ripWall,
ripWall,
" | killed by a |",
ripWall,
" | 1980 |",
" *| * * * | *",
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
},
killnameTable: []helpEntry{
{'a', "arrow", true},
{'b', "bolt", true},
{'d', "dart", true},
{'h', "hypothermia", false},
{'s', "starvation", false},
},
scoreReasons: [4]string{
"killed",
"quit",
"A total winner",
"killed with Amulet",
},
quaffHandlers: [NumPotionTypes]func(*RogueGame, bool){
PotionConfusion: (*RogueGame).quaffConfusion,
PotionLSD: (*RogueGame).quaffLSD,
PotionPoison: (*RogueGame).quaffPoison,
PotionGainStrength: (*RogueGame).quaffGainStrength,
PotionSeeInvisible: (*RogueGame).quaffSeeInvisible,
PotionHealing: (*RogueGame).quaffHealing,
PotionDetectMonsters: (*RogueGame).quaffDetectMonsters,
PotionDetectMagic: (*RogueGame).quaffDetectMagic,
PotionRaiseLevel: (*RogueGame).quaffRaiseLevel,
PotionExtraHealing: (*RogueGame).quaffExtraHealing,
PotionHaste: (*RogueGame).quaffHaste,
PotionRestoreStrength: (*RogueGame).quaffRestoreStrength,
PotionBlindness: (*RogueGame).quaffBlindness,
PotionLevitation: (*RogueGame).quaffLevitation,
},
readHandlers: [NumScrollTypes]func(*RogueGame, *Object){
ScrollMonsterConfusion: (*RogueGame).readMonsterConfusion,
ScrollMagicMapping: (*RogueGame).readMagicMapping,
ScrollHoldMonster: (*RogueGame).readHoldMonster,
ScrollSleep: (*RogueGame).readSleep,
ScrollEnchantArmor: (*RogueGame).readEnchantArmor,
ScrollIdentifyPotion: (*RogueGame).readIdentify,
ScrollIdentifyScroll: (*RogueGame).readIdentify,
ScrollIdentifyWeapon: (*RogueGame).readIdentify,
ScrollIdentifyArmor: (*RogueGame).readIdentify,
ScrollIdentifyRingOrStick: (*RogueGame).readIdentify,
ScrollScareMonster: (*RogueGame).readScareMonster,
ScrollFoodDetection: (*RogueGame).readFoodDetection,
ScrollTeleportation: (*RogueGame).readTeleportation,
ScrollEnchantWeapon: (*RogueGame).readEnchantWeapon,
ScrollCreateMonster: (*RogueGame).readCreateMonster,
ScrollRemoveCurse: (*RogueGame).readRemoveCurse,
ScrollAggravateMonsters: (*RogueGame).readAggravateMonsters,
ScrollProtectArmor: (*RogueGame).readProtectArmor,
},
zapHandlers: [NumWandTypes]func(*RogueGame, *Object) bool{
WandLight: (*RogueGame).zapLight,
WandInvisibility: (*RogueGame).zapInvisibility,
WandLightning: (*RogueGame).zapBolt,
WandFire: (*RogueGame).zapBolt,
WandCold: (*RogueGame).zapBolt,
WandPolymorph: (*RogueGame).zapPolymorph,
WandMagicMissile: (*RogueGame).zapMagicMissile,
WandHasteMonster: (*RogueGame).zapSpeed,
WandSlowMonster: (*RogueGame).zapSpeed,
WandDrainLife: (*RogueGame).zapDrainLife,
WandTeleportAway: (*RogueGame).zapTeleport,
WandTeleportTo: (*RogueGame).zapTeleport,
WandCancellation: (*RogueGame).zapCancellation,
},
hitHandlers: [26]func(*RogueGame, *Monster, string) bool{
0: (*RogueGame).hitAquator, // 'A' - 'A'
'F' - 'A': (*RogueGame).hitFlytrap,
'I' - 'A': (*RogueGame).hitIceMonster,
'L' - 'A': (*RogueGame).hitLeprechaun,
'N' - 'A': (*RogueGame).hitNymph,
'R' - 'A': (*RogueGame).hitRattlesnake,
'V' - 'A': (*RogueGame).hitLifeDrainer,
'W' - 'A': (*RogueGame).hitLifeDrainer,
},
commandHandlers: map[byte]func(*RogueGame){
',': (*RogueGame).pickupCommand,
'!': (*RogueGame).shell,
'h': func(g *RogueGame) { g.moveHero(0, -1) },
'j': func(g *RogueGame) { g.moveHero(1, 0) },
'k': func(g *RogueGame) { g.moveHero(-1, 0) },
'l': func(g *RogueGame) { g.moveHero(0, 1) },
'y': func(g *RogueGame) { g.moveHero(-1, -1) },
'u': func(g *RogueGame) { g.moveHero(-1, 1) },
'b': func(g *RogueGame) { g.moveHero(1, -1) },
'n': func(g *RogueGame) { g.moveHero(1, 1) },
'H': func(g *RogueGame) { g.startRun('h') },
'J': func(g *RogueGame) { g.startRun('j') },
'K': func(g *RogueGame) { g.startRun('k') },
'L': func(g *RogueGame) { g.startRun('l') },
'Y': func(g *RogueGame) { g.startRun('y') },
'U': func(g *RogueGame) { g.startRun('u') },
'B': func(g *RogueGame) { g.startRun('b') },
'N': func(g *RogueGame) { g.startRun('n') },
't': func(g *RogueGame) {
if !g.promptDirection() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
}
},
'q': (*RogueGame).quaff,
'Q': func(g *RogueGame) {
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
},
'i': func(g *RogueGame) {
g.After = false
g.inventory(g.Player.Pack, 0)
},
'I': func(g *RogueGame) {
g.After = false
g.pickyInven()
},
'd': (*RogueGame).dropIt,
'r': (*RogueGame).readScroll,
'e': (*RogueGame).eat,
'w': (*RogueGame).wield,
'W': (*RogueGame).wear,
'T': (*RogueGame).takeOff,
'P': (*RogueGame).ringOn,
'R': (*RogueGame).ringOff,
'o': func(g *RogueGame) {
g.option()
g.After = false
},
'c': func(g *RogueGame) {
g.call()
g.After = false
},
'>': func(g *RogueGame) {
g.After = false
g.dLevel()
},
'<': func(g *RogueGame) {
g.After = false
g.uLevel()
},
'?': func(g *RogueGame) {
g.After = false
g.help()
},
'/': func(g *RogueGame) {
g.After = false
g.identify()
},
's': (*RogueGame).search,
'z': func(g *RogueGame) {
if g.promptDirection() {
g.doZap()
} else {
g.After = false
}
},
'D': func(g *RogueGame) {
g.After = false
g.discovered()
},
CTRL('P'): func(g *RogueGame) {
g.After = false
g.msg("%s", g.Msgs.Huh)
},
CTRL('R'): func(g *RogueGame) {
g.After = false
g.refresh()
},
'v': func(g *RogueGame) {
g.After = false
g.msg("version %s. (mctesq was here)", Release)
},
'S': func(g *RogueGame) {
g.After = false
g.saveGame()
},
'.': func(*RogueGame) {
// rest command
},
' ': func(g *RogueGame) {
g.After = false // "legal" illegal command
},
'^': (*RogueGame).identifyTrapCommand,
Escape: func(g *RogueGame) {
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
},
')': func(g *RogueGame) {
g.current(g.Player.CurWeapon, "wielding", "")
},
']': func(g *RogueGame) {
g.current(g.Player.CurArmor, "wearing", "")
},
'=': func(g *RogueGame) {
g.current(g.Player.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(g.Player.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
},
'@': func(g *RogueGame) {
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
},
},
trapHandlers: [NumTrapTypes]func(*RogueGame, Coord){
TrapDoor: (*RogueGame).trapFall,
TrapArrow: (*RogueGame).trapArrow,
TrapSleep: (*RogueGame).trapSleep,
TrapBear: (*RogueGame).trapBear,
TrapTeleport: (*RogueGame).trapTeleport,
TrapDart: (*RogueGame).trapDart,
TrapRust: (*RogueGame).trapRust,
TrapMystery: (*RogueGame).trapMystery,
},
daemonHandlers: [DTurnSee + 1]func(*RogueGame, int){
DRollwand: (*RogueGame).rollwand,
DDoctor: (*RogueGame).doctor,
DStomach: (*RogueGame).stomach,
DRunners: (*RogueGame).runners,
DSwander: (*RogueGame).swander,
DNohaste: (*RogueGame).nohaste,
DUnconfuse: (*RogueGame).unconfuse,
DUnsee: (*RogueGame).unsee,
DSight: (*RogueGame).sight,
DVisuals: (*RogueGame).visuals,
DComeDown: (*RogueGame).comeDown,
DLand: (*RogueGame).land,
DTurnSee: func(g *RogueGame, arg int) {
g.turnSee(arg != 0)
},
},
}
} }
// Version strings (vers.c). The encstr/statlist XOR keys are not ported: // Version strings (vers.c). The encstr/statlist XOR keys are not ported:

View File

@@ -13,14 +13,16 @@ func TestProbabilitiesSumTo100(t *testing.T) {
return s return s
} }
data := newGameData()
tables := map[string][]ObjInfo{ tables := map[string][]ObjInfo{
"things": baseThings[:], "things": data.baseThings[:],
"potions": basePotInfo[:], "potions": data.basePotInfo[:],
"scrolls": baseScrInfo[:], "scrolls": data.baseScrInfo[:],
"rings": baseRingInfo[:], "rings": data.baseRingInfo[:],
"sticks": baseWsInfo[:], "sticks": data.baseWsInfo[:],
"weapons": baseWeapInfo[:NumWeaponTypes], // excludes the flame entry "weapons": data.baseWeapInfo[:NumWeaponTypes], // excludes the flame entry
"armor": baseArmInfo[:], "armor": data.baseArmInfo[:],
} }
for name, tab := range tables { for name, tab := range tables {
if s := sum(tab); s != 100 { if s := sum(tab); s != 100 {
@@ -30,7 +32,7 @@ func TestProbabilitiesSumTo100(t *testing.T) {
} }
func TestInitProbsCumulative(t *testing.T) { func TestInitProbsCumulative(t *testing.T) {
g := NewGame(Config{Seed: 1}) g := New(Params{Seed: 1})
last := g.Items.Potions[NumPotionTypes-1].Prob last := g.Items.Potions[NumPotionTypes-1].Prob
if last != 100 { if last != 100 {
@@ -45,7 +47,23 @@ func TestInitProbsCumulative(t *testing.T) {
} }
func TestNewGameRandomizesAppearances(t *testing.T) { func TestNewGameRandomizesAppearances(t *testing.T) {
g := NewGame(Config{Seed: 12345}) g := New(Params{Seed: 12345})
checkPotionColors(t, g)
checkScrollNames(t, g)
checkWandMaterials(t, g)
// Determinism: same seed, same appearances.
h := New(Params{Seed: 12345})
if h.Items != g.Items {
t.Error("two games with the same seed produced different item lore")
}
}
// checkPotionColors verifies every potion has a distinct color.
func checkPotionColors(t *testing.T, g *RogueGame) {
t.Helper()
seen := map[string]bool{} seen := map[string]bool{}
for i, c := range g.Items.PotColors { for i, c := range g.Items.PotColors {
@@ -59,6 +77,12 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
seen[c] = true seen[c] = true
} }
}
// checkScrollNames verifies every scroll has a name within the C buffer
// limit.
func checkScrollNames(t *testing.T, g *RogueGame) {
t.Helper()
for i, n := range g.Items.ScrNames { for i, n := range g.Items.ScrNames {
if n == "" { if n == "" {
@@ -69,9 +93,15 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
t.Errorf("scroll name %q longer than C buffer allows", n) t.Errorf("scroll name %q longer than C buffer allows", n)
} }
} }
}
// checkWandMaterials verifies every stick has a wand/staff type and a
// material.
func checkWandMaterials(t *testing.T, g *RogueGame) {
t.Helper()
for i := range g.Items.WandType { for i := range g.Items.WandType {
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" { if g.Items.WandType[i] != wandName && g.Items.WandType[i] != staffName {
t.Errorf("stick %d has type %q", i, g.Items.WandType[i]) t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
} }
@@ -79,20 +109,15 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
t.Errorf("stick %d has no material", i) t.Errorf("stick %d has no material", i)
} }
} }
// Determinism: same seed, same appearances.
h := NewGame(Config{Seed: 12345})
if h.Items != g.Items {
t.Error("two games with the same seed produced different item lore")
}
} }
func TestMonsterTable(t *testing.T) { func TestMonsterTable(t *testing.T) {
if monsterTable[0].Name != "aquator" || monsterTable[25].Name != "zombie" { data := newGameData()
if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" {
t.Error("monster table order broken") t.Error("monster table order broken")
} }
if monsterTable['D'-'A'].Name != "dragon" { if data.monsterTable['D'-'A'].Name != "dragon" {
t.Error("letter indexing broken") t.Error("letter indexing broken")
} }
} }

View File

@@ -11,6 +11,8 @@ type testTerm struct {
func (t *testTerm) Render(*Window) {} func (t *testTerm) Render(*Window) {}
func (t *testTerm) Fini() {}
func (t *testTerm) ReadChar() byte { func (t *testTerm) ReadChar() byte {
if t.pos < len(t.input) { if t.pos < len(t.input) {
c := t.input[t.pos] c := t.input[t.pos]

27
game/testdata/README.md vendored Normal file
View File

@@ -0,0 +1,27 @@
# Seed-compatibility golden
`item_tables.golden` is the per-seed item appearance tables (potion colors,
scroll names, ring stones, wand/staff materials) captured from the **C
reference** on the `modern-rogue` branch, for the seeds in the `seeds` list in
`TestSeedCompatItemTables`. That test regenerates the same tables from the Go
port and checks they match byte for byte — proving the LCG and its consumption
order through the whole init sequence (`init_probs``init_player`
`init_names``init_colors``init_stones``init_materials`) agree with C.
## Regenerating the golden
`c_seedcompat.patch` adds a `DUMP` mode to the C `main.c`: with `DUMP` set it
forces the RNG seed from `SEED`, runs the item-table init in the normal order,
prints the tables, and exits before `initscr` (so no terminal is needed).
```sh
# from a checkout of the C reference (modern-rogue branch):
git archive modern-rogue | tar -x -C /tmp/rogue-c
cd /tmp/rogue-c
patch -p1 < .../game/testdata/c_seedcompat.patch
./configure && make
for s in 1 42 12345 99999; do DUMP=1 SEED=$s ./rogue; done \
> .../game/testdata/item_tables.golden
```
The seed list must match the `seeds` slice in `TestSeedCompatItemTables`.

37
game/testdata/c_seedcompat.patch vendored Normal file
View File

@@ -0,0 +1,37 @@
--- a/main.c 2026-07-24 03:02:38
+++ b/main.c 2026-07-24 02:48:31
@@ -63,6 +63,34 @@
#endif
dnum = lowtime + md_getpid();
seed = dnum;
+
+ /* SEEDCOMPAT: dump the per-game item appearance tables for a fixed
+ * seed and exit, without initscr. The init sequence and everything
+ * it consumes from rnd() mirror the normal startup (main.c), so the
+ * tables are exactly what a real game with SEED would show. */
+ if (getenv("DUMP") != NULL)
+ {
+ int di;
+ char *sv = getenv("SEED");
+ if (sv != NULL)
+ seed = atoi(sv);
+ printf("SEED %d\n", seed);
+ init_probs();
+ init_player();
+ init_names();
+ init_colors();
+ init_stones();
+ init_materials();
+ printf("POTIONS\n");
+ for (di = 0; di < MAXPOTIONS; di++) printf("%s\n", p_colors[di]);
+ printf("SCROLLS\n");
+ for (di = 0; di < MAXSCROLLS; di++) printf("%s\n", s_names[di]);
+ printf("RINGS\n");
+ for (di = 0; di < MAXRINGS; di++) printf("%s\n", r_stones[di]);
+ printf("STICKS\n");
+ for (di = 0; di < MAXSTICKS; di++) printf("%s %s\n", ws_type[di], ws_made[di]);
+ exit(0);
+ }
open_score();

260
game/testdata/item_tables.golden vendored Normal file
View File

@@ -0,0 +1,260 @@
SEED 1
POTIONS
tangerine
white
ecru
gold
amber
violet
vermilion
pink
aquamarine
plaid
clear
orange
cyan
tan
SCROLLS
miwhon garsnanih
xomimi roke eshwedshu
potwexrol ipbjorod turs evsnelg
bekornan oxyfatox
iv wexpo wun
ha sefnelgtue whon pay
alari wedit
zantmon umzonski umwhonjo yot
bluoxun rokkho yottrol sta
vomarg microgcomp iteulkshu mung
jo urokeep yuskiun
ox xozantaks klisstaevs ag
ipnih bek
shu ami erk
nejti zim
iprol mic ishoxyvom fagan
reacreti oodrol
bytsri solsa tabu fri
RINGS
agate
zircon
jade
tiger eye
onyx
germanium
lapis lazuli
emerald
taaffeite
kryptonite
garnet
ruby
turquoise
pearl
STICKS
wand steel
wand platinum
staff redwood
staff pine
wand silicon
staff spruce
staff pecan
wand bone
staff maple
wand zinc
wand iron
wand pewter
wand electrum
staff dogwood
SEED 42
POTIONS
blue
green
grey
amber
violet
gold
pink
tan
purple
yellow
plaid
magenta
turquoise
cyan
SCROLLS
bek itod oxytaod oxy
tarhovzant cre sname oxroy
plemik ganod hyd wergerkpot
hyd sol um bekzok
esh eep ganmung
anera ishsa ingala mon
alasniklech viv
yunejorn garro con nej
dotrolther gopum eltitrol trolmonsri
nes alazum
itegopmung ti
ere haeta wergla
nejerecre poipi iprea
ha falechrhov
monskiwex sabitla frido
rhovmar sno
mar bekurzant satbuzum
somon sri
RINGS
carnelian
onyx
jade
granite
stibotantalite
kryptonite
lapis lazuli
germanium
garnet
tiger eye
opal
topaz
agate
peridot
STICKS
staff birch
staff ebony
staff redwood
wand gold
wand copper
wand aluminum
wand titanium
wand mercury
staff cypress
staff bamboo
staff dogwood
wand silicon
staff zebrawood
wand beryllium
SEED 12345
POTIONS
purple
black
grey
brown
plaid
violet
vermilion
ecru
orange
turquoise
tan
magenta
silver
gold
SCROLLS
readalf shuplu ivnin
plelaiv solel skibyt monha
xo wun
wedyfri o ewhonxo favompay
eep zantreanelg
plu buxo
un zontabdan
bie snik
ulkitzant bluri
apporg ash posnevly dennepwex
u urval rol
arzepotsno snovly pay snoropay
pottox erewed faoxro
ther sun ulkipo mik
argzebfri elgrekli tuenepzon sehturssef
isheep blumur
wedash yuzimsun
plupofri ski rejo fa
RINGS
onyx
tiger eye
alexandrite
turquoise
pearl
emerald
germanium
sapphire
zircon
ruby
granite
stibotantalite
opal
diamond
STICKS
wand silicon
staff ironwood
staff holly
wand gold
staff mahogany
wand iron
wand brass
wand pewter
staff hemlock
staff cherry
staff elm
wand mercury
staff banyan
staff dogwood
SEED 99999
POTIONS
aquamarine
plaid
gold
black
vermilion
red
cyan
tan
orange
violet
brown
clear
silver
green
SCROLLS
zant jocompan vomervly
mur shusat prok
prokmurklis oxysriklis
ingcre prokbu whonengarg kli
kli bot
rokcoswerg ipsolsan klisvlypay
glen yot whontox
lechme markho fazim
dalfsunbie micjosef cre comp
vlyfumi bjorzantbot werg
po argfidcos klipones
ashtemarg ycrezim dalfiv whon
turs unmisa zimpo therdo
miccompuni uni neswex sef
odwexing elwergmur mung
itcon rhov nejmic lech
garturs engseh ganish
oodta whonorgsno monabmik vomyeng
RINGS
obsidian
moonstone
jade
carnelian
tiger eye
taaffeite
turquoise
stibotantalite
agate
ruby
onyx
topaz
germanium
granite
STICKS
wand titanium
wand brass
wand silicon
staff zebrawood
wand mercury
staff dogwood
wand pewter
staff cinnibar
staff kukui wood
staff banyan
wand magnesium
wand gold
staff maple
wand nickel

View File

@@ -9,7 +9,7 @@ import (
// invName returns the name of something as it would appear in an inventory // invName returns the name of something as it would appear in an inventory
// (things.c inv_name). // (things.c inv_name).
func (g *RogueGame) invName(obj *Object, drop bool) string { func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
var pb strings.Builder var pb strings.Builder
which := obj.Which which := obj.Which
@@ -17,53 +17,80 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
switch obj.Kind { switch obj.Kind {
case KindPotion: case KindPotion:
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr) g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr)
case KindRing: case KindRing:
g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum) g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
case KindWand: case KindWand:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr) g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case KindScroll: case KindScroll:
g.nameScroll(&pb, obj)
case KindFood:
g.nameFood(&pb, obj)
case KindWeapon:
g.nameWeapon(&pb, obj)
case KindArmor:
g.nameArmor(&pb, obj)
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
return fixNameCase(g.describeWorn(obj, pb.String()), drop)
}
// nameScroll writes a scroll's inventory name (things.c inv_name).
func (g *RogueGame) nameScroll(pb *strings.Builder, obj *Object) {
if obj.Count == 1 { if obj.Count == 1 {
pb.WriteString("A scroll ") pb.WriteString("A scroll ")
} else { } else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count) fmt.Fprintf(pb, "%d scrolls ", obj.Count)
} }
op := &it.Scrolls[which] op := &g.Items.Scrolls[obj.Which]
switch { switch {
case op.Know: case op.Know:
fmt.Fprintf(&pb, "of %s", op.Name) fmt.Fprintf(pb, "of %s", op.Name)
case op.Guess != "": case op.Guess != "":
fmt.Fprintf(&pb, "called %s", op.Guess) fmt.Fprintf(pb, "called %s", op.Guess)
default: default:
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which]) fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
} }
case KindFood: }
if which == 1 {
// nameFood writes a food item's inventory name; which 1 is the fruit
// (things.c inv_name).
func (g *RogueGame) nameFood(pb *strings.Builder, obj *Object) {
if obj.Which == 1 {
if obj.Count == 1 { if obj.Count == 1 {
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit) fmt.Fprintf(pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else { } else {
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit) fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
} }
} else {
return
}
if obj.Count == 1 { if obj.Count == 1 {
pb.WriteString("Some food") pb.WriteString("Some food")
} else { } else {
fmt.Fprintf(&pb, "%d rations of food", obj.Count) fmt.Fprintf(pb, "%d rations of food", obj.Count)
} }
} }
case KindWeapon:
sp := it.Weapons[which].Name // nameWeapon writes a weapon's inventory name (things.c inv_name).
func (g *RogueGame) nameWeapon(pb *strings.Builder, obj *Object) {
sp := g.Items.Weapons[obj.Which].Name
if obj.Count > 1 { if obj.Count > 1 {
fmt.Fprintf(&pb, "%d ", obj.Count) fmt.Fprintf(pb, "%d ", obj.Count)
} else { } else {
fmt.Fprintf(&pb, "A%s ", vowelstr(sp)) fmt.Fprintf(pb, "A%s ", vowelstr(sp))
} }
if obj.Flags.Has(Known) { if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp) fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else { } else {
pb.WriteString(sp) pb.WriteString(sp)
} }
@@ -73,34 +100,38 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
} }
if obj.Label != "" { if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label) fmt.Fprintf(pb, " called %s", obj.Label)
} }
case KindArmor: }
sp := it.Armors[which].Name
// nameArmor writes an armor's inventory name (things.c inv_name).
func (g *RogueGame) nameArmor(pb *strings.Builder, obj *Object) {
sp := g.Items.Armors[obj.Which].Name
if obj.Flags.Has(Known) { if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp) fmt.Fprintf(pb, "%s %s [",
num(g.data.aClass[obj.Which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse { if !g.Options.Terse {
pb.WriteString("protection ") pb.WriteString("protection ")
} }
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass) fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
} else { } else {
pb.WriteString(sp) pb.WriteString(sp)
} }
if obj.Label != "" { if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label) fmt.Fprintf(pb, " called %s", obj.Label)
} }
case KindAmulet: }
pb.WriteString("The Amulet of Yendor")
case KindGold: // describeWorn appends the equipped-status notes to an inventory name
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue) // (things.c inv_name).
func (g *RogueGame) describeWorn(obj *Object, out string) string {
if !g.InvDescribe {
return out
} }
out := pb.String()
if g.InvDescribe {
p := &g.Player p := &g.Player
if obj == p.CurArmor { if obj == p.CurArmor {
out += " (being worn)" out += " (being worn)"
@@ -116,14 +147,23 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
case p.CurRing[Right]: case p.CurRing[Right]:
out += " (on right hand)" out += " (on right hand)"
} }
return out
}
// fixNameCase upper- or lowercases the leading letter to suit the
// sentence it will land in (things.c inv_name).
func fixNameCase(out string, drop bool) string {
if out == "" {
return out
} }
if out != "" {
if drop && isUpper(out[0]) { if drop && isUpper(out[0]) {
out = string(toLower(out[0])) + out[1:] return string(toLower(out[0])) + out[1:]
} else if !drop && isLower(out[0]) {
out = string(toUpper(out[0])) + out[1:]
} }
if !drop && isLower(out[0]) {
return string(toUpper(out[0])) + out[1:]
} }
return out return out
@@ -142,8 +182,8 @@ func (g *RogueGame) dropIt() {
return return
} }
obj := g.getItem("drop", KindNone) obj, ok := g.promptPackItem("drop", KindNone)
if obj == nil { if !ok {
return return
} }
@@ -153,7 +193,7 @@ func (g *RogueGame) dropIt() {
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack()) obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
// Link it into the level object list // Link it into the level object list
attachObj(&g.Level.Objects, obj) g.Level.AddObject(obj)
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph()) g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph())
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped) g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
@@ -162,7 +202,7 @@ func (g *RogueGame) dropIt() {
g.HasAmulet = false g.HasAmulet = false
} }
g.msg("dropped %s", g.invName(obj, true)) g.msg("dropped %s", g.inventoryName(obj, true))
} }
// dropCheck does special checks for dropping or unwielding|unwearing| // dropCheck does special checks for dropping or unwielding|unwearing|
@@ -192,6 +232,17 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
p.CurArmor = nil p.CurArmor = nil
default: default:
g.dropRing(obj)
}
return true
}
// dropRing takes a worn ring off with its side effects (things.c
// dropcheck).
func (g *RogueGame) dropRing(obj *Object) {
p := &g.Player
hand := Right hand := Right
if obj == p.CurRing[Left] { if obj == p.CurRing[Left] {
hand = Left hand = Left
@@ -201,14 +252,11 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(-obj.Bonus) g.changeStrength(-obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.unsee(0) g.unsee(0)
g.Extinguish(DUnsee) g.Extinguish(DUnsee)
} }
}
return true
} }
// newThing returns a new random thing for the dungeon (things.c new_thing). // newThing returns a new random thing for the dungeon (things.c new_thing).
@@ -236,6 +284,25 @@ func (g *RogueGame) newThing() *Object {
cur.Kind = KindScroll cur.Kind = KindScroll
cur.Which = pickOne(g, g.Items.Scrolls[:]) cur.Which = pickOne(g, g.Items.Scrolls[:])
case 2: case 2:
g.newFoodThing(cur)
case 3:
g.newWeaponThing(cur)
case 4:
g.newArmorThing(cur)
case 5:
g.newRingThing(cur)
case 6:
cur.Kind = KindWand
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
}
// newFoodThing rolls food, one time in ten the fruit (things.c
// new_thing).
func (g *RogueGame) newFoodThing(cur *Object) {
cur.Kind = KindFood cur.Kind = KindFood
g.Player.NoFood = 0 g.Player.NoFood = 0
@@ -244,7 +311,11 @@ func (g *RogueGame) newThing() *Object {
} else { } else {
cur.Which = 1 cur.Which = 1
} }
case 3: }
// newWeaponThing rolls a weapon, sometimes cursed or blessed (things.c
// new_thing).
func (g *RogueGame) newWeaponThing(cur *Object) {
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes]))) g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
if r := g.rnd(100); r < 10 { if r := g.rnd(100); r < 10 {
@@ -253,18 +324,25 @@ func (g *RogueGame) newThing() *Object {
} else if r < 15 { } else if r < 15 {
cur.HPlus += g.rnd(3) + 1 cur.HPlus += g.rnd(3) + 1
} }
case 4: }
// newArmorThing rolls armor, sometimes cursed or blessed (things.c
// new_thing).
func (g *RogueGame) newArmorThing(cur *Object) {
cur.Kind = KindArmor cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:]) cur.Which = pickOne(g, g.Items.Armors[:])
cur.ArmorClass = aClass[cur.Which] cur.ArmorClass = g.data.aClass[cur.Which]
if r := g.rnd(100); r < 20 { if r := g.rnd(100); r < 20 {
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
cur.ArmorClass += g.rnd(3) + 1 cur.ArmorClass += g.rnd(3) + 1
} else if r < 28 { } else if r < 28 {
cur.ArmorClass -= g.rnd(3) + 1 cur.ArmorClass -= g.rnd(3) + 1
} }
case 5: }
// newRingThing rolls a ring, cursing the bad ones (things.c new_thing).
func (g *RogueGame) newRingThing(cur *Object) {
cur.Kind = KindRing cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:]) cur.Which = pickOne(g, g.Items.Rings[:])
@@ -277,13 +355,6 @@ func (g *RogueGame) newThing() *Object {
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
} }
case 6:
cur.Kind = KindWand
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
} }
// pickOne picks an item out of a list of possible objects using their // pickOne picks an item out of a list of possible objects using their
@@ -392,7 +463,7 @@ func (g *RogueGame) printDisc(typ byte) {
if info[order[i]].Know || info[order[i]].Guess != "" { if info[order[i]].Know || info[order[i]].Guess != "" {
obj.Kind = objectKindForGlyph(typ) obj.Kind = objectKindForGlyph(typ)
obj.Which = order[i] obj.Which = order[i]
g.addLine("%s", g.invName(&obj, false)) g.addLine("%s", g.inventoryName(&obj, false))
numFound++ numFound++
} }
@@ -423,7 +494,6 @@ const flushSentinel = "\x00"
func (g *RogueGame) addLine(format string, a ...any) int { func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage pg := &g.invPage
prompt := "--Press space to continue--"
isFlush := format == flushSentinel isFlush := format == flushSentinel
var line string var line string
@@ -440,23 +510,80 @@ func (g *RogueGame) addLine(format string, a ...any) int {
} }
if g.Options.InvType == InvSlow { if g.Options.InvType == InvSlow {
return g.addLineSlow(line, isFlush)
}
g.addLinePaged(line, isFlush)
return ^Escape
}
// addLineSlow shows one discovery line as a message (the slow-inventory
// arm of things.c add_line).
func (g *RogueGame) addLineSlow(line string, isFlush bool) int {
if !isFlush && line != "" { if !isFlush && line != "" {
if g.msg("%s", line) == Escape { if g.msg("%s", line) == Escape {
return Escape return Escape
} }
} }
pg.lineCnt++ g.invPage.lineCnt++
} else {
return ^Escape
}
// addLinePaged accumulates discovery lines into the paged window,
// prompting between full pages (the windowed arm of things.c add_line).
func (g *RogueGame) addLinePaged(line string, isFlush bool) {
pg := &g.invPage
prompt := "--Press space to continue--"
if !pg.init { if !pg.init {
pg.maxlen = len(prompt) pg.maxlen = len(prompt)
pg.init = true pg.init = true
} }
if pg.lineCnt >= NumLines-1 || isFlush { if pg.lineCnt >= NumLines-1 || isFlush {
g.addLinePageBreak(prompt, isFlush)
}
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
// addLinePageBreak prompts at a full page and starts a fresh one
// (things.c add_line).
func (g *RogueGame) addLinePageBreak(prompt string, isFlush bool) {
pg := &g.invPage
if g.Options.InvType == InvOver && isFlush && !pg.newpage { if g.Options.InvType == InvOver && isFlush && !pg.newpage {
// Overlay the accumulated list in a box at the top right g.addLineOverlay(prompt)
// of the screen, prompt, and restore what was beneath. } else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
// addLineOverlay draws the accumulated list in a box at the top right
// of the screen, prompts, and restores what was beneath (things.c
// add_line).
func (g *RogueGame) addLineOverlay(prompt string) {
pg := &g.invPage
g.msg("") g.msg("")
g.refresh() g.refresh()
@@ -475,32 +602,6 @@ func (g *RogueGame) addLine(format string, a ...any) int {
g.waitFor(' ') g.waitFor(' ')
g.scr.Std.CopyFrom(saved) g.scr.Std.CopyFrom(saved)
g.refresh() g.refresh()
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
return ^Escape
} }
// flushLine is add_line(NULL): force out the accumulated page. // flushLine is add_line(NULL): force out the accumulated page.
@@ -536,11 +637,11 @@ func (g *RogueGame) nothing(typ byte) string {
switch typ { switch typ {
case Potion: case Potion:
tystr = "potion" tystr = potionName
case Scroll: case Scroll:
tystr = "scroll" tystr = scrollName
case Ring: case Ring:
tystr = "ring" tystr = ringName
case Stick: case Stick:
tystr = "stick" tystr = "stick"
} }

View File

@@ -210,16 +210,6 @@ const (
NumTrapTypes = 8 NumTrapTypes = 8
) )
// String returns the trap's display name, article included, as the C
// tr_name table had it.
func (t TrapKind) String() string {
if t < 0 || t >= NumTrapTypes {
return "a bizarre trap"
}
return trName[t]
}
// PotionKind identifies a potion (rogue.h potion types). // PotionKind identifies a potion (rogue.h potion types).
type PotionKind int type PotionKind int
@@ -242,15 +232,6 @@ const (
NumPotionTypes NumPotionTypes
) )
// String returns the potion's true name ("healing", "haste self", ...).
func (p PotionKind) String() string {
if p < 0 || p >= NumPotionTypes {
return "strange potion"
}
return basePotInfo[p].Name
}
// ScrollKind identifies a scroll (rogue.h scroll types). // ScrollKind identifies a scroll (rogue.h scroll types).
type ScrollKind int type ScrollKind int
@@ -277,15 +258,6 @@ const (
NumScrollTypes NumScrollTypes
) )
// String returns the scroll's true name ("magic mapping", ...).
func (s ScrollKind) String() string {
if s < 0 || s >= NumScrollTypes {
return "strange scroll"
}
return baseScrInfo[s].Name
}
// WeaponKind identifies a weapon (rogue.h weapon types). // WeaponKind identifies a weapon (rogue.h weapon types).
type WeaponKind int type WeaponKind int
@@ -307,15 +279,6 @@ const (
// just past them in the tables (C's MAXWEAPONS == FLAME). // just past them in the tables (C's MAXWEAPONS == FLAME).
const NumWeaponTypes = WeaponFlame const NumWeaponTypes = WeaponFlame
// String returns the weapon's name ("mace", "two handed sword", ...).
func (w WeaponKind) String() string {
if w < 0 || w > WeaponFlame {
return "strange weapon"
}
return baseWeapInfo[w].Name
}
// ArmorKind identifies a suit of armor (rogue.h armor types). // ArmorKind identifies a suit of armor (rogue.h armor types).
type ArmorKind int type ArmorKind int
@@ -332,15 +295,6 @@ const (
NumArmorTypes NumArmorTypes
) )
// String returns the armor's name ("ring mail", "plate mail", ...).
func (a ArmorKind) String() string {
if a < 0 || a >= NumArmorTypes {
return "strange armor"
}
return baseArmInfo[a].Name
}
// RingKind identifies a ring (rogue.h ring types). // RingKind identifies a ring (rogue.h ring types).
type RingKind int type RingKind int
@@ -363,15 +317,6 @@ const (
NumRingTypes NumRingTypes
) )
// String returns the ring's true name ("add strength", "stealth", ...).
func (r RingKind) String() string {
if r < 0 || r >= NumRingTypes {
return "strange ring"
}
return baseRingInfo[r].Name
}
// WandKind identifies a wand or staff (rogue.h rod/wand/staff types). // WandKind identifies a wand or staff (rogue.h rod/wand/staff types).
type WandKind int type WandKind int
@@ -394,15 +339,6 @@ const (
NumWandTypes NumWandTypes
) )
// String returns the wand/staff's true name ("lightning", ...).
func (w WandKind) String() string {
if w < 0 || w >= NumWandTypes {
return "strange stick"
}
return baseWsInfo[w].Name
}
// Coord is a position on the level (rogue.h coord). A value type: the C // Coord is a position on the level (rogue.h coord). A value type: the C
// ce(a,b) macro is plain == here. // ce(a,b) macro is plain == here.
type Coord struct { type Coord struct {

View File

@@ -9,8 +9,8 @@ const noWeapon WeaponKind = -1
// missile fires a missile in a given direction (weapons.c missile). // missile fires a missile in a given direction (weapons.c missile).
func (g *RogueGame) missile(ydelta, xdelta int) { func (g *RogueGame) missile(ydelta, xdelta int) {
// Get which thing we are hurling // Get which thing we are hurling
obj := g.getItem("throw", KindWeapon) obj, ok := g.promptPackItem("throw", KindWeapon)
if obj == nil { if !ok {
return return
} }
@@ -35,32 +35,36 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
// Come fly with us ... // Come fly with us ...
obj.Pos = p.Pos obj.Pos = p.Pos
for { for {
// Erase the old one g.eraseFlight(obj, p.Pos)
if obj.Pos != p.Pos && g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse { // Get the new position
obj.Pos.Y += ydelta
obj.Pos.X += xdelta
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
if !stepOk(ch) || ch == Door {
break
}
// It hasn't hit anything yet, so display it if it's alright.
if g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
g.refresh()
}
}
}
// eraseFlight erases a flying object from its current square, unless it
// still sits on the hero (the erase step of weapons.c do_motion).
func (g *RogueGame) eraseFlight(obj *Object, heroPos Coord) {
if obj.Pos == heroPos || !g.canSee(obj.Pos.Y, obj.Pos.X) || g.Options.Terse {
return
}
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch == Floor && !g.showFloor() { if ch == Floor && !g.showFloor() {
ch = ' ' ch = ' '
} }
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch) g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
}
// Get the new position
obj.Pos.Y += ydelta
obj.Pos.X += xdelta
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
if stepOk(ch) && ch != Door {
// It hasn't hit anything yet, so display it if it's alright.
if g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
g.refresh()
}
continue
}
break
}
} }
// fall drops an item someplace around here (weapons.c fall). // fall drops an item someplace around here (weapons.c fall).
@@ -70,7 +74,7 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
pp.Ch = obj.Kind.Glyph() pp.Ch = obj.Kind.Glyph()
obj.Pos = fpos obj.Pos = fpos
if g.cansee(fpos.Y, fpos.X) { if g.canSee(fpos.Y, fpos.X) {
if pp.Monst != nil { if pp.Monst != nil {
pp.Monst.OldCh = obj.Kind.Glyph() pp.Monst.OldCh = obj.Kind.Glyph()
} else { } else {
@@ -78,7 +82,7 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
} }
} }
attachObj(&g.Level.Objects, obj) g.Level.AddObject(obj)
return return
} }
@@ -113,8 +117,8 @@ func (g *RogueGame) wield() {
p.CurWeapon = oweapon p.CurWeapon = oweapon
obj := g.getItem("wield", KindWeapon) obj, ok := g.promptPackItem("wield", KindWeapon)
if obj == nil { if !ok {
g.After = false g.After = false
return return
@@ -133,7 +137,7 @@ func (g *RogueGame) wield() {
return return
} }
sp := g.invName(obj, true) sp := g.inventoryName(obj, true)
p.CurWeapon = obj p.CurWeapon = obj
if !g.Options.Terse { if !g.Options.Terse {
@@ -143,27 +147,18 @@ func (g *RogueGame) wield() {
g.msg("wielding %s (%c)", sp, obj.PackCh) g.msg("wielding %s (%c)", sp, obj.PackCh)
} }
// initWeaps is the weapons.c init_dam[] table. // weaponSetup is one row of the weapons.c init_dam[] table (see
var initWeaps = [NumWeaponTypes]struct { // gameData.initWeaps).
type weaponSetup struct {
dam DiceSpec // damage when wielded dam DiceSpec // damage when wielded
hrl DiceSpec // damage when thrown hrl DiceSpec // damage when thrown
launch WeaponKind // launching weapon launch WeaponKind // launching weapon
flags ObjFlags flags ObjFlags
}{
{dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
{dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
{dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
{dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
{dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
{dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
{dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
{dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
{dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
} }
// initWeapon sets up a new weapon (weapons.c init_weapon). // initWeapon sets up a new weapon (weapons.c init_weapon).
func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) { func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
iwp := &initWeaps[which] iwp := &g.data.initWeaps[which]
weap.Kind = KindWeapon weap.Kind = KindWeapon
weap.Which = int(which) weap.Which = int(which)
weap.Damage = iwp.dam weap.Damage = iwp.dam

View File

@@ -27,6 +27,26 @@ func (g *RogueGame) createObj() {
switch obj.Kind { switch obj.Kind {
case KindWeapon, KindArmor: case KindWeapon, KindArmor:
g.createWeaponArmor(obj)
case KindRing:
g.createRing(obj)
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
}
// createWeaponArmor sets up a wizard-created weapon or armor with an
// optional blessing (the weapon/armor arm of wizard.c create_obj).
func (g *RogueGame) createWeaponArmor(obj *Object) {
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
bless := g.readchar() bless := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -45,8 +65,11 @@ func (g *RogueGame) createObj() {
if bless == '+' { if bless == '+' {
obj.HPlus += g.rnd(3) + 1 obj.HPlus += g.rnd(3) + 1
} }
} else {
obj.ArmorClass = aClass[obj.Which] return
}
obj.ArmorClass = g.data.aClass[obj.Which]
if bless == '-' { if bless == '-' {
obj.ArmorClass += g.rnd(3) + 1 obj.ArmorClass += g.rnd(3) + 1
} }
@@ -54,8 +77,11 @@ func (g *RogueGame) createObj() {
if bless == '+' { if bless == '+' {
obj.ArmorClass -= g.rnd(3) + 1 obj.ArmorClass -= g.rnd(3) + 1
} }
} }
case KindRing:
// createRing sets up a wizard-created ring, prompting for a bonus on
// the bonus rings (the ring arm of wizard.c create_obj).
func (g *RogueGame) createRing(obj *Object) {
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage: case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
@@ -71,18 +97,6 @@ func (g *RogueGame) createObj() {
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
} }
case KindWand:
g.fixStick(obj)
case KindGold:
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
} }
// showMap prints out the whole map for the wizard (wizard.c show_map). // showMap prints out the whole map for the wizard (wizard.c show_map).
@@ -117,34 +131,8 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
return return
} }
var obj *Object obj, ok := g.whatisPick(insist, kind)
for { if !ok {
obj = g.getItem("identify", kind)
if !insist {
break
}
if g.NObjs == 0 {
return
}
if obj == nil {
g.msg("you must identify something")
continue
}
if !matchesFilter(kind, obj) {
g.msg("you must identify a %s", kind)
continue
}
break
}
if obj == nil {
return return
} }
@@ -161,7 +149,38 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
setKnow(obj, g.Items.Rings[:]) setKnow(obj, g.Items.Rings[:])
} }
g.msg("%s", g.invName(obj, false)) g.msg("%s", g.inventoryName(obj, false))
}
// whatisPick prompts for the item to identify, re-asking until a
// matching one is chosen when insist is set; ok is false when the
// player gives up (the prompt loop of wizard.c whatis).
func (g *RogueGame) whatisPick(insist bool, kind ObjectKind) (*Object, bool) {
for {
obj, _ := g.promptPackItem("identify", kind)
if !insist {
return obj, obj != nil
}
if g.NObjs == 0 {
return nil, false
}
if obj == nil {
g.msg("you must identify something")
continue
}
if !matchesFilter(kind, obj) {
g.msg("you must identify a %s", kind)
continue
}
return obj, true
}
} }
// setKnow sets things up when we really know what a thing is (wizard.c // setKnow sets things up when we really know what a thing is (wizard.c
@@ -181,7 +200,7 @@ func (g *RogueGame) teleport() {
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
c, _ := g.findFloor(true) c, _ := g.findFloor(true)
if g.roomin(c) != p.Room { if g.roomIn(c) != p.Room {
g.leaveRoom(p.Pos) g.leaveRoom(p.Pos)
p.Pos = c p.Pos = c
g.enterRoom(p.Pos) g.enterRoom(p.Pos)

View File

@@ -89,46 +89,86 @@ func (t *Tcell) ReadChar() byte {
t.Render(t.last) t.Render(t.last)
} }
case *tcell.EventKey: case *tcell.EventKey:
switch ev.Key() { if b, ok := translateKey(ev); ok {
case tcell.KeyUp: return b
return 'k' }
case tcell.KeyDown: }
return 'j' }
case tcell.KeyLeft: }
return 'h'
case tcell.KeyRight: // translateKey converts a key event to a game input byte; ok is false
return 'l' // for keys the C game does not understand.
case tcell.KeyHome: func translateKey(ev *tcell.EventKey) (byte, bool) {
return 'y' if b, ok := namedKey(ev.Key()); ok {
case tcell.KeyPgUp: return b, true
return 'u' }
case tcell.KeyEnd:
return 'b'
case tcell.KeyPgDn:
return 'n'
case tcell.KeyEnter:
return '\n'
case tcell.KeyEscape:
return game.Escape
case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8
case tcell.KeyDelete:
return 0x7f
case tcell.KeyTab:
return '\t'
case tcell.KeyCtrlC:
return 3
default:
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ { if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits return byte(ev.Key()), true //nolint:gosec // G115: 1..26 fits
} }
if r := ev.Rune(); r > 0 && r < 0x80 { if r := ev.Rune(); r > 0 && r < 0x80 {
return byte(r) return byte(r), true
} }
return 0, false
}
// namedKey translates tcell's navigation and editing keys to the single
// bytes the C game reads (arrows become hjkl, etc.); ok is false for
// keys handled elsewhere.
func namedKey(k tcell.Key) (byte, bool) {
if b, ok := motionKey(k); ok {
return b, true
} }
return editingKey(k)
}
// motionKey translates the arrow and paging keys to Rogue's movement
// letters (tcell.go ReadChar).
func motionKey(k tcell.Key) (byte, bool) {
switch k {
case tcell.KeyUp:
return 'k', true
case tcell.KeyDown:
return 'j', true
case tcell.KeyLeft:
return 'h', true
case tcell.KeyRight:
return 'l', true
case tcell.KeyHome:
return 'y', true
case tcell.KeyPgUp:
return 'u', true
case tcell.KeyEnd:
return 'b', true
case tcell.KeyPgDn:
return 'n', true
} }
return 0, false
}
// editingKey translates the editing and control keys to their C0 codes
// (tcell.go ReadChar).
func editingKey(k tcell.Key) (byte, bool) {
switch k {
case tcell.KeyEnter:
return '\n', true
case tcell.KeyEscape:
return game.Escape, true
case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8, true
case tcell.KeyDelete:
return 0x7f, true
case tcell.KeyTab:
return '\t', true
case tcell.KeyCtrlC:
return 3, true
} }
return 0, false
} }
// ShellEscape suspends the screen and runs the user's shell (main.c // ShellEscape suspends the screen and runs the user's shell (main.c