57 Commits

Author SHA1 Message Date
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
50afbec8e3 Merge refactor/lint-adoption (house lint config, findings fixed) 2026-07-07 00:03:45 +02:00
5ba9fe8f66 Adopt house golangci-lint config; fix all approved-linter findings
.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:

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

Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
2026-07-07 00:03:45 +02:00
52 changed files with 8517 additions and 5133 deletions

38
.golangci.yml Normal file
View File

@@ -0,0 +1,38 @@
version: "2"
run:
timeout: 5m
modules-download-mode: readonly
linters:
default: all
disable:
# Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields
- depguard # Dependency allow/block lists
- godot # Requires comments to end with periods
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
# Repo-specific exceptions approved by sneak (2026-07-06)
- 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:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

File diff suppressed because it is too large Load Diff

41
MEMORY.md Normal file
View File

@@ -0,0 +1,41 @@
# Project Memory
Working notes for agents on this repo. Read this alongside TODO.md (which holds
the step queue and workflow) before starting work.
## Error handling
Panicking on bad/unexpected errors is allowed and preferred over threading
unlikely error returns through game code — e.g. write-side Close/encode failures
where continuing would mean corrupt state. The game already unwinds C's exit()
calls via a gameEnd panic recovered in Run. Return errors where a caller
genuinely handles them (save-file prompts, restore validation). Reserve
deliberate `_ =` discards for true best-effort paths (scorefile writes,
signal-time autosave), always with a comment saying why.
## Linting
The .golangci.yml is the house standard and may only be modified with sneak's
explicit permission. To disable a linter, ask, explaining what the linter does;
he approves specific exceptions, which are recorded in the config's
"Repo-specific exceptions" block with the approval date. Approved so far:
paralleltest (2026-07-06); testpackage, exhaustive, and mnd (2026-07-07). The
complexity linters (cyclop, gocognit, nestif) are enabled and clean as of
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.
## Faithfulness
Behavior must not change during the idiomatic-Go refactor unless a TODO step
says so. The 80x24 seed-compatible gameplay, message text (including original
typos), RNG call order, and C quirks (documented in tests like
TestHoldScrollGreedyMonsterQuirk) are contract. Doc comments keep their "(file.c
func_name)" breadcrumbs.
## Debugging
Write real, committed test files with t.Logf output and run plain `go test -v`;
no throwaway scratch scripts. Successful debug probes 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)
**Rogue** is the original dungeon-crawling adventure game that spawned an
entire genre. This branch is a faithful Go port of Rogue 5.4.4: explore
procedurally generated dungeons, fight monsters, collect treasure, and
attempt to retrieve the Amulet of Yendor.
**Rogue** is the original dungeon-crawling adventure game that spawned an entire
genre. This branch is a faithful Go port of Rogue 5.4.4: explore procedurally
generated dungeons, fight monsters, collect treasure, and attempt to retrieve
the Amulet of Yendor.
**Original authors:** Michael Toy, Ken Arnold, and Glenn Wichman
(19801983, 1985, 1999).
**Original authors:** Michael Toy, Ken Arnold, and Glenn Wichman (19801983,
1985, 1999).
The port is function-by-function faithful to the classic C sources — same
dungeon generation (seed-compatible RNG), same combat math, same item
tables, same messages. The C reference implementation lives on the
`master` and `modern-rogue` branches; [ARCHITECTURE.md](ARCHITECTURE.md)
documents both the original program structure and the design of this port.
dungeon generation (seed-compatible RNG), same combat math, same item tables,
same messages. The C reference implementation lives on the `master` and
`modern-rogue` branches; [ARCHITECTURE.md](ARCHITECTURE.md) documents both the
original program structure and the design of this port.
## Building and running
@@ -40,13 +40,12 @@ go build ./cmd/rogue
Press `?` in game for the full list.
- **arrows** or **h/j/k/l/y/u/b/n** — move (shift to run, ctrl to run
until adjacent)
- **arrows** or **h/j/k/l/y/u/b/n** — move (shift to run, ctrl to run until
adjacent)
- **`.`** rest, **`s`** search for hidden doors and traps
- **`i`** inventory, **`,`** pick up, **`d`** drop
- **`q`** quaff potion, **`r`** read scroll, **`e`** eat food
- **`w`** wield weapon, **`W`** wear armor, **`P`**/**`R`** put on /
remove ring
- **`w`** wield weapon, **`W`** wear armor, **`P`**/**`R`** put on / remove ring
- **`t`** throw, **`z`** zap a wand, **`f`**/**`F`** fight
- **`>`**/**`<`** take the stairs
- **`S`** save, **`Q`** quit
@@ -61,8 +60,8 @@ export ROGUEOPTS="name=YourName,terse,jump,fruit=mango"
ROGUE_WIZARD=1 SEED=12345 ./rogue
```
The scoreboard is kept in `~/.rogue.scores`. Save files are Go gob
snapshots and, as in the original, are deleted when restored.
The scoreboard is kept in `~/.rogue.scores`. Save files are Go gob snapshots
and, as in the original, are deleted when restored.
## Code layout
@@ -73,13 +72,13 @@ term/ tcell-backed terminal, replacing curses
cmd/rogue/ the executable
```
The engine package is fully headless-testable: `go test ./game/` runs
scripted game sessions, dungeon-generation golden checks, and an RNG
compatibility test against the original C generator.
The engine package is fully headless-testable: `go test ./game/` runs scripted
game sessions, dungeon-generation golden checks, and an RNG compatibility test
against the original C generator.
## License
BSD-style; see [LICENSE.TXT](LICENSE.TXT).
Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn
Wichman. All rights reserved.
Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman.
All rights reserved.

245
TODO.md
View File

@@ -1,120 +1,167 @@
# Workflow
* branch (from `main`)
* do the work in Next Step
* move Next Step to the top of Completed Steps
* move the top item of Future Steps into Next Step
* commit (`TODO.md` changes in the same commit as the work)
* merge to `main` if the branch is not protected, otherwise open a PR
* push
- branch (from `main`)
- do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (`TODO.md` changes in the same commit as the work)
- merge to `main` if the branch is not protected, otherwise open a PR
- push
# Status
pre-1.0
The port on main is complete and faithful (function-by-function from
Rogue 5.4.4 C; reference sources on c-master/modern-rogue). Current
phase: refactor from a transliterated port into idiomatic Go — one
feature branch per step below, descriptive naming, real types, house
style per ~/dev/prompts/prompts/CODE_STYLEGUIDE_GO.md.
The port on main is complete and faithful (function-by-function from Rogue 5.4.4
C; reference sources on c-master/modern-rogue). Current phase: refactor from a
transliterated port into idiomatic Go — one feature branch per step below,
descriptive naming, real types, house style per
~/dev/prompts/prompts/CODE_STYLEGUIDE_GO.md.
Refactor ground rules:
- Behavior must not change unless a step says so. The full test suite
(scripted sessions, generation invariants, C-compatible RNG goldens)
gates every step; 80x24 seed-compatible gameplay stays intact.
- Renames keep the C lineage greppable: doc comments retain their
"(file.c func_name)" breadcrumbs, and the docs refresh step adds a
C-name → Go-name table to ARCHITECTURE.md.
- Behavior must not change unless a step says so. The full test suite (scripted
sessions, generation invariants, C-compatible RNG goldens) gates every step;
80x24 seed-compatible gameplay stays intact.
- Renames keep the C lineage greppable: doc comments retain their "(file.c
func_name)" breadcrumbs, and the docs refresh step adds a C-name → Go-name
table to ARCHITECTURE.md.
# Next Step
Adopt the house Go linting standards: copy .golangci.yml from the
prompts repo and bring game/, term/, and cmd/ lint-clean (the port is
greenfield code, so no exemptions apply).
Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the post-refactor
names; add the C name → Go name rename table.
# Completed Steps
- 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-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
autoconf/VS build system (they remain on master and modern-rogue),
ported the last wizard command (item-probability listing), rewrote
README.md for the Go port (c0b533e)
- 2026-07-06 Ported the command loop, save/restore, the tcell terminal
layer, and the playable binary at cmd/rogue (41fc104)
- 2026-07-06 Ported item effects: potions, scrolls, options, call_it
(cdf9bf7)
- 2026-07-06 Ported combat, the chase driver, traps, zapping, death and
scores (3c5add8)
- 2026-07-06 Ported dungeon generation, base items, the pack, and
monster creation (a69ef7d)
- 2026-07-06 Ported the foundation: types, seed-compatible RNG, item
tables, daemon scheduler (7fa2048)
- 2026-07-06 Wrote ARCHITECTURE.md Parts 1 and 2: complete map of the C
program and the Go port design (91eeee0, 45dba95)
- Fork base: Davidslv/rogue C 5.4.4 with modernization fixes (C23
prototypes, ncurses compat), preserved on master/modern-rogue
autoconf/VS build system (they remain on master and modern-rogue), ported the
last wizard command (item-probability listing), rewrote README.md for the Go
port (c0b533e)
- 2026-07-06 Ported the command loop, save/restore, the tcell terminal layer,
and the playable binary at cmd/rogue (41fc104)
- 2026-07-06 Ported item effects: potions, scrolls, options, call_it (cdf9bf7)
- 2026-07-06 Ported combat, the chase driver, traps, zapping, death and scores
(3c5add8)
- 2026-07-06 Ported dungeon generation, base items, the pack, and monster
creation (a69ef7d)
- 2026-07-06 Ported the foundation: types, seed-compatible RNG, item tables,
daemon scheduler (7fa2048)
- 2026-07-06 Wrote ARCHITECTURE.md Parts 1 and 2: complete map of the C program
and the Go port design (91eeee0, 45dba95)
- Fork base: Davidslv/rogue C 5.4.4 with modernization fixes (C23 prototypes,
ncurses compat), preserved on master/modern-rogue
# Future Steps
1. Refactor step 4: method renames, movement/world subsystem
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms/doPassages/doMaze→digRooms/digPassages/digMaze,
chgStr→changeStrength, ...); remove the goto/label flows in doMove,
dispatch, and saveGame in favor of loops and helpers.
2. Refactor step 5: method renames, items/combat/UI subsystems
(invName→inventoryName, rollEm→rollAttacks, doPot→applyPotionFuse,
getItem→promptPackItem returning (obj, ok), getDir→promptDirection);
int status codes (attack returning -1) become named results. Two or
three commits, one subsystem each.
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
1. Playtest hardening pass: play several full games with the tcell binary and
extend run_test.go to drive a deeper multi-level playthrough (descend past
level 5, use potions, scrolls, zapping, save/restore). Note: game-over now
exits the process (step 8), so scripted drives must stay bounded and avoid
death/quit/save-command; exercise the exit paths (death display, 'S' save)
another way if needed. 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.
2. Verify the seed-compatibility claim against the C reference on c-master: same
seed, same dungeon, same item tables, for several seeds.
3. Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
wizard commands).
4. Tag a release once a full game (Amulet retrieval and score entry) completes
without defects.
5. 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.
6. Note: this repo is exempt from the standard policy scaffold. A minimal dev
Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
2026-07-07 request, but do not add a Dockerfile, CI config, or
REPO_POLICIES.md.

View File

@@ -18,84 +18,116 @@ import (
)
func main() {
os.Exit(run())
}
// run does the real work and returns an exit code. It only returns on a
// 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 {
scores := flag.Bool("s", false, "print the scoreboard and exit")
deathDemo := flag.Bool("d", false, "die a random death (demo)")
flag.Parse()
home, _ := os.UserHomeDir()
// get options from environment (main.c)
rogueOpts := os.Getenv("ROGUEOPTS")
name := ""
if u, err := user.Current(); err == nil {
name = u.Username
}
wizard := os.Getenv("ROGUE_WIZARD") != ""
// dungeon number: SEED for reproducible dungeons (wizard mode in C),
// else time+pid
var seed int32
if env := os.Getenv("SEED"); env != "" && wizard {
n, _ := strconv.Atoi(env)
seed = int32(n)
} else {
seed = int32(time.Now().Unix()) + int32(os.Getpid())
}
cfg := game.Config{
Seed: seed,
Name: name,
RogueOpts: rogueOpts,
Home: home,
ScorePath: home + "/.rogue.scores",
Wizard: wizard,
}
params := loadParams()
if *scores {
g := game.NewGame(cfg)
g.ShowScores()
return
game.New(params).ShowScores()
return 0
}
t, err := term.New()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return 1
}
defer t.Fini()
cfg.Term = t
params.Term = t
var g *game.RogueGame
if args := flag.Args(); len(args) == 1 && !*deathDemo {
// restore a saved game
g, err = game.Restore(args[0], cfg)
g, err = game.Restore(args[0], params)
if err != nil {
t.Fini()
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
fmt.Fprintln(os.Stderr, err) // deferred Fini restores the terminal
return 1
}
} else {
g = game.NewGame(cfg)
g = game.New(params)
}
if *deathDemo {
g.DeathDemo()
return
g.DeathDemo() // does not return: death exits the process
return 0
}
// SIGHUP/SIGTERM autosave (save.c auto_save)
installAutosave(g, t)
g.Run() // does not return: the game ends by exiting the process
return 0
}
// loadParams gathers the game parameters from the environment: home
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
// (main.c's startup).
func loadParams() game.Params {
home, _ := os.UserHomeDir()
name := ""
u, userErr := user.Current()
if userErr == nil {
name = u.Username
}
wizard := os.Getenv("ROGUE_WIZARD") != ""
return game.Params{
Seed: chooseSeed(wizard),
Name: name,
RogueOpts: os.Getenv("ROGUEOPTS"),
Home: home,
ScorePath: home + "/.rogue.scores",
Wizard: wizard,
}
}
// installAutosave saves the game and exits on SIGHUP/SIGTERM (save.c
// auto_save).
func installAutosave(g *game.RogueGame, t *term.Tcell) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM)
go func() {
<-sig
g.AutoSave()
t.Fini()
os.Exit(0)
}()
if err := g.Run(); err != nil {
t.Fini()
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// chooseSeed picks the dungeon number: SEED for reproducible dungeons
// (wizard mode, as in the C game), else time+pid (main.c).
func chooseSeed(wizard bool) int32 {
if env := os.Getenv("SEED"); env != "" && wizard {
n, err := strconv.ParseInt(env, 10, 32)
if err == nil {
return int32(n)
}
}
// The C game computed `lowtime + getpid()` in int; the truncation to
// 32 bits is the same wraparound the C int arithmetic performed.
return int32(time.Now().Unix()&0x7fffffff) +
int32(os.Getpid()&0x7fffffff)
}

View File

@@ -5,36 +5,47 @@ package game
// wear lets the player put armor on (armor.c wear).
func (g *RogueGame) wear() {
p := &g.Player
obj := g.getItem("wear", KindArmor)
if obj == nil {
obj, ok := g.promptPackItem("wear", KindArmor)
if !ok {
return
}
if p.CurArmor != nil {
g.addmsg("you are already wearing some")
g.addmsgf("you are already wearing some")
if !g.Options.Terse {
g.addmsg(". You'll have to take it off first")
g.addmsgf(". You'll have to take it off first")
}
g.endmsg()
g.After = false
return
}
if obj.Kind != KindArmor {
g.msg("you can't wear that")
return
}
g.wasteTime()
obj.Flags.Set(Known)
sp := g.invName(obj, true)
sp := g.inventoryName(obj, true)
p.CurArmor = obj
if !g.Options.Terse {
g.addmsg("you are now ")
g.addmsgf("you are now ")
}
g.msg("wearing %s", sp)
}
// takeOff gets the armor off of the player's back (armor.c take_off).
func (g *RogueGame) takeOff() {
p := &g.Player
obj := p.CurArmor
if obj == nil {
g.After = false
@@ -43,18 +54,23 @@ func (g *RogueGame) takeOff() {
} else {
g.msg("you aren't wearing any armor")
}
return
}
if !g.dropCheck(p.CurArmor) {
return
}
p.CurArmor = nil
if g.Options.Terse {
g.addmsg("was")
g.addmsgf("was")
} else {
g.addmsg("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).

View File

@@ -9,44 +9,61 @@ const dragonShot = 5
func (g *RogueGame) runners(int) {
list := append([]*Monster(nil), g.Level.Monsters...)
for _, tp := range list {
if !tp.On(Held) && tp.On(Awake) {
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
}
}
g.runnerTurn(tp)
}
if g.HasHit {
g.endmsg()
g.HasHit = false
}
}
// moveMonst executes a single turn of running for a monster (chase.c
// move_monst). Returns -1 if the monster died or left the level.
func (g *RogueGame) moveMonst(tp *Monster) int {
// runnerTurn gives one monster its motion for the turn; flying monsters
// far from the hero move twice (the loop body of chase.c runners).
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 g.doChase(tp) == -1 {
return -1
if g.chaseStep(tp) {
return true
}
}
if tp.On(Hasted) {
if g.doChase(tp) == -1 {
return -1
if g.chaseStep(tp) {
return true
}
}
tp.Turn = !tp.Turn
return 0
return false
}
// relocate makes the monster's new location be the specified one, updating
@@ -54,18 +71,21 @@ func (g *RogueGame) moveMonst(tp *Monster) int {
func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
if newLoc != th.Pos {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
th.Room = g.roomin(newLoc)
g.setOldch(th, newLoc)
th.Room = g.roomIn(newLoc)
g.setOldChar(th, newLoc)
oroom := th.Room
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
if oroom != th.Room {
th.Dest = g.findDest(th)
}
th.Pos = newLoc
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
}
g.move(newLoc.Y, newLoc.X)
if g.seeMonst(th) {
g.addch(th.Disguise)
} else if g.Player.On(SenseMonsters) {
@@ -75,31 +95,80 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
}
}
// doChase makes one thing chase another (chase.c do_chase). Returns -1 if
// the chaser died in the attempt.
func (g *RogueGame) doChase(th *Monster) int {
p := &g.Player
// chaseStep makes one thing chase another (chase.c do_chase). The
// result reports that the chaser died or left the level in the attempt
// (the C -1 return).
func (g *RogueGame) chaseStep(th *Monster) bool {
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
if th.On(Greedy) && rer.GoldVal == 0 {
th.Dest = &p.Pos // if gold has been taken, run after hero
}
var ree *Room // find room of chasee
if th.Dest == &p.Pos {
ree = p.Room
} 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
over:
// If the object of our desire is in a different room, and we are not
// in a corridor, run to the door nearest to our goal.
if rer != ree {
for {
// If the object of our desire is in a different room, and we are
// not in a corridor, run to the door nearest to our goal.
if rer == ree {
this = *th.Dest
return this, g.dragonBreath(th)
}
for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist {
@@ -107,79 +176,86 @@ over:
mindist = curdist
}
}
if door {
if !door {
return this, false
}
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
door = false
goto over
// the C goto over: redo with the passage as room
}
} else {
this = *th.Dest
// For dragons check and see if (a) the hero is on a straight line
// from it, and (b) that it is within shooting distance, but
// outside of striking range.
if th.Type == 'D' && (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)) &&
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
!th.On(Cancelled) && g.rnd(dragonShot) == 0 {
}
// dragonBreath checks whether a dragon shoots flame at the hero instead
// of moving, and shoots it (the D block of chase.c do_chase).
func (g *RogueGame) dragonBreath(th *Monster) bool {
if th.Type != 'D' || !g.dragonShoots(th) {
return false
}
p := &g.Player
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
g.Delta.X = sign(p.Pos.X - th.Pos.X)
if g.HasHit {
g.endmsg()
}
g.fireBolt(th.Pos, &g.Delta, "flame")
g.Running = false
g.Count = 0
g.Quiet = 0
if g.ToDeath && !th.On(Targeted) {
g.ToDeath = 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.
// If we hit it we either want to fight it or stop running
if !g.chase(th, this) {
if this == p.Pos {
return g.attack(th)
} else if this == *th.Dest {
return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
!th.On(Cancelled) && g.rnd(dragonShot) == 0
}
// chaseTakeObject has the monster pick up the object it was running to
// (the dest arm of chase.c do_chase).
func (g *RogueGame) chaseTakeObject(th *Monster) {
for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos {
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
attachObj(&th.Pack, obj)
if th.Room.Flags.Has(Gone) {
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
} else {
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
}
th.Dest = g.findDest(th)
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.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.
func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
p := &g.Player
er := tp.Pos
plcnt := 1
var curdist int
// If the thing is confused, let it move randomly. Invisible Stalkers
@@ -188,24 +264,41 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
(tp.Type == 'B' && g.rnd(2) == 0) {
// get a valid random move
g.chRet = g.rndmove(&tp.Creature)
g.chRet = g.randomStep(&tp.Creature)
curdist = distCp(g.chRet, ee)
// Small chance that it will become un-confused
if g.rnd(20) == 0 {
tp.Flags.Clear(Confused)
}
} else {
// Otherwise, find the empty spot next to the chaser that is
// 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.
curdist = distCp(er, ee)
curdist = g.chaseBestSpot(tp, ee)
}
return curdist != 0 && g.chRet != g.Player.Pos
}
// 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
ey := er.Y + 1
if ey >= NumLines-1 {
ey = NumLines - 2
}
ex := er.X + 1
if ex >= NumCols {
ex = NumCols - 1
@@ -215,58 +308,71 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if x < 0 {
continue
}
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}
if !g.diagOk(er, tryp) {
continue
if !g.diagOk(s.er, tryp) {
return
}
ch := g.Level.VisibleChar(y, x)
if stepOk(ch) {
// 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 {
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 !stepOk(ch) {
return
}
// 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
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
// wasn't a scare scroll, then this place counts
thisdist := distance(y, x, ee.Y, ee.X)
if thisdist < curdist {
plcnt = 1
// If we didn't find any scrolls at this place or it wasn't a scare
// scroll, then this place counts
thisdist := distance(y, x, s.ee.Y, s.ee.X)
if thisdist < s.curdist {
s.plcnt = 1
g.chRet = tryp
curdist = thisdist
} else if thisdist == curdist {
if plcnt++; g.rnd(plcnt) == 0 {
s.curdist = thisdist
} else if thisdist == s.curdist {
if s.plcnt++; g.rnd(s.plcnt) == 0 {
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).
func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
// scareScrollAt reports whether the object lying at (y, x) is a scare
// 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 {
return
}
sch := tp.OldCh
tp.OldCh = g.mvinch(cp.Y, cp.X)
if !g.Player.On(Blind) {
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) {
@@ -284,25 +390,30 @@ func (g *RogueGame) seeMonst(mp *Monster) bool {
if p.On(Blind) {
return false
}
if mp.On(Invisible) && !p.On(CanSeeInvisible) {
return false
}
y, x := mp.Pos.Y, mp.Pos.X
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if y != p.Pos.Y && x != p.Pos.X &&
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
return false
}
return true
}
if mp.Room != p.Room {
return false
}
return !mp.Room.Flags.Has(Dark)
}
// runto sets a monster running after the hero (chase.c runto).
func (g *RogueGame) runto(runner Coord) {
// runTo sets a monster running after the hero (chase.c runto).
func (g *RogueGame) runTo(runner Coord) {
tp := g.Level.MonsterAt(runner.Y, runner.X)
if tp == nil {
return
@@ -313,9 +424,9 @@ func (g *RogueGame) runto(runner Coord) {
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).
func (g *RogueGame) roomin(cp Coord) *Room {
func (g *RogueGame) roomIn(cp Coord) *Room {
fp := *g.Level.FlagsAt(cp.Y, cp.X)
if fp.Has(FPassage) {
return &g.Level.Passages[fp&FPassNum]
@@ -330,6 +441,7 @@ func (g *RogueGame) roomin(cp Coord) *Room {
}
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
return nil
}
@@ -338,19 +450,22 @@ func (g *RogueGame) diagOk(sp, ep Coord) bool {
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
return false
}
if ep.X == sp.X || ep.Y == sp.Y {
return true
}
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).
func (g *RogueGame) cansee(y, x int) bool {
func (g *RogueGame) canSee(y, x int) bool {
p := &g.Player
if p.On(Blind) {
return false
}
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if g.Level.FlagsAt(y, x).Has(FPassage) {
if y != p.Pos.Y && x != p.Pos.X &&
@@ -359,11 +474,13 @@ func (g *RogueGame) cansee(y, x int) bool {
return false
}
}
return true
}
// 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.
rer := g.roomin(Coord{X: x, Y: y})
rer := g.roomIn(Coord{X: x, Y: y})
return rer == p.Room && !rer.Flags.Has(Dark)
}
@@ -374,22 +491,29 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
return &g.Player.Pos
}
for _, obj := range g.Level.Objects {
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster {
continue
}
if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob {
claimed := false
for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos {
claimed = true
break
}
}
if !claimed {
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob &&
!g.objectClaimed(obj) {
return &obj.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

@@ -23,6 +23,7 @@ type Monster struct {
// in the C sources (cur_armor, purse, food_left, ...).
type Player struct {
Creature
CurArmor *Object // what he is wearing
CurWeapon *Object // which weapon he is wielding
CurRing [2]*Object // which rings are being worn (Left/Right)
@@ -49,6 +50,47 @@ func (p *Player) IsWearing(ring RingKind) bool {
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).
func attachMon(list *[]*Monster, item *Monster) {
*list = append([]*Monster{item}, *list...)
@@ -59,6 +101,7 @@ func detachMon(list *[]*Monster, item *Monster) {
for i, m := range *list {
if m == item {
*list = append((*list)[:i], (*list)[i+1:]...)
return
}
}

View File

@@ -10,6 +10,10 @@ package game
// function-pointer-to-int mapping in state.c rs_write_daemons.
type DaemonID int
// Daemon and fuse callback identifiers. The first block's numeric values
// match the function-pointer-to-int mapping in state.c rs_write_daemons;
// the second block covers fuses state.c never saved (the Go save format
// handles them all uniformly).
const (
DNone DaemonID = 0
DRollwand DaemonID = 1
@@ -21,8 +25,6 @@ const (
DUnconfuse DaemonID = 7
DUnsee DaemonID = 8
DSight DaemonID = 9
// Fuses beyond the C save map (state.c never saved these; the Go save
// format handles them uniformly).
DVisuals DaemonID = 10
DComeDown DaemonID = 11
DLand DaemonID = 12
@@ -59,6 +61,7 @@ func (g *RogueGame) dSlot() *delayedAction {
return &g.Daemons.List[i]
}
}
panic("ran out of fuse slots") // C: debug message in MASTER, NULL deref otherwise
}
@@ -70,6 +73,7 @@ func (g *RogueGame) findSlot(f DaemonID) *delayedAction {
return d
}
}
return nil
}

View File

@@ -5,38 +5,14 @@ package game
// runDaemon invokes the callback named by id (the call through d_func in C).
func (g *RogueGame) runDaemon(id DaemonID, arg int) {
switch id {
case DRollwand:
g.rollwand(arg)
case DDoctor:
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.
h := g.data.daemonHandlers[id]
if h == nil {
// Handlers are added to the table as their subsystems are
// ported; reaching one that isn't there is a porting bug.
panic("daemon not yet ported")
}
h(g, arg)
}
// doctor is the healing daemon that restores hit points after rest
@@ -45,6 +21,7 @@ func (g *RogueGame) doctor(int) {
p := &g.Player
lv := p.Stats.Lvl
ohp := p.Stats.HP
g.Quiet++
if lv < 8 {
if g.Quiet+(lv<<1) > 20 {
@@ -53,16 +30,20 @@ func (g *RogueGame) doctor(int) {
} else if g.Quiet >= 3 {
p.Stats.HP += g.rnd(lv-7) + 1
}
if p.IsRing(Left, RingRegeneration) {
p.Stats.HP++
}
if p.IsRing(Right, RingRegeneration) {
p.Stats.HP++
}
if ohp != p.Stats.HP {
if p.Stats.HP > p.Stats.MaxHP {
p.Stats.HP = p.Stats.MaxHP
}
g.Quiet = 0
}
}
@@ -82,6 +63,7 @@ func (g *RogueGame) rollwand(int) {
g.KillDaemon(DRollwand)
g.Fuse(DSwander, 0, wanderTime(g), Before)
}
g.Daemons.Between = 0
}
}
@@ -103,6 +85,7 @@ func (g *RogueGame) unsee(int) {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
}
}
g.Player.Flags.Clear(CanSeeInvisible)
}
@@ -112,9 +95,11 @@ func (g *RogueGame) sight(int) {
if p.On(Blind) {
g.Extinguish(DSight)
p.Flags.Clear(Blind)
if !p.Room.Flags.Has(Gone) {
g.enterRoom(p.Pos)
}
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
"the veil of darkness lifts"))
}
@@ -129,8 +114,27 @@ func (g *RogueGame) nohaste(int) {
// stomach digests the hero's food (daemons.c stomach).
func (g *RogueGame) stomach(int) {
p := &g.Player
origHungry := p.HungryState
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 {
g.death('s')
}
@@ -138,29 +142,41 @@ func (g *RogueGame) stomach(int) {
if g.NoCommand != 0 || g.rnd(5) != 0 {
return
}
g.NoCommand += g.rnd(8) + 4
p.HungryState = 3
if !g.Options.Terse {
g.addmsg("%s", g.chooseStr(
g.addmsgf("%s", g.chooseStr(
"the munchies overpower your motor capabilities. ",
"you feel too weak from lack of food. "))
}
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
amulet := 0
if g.HasAmulet {
amulet = 1
}
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
p.HungryState = 2
g.msg("%s", g.chooseStr(
"the munchies are interfering with your motor capabilites",
"the munchies are interfering with your motor capabilities",
"you are starting to feel weak"))
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
p.HungryState = 1
if g.Options.Terse {
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
} else {
@@ -168,13 +184,6 @@ func (g *RogueGame) stomach(int) {
"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).
@@ -193,16 +202,18 @@ func (g *RogueGame) comeDown(int) {
// undo the things
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())
}
}
// undo the monsters
seemonst := p.On(SenseMonsters)
for _, tp := range g.Level.Monsters {
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) {
g.addch(tp.Disguise)
} else {
@@ -214,41 +225,49 @@ func (g *RogueGame) comeDown(int) {
g.standend()
}
}
g.msg("Everything looks SO boring now.")
}
// visuals changes the characters for the player while hallucinating
// (daemons.c visuals).
func (g *RogueGame) visuals(int) {
p := &g.Player
if !g.After || (g.Running && g.Options.Jump) {
return
}
// change the things
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())
}
}
// 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())
}
// 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 {
g.move(tp.Pos.Y, tp.Pos.X)
if g.seeMonst(tp) {
if tp.Type == 'X' && tp.Disguise != 'X' {
g.addch(g.rndThing())
} else {
g.addch(byte(g.rnd(26) + 'A'))
g.addch(g.randomMonsterLetter())
}
} else if seemonst {
g.standout()
g.addch(byte(g.rnd(26) + 'A'))
g.addch(g.randomMonsterLetter())
g.standend()
}
}

View File

@@ -25,20 +25,26 @@ type DiceSpec []DiceRoll
// a single 0x0 attack, as it did in C.
func ParseDice(s string) DiceSpec {
var spec DiceSpec
for s != "" {
count := cAtoi(s)
xi := strings.IndexByte(s, 'x')
if xi < 0 {
break
}
s = s[xi+1:]
spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)})
si := strings.IndexByte(s, '/')
if si < 0 {
break
}
s = s[si+1:]
}
return spec
}
@@ -48,11 +54,14 @@ func dice(s string) DiceSpec { return ParseDice(s) }
// String renders the spec back in the classic "NxM/NxM" form.
func (d DiceSpec) String() string {
var sb strings.Builder
for i, r := range d {
if i > 0 {
sb.WriteByte('/')
}
fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides)
}
return sb.String()
}

View File

@@ -32,14 +32,16 @@ func TestParseDice(t *testing.T) {
// The bestiary and weapon tables must parse to at least one attack each so
// every creature and weapon actually swings.
func TestTablesHaveDice(t *testing.T) {
for i, m := range monsterTable {
data := newGameData()
for i, m := range data.monsterTable {
if len(m.Stats.Dmg) == 0 {
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 {
t.Errorf("weapon %v has empty dice", WeaponKind(w))
t.Errorf("weapon %d has empty dice", w)
}
}
}

View File

@@ -2,13 +2,16 @@ package game
import "testing"
// mkGameInput builds a headless game whose input plays the given script.
func mkGameInput(t *testing.T, seed int32, input string) *RogueGame {
// mkGameInput builds a headless game; tests script it via setInput. The
// fixed seed keeps the scripted item/monster interactions stable.
func mkGameInput(t *testing.T) *RogueGame {
t.Helper()
g := NewGame(Config{Seed: seed, Term: &testTerm{input: []byte(input)}})
g := New(Params{Seed: 5, Term: &testTerm{}})
g.NewLevel()
g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos)
g.Oldrp = g.roomIn(g.Player.Pos)
return g
}
@@ -16,42 +19,61 @@ func mkGameInput(t *testing.T, seed int32, input string) *RogueGame {
func give(g *RogueGame, obj *Object) byte {
obj.Count = 1
g.addPack(obj, true)
return obj.PackCh
}
// setInput replaces the scripted terminal input.
func setInput(t *testing.T, g *RogueGame, input ...byte) {
t.Helper()
tt, ok := g.scr.term.(*testTerm)
if !ok {
t.Fatal("game terminal is not a testTerm")
}
tt.input = input
tt.pos = 0
}
func TestQuaffHealingPotion(t *testing.T) {
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
pot := newObject()
pot.Kind = KindPotion
pot.Which = int(PotionHealing)
ch := give(g, pot)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
g.Player.Stats.HP = 1
g.quaff()
if g.Player.Stats.HP <= 1 {
t.Error("healing potion did not heal")
}
if !g.Items.Potions[PotionHealing].Know {
t.Error("healing potion not identified after drinking")
}
if len(g.Player.Pack) != 5 {
t.Errorf("potion not consumed: %d items", len(g.Player.Pack))
}
}
func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
pot := newObject()
pot.Kind = KindPotion
pot.Which = int(PotionConfusion)
ch := give(g, pot)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
g.quaff()
if !g.Player.On(Confused) {
t.Error("confusion potion did not confuse")
}
if g.findSlot(DUnconfuse) == nil {
t.Error("no unconfuse fuse pending")
}
@@ -59,21 +81,23 @@ func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
for range 30 {
g.DoFuses(After)
}
if g.Player.On(Confused) {
t.Error("confusion never wore off")
}
}
func TestReadEnchantArmor(t *testing.T) {
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
scr := newObject()
scr.Kind = KindScroll
scr.Which = int(ScrollEnchantArmor)
ch := give(g, scr)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
before := g.Player.CurArmor.ArmorClass
g.readScroll()
if g.Player.CurArmor.ArmorClass != before-1 {
t.Errorf("enchant armor: AC %d -> %d, want %d",
before, g.Player.CurArmor.ArmorClass, before-1)
@@ -86,17 +110,19 @@ func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
// look(TRUE) at the end of read_scroll immediately re-wakes greedy
// monsters. The port reproduces that quirk faithfully — see
// TestHoldScrollGreedyMonsterQuirk.
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
tp := spawnAdjacent(g, 'Z')
tp.Flags.Set(Awake)
scr := newObject()
scr.Kind = KindScroll
scr.Which = int(ScrollHoldMonster)
ch := give(g, scr)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
g.readScroll()
t.Logf("after scroll: flags=%o huh=%q", tp.Flags, g.Msgs.Huh)
if tp.On(Awake) || !tp.On(Held) {
t.Error("hold monster scroll did not hold the adjacent monster")
}
@@ -107,21 +133,24 @@ func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
// (orc) held by a scroll is re-woken by the look(TRUE) that read_scroll
// performs, ending up both held and running again.
func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
tp := spawnAdjacent(g, 'O')
tp.Flags.Set(Awake)
scr := newObject()
scr.Kind = KindScroll
scr.Which = int(ScrollHoldMonster)
ch := give(g, scr)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
g.readScroll()
t.Logf("orc after scroll: flags=%o (Awake=%v Held=%v)",
tp.Flags, tp.On(Awake), tp.On(Held))
if !tp.On(Held) {
t.Error("orc lost Held entirely")
}
if !tp.On(Awake) {
t.Error("quirk changed: greedy monster stayed held; if this is a " +
"deliberate fix, update this test and ARCHITECTURE.md")
@@ -129,41 +158,50 @@ func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
}
func TestZapSlowMonster(t *testing.T) {
g := mkGameInput(t, 5, "")
g := mkGameInput(t)
tp := spawnAdjacent(g, 'Z')
stick := newObject()
stick.Kind = KindWand
stick.Which = int(WandSlowMonster)
g.fixStick(stick)
ch := give(g, stick)
g.scr.term.(*testTerm).input = []byte{ch}
setInput(t, g, ch)
g.Delta = Coord{X: 1, Y: 0} // aim at the monster
charges := stick.Charges
g.doZap()
if !tp.On(Slowed) {
t.Error("slow monster wand did not slow")
}
if stick.Charges != charges-1 {
t.Error("zap did not use a charge")
}
}
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")
if !g.Options.Terse {
t.Error("terse not set")
}
if g.Options.Jump {
t.Error("nojump not honored")
}
if g.Whoami != "Conan" {
t.Errorf("name = %q", g.Whoami)
}
if g.Fruit != "mango" {
t.Errorf("fruit = %q", g.Fruit)
}
if g.Options.InvType != InvSlow {
t.Errorf("inven = %d", g.Options.InvType)
}

View File

@@ -4,43 +4,6 @@ import "strconv"
// 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
// set_mname).
func (g *RogueGame) setMname(tp *Monster) string {
@@ -48,20 +11,27 @@ func (g *RogueGame) setMname(tp *Monster) string {
if g.Options.Terse {
return "it"
}
return "something"
}
var mname string
if g.Player.On(Hallucinating) {
ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X))
if !isUpper(byte(ch)) {
ch = g.rnd(26)
var idx int
ch := g.mvinch(tp.Pos.Y, tp.Pos.X)
if isUpper(ch) {
idx = int(ch - 'A')
} else {
ch -= 'A'
idx = g.rnd(26)
}
mname = g.Monsters[ch].Name
mname = g.Monsters[idx].Name
} else {
mname = g.Monsters[tp.Type-'A'].Name
}
return "the " + mname
}
@@ -77,84 +47,136 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
// place.
g.Count = 0
g.Quiet = 0
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) {
tp.Disguise = 'X'
if p.On(Hallucinating) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, byte(g.rnd(26)+'A'))
}
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!"))
if !thrown {
g.runTo(mp)
if g.revealXeroc(tp) && !thrown {
return false
}
}
mname := g.setMname(tp)
didHit := false
g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) {
didHit = false
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'
if p.On(Hallucinating) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
}
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!"))
return true
}
// heroHits lands the hero's blow on a monster: messages, the confusing
// touch, and the kill check (the hit arm of fight.c fight).
func (g *RogueGame) heroHits(tp *Monster, mname string, weap *Object, thrown bool) {
p := &g.Player
confused := false
if thrown {
g.thunk(weap, mname, g.Options.Terse)
} else {
g.hit("", mname, g.Options.Terse)
}
if p.On(CanConfuse) {
didHit = true
confused = true
tp.Flags.Set(Confused)
p.Flags.Clear(CanConfuse)
g.endmsg()
g.HasHit = false
g.msg("your hands stop glowing %s", g.pickColor("red"))
}
if tp.Stats.HP <= 0 {
g.killed(tp, true)
} else if didHit && !p.On(Blind) {
} else if confused && !p.On(Blind) {
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
// the monster removed itself from the level during its own attack.
func (g *RogueGame) attack(mp *Monster) int {
// attack has the monster attack the player (fight.c attack). The result
// reports that the monster took itself off the level during its own
// attack (the C -1 return).
func (g *RogueGame) attack(mp *Monster) bool {
p := &g.Player
// Since this is an attack, stop running and any healing that was
// going on at the time.
g.Running = false
g.Count = 0
g.Quiet = 0
if g.ToDeath && !mp.On(Targeted) {
g.ToDeath = false
g.Kamikaze = false
}
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) {
mp.Disguise = 'X'
if p.On(Hallucinating) {
g.mvaddch(mp.Pos.Y, mp.Pos.X, byte(g.rnd(26)+'A'))
g.mvaddch(mp.Pos.Y, mp.Pos.X, g.randomMonsterLetter())
}
}
mname := g.setMname(mp)
oldhp := p.Stats.HP
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 g.HasHit {
g.addmsg(". ")
g.addmsgf(". ")
}
g.hit(mname, "", false)
} else if g.HasHit {
g.endmsg()
}
g.HasHit = false
if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ...
@@ -163,181 +185,274 @@ func (g *RogueGame) attack(mp *Monster) int {
if oldhp > g.MaxHit {
g.MaxHit = oldhp
}
if p.Stats.HP <= g.MaxHit {
g.ToDeath = false
}
}
if !mp.On(Cancelled) {
switch mp.Type {
case 'A':
// If an aquator hits, you can lose armor class.
g.rustArmor(p.CurArmor)
case 'I':
// The ice monster freezes you
p.Flags.Clear(Awake)
if g.NoCommand == 0 {
g.addmsg("you are frozen")
if !g.Options.Terse {
g.addmsg(" by the %s", mname)
}
g.endmsg()
}
g.NoCommand += g.rnd(2) + 2
if g.NoCommand > BoreLevel {
g.death('h')
}
case 'R':
// Rattlesnakes have poisonous bites
if !g.save(VsPoison) {
if !p.IsWearing(RingSustainStrength) {
g.chgStr(-1)
if !g.Options.Terse {
g.msg("you feel a bite in your leg and now feel weaker")
} else {
g.msg("a bite has weakened you")
}
} else if !g.ToDeath {
if !g.Options.Terse {
g.msg("a bite momentarily weakens you")
} else {
g.msg("bite has no effect")
if h := g.data.hitHandlers[mp.Type-'A']; h != nil {
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
}
case 'W', 'V':
// Wraiths might drain energy levels, and Vampires can
// steal max_hp
chance := 30
if mp.Type == 'W' {
chance = 15
}
if g.rnd(100) < chance {
var fewer int
if mp.Type == 'W' {
if p.Stats.Exp == 0 {
g.death('W') // All levels gone
}
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
p.Stats.Exp = 0
p.Stats.Lvl = 1
} else {
p.Stats.Exp = eLevels[p.Stats.Lvl-1] + 1
}
fewer = g.roll(1, 10)
} else {
fewer = g.roll(1, 3)
}
p.Stats.HP -= fewer
p.Stats.MaxHP -= fewer
if p.Stats.HP <= 0 {
p.Stats.HP = 1
}
if p.Stats.MaxHP <= 0 {
g.death(mp.Type)
}
g.msg("you suddenly feel weaker")
}
case 'F':
// Venus Flytrap stops the poor guy from moving
p.Flags.Set(Held)
p.VfHit++
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F')
}
case 'L':
// Leprechaun steals some gold
lastpurse := p.Purse
p.Purse -= g.goldCalc()
if !g.save(VsMagic) {
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
if p.Purse < 0 {
p.Purse = 0
}
g.removeMon(mp.Pos, mp, false)
removed = true
if p.Purse != lastpurse {
g.msg("your purse feels lighter")
}
case 'N':
// Nymphs steal a magic item; look through the pack and
// pick out one we like.
var steal *Object
nobj := 0
for _, obj := range p.Pack {
if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
obj.isMagic() {
if nobj++; g.rnd(nobj) == 0 {
steal = obj
}
}
}
if steal != nil {
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
removed = true
g.leavePack(steal, false, false)
g.msg("she stole %s!", g.invName(steal, true))
}
}
}
} else if mp.Type != 'I' {
p := &g.Player
if g.HasHit {
g.addmsg(". ")
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.
g.rustArmor(g.Player.CurArmor)
return false
}
func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool {
// The ice monster freezes you
g.Player.Flags.Clear(Awake)
if g.NoCommand == 0 {
g.addmsgf("you are frozen")
if !g.Options.Terse {
g.addmsgf(" by the %s", mname)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
g.endmsg()
}
g.Count = 0
g.status()
if removed {
return -1
g.NoCommand += g.rnd(2) + 2
if g.NoCommand > BoreLevel {
g.death('h')
}
return 0
return false
}
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 {
g.msg("%s", g.chooseTerse("bite has no effect",
"a bite momentarily weakens you"))
}
return false
}
func (g *RogueGame) hitLifeDrainer(mp *Monster, _ string) bool {
// Wraiths might drain energy levels, and Vampires can steal max_hp
p := &g.Player
chance := 30
if mp.Type == 'W' {
chance = 15
}
if g.rnd(100) >= chance {
return false
}
var fewer int
if mp.Type == 'W' {
if p.Stats.Exp == 0 {
g.death('W') // All levels gone
}
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
p.Stats.Exp = 0
p.Stats.Lvl = 1
} else {
p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
}
fewer = g.roll(1, 10)
} else {
fewer = g.roll(1, 3)
}
p.Stats.HP -= fewer
p.Stats.MaxHP -= fewer
if p.Stats.HP <= 0 {
p.Stats.HP = 1
}
if p.Stats.MaxHP <= 0 {
g.death(mp.Type)
}
g.msg("you suddenly feel weaker")
return false
}
func (g *RogueGame) hitFlytrap(*Monster, string) bool {
// Venus Flytrap stops the poor guy from moving
p := &g.Player
p.Flags.Set(Held)
p.VfHit++
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F')
}
return false
}
func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool {
// Leprechaun steals some gold
p := &g.Player
lastpurse := p.Purse
p.Purse -= g.goldCalc()
if !g.save(VsMagic) {
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
if p.Purse < 0 {
p.Purse = 0
}
g.removeMon(mp.Pos, mp, false)
if p.Purse != lastpurse {
g.msg("your purse feels lighter")
}
return true
}
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
nobj := 0
for _, obj := range p.Pack {
if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
g.isMagic(obj) {
if nobj++; g.rnd(nobj) == 0 {
steal = obj
}
}
}
if steal == nil {
return false
}
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
g.leavePack(steal, false, false)
g.msg("she stole %s!", g.inventoryName(steal, true))
return true
}
// swing returns true if the swing hits (fight.c swing).
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
res := g.rnd(20)
need := (20 - atLvl) - opArm
return res+wplus >= need
}
// rollEm rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player
// rollAttacks rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
att := &thatt.Stats
def := &thdef.Stats
var attacks DiceSpec
var hplus, dplus int
var (
attacks DiceSpec
hplus, dplus int
)
if weap == nil {
attacks = att.Dmg
} else {
hplus = weap.HPlus
dplus = weap.DPlus
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
}
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 p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Bonus
hplus, dplus = g.wieldedRingBonus(hplus, dplus)
}
if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus
}
}
attacks = weap.Damage
attacks := weap.Damage
if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch {
@@ -348,36 +463,52 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
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) {
dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Bonus
}
// 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
if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus
}
return hplus, dplus
}
// defenderArmor computes the defender's effective armor class: worn
// armor and protection rings when the hero defends (the def_arm
// computation of fight.c roll_em).
func (g *RogueGame) defenderArmor(thdef *Creature) int {
p := &g.Player
def := &thdef.Stats
defArm := def.ArmorClass
if def == &p.Stats {
if p.CurArmor != nil {
defArm = p.CurArmor.ArmorClass
}
if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Bonus
}
}
didHit := false
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
return defArm
}
// cAtoi parses a leading integer like C atoi: trailing non-digits are
@@ -387,7 +518,9 @@ func cAtoi(s string) int {
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
n, _ := strconv.Atoi(s[:i])
return n
}
@@ -398,9 +531,11 @@ func prname(mname string, upper bool) string {
if out == "" {
out = "you"
}
if upper {
out = string(toUpper(out[0])) + out[1:]
}
return out
}
@@ -409,12 +544,15 @@ func (g *RogueGame) thunk(weap *Object, mname string, noend bool) {
if g.ToDeath {
return
}
if weap.Kind == KindWeapon {
g.addmsg("the %s hits ", g.Items.Weapons[weap.Which].Name)
g.addmsgf("the %s hits ", g.Items.Weapons[weap.Which].Name)
} else {
g.addmsg("you hit ")
g.addmsgf("you hit ")
}
g.addmsg("%s", mname)
g.addmsgf("%s", mname)
if !noend {
g.endmsg()
}
@@ -425,7 +563,9 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
if g.ToDeath {
return
}
g.addmsg("%s", prname(er, true))
g.addmsgf("%s", prname(er, true))
var s string
if g.Options.Terse {
s = " hit"
@@ -434,12 +574,16 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
if er != "" {
i += 4
}
s = hNames[i]
s = g.data.hNames[i]
}
g.addmsg("%s", s)
g.addmsgf("%s", s)
if !g.Options.Terse {
g.addmsg("%s", prname(ee, false))
g.addmsgf("%s", prname(ee, false))
}
if !noend {
g.endmsg()
}
@@ -450,18 +594,24 @@ func (g *RogueGame) miss(er, ee string, noend bool) {
if g.ToDeath {
return
}
g.addmsg("%s", prname(er, true))
g.addmsgf("%s", prname(er, true))
i := 0
if !g.Options.Terse {
i = g.rnd(4)
}
if er != "" {
i += 4
}
g.addmsg("%s", mNames[i])
g.addmsgf("%s", g.data.mNames[i])
if !g.Options.Terse {
g.addmsg(" %s", prname(ee, false))
g.addmsgf(" %s", prname(ee, false))
}
if !noend {
g.endmsg()
}
@@ -472,12 +622,15 @@ func (g *RogueGame) bounce(weap *Object, mname string, noend bool) {
if g.ToDeath {
return
}
if weap.Kind == KindWeapon {
g.addmsg("the %s misses ", g.Items.Weapons[weap.Which].Name)
g.addmsgf("the %s misses ", g.Items.Weapons[weap.Which].Name)
} else {
g.addmsg("you missed ")
g.addmsgf("you missed ")
}
g.addmsg("%s", mname)
g.addmsgf("%s", mname)
if !noend {
g.endmsg()
}
@@ -489,15 +642,19 @@ func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
for _, obj := range pack {
obj.Pos = tp.Pos
detachObj(&tp.Pack, obj)
if waskill {
g.fall(obj, false)
}
}
g.Level.SetMonsterAt(mp.Y, mp.X, nil)
g.mvaddch(mp.Y, mp.X, tp.OldCh)
detachMon(&g.Level.Monsters, tp)
g.Level.RemoveMonster(tp)
if tp.On(Targeted) {
g.Kamikaze = false
g.ToDeath = false
if g.Options.FightFlush {
g.flushType()
@@ -510,6 +667,38 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
p := &g.Player
p.Stats.Exp += tp.Stats.Exp
g.killedSpecial(tp)
// Get rid of the monster.
mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true)
if pr {
if g.HasHit {
g.addmsgf(". Defeated ")
g.HasHit = false
} else {
if !g.Options.Terse {
g.addmsgf("you have ")
}
g.addmsgf("defeated ")
}
g.msg("%s", mname)
}
// Do adjustments if he went up a level
g.checkLevel()
if g.Options.FightFlush {
g.flushType()
}
}
// 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':
@@ -521,36 +710,19 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
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.
mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true)
if pr {
if g.HasHit {
g.addmsg(". Defeated ")
g.HasHit = false
} else {
if !g.Options.Terse {
g.addmsg("you have ")
}
g.addmsg("defeated ")
}
g.msg("%s", mname)
}
// Do adjustments if he went up a level
g.checkLevel()
if g.Options.FightFlush {
g.flushType()
}
}
// flushType flushes typeahead for the fight_flush option (mach_dep.c

View File

@@ -6,10 +6,12 @@ import "testing"
// look() state the way playit() does before the first command.
func mkGame(t *testing.T, seed int32) *RogueGame {
t.Helper()
g := NewGame(Config{Seed: seed, Term: &testTerm{}})
g := New(Params{Seed: seed, Term: &testTerm{}})
g.NewLevel()
g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos)
g.Oldrp = g.roomIn(g.Player.Pos)
return g
}
@@ -18,6 +20,7 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster {
pos := Coord{X: g.Player.Pos.X + 1, Y: g.Player.Pos.Y}
tp := &Monster{}
g.newMonster(tp, typ, pos)
return tp
}
@@ -29,9 +32,10 @@ func TestRollEmParsesMultiAttackDice(t *testing.T) {
// With attacker level 20 vs armor 10, swing always hits
// (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.
if !g.rollEm(att, def, nil, false) {
if !g.rollAttacks(att, def, nil, false) {
t.Fatal("attack with guaranteed swing missed")
}
dmg := 1000 - def.Stats.HP
if dmg < 6 || dmg > 15 {
t.Errorf("three 1x4+1 attacks dealt %d damage, want 6..15", dmg)
@@ -45,12 +49,15 @@ func TestFightKillsMonster(t *testing.T) {
g.Player.Stats.Lvl = 20 // always hits
before := len(g.Level.Monsters)
g.fight(tp.Pos, g.Player.CurWeapon, false)
if len(g.Level.Monsters) != before-1 {
t.Error("monster not removed after fatal fight")
}
if g.Level.MonsterAt(tp.Pos.Y, tp.Pos.X) != nil {
t.Error("map still records dead monster")
}
if g.Player.Stats.Exp == 0 {
t.Error("no experience for the kill")
}
@@ -61,45 +68,36 @@ func TestAttackHurtsPlayer(t *testing.T) {
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
tp.Stats.Lvl = 20 // always hits
tp.Flags.Clear(Cancelled)
hpBefore := g.Player.Stats.HP
g.Player.Stats.HP = 500
g.Player.Stats.MaxHP = 500
g.attack(tp)
if g.Player.Stats.HP >= 500 {
t.Errorf("player HP unchanged (%d -> %d)", hpBefore, g.Player.Stats.HP)
}
}
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) {
g := mkGame(t, 3)
// Place a hobgoblin a few squares away in the hero's room and set it
// running at the hero.
p := &g.Player
pos := Coord{X: p.Pos.X + 3, Y: p.Pos.Y}
if !stepOk(g.Level.Char(pos.Y, pos.X)) || g.Level.MonsterAt(pos.Y, pos.X) != nil {
t.Skip("no clear lane on this seed")
}
tp := &Monster{}
g.newMonster(tp, 'H', pos)
tp.Flags.Set(Awake)
tp.Dest = &p.Pos
d0 := distCp(tp.Pos, p.Pos)
g.runners(0)
if d1 := distCp(tp.Pos, p.Pos); d1 >= d0 {
t.Errorf("monster did not close distance: %d -> %d", d0, d1)
}

View File

@@ -31,9 +31,9 @@ type Options struct {
InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
}
// Config carries everything needed to construct a game.
type Config struct {
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
// Params carries everything needed to construct a game.
type Params struct {
Seed int32 // dungeon number; caller derives it (time+pid or SEED)
Name string // player name (overridden by ROGUEOPTS name=)
RogueOpts string // the ROGUEOPTS environment string
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
// 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
// code owning them.
@@ -110,7 +110,7 @@ type RogueGame struct {
// screen / messages
scr *Screen
Msgs MsgLine
Msgs MessageLine
statusCache statusCache
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
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
// and first level arrive with later porting phases).
func NewGame(cfg Config) *RogueGame {
func New(params Params) *RogueGame {
g := &RogueGame{
Rng: &Rng{Seed: cfg.Seed},
Dnum: int(cfg.Seed),
Whoami: cfg.Name,
data: newGameData(),
Rng: &Rng{Seed: params.Seed},
Dnum: int(params.Seed),
Whoami: params.Name,
Fruit: "slime-mold",
Home: cfg.Home,
Wizard: cfg.Wizard,
NoScore: cfg.Wizard,
Home: params.Home,
Wizard: params.Wizard,
NoScore: params.Wizard,
Playing: true,
Depth: 1,
ScorePath: cfg.ScorePath,
ScorePath: params.ScorePath,
LastScore: -1,
}
g.Options = Options{
@@ -164,17 +168,21 @@ func NewGame(cfg Config) *RogueGame {
}
g.InvDescribe = true
g.Msgs.SaveMsg = true
g.scr = NewScreen(cfg.Term)
g.FileName = cfg.Home + "/rogue.save"
g.rogueOpts = cfg.RogueOpts
if cfg.Wizard {
g.scr = NewScreen(params.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.FileName = params.Home + "/rogue.save"
g.rogueOpts = params.RogueOpts
if params.Wizard {
g.Player.Flags.Set(SenseMonsters)
}
if cfg.RogueOpts != "" {
g.ParseOpts(cfg.RogueOpts)
if params.RogueOpts != "" {
g.ParseOpts(params.RogueOpts)
}
g.Monsters = monsterTable
g.Monsters = g.data.monsterTable
g.Items.Group = 2 // weapons.c: int group = 2
for i := range g.Level.Passages {
g.Level.Passages[i].Flags = Gone | Dark
@@ -186,34 +194,50 @@ func NewGame(cfg Config) *RogueGame {
g.initColors() // set up colors of potions
g.initStones() // set up stone settings of rings
g.initMaterials() // set up materials of wands
return g
}
// Run plays the game to its end: the back half of main.c main() plus
// playit(). It returns after death, victory, quitting, or saving.
func (g *RogueGame) Run() (err error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(gameEnd); ok {
return // normal game over / save exit
// playit(). It does not return — the game ends by exiting the process
// (see myExit); one game run is one process.
func (g *RogueGame) Run() {
g.startLevel()
g.playit()
}
// startLevel draws the first level and starts the standing daemons and
// 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
// Start up daemons and fuses
g.StartDaemon(DRunners, 0, After)
g.StartDaemon(DDoctor, 0, After)
g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After)
}
g.playit()
return nil
}
// playit is the main loop of the program (main.c 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
// always true, so the C default inventory style applies
if !g.restored {
@@ -226,11 +250,7 @@ func (g *RogueGame) playit() {
}
g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos)
for g.Playing {
g.command() // command execution
}
g.endit()
g.Oldrp = g.roomIn(g.Player.Pos)
}
// endit exits the game (main.c endit).
@@ -242,7 +262,7 @@ func (g *RogueGame) endit() {
func (g *RogueGame) fatal(s string) {
g.mvaddstr(NumLines-2, 0, s)
g.refresh()
g.myExit(0)
g.myExit()
}
// quit has the player make certain, then exits (main.c quit). The final
@@ -252,17 +272,21 @@ func (g *RogueGame) quit(int) {
if !g.QComm {
g.Msgs.Mpos = 0
}
oy, ox := g.scr.Std.GetYX()
g.msg("really quit?")
if g.readchar() == 'y' {
g.clear()
g.scr.Std.MvPrintw(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
g.scr.Std.MvPrintwf(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
g.move(NumLines-1, 0)
g.refresh()
g.score(g.Player.Purse, 1, 0)
g.myExit(0)
g.myExit()
return
}
g.move(0, 0)
g.clrtoeol()
g.status()

View File

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

View File

@@ -10,9 +10,11 @@ import (
// maxMsg is io.c MAXMSG: how much message fits before --More--.
const maxMsg = NumCols - len("--More--") - 1
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus
// the related globals (mpos, huh, and the message-behavior flags).
type MsgLine struct {
// MessageLine is the io.c message machinery: the static msgbuf/newpos
// pair plus the related globals (mpos, huh, and the message-behavior
// 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
newpos int
Mpos int // where cursor is on top line
@@ -20,84 +22,137 @@ type MsgLine struct {
SaveMsg bool // remember last msg
LowerMsg bool // messages should start w/lower case
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
// Escape if the player escaped out of a --More--, ^Escape otherwise (the C
// convention: callers compare against ESCAPE).
func (g *RogueGame) msg(format string, a ...any) int {
// Escape if the player escaped out of a --More--, ^Escape otherwise (the
// C convention: callers compare against ESCAPE).
func (m *MessageLine) Msg(format string, a ...any) int {
// if the string is "", just clear the line
if format == "" {
g.move(0, 0)
g.clrtoeol()
g.Msgs.Mpos = 0
m.scr.Std.Move(0, 0)
m.scr.Std.Clrtoeol()
m.Mpos = 0
return ^Escape
}
// otherwise add to the message and flush it out
g.doadd(format, a...)
return g.endmsg()
m.doaddf(format, a...)
return m.End()
}
// addmsg adds things to the current message (io.c addmsg).
func (g *RogueGame) addmsg(format string, a ...any) {
g.doadd(format, a...)
// Addf adds things to the current message (io.c addmsg).
func (m *MessageLine) Addf(format string, a ...any) {
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).
func (g *RogueGame) endmsg() int {
m := &g.Msgs
func (m *MessageLine) End() int {
if m.SaveMsg {
m.Huh = m.buf.String()
}
if m.Mpos != 0 {
g.look(false)
g.mvaddstr(0, m.Mpos, "--More--")
g.refresh()
if !m.MsgEsc {
g.waitFor(' ')
} else {
for {
ch := g.readchar()
if ch == ' ' {
break
}
if ch == Escape {
m.buf.Reset()
m.Mpos = 0
m.newpos = 0
if m.Mpos != 0 && m.promptMore() == Escape {
return Escape
}
}
}
}
// 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] == ')') {
(len(out) <= 1 || out[1] != ')') {
out = string(toUpper(out[0])) + out[1:]
}
g.mvaddstr(0, 0, out)
g.clrtoeol()
m.scr.Std.MvAddStr(0, 0, out)
m.scr.Std.Clrtoeol()
m.Mpos = m.newpos
m.newpos = 0
m.buf.Reset()
g.refresh()
m.scr.Refresh()
return ^Escape
}
// doadd performs an add onto the message buffer (io.c doadd).
func (g *RogueGame) doadd(format string, a ...any) {
m := &g.Msgs
// 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 {
m.waitForSpace()
return ^Escape
}
for {
ch := m.readChar()
if ch == ' ' {
return ^Escape
}
if ch == Escape {
m.buf.Reset()
m.Mpos = 0
m.newpos = 0
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
}
}
}
// doaddf performs an add onto the message buffer (io.c doadd).
func (m *MessageLine) doaddf(format string, a ...any) {
s := fmt.Sprintf(format, a...)
if len(s)+m.newpos >= maxMsg {
g.endmsg()
m.End()
}
m.buf.WriteString(s)
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).
func stepOk(ch byte) bool {
switch ch {
@@ -114,8 +169,10 @@ func (g *RogueGame) readchar() byte {
ch := g.scr.term.ReadChar()
if ch == 3 { // ^C
g.quit(0)
return 27
}
return ch
}
@@ -133,8 +190,6 @@ type statusCache struct {
init bool
}
var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"}
// status displays the important stats line, keeping the cursor where it was
// (io.c status).
func (g *RogueGame) status() {
@@ -146,17 +201,19 @@ func (g *RogueGame) status() {
if p.CurArmor != nil {
temp = p.CurArmor.ArmorClass
}
if 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 {
if g.statusUnchanged(temp) {
return
}
s.init = true
s.arm = temp
oy, ox := g.scr.Std.GetYX()
if s.hp != p.Stats.MaxHP {
s.hp = p.Stats.MaxHP
s.hpwidth = 0
for t := p.Stats.MaxHP; t != 0; t /= 10 {
s.hpwidth++
@@ -175,7 +232,7 @@ func (g *RogueGame) status() {
"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,
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 {
g.move(0, 0)
g.msg("%s", line)
@@ -183,10 +240,23 @@ func (g *RogueGame) status() {
g.move(StatLine, 0)
g.addstr(line)
}
g.clrtoeol()
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).
func (g *RogueGame) waitFor(ch byte) {
if ch == '\n' {
@@ -197,7 +267,11 @@ func (g *RogueGame) waitFor(ch byte) {
}
}
}
for g.readchar() != ch {
for {
if g.readchar() == ch {
return
}
}
}
@@ -221,11 +295,13 @@ func toUpper(c byte) byte {
if isLower(c) {
return c - 'a' + 'A'
}
return c
}
func toLower(c byte) byte {
if isUpper(c) {
return c - 'A' + 'a'
}
return c
}

View File

@@ -47,9 +47,33 @@ func (l *Level) VisibleChar(y, x int) byte {
if m := l.MonsterAt(y, x); m != nil {
return m.Disguise
}
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.
func (g *RogueGame) goldCalc() int {
return g.rnd(50+10*g.Depth) + 2

View File

@@ -4,84 +4,151 @@ package game
// and small utilities. call_it arrives with the scroll/potion phase (it
// 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).
func (g *RogueGame) look(wakeup bool) {
p := &g.Player
hero := p.Pos
passcount := 0
rp := p.Room
if g.Oldpos != hero {
g.eraseLamp(g.Oldpos, g.Oldrp)
g.Oldpos = hero
g.Oldrp = rp
}
ey := hero.Y + 1
ex := hero.X + 1
sx := hero.X - 1
sy := hero.Y - 1
sumhero, diffhero := 0, 0
if g.DoorStop && !g.Firstmove && g.Running {
sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X
}
pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch
pfl := pp.Flags
for y := sy; y <= ey; y++ {
s := lookScan{
hero: hero,
wakeup: wakeup,
sy: hero.Y - 1,
sx: hero.X - 1,
ey: hero.Y + 1,
ex: hero.X + 1,
}
s.doorStop = g.DoorStop && !g.Firstmove
if s.doorStop && g.Running {
s.sumhero = hero.Y + hero.X
s.diffhero = hero.Y - hero.X
}
pp := g.Level.At(hero.Y, hero.X)
s.pch = pp.Ch
s.pfl = pp.Flags
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 {
continue
}
for x := sx; x <= ex; x++ {
for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols {
continue
}
if !p.On(Blind) {
if y == hero.Y && x == hero.X {
continue
}
}
g.lookCell(s, y, x)
}
}
}
// 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)
ch := pp.Ch
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
}
if g.lookSkips(s, pp, y, x) {
return
}
tp := pp.Monst
if tp == nil {
ch = g.tripCh(y, x, ch)
} else if p.On(SenseMonsters) && tp.On(Invisible) {
if g.DoorStop && !g.Firstmove {
g.Running = false
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
if skip {
return
}
continue
} else {
if wakeup {
g.wakeMonster(y, x)
if !g.lookShow(s, tp, ch, y, x) {
return
}
if g.seeMonst(tp) {
if p.On(Hallucinating) {
ch = byte(g.rnd(26) + 'A')
} else {
ch = tp.Disguise
if s.doorStop && g.Running {
g.lookRunCheck(s, ch, y, x)
}
}
// 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 pp.Ch == ' ' { // nothing need be done with a ' '
return true
}
if p.On(Blind) && (y != hero.Y || x != hero.X) {
continue
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)
@@ -94,63 +161,88 @@ func (g *RogueGame) look(wakeup bool) {
g.addch(ch)
}
if g.DoorStop && !g.Firstmove && g.Running {
switch g.RunCh {
case 'h':
if x == ex {
continue
return true
}
// lookCellChar picks what the square shows: trip rendering for empty
// 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 {
continue
return ch, true
default:
if s.wakeup {
g.wakeMonster(y, x)
}
case 'k':
if y == ey {
continue
if g.seeMonst(tp) {
if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
}
case 'l':
if x == sx {
continue
return tp.Disguise, false
}
case 'y':
if (y+x)-sumhero >= 1 {
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
return ch, false
}
}
// 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 {
case Door:
if x == hero.X || y == hero.Y {
if x == s.hero.X || y == s.hero.Y {
g.Running = false
}
case Passage:
if x == hero.X || y == hero.Y {
passcount++
if x == s.hero.X || y == s.hero.Y {
s.passcount++
}
case Floor, '|', '-', ' ':
default:
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 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
return false
}
// tripCh returns the character for this space, taking into account whether
@@ -165,26 +257,30 @@ func (g *RogueGame) tripCh(y, x int, ch byte) byte {
}
}
}
return ch
}
// eraseLamp erases the area shown by a lamp in a dark room
// (misc.c erase_lamp).
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
if !(g.Options.SeeFloor && rp.Flags&(Gone|Dark) == Dark &&
!g.Player.On(Blind)) {
if !g.Options.SeeFloor || rp.Flags&(Gone|Dark) != Dark ||
g.Player.On(Blind) {
return
}
ey := pos.Y + 1
ex := pos.X + 1
sy := pos.Y - 1
for x := pos.X - 1; x <= ex; x++ {
for y := sy; y <= ey; y++ {
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
continue
}
g.move(y, x)
if g.inch() == Floor {
g.addch(' ')
}
@@ -198,53 +294,53 @@ func (g *RogueGame) showFloor() bool {
if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) {
return g.Options.SeeFloor
}
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
return true
}
// eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() {
obj := g.getItem("eat", KindFood)
if obj == nil {
obj, ok := g.promptPackItem("eat", KindFood)
if !ok {
return
}
if obj.Kind != KindFood {
if !g.Options.Terse {
g.msg("ugh, you would get ill if you ate that")
} else {
g.msg("that's Inedible!")
}
return
}
p := &g.Player
if p.FoodLeft < 0 {
p.FoodLeft = 0
}
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
p.FoodLeft = StomachSize
}
p.HungryState = 0
if obj == p.CurWeapon {
p.CurWeapon = nil
}
if obj.Which == 1 {
switch {
case obj.Which == 1:
g.msg("my, that was a yummy %s", g.Fruit)
} else if g.rnd(100) > 70 {
case g.rnd(100) > 70:
p.Stats.Exp++
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
g.checkLevel()
} else {
default:
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
}
g.leavePack(obj, false, false)
}
@@ -252,38 +348,46 @@ func (g *RogueGame) eat() {
// check_level).
func (g *RogueGame) checkLevel() {
p := &g.Player
var i int
for i = 0; eLevels[i] != 0; i++ {
if eLevels[i] > p.Stats.Exp {
for i = 0; g.data.eLevels[i] != 0; i++ {
if g.data.eLevels[i] > p.Stats.Exp {
break
}
}
i++
olevel := p.Stats.Lvl
p.Stats.Lvl = i
if i > olevel {
add := g.roll(i-olevel, 10)
p.Stats.MaxHP += add
p.Stats.HP += add
g.msg("welcome to level %d", i)
}
}
// chgStr modifies the player's strength, keeping track of the highest it
// has been (misc.c chg_str).
func (g *RogueGame) chgStr(amt int) {
// changeStrength modifies the player's strength, keeping track of the
// highest it has been (misc.c chg_str).
func (g *RogueGame) changeStrength(amt int) {
if amt == 0 {
return
}
p := &g.Player
addStr(&p.Stats.Str, amt)
comp := p.Stats.Str
if p.IsRing(Left, RingAddStrength) {
addStr(&comp, -p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&comp, -p.CurRing[Right].Bonus)
}
if comp > p.MaxStats.Str {
p.MaxStats.Str = comp
}
@@ -303,24 +407,29 @@ func (g *RogueGame) addHaste(potion bool) bool {
p := &g.Player
if p.On(Hasted) {
g.NoCommand += g.rnd(8)
p.Flags.Clear(Awake | Hasted)
g.Extinguish(DNohaste)
g.msg("you faint from exhaustion")
return false
}
p.Flags.Set(Hasted)
if potion {
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
}
return true
}
// aggravate aggravates all the monsters on this level (misc.c 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...)
for _, mp := range monsters {
g.runto(mp.Pos)
g.runTo(mp.Pos)
}
}
@@ -330,10 +439,12 @@ func vowelstr(str string) string {
if str == "" {
return ""
}
switch str[0] {
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
return "n"
}
return ""
}
@@ -343,21 +454,25 @@ func (g *RogueGame) isCurrent(obj *Object) bool {
if obj == nil {
return false
}
p := &g.Player
if obj == p.CurArmor || obj == p.CurWeapon ||
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
if !g.Options.Terse {
g.addmsg("That's already ")
g.addmsgf("That's already ")
}
g.msg("in use")
return true
}
return false
}
// getDir sets up the direction coordinate for use in various "prefix"
// commands (misc.c get_dir).
func (g *RogueGame) getDir() bool {
// promptDirection sets up the direction coordinate for use in various
// "prefix" commands (misc.c get_dir).
func (g *RogueGame) promptDirection() bool {
if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt
g.DirCh = g.LastDir
@@ -367,53 +482,76 @@ func (g *RogueGame) getDir() bool {
prompt = "which direction? "
g.msg("%s", prompt)
}
for {
gotit := true
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.DirCh = g.readchar()
if g.DirCh == Escape {
g.LastDir = 0
g.resetLast()
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
}
g.Msgs.Mpos = 0
g.msg("%s", prompt)
}
g.DirCh = toLower(g.DirCh)
g.LastDir = g.DirCh
g.lastDelt = g.Delta
}
if g.Player.On(Confused) && g.rnd(5) == 0 {
g.confuseDirection()
}
g.Msgs.Mpos = 0
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 {
break
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
}
g.Msgs.Mpos = 0
return true
return Coord{}, false
}
// callIt calls an object something after use (misc.c call_it).
@@ -422,6 +560,7 @@ func (g *RogueGame) callIt(info *ObjInfo) {
info.Guess = ""
} else if info.Guess == "" {
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
if buf != "" {
@@ -431,21 +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
// rnd_thing).
func (g *RogueGame) rndThing() byte {
var i int
if g.Depth >= AmuletLevel {
i = g.rnd(len(thingList))
i = g.rnd(len(g.data.thingList))
} 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
@@ -454,6 +589,7 @@ func (g *RogueGame) chooseStr(ts, ns string) string {
if g.Player.On(Hallucinating) {
return ts
}
return ns
}
@@ -463,8 +599,10 @@ func unctrl(ch byte) string {
if ch < ' ' {
return "^" + string(ch+'@')
}
if ch == 0x7f {
return "^?"
}
return string(ch)
}

View File

@@ -2,33 +2,24 @@ package game
// 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
// the monster (monsters.c randmonster).
func (g *RogueGame) randMonster(wander bool) byte {
mons := &lvlMons
mons := &g.data.lvlMons
if wander {
mons = &wandMons
mons = &g.data.wandMons
}
for {
d := g.Depth + (g.rnd(10) - 6)
if d < 0 {
d = g.rnd(5)
}
if d > 25 {
d = g.rnd(5) + 21
}
if mons[d] != 0 {
return mons[d]
}
@@ -38,17 +29,15 @@ func (g *RogueGame) randMonster(wander bool) byte {
// newMonster picks a new monster and adds it to the list (monsters.c
// new_monster).
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
levAdd := g.Depth - AmuletLevel
if levAdd < 0 {
levAdd = 0
}
attachMon(&g.Level.Monsters, tp)
levAdd := max(g.Depth-AmuletLevel, 0)
g.Level.AddMonster(tp)
tp.Type = typ
tp.Disguise = typ
tp.Pos = cp
g.move(cp.Y, cp.X)
tp.OldCh = g.inch()
tp.Room = g.roomin(cp)
tp.Room = g.roomIn(cp)
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
mp := &g.Monsters[tp.Type-'A']
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
@@ -58,15 +47,19 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.Stats.Dmg = mp.Stats.Dmg
tp.Stats.Str = mp.Stats.Str
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
tp.Flags = mp.Flags
if g.Depth > 29 {
tp.Flags.Set(Hasted)
}
tp.Turn = true
tp.Pack = nil
if g.Player.IsWearing(RingAggravateMonsters) {
g.runto(cp)
g.runTo(cp)
}
if typ == 'X' {
tp.Disguise = g.rndThing()
}
@@ -81,11 +74,13 @@ func expAdd(tp *Monster) int {
} else {
mod = tp.Stats.MaxHP / 6
}
if tp.Stats.Lvl > 9 {
mod *= 20
} else if tp.Stats.Lvl > 6 {
mod *= 4
}
return mod
}
@@ -93,73 +88,115 @@ func expAdd(tp *Monster) int {
// (monsters.c wanderer).
func (g *RogueGame) wanderer() {
tp := &Monster{}
var cp Coord
for {
cp, _ = g.findFloor(nil, 0, true)
if g.roomin(cp) != g.Player.Room {
cp, _ = g.findFloor(true)
if g.roomIn(cp) != g.Player.Room {
break
}
}
g.newMonster(tp, g.randMonster(true), cp)
if g.Player.On(SenseMonsters) {
g.standout()
if !g.Player.On(Hallucinating) {
g.addch(tp.Type)
} else {
g.addch(byte(g.rnd(26) + 'A'))
g.addch(g.randomMonsterLetter())
}
g.standend()
}
g.runto(tp.Pos)
g.runTo(tp.Pos)
}
// wakeMonster is what to do when the hero steps next to a monster
// (monsters.c wake_monster).
func (g *RogueGame) wakeMonster(y, x int) *Monster {
func (g *RogueGame) wakeMonster(y, x int) {
p := &g.Player
tp := g.Level.MonsterAt(y, x)
if tp == nil {
panic("can't find monster in wake_monster")
}
ch := tp.Type
// 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) &&
!p.IsWearing(RingStealth) && !p.On(Levitating) {
if g.meanWakes(tp) {
tp.Dest = &p.Pos
tp.Flags.Set(Awake)
}
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) {
rp := p.Room
if (rp != nil && !rp.Flags.Has(Dark)) ||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
tp.Flags.Set(Found)
if !g.save(VsMagic) {
if p.On(Confused) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else {
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
}
p.Flags.Set(Confused)
mname := g.setMname(tp)
g.addmsg("%s", mname)
if mname != "it" {
g.addmsg("'")
}
g.msg("s gaze has confused you")
}
}
if g.medusaCatches(tp) {
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
}
}
return tp
}
// 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
if (rp == nil || rp.Flags.Has(Dark)) &&
distance(y, x, p.Pos.Y, p.Pos.X) >= LampDist {
return
}
tp.Flags.Set(Found)
if g.save(VsMagic) {
return
}
if p.On(Confused) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else {
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
}
p.Flags.Set(Confused)
mname := g.setMname(tp)
g.addmsgf("%s", mname)
if mname != "it" {
g.addmsgf("'")
}
g.msg("s gaze has confused you")
}
// givePack gives a pack to a monster if it deserves one (monsters.c
@@ -174,6 +211,7 @@ func (g *RogueGame) givePack(tp *Monster) {
// save_throw).
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
need := 14 + which - st.Lvl/2
return g.roll(1, 20) >= need
}
@@ -185,9 +223,17 @@ func (g *RogueGame) save(which int) bool {
if p.IsRing(Left, RingProtection) {
which -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
which -= p.CurRing[Right].Bonus
}
}
return g.saveThrow(which, &p.Stats)
}
// randomMonsterLetter picks a random monster display letter, used by the
// hallucination effects (the C rnd(26)+'A' idiom).
func (g *RogueGame) randomMonsterLetter() byte {
return byte(g.rnd(26) + 'A') //nolint:gosec // G115: 'A'..'Z' fits a byte
}

View File

@@ -2,55 +2,148 @@ package game
// move.c — hero movement commands.
// doRun starts the hero running (move.c do_run).
func (g *RogueGame) doRun(ch byte) {
// startRun starts the hero running (move.c do_run).
func (g *RogueGame) startRun(ch byte) {
g.Running = true
g.After = false
g.RunCh = ch
}
// doMove checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move).
func (g *RogueGame) doMove(dy, dx int) {
// moveHero checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move). The C `goto over`
// re-check after a passage turn is the retry loop.
func (g *RogueGame) moveHero(dy, dx int) {
p := &g.Player
g.Firstmove = false
if g.NoMove > 0 {
g.NoMove--
g.msg("you are still stuck in the bear trap")
return
}
// Do a confused move (maybe)
var nh Coord
if p.On(Confused) && g.rnd(5) != 0 {
nh = g.rndmove(&p.Creature)
nh = g.randomStep(&p.Creature)
if nh == p.Pos {
g.After = false
g.Running = false
g.ToDeath = false
return
}
} else {
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
}
over:
// Check if he tried to move off the screen or make an illegal diagonal
// move, and stop him if he did.
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
var ch byte
var fl PlaceFlags
if !hitBound {
for {
ch, fl, stop := g.moveTarget(nh)
if stop {
return
}
turned, ndy, ndx := g.moveResolve(nh, ch, fl, dy, dx)
if !turned {
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 !g.diagOk(p.Pos, nh) {
g.After = false
g.Running = false
return
return 0, 0, true
}
if g.Running && p.Pos == nh {
g.After = false
g.Running = false
}
fl = *g.Level.FlagsAt(nh.Y, nh.X)
ch = g.Level.VisibleChar(nh.Y, nh.X)
fl := *g.Level.FlagsAt(nh.Y, nh.X)
ch := g.Level.VisibleChar(nh.Y, nh.X)
if !fl.Has(FReal) && ch == Floor {
if !p.On(Levitating) {
ch = Trap
@@ -59,82 +152,21 @@ over:
}
} else if p.On(Held) && ch != 'F' {
g.msg("you are being held")
return
return 0, 0, true
}
}
if hitBound {
ch = ' ' // fall into the wall case below
}
switch ch {
case ' ', '|', '-':
if g.Options.PassGo && g.Running && p.Room.Flags.Has(Gone) &&
!p.On(Blind) {
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:
return ch, fl, false
}
// moveOnto handles stepping at a monster or onto an item (the default
// arm of the move.c do_move switch).
func (g *RogueGame) moveOnto(nh Coord, fl PlaceFlags, ch byte) {
p := &g.Player
if ch == Stairs {
g.SeenStairs = true
}
g.Running = false
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
g.fight(nh, p.CurWeapon, false)
@@ -142,18 +174,90 @@ over:
if ch != Stairs {
g.Take = ch
}
g.moveStuff(nh, fl)
}
g.finishMove(nh, fl)
}
}
// moveStuff is the move_stuff label in do_move: complete the step.
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
// passageTurn checks whether a runner in a gone-room passage should turn
// 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
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
if fl.Has(FPassage) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
g.leaveRoom(nh)
}
p.Pos = nh
}
@@ -161,17 +265,21 @@ func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
// (move.c turn_ok).
func (g *RogueGame) turnOk(y, x int) bool {
pp := g.Level.At(y, x)
return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage)
}
// turnref decides whether to refresh at a passage turning (move.c turnref).
func (g *RogueGame) turnref() {
// turnRefresh decides whether to refresh at a passage turning (move.c
// turnref).
func (g *RogueGame) turnRefresh() {
p := &g.Player
pp := g.Level.At(p.Pos.Y, p.Pos.X)
if !pp.Flags.Has(FSeen) {
if g.Options.Jump {
g.refresh()
}
pp.Flags.Set(FSeen)
}
}
@@ -182,6 +290,7 @@ func (g *RogueGame) doorOpen(rp *Room) {
if rp.Flags.Has(Gone) {
return
}
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
if isUpper(g.Level.VisibleChar(y, x)) {
@@ -191,42 +300,68 @@ func (g *RogueGame) doorOpen(rp *Room) {
}
}
// beTrapped makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) beTrapped(tc Coord) TrapKind {
// springTrap makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) springTrap(tc Coord) TrapKind {
p := &g.Player
if p.On(Levitating) {
return TrapRust // anything that's not a door or teleport
}
g.Running = false
g.Count = 0
pp := g.Level.At(tc.Y, tc.X)
pp.Ch = Trap
tr := TrapKind(pp.Flags & FTrapMask)
pp.Flags.Set(FSeen)
switch tr {
case TrapDoor:
if h := g.data.trapHandlers[tr]; h != nil {
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.NewLevel()
g.msg("you fell into a trap!")
case TrapBear:
}
func (g *RogueGame) trapBear(Coord) {
g.NoMove += g.spread(3) // BEARTIME
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:
g.msg("you are suddenly in a parallel dimension")
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:
g.msg("you feel a sting in the side of your neck")
case 3:
g.msg("multi-colored lines swirl around you, then fade")
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:
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:
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:
g.msg("you suddenly feel very thirsty")
case 8:
@@ -234,13 +369,19 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
case 9:
g.msg("time now seems to be going slower")
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
p.Flags.Clear(Awake)
g.Player.Flags.Clear(Awake)
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) {
p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 {
@@ -257,36 +398,44 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
g.fall(arrow, false)
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,
// so we have to do it ourself
g.teleport()
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) {
g.msg("a small dart whizzes by your ear and vanishes")
} else {
return
}
p.Stats.HP -= g.roll(1, 4)
if p.Stats.HP <= 0 {
g.msg("a poisoned dart killed you")
g.death('d')
}
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
g.chgStr(-1)
g.changeStrength(-1)
}
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
// (move.c rndmove).
func (g *RogueGame) rndmove(who *Creature) Coord {
func (g *RogueGame) trapRust(Coord) {
g.msg("a gush of water hits you on the head")
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{
Y: who.Pos.Y + g.rnd(3) - 1,
X: who.Pos.X + g.rnd(3) - 1,
@@ -296,25 +445,32 @@ func (g *RogueGame) rndmove(who *Creature) Coord {
if ret == who.Pos {
return ret
}
if !g.diagOk(who.Pos, ret) {
return who.Pos
}
ch := g.Level.VisibleChar(ret.Y, ret.X)
if !stepOk(ch) {
return who.Pos
}
if ch == Scroll {
var found *Object
for _, obj := range g.Level.Objects {
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
found = obj
break
}
}
if found != nil && found.ScrollKind() == ScrollScareMonster {
return who.Pos
}
}
return ret
}
@@ -332,6 +488,7 @@ func (g *RogueGame) rustArmor(arm *Object) {
}
} else {
arm.ArmorClass++
if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!")
} else {

View File

@@ -13,6 +13,7 @@ const (
func (g *RogueGame) NewLevel() {
p := &g.Player
p.Flags.Clear(Held) // unhold when you go down just in case
if g.Depth > g.MaxDepth {
g.MaxDepth = g.Depth
}
@@ -20,61 +21,65 @@ func (g *RogueGame) NewLevel() {
for i := range g.Level.Places {
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
}
g.clear()
// Free up the monsters on the last level; the objects and their packs
// go with them (the garbage collector is our free_list).
g.Level.Monsters = nil
g.Level.Objects = nil
g.doRooms() // Draw rooms
g.doPassages() // Draw passages
g.digRooms() // Draw rooms
g.digPassages() // Draw passages
p.NoFood++
g.putThings() // Place objects (if any)
// Place the traps
if g.rnd(10) < g.Depth {
g.Level.TrapCount = g.rnd(g.Depth/4) + 1
if g.Level.TrapCount > MaxTraps {
g.Level.TrapCount = MaxTraps
}
g.Level.TrapCount = min(g.rnd(g.Depth/4)+1, MaxTraps)
for i := g.Level.TrapCount; i > 0; i-- {
// not only wouldn't it be NICE to have traps in mazes (not
// that we care about being nice), since the trap number is
// stored where the passage number is, we can't actually do it.
var stairs Coord
for {
stairs, _ = g.findFloor(nil, 0, false)
stairs, _ = g.findFloor(false)
if g.Level.Char(stairs.Y, stairs.X) == Floor {
break
}
}
sp := g.Level.FlagsAt(stairs.Y, stairs.X)
sp.Clear(FReal)
*sp |= PlaceFlags(g.rnd(NumTrapTypes))
*sp |= PlaceFlags(g.rnd(NumTrapTypes)) //nolint:gosec // G115: 0..7 fits
}
}
// Place the staircase down.
stairs, _ := g.findFloor(nil, 0, false)
stairs, _ := g.findFloor(false)
g.Level.Stairs = stairs
g.Level.SetChar(stairs.Y, stairs.X, Stairs)
g.SeenStairs = false
for _, tp := range g.Level.Monsters {
tp.Room = g.roomin(tp.Pos)
tp.Room = g.roomIn(tp.Pos)
}
hero, _ := g.findFloor(nil, 0, true)
hero, _ := g.findFloor(true)
p.Pos = hero
g.enterRoom(hero)
g.mvaddch(hero.Y, hero.X, PlayerCh)
if p.On(SenseMonsters) {
g.turnSee(false)
}
if p.On(Hallucinating) {
g.visuals(0)
}
}
// rndRoom picks a room that is really there (new_level.c rnd_room).
func (g *RogueGame) rndRoom() int {
// randomRoom picks a room that is really there (new_level.c rnd_room).
func (g *RogueGame) randomRoom() int {
for {
rm := g.rnd(MaxRooms)
if !g.Level.Rooms[rm].Flags.Has(Gone) {
@@ -93,16 +98,16 @@ func (g *RogueGame) putThings() {
}
// check for treasure rooms, and if so, put it in.
if g.rnd(treasRoomChance) == 0 {
g.treasRoom()
g.treasureRoom()
}
// Do MAXOBJ attempts to put things on a level
for i := 0; i < MaxObj; i++ {
for range MaxObj {
if g.rnd(100) < 36 {
// Pick a new object and link it in the list
obj := g.newThing()
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
// Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false)
obj.Pos, _ = g.findFloor(false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
}
}
@@ -110,42 +115,40 @@ func (g *RogueGame) putThings() {
// yet, put it somewhere on the ground
if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject()
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
obj.Damage = dice("0x0")
obj.HurlDmg = dice("0x0")
obj.ArmorClass = 11
obj.Kind = KindAmulet
// Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false)
obj.Pos, _ = g.findFloor(false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet)
}
}
// treasRoom adds a treasure room (new_level.c treas_room).
func (g *RogueGame) treasRoom() {
rp := &g.Level.Rooms[g.rndRoom()]
spots := (rp.Max.Y-2)*(rp.Max.X-2) - minTreas
if spots > maxTreas-minTreas {
spots = maxTreas - minTreas
}
// treasureRoom adds a treasure room (new_level.c treas_room).
func (g *RogueGame) treasureRoom() {
rp := &g.Level.Rooms[g.randomRoom()]
spots := min((rp.Max.Y-2)*(rp.Max.X-2)-minTreas, maxTreas-minTreas)
numMonst := g.rnd(spots) + minTreas
for nm := numMonst; nm > 0; nm-- {
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
tp := g.newThing()
tp.Pos = mp
attachObj(&g.Level.Objects, tp)
g.Level.AddObject(tp)
g.Level.SetChar(mp.Y, mp.X, tp.Kind.Glyph())
}
// fill up room with monsters from the next level down
nm := g.rnd(spots) + minTreas
if nm < numMonst+2 {
nm = numMonst + 2
}
nm := max(g.rnd(spots)+minTreas, numMonst+2)
spots = (rp.Max.Y - 2) * (rp.Max.X - 2)
if nm > spots {
nm = spots
}
g.Depth++
for ; nm > 0; nm-- {
if mp, ok := g.findFloorIn(rp, maxTries, true); ok {
@@ -155,5 +158,6 @@ func (g *RogueGame) treasRoom() {
g.givePack(tp)
}
}
g.Depth--
}

View File

@@ -7,24 +7,30 @@ import (
func genLevel(t *testing.T, seed int32) *RogueGame {
t.Helper()
g := NewGame(Config{Seed: seed})
g := New(Params{Seed: seed})
g.NewLevel()
return g
}
// renderMap draws the raw level map (not the screen) as text.
func renderMap(g *RogueGame) string {
var sb strings.Builder
for y := 0; y < NumLines; y++ {
for x := 0; x < NumCols; x++ {
for y := range NumLines {
for x := range NumCols {
ch := g.Level.Char(y, x)
if m := g.Level.MonsterAt(y, x); m != nil {
ch = m.Type
}
sb.WriteByte(ch)
}
sb.WriteByte('\n')
}
return sb.String()
}
@@ -32,6 +38,19 @@ func TestNewLevelInvariants(t *testing.T) {
for _, seed := range []int32{1, 12345, 2026, 99999} {
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.
st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs {
@@ -44,35 +63,53 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
g.Level.Char(hp.Y, hp.X))
}
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
t.Errorf("seed %d: hero standing on a monster", seed)
}
if g.Player.Room == nil {
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)
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
t.Errorf("seed %d: no room walls drawn", seed)
}
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
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 {
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
t.Errorf("seed %d: monster %c not indexed at its position",
seed, mon.Type)
}
if mon.Room == nil {
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 {
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch != obj.Kind.Glyph() &&
@@ -81,29 +118,37 @@ func TestNewLevelInvariants(t *testing.T) {
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 {
t.Errorf("seed %d: starting pack has %d items, want 5",
seed, len(g.Player.Pack))
}
if g.Player.CurWeapon == nil ||
g.Player.CurWeapon.WeaponKind() != WeaponMace {
t.Errorf("seed %d: not wielding the starting mace", seed)
}
if g.Player.CurArmor == nil ||
g.Player.CurArmor.ArmorKind() != ArmorRingMail {
t.Errorf("seed %d: not wearing the starting ring mail", seed)
}
}
}
func TestNewLevelDeterministic(t *testing.T) {
a := renderMap(genLevel(t, 12345))
b := renderMap(genLevel(t, 12345))
if a != b {
t.Error("same seed produced different levels")
}
c := renderMap(genLevel(t, 54321))
if a == c {
t.Error("different seeds produced identical levels")
@@ -114,10 +159,11 @@ func TestNewLevelDeterministic(t *testing.T) {
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
func TestDeeperLevels(t *testing.T) {
for _, seed := range []int32{7, 42, 1000, 31337} {
g := NewGame(Config{Seed: seed})
g := New(Params{Seed: seed})
for depth := 1; depth <= 30; depth++ {
g.Depth = depth
g.NewLevel()
st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs {
t.Fatalf("seed %d depth %d: missing staircase", seed, depth)

View File

@@ -5,6 +5,7 @@ package game
// category (ObjectKind) from its display character (Glyph).
type ObjectKind int
// Item categories.
const (
KindNone ObjectKind = iota
KindPotion
@@ -25,35 +26,49 @@ const (
KindRingOrStick ObjectKind = -2
)
// kindGlyphs maps each kind to the character Rogue draws for it.
var kindGlyphs = [...]byte{
KindNone: ' ',
KindPotion: Potion,
KindScroll: Scroll,
KindFood: Food,
KindWeapon: Weapon,
KindArmor: Armor,
KindRing: Ring,
KindWand: Stick,
KindAmulet: Amulet,
KindGold: Gold,
}
// Category words shared by ObjectKind.String, the discovery list, and the
// ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the
// glyph byte constants.)
const (
potionName = "potion"
scrollName = "scroll"
ringName = "ring"
goldName = "gold"
)
// Glyph returns the map/display character for this kind of object.
func (k ObjectKind) Glyph() byte {
if k < 0 || int(k) >= len(kindGlyphs) {
return ' '
switch k {
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.
func (k ObjectKind) String() string {
switch k {
case KindPotion:
return "potion"
return potionName
case KindScroll:
return "scroll"
return scrollName
case KindFood:
return "food"
case KindWeapon:
@@ -61,27 +76,36 @@ func (k ObjectKind) String() string {
case KindArmor:
return "suit of armor"
case KindRing:
return "ring"
case KindWand:
return "wand or staff"
case KindAmulet:
return "amulet"
case KindGold:
return "gold"
case KindRingOrStick:
return "ring, wand or staff"
return ringName
default:
return k.stringRest()
}
return "bizarre thing"
}
// objectKindForGlyph is the reverse of Glyph: what category of item does a
// map character denote. Returns KindNone for non-item characters.
func objectKindForGlyph(ch byte) ObjectKind {
for k, g := range kindGlyphs {
if g == ch && ObjectKind(k) != KindNone {
return ObjectKind(k)
}
switch ch {
case Potion:
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
}
@@ -91,6 +115,23 @@ func (k ObjectKind) MergesInPack() bool {
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
// floor or ride in a pack.
type Object struct {
@@ -148,6 +189,7 @@ func detachObj(list *[]*Object, item *Object) {
for i, o := range *list {
if o == item {
*list = append((*list)[:i], (*list)[i+1:]...)
return
}
}

View File

@@ -28,6 +28,7 @@ type optDesc struct {
// optList builds the options.c optlist for this game.
func (g *RogueGame) optList() []optDesc {
o := &g.Options
return []optDesc{
{"terse", "Terse output", optBool, &o.Terse, nil, nil},
{"flush", "Flush typeahead during battle", optBool, &o.FightFlush, nil, nil},
@@ -46,6 +47,7 @@ func (g *RogueGame) optList() []optDesc {
func (g *RogueGame) option() {
hw := g.scr.Hw
optlist := g.optList()
hw.Clear()
// Display current values of options
for i := range optlist {
@@ -56,9 +58,11 @@ func (g *RogueGame) option() {
}
// Set values
hw.Move(0, 0)
for i := 0; i < len(optlist); i++ {
op := &optlist[i]
g.prOptname(op)
retval := g.getOpt(op)
if retval != Norm {
if retval == Quit {
@@ -70,6 +74,7 @@ func (g *RogueGame) option() {
i -= 2
} else { // trying to back up beyond the top
hw.Move(0, 0)
i--
}
}
@@ -84,18 +89,19 @@ func (g *RogueGame) option() {
// prOptname prints out the option name prompt (options.c pr_optname).
func (g *RogueGame) prOptname(op *optDesc) {
g.scr.Hw.Printw("%s (\"%s\"): ", op.prompt, op.name)
g.scr.Hw.Printwf("%s (\"%s\"): ", op.prompt, op.name)
}
// putOpt prints an option's current value (options.c put_bool/put_str/
// put_inv_t).
func (g *RogueGame) putOpt(op *optDesc) {
hw := g.scr.Hw
switch op.kind {
case optBool, optSeeFloor:
hw.AddStr(boolStr(*op.boolP))
case optInvT:
hw.AddStr(invTName[*op.intP])
hw.AddStr(g.data.invTName[*op.intP])
case optStr:
hw.AddStr(*op.strP)
}
@@ -105,6 +111,7 @@ func boolStr(b bool) string {
if b {
return "True"
}
return "False"
}
@@ -129,9 +136,11 @@ func (g *RogueGame) getBool(bp *bool) int {
win := g.scr.Hw
oy, ox := win.GetYX()
win.AddStr(boolStr(*bp))
for {
win.Move(oy, ox)
g.scr.RefreshWin(win)
switch g.readchar() {
case 't', 'T':
*bp = true
@@ -145,13 +154,17 @@ func (g *RogueGame) getBool(bp *bool) int {
default:
win.Move(oy, ox+10)
win.AddStr("(T or F)")
continue
}
break
}
win.Move(oy, ox)
win.AddStr(boolStr(*bp))
win.AddCh('\n')
return Norm
}
@@ -159,10 +172,12 @@ func (g *RogueGame) getBool(bp *bool) int {
// get_sf).
func (g *RogueGame) getSf(bp *bool) int {
wasSf := g.Options.SeeFloor
retval := g.getBool(bp)
if retval == Quit {
return Quit
}
if wasSf != g.Options.SeeFloor {
if !g.Options.SeeFloor {
g.Options.SeeFloor = true
@@ -172,6 +187,7 @@ func (g *RogueGame) getSf(bp *bool) int {
g.look(false)
}
}
return Norm
}
@@ -183,57 +199,36 @@ func (g *RogueGame) getStr(opt *string, win *Window) int {
oy, ox := win.GetYX()
g.scr.RefreshWin(win)
// loop reading in the string, and put it in a temporary buffer
var buf []byte
var c byte
var (
buf []byte
c byte
)
for {
c = g.readchar()
if c == '\n' || c == '\r' || c == Escape {
if endsInput(c) || (len(buf) == 0 && c == '-' && !onStd) {
break
}
if c == 8 || c == 0x7f { // erase character
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)
buf = g.getStrEdit(win, buf, c, oy, ox)
}
if len(buf) > 0 { // only change option if something has been typed
*opt = strucpy(string(buf))
}
win.MvPrintw(oy, ox, "%s\n", *opt)
win.MvPrintwf(oy, ox, "%s\n", *opt)
g.scr.RefreshWin(win)
if onStd {
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 {
case '-':
return Minus
@@ -244,12 +239,59 @@ 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.
func displayStr(buf []byte) string {
var sb strings.Builder
for _, c := range buf {
sb.WriteString(unctrl(c))
}
return sb.String()
}
@@ -257,10 +299,12 @@ func displayStr(buf []byte) string {
func (g *RogueGame) getInvT(ip *int) int {
win := g.scr.Hw
oy, ox := win.GetYX()
win.AddStr(invTName[*ip])
win.AddStr(g.data.invTName[*ip])
for {
win.Move(oy, ox)
g.scr.RefreshWin(win)
switch g.readchar() {
case 'o', 'O':
*ip = InvOver
@@ -276,11 +320,15 @@ func (g *RogueGame) getInvT(ip *int) int {
default:
win.Move(oy, ox+15)
win.AddStr("(O, S, or C)")
continue
}
break
}
win.MvPrintw(oy, ox, "%s\n", invTName[*ip])
win.MvPrintwf(oy, ox, "%s\n", g.data.invTName[*ip])
return Norm
}
@@ -289,31 +337,68 @@ func (g *RogueGame) getInvT(ip *int) int {
// "noname" (false), strings as "name=..." (options.c parse_opts).
func (g *RogueGame) ParseOpts(str string) {
optlist := g.optList()
for str != "" {
// Get option name
i := 0
for i < len(str) && isAlpha(str[i]) {
i++
}
name := str[:i]
rest := str[i:]
matched := false
rest := g.parseOptName(optlist, str[:i], str[i:])
// skip to start of next option name
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 {
op := &optlist[oi]
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
if strings.HasPrefix(op.name, name) && name != "" {
matched = true
if isBoolOpt {
*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
for rest != "" && rest[0] == '=' {
rest = rest[1:]
}
val := rest
var prefix string
if val != "" && val[0] == '~' {
prefix = g.Home
val = val[1:]
for val != "" && val[0] == '/' {
val = val[1:]
@@ -324,39 +409,33 @@ func (g *RogueGame) ParseOpts(str string) {
if end < 0 {
end = len(val)
}
word := val[:end]
rest = val[end:]
if op.kind == optInvT {
// check for type of inventory
w := word
if w != "" {
w = string(toUpper(w[0])) + w[1:]
}
for ti, tn := range invTName {
if strings.HasPrefix(tn, w) {
*op.intP = ti
break
}
}
g.parseInvType(op, word)
} else {
*op.strP = prefix + strucpy(word)
}
return val[end:]
}
// 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:]
}
break
} else if isBoolOpt && strings.HasPrefix(name, "no") &&
strings.HasPrefix(op.name, name[2:]) {
matched = true
*op.boolP = false
for ti, tn := range g.data.invTName {
if strings.HasPrefix(tn, word) {
*op.intP = ti
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
@@ -365,11 +444,14 @@ func strucpy(s string) string {
if len(s) > MaxInp {
s = s[:MaxInp]
}
var sb strings.Builder
for i := 0; i < len(s); i++ {
for i := range len(s) {
if isPrint(s[i]) || s[i] == ' ' {
sb.WriteByte(s[i])
}
}
return sb.String()
}

View File

@@ -7,99 +7,24 @@ package game
func (g *RogueGame) addPack(obj *Object, silent bool) {
p := &g.Player
fromFloor := false
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
}
fromFloor = true
}
// Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
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")
if g.pickupScareScroll(obj) {
return
}
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
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 {
if op.Kind.MergesInPack() {
if !g.packRoom(fromFloor, obj) {
obj, ok := g.packInsert(obj, fromFloor)
if !ok {
return
}
op.Count++
obj = op
lp = -1
merged = true
} else if 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
}
} else {
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)
@@ -117,119 +42,260 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
// Notify the user
if !silent {
if !g.Options.Terse {
g.addmsg("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
// message (pack.c pack_room).
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
p := &g.Player
if p.Inpack++; p.Inpack > MaxPack {
if !g.Options.Terse {
g.addmsg("there's ")
g.addmsgf("there's ")
}
g.addmsg("no room")
g.addmsgf("no room")
if !g.Options.Terse {
g.addmsg(" in your pack")
g.addmsgf(" in your pack")
}
g.endmsg()
if fromFloor {
g.moveMsg(obj)
}
p.Inpack = MaxPack
return false
}
if fromFloor {
detachObj(&g.Level.Objects, obj)
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)
}
}
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 {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// 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
return g.Player.removeFromPack(obj, newobj, all)
}
// inventory lists what is in the pack; returns true if there is something
// of the given type (pack.c inventory).
func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.NObjs = 0
for _, item := range list {
if kind != KindNone && kind != item.Kind &&
!(kind == KindCallable && item.Kind != KindFood &&
item.Kind != KindAmulet) &&
!(kind == KindRingOrStick &&
(item.Kind == KindRing || item.Kind == KindWand)) {
if !matchesFilter(kind, item) {
continue
}
g.NObjs++
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 {
g.Msgs.MsgEsc = false
g.msg("")
return true
}
g.Msgs.MsgEsc = false
}
if g.NObjs == 0 {
if g.Options.Terse {
if kind == KindNone {
g.msg("empty handed")
g.msg("%s", g.chooseTerse("empty handed", "you are empty handed"))
} else {
g.msg("nothing appropriate")
}
} else {
if kind == KindNone {
g.msg("you are empty handed")
} else {
g.msg("you don't have anything appropriate")
}
g.msg("%s", g.chooseTerse("nothing appropriate",
"you don't have anything appropriate"))
}
return false
}
g.endLine()
return true
}
@@ -240,31 +306,53 @@ func (g *RogueGame) pickUp(ch byte) {
return
}
obj := g.findObj(p.Pos.Y, p.Pos.X)
obj := g.Level.ObjectAt(p.Pos.Y, p.Pos.X)
if g.MoveOn {
g.moveMsg(obj)
return
}
switch ch {
case Gold:
if obj == nil {
return
}
g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
p.Room.GoldVal = 0
default:
g.addPack(nil, false)
}
}
// matchesFilter reports whether an item passes a get_item/inventory kind
// filter (the C condition in pack.c inventory, untangled): KindNone takes
// everything, KindCallable takes anything nameable (not food, not the
// amulet), KindRingOrStick takes rings and wands.
func matchesFilter(kind ObjectKind, item *Object) bool {
switch kind {
case KindNone:
return true
case KindCallable:
return item.Kind != KindFood && item.Kind != KindAmulet
case KindRingOrStick:
return item.Kind == KindRing || item.Kind == KindWand
default:
return item.Kind == kind
}
}
// moveMsg prints the message if you are just moving onto an object
// (pack.c move_msg).
func (g *RogueGame) moveMsg(obj *Object) {
if !g.Options.Terse {
g.addmsg("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
@@ -273,51 +361,55 @@ func (g *RogueGame) pickyInven() {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return
}
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
}
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
g.Msgs.Mpos = 0
mch := g.readchar()
if mch == Escape {
g.msg("")
return
}
for _, obj := range p.Pack {
if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false))
g.msg("%c) %s", mch, g.inventoryName(obj, false))
return
}
}
g.msg("'%s' not in pack", unctrl(mch))
}
// getItem picks something out of a pack for a purpose (pack.c get_item).
func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
// promptPackItem picks something out of a pack for a purpose (pack.c
// 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
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return nil
return nil, false
}
if g.Again {
if g.LastPick != nil {
return g.LastPick
}
g.msg("you ran out")
return nil
return g.repeatLastItem()
}
for {
if !g.Options.Terse {
g.addmsg("which object do you want to ")
}
g.addmsg("%s", purpose)
if g.Options.Terse {
g.addmsg(" what")
}
g.msg("? (* for list): ")
g.promptItemPurpose(purpose)
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
@@ -325,40 +417,77 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
g.resetLast()
g.After = false
g.msg("")
return nil
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
return nil, false
}
continue
}
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj
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 {
return g.LastPick, true
}
g.msg("you ran out")
return nil, false
}
// promptItemPurpose prints the "which object do you want to ...?"
// prompt (pack.c get_item).
func (g *RogueGame) promptItemPurpose(purpose string) {
if !g.Options.Terse {
g.addmsgf("which object do you want to ")
}
g.addmsgf("%s", purpose)
if g.Options.Terse {
g.addmsgf(" what")
}
g.msg("? (* for list): ")
}
// money adds or subtracts gold from the pack (pack.c money).
func (g *RogueGame) money(value int) {
p := &g.Player
p.Purse += value
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)
}
if value > 0 {
if !g.Options.Terse {
g.addmsg("you found ")
g.addmsgf("you found ")
}
g.msg("%d gold pieces", value)
}
}
@@ -369,9 +498,11 @@ func (g *RogueGame) floorCh() byte {
if g.Player.Room.Flags.Has(Gone) {
return Passage
}
if g.showFloor() {
return Floor
}
return ' '
}
@@ -382,6 +513,7 @@ func (g *RogueGame) floorAt() byte {
if ch == Floor {
ch = g.floorCh()
}
return ch
}
@@ -398,5 +530,6 @@ func (g *RogueGame) chooseTerse(terse, verbose string) string {
if g.Options.Terse {
return terse
}
return verbose
}

View File

@@ -2,221 +2,269 @@ package game
// passages.c — draw the connecting passages.
// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages.
var 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},
}
// doPassages draws all the passages on a level (passages.c do_passages).
func (g *RogueGame) doPassages() {
var isconn [MaxRooms][MaxRooms]bool
var ingraph [MaxRooms]bool
// digPassages draws all the passages on a level (passages.c do_passages).
func (g *RogueGame) digPassages() {
var (
isconn [MaxRooms][MaxRooms]bool
ingraph [MaxRooms]bool
)
// starting with one room, connect it to a random adjacent room and
// then pick a new room to start with.
roomcount := 1
r1 := g.rnd(MaxRooms)
ingraph[r1] = true
for {
for roomcount < MaxRooms {
// find a room to connect with
j := 0
r2 := -1
for i := 0; i < MaxRooms; i++ {
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
r2 := g.pickNeighbor(r1, func(i int) bool { return !ingraph[i] })
if r2 < 0 {
// if no adjacent rooms are outside the graph, pick a new
// room to look from
for {
r1 = g.rnd(MaxRooms)
if ingraph[r1] {
break
}
}
} else {
continue
}
// otherwise, connect new room to the graph, and draw a tunnel
// to it
ingraph[r2] = true
g.conn(r1, r2)
g.connectRooms(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true
roomcount++
}
if roomcount >= MaxRooms {
break
}
}
// attempt to add passages to the graph a random number of times so that
// there isn't always just one unique passage through it.
for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
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
r2 := -1
for i := 0; i < MaxRooms; i++ {
if rdesConn[r1][i] && !isconn[r1][i] {
for i := range MaxRooms {
if g.data.rdesConn[r1][i] && ok(i) {
if j++; g.rnd(j) == 0 {
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
}
}
g.passnum()
return r2
}
// conn draws a corridor from a room in a certain direction (passages.c
// conn).
func (g *RogueGame) conn(r1, r2 int) {
var rm int
var direc byte
if r1 < r2 {
rm = r1
if r1+1 == r2 {
direc = 'r'
} else {
direc = 'd'
}
} else {
rm = r2
if r2+1 == r1 {
direc = 'r'
} else {
direc = 'd'
}
}
rpf := &g.Level.Rooms[rm]
// corridorPlan is the movement setup connectRooms computes before it
// digs (the local variables of passages.c conn).
type corridorPlan struct {
rpf, rpt *Room // the rooms being joined
del Coord // direction of move
turnDelta Coord // direction to turn
spos, epos Coord // start and end of move
distance, turnDistance int // how far to move and to turn
}
// Set up the movement variables, in two cases: first drawing one down.
var rpt *Room
var del, turnDelta, spos, epos Coord
var distance, turnDistance int
// connectRooms draws a corridor from a room in a certain direction
// (passages.c conn).
func (g *RogueGame) connectRooms(r1, r2 int) {
rm, direc := connOrient(r1, r2)
var plan corridorPlan
if direc == 'd' {
rmt := rm + 3 // room # of dest
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
plan = g.connPlanDown(rm)
} else {
turnDelta.X = -1
}
turnDistance = abs(spos.X - epos.X) // how far to turn
} 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)
plan = g.connPlanRight(rm)
}
turnSpot := g.rnd(distance-1) + 1 // where turn starts
turnSpot := g.rnd(plan.distance-1) + 1 // where turn starts
// Draw in the doors on either side of the passage or just put #'s if
// the rooms are gone.
if !rpf.Flags.Has(Gone) {
g.door(rpf, spos)
} else {
g.putpass(spos)
g.connEnd(plan.rpf, plan.spos)
g.connEnd(plan.rpt, plan.epos)
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) {
g.door(rpt, epos)
} else {
g.putpass(epos)
for {
plan.epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
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 {
// Move to new position
curr.X += del.X
curr.Y += del.Y
curr.X += plan.del.X
curr.Y += plan.del.Y
// Check if we are at the turn place, if so do the turn
if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- {
g.putpass(curr)
curr.X += turnDelta.X
curr.Y += turnDelta.Y
g.putPassage(curr)
curr.X += plan.turnDelta.X
curr.Y += plan.turnDelta.Y
}
}
// Continue digging along
g.putpass(curr)
g.putPassage(curr)
distance--
}
curr.X += del.X
curr.Y += del.Y
if curr != epos {
curr.X += plan.del.X
curr.Y += plan.del.Y
if curr != plan.epos {
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).
func (g *RogueGame) putpass(cp Coord) {
func (g *RogueGame) putPassage(cp Coord) {
pp := g.Level.At(cp.Y, cp.X)
pp.Flags.Set(FPassage)
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
pp.Flags.Clear(FReal)
} else {
@@ -240,6 +288,7 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
} else {
pp.Ch = '|'
}
pp.Flags.Clear(FReal)
} else {
pp.Ch = Door
@@ -250,79 +299,100 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
// (passages.c add_pass).
func (g *RogueGame) addPass() {
for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ {
pp := g.Level.At(y, x)
if pp.Flags.Has(FPassage) || pp.Ch == Door ||
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) {
for x := range NumCols {
g.addPassSpot(g.Level.At(y, x), y, x)
}
}
}
// 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
if pp.Flags.Has(FPassage) {
ch = Passage
}
pp.Flags.Set(FSeen)
g.move(y, x)
if pp.Monst != nil {
switch {
case pp.Monst != nil:
pp.Monst.OldCh = pp.Ch
} else if pp.Flags.Has(FReal) {
case pp.Flags.Has(FReal):
g.addch(ch)
} else {
default:
g.standout()
if pp.Flags.Has(FPassage) {
g.addch(Passage)
} else {
g.addch(Door)
}
g.standend()
}
}
}
}
}
// passnum assigns a number to each passageway (passages.c passnum).
func (g *RogueGame) passnum() {
// hiddenExit reports a door, or a secret door still drawn as a wall
// (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.newpnum = false
for i := range g.Level.Passages {
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
}
for i := range g.Level.Rooms {
rp := &g.Level.Rooms[i]
for j := range rp.Exits {
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).
func (g *RogueGame) numpass(y, x int) {
func (g *RogueGame) numberPassage(y, x int) {
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
return
}
fp := g.Level.FlagsAt(y, x)
if fp.Has(FPassNum) {
return
}
if g.newpnum {
g.pnum++
g.newpnum = false
}
// check to see if it is a door or secret door, i.e., a new exit, or a
// numerable type of place
if ch := g.Level.Char(y, x); ch == Door ||
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
if hiddenExit(*fp, g.Level.Char(y, x)) {
rp := &g.Level.Passages[g.pnum]
rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
} else if !fp.Has(FPassage) {
return
}
*fp |= PlaceFlags(g.pnum)
*fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13
// recurse on the surrounding places
g.numpass(y+1, x)
g.numpass(y-1, x)
g.numpass(y, x+1)
g.numpass(y, x-1)
g.numberPassage(y+1, x)
g.numberPassage(y-1, x)
g.numberPassage(y, x+1)
g.numberPassage(y, x-1)
}
// abs is C abs() for ints.
@@ -330,5 +400,6 @@ func abs(n int) int {
if n < 0 {
return -n
}
return n
}

View File

@@ -13,99 +13,117 @@ type pact struct {
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).
func (g *RogueGame) quaff() {
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
if obj == nil {
if !ok {
return
}
if obj.Kind != KindPotion {
if !g.Options.Terse {
g.msg("yuk! Why would you want to drink that?")
} else {
g.msg("that's undrinkable")
}
return
}
if obj == p.CurWeapon {
p.CurWeapon = nil
}
// Calculate the effect it has on the poor guy.
trip := p.On(Hallucinating)
g.leavePack(obj, false, false)
switch obj.PotionKind() {
case PotionConfusion:
g.doPot(PotionConfusion, !trip)
case PotionPoison:
if h := g.data.quaffHandlers[obj.PotionKind()]; h != nil {
h(g, trip)
}
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
if p.IsWearing(RingSustainStrength) {
if g.Player.IsWearing(RingSustainStrength) {
g.msg("you feel momentarily sick")
} else {
g.chgStr(-(g.rnd(3) + 1))
g.changeStrength(-(g.rnd(3) + 1))
g.msg("you feel very sick now")
g.comeDown(0)
}
case PotionHealing:
}
func (g *RogueGame) quaffHealing(bool) {
p := &g.Player
g.Items.Potions[PotionHealing].Know = true
if p.Stats.HP += g.roll(p.Stats.Lvl, 4); p.Stats.HP > p.Stats.MaxHP {
p.Stats.MaxHP++
p.Stats.HP = p.Stats.MaxHP
}
g.sight(0)
g.msg("you begin to feel better")
case PotionGainStrength:
}
func (g *RogueGame) quaffGainStrength(bool) {
g.Items.Potions[PotionGainStrength].Know = true
g.chgStr(1)
g.changeStrength(1)
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)
if !g.turnSee(false) {
g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange"))
}
case PotionDetectMagic:
}
func (g *RogueGame) quaffDetectMagic(bool) {
// Potion of magic detection. Show the potions and scrolls
show := false
if len(g.Level.Objects) > 0 {
g.scr.Hw.Clear()
for _, tp := range g.Level.Objects {
if tp.isMagic() {
if g.isMagic(tp) {
show = true
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
g.Items.Potions[PotionDetectMagic].Know = true
}
}
for _, mp := range g.Level.Monsters {
for _, tp := range mp.Pack {
if tp.isMagic() {
if g.isMagic(tp) {
show = true
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
}
}
}
}
if show {
g.Items.Potions[PotionDetectMagic].Know = true
g.showWin("You sense the presence of magic on this level.--More--")
@@ -113,85 +131,115 @@ func (g *RogueGame) quaff() {
g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange"))
}
case PotionLSD:
}
func (g *RogueGame) quaffLSD(trip bool) {
p := &g.Player
if !trip {
if p.On(SenseMonsters) {
g.turnSee(false)
}
g.StartDaemon(DVisuals, 0, Before)
g.SeenStairs = g.seenStairs()
}
g.doPot(PotionLSD, true)
case PotionSeeInvisible:
show := p.On(CanSeeInvisible)
g.doPot(PotionSeeInvisible, false)
g.applyPotionFuse(PotionLSD, true)
}
func (g *RogueGame) quaffSeeInvisible(bool) {
show := g.Player.On(CanSeeInvisible)
g.applyPotionFuse(PotionSeeInvisible, false)
if !show {
g.invisOn()
}
g.sight(0)
case PotionRaiseLevel:
}
func (g *RogueGame) quaffRaiseLevel(bool) {
g.Items.Potions[PotionRaiseLevel].Know = true
g.msg("you suddenly feel much more skillful")
g.raiseLevel()
case PotionExtraHealing:
}
func (g *RogueGame) quaffExtraHealing(bool) {
p := &g.Player
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 > p.Stats.MaxHP+p.Stats.Lvl+1 {
p.Stats.MaxHP++
}
p.Stats.MaxHP++
p.Stats.HP = p.Stats.MaxHP
}
g.sight(0)
g.comeDown(0)
g.msg("you begin to feel much better")
case PotionHaste:
}
func (g *RogueGame) quaffHaste(bool) {
g.Items.Potions[PotionHaste].Know = true
g.After = false
if g.addHaste(true) {
g.msg("you feel yourself moving much faster")
}
case PotionRestoreStrength:
}
func (g *RogueGame) quaffRestoreStrength(bool) {
p := &g.Player
if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
}
if p.Stats.Str < p.MaxStats.Str {
p.Stats.Str = p.MaxStats.Str
}
if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
}
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()
// Throw the item away
g.callIt(&g.Items.Potions[obj.Which])
}
func (g *RogueGame) quaffBlindness(bool) {
g.applyPotionFuse(PotionBlindness, true)
}
func (g *RogueGame) quaffLevitation(bool) {
g.applyPotionFuse(PotionLevitation, true)
}
// raiseLevel: the guy just magically went up a level (potions.c
// raise_level).
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()
}
// doPot does a potion with standard setup: it uses a fuse and turns on a
// flag (potions.c do_pot).
func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
pp := &pActions[kind]
// applyPotionFuse does a potion with standard setup: it uses a fuse and
// turns on a flag (potions.c do_pot).
func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
pp := &g.data.pActions[kind]
if !g.Items.Potions[kind].Know {
g.Items.Potions[kind].Know = knowit
}
t := g.spread(pp.time)
if !g.Player.On(pp.flags) {
g.Player.Flags.Set(pp.flags)
@@ -200,24 +248,28 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
} else {
g.Lengthen(pp.daemon, t)
}
high, straight := pp.high, pp.straight
if kind == PotionSeeInvisible {
s := fmt.Sprintf("this potion tastes like %s juice", g.Fruit)
high, straight = s, s
}
g.msg("%s", g.chooseStr(high, straight))
}
// 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 {
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:
return o.HPlus != 0 || o.DPlus != 0
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:
return true
}
return false
}
@@ -225,6 +277,7 @@ func (o *Object) isMagic() bool {
// invis_on).
func (g *RogueGame) invisOn() {
g.Player.Flags.Set(CanSeeInvisible)
for _, mp := range g.Level.Monsters {
if mp.On(Invisible) && g.seeMonst(mp) && !g.Player.On(Hallucinating) {
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
@@ -236,44 +289,62 @@ func (g *RogueGame) invisOn() {
// turn_see).
func (g *RogueGame) turnSee(turnOff bool) bool {
addNew := false
for _, mp := range g.Level.Monsters {
g.move(mp.Pos.Y, mp.Pos.X)
canSee := g.seeMonst(mp)
if turnOff {
if !canSee {
g.addch(mp.OldCh)
}
} else {
if !canSee {
g.standout()
}
if !g.Player.On(Hallucinating) {
g.addch(mp.Type)
} else {
g.addch(byte(g.rnd(26) + 'A'))
}
if !canSee {
g.standend()
} else if g.showSensed(mp, canSee) {
addNew = true
}
}
}
if turnOff {
g.Player.Flags.Clear(SenseMonsters)
} 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 {
g.standout()
}
if !g.Player.On(Hallucinating) {
g.addch(mp.Type)
} else {
g.addch(g.randomMonsterLetter())
}
if !canSee {
g.standend()
return true
}
return false
}
// seenStairs reports whether the player has seen the stairs (potions.c
// seen_stairs).
func (g *RogueGame) seenStairs() bool {
st := g.Level.Stairs
g.move(st.Y, st.X)
if g.inch() == Stairs { // it's on the map
return true
}
if g.Player.Pos == st { // it's under him
return true
}
@@ -282,9 +353,11 @@ func (g *RogueGame) seenStairs() bool {
if g.seeMonst(tp) && tp.On(Awake) { // visible and awake:
return true // it must have moved there
}
if g.Player.On(SenseMonsters) && tp.OldCh == Stairs {
return true
}
}
return false
}

View File

@@ -7,17 +7,16 @@ import "fmt"
// ringOn puts a ring on a hand (rings.c ring_on).
func (g *RogueGame) ringOn() {
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
if obj == nil {
if !ok {
return
}
if obj.Kind != KindRing {
if !g.Options.Terse {
g.msg("it would be difficult to wrap that around a finger")
} else {
g.msg("not a ring")
}
g.msg("%s", g.chooseTerse("not a ring",
"it would be difficult to wrap that around a finger"))
return
}
@@ -26,30 +25,17 @@ func (g *RogueGame) ringOn() {
return
}
var ring int
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")
}
ring := g.pickRingHand()
if ring < 0 {
return
}
p.CurRing[ring] = obj
// Calculate the effect it has on the poor guy.
switch obj.RingKind() {
case RingAddStrength:
g.chgStr(obj.Bonus)
g.changeStrength(obj.Bonus)
case RingSeeInvisible:
g.invisOn()
case RingAggravateMonsters:
@@ -57,15 +43,38 @@ func (g *RogueGame) ringOn() {
}
if !g.Options.Terse {
g.addmsg("you are now wearing ")
g.addmsgf("you are now wearing ")
}
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
}
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh)
}
// ringOff takes off a ring (rings.c ring_off).
func (g *RogueGame) ringOff() {
p := &g.Player
var ring int
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if g.Options.Terse {
@@ -73,6 +82,7 @@ func (g *RogueGame) ringOff() {
} else {
g.msg("you aren't wearing any rings")
}
return
case p.CurRing[Left] == nil:
ring = Right
@@ -83,14 +93,18 @@ func (g *RogueGame) ringOff() {
return
}
}
g.Msgs.Mpos = 0
obj := p.CurRing[ring]
if obj == nil {
g.msg("not wearing such a ring")
return
}
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)
}
}
@@ -102,17 +116,22 @@ func (g *RogueGame) gethand() int {
} else {
g.msg("left hand or right hand? ")
}
c := g.readchar()
if c == Escape {
return -1
}
g.Msgs.Mpos = 0
if c == 'l' || c == 'L' {
return Left
}
if c == 'r' || c == 'R' {
return Right
}
if g.Options.Terse {
g.msg("L or R")
} else {
@@ -121,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
// (rings.c ring_eat).
func (g *RogueGame) ringEat(hand int) int {
@@ -147,7 +147,8 @@ func (g *RogueGame) ringEat(hand int) int {
if ring == nil {
return 0
}
eat := ringUses[ring.RingKind()]
eat := g.data.ringUses[ring.RingKind()]
if eat < 0 {
if g.rnd(-eat) == 0 {
eat = 1
@@ -155,20 +156,25 @@ func (g *RogueGame) ringEat(hand int) int {
eat = 0
}
}
if ring.RingKind() == RingSlowDigestion {
eat = -eat
}
return eat
}
// ringNum prints ring bonuses (rings.c ring_num).
func ringNum(g *RogueGame, obj *Object) string {
// ringNum prints ring bonuses (rings.c ring_num). The unused game
// parameter keeps the nameit prfunc signature.
func ringNum(_ *RogueGame, obj *Object) string {
if !obj.Flags.Has(Known) {
return ""
}
switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
}
return ""
}

View File

@@ -2,76 +2,71 @@ package game
import (
"fmt"
"os"
"time"
)
// rip.c — the fun ends: death or a total win.
//
// The C functions here call exit(); the port panics with a gameEnd sentinel
// that Run recovers, so the terminal is restored by normal unwinding.
// The C functions here call exit(). One game run is one process, so the
// port does the same: myExit restores the terminal and exits directly.
// gameEnd is the sentinel carried by the panic that replaces my_exit().
type gameEnd struct{ status int }
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
func (g *RogueGame) myExit(st int) {
g.Playing = false
panic(gameEnd{status: st})
}
var ripArt = []string{
" __________",
" / \\",
" / REST \\",
" / IN \\",
" / PEACE \\",
" / \\",
" | |",
" | |",
" | killed by a |",
" | |",
" | 1980 |",
" *| * * * | *",
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
// myExit leaves the process properly (main.c my_exit): it restores the
// terminal and ends the process. Every C caller exited with status 0.
func (g *RogueGame) myExit() {
g.scr.Fini()
os.Exit(0)
}
// death does something really fun when he dies (rip.c death).
func (g *RogueGame) death(monst byte) {
p := &g.Player
p.Purse -= p.Purse / 10
g.clear()
killer := g.killname(monst, false)
if !g.Options.Tombstone {
g.scr.Std.MvPrintw(NumLines-2, 0, "Killed by ")
g.scr.Std.MvPrintwf(NumLines-2, 0, "Killed by ")
if monst != 's' && monst != 'h' {
g.printw("a%s ", vowelstr(killer))
}
g.printw("%s with %d gold", killer, p.Purse)
} else {
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.mvaddstr(17, center(killer), killer)
if monst == 's' || monst == 'h' {
g.mvaddch(16, 32, ' ')
} else {
g.mvaddstr(16, 33, vowelstr(killer))
}
g.mvaddstr(14, center(g.Whoami), g.Whoami)
au := fmt.Sprintf("%d Au", p.Purse)
g.mvaddstr(15, center(au), au)
g.mvaddstr(18, 26, fmt.Sprintf("%4d", year))
}
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
g.refresh()
flags := 0
if g.HasAmulet {
flags = 3
}
g.score(p.Purse, flags, monst)
g.waitFor('\n')
g.myExit(0)
g.myExit()
}
// center returns the column to center the given string on the tombstone
@@ -85,6 +80,7 @@ func (g *RogueGame) totalWinner() {
p := &g.Player
g.clear()
g.standout()
banner := []string{
" ",
" @ @ @ @ @ @@@ @ @ ",
@@ -100,6 +96,7 @@ func (g *RogueGame) totalWinner() {
for i, line := range banner {
g.scr.Std.MvAddStr(i, 0, line)
}
g.standend()
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the")
g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at")
@@ -110,11 +107,31 @@ func (g *RogueGame) totalWinner() {
g.clear()
g.mvaddstr(0, 0, " Worth Item")
g.move(1, 0)
oldpurse := p.Purse
line := 1
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
worth := 0
switch obj.Kind {
case KindFood:
worth = 2 * obj.Count
@@ -125,25 +142,46 @@ func (g *RogueGame) totalWinner() {
case KindArmor:
worth = it.Armors[obj.Which].Worth
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)
case KindScroll:
op := &it.Scrolls[obj.Which]
worth = op.Worth * obj.Count
if !op.Know {
worth /= 2
}
op.Know = true
worth = loreWorth(&it.Scrolls[obj.Which], obj.Count)
case KindPotion:
op := &it.Potions[obj.Which]
worth = op.Worth * obj.Count
worth = loreWorth(&it.Potions[obj.Which], obj.Count)
case KindRing:
worth = g.ringWorth(obj)
case KindWand:
worth = g.wandWorth(obj)
case KindAmulet:
worth = 1000
}
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 {
worth /= 2
}
op.Know = true
case KindRing:
op := &it.Rings[obj.Which]
worth = op.Worth
return 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 ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
if obj.Bonus > 0 {
@@ -152,85 +190,76 @@ func (g *RogueGame) totalWinner() {
worth = 10
}
}
if !obj.Flags.Has(Known) {
worth /= 2
}
obj.Flags.Set(Known)
op.Know = true
case KindWand:
op := &it.Sticks[obj.Which]
worth = op.Worth
return 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
if !obj.Flags.Has(Known) {
worth /= 2
}
obj.Flags.Set(Known)
op.Know = true
case KindAmulet:
worth = 1000
}
if worth < 0 {
worth = 0
}
g.scr.Std.MvPrintw(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.invName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintw(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit(0)
}
// 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},
obj.Flags.Set(Known)
op.Know = true
return worth
}
// killname converts a code to a monster name (rip.c killname).
func (g *RogueGame) killname(monst byte, doart bool) string {
var sp string
var article bool
var (
sp string
article bool
)
if isUpper(monst) {
sp = g.Monsters[monst-'A'].Name
article = true
} else {
sp = "Wally the Wonder Badger"
article = false
for _, hp := range killnameTable {
for _, hp := range g.data.killnameTable {
if hp.Ch == monst {
sp = hp.Desc
article = hp.Print
break
}
}
}
if doart && article {
return "a" + vowelstr(sp) + " " + sp
}
return sp
}
// 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() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(gameEnd); ok {
return
}
panic(r)
}
}()
dnum := g.rnd(100)
for dnum--; dnum > 0; dnum-- {
g.rnd(100)
}
g.Player.Purse = g.rnd(100) + 1
g.Depth = g.rnd(100) + 1
g.death(g.deathMonst())
@@ -245,5 +274,6 @@ func (g *RogueGame) deathMonst() byte {
'Y', 'Z', 'a', 'b', 'h', 'd', 's',
' ', // generates the "Wally the Wonder Badger" message
}
return poss[g.rnd(len(poss))]
}

View File

@@ -11,21 +11,17 @@ type Rng struct {
Seed int32
}
// next steps the generator and returns the next raw value (the RN macro).
func (r *Rng) next() int {
r.Seed = r.Seed*11109 + 13849
return int(r.Seed>>16) & 0xffff
}
// Rnd picks a very random number in [0, rng) (main.c rnd).
func (r *Rng) Rnd(rng int) int {
if rng == 0 {
return 0
}
v := r.next()
if v < 0 {
v = -v
}
return v % rng
}
@@ -35,9 +31,17 @@ func (r *Rng) Roll(number, sides int) int {
for ; number > 0; number-- {
dtotal += r.Rnd(sides) + 1
}
return dtotal
}
// next steps the generator and returns the next raw value (the RN macro).
func (r *Rng) next() int {
r.Seed = r.Seed*11109 + 13849
return int(r.Seed>>16) & 0xffff
}
// rnd is the ported code's spelling of C rnd(): every call site in the C
// sources reads rnd(x), and keeping that shape makes cross-checking easy.
func (g *RogueGame) rnd(rng int) int { return g.Rng.Rnd(rng) }

View File

@@ -46,6 +46,7 @@ func TestRndZeroDoesNotStep(t *testing.T) {
if got := r.Rnd(0); got != 0 {
t.Fatalf("rnd(0) = %d, want 0", got)
}
if r.Seed != 42 {
t.Fatalf("rnd(0) stepped the generator: seed = %d, want 42", r.Seed)
}
@@ -58,6 +59,7 @@ func TestSpreadSmallValues(t *testing.T) {
if got := g.spread(1); got != 1 {
t.Errorf("spread(1) = %d, want 1", got)
}
if got := g.spread(2); got != 2 {
t.Errorf("spread(2) = %d, want 2", got)
}

View File

@@ -17,10 +17,11 @@ type mazeState struct {
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).
func (g *RogueGame) doRooms() {
func (g *RogueGame) digRooms() {
var bsze Coord // maximum room size
bsze.X = NumCols / 3
bsze.Y = NumLines / 3
// Clear things for a new level
@@ -32,92 +33,136 @@ func (g *RogueGame) doRooms() {
}
// Put the gone rooms, if any, on the level
leftOut := g.rnd(4)
for i := 0; i < leftOut; i++ {
g.Level.Rooms[g.rndRoom()].Flags.Set(Gone)
for range leftOut {
g.Level.Rooms[g.randomRoom()].Flags.Set(Gone)
}
// dig and populate all the rooms on the level
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]
// Find upper left corner of box that this room goes in
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
if rp.Flags.Has(Gone) {
// Place a gone room. Make certain that there is a blank line
// 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
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
break
}
}
continue
g.placeGoneRoom(rp, top, bsze)
return
}
// set room type
if g.rnd(10) < g.Depth-1 {
rp.Flags.Set(Dark) // dark room
if g.rnd(15) == 0 {
rp.Flags = Maze // maze room
}
}
// Find a place and size for a random room
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.Y = bsze.Y - 1
if rp.Pos.X = top.X; rp.Pos.X == 1 {
rp.Pos.X = 0
}
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
rp.Pos.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 {
rp.Max.X = g.rnd(bsze.X-4) + 4
rp.Max.Y = g.rnd(bsze.Y-4) + 4
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
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()
rp.GoldVal = g.goldCalc()
gold.GoldValue = rp.GoldVal
rp.Gold, _ = g.findFloorIn(rp, 0, false)
gold.Pos = rp.Gold
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
gold.Flags = Stackable
gold.Group = goldGrp
gold.Kind = KindGold
attachObj(&g.Level.Objects, gold)
}
// Put the monster in
g.Level.AddObject(gold)
}
// roomMonster maybe puts a monster in the room; gold attracts them
// (rooms.c do_rooms).
func (g *RogueGame) roomMonster(rp *Room) {
prob := 25
if rp.GoldVal > 0 {
prob = 80
}
if g.rnd(100) < prob {
tp := &Monster{}
mp, _ := g.findFloorIn(rp, 0, true)
g.newMonster(tp, g.randMonster(false), mp)
g.givePack(tp)
}
}
}
// 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).
func (g *RogueGame) drawRoom(rp *Room) {
if rp.Flags.Has(Maze) {
g.doMaze(rp)
g.digMaze(rp)
return
}
g.vert(rp, rp.Pos.X) // Draw left side
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
g.horiz(rp, rp.Pos.Y) // Draw top
@@ -145,8 +190,8 @@ func (g *RogueGame) horiz(rp *Room, starty int) {
}
}
// doMaze digs a maze (rooms.c do_maze).
func (g *RogueGame) doMaze(rp *Room) {
// digMaze digs a maze (rooms.c do_maze).
func (g *RogueGame) digMaze(rp *Room) {
m := &g.maze
for y := range m.maze {
for x := range m.maze[y] {
@@ -162,65 +207,84 @@ func (g *RogueGame) doMaze(rp *Room) {
starty := (g.rnd(rp.Max.Y) / 2) * 2
startx := (g.rnd(rp.Max.X) / 2) * 2
pos := Coord{Y: starty + m.starty, X: startx + m.startx}
g.putpass(pos)
g.putPassage(pos)
g.dig(starty, startx)
}
// dig digs out from around where we are now, if possible (rooms.c dig).
func (g *RogueGame) dig(y, x int) {
m := &g.maze
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
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
var nexty, nextx int
for _, cp := range del {
newy := y + cp.Y
newx := x + cp.X
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
continue
}
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
continue
}
if cnt++; g.rnd(cnt) == 0 {
nexty = newy
nextx = newx
}
}
if 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)
}
return nexty, nextx, cnt != 0
}
// accntMaze accounts for maze exits (rooms.c accnt_maze).
func (g *RogueGame) accntMaze(y, x, ny, nx int) {
// digWallGap picks the wall square to knock out between two maze cells
// (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]
for i := 0; i < sp.nexits; i++ {
for i := range sp.nexits {
if sp.exits[i].Y == ny && sp.exits[i].X == nx {
return
}
@@ -233,18 +297,21 @@ func (g *RogueGame) accntMaze(y, x, ny, nx int) {
}
}
// rndPos picks a random spot in a room (rooms.c rnd_pos).
func (g *RogueGame) rndPos(rp *Room) Coord {
// randomPos picks a random spot in a room (rooms.c rnd_pos).
func (g *RogueGame) randomPos(rp *Room) Coord {
var cp Coord
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
return cp
}
// findFloor finds a valid floor spot, picking a new random room each time
// around the loop (rooms.c find_floor with rp == NULL).
func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) {
return g.findFloorImpl(rp, limit, monst, rp == nil)
// around the loop; it retries forever (rooms.c find_floor with rp == NULL
// — every such C call site passed FALSE for the limit).
func (g *RogueGame) findFloor(monst bool) (Coord, bool) {
return g.findFloorImpl(nil, 0, monst, true)
}
// findFloorIn finds a valid floor spot in this room (rooms.c find_floor
@@ -253,14 +320,22 @@ func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
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) {
var compchar byte
if !pickroom {
compchar = Floor
if rp.Flags.Has(Maze) {
compchar = Passage
}
compchar = floorChar(rp)
}
cnt := limit
for {
if limit != 0 {
@@ -268,14 +343,14 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
return Coord{}, false
}
}
if pickroom {
rp = &g.Level.Rooms[g.rndRoom()]
compchar = Floor
if rp.Flags.Has(Maze) {
compchar = Passage
rp = &g.Level.Rooms[g.randomRoom()]
compchar = floorChar(rp)
}
}
cp := g.rndPos(rp)
cp := g.randomPos(rp)
pp := g.Level.At(cp.Y, cp.X)
if monst {
if pp.Monst == nil && stepOk(pp.Ch) {
@@ -291,14 +366,26 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
// enter_room).
func (g *RogueGame) enterRoom(cp Coord) {
p := &g.Player
rp := g.roomin(cp)
rp := g.roomIn(cp)
p.Room = rp
g.doorOpen(rp)
if !rp.Flags.Has(Dark) && !p.On(Blind) {
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
g.move(y, rp.Pos.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)
ch := g.Level.Char(y, x)
if tp == nil {
if g.inch() != ch {
@@ -306,23 +393,24 @@ func (g *RogueGame) enterRoom(cp Coord) {
} else {
g.move(y, x+1)
}
} else {
return
}
tp.OldCh = ch
if !g.seeMonst(tp) {
if p.On(SenseMonsters) {
if g.seeMonst(tp) {
g.addch(tp.Disguise)
return
}
if g.Player.On(SenseMonsters) {
g.standout()
g.addch(tp.Disguise)
g.standend()
} else {
g.addch(ch)
}
} else {
g.addch(tp.Disguise)
}
}
}
}
}
}
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
@@ -335,6 +423,7 @@ func (g *RogueGame) leaveRoom(cp Coord) {
}
var floor byte
switch {
case rp.Flags.Has(Gone):
floor = Passage
@@ -347,7 +436,18 @@ func (g *RogueGame) leaveRoom(cp Coord) {
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 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)
switch ch := g.inch(); ch {
case Floor:
if floor == ' ' {
@@ -356,13 +456,18 @@ func (g *RogueGame) leaveRoom(cp Coord) {
default:
// to check for monster, we have to strip out the standout
// bit (our Window returns the bare character already)
if isUpper(ch) {
if p.On(SenseMonsters) {
if !isUpper(ch) {
return
}
if g.Player.On(SenseMonsters) {
g.standout()
g.addch(ch)
g.standend()
break
return
}
pp := g.Level.At(y, x)
if pp.Ch == Door {
g.addch(Door)
@@ -370,8 +475,4 @@ func (g *RogueGame) leaveRoom(cp Coord) {
g.addch(floor)
}
}
}
}
}
g.doorOpen(rp)
}

View File

@@ -5,100 +5,65 @@ import (
"testing"
)
// TestRunScriptedSession drives a complete game through Run(): a few
// moves, a rest, an inventory, then Q-quit answered yes.
func TestRunScriptedSession(t *testing.T) {
tt := &testTerm{input: []byte("hjkl.i Qy")}
g := NewGame(Config{Seed: 99, Term: tt})
if err := g.Run(); err != nil {
t.Fatalf("Run: %v", err)
}
if g.Playing {
t.Error("still playing after quit")
}
// After quitting, the scoreboard is the last thing shown (in C it went
// to stdout after endwin; here it is drawn on the screen).
found := false
for y := 0; y < NumLines; y++ {
if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
found = true
}
}
if !found {
t.Error("score list not on screen after quit")
// 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()
g.startLevel()
g.prePlay()
for range n {
g.command()
}
}
// TestRunManyTurns mashes movement keys for a while as a crash sweep of
// the whole turn loop (daemons, hunger, monsters, combat), ending with a
// quit. The input alternates directions so the hero bumps around rooms.
func TestRunManyTurns(t *testing.T) {
// Spaces between commands double as answers to any --More-- prompts;
// without them a single prompt would swallow the rest of the script
// (wait_for eats everything that isn't a space).
var script []byte
moves := []byte("h h j j k k l l y u b n s s . . ")
for range 200 {
script = append(script, moves...)
}
script = append(script, " Q y Qy"...)
tt := &testTerm{input: script}
g := NewGame(Config{Seed: 31337, Term: tt})
if err := g.Run(); 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
// wizard style, then descends and keeps playing.
// TestRunDownStairs stands the hero on the staircase and descends via the
// '>' command through the real turn loop, then checks the level changed.
func TestRunDownStairs(t *testing.T) {
tt := &testTerm{input: []byte(">..Qy")}
g := NewGame(Config{Seed: 7, Term: tt})
// '>' 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 Run from regenerating the level
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)
if err := g.Run(); err != nil {
t.Fatalf("Run: %v", err)
}
driveTurns(t, g, 1)
if g.Depth != 2 {
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
if err := g.Run(); err != nil {
t.Fatalf("Run: %v", err)
// 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
for y := range NumLines {
if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
found = true
}
}
h, err := Restore(path, Config{Term: &testTerm{}})
if err != nil {
t.Fatalf("Restore: %v", err)
}
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
t.Error("restored game does not match saved game")
}
// The restored game must be playable.
h.scr.term.(*testTerm).input = []byte("..Qy")
if err := h.Run(); err != nil {
t.Fatalf("restored Run: %v", err)
if !found {
t.Error("score list not on screen")
}
}

View File

@@ -2,6 +2,7 @@ package game
import (
"encoding/gob"
"errors"
"fmt"
"os"
)
@@ -131,16 +132,19 @@ func (g *RogueGame) roomIdx(rp *Room) int {
if rp == nil {
return -1
}
for i := range g.Level.Rooms {
if rp == &g.Level.Rooms[i] {
return i
}
}
for i := range g.Level.Passages {
if rp == &g.Level.Passages[i] {
return 100 + i
}
}
return -1
}
@@ -152,6 +156,7 @@ func (g *RogueGame) roomAt(i int) *Room {
case i >= 0:
return &g.Level.Rooms[i]
}
return nil
}
@@ -160,18 +165,60 @@ func (g *RogueGame) packIdx(obj *Object) int {
if obj == nil {
return -1
}
for i, o := range g.Player.Pack {
if o == obj {
return i
}
}
return -1
}
// snapshot captures the complete game state.
func (g *RogueGame) snapshot() *SaveState {
p := &g.Player
st := &SaveState{
st := g.snapshotHeader()
// 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,
Seed: g.Rng.Seed,
Dnum: g.Dnum,
@@ -219,25 +266,14 @@ func (g *RogueGame) snapshot() *SaveState {
AllScore: g.AllScore,
Screen: g.scr.Std.Contents(),
}
}
// 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,
}
}
// snapshotPlayer captures the player, equipment as pack indices (the
// player half of snapshot).
func (g *RogueGame) snapshotPlayer() savedPlayer {
p := &g.Player
// 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
}
// the player
st.Player = savedPlayer{
sp := savedPlayer{
Body: savedCreature{
Pos: p.Pos, Turn: p.Turn, Type: p.Type, Disguise: p.Disguise,
OldCh: p.OldCh, Flags: p.Flags, Stats: p.Stats,
@@ -253,55 +289,72 @@ func (g *RogueGame) snapshot() *SaveState {
MaxStats: p.MaxStats, VfHit: p.VfHit,
}
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
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)
return sp
}
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 {
case m.Dest == nil:
case m.Dest == &p.Pos:
ref = destRef{Kind: 1}
default:
return destRef{}
case m.Dest == &g.Player.Pos:
return destRef{Kind: 1}
}
for mi, om := range g.Level.Monsters {
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 {
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 {
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 st
return destRef{}
}
// applySnapshot rebuilds live game state from a snapshot.
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.Dnum = st.Dnum
g.Whoami = st.Whoami
@@ -316,6 +369,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Passages = st.Passages
g.Level.Stairs = st.Stairs
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.Again = st.Again
g.NoScore = st.NoScoreF
@@ -346,22 +405,12 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.LastScore = st.LastScore
g.AllScore = st.AllScore
g.Playing = true
}
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)
}
// the player
// applyPlayer restores the player, resolving equipment pack indices
// (the player half of applySnapshot).
func (g *RogueGame) applyPlayer(st *SaveState) {
p := &g.Player
sp := &st.Player
p.Pos = sp.Body.Pos
p.Turn = sp.Body.Turn
@@ -371,15 +420,18 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.Flags = sp.Body.Flags
p.Stats = sp.Body.Stats
p.Room = g.roomAt(sp.Body.RoomIdx)
p.Pack = nil
for i := range sp.Body.Pack {
o := sp.Body.Pack[i]
p.Pack = append(p.Pack, &o)
}
pick := func(i int) *Object {
if i < 0 || i >= len(p.Pack) {
return nil
}
return p.Pack[i]
}
p.CurArmor = pick(sp.CurArmor)
@@ -395,11 +447,15 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.NoFood = sp.NoFood
p.MaxStats = sp.MaxStats
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
for i := range st.Monsters {
sc := &st.Monsters[i]
m := &Monster{Creature: Creature{
Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise,
OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats,
@@ -409,14 +465,21 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
o := sc.Pack[j]
m.Pack = append(m.Pack, &o)
}
g.Level.Monsters = append(g.Level.Monsters, 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 {
m := g.Level.Monsters[i]
switch ref.Kind {
case 1:
m.Dest = &p.Pos
m.Dest = &g.Player.Pos
case 2:
m.Dest = &g.Level.Monsters[ref.Idx].Pos
case 3:
@@ -425,136 +488,236 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
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
// goto over/gotfile flow becomes the useDefault flag.
// saveAnswer is a yes/no/escape prompt result in the save-game flow.
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() {
g.Msgs.Mpos = 0
over:
useDefault := false
if g.FileName != "" {
var c byte
prompt:
for {
g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0
if c == Escape {
g.msg("")
useDefault := false
if g.FileName != "" {
a := g.askDefaultSave()
if a == saveAbort {
return
}
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
break
}
g.msg("please answer Y or N")
}
if c == 'y' || c == 'Y' {
g.addstr("Yes\n")
g.refresh()
useDefault = true
}
useDefault = a == saveYes
}
for {
var buf string
if useDefault {
buf = g.FileName
useDefault = false
} else {
g.Msgs.Mpos = 0
g.msg("file name: ")
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
buf, ok := g.saveFileName(useDefault)
if !ok {
return
}
g.Msgs.Mpos = 0
useDefault = false
a := g.saveCheckOverwrite(buf)
if a == saveAbort {
return
}
if a == saveNo {
continue prompt // the C goto over: start again
}
g.FileName = buf
err := g.saveFile(g.FileName)
if err != nil {
g.msg("%s", err.Error())
continue
}
break prompt
}
}
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
if _, err := os.Stat(buf); err == nil {
_, 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
c := g.readchar()
if c == Escape {
switch g.readchar() {
case Escape:
g.msg("")
return
}
if c == 'y' || c == 'Y' {
break
}
if c == 'n' || c == 'N' {
goto over
return saveAbort
case 'y', 'Y':
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("Please answer Y or N")
}
g.msg("file name: %s", buf)
os.Remove(g.FileName)
}
g.FileName = buf
if err := g.saveFile(g.FileName); err != nil {
g.msg("%s", err.Error())
continue
}
break
}
g.myExit(0)
}
// saveFile writes the saved game (save.c save_file).
// 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.
func (g *RogueGame) saveFile(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400)
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400) //nolint:gosec,lll // G304: user-chosen save path
if err != nil {
return err
}
defer f.Close()
if err := gob.NewEncoder(f).Encode(g.snapshot()); err != nil {
os.Remove(path)
return err
encErr := gob.NewEncoder(f).Encode(g.snapshot())
closeErr := f.Close()
if encErr != nil || closeErr != nil {
_ = os.Remove(path) // don't leave a corrupt save behind
if encErr != nil {
return encErr
}
return closeErr
}
return os.Chmod(path, 0o400)
}
// AutoSave silently saves to the current file name; used on SIGHUP/SIGTERM
// (save.c auto_save).
// (save.c auto_save). Best effort by design: it runs on the way out of a
// dying process.
func (g *RogueGame) AutoSave() {
if g.FileName != "" {
os.Remove(g.FileName)
g.saveFile(g.FileName)
_ = os.Remove(g.FileName)
_ = g.saveFile(g.FileName)
}
}
// ErrSaveOutOfDate reports a save file from an incompatible version.
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
// deleted, as in C, to defeat restarting from the same save.
func Restore(path string, cfg Config) (*RogueGame, error) {
f, err := os.Open(path)
func Restore(path string, params Params) (*RogueGame, error) {
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
if err != nil {
return nil, err
}
defer f.Close()
defer func() { _ = f.Close() }() // read-only handle
var st SaveState
if err := gob.NewDecoder(f).Decode(&st); err != nil {
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w", path, err)
decErr := gob.NewDecoder(f).Decode(&st)
if decErr != nil {
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w",
path, decErr)
}
if st.Version != saveFormatVersion {
return nil, fmt.Errorf("sorry, saved game is out of date")
return nil, ErrSaveOutOfDate
}
g := &RogueGame{
data: newGameData(),
Rng: &Rng{},
Playing: true,
ScorePath: cfg.ScorePath,
ScorePath: params.ScorePath,
FileName: path,
rogueOpts: cfg.RogueOpts,
rogueOpts: params.RogueOpts,
restored: true,
}
g.scr = NewScreen(cfg.Term)
g.scr = NewScreen(params.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.applySnapshot(&st)
// defeat multiple restarting from the same place
if err := os.Remove(path); err != nil {
return nil, fmt.Errorf("cannot unlink file: %w", err)
rmErr := os.Remove(path)
if rmErr != nil {
return nil, fmt.Errorf("cannot unlink file: %w", rmErr)
}
return g, nil
}

View File

@@ -15,6 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
g.HasAmulet = true
g.Items.Potions[PotionHealing].Know = true
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???"
g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive
if len(g.Level.Monsters) > 0 {
g.Level.Monsters[0].Flags.Set(Awake)
@@ -22,72 +23,111 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
}
path := filepath.Join(t.TempDir(), "rogue.save")
if err := g.saveFile(path); err != nil {
t.Fatalf("saveFile: %v", err)
saveErr := g.saveFile(path)
if saveErr != nil {
t.Fatalf("saveFile: %v", saveErr)
}
h, err := Restore(path, Config{Term: &testTerm{}})
h, err := Restore(path, Params{Term: &testTerm{}})
if err != nil {
t.Fatalf("Restore: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
_, statErr := os.Stat(path)
if !os.IsNotExist(statErr) {
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 {
t.Errorf("player state lost: purse=%d food=%d",
h.Player.Purse, h.Player.FoodLeft)
}
if !h.HasAmulet {
t.Error("amulet flag lost")
}
if !h.Items.Potions[PotionHealing].Know {
t.Error("potion identification lost")
}
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
t.Error("scroll guess lost")
}
if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" {
t.Error("mutated bestiary lost")
}
if h.Rng.Seed != g.Rng.Seed {
t.Error("RNG state lost")
}
if renderMap(h) != renderMap(g) {
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) {
t.Fatalf("monster count %d != %d",
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]
if m.Dest != &h.Player.Pos {
t.Error("monster chase target not re-aliased to the hero")
}
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
t.Error("map monster index not rebuilt")
}
if m.Room == nil {
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()
t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d",
st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing,
len(st.Player.Body.Pack))
t.Logf("restored: CurWeapon=%p pack has %d items", h.Player.CurWeapon,
len(h.Player.Pack))
found := false
for i, o := range h.Player.Pack {
t.Logf(" pack[%d]=%p type=%v which=%d", i, o, o.Kind, o.Which)
if o == h.Player.CurWeapon {
found = true
}
}
if !found {
t.Error("restored CurWeapon is not aliased into the pack")
}
@@ -98,15 +138,24 @@ func TestRestoreRejectsWrongVersion(t *testing.T) {
path := filepath.Join(t.TempDir(), "rogue.save")
st := g.snapshot()
st.Version = "0.0.0"
f, err := os.Create(path)
f, err := os.Create(path) //nolint:gosec // G304: test temp path
if err != nil {
t.Fatal(err)
}
if err := gob.NewEncoder(f).Encode(st); err != nil {
t.Fatal(err)
encErr := gob.NewEncoder(f).Encode(st)
if encErr != nil {
t.Fatal(encErr)
}
f.Close()
if _, err := Restore(path, Config{}); err == nil {
closeErr := f.Close()
if closeErr != nil {
t.Fatal(closeErr)
}
_, restoreErr := Restore(path, Params{})
if restoreErr == nil {
t.Error("restore accepted an out-of-date save")
}
}

View File

@@ -25,29 +25,28 @@ type ScoreEnt struct {
Time int64
}
var scoreReasons = [4]string{
"killed",
"quit",
"A total winner",
"killed with Amulet",
}
// rdScore reads the scoreboard file (save.c rd_score).
func (g *RogueGame) rdScore() []ScoreEnt {
topTen := make([]ScoreEnt, numScores)
if g.ScorePath == "" {
return topTen
}
f, err := os.Open(g.ScorePath)
if err != nil {
return topTen
}
defer f.Close()
defer func() { _ = f.Close() }() // read-only handle
var onDisk []ScoreEnt
if err := gob.NewDecoder(f).Decode(&onDisk); err != nil {
return topTen
decErr := gob.NewDecoder(f).Decode(&onDisk)
if decErr != nil {
return topTen // unreadable scoreboard reads as empty, as in C
}
copy(topTen, onDisk)
return topTen
}
@@ -57,29 +56,45 @@ func (g *RogueGame) wrScore(topTen []ScoreEnt) {
return
}
// lock_sc/unlock_sc: exclusive-create lock file with stale takeover.
// The whole scoreboard write is best effort, as it was in C: a shared
// scoreboard must never take the game down.
lock := g.ScorePath + ".lck"
for range 5 {
lf, err := os.OpenFile(lock, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
lf, err := os.OpenFile(lock, //nolint:gosec // G304: configured path
os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err == nil {
lf.Close()
defer os.Remove(lock)
_ = lf.Close()
defer func() { _ = os.Remove(lock) }()
break
}
if fi, serr := os.Stat(lock); serr == nil &&
time.Since(fi.ModTime()) > 10*time.Second {
os.Remove(lock)
fi, statErr := os.Stat(lock)
if statErr == nil && time.Since(fi.ModTime()) > staleLockAge {
_ = os.Remove(lock)
continue
}
time.Sleep(time.Second)
}
f, err := os.OpenFile(g.ScorePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
f, err := os.OpenFile(g.ScorePath,
os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
if err != nil {
return
}
defer f.Close()
gob.NewEncoder(f).Encode(topTen)
_ = gob.NewEncoder(f).Encode(topTen)
_ = f.Close()
}
// staleLockAge is how old a scoreboard lock file may be before another
// process assumes its owner died and takes it over (mach_dep.c lock_sc
// aged its lock the same way).
const staleLockAge = 10 * time.Second
// score figures the score and posts it (rip.c score). flags -1 means just
// display the list (the -s command line option).
func (g *RogueGame) score(amount, flags int, monst byte) {
@@ -93,37 +108,50 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
// Insert her in list if need be
ins := -1
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()
scp := len(topTen)
for i := range topTen {
if amount > topTen[i].Score {
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
scp := g.scoreSlot(topTen, amount, flags, uid)
if scp >= len(topTen) {
return -1
}
}
if scp < len(topTen) {
sc2 := len(topTen) - 1
if flags != 2 && !g.AllScore {
for i := scp; i < len(topTen); i++ {
if topTen[i].UID == uid && topTen[i].Flags != 2 {
sc2 = i
break
}
}
}
for sc2 > scp {
topTen[sc2] = topTen[sc2-1]
sc2--
}
lvl := g.Depth
if flags == 2 {
lvl = g.MaxDepth
}
topTen[scp] = ScoreEnt{
UID: uid,
Score: amount,
@@ -133,59 +161,93 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
Level: lvl,
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"
if g.AllScore {
label = "Scores"
}
lines := []string{
fmt.Sprintf("Top Ten %s:", label),
" Score Name",
}
highlight := -1
for i := range topTen {
scp := &topTen[i]
if scp.Score == 0 {
break
}
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 {
line += fmt.Sprintf(" by %s", g.killname(scp.Monster, true))
line += " by " + g.killname(scp.Monster, true)
}
line += "."
if i == ins {
highlight = len(lines)
}
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()
for i, line := range lines {
if i == highlight {
g.standout()
}
g.mvaddstr(i, 0, line)
if i == highlight {
g.standend()
}
}
g.refresh()
} else {
for _, line := range lines {
fmt.Println(line)
}
}
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
g.refresh()
}
// 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
// (arrows become hjkl, control keys their C0 codes).
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.
@@ -37,26 +40,28 @@ type Window struct {
func NewWindow(rows, cols int) *Window {
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
w.Clear()
return w
}
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
// Move positions the cursor (curses move/wmove).
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
// GetYX reports the cursor position (curses getyx).
func (w *Window) GetYX() (y, x int) { return w.cy, w.cx }
func (w *Window) GetYX() (int, int) { return w.cy, w.cx }
// AddCh writes a character at the cursor and advances it (curses addch).
func (w *Window) AddCh(ch byte) {
if ch == '\n' {
w.cy, w.cx = w.cy+1, 0
return
}
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return
}
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
if w.cx++; w.cx >= w.cols {
w.cx = 0
@@ -68,7 +73,7 @@ func (w *Window) AddCh(ch byte) {
// AddStr writes a string at the cursor (curses addstr).
func (w *Window) AddStr(s string) {
for i := 0; i < len(s); i++ {
for i := range len(s) {
w.AddCh(s[i])
}
}
@@ -85,15 +90,15 @@ func (w *Window) MvAddStr(y, x int, s string) {
w.AddStr(s)
}
// Printw writes formatted text at the cursor (curses printw).
func (w *Window) Printw(format string, a ...any) {
// Printwf writes formatted text at the cursor (curses printw).
func (w *Window) Printwf(format string, a ...any) {
w.AddStr(fmt.Sprintf(format, a...))
}
// MvPrintw moves then writes formatted text (curses mvprintw).
func (w *Window) MvPrintw(y, x int, format string, a ...any) {
// MvPrintwf moves then writes formatted text (curses mvprintw).
func (w *Window) MvPrintwf(y, x int, format string, a ...any) {
w.Move(y, x)
w.Printw(format, a...)
w.Printwf(format, a...)
}
// Inch returns the character under the cursor (curses inch, sans
@@ -102,12 +107,14 @@ func (w *Window) Inch() byte {
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return ' '
}
return w.at(w.cy, w.cx).ch
}
// MvInch moves then reads (curses mvinch).
func (w *Window) MvInch(y, x int) byte {
w.Move(y, x)
return w.Inch()
}
@@ -120,6 +127,7 @@ func (w *Window) Clear() {
for i := range w.cells {
w.cells[i] = cell{ch: ' '}
}
w.cy, w.cx = 0, 0
}
@@ -128,6 +136,7 @@ func (w *Window) Clrtoeol() {
if w.cy < 0 || w.cy >= w.rows {
return
}
for x := w.cx; x < w.cols; x++ {
*w.at(w.cy, x) = cell{ch: ' '}
}
@@ -138,13 +147,14 @@ func (w *Window) CopyFrom(src *Window) {
copy(w.cells, src.cells)
}
// Size reports the window dimensions.
func (w *Window) Size() (rows, cols int) { return w.rows, w.cols }
// Size reports the window dimensions as rows, columns.
func (w *Window) Size() (int, int) { return w.rows, w.cols }
// CellAt reports the character and standout attribute at a position; used
// by Terminal implementations to render the window.
func (w *Window) CellAt(y, x int) (ch byte, standout bool) {
func (w *Window) CellAt(y, x int) (byte, bool) {
c := w.at(y, x)
return c.ch, c.standout
}
@@ -155,6 +165,7 @@ func (w *Window) Contents() []byte {
for i, c := range w.cells {
out[i] = c.ch
}
return out
}
@@ -171,12 +182,16 @@ func (w *Window) SetContents(data []byte) {
// victory screens.
func (w *Window) Line(y int) string {
buf := make([]byte, w.cols)
for x := 0; x < w.cols; x++ {
for x := range w.cols {
buf[x] = w.at(y, x).ch
}
return string(buf)
}
// at addresses the cell at (y, x) in the backing array.
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
// Screen bundles the two windows the game draws on with the device that
// shows them.
type Screen struct {
@@ -201,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).
func (s *Screen) RefreshWin(w *Window) {
if s.term != nil {
@@ -217,7 +239,7 @@ func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) }
func (g *RogueGame) mvaddstr(y, x int, s string) {
g.scr.Std.MvAddStr(y, x, s)
}
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printw(f, a...) }
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printwf(f, a...) }
func (g *RogueGame) inch() byte { return g.scr.Std.Inch() }
func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
func (g *RogueGame) standout() { g.scr.Std.Standout(true) }

View File

@@ -2,30 +2,23 @@ package game
// 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
// (scrolls.c read_scroll).
func (g *RogueGame) readScroll() {
p := &g.Player
obj := g.getItem("read", KindScroll)
if obj == nil {
obj, ok := g.promptPackItem("read", KindScroll)
if !ok {
return
}
if obj.Kind != KindScroll {
if !g.Options.Terse {
g.msg("there is nothing on it to read")
} else {
g.msg("nothing to read")
}
return
}
// Calculate the effect it has on the poor guy.
@@ -35,61 +28,117 @@ func (g *RogueGame) readScroll() {
// Get rid of the thing
g.leavePack(obj, false, false)
switch obj.ScrollKind() {
case ScrollMonsterConfusion:
if h := g.data.readHandlers[obj.ScrollKind()]; h != nil {
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.
p.Flags.Set(CanConfuse)
g.Player.Flags.Set(CanConfuse)
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 {
p.CurArmor.ArmorClass--
p.CurArmor.Flags.Clear(Cursed)
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
// chasing after the hero.
held := 0
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
if x < 0 || x >= NumCols {
continue
}
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
if y < 0 || y > NumLines-1 {
continue
}
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
mp.Flags.Clear(Awake)
mp.Flags.Set(Held)
held++
}
}
}
held := g.holdMonstersNear()
if held > 0 {
g.addmsg("the monster")
g.addmsgf("the monster")
if held > 1 {
g.addmsg("s around you")
g.addmsgf("s around you")
}
g.addmsg(" freeze")
g.addmsgf(" freeze")
if held == 1 {
g.addmsg("s")
g.addmsgf("s")
}
g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true
} else {
g.msg("you feel a strange sense of loss")
}
case ScrollSleep:
}
// 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
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
if x < 0 || x >= NumCols {
continue
}
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
if y < 0 || y > NumLines-1 {
continue
}
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
mp.Flags.Clear(Awake)
mp.Flags.Set(Held)
held++
}
}
}
return held
}
func (g *RogueGame) readSleep(*Object) {
// Scroll which makes you fall asleep
g.Items.Scrolls[ScrollSleep].Know = true
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
p.Flags.Clear(Awake)
g.Player.Flags.Clear(Awake)
g.msg("you fall asleep")
case ScrollCreateMonster:
}
func (g *RogueGame) readCreateMonster(*Object) {
// Create a monster: first look in a circle around him, next try
// 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
var mp Coord
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
// Don't put a monster on top of the player.
@@ -99,68 +148,75 @@ func (g *RogueGame) readScroll() {
// Or anything else nasty
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
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
}
}
if i++; g.rnd(i) == 0 {
mp = Coord{Y: y, X: x}
}
}
}
}
if i == 0 {
g.msg("you hear a faint cry of anguish in the distance")
} else {
tp := &Monster{}
g.newMonster(tp, g.randMonster(false), mp)
}
case ScrollIdentifyPotion, ScrollIdentifyScroll, ScrollIdentifyWeapon, ScrollIdentifyArmor, ScrollIdentifyRingOrStick:
return mp, i != 0
}
func (g *RogueGame) readIdentify(obj *Object) {
// Identify, let him figure something out
g.Items.Scrolls[obj.Which].Know = true
g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name)
g.whatis(true, idType[obj.ScrollKind()])
case ScrollMagicMapping:
g.whatis(true, g.data.idType[obj.ScrollKind()])
}
func (g *RogueGame) readMagicMapping(*Object) {
// Scroll of magic mapping.
g.Items.Scrolls[ScrollMagicMapping].Know = true
g.msg("oh, now this scroll has a map on it")
// take all the things we want to keep hidden out of the window
for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ {
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)
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
pass := false
switch ch {
case Door, Stairs:
case '-', '|':
if !pp.Flags.Has(FReal) {
ch = Door
pp.Ch = Door
pp.Flags.Set(FReal)
}
ch = revealWall(pp)
case ' ':
if pp.Flags.Has(FReal) {
// 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
}
pass = revealSolid(pp)
case Passage:
pass = true
case Floor:
if pp.Flags.Has(FReal) {
ch = ' '
} else {
ch = Trap
pp.Ch = Trap
pp.Flags.Set(FSeen | FReal)
}
ch = revealFloor(pp)
default:
if pp.Flags.Has(FPassage) {
pass = true
@@ -168,78 +224,138 @@ func (g *RogueGame) readScroll() {
ch = ' '
}
}
if pass {
if !pp.Flags.Has(FReal) {
pp.Ch = Passage
}
pp.Flags.Set(FSeen | FReal)
ch = Passage
}
if ch != ' ' {
if tp := pp.Monst; tp != nil {
tp.OldCh = ch
if !p.On(SenseMonsters) {
g.mvaddch(y, x, ch)
return ch
}
// revealWall handles a wall cell for magic mapping: a secret door
// 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 ' '
}
}
}
case ScrollFoodDetection:
pp.Ch = Trap
pp.Flags.Set(FSeen | FReal)
return Trap
}
func (g *RogueGame) readFoodDetection(*Object) {
// Food detection
found := false
g.scr.Hw.Clear()
for _, fo := range g.Level.Objects {
if fo.Kind == KindFood {
found = true
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
}
}
if found {
g.Items.Scrolls[ScrollFoodDetection].Know = true
g.showWin("Your nose tingles and you smell food.--More--")
} else {
g.msg("your nose tingles")
}
case ScrollTeleportation:
}
func (g *RogueGame) readTeleportation(*Object) {
// Scroll of teleportation: make him disappear and reappear
p := &g.Player
curRoom := p.Room
g.teleport()
if curRoom != p.Room {
g.Items.Scrolls[ScrollTeleportation].Know = true
}
case ScrollEnchantWeapon:
}
func (g *RogueGame) readEnchantWeapon(*Object) {
p := &g.Player
if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon {
g.msg("you feel a strange sense of loss")
} else {
p.CurWeapon.Flags.Clear(Cursed)
if g.rnd(2) == 0 {
p.CurWeapon.HPlus++
} else {
p.CurWeapon.DPlus++
}
g.msg("your %s glows %s for a moment",
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
// boo.
g.msg("you hear maniacal laughter in the distance")
case ScrollRemoveCurse:
}
func (g *RogueGame) readRemoveCurse(*Object) {
p := &g.Player
uncurse(p.CurArmor)
uncurse(p.CurWeapon)
uncurse(p.CurRing[Left])
uncurse(p.CurRing[Right])
g.msg("%s", g.chooseStr("you feel in touch with the Universal Onenes",
"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
// and sets them running towards the hero
g.aggravate()
g.msg("you hear a high pitched humming noise")
case ScrollProtectArmor:
}
func (g *RogueGame) readProtectArmor(*Object) {
p := &g.Player
if p.CurArmor != nil {
p.CurArmor.Flags.Set(Protected)
g.msg("your armor is covered by a shimmering %s shield",
@@ -247,11 +363,6 @@ func (g *RogueGame) readScroll() {
} else {
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).

View File

@@ -4,25 +4,77 @@ import "fmt"
// 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).
func (g *RogueGame) doZap() {
p := &g.Player
obj := g.getItem("zap with", KindWand)
if obj == nil {
obj, ok := g.promptPackItem("zap with", KindWand)
if !ok {
return
}
if obj.Kind != KindWand {
g.After = false
g.msg("you can't zap with that!")
return
}
if obj.Charges == 0 {
g.msg("nothing happens")
return
}
switch obj.WandKind() {
case WandLight:
if h := g.data.zapHandlers[obj.WandKind()]; h != nil {
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
p := &g.Player
g.Items.Sticks[WandLight].Know = true
if p.Room.Flags.Has(Gone) {
g.msg("the corridor glows and then fades")
@@ -30,70 +82,107 @@ func (g *RogueGame) doZap() {
p.Room.Flags.Clear(Dark)
// Light the room and put the player back up
g.enterRoom(p.Pos)
g.addmsg("the room is lit")
g.addmsgf("the room is lit")
if !g.Options.Terse {
g.addmsg(" by a shimmering %s light", g.pickColor("blue"))
g.addmsgf(" by a shimmering %s light", g.pickColor("blue"))
}
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
// from the monsters in the room (or next to hero if he is in a
// passage)
if p.Stats.HP < 2 {
if g.Player.Stats.HP < 2 {
g.msg("you are too weak to use it")
return
return false
}
g.drain()
case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation:
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
}
if tp := g.Level.MonsterAt(y, x); tp != nil {
monster := tp.Type
if monster == 'F' {
p.Flags.Clear(Held)
}
switch obj.WandKind() {
case WandInvisibility:
return true
}
func (g *RogueGame) zapInvisibility(*Object) bool {
if tp := g.zapVictim(); tp != nil {
tp.Flags.Set(Invisible)
if g.cansee(y, x) {
g.mvaddch(y, x, tp.OldCh)
if g.canSee(tp.Pos.Y, tp.Pos.X) {
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
detachMon(&g.Level.Monsters, tp)
g.Level.RemoveMonster(tp)
if g.seeMonst(tp) {
g.mvaddch(y, x, g.Level.Char(y, x))
}
oldch := tp.OldCh
g.Delta.Y = y
g.Delta.X = x
monster = byte(g.rnd(26) + 'A')
monster := g.randomMonsterLetter()
g.newMonster(tp, monster, g.Delta)
if g.seeMonst(tp) {
g.mvaddch(y, x, monster)
}
tp.OldCh = oldch
tp.Pack = pp
if g.seeMonst(tp) {
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.Clear(Invisible | CanConfuse)
tp.Disguise = tp.Type
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
if obj.WandKind() == WandTeleportAway {
for {
newPos, _ = g.findFloor(nil, 0, true)
newPos, _ = g.findFloor(true)
if newPos != p.Pos {
break
}
@@ -102,59 +191,87 @@ func (g *RogueGame) doZap() {
newPos.Y = p.Pos.Y + g.Delta.Y
newPos.X = p.Pos.X + g.Delta.X
}
tp.Dest = &p.Pos
tp.Flags.Set(Awake)
g.relocate(tp, newPos)
}
}
case WandMagicMissile:
return true
}
func (g *RogueGame) zapMagicMissile(*Object) bool {
p := &g.Player
g.Items.Sticks[WandMagicMissile].Know = true
bolt := newObject()
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
bolt.HurlDmg = dice("1x4")
bolt.HPlus = 100
bolt.DPlus = 1
bolt.Flags = Missile
if p.CurWeapon != nil {
bolt.Launch = WeaponKind(p.CurWeapon.Which)
}
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
!g.saveThrow(VsMagic, &tp.Stats) {
g.hitMonster(bolt.Pos, bolt)
} else if g.Options.Terse {
g.msg("missle vanishes")
g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
} else {
g.msg("the missle vanishes with a puff of smoke")
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
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
return true
}
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 {
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) {
tp.Flags.Clear(Slowed)
} else {
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) {
tp.Flags.Clear(Hasted)
} else {
tp.Flags.Set(Slowed)
}
tp.Turn = true
}
g.Delta.Y = y
g.Delta.X = x
g.runto(g.Delta)
}
case WandLightning, WandFire, WandCold:
}
func (g *RogueGame) zapBolt(obj *Object) bool {
var name string
switch obj.WandKind() {
case WandLightning:
name = "bolt"
@@ -163,11 +280,11 @@ func (g *RogueGame) doZap() {
default:
name = "ice"
}
g.fireBolt(p.Pos, &g.Delta, name)
g.fireBolt(g.Player.Pos, &g.Delta, name)
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).
@@ -178,20 +295,24 @@ func (g *RogueGame) drain() {
if g.Level.Char(p.Pos.Y, p.Pos.X) == Door {
corp = &g.Level.Passages[*g.Level.FlagsAt(p.Pos.Y, p.Pos.X)&FPassNum]
}
inpass := p.Room.Flags.Has(Gone)
var drainee []*Monster
for _, mp := range g.Level.Monsters {
if 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) {
if g.drainReaches(mp, corp, inpass) {
drainee = append(drainee, mp)
}
}
cnt := len(drainee)
if cnt == 0 {
g.msg("you have a tingling feeling")
return
}
p.Stats.HP /= 2
cnt = p.Stats.HP / cnt
// Now zot all of the monsters
@@ -199,11 +320,23 @@ func (g *RogueGame) drain() {
if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 {
g.killed(mp, g.seeMonst(mp))
} 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
// place (sticks.c fire_bolt).
func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
@@ -217,99 +350,45 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
bolt.HPlus = 100
bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name
var dirch byte
switch dir.Y + dir.X {
case 0:
dirch = '/'
case 1, -1:
if dir.Y == 0 {
dirch = '-'
} else {
dirch = '|'
}
case 2, -2:
dirch = '\\'
}
dirch := boltDirChar(*dir)
pos := start
hitHero := !fromHero
used := false
changed := false
var spotpos []Coord
for len(spotpos) < BoltLength && !used {
pos.Y += dir.Y
pos.X += dir.X
spotpos = append(spotpos, pos)
ch := g.Level.VisibleChar(pos.Y, pos.X)
bounce := false
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 boltBounces(ch, p.Pos, pos) {
if !changed {
hitHero = !hitHero
}
changed = false
dir.Y = -dir.Y
dir.X = -dir.X
spotpos = spotpos[:len(spotpos)-1]
g.msg("the %s bounces", name)
continue
}
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
hitHero = true
changed = !changed
tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos
used = true
if tp.Type == 'D' && name == "flame" {
g.addmsg("the flame bounces")
if !g.Options.Terse {
g.addmsg(" off the dragon")
}
g.endmsg()
} else {
g.hitMonster(pos, bolt)
}
} else if ch != 'M' || tp.Disguise == 'M' {
if fromHero {
g.runto(pos)
}
if g.Options.Terse {
g.msg("%s misses", name)
} else {
g.msg("the %s whizzes past %s", name, g.setMname(tp))
}
}
used = g.boltStrikesMonster(tp, bolt, pos, ch, name, fromHero)
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
if !g.save(VsMagic) {
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
if fromHero {
g.death('b')
} else {
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
}
}
used = true
if g.Options.Terse {
g.msg("the %s hits", name)
} else {
g.msg("you are hit by the %s", name)
}
} else {
g.msg("the %s whizzes by you", name)
}
used = g.boltStrikesHero(start, name, fromHero)
}
g.mvaddch(pos.Y, pos.X, dirch)
g.refresh()
}
@@ -319,13 +398,112 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
}
}
// 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)
if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos
if tp.Type == 'D' && name == "flame" {
g.addmsgf("the flame bounces")
if !g.Options.Terse {
g.addmsgf(" off the dragon")
}
g.endmsg()
} else {
g.hitMonster(pos, bolt)
}
return true
}
if ch != 'M' || tp.Disguise == 'M' {
if fromHero {
g.runTo(pos)
}
if g.Options.Terse {
g.msg("%s misses", name)
} else {
g.msg("the %s whizzes past %s", name, g.setMname(tp))
}
}
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 fromHero {
g.death('b')
} else {
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
}
}
if g.Options.Terse {
g.msg("the %s hits", name)
} else {
g.msg("you are hit by the %s", name)
}
return true
}
// fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" {
if g.Items.WandType[cur.Which] == staffName {
cur.Damage = dice("2x3")
} else {
cur.Damage = dice("1x1")
}
cur.HurlDmg = dice("1x1")
switch cur.WandKind() {
@@ -342,8 +520,10 @@ func chargeStr(g *RogueGame, obj *Object) string {
if !obj.Flags.Has(Known) {
return ""
}
if g.Options.Terse {
return fmt.Sprintf(" [%d]", obj.Charges)
}
return fmt.Sprintf(" [%d charges]", obj.Charges)
}

View File

@@ -1,15 +1,174 @@
package game
// This file is the immutable data from extern.c and init.c: tables that are
// never written after program start. 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.
// tables.go — the static data tables the C game kept as file-scope globals
// (extern.c, init.c, and assorted per-file statics). The port gathers them
// into gameData: every RogueGame carries its own copy, so the package holds
// 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.
var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12}
// helpEntry is rogue.h struct h_list.
type helpEntry struct {
Ch byte
Desc string
Print bool
}
// aClass is extern.c a_class[]: armor class for each armor type.
var aClass = [NumArmorTypes]int{
// gameData is the bundle of static tables, built by newGameData and hung on
// 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
7, // RING_MAIL
7, // STUDDED_LEATHER
@@ -18,17 +177,14 @@ var aClass = [NumArmorTypes]int{
4, // SPLINT_MAIL
4, // BANDED_MAIL
3, // PLATE_MAIL
}
},
// eLevels is extern.c e_levels[]: experience thresholds per level; the
// zero terminates the table as in C.
var eLevels = []int{
eLevels: []int{
10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000,
50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0,
}
},
// trName is extern.c tr_name[]: names of the traps.
var trName = [NumTrapTypes]string{
trName: [NumTrapTypes]string{
"a trapdoor",
"an arrow trap",
"a sleeping gas trap",
@@ -37,15 +193,11 @@ var trName = [NumTrapTypes]string{
"a poison dart trap",
"a rust trap",
"a mysterious trap",
}
},
// invTName is extern.c inv_t_name[]: the inventory style names.
var invTName = []string{"Overwrite", "Slow", "Clear"}
invTName: []string{"Overwrite", "Slow", "Clear"},
// 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.
var monsterTable = [26]MonsterKind{
monsterTable: [26]MonsterKind{
/* Name CARRY FLAGS str exp lvl arm hp dmg */
{"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}},
@@ -73,12 +225,9 @@ var monsterTable = [26]MonsterKind{
{"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}},
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
}
},
// Base ObjInfo tables (extern.c). These are templates: NewGame copies them
// into ItemLore before initProbs converts Prob to cumulative form.
var baseThings = [NumThings]ObjInfo{
baseThings: [NumThings]ObjInfo{
{Prob: 26}, // potion
{Prob: 36}, // scroll
{Prob: 16}, // food
@@ -86,9 +235,9 @@ var baseThings = [NumThings]ObjInfo{
{Prob: 7}, // armor
{Prob: 4}, // ring
{Prob: 4}, // stick
}
},
var baseArmInfo = [NumArmorTypes]ObjInfo{
baseArmInfo: [NumArmorTypes]ObjInfo{
{Name: "leather armor", Prob: 20, Worth: 20},
{Name: "ring mail", Prob: 15, Worth: 25},
{Name: "studded leather armor", Prob: 15, Worth: 20},
@@ -97,9 +246,9 @@ var baseArmInfo = [NumArmorTypes]ObjInfo{
{Name: "splint mail", Prob: 10, Worth: 80},
{Name: "banded mail", Prob: 10, Worth: 90},
{Name: "plate mail", Prob: 5, Worth: 150},
}
},
var basePotInfo = [NumPotionTypes]ObjInfo{
basePotInfo: [NumPotionTypes]ObjInfo{
{Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", 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: "blindness", Prob: 5, Worth: 5},
{Name: "levitation", Prob: 6, Worth: 75},
}
},
var baseRingInfo = [NumRingTypes]ObjInfo{
baseRingInfo: [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", Prob: 9, Worth: 400},
{Name: "sustain strength", Prob: 5, Worth: 280},
@@ -131,9 +280,9 @@ var baseRingInfo = [NumRingTypes]ObjInfo{
{Name: "teleportation", Prob: 5, Worth: 30},
{Name: "stealth", Prob: 7, Worth: 470},
{Name: "maintain armor", Prob: 5, Worth: 380},
}
},
var baseScrInfo = [NumScrollTypes]ObjInfo{
baseScrInfo: [NumScrollTypes]ObjInfo{
{Name: "monster confusion", Prob: 7, Worth: 140},
{Name: "magic mapping", Prob: 4, Worth: 150},
{Name: "hold monster", Prob: 2, Worth: 180},
@@ -152,9 +301,9 @@ var baseScrInfo = [NumScrollTypes]ObjInfo{
{Name: "remove curse", Prob: 7, Worth: 105},
{Name: "aggravate monsters", Prob: 3, Worth: 20},
{Name: "protect armor", Prob: 2, Worth: 250},
}
},
var baseWeapInfo = [NumWeaponTypes + 1]ObjInfo{
baseWeapInfo: [NumWeaponTypes + 1]ObjInfo{
{Name: "mace", Prob: 11, Worth: 8},
{Name: "long sword", Prob: 11, 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: "spear", Prob: 12, Worth: 5},
{}, // DO NOT REMOVE: fake entry for dragon's breath
}
},
var baseWsInfo = [NumWandTypes]ObjInfo{
baseWsInfo: [NumWandTypes]ObjInfo{
{Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5},
{Name: "lightning", Prob: 3, Worth: 330},
@@ -182,18 +331,17 @@ var baseWsInfo = [NumWandTypes]ObjInfo{
{Name: "teleport away", Prob: 6, Worth: 340},
{Name: "teleport to", Prob: 6, Worth: 50},
{Name: "cancellation", Prob: 5, Worth: 280},
}
},
// rainbow is init.c rainbow[]: the possible potion colors.
var rainbow = []string{
rainbow: []string{
"amber", "aquamarine", "black", "blue", "brown", "clear", "crimson",
"cyan", "ecru", "gold", "green", "grey", "magenta", "orange", "pink",
"plaid", "purple", "red", "silver", "tan", "tangerine", "topaz",
"turquoise", "vermilion", "violet", "white", "yellow",
}
},
// sylls is init.c sylls[]: syllables for generated scroll names.
var sylls = []string{
//nolint:misspell // "ther" is a C scroll syllable, kept faithfully
sylls: []string{
"a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash",
"bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp",
"con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el",
@@ -210,10 +358,9 @@ var sylls = []string{
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
"zok", "zon", "zum",
}
},
// stoneTable is init.c stones[]: ring stones and their worth.
var stoneTable = []Stone{
stoneTable: []Stone{
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
{"germanium", 225}, {"granite", 5}, {"garnet", 50},
@@ -223,35 +370,25 @@ var stoneTable = []Stone{
{"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200},
{"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70},
{"taaffeite", 300}, {"zircon", 80},
}
},
// woods is init.c wood[]: what staffs are made of.
var woods = []string{
woods: []string{
"avocado wood", "balsa", "bamboo", "banyan", "birch", "cedar",
"cherry", "cinnibar", "cypress", "dogwood", "driftwood", "ebony",
"elm", "eucalyptus", "fall", "hemlock", "holly", "ironwood",
"kukui wood", "mahogany", "manzanita", "maple", "oaken",
"persimmon wood", "pecan", "pine", "poplar", "redwood", "rosewood",
"spruce", "teak", "walnut", "zebrawood",
}
},
// metals is init.c metal[]: what wands are made of.
var metals = []string{
metals: []string{
"aluminum", "beryllium", "bone", "brass", "bronze", "copper",
"electrum", "gold", "iron", "lead", "magnesium", "mercury",
"nickel", "pewter", "platinum", "steel", "silver", "silicon",
"tin", "titanium", "tungsten", "zinc",
}
},
// helpEntry is rogue.h struct h_list.
type helpEntry struct {
Ch byte
Desc string
Print bool
}
// helpStr is extern.c helpstr[]: the '?' command help text.
var helpStr = []helpEntry{
helpStr: []helpEntry{
{'?', " prints help", true},
{'/', " identify object", true},
{'h', " left", true},
@@ -317,6 +454,401 @@ var helpStr = []helpEntry{
{'!', " shell escape", true},
{'F', "<dir> fight till either of you dies", 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:

View File

@@ -9,16 +9,20 @@ func TestProbabilitiesSumTo100(t *testing.T) {
for _, oi := range info {
s += oi.Prob
}
return s
}
data := newGameData()
tables := map[string][]ObjInfo{
"things": baseThings[:],
"potions": basePotInfo[:],
"scrolls": baseScrInfo[:],
"rings": baseRingInfo[:],
"sticks": baseWsInfo[:],
"weapons": baseWeapInfo[:NumWeaponTypes], // excludes the flame entry
"armor": baseArmInfo[:],
"things": data.baseThings[:],
"potions": data.basePotInfo[:],
"scrolls": data.baseScrInfo[:],
"rings": data.baseRingInfo[:],
"sticks": data.baseWsInfo[:],
"weapons": data.baseWeapInfo[:NumWeaponTypes], // excludes the flame entry
"armor": data.baseArmInfo[:],
}
for name, tab := range tables {
if s := sum(tab); s != 100 {
@@ -28,11 +32,13 @@ func TestProbabilitiesSumTo100(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
if last != 100 {
t.Errorf("cumulative potion probability ends at %d, want 100", last)
}
for i := PotionKind(1); i < NumPotionTypes; i++ {
if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob {
t.Errorf("potion probs not nondecreasing at %d", i)
@@ -41,46 +47,77 @@ func TestInitProbsCumulative(t *testing.T) {
}
func TestNewGameRandomizesAppearances(t *testing.T) {
g := NewGame(Config{Seed: 12345})
seen := map[string]bool{}
for i, c := range g.Items.PotColors {
if c == "" {
t.Fatalf("potion %d has no color", i)
}
if seen[c] {
t.Errorf("potion color %q assigned twice", c)
}
seen[c] = true
}
for i, n := range g.Items.ScrNames {
if n == "" {
t.Fatalf("scroll %d has no name", i)
}
if len(n) > MaxNameLen+1 {
t.Errorf("scroll name %q longer than C buffer allows", n)
}
}
for i := range g.Items.WandType {
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" {
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
}
if g.Items.WandMade[i] == "" {
t.Errorf("stick %d has no material", i)
}
}
g := New(Params{Seed: 12345})
checkPotionColors(t, g)
checkScrollNames(t, g)
checkWandMaterials(t, g)
// Determinism: same seed, same appearances.
h := NewGame(Config{Seed: 12345})
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{}
for i, c := range g.Items.PotColors {
if c == "" {
t.Fatalf("potion %d has no color", i)
}
if seen[c] {
t.Errorf("potion color %q assigned twice", c)
}
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 {
if n == "" {
t.Fatalf("scroll %d has no name", i)
}
if len(n) > MaxNameLen+1 {
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 {
if g.Items.WandType[i] != wandName && g.Items.WandType[i] != staffName {
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
}
if g.Items.WandMade[i] == "" {
t.Errorf("stick %d has no material", i)
}
}
}
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")
}
if monsterTable['D'-'A'].Name != "dragon" {
if data.monsterTable['D'-'A'].Name != "dragon" {
t.Error("letter indexing broken")
}
}

View File

@@ -11,15 +11,20 @@ type testTerm struct {
func (t *testTerm) Render(*Window) {}
func (t *testTerm) Fini() {}
func (t *testTerm) ReadChar() byte {
if t.pos < len(t.input) {
c := t.input[t.pos]
t.pos++
return c
}
t.tick++
if t.tick%2 == 0 {
return '\n'
}
return ' '
}

View File

@@ -9,105 +9,163 @@ import (
// invName returns the name of something as it would appear in an inventory
// (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
which := obj.Which
it := &g.Items
switch obj.Kind {
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:
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:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case KindScroll:
if obj.Count == 1 {
pb.WriteString("A scroll ")
} else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
}
op := &it.Scrolls[which]
if op.Know {
fmt.Fprintf(&pb, "of %s", op.Name)
} else if op.Guess != "" {
fmt.Fprintf(&pb, "called %s", op.Guess)
} else {
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
}
g.nameScroll(&pb, obj)
case KindFood:
if which == 1 {
if obj.Count == 1 {
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else {
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
}
} else {
if obj.Count == 1 {
pb.WriteString("Some food")
} else {
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
}
}
g.nameFood(&pb, obj)
case KindWeapon:
sp := it.Weapons[which].Name
if obj.Count > 1 {
fmt.Fprintf(&pb, "%d ", obj.Count)
} else {
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
}
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else {
pb.WriteString(sp)
}
if obj.Count > 1 {
pb.WriteString("s")
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
}
g.nameWeapon(&pb, obj)
case KindArmor:
sp := it.Armors[which].Name
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
} else {
pb.WriteString(sp)
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
}
g.nameArmor(&pb, obj)
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
out := pb.String()
if g.InvDescribe {
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 {
pb.WriteString("A scroll ")
} else {
fmt.Fprintf(pb, "%d scrolls ", obj.Count)
}
op := &g.Items.Scrolls[obj.Which]
switch {
case op.Know:
fmt.Fprintf(pb, "of %s", op.Name)
case op.Guess != "":
fmt.Fprintf(pb, "called %s", op.Guess)
default:
fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
}
}
// 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 {
fmt.Fprintf(pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else {
fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
}
return
}
if obj.Count == 1 {
pb.WriteString("Some food")
} else {
fmt.Fprintf(pb, "%d rations of food", obj.Count)
}
}
// 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 {
fmt.Fprintf(pb, "%d ", obj.Count)
} else {
fmt.Fprintf(pb, "A%s ", vowelstr(sp))
}
if obj.Flags.Has(Known) {
fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else {
pb.WriteString(sp)
}
if obj.Count > 1 {
pb.WriteString("s")
}
if obj.Label != "" {
fmt.Fprintf(pb, " called %s", obj.Label)
}
}
// 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) {
fmt.Fprintf(pb, "%s %s [",
num(g.data.aClass[obj.Which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
} else {
pb.WriteString(sp)
}
if obj.Label != "" {
fmt.Fprintf(pb, " called %s", obj.Label)
}
}
// describeWorn appends the equipped-status notes to an inventory name
// (things.c inv_name).
func (g *RogueGame) describeWorn(obj *Object, out string) string {
if !g.InvDescribe {
return out
}
p := &g.Player
if obj == p.CurArmor {
out += " (being worn)"
}
if obj == p.CurWeapon {
out += " (weapon in hand)"
}
if obj == p.CurRing[Left] {
switch obj {
case p.CurRing[Left]:
out += " (on left hand)"
} else if obj == p.CurRing[Right] {
case p.CurRing[Right]:
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]) {
out = string(toLower(out[0])) + out[1:]
} else if !drop && isLower(out[0]) {
out = string(toUpper(out[0])) + out[1:]
return string(toLower(out[0])) + out[1:]
}
if !drop && isLower(out[0]) {
return string(toUpper(out[0])) + out[1:]
}
return out
}
@@ -115,29 +173,36 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
// leavePack/detach vocabulary collision).
func (g *RogueGame) dropIt() {
p := &g.Player
ch := g.Level.Char(p.Pos.Y, p.Pos.X)
if ch != Floor && ch != Passage {
g.After = false
g.msg("there is something there already")
return
}
obj := g.getItem("drop", KindNone)
if obj == nil {
obj, ok := g.promptPackItem("drop", KindNone)
if !ok {
return
}
if !g.dropCheck(obj) {
return
}
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
// 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.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
obj.Pos = p.Pos
if obj.Kind == KindAmulet {
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|
@@ -146,35 +211,52 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
if obj == nil {
return true
}
p := &g.Player
if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] {
return true
}
if obj.Flags.Has(Cursed) {
g.msg("you can't. It appears to be cursed")
return false
}
if obj == p.CurWeapon {
switch obj {
case p.CurWeapon:
p.CurWeapon = nil
} else if obj == p.CurArmor {
case p.CurArmor:
g.wasteTime()
p.CurArmor = nil
} else {
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
if obj == p.CurRing[Left] {
hand = Left
}
p.CurRing[hand] = nil
switch obj.RingKind() {
case RingAddStrength:
g.chgStr(-obj.Bonus)
g.changeStrength(-obj.Bonus)
case RingSeeInvisible:
g.unsee(0)
g.Extinguish(DUnsee)
}
}
return true
}
// newThing returns a new random thing for the dungeon (things.c new_thing).
@@ -193,6 +275,7 @@ func (g *RogueGame) newThing() *Object {
} else {
kind = pickOne(g, g.Items.Things[:])
}
switch kind {
case 0:
cur.Kind = KindPotion
@@ -201,33 +284,67 @@ func (g *RogueGame) newThing() *Object {
cur.Kind = KindScroll
cur.Which = pickOne(g, g.Items.Scrolls[:])
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
g.Player.NoFood = 0
if g.rnd(10) != 0 {
cur.Which = 0
} else {
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])))
if r := g.rnd(100); r < 10 {
cur.Flags.Set(Cursed)
cur.HPlus -= g.rnd(3) + 1
} else if r < 15 {
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.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 {
cur.Flags.Set(Cursed)
cur.ArmorClass += g.rnd(3) + 1
} else if r < 28 {
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.Which = pickOne(g, g.Items.Rings[:])
switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
@@ -238,12 +355,6 @@ func (g *RogueGame) newThing() *Object {
case RingAggravateMonsters, RingTeleportation:
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
@@ -255,6 +366,7 @@ func pickOne(g *RogueGame, info []ObjInfo) int {
return idx
}
}
return 0 // bad pick_one: C resets to the start of the table
}
@@ -272,20 +384,27 @@ type invPage struct {
// (things.c discovered).
func (g *RogueGame) discovered() {
var ch byte
for {
discList := false
if !g.Options.Terse {
g.addmsg("for ")
g.addmsgf("for ")
}
g.addmsg("what type")
g.addmsgf("what type")
if !g.Options.Terse {
g.addmsg(" of object do you want a list")
g.addmsgf(" of object do you want a list")
}
g.msg("? (* for all)")
ch = g.readchar()
switch ch {
case Escape:
g.msg("")
return
case Potion, Scroll, Ring, Stick, '*':
discList = true
@@ -297,10 +416,12 @@ func (g *RogueGame) discovered() {
Potion, Scroll, Ring, Stick)
}
}
if discList {
break
}
}
if ch == '*' {
g.printDisc(Potion)
g.addLine("")
@@ -320,6 +441,7 @@ func (g *RogueGame) discovered() {
// (things.c print_disc).
func (g *RogueGame) printDisc(typ byte) {
var info []ObjInfo
switch typ {
case Scroll:
info = g.Items.Scrolls[:]
@@ -330,18 +452,23 @@ func (g *RogueGame) printDisc(typ byte) {
case Stick:
info = g.Items.Sticks[:]
}
order := make([]int, len(info))
g.setOrder(order)
obj := Object{Count: 1}
numFound := 0
for i := range info {
if info[order[i]].Know || info[order[i]].Guess != "" {
obj.Kind = objectKindForGlyph(typ)
obj.Which = order[i]
g.addLine("%s", g.invName(&obj, false))
g.addLine("%s", g.inventoryName(&obj, false))
numFound++
}
}
if numFound == 0 {
g.addLine("%s", g.nothing(typ))
}
@@ -353,6 +480,7 @@ func (g *RogueGame) setOrder(order []int) {
for i := range order {
order[i] = i
}
for i := len(order); i > 0; i-- {
r := g.rnd(i)
order[i-1], order[r] = order[r], order[i-1]
@@ -366,8 +494,8 @@ const flushSentinel = "\x00"
func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage
prompt := "--Press space to continue--"
isFlush := format == flushSentinel
var line string
if !isFlush {
line = fmt.Sprintf(format, a...)
@@ -375,41 +503,68 @@ func (g *RogueGame) addLine(format string, a ...any) int {
if pg.lineCnt == 0 {
g.scr.Hw.Clear()
if g.Options.InvType == InvSlow {
g.Msgs.Mpos = 0
}
}
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 g.msg("%s", line) == Escape {
return Escape
}
}
pg.lineCnt++
} else {
g.invPage.lineCnt++
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 {
pg.maxlen = len(prompt)
pg.init = true
}
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 {
// Overlay the accumulated list in a box at the top right
// of the screen, prompt, and restore what was beneath.
g.msg("")
g.refresh()
saved := NewWindow(NumLines, NumCols)
saved.CopyFrom(g.scr.Std)
lx := NumCols - pg.maxlen - 2
for y := 0; y <= pg.lineCnt; y++ {
for x := 0; x <= pg.maxlen; x++ {
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
}
}
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
g.refresh()
g.waitFor(' ')
g.scr.Std.CopyFrom(saved)
g.refresh()
g.addLineOverlay(prompt)
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
@@ -417,20 +572,36 @@ func (g *RogueGame) addLine(format string, a ...any) int {
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
}
// 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.refresh()
saved := NewWindow(NumLines, NumCols)
saved.CopyFrom(g.scr.Std)
lx := NumCols - pg.maxlen - 2
for y := 0; y <= pg.lineCnt; y++ {
for x := 0; x <= pg.maxlen; x++ {
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
}
}
return ^Escape
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
g.refresh()
g.waitFor(' ')
g.scr.Std.CopyFrom(saved)
g.refresh()
}
// flushLine is add_line(NULL): force out the accumulated page.
@@ -447,6 +618,7 @@ func (g *RogueGame) endLine() {
g.flushLine()
}
}
pg.lineCnt = 0
pg.newpage = false
}
@@ -459,20 +631,24 @@ func (g *RogueGame) nothing(typ byte) string {
} else {
out = "Haven't discovered anything"
}
if typ != '*' {
var tystr string
switch typ {
case Potion:
tystr = "potion"
tystr = potionName
case Scroll:
tystr = "scroll"
tystr = scrollName
case Ring:
tystr = "ring"
tystr = ringName
case Stick:
tystr = "stick"
}
out += fmt.Sprintf(" about any %ss", tystr)
}
return out
}
@@ -480,20 +656,22 @@ func (g *RogueGame) nothing(typ byte) string {
// (things.c nameit).
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
if op.Know || op.Guess != "" {
switch {
case op.Know || op.Guess != "":
if obj.Count == 1 {
fmt.Fprintf(pb, "A %s ", typ)
} else {
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
}
if op.Know {
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
} else {
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which)
}
} else if obj.Count == 1 {
case obj.Count == 1:
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
} else {
default:
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
}
}
@@ -505,13 +683,17 @@ func nullstr(*RogueGame, *Object) string { return "" }
// pr_list).
func (g *RogueGame) prList() {
if !g.Options.Terse {
g.addmsg("for ")
g.addmsgf("for ")
}
g.addmsg("what type")
g.addmsgf("what type")
if !g.Options.Terse {
g.addmsg(" of object do you want a list")
g.addmsgf(" of object do you want a list")
}
g.msg("? ")
ch := g.readchar()
switch ch {
case Potion:
@@ -533,14 +715,17 @@ func (g *RogueGame) prList() {
// (things.c pr_spec).
func (g *RogueGame) prSpec(info []ObjInfo) {
lastprob := 0
i := byte('0')
for idx := range info {
if i == '9'+1 {
i = 'a'
}
g.addLine("%c: %s (%d%%)", i, info[idx].Name, info[idx].Prob-lastprob)
lastprob = info[idx].Prob
i++
}
g.endLine()
}

View File

@@ -90,22 +90,29 @@ const (
VsMagic = 3
)
// Flags for rooms (rogue.h)
// RoomFlags are the room state bits (rogue.h room flags).
type RoomFlags int16
// Room state bits (rogue.h ISDARK/ISGONE/ISMAZE).
const (
Dark RoomFlags = 1 << iota // room is dark
Gone // room is gone (a corridor)
Maze // room is a maze
)
func (f RoomFlags) Has(b RoomFlags) bool { return f&b != 0 }
// Has reports whether any of the given bits are set.
func (f *RoomFlags) Has(b RoomFlags) bool { return *f&b != 0 }
// Set turns the given bits on.
func (f *RoomFlags) Set(b RoomFlags) { *f |= b }
// Clear turns the given bits off.
func (f *RoomFlags) Clear(b RoomFlags) { *f &^= b }
// Flags for objects (rogue.h)
// ObjFlags are the object state bits (rogue.h object flags).
type ObjFlags int32
// Object state bits (rogue.h).
const (
Cursed ObjFlags = 1 << iota // ISCURSED: object is cursed
Known // ISKNOW: player knows details about the object
@@ -115,15 +122,22 @@ const (
Protected // ISPROT: armor is permanently protected
)
func (f ObjFlags) Has(b ObjFlags) bool { return f&b != 0 }
// Has reports whether any of the given bits are set.
func (f *ObjFlags) Has(b ObjFlags) bool { return *f&b != 0 }
// Set turns the given bits on.
func (f *ObjFlags) Set(b ObjFlags) { *f |= b }
// Clear turns the given bits off.
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
// Flags for creatures (rogue.h). The C bit collisions are deliberate and
// preserved: one name of each pair applies to monsters, the other to the
// hero, and they never coexist on one creature.
// CreatureFlags are the creature state bits (rogue.h creature flags). The
// C bit collisions are deliberate and preserved: one name of each pair
// applies to monsters, the other to the hero, and they never coexist on
// one creature.
type CreatureFlags int32
// Creature state bits (rogue.h).
const (
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: creature can see invisible creatures
@@ -146,13 +160,20 @@ const (
Slowed CreatureFlags = 0o100000 // ISSLOW: creature has been slowed
)
func (f CreatureFlags) Has(b CreatureFlags) bool { return f&b != 0 }
// Has reports whether any of the given bits are set.
func (f *CreatureFlags) Has(b CreatureFlags) bool { return *f&b != 0 }
// Set turns the given bits on.
func (f *CreatureFlags) Set(b CreatureFlags) { *f |= b }
// Clear turns the given bits off.
func (f *CreatureFlags) Clear(b CreatureFlags) { *f &^= b }
// Flags for the level map (rogue.h)
// PlaceFlags are the per-map-cell bits (rogue.h level map flags). The low
// bits double as the passage number (FPassNum) or trap kind (FTrapMask).
type PlaceFlags uint8
// Map cell bits (rogue.h).
const (
FPassage PlaceFlags = 0x80 // F_PASS: is a passageway
FSeen PlaceFlags = 0x40 // have seen this spot before
@@ -163,14 +184,20 @@ const (
FTrapMask PlaceFlags = 0x07 // F_TMASK: trap number mask
)
func (f PlaceFlags) Has(b PlaceFlags) bool { return f&b != 0 }
// Has reports whether any of the given bits are set.
func (f *PlaceFlags) Has(b PlaceFlags) bool { return *f&b != 0 }
// Set turns the given bits on.
func (f *PlaceFlags) Set(b PlaceFlags) { *f |= b }
// Clear turns the given bits off.
func (f *PlaceFlags) Clear(b PlaceFlags) { *f &^= b }
// TrapKind identifies a trap (rogue.h trap types). The kind is stored in
// the low bits of a map cell's PlaceFlags (FTrapMask).
type TrapKind int
// Trap kinds (rogue.h T_* constants).
const (
TrapDoor TrapKind = 0
TrapArrow TrapKind = 1
@@ -183,15 +210,6 @@ const (
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).
type PotionKind int
@@ -214,14 +232,6 @@ const (
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).
type ScrollKind int
@@ -248,14 +258,6 @@ const (
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).
type WeaponKind int
@@ -271,16 +273,11 @@ const (
WeaponShuriken
WeaponSpear
WeaponFlame // fake entry for dragon breath (ick)
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
}
// NumWeaponTypes counts the real weapons; the flame pseudo-weapon sits
// just past them in the tables (C's MAXWEAPONS == FLAME).
const NumWeaponTypes = WeaponFlame
// ArmorKind identifies a suit of armor (rogue.h armor types).
type ArmorKind int
@@ -298,14 +295,6 @@ const (
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).
type RingKind int
@@ -328,14 +317,6 @@ const (
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).
type WandKind int
@@ -358,14 +339,6 @@ const (
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
// ce(a,b) macro is plain == here.
type Coord struct {
@@ -425,6 +398,7 @@ func CTRL(c byte) byte { return c & 0o37 }
func distance(y1, x1, y2, x2 int) int {
dx := x2 - x1
dy := y2 - y1
return dx*dx + dy*dy
}
@@ -439,5 +413,6 @@ func sign(nm int) int {
case nm > 0:
return 1
}
return 0
}

View File

@@ -9,13 +9,15 @@ const noWeapon WeaponKind = -1
// missile fires a missile in a given direction (weapons.c missile).
func (g *RogueGame) missile(ydelta, xdelta int) {
// Get which thing we are hurling
obj := g.getItem("throw", KindWeapon)
if obj == nil {
obj, ok := g.promptPackItem("throw", KindWeapon)
if !ok {
return
}
if !g.dropCheck(obj) || g.isCurrent(obj) {
return
}
obj = g.leavePack(obj, true, false)
g.doMotion(obj, ydelta, xdelta)
// AHA! Here it has hit something. If it is a wall or a door, or if
@@ -33,28 +35,36 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
// Come fly with us ...
obj.Pos = p.Pos
for {
// Erase the old one
if obj.Pos != p.Pos && g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.eraseFlight(obj, p.Pos)
// 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)
if ch == Floor && !g.showFloor() {
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).
@@ -62,22 +72,27 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
if fpos, ok := g.fallpos(obj.Pos); ok {
pp := g.Level.At(fpos.Y, fpos.X)
pp.Ch = obj.Kind.Glyph()
obj.Pos = fpos
if g.cansee(fpos.Y, fpos.X) {
if g.canSee(fpos.Y, fpos.X) {
if pp.Monst != nil {
pp.Monst.OldCh = obj.Kind.Glyph()
} else {
g.mvaddch(fpos.Y, fpos.X, obj.Kind.Glyph())
}
}
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
return
}
if pr {
if g.HasHit {
g.endmsg()
g.HasHit = false
}
g.msg("the %s vanishes as it hits the ground",
g.Items.Weapons[obj.Which].Name)
}
@@ -92,56 +107,58 @@ func (g *RogueGame) hitMonster(mp Coord, obj *Object) bool {
// wield pulls out a certain weapon (weapons.c wield).
func (g *RogueGame) wield() {
p := &g.Player
oweapon := p.CurWeapon
if !g.dropCheck(p.CurWeapon) {
p.CurWeapon = oweapon
return
}
p.CurWeapon = oweapon
obj := g.getItem("wield", KindWeapon)
if obj == nil {
obj, ok := g.promptPackItem("wield", KindWeapon)
if !ok {
g.After = false
return
}
if obj.Kind == KindArmor {
g.msg("you can't wield armor")
g.After = false
return
}
if g.isCurrent(obj) {
g.After = false
return
}
sp := g.invName(obj, true)
p.CurWeapon = obj
if !g.Options.Terse {
g.addmsg("you are now ")
if g.isCurrent(obj) {
g.After = false
return
}
sp := g.inventoryName(obj, true)
p.CurWeapon = obj
if !g.Options.Terse {
g.addmsgf("you are now ")
}
g.msg("wielding %s (%c)", sp, obj.PackCh)
}
// initWeaps is the weapons.c init_dam[] table.
var initWeaps = [NumWeaponTypes]struct {
// weaponSetup is one row of the weapons.c init_dam[] table (see
// gameData.initWeaps).
type weaponSetup struct {
dam DiceSpec // damage when wielded
hrl DiceSpec // damage when thrown
launch WeaponKind // launching weapon
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).
func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
iwp := &initWeaps[which]
iwp := &g.data.initWeaps[which]
weap.Kind = KindWeapon
weap.Which = int(which)
weap.Damage = iwp.dam
@@ -149,16 +166,19 @@ func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
weap.Launch = iwp.launch
weap.Flags = iwp.flags
weap.HPlus = 0
weap.DPlus = 0
if which == WeaponDagger {
switch {
case which == WeaponDagger:
weap.Count = g.rnd(4) + 2
weap.Group = g.Items.Group
g.Items.Group++
} else if weap.Flags.Has(Stackable) {
case weap.Flags.Has(Stackable):
weap.Count = g.rnd(8) + 8
weap.Group = g.Items.Group
g.Items.Group++
} else {
default:
weap.Count = 1
weap.Group = 0
}
@@ -170,6 +190,7 @@ func num(n1, n2 int, typ byte) string {
if typ == Weapon {
out += fmt.Sprintf(",%+d", n2)
}
return out
}
@@ -177,7 +198,9 @@ func num(n1, n2 int, typ byte) string {
// (weapons.c fallpos).
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
var newpos Coord
cnt := 0
for y := pos.Y - 1; y <= pos.Y+1; y++ {
for x := pos.X - 1; x <= pos.X+1; x++ {
// check to make certain the spot is empty, if it is, put the
@@ -187,6 +210,7 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
y < 0 || x < 0 {
continue
}
ch := g.Level.Char(y, x)
if ch == Floor || ch == Passage {
if cnt++; g.rnd(cnt) == 0 {
@@ -196,5 +220,6 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
}
}
}
return newpos, cnt != 0
}

View File

@@ -8,50 +8,86 @@ package game
// create_obj).
func (g *RogueGame) createObj() {
obj := newObject()
g.msg("type of item: ")
obj.Kind = objectKindForGlyph(g.readchar())
g.Msgs.Mpos = 0
g.msg("which %c do you want? (0-f)", obj.Kind.Glyph())
ch := g.readchar()
if isDigit(ch) {
obj.Which = int(ch - '0')
} else {
obj.Which = int(ch-'a') + 10
}
obj.Group = 0
obj.Count = 1
g.Msgs.Mpos = 0
switch {
case obj.Kind == KindWeapon || obj.Kind == KindArmor:
switch obj.Kind {
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)")
bless := g.readchar()
g.Msgs.Mpos = 0
if bless == '-' {
obj.Flags.Set(Cursed)
}
if obj.Kind == KindWeapon {
g.initWeapon(obj, WeaponKind(obj.Which))
if bless == '-' {
obj.HPlus -= g.rnd(3) + 1
}
if bless == '+' {
obj.HPlus += g.rnd(3) + 1
}
} else {
obj.ArmorClass = aClass[obj.Which]
return
}
obj.ArmorClass = g.data.aClass[obj.Which]
if bless == '-' {
obj.ArmorClass += g.rnd(3) + 1
}
if bless == '+' {
obj.ArmorClass -= g.rnd(3) + 1
}
}
case obj.Kind == 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() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)")
bless := g.readchar()
g.Msgs.Mpos = 0
if bless == '-' {
obj.Flags.Set(Cursed)
obj.Bonus = -1
@@ -61,34 +97,28 @@ func (g *RogueGame) createObj() {
case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed)
}
case obj.Kind == KindWand:
g.fixStick(obj)
case obj.Kind == 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).
func (g *RogueGame) showMap() {
hw := g.scr.Hw
hw.Clear()
for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ {
real := g.Level.FlagsAt(y, x).Has(FReal)
if !real {
for x := range NumCols {
isReal := g.Level.FlagsAt(y, x).Has(FReal)
if !isReal {
hw.Standout(true)
}
hw.MvAddCh(y, x, g.Level.Char(y, x))
if !real {
if !isReal {
hw.Standout(false)
}
}
}
g.showWin("---More (level map)---")
}
@@ -97,30 +127,12 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you don't have anything in your pack to identify")
return
}
var obj *Object
for {
obj = g.getItem("identify", kind)
if !insist {
break
}
if g.NObjs == 0 {
return
}
if obj == nil {
g.msg("you must identify something")
} else if kind != KindNone && obj.Kind != kind &&
!(kind == KindRingOrStick &&
(obj.Kind == KindRing || obj.Kind == KindWand)) {
g.msg("you must identify a %s", kind)
} else {
break
}
}
if obj == nil {
obj, ok := g.whatisPick(insist, kind)
if !ok {
return
}
@@ -136,7 +148,39 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
case KindRing:
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
@@ -154,15 +198,18 @@ func setKnow(obj *Object, info []ObjInfo) {
func (g *RogueGame) teleport() {
p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
c, _ := g.findFloor(nil, 0, true)
if g.roomin(c) != p.Room {
c, _ := g.findFloor(true)
if g.roomIn(c) != p.Room {
g.leaveRoom(p.Pos)
p.Pos = c
g.enterRoom(p.Pos)
} else {
p.Pos = c
g.look(true)
}
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
// turn off ISHELD in case teleportation was done while fighting a
// Flytrap
@@ -171,6 +218,7 @@ func (g *RogueGame) teleport() {
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
}
g.NoMove = 0
g.Count = 0
g.Running = false

View File

@@ -5,6 +5,8 @@
package term
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
@@ -21,6 +23,9 @@ type Tcell struct {
last *game.Window // last rendered window, for resize redraws
}
// ErrScreenTooSmall reports a terminal below the required 80x24.
var ErrScreenTooSmall = errors.New("screen too small")
// New initializes the terminal. The screen must be at least 80x24, as the
// C game required.
func New() (*Tcell, error) {
@@ -28,16 +33,22 @@ func New() (*Tcell, error) {
if err != nil {
return nil, err
}
if err := s.Init(); err != nil {
return nil, err
initErr := s.Init()
if initErr != nil {
return nil, initErr
}
w, h := s.Size()
if h < game.NumLines || w < game.NumCols {
s.Fini()
return nil, fmt.Errorf("sorry, the screen must be at least %dx%d",
game.NumLines, game.NumCols)
return nil, fmt.Errorf("sorry, %w: %dx%d required",
ErrScreenTooSmall, game.NumCols, game.NumLines)
}
s.HideCursor()
return &Tcell{screen: s}, nil
}
@@ -49,17 +60,21 @@ func (t *Tcell) Fini() {
// Render blits a game window to the terminal (curses refresh).
func (t *Tcell) Render(w *game.Window) {
t.last = w
rows, cols := w.Size()
for y := 0; y < rows; y++ {
for x := 0; x < cols; x++ {
for y := range rows {
for x := range cols {
ch, standout := w.CellAt(y, x)
style := tcell.StyleDefault
if standout {
style = style.Reverse(true)
}
t.screen.SetContent(x, y, rune(ch), nil, style)
}
}
t.screen.Show()
}
@@ -74,62 +89,114 @@ func (t *Tcell) ReadChar() byte {
t.Render(t.last)
}
case *tcell.EventKey:
switch ev.Key() {
case tcell.KeyUp:
return 'k'
case tcell.KeyDown:
return 'j'
case tcell.KeyLeft:
return 'h'
case tcell.KeyRight:
return 'l'
case tcell.KeyHome:
return 'y'
case tcell.KeyPgUp:
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 {
return byte(ev.Key())
}
if r := ev.Rune(); r > 0 && r < 0x80 {
return byte(r)
}
if b, ok := translateKey(ev); ok {
return b
}
}
}
}
// translateKey converts a key event to a game input byte; ok is false
// for keys the C game does not understand.
func translateKey(ev *tcell.EventKey) (byte, bool) {
if b, ok := namedKey(ev.Key()); ok {
return b, true
}
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
return byte(ev.Key()), true //nolint:gosec // G115: 1..26 fits
}
if r := ev.Rune(); r > 0 && r < 0x80 {
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
// shell + md_shellescape).
func (t *Tcell) ShellEscape() {
if err := t.screen.Suspend(); err != nil {
err := t.screen.Suspend()
if err != nil {
return
}
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}
fmt.Println("[Entering shell; exit to return to the game]")
cmd := exec.Command(shell)
_, _ = fmt.Fprintln(os.Stdout,
"[Entering shell; exit to return to the game]")
// The shell session has no deadline by design; Background context.
cmd := exec.CommandContext(context.Background(), //nolint:gosec // G204: the user's own $SHELL
shell)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
t.screen.Resume()
_ = cmd.Run() // best effort: the shell is the user's business
resumeErr := t.screen.Resume()
if resumeErr != nil {
panic(resumeErr) // terminal resume failure is unrecoverable
}
}