18 Commits

Author SHA1 Message Date
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
49 changed files with 3542 additions and 1803 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

43
MEMORY.md Normal file
View File

@@ -0,0 +1,43 @@
# 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). Complexity linters (cyclop, gocognit,
nestif) stay enabled and red until refactor step 7 fixes the findings,
per sneak 2026-07-07. 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.

116
TODO.md
View File

@@ -29,12 +29,64 @@ Refactor ground rules:
# Next Step # Next Step
Adopt the house Go linting standards: copy .golangci.yml from the Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
prompts repo and bring game/, term/, and cmd/ lint-clean (the port is switches become per-kind handler tables of small named methods,
greenfield code, so no exemptions apply). keeping effect order and RNG call sequence identical. This step also
clears the outstanding cyclop/gocognit/nestif lint findings.
# Completed Steps # Completed Steps
- 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 - 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue
(go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string). (go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string).
- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split - 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split
@@ -76,45 +128,27 @@ greenfield code, so no exemptions apply).
# Future Steps # Future Steps
1. Refactor step 4: method renames, movement/world subsystem 1. Refactor step 8: constructor and style pass per the house
(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); styleguide — game.New(game.Params{...}) replacing NewGame(Config);
replace the gameEnd panic unwind with error-based turn results where replace the gameEnd panic unwind with error-based turn results where
feasible; 77-column wrap sweep. feasible; 77-column wrap sweep.
6. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the 2. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
post-refactor names; add the C name → Go name rename table. post-refactor names; add the C name → Go name rename table.
7. Playtest hardening pass: play several full games with the tcell 3. Playtest hardening pass: play several full games with the tcell
binary and extend run_test.go to script a deeper multi-level binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping, playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests. from the C behavior that this uncovers, with regression tests.
8. Verify the seed-compatibility claim against the C reference on 4. Verify the seed-compatibility claim against the C reference on
c-master: same seed, same dungeon, same item tables, for several c-master: same seed, same dungeon, same item tables, for several
seeds. seeds.
9. Broaden unit test coverage where playtesting finds thin spots 5. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands). (rings, sticks, wizard commands).
10. Tag a release once a full game (Amulet retrieval and score entry) 6. Tag a release once a full game (Amulet retrieval and score entry)
completes without defects. completes without defects.
11. Full-terminal-size support (deferred by explicit decision 7. Full-terminal-size support (deferred by explicit decision
2026-07-06): per-game dungeon dimensions instead of the 80x24 2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode. tuning at larger sizes, and a --classic 80x24 mode.
12. Note: this repo is exempt from the standard policy scaffold. Do not 8. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md. add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -18,61 +18,45 @@ import (
) )
func main() { func main() {
os.Exit(run())
}
// run carries the real main so that deferred terminal restoration runs
// before the process exits (os.Exit skips defers).
func run() int {
scores := flag.Bool("s", false, "print the scoreboard and exit") scores := flag.Bool("s", false, "print the scoreboard and exit")
deathDemo := flag.Bool("d", false, "die a random death (demo)") deathDemo := flag.Bool("d", false, "die a random death (demo)")
flag.Parse() flag.Parse()
home, _ := os.UserHomeDir() cfg := loadConfig()
// 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,
}
if *scores { if *scores {
g := game.NewGame(cfg) game.NewGame(cfg).ShowScores()
g.ShowScores()
return return 0
} }
t, err := term.New() t, err := term.New()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return 1
} }
defer t.Fini() defer t.Fini()
cfg.Term = t cfg.Term = t
var g *game.RogueGame var g *game.RogueGame
if args := flag.Args(); len(args) == 1 && !*deathDemo { if args := flag.Args(); len(args) == 1 && !*deathDemo {
// restore a saved game // restore a saved game
g, err = game.Restore(args[0], cfg) g, err = game.Restore(args[0], cfg)
if err != nil { if err != nil {
t.Fini() t.Fini()
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return 1
} }
} else { } else {
g = game.NewGame(cfg) g = game.NewGame(cfg)
@@ -80,22 +64,75 @@ func main() {
if *deathDemo { if *deathDemo {
g.DeathDemo() g.DeathDemo()
return
return 0
} }
// SIGHUP/SIGTERM autosave (save.c auto_save) installAutosave(g, t)
runErr := g.Run()
if runErr != nil {
t.Fini()
fmt.Fprintln(os.Stderr, runErr)
return 1
}
return 0
}
// loadConfig gathers the game configuration from the environment: home
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
// (main.c's startup).
func loadConfig() game.Config {
home, _ := os.UserHomeDir()
name := ""
u, userErr := user.Current()
if userErr == nil {
name = u.Username
}
wizard := os.Getenv("ROGUE_WIZARD") != ""
return game.Config{
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) sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM) signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM)
go func() { go func() {
<-sig <-sig
g.AutoSave() g.AutoSave()
t.Fini() t.Fini()
os.Exit(0) os.Exit(0)
}() }()
}
if err := g.Run(); err != nil {
t.Fini() // chooseSeed picks the dungeon number: SEED for reproducible dungeons
fmt.Fprintln(os.Stderr, err) // (wizard mode, as in the C game), else time+pid (main.c).
os.Exit(1) 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). // wear lets the player put armor on (armor.c wear).
func (g *RogueGame) wear() { func (g *RogueGame) wear() {
p := &g.Player p := &g.Player
obj := g.getItem("wear", KindArmor)
if obj == nil { obj, ok := g.promptPackItem("wear", KindArmor)
if !ok {
return return
} }
if p.CurArmor != nil { if p.CurArmor != nil {
g.addmsg("you are already wearing some") g.addmsgf("you are already wearing some")
if !g.Options.Terse { 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.endmsg()
g.After = false g.After = false
return return
} }
if obj.Kind != KindArmor { if obj.Kind != KindArmor {
g.msg("you can't wear that") g.msg("you can't wear that")
return return
} }
g.wasteTime() g.wasteTime()
obj.Flags.Set(Known) obj.Flags.Set(Known)
sp := g.invName(obj, true) sp := g.inventoryName(obj, true)
p.CurArmor = obj p.CurArmor = obj
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you are now ") g.addmsgf("you are now ")
} }
g.msg("wearing %s", sp) g.msg("wearing %s", sp)
} }
// takeOff gets the armor off of the player's back (armor.c take_off). // takeOff gets the armor off of the player's back (armor.c take_off).
func (g *RogueGame) takeOff() { func (g *RogueGame) takeOff() {
p := &g.Player p := &g.Player
obj := p.CurArmor obj := p.CurArmor
if obj == nil { if obj == nil {
g.After = false g.After = false
@@ -43,18 +54,23 @@ func (g *RogueGame) takeOff() {
} else { } else {
g.msg("you aren't wearing any armor") g.msg("you aren't wearing any armor")
} }
return return
} }
if !g.dropCheck(p.CurArmor) { if !g.dropCheck(p.CurArmor) {
return return
} }
p.CurArmor = nil p.CurArmor = nil
if g.Options.Terse { if g.Options.Terse {
g.addmsg("was") g.addmsgf("was")
} else { } 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). // wasteTime does nothing but let other things happen (armor.c waste_time).

View File

@@ -11,42 +11,51 @@ func (g *RogueGame) runners(int) {
for _, tp := range list { for _, tp := range list {
if !tp.On(Held) && tp.On(Awake) { if !tp.On(Held) && tp.On(Awake) {
origPos := tp.Pos origPos := tp.Pos
wastarget := tp.On(Targeted) wastarget := tp.On(Targeted)
if g.moveMonst(tp) == -1 { if removed := g.moveMonster(tp); removed {
continue continue
} }
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 { if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if g.moveMonst(tp) == -1 { if removed := g.moveMonster(tp); removed {
continue continue
} }
} }
if wastarget && origPos != tp.Pos { if wastarget && origPos != tp.Pos {
tp.Flags.Clear(Targeted) tp.Flags.Clear(Targeted)
g.ToDeath = false g.ToDeath = false
} }
} }
} }
if g.HasHit { if g.HasHit {
g.endmsg() g.endmsg()
g.HasHit = false g.HasHit = false
} }
} }
// moveMonst executes a single turn of running for a monster (chase.c // moveMonster executes a single turn of running for a monster (chase.c
// move_monst). Returns -1 if the monster died or left the level. // move_monst). The result reports that the monster died or left the
func (g *RogueGame) moveMonst(tp *Monster) int { // level (the C -1 return).
func (g *RogueGame) moveMonster(tp *Monster) bool {
if !tp.On(Slowed) || tp.Turn { if !tp.On(Slowed) || tp.Turn {
if g.doChase(tp) == -1 { if g.chaseStep(tp) {
return -1 return true
} }
} }
if tp.On(Hasted) { if tp.On(Hasted) {
if g.doChase(tp) == -1 { if g.chaseStep(tp) {
return -1 return true
} }
} }
tp.Turn = !tp.Turn tp.Turn = !tp.Turn
return 0
return false
} }
// relocate makes the monster's new location be the specified one, updating // relocate makes the monster's new location be the specified one, updating
@@ -54,18 +63,21 @@ func (g *RogueGame) moveMonst(tp *Monster) int {
func (g *RogueGame) relocate(th *Monster, newLoc Coord) { func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
if newLoc != th.Pos { if newLoc != th.Pos {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh) g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
th.Room = g.roomin(newLoc) th.Room = g.roomIn(newLoc)
g.setOldch(th, newLoc) g.setOldChar(th, newLoc)
oroom := th.Room oroom := th.Room
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil) g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
if oroom != th.Room { if oroom != th.Room {
th.Dest = g.findDest(th) th.Dest = g.findDest(th)
} }
th.Pos = newLoc th.Pos = newLoc
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th) g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
} }
g.move(newLoc.Y, newLoc.X) g.move(newLoc.Y, newLoc.X)
if g.seeMonst(th) { if g.seeMonst(th) {
g.addch(th.Disguise) g.addch(th.Disguise)
} else if g.Player.On(SenseMonsters) { } else if g.Player.On(SenseMonsters) {
@@ -75,9 +87,10 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
} }
} }
// doChase makes one thing chase another (chase.c do_chase). Returns -1 if // chaseStep makes one thing chase another (chase.c do_chase). removed
// the chaser died in the attempt. // reports that the chaser died or left the level in the attempt (the C
func (g *RogueGame) doChase(th *Monster) int { // -1 return).
func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
p := &g.Player p := &g.Player
stoprun := false // true means we are there stoprun := false // true means we are there
mindist := 32767 mindist := 32767
@@ -86,56 +99,67 @@ func (g *RogueGame) doChase(th *Monster) int {
if th.On(Greedy) && rer.GoldVal == 0 { if th.On(Greedy) && rer.GoldVal == 0 {
th.Dest = &p.Pos // if gold has been taken, run after hero th.Dest = &p.Pos // if gold has been taken, run after hero
} }
var ree *Room // find room of chasee var ree *Room // find room of chasee
if th.Dest == &p.Pos { if th.Dest == &p.Pos {
ree = p.Room ree = p.Room
} else { } else {
ree = g.roomin(*th.Dest) ree = g.roomIn(*th.Dest)
} }
// We don't count doors as inside rooms for this routine // We don't count doors as inside rooms for this routine
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
var this Coord var this Coord
over: for {
// If the object of our desire is in a different room, and we are not // If the object of our desire is in a different room, and we are
// in a corridor, run to the door nearest to our goal. // not in a corridor, run to the door nearest to our goal.
if rer != ree { if rer != ree {
for i := range rer.Exits { for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i]) curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist { if curdist < mindist {
this = rer.Exits[i] this = rer.Exits[i]
mindist = curdist mindist = curdist
}
}
if door {
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
door = false
continue // 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 {
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 false
} }
} }
if door {
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum] break
door = false
goto over
}
} 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 {
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
}
} }
// This now contains what we want to run to this time so we run to it. // 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 we hit it we either want to fight it or stop running
@@ -145,32 +169,38 @@ over:
} else if this == *th.Dest { } else if this == *th.Dest {
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos { if th.Dest == &obj.Pos {
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
attachObj(&th.Pack, obj) attachObj(&th.Pack, obj)
if th.Room.Flags.Has(Gone) { if th.Room.Flags.Has(Gone) {
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage) g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
} else { } else {
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor) g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
} }
th.Dest = g.findDest(th) th.Dest = g.findDest(th)
break break
} }
} }
if th.Type != 'F' { if th.Type != 'F' {
stoprun = true stoprun = true
} }
} }
} else { } else {
if th.Type == 'F' { if th.Type == 'F' {
return 0 return false
} }
} }
g.relocate(th, g.chRet) g.relocate(th, g.chRet)
// And stop running if need be // And stop running if need be
if stoprun && th.Pos == *th.Dest { if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(Awake) th.Flags.Clear(Awake)
} }
return 0
return false
} }
// chase finds the spot for the chaser to move closer to the chasee // chase finds the spot for the chaser to move closer to the chasee
@@ -180,6 +210,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
p := &g.Player p := &g.Player
er := tp.Pos er := tp.Pos
plcnt := 1 plcnt := 1
var curdist int var curdist int
// If the thing is confused, let it move randomly. Invisible Stalkers // If the thing is confused, let it move randomly. Invisible Stalkers
@@ -188,7 +219,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) || if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
(tp.Type == 'B' && g.rnd(2) == 0) { (tp.Type == 'B' && g.rnd(2) == 0) {
// get a valid random move // get a valid random move
g.chRet = g.rndmove(&tp.Creature) g.chRet = g.randomStep(&tp.Creature)
curdist = distCp(g.chRet, ee) curdist = distCp(g.chRet, ee)
// Small chance that it will become un-confused // Small chance that it will become un-confused
if g.rnd(20) == 0 { if g.rnd(20) == 0 {
@@ -206,6 +237,7 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if ey >= NumLines-1 { if ey >= NumLines-1 {
ey = NumLines - 2 ey = NumLines - 2
} }
ex := er.X + 1 ex := er.X + 1
if ex >= NumCols { if ex >= NumCols {
ex = NumCols - 1 ex = NumCols - 1
@@ -215,11 +247,13 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
if x < 0 { if x < 0 {
continue continue
} }
for y := er.Y - 1; y <= ey; y++ { for y := er.Y - 1; y <= ey; y++ {
tryp := Coord{X: x, Y: y} tryp := Coord{X: x, Y: y}
if !g.diagOk(er, tryp) { if !g.diagOk(er, tryp) {
continue continue
} }
ch := g.Level.VisibleChar(y, x) ch := g.Level.VisibleChar(y, x)
if stepOk(ch) { if stepOk(ch) {
// If it is a scroll, it might be a scare monster // If it is a scroll, it might be a scare monster
@@ -227,12 +261,15 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
// it is. // it is.
if ch == Scroll { if ch == Scroll {
var found *Object var found *Object
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X { if y == obj.Pos.Y && x == obj.Pos.X {
found = obj found = obj
break break
} }
} }
if found != nil && found.ScrollKind() == ScrollScareMonster { if found != nil && found.ScrollKind() == ScrollScareMonster {
continue continue
} }
@@ -258,15 +295,18 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
} }
} }
} }
return curdist != 0 && g.chRet != p.Pos return curdist != 0 && g.chRet != p.Pos
} }
// setOldch sets the oldch character for the monster (chase.c set_oldch). // setOldChar sets the oldch character for the monster (chase.c set_oldch).
func (g *RogueGame) setOldch(tp *Monster, cp Coord) { func (g *RogueGame) setOldChar(tp *Monster, cp Coord) {
if tp.Pos == cp { if tp.Pos == cp {
return return
} }
sch := tp.OldCh sch := tp.OldCh
tp.OldCh = g.mvinch(cp.Y, cp.X) tp.OldCh = g.mvinch(cp.Y, cp.X)
if !g.Player.On(Blind) { if !g.Player.On(Blind) {
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) { if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) {
@@ -284,25 +324,30 @@ func (g *RogueGame) seeMonst(mp *Monster) bool {
if p.On(Blind) { if p.On(Blind) {
return false return false
} }
if mp.On(Invisible) && !p.On(CanSeeInvisible) { if mp.On(Invisible) && !p.On(CanSeeInvisible) {
return false return false
} }
y, x := mp.Pos.Y, mp.Pos.X y, x := mp.Pos.Y, mp.Pos.X
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if y != p.Pos.Y && x != p.Pos.X && 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)) { !stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
return false return false
} }
return true return true
} }
if mp.Room != p.Room { if mp.Room != p.Room {
return false return false
} }
return !mp.Room.Flags.Has(Dark) return !mp.Room.Flags.Has(Dark)
} }
// runto sets a monster running after the hero (chase.c runto). // runTo sets a monster running after the hero (chase.c runto).
func (g *RogueGame) runto(runner Coord) { func (g *RogueGame) runTo(runner Coord) {
tp := g.Level.MonsterAt(runner.Y, runner.X) tp := g.Level.MonsterAt(runner.Y, runner.X)
if tp == nil { if tp == nil {
return return
@@ -313,9 +358,9 @@ func (g *RogueGame) runto(runner Coord) {
tp.Dest = g.findDest(tp) tp.Dest = g.findDest(tp)
} }
// roomin finds what room some coordinates are in; nil means they aren't in // roomIn finds what room some coordinates are in; nil means they aren't in
// any room (chase.c roomin). // any room (chase.c roomin).
func (g *RogueGame) roomin(cp Coord) *Room { func (g *RogueGame) roomIn(cp Coord) *Room {
fp := *g.Level.FlagsAt(cp.Y, cp.X) fp := *g.Level.FlagsAt(cp.Y, cp.X)
if fp.Has(FPassage) { if fp.Has(FPassage) {
return &g.Level.Passages[fp&FPassNum] return &g.Level.Passages[fp&FPassNum]
@@ -330,6 +375,7 @@ func (g *RogueGame) roomin(cp Coord) *Room {
} }
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X) g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
return nil return nil
} }
@@ -338,19 +384,22 @@ func (g *RogueGame) diagOk(sp, ep Coord) bool {
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 { if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
return false return false
} }
if ep.X == sp.X || ep.Y == sp.Y { if ep.X == sp.X || ep.Y == sp.Y {
return true return true
} }
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X)) return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
} }
// cansee returns true if the hero can see a certain coordinate (chase.c // canSee returns true if the hero can see a certain coordinate (chase.c
// cansee). // cansee).
func (g *RogueGame) cansee(y, x int) bool { func (g *RogueGame) canSee(y, x int) bool {
p := &g.Player p := &g.Player
if p.On(Blind) { if p.On(Blind) {
return false return false
} }
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if g.Level.FlagsAt(y, x).Has(FPassage) { if g.Level.FlagsAt(y, x).Has(FPassage) {
if y != p.Pos.Y && x != p.Pos.X && if y != p.Pos.Y && x != p.Pos.X &&
@@ -359,11 +408,13 @@ func (g *RogueGame) cansee(y, x int) bool {
return false return false
} }
} }
return true return true
} }
// We can only see if the hero is in the same room as the coordinate // We can only see if the hero is in the same room as the coordinate
// and the room is lit, or if it is close. // and the room is lit, or if it is close.
rer := g.roomin(Coord{X: x, Y: y}) rer := g.roomIn(Coord{X: x, Y: y})
return rer == p.Room && !rer.Flags.Has(Dark) return rer == p.Room && !rer.Flags.Has(Dark)
} }
@@ -374,22 +425,28 @@ func (g *RogueGame) findDest(tp *Monster) *Coord {
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) { if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
return &g.Player.Pos return &g.Player.Pos
} }
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster { if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster {
continue continue
} }
if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob {
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob {
claimed := false claimed := false
for _, other := range g.Level.Monsters { for _, other := range g.Level.Monsters {
if other.Dest == &obj.Pos { if other.Dest == &obj.Pos {
claimed = true claimed = true
break break
} }
} }
if !claimed { if !claimed {
return &obj.Pos return &obj.Pos
} }
} }
} }
return &g.Player.Pos return &g.Player.Pos
} }

View File

@@ -6,6 +6,7 @@ package game
// bracketing (command.c command). // bracketing (command.c command).
func (g *RogueGame) command() { func (g *RogueGame) command() {
p := &g.Player p := &g.Player
ntimes := 1 // number of player moves ntimes := 1 // number of player moves
if p.On(Hasted) { if p.On(Hasted) {
ntimes++ ntimes++
@@ -13,6 +14,7 @@ func (g *RogueGame) command() {
// Let the daemons start up // Let the daemons start up
g.DoDaemons(Before) g.DoDaemons(Before)
g.DoFuses(Before) g.DoFuses(Before)
for ; ntimes > 0; ntimes-- { for ; ntimes > 0; ntimes-- {
g.Again = false g.Again = false
if g.HasHit { if g.HasHit {
@@ -26,29 +28,37 @@ func (g *RogueGame) command() {
} }
g.look(true) g.look(true)
if !g.Running { if !g.Running {
g.DoorStop = false g.DoorStop = false
} }
g.status() g.status()
g.LastScore = p.Purse g.LastScore = p.Purse
g.move(p.Pos.Y, p.Pos.X) g.move(p.Pos.Y, p.Pos.X)
if !((g.Running || g.Count != 0) && g.Options.Jump) {
if (!g.Running && g.Count == 0) || !g.Options.Jump {
g.refresh() // draw screen g.refresh() // draw screen
} }
g.Take = 0 g.Take = 0
g.After = true g.After = true
// Read command or continue run // Read command or continue run
if g.Wizard { if g.Wizard {
g.NoScore = true g.NoScore = true
} }
var ch byte var ch byte
if g.NoCommand == 0 { if g.NoCommand == 0 {
if g.Running || g.ToDeath { switch {
case g.Running || g.ToDeath:
ch = g.RunCh ch = g.RunCh
} else if g.Count != 0 { case g.Count != 0:
ch = g.countCh ch = g.countCh
} else { default:
ch = g.readchar() ch = g.readchar()
g.MoveOn = false g.MoveOn = false
if g.Msgs.Mpos != 0 { // erase message if its there if g.Msgs.Mpos != 0 { // erase message if its there
g.msg("") g.msg("")
@@ -57,6 +67,7 @@ func (g *RogueGame) command() {
} else { } else {
ch = '.' ch = '.'
} }
if g.NoCommand != 0 { if g.NoCommand != 0 {
if g.NoCommand--; g.NoCommand == 0 { if g.NoCommand--; g.NoCommand == 0 {
p.Flags.Set(Awake) p.Flags.Set(Awake)
@@ -67,14 +78,14 @@ func (g *RogueGame) command() {
g.newCount = false g.newCount = false
if isDigit(ch) { if isDigit(ch) {
g.Count = 0 g.Count = 0
g.newCount = true g.newCount = true
for isDigit(ch) { for isDigit(ch) {
g.Count = g.Count*10 + int(ch-'0') g.Count = min(g.Count*10+int(ch-'0'), 255)
if g.Count > 255 {
g.Count = 255
}
ch = g.readchar() ch = g.readchar()
} }
g.countCh = ch g.countCh = ch
// turn off count for commands which don't make sense // turn off count for commands which don't make sense
// to repeat // to repeat
@@ -93,8 +104,9 @@ func (g *RogueGame) command() {
if g.Count != 0 && !g.Running { if g.Count != 0 && !g.Running {
g.Count-- g.Count--
} }
if ch != 'a' && ch != Escape && if ch != 'a' && ch != Escape &&
!(g.Running || g.Count != 0 || g.ToDeath) { !g.Running && g.Count == 0 && !g.ToDeath {
g.LLastComm = g.LastComm g.LLastComm = g.LastComm
g.LLastDir = g.LastDir g.LLastDir = g.LastDir
g.LLastPick = g.LastPick g.LLastPick = g.LastPick
@@ -102,6 +114,7 @@ func (g *RogueGame) command() {
g.LastDir = 0 g.LastDir = 0
g.LastPick = nil g.LastPick = nil
} }
g.dispatch(ch) g.dispatch(ch)
// turn off flags if no longer needed // turn off flags if no longer needed
if !g.Running { if !g.Running {
@@ -112,20 +125,25 @@ func (g *RogueGame) command() {
if g.Take != 0 { if g.Take != 0 {
g.pickUp(g.Take) g.pickUp(g.Take)
} }
if !g.Running { if !g.Running {
g.DoorStop = false g.DoorStop = false
} }
if !g.After { if !g.After {
ntimes++ ntimes++
} }
} }
g.DoDaemons(After) g.DoDaemons(After)
g.DoFuses(After) g.DoFuses(After)
if p.IsRing(Left, RingSearching) { if p.IsRing(Left, RingSearching) {
g.search() g.search()
} else if p.IsRing(Left, RingTeleportation) && g.rnd(50) == 0 { } else if p.IsRing(Left, RingTeleportation) && g.rnd(50) == 0 {
g.teleport() g.teleport()
} }
if p.IsRing(Right, RingSearching) { if p.IsRing(Right, RingSearching) {
g.search() g.search()
} else if p.IsRing(Right, RingTeleportation) && g.rnd(50) == 0 { } else if p.IsRing(Right, RingTeleportation) && g.rnd(50) == 0 {
@@ -133,254 +151,280 @@ func (g *RogueGame) command() {
} }
} }
// dispatch runs one command character (the big switch in command.c, // dispatch runs one command character (the big switch in command.c; the
// including its `goto over` re-dispatch). // loop stands in for its `goto over` re-dispatch).
func (g *RogueGame) dispatch(ch byte) { func (g *RogueGame) dispatch(ch byte) {
p := &g.Player p := &g.Player
over:
switch ch { for {
case ',': switch ch {
var found *Object case ',':
for _, obj := range g.Level.Objects { var found *Object
if obj.Pos.Y == p.Pos.Y && obj.Pos.X == p.Pos.X {
found = obj for _, obj := range g.Level.Objects {
if obj.Pos.Y == p.Pos.Y && obj.Pos.X == p.Pos.X {
found = obj
break
}
}
if found != nil {
if !g.levitCheck() {
g.pickUp(found.Kind.Glyph())
}
} else {
if !g.Options.Terse {
g.addmsgf("there is ")
}
g.addmsgf("nothing here")
if !g.Options.Terse {
g.addmsgf(" to pick up")
}
g.endmsg()
}
case '!':
g.shell()
case 'h':
g.moveHero(0, -1)
case 'j':
g.moveHero(1, 0)
case 'k':
g.moveHero(-1, 0)
case 'l':
g.moveHero(0, 1)
case 'y':
g.moveHero(-1, -1)
case 'u':
g.moveHero(-1, 1)
case 'b':
g.moveHero(1, -1)
case 'n':
g.moveHero(1, 1)
case 'H':
g.startRun('h')
case 'J':
g.startRun('j')
case 'K':
g.startRun('k')
case 'L':
g.startRun('l')
case 'Y':
g.startRun('y')
case 'U':
g.startRun('u')
case 'B':
g.startRun('b')
case 'N':
g.startRun('n')
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
if !p.On(Blind) {
g.DoorStop = true
g.Firstmove = true
}
if g.Count != 0 && !g.newCount {
ch = g.direction
} else {
ch += 'A' - CTRL('A')
g.direction = ch
}
continue // the C goto over: re-dispatch as the run command
case 'F', 'f':
if ch == 'F' {
g.Kamikaze = true
}
if !g.promptDirection() {
g.After = false
break break
} }
}
if found != nil {
if !g.levitCheck() {
g.pickUp(found.Kind.Glyph())
}
} else {
if !g.Options.Terse {
g.addmsg("there is ")
}
g.addmsg("nothing here")
if !g.Options.Terse {
g.addmsg(" to pick up")
}
g.endmsg()
}
case '!':
g.shell()
case 'h':
g.doMove(0, -1)
case 'j':
g.doMove(1, 0)
case 'k':
g.doMove(-1, 0)
case 'l':
g.doMove(0, 1)
case 'y':
g.doMove(-1, -1)
case 'u':
g.doMove(-1, 1)
case 'b':
g.doMove(1, -1)
case 'n':
g.doMove(1, 1)
case 'H':
g.doRun('h')
case 'J':
g.doRun('j')
case 'K':
g.doRun('k')
case 'L':
g.doRun('l')
case 'Y':
g.doRun('y')
case 'U':
g.doRun('u')
case 'B':
g.doRun('b')
case 'N':
g.doRun('n')
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
if !p.On(Blind) {
g.DoorStop = true
g.Firstmove = true
}
if g.Count != 0 && !g.newCount {
ch = g.direction
} else {
ch += 'A' - CTRL('A')
g.direction = ch
}
goto over
case 'F', 'f':
if ch == 'F' {
g.Kamikaze = true
}
if !g.getDir() {
g.After = false
break
}
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
mp := g.Level.MonsterAt(g.Delta.Y, g.Delta.X)
if mp == nil || (!g.seeMonst(mp) && !p.On(SenseMonsters)) {
if !g.Options.Terse {
g.addmsg("I see ")
}
g.msg("no monster there")
g.After = false
} else if g.diagOk(p.Pos, g.Delta) {
g.ToDeath = true
g.MaxHit = 0
mp.Flags.Set(Targeted)
g.RunCh = g.DirCh
ch = g.DirCh
goto over
}
case 't':
if !g.getDir() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
}
case 'a':
if g.LastComm == 0 {
g.msg("you haven't typed a command yet")
g.After = false
} else {
ch = g.LastComm
g.Again = true
goto over
}
case 'q':
g.quaff()
case 'Q':
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
case 'i':
g.After = false
g.inventory(p.Pack, 0)
case 'I':
g.After = false
g.pickyInven()
case 'd':
g.dropIt()
case 'r':
g.readScroll()
case 'e':
g.eat()
case 'w':
g.wield()
case 'W':
g.wear()
case 'T':
g.takeOff()
case 'P':
g.ringOn()
case 'R':
g.ringOff()
case 'o':
g.option()
g.After = false
case 'c':
g.call()
g.After = false
case '>':
g.After = false
g.dLevel()
case '<':
g.After = false
g.uLevel()
case '?':
g.After = false
g.help()
case '/':
g.After = false
g.identify()
case 's':
g.search()
case 'z':
if g.getDir() {
g.doZap()
} else {
g.After = false
}
case 'D':
g.After = false
g.discovered()
case CTRL('P'):
g.After = false
g.msg("%s", g.Msgs.Huh)
case CTRL('R'):
g.After = false
g.refresh()
case 'v':
g.After = false
g.msg("version %s. (mctesq was here)", Release)
case 'S':
g.After = false
g.saveGame()
case '.':
// rest command
case ' ':
g.After = false // "legal" illegal command
case '^':
g.After = false
if g.getDir() {
g.Delta.Y += p.Pos.Y g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X g.Delta.X += p.Pos.X
fp := g.Level.FlagsAt(g.Delta.Y, g.Delta.X)
if !g.Options.Terse { mp := g.Level.MonsterAt(g.Delta.Y, g.Delta.X)
g.addmsg("You have found ") if mp == nil || (!g.seeMonst(mp) && !p.On(SenseMonsters)) {
if !g.Options.Terse {
g.addmsgf("I see ")
}
g.msg("no monster there")
g.After = false
} else if g.diagOk(p.Pos, g.Delta) {
g.ToDeath = true
g.MaxHit = 0
mp.Flags.Set(Targeted)
g.RunCh = g.DirCh
ch = g.DirCh
continue // the C goto over: fight by running at it
} }
if g.Level.Char(g.Delta.Y, g.Delta.X) != Trap { case 't':
g.msg("no trap there") if !g.promptDirection() {
} else if p.On(Hallucinating) { g.After = false
g.msg("%s", trName[g.rnd(NumTrapTypes)])
} else { } else {
g.msg("%s", trName[*fp&FTrapMask]) g.missile(g.Delta.Y, g.Delta.X)
fp.Set(FSeen) }
case 'a':
if g.LastComm == 0 {
g.msg("you haven't typed a command yet")
g.After = false
} else {
ch = g.LastComm
g.Again = true
continue // the C goto over: replay the last command
}
case 'q':
g.quaff()
case 'Q':
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
case 'i':
g.After = false
g.inventory(p.Pack, 0)
case 'I':
g.After = false
g.pickyInven()
case 'd':
g.dropIt()
case 'r':
g.readScroll()
case 'e':
g.eat()
case 'w':
g.wield()
case 'W':
g.wear()
case 'T':
g.takeOff()
case 'P':
g.ringOn()
case 'R':
g.ringOff()
case 'o':
g.option()
g.After = false
case 'c':
g.call()
g.After = false
case '>':
g.After = false
g.dLevel()
case '<':
g.After = false
g.uLevel()
case '?':
g.After = false
g.help()
case '/':
g.After = false
g.identify()
case 's':
g.search()
case 'z':
if g.promptDirection() {
g.doZap()
} else {
g.After = false
}
case 'D':
g.After = false
g.discovered()
case CTRL('P'):
g.After = false
g.msg("%s", g.Msgs.Huh)
case CTRL('R'):
g.After = false
g.refresh()
case 'v':
g.After = false
g.msg("version %s. (mctesq was here)", Release)
case 'S':
g.After = false
g.saveGame()
case '.':
// rest command
case ' ':
g.After = false // "legal" illegal command
case '^':
g.After = false
if g.promptDirection() {
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
fp := g.Level.FlagsAt(g.Delta.Y, g.Delta.X)
if !g.Options.Terse {
g.addmsgf("You have found ")
}
switch {
case g.Level.Char(g.Delta.Y, g.Delta.X) != Trap:
g.msg("no trap there")
case p.On(Hallucinating):
g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
default:
g.msg("%s", g.data.trName[*fp&FTrapMask])
fp.Set(FSeen)
}
}
case Escape: // escape
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
case 'm':
g.MoveOn = true
if !g.promptDirection() {
g.After = false
} else {
ch = g.DirCh
g.countCh = g.DirCh
continue // the C goto over: move onto it without pickup
}
case ')':
g.current(p.CurWeapon, "wielding", "")
case ']':
g.current(p.CurArmor, "wearing", "")
case '=':
g.current(p.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(p.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
case '@':
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
default:
g.After = false
if g.Wizard {
g.wizardCommand(ch)
} else {
g.illcom(ch)
} }
} }
case Escape: // escape
g.DoorStop = false return
g.Count = 0
g.After = false
g.Again = false
case 'm':
g.MoveOn = true
if !g.getDir() {
g.After = false
} else {
ch = g.DirCh
g.countCh = g.DirCh
goto over
}
case ')':
g.current(p.CurWeapon, "wielding", "")
case ']':
g.current(p.CurArmor, "wearing", "")
case '=':
g.current(p.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(p.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
case '@':
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
default:
g.After = false
if g.Wizard {
g.wizardCommand(ch)
} else {
g.illcom(ch)
}
} }
} }
// wizardCommand handles the MASTER debug commands (command.c). // wizardCommand handles the MASTER debug commands (command.c).
func (g *RogueGame) wizardCommand(ch byte) { func (g *RogueGame) wizardCommand(ch byte) {
p := &g.Player p := &g.Player
switch ch { switch ch {
case '|': case '|':
g.msg("@ %d,%d", p.Pos.Y, p.Pos.X) g.msg("@ %d,%d", p.Pos.Y, p.Pos.X)
@@ -409,7 +453,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
case CTRL('X'): case CTRL('X'):
g.turnSee(p.On(SenseMonsters)) g.turnSee(p.On(SenseMonsters))
case CTRL('~'): case CTRL('~'):
if item := g.getItem("charge", KindWand); item != nil { if item, ok := g.promptPackItem("charge", KindWand); ok {
item.Charges = 10000 item.Charges = 10000
} }
case CTRL('I'): case CTRL('I'):
@@ -452,62 +496,81 @@ func (g *RogueGame) search() {
p := &g.Player p := &g.Player
ey := p.Pos.Y + 1 ey := p.Pos.Y + 1
ex := p.Pos.X + 1 ex := p.Pos.X + 1
probinc := 0 probinc := 0
if p.On(Hallucinating) { if p.On(Hallucinating) {
probinc = 3 probinc = 3
} }
if p.On(Blind) { if p.On(Blind) {
probinc += 2 probinc += 2
} }
found := false found := false
for y := p.Pos.Y - 1; y <= ey; y++ { for y := p.Pos.Y - 1; y <= ey; y++ {
for x := p.Pos.X - 1; x <= ex; x++ { for x := p.Pos.X - 1; x <= ex; x++ {
if y == p.Pos.Y && x == p.Pos.X { if y == p.Pos.Y && x == p.Pos.X {
continue continue
} }
fp := g.Level.FlagsAt(y, x) fp := g.Level.FlagsAt(y, x)
if fp.Has(FReal) { if fp.Has(FReal) {
continue continue
} }
foundone := false foundone := false
switch g.Level.Char(y, x) { switch g.Level.Char(y, x) {
case '|', '-': case '|', '-':
if g.rnd(5+probinc) != 0 { if g.rnd(5+probinc) != 0 {
break break
} }
g.Level.SetChar(y, x, Door) g.Level.SetChar(y, x, Door)
g.msg("a secret door") g.msg("a secret door")
foundone = true foundone = true
case Floor: case Floor:
if g.rnd(2+probinc) != 0 { if g.rnd(2+probinc) != 0 {
break break
} }
g.Level.SetChar(y, x, Trap) g.Level.SetChar(y, x, Trap)
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you found ") g.addmsgf("you found ")
} }
if p.On(Hallucinating) { if p.On(Hallucinating) {
g.msg("%s", trName[g.rnd(NumTrapTypes)]) g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
} else { } else {
g.msg("%s", trName[*fp&FTrapMask]) g.msg("%s", g.data.trName[*fp&FTrapMask])
fp.Set(FSeen) fp.Set(FSeen)
} }
foundone = true foundone = true
case ' ': case ' ':
if g.rnd(3+probinc) != 0 { if g.rnd(3+probinc) != 0 {
break break
} }
g.Level.SetChar(y, x, Passage) g.Level.SetChar(y, x, Passage)
foundone = true foundone = true
} }
if foundone { if foundone {
found = true found = true
fp.Set(FReal) fp.Set(FReal)
g.Count = 0 g.Count = 0
g.Running = false g.Running = false
} }
} }
} }
if found { if found {
g.look(false) g.look(false)
} }
@@ -523,28 +586,35 @@ func (g *RogueGame) help() {
// typed a funny character. // typed a funny character.
if helpch != '*' { if helpch != '*' {
g.move(0, 0) g.move(0, 0)
for _, strp := range helpStr {
for _, strp := range g.data.helpStr {
if strp.Ch == helpch { if strp.Ch == helpch {
g.Msgs.LowerMsg = true g.Msgs.LowerMsg = true
g.msg("%s%s", unctrl(strp.Ch), strp.Desc) g.msg("%s%s", unctrl(strp.Ch), strp.Desc)
g.Msgs.LowerMsg = false g.Msgs.LowerMsg = false
return return
} }
} }
g.msg("unknown character '%s'", unctrl(helpch)) g.msg("unknown character '%s'", unctrl(helpch))
return return
} }
// Here we print help for everything, then wait before we return to // Here we print help for everything, then wait before we return to
// command mode. // command mode.
numprint := 0 numprint := 0
for _, strp := range helpStr {
for _, strp := range g.data.helpStr {
if strp.Print { if strp.Print {
numprint++ numprint++
} }
} }
if numprint&1 == 1 { // round odd numbers up if numprint&1 == 1 { // round odd numbers up
numprint++ numprint++
} }
numprint /= 2 numprint /= 2
if numprint > NumLines-1 { if numprint > NumLines-1 {
numprint = NumLines - 1 numprint = NumLines - 1
@@ -552,24 +622,32 @@ func (g *RogueGame) help() {
hw := g.scr.Hw hw := g.scr.Hw
hw.Clear() hw.Clear()
cnt := 0 cnt := 0
for _, strp := range helpStr {
for _, strp := range g.data.helpStr {
if !strp.Print { if !strp.Print {
continue continue
} }
x := 0 x := 0
if cnt >= numprint { if cnt >= numprint {
x = NumCols / 2 x = NumCols / 2
} }
hw.Move(cnt%numprint, x) hw.Move(cnt%numprint, x)
if strp.Ch != 0 { if strp.Ch != 0 {
hw.AddStr(unctrl(strp.Ch)) hw.AddStr(unctrl(strp.Ch))
} }
hw.AddStr(strp.Desc) hw.AddStr(strp.Desc)
if cnt++; cnt >= numprint*2 { if cnt++; cnt >= numprint*2 {
break break
} }
} }
hw.MvAddStr(NumLines-1, 0, "--Press space to continue--") hw.MvAddStr(NumLines-1, 0, "--Press space to continue--")
g.scr.RefreshWin(hw) g.scr.RefreshWin(hw)
g.waitFor(' ') g.waitFor(' ')
@@ -577,49 +655,33 @@ func (g *RogueGame) help() {
g.refresh() g.refresh()
} }
// identList is command.c's static ident_list.
var identList = []helpEntry{
{'|', "wall of a room", false},
{'-', "wall of a room", false},
{Gold, "gold", false},
{Stairs, "a staircase", false},
{Door, "door", false},
{Floor, "room floor", false},
{PlayerCh, "you", false},
{Passage, "passage", false},
{Trap, "trap", false},
{Potion, "potion", false},
{Scroll, "scroll", false},
{Food, "food", false},
{Weapon, "weapon", false},
{' ', "solid rock", false},
{Armor, "armor", false},
{Amulet, "the Amulet of Yendor", false},
{Ring, "ring", false},
{Stick, "wand or staff", false},
}
// identify tells the player what a certain thing is (command.c identify). // identify tells the player what a certain thing is (command.c identify).
func (g *RogueGame) identify() { func (g *RogueGame) identify() {
g.msg("what do you want identified? ") g.msg("what do you want identified? ")
ch := g.readchar() ch := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if ch == Escape { if ch == Escape {
g.msg("") g.msg("")
return return
} }
var str string var str string
if isUpper(ch) { if isUpper(ch) {
str = g.Monsters[ch-'A'].Name str = g.Monsters[ch-'A'].Name
} else { } else {
str = "unknown character" str = "unknown character"
for _, hp := range identList {
for _, hp := range g.data.identList {
if hp.Ch == ch { if hp.Ch == ch {
str = hp.Desc str = hp.Desc
break break
} }
} }
} }
g.msg("'%s': %s", unctrl(ch), str) g.msg("'%s': %s", unctrl(ch), str)
} }
@@ -628,6 +690,7 @@ func (g *RogueGame) dLevel() {
if g.levitCheck() { if g.levitCheck() {
return return
} }
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs { if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs {
g.msg("I see no way down") g.msg("I see no way down")
} else { } else {
@@ -642,12 +705,14 @@ func (g *RogueGame) uLevel() {
if g.levitCheck() { if g.levitCheck() {
return return
} }
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs { if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs {
if g.HasAmulet { if g.HasAmulet {
g.Depth-- g.Depth--
if g.Depth == 0 { if g.Depth == 0 {
g.totalWinner() g.totalWinner()
} }
g.NewLevel() g.NewLevel()
g.msg("you feel a wrenching sensation in your gut") g.msg("you feel a wrenching sensation in your gut")
} else { } else {
@@ -664,23 +729,30 @@ func (g *RogueGame) levitCheck() bool {
if !g.Player.On(Levitating) { if !g.Player.On(Levitating) {
return false return false
} }
g.msg("You can't. You're floating off the ground!") g.msg("You can't. You're floating off the ground!")
return true return true
} }
// call allows a user to call a potion, scroll, or ring something // call allows a user to call a potion, scroll, or ring something
// (command.c call). // (command.c call).
func (g *RogueGame) call() { func (g *RogueGame) call() {
obj := g.getItem("call", KindCallable) obj, ok := g.promptPackItem("call", KindCallable)
// Make certain that it is something that we want to name // Make certain that it is something that we want to name
if obj == nil { if !ok {
return return
} }
var op *ObjInfo
var elsewise string var (
var know *bool op *ObjInfo
var guess *string elsewise string
know *bool
guess *string
)
it := &g.Items it := &g.Items
switch obj.Kind { switch obj.Kind {
case KindRing: case KindRing:
op = &it.Rings[obj.Which] op = &it.Rings[obj.Which]
@@ -696,14 +768,18 @@ func (g *RogueGame) call() {
elsewise = it.WandMade[obj.Which] elsewise = it.WandMade[obj.Which]
case KindFood: case KindFood:
g.msg("you can't call that anything") g.msg("you can't call that anything")
return return
default: default:
guess = &obj.Label guess = &obj.Label
elsewise = obj.Label elsewise = obj.Label
} }
fromGuess := false fromGuess := false
if op != nil { if op != nil {
know = &op.Know know = &op.Know
guess = &op.Guess guess = &op.Guess
if *guess != "" { if *guess != "" {
elsewise = *guess elsewise = *guess
@@ -712,16 +788,21 @@ func (g *RogueGame) call() {
} else { } else {
fromGuess = elsewise != "" && elsewise == *guess fromGuess = elsewise != "" && elsewise == *guess
} }
if know != nil && *know { if know != nil && *know {
g.msg("that has already been identified") g.msg("that has already been identified")
return return
} }
if fromGuess { if fromGuess {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("Was ") g.addmsgf("Was ")
} }
g.msg("called \"%s\"", elsewise) g.msg("called \"%s\"", elsewise)
} }
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? ")) g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
buf := elsewise buf := elsewise
@@ -735,23 +816,29 @@ func (g *RogueGame) current(cur *Object, how, where string) {
g.After = false g.After = false
if cur != nil { if cur != nil {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you are %s (", how) g.addmsgf("you are %s (", how)
} }
g.InvDescribe = false g.InvDescribe = false
g.addmsg("%c) %s", cur.PackCh, g.invName(cur, true)) g.addmsgf("%c) %s", cur.PackCh, g.inventoryName(cur, true))
g.InvDescribe = true g.InvDescribe = true
if where != "" { if where != "" {
g.addmsg(" %s", where) g.addmsgf(" %s", where)
} }
g.endmsg() g.endmsg()
} else { } else {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you are ") g.addmsgf("you are ")
} }
g.addmsg("%s nothing", how)
g.addmsgf("%s nothing", how)
if where != "" { if where != "" {
g.addmsg(" %s", where) g.addmsgf(" %s", where)
} }
g.endmsg() g.endmsg()
} }
} }
@@ -762,7 +849,9 @@ func (g *RogueGame) shell() {
g.After = false g.After = false
if se, ok := g.scr.term.(interface{ ShellEscape() }); ok { if se, ok := g.scr.term.(interface{ ShellEscape() }); ok {
g.InShell = true g.InShell = true
se.ShellEscape() se.ShellEscape()
g.InShell = false g.InShell = false
g.refresh() g.refresh()
} else { } else {

View File

@@ -23,6 +23,7 @@ type Monster struct {
// in the C sources (cur_armor, purse, food_left, ...). // in the C sources (cur_armor, purse, food_left, ...).
type Player struct { type Player struct {
Creature Creature
CurArmor *Object // what he is wearing CurArmor *Object // what he is wearing
CurWeapon *Object // which weapon he is wielding CurWeapon *Object // which weapon he is wielding
CurRing [2]*Object // which rings are being worn (Left/Right) 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) return p.IsRing(Left, ring) || p.IsRing(Right, ring)
} }
// nextPackChar claims and returns the next unused pack character (pack.c
// pack_char).
func (p *Player) nextPackChar() byte {
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
}
// removeFromPack takes an item out of the pack: the whole entry, or one
// of a stack when all is false (the bookkeeping half of pack.c
// leave_pack). It returns the object that left the pack — a copy when
// newobj asks for a split.
func (p *Player) removeFromPack(obj *Object, newobj, all bool) *Object {
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// attachMon pushes a monster onto the front of a list (list.c attach). // attachMon pushes a monster onto the front of a list (list.c attach).
func attachMon(list *[]*Monster, item *Monster) { func attachMon(list *[]*Monster, item *Monster) {
*list = append([]*Monster{item}, *list...) *list = append([]*Monster{item}, *list...)
@@ -59,6 +101,7 @@ func detachMon(list *[]*Monster, item *Monster) {
for i, m := range *list { for i, m := range *list {
if m == item { if m == item {
*list = append((*list)[:i], (*list)[i+1:]...) *list = append((*list)[:i], (*list)[i+1:]...)
return return
} }
} }

View File

@@ -10,6 +10,10 @@ package game
// function-pointer-to-int mapping in state.c rs_write_daemons. // function-pointer-to-int mapping in state.c rs_write_daemons.
type DaemonID int 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 ( const (
DNone DaemonID = 0 DNone DaemonID = 0
DRollwand DaemonID = 1 DRollwand DaemonID = 1
@@ -21,12 +25,10 @@ const (
DUnconfuse DaemonID = 7 DUnconfuse DaemonID = 7
DUnsee DaemonID = 8 DUnsee DaemonID = 8
DSight DaemonID = 9 DSight DaemonID = 9
// Fuses beyond the C save map (state.c never saved these; the Go save DVisuals DaemonID = 10
// format handles them uniformly). DComeDown DaemonID = 11
DVisuals DaemonID = 10 DLand DaemonID = 12
DComeDown DaemonID = 11 DTurnSee DaemonID = 13 // potions.c casts turn_see to a fuse callback
DLand DaemonID = 12
DTurnSee DaemonID = 13 // potions.c casts turn_see to a fuse callback
) )
// Scheduling phases and slot states (daemon.c). // Scheduling phases and slot states (daemon.c).
@@ -59,6 +61,7 @@ func (g *RogueGame) dSlot() *delayedAction {
return &g.Daemons.List[i] return &g.Daemons.List[i]
} }
} }
panic("ran out of fuse slots") // C: debug message in MASTER, NULL deref otherwise 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 d
} }
} }
return nil return nil
} }

View File

@@ -45,6 +45,7 @@ func (g *RogueGame) doctor(int) {
p := &g.Player p := &g.Player
lv := p.Stats.Lvl lv := p.Stats.Lvl
ohp := p.Stats.HP ohp := p.Stats.HP
g.Quiet++ g.Quiet++
if lv < 8 { if lv < 8 {
if g.Quiet+(lv<<1) > 20 { if g.Quiet+(lv<<1) > 20 {
@@ -53,16 +54,20 @@ func (g *RogueGame) doctor(int) {
} else if g.Quiet >= 3 { } else if g.Quiet >= 3 {
p.Stats.HP += g.rnd(lv-7) + 1 p.Stats.HP += g.rnd(lv-7) + 1
} }
if p.IsRing(Left, RingRegeneration) { if p.IsRing(Left, RingRegeneration) {
p.Stats.HP++ p.Stats.HP++
} }
if p.IsRing(Right, RingRegeneration) { if p.IsRing(Right, RingRegeneration) {
p.Stats.HP++ p.Stats.HP++
} }
if ohp != p.Stats.HP { if ohp != p.Stats.HP {
if p.Stats.HP > p.Stats.MaxHP { if p.Stats.HP > p.Stats.MaxHP {
p.Stats.HP = p.Stats.MaxHP p.Stats.HP = p.Stats.MaxHP
} }
g.Quiet = 0 g.Quiet = 0
} }
} }
@@ -82,6 +87,7 @@ func (g *RogueGame) rollwand(int) {
g.KillDaemon(DRollwand) g.KillDaemon(DRollwand)
g.Fuse(DSwander, 0, wanderTime(g), Before) g.Fuse(DSwander, 0, wanderTime(g), Before)
} }
g.Daemons.Between = 0 g.Daemons.Between = 0
} }
} }
@@ -103,6 +109,7 @@ func (g *RogueGame) unsee(int) {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh) g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
} }
} }
g.Player.Flags.Clear(CanSeeInvisible) g.Player.Flags.Clear(CanSeeInvisible)
} }
@@ -112,9 +119,11 @@ func (g *RogueGame) sight(int) {
if p.On(Blind) { if p.On(Blind) {
g.Extinguish(DSight) g.Extinguish(DSight)
p.Flags.Clear(Blind) p.Flags.Clear(Blind)
if !p.Room.Flags.Has(Gone) { if !p.Room.Flags.Has(Gone) {
g.enterRoom(p.Pos) g.enterRoom(p.Pos)
} }
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again", g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
"the veil of darkness lifts")) "the veil of darkness lifts"))
} }
@@ -129,6 +138,7 @@ func (g *RogueGame) nohaste(int) {
// stomach digests the hero's food (daemons.c stomach). // stomach digests the hero's food (daemons.c stomach).
func (g *RogueGame) stomach(int) { func (g *RogueGame) stomach(int) {
p := &g.Player p := &g.Player
origHungry := p.HungryState origHungry := p.HungryState
if p.FoodLeft <= 0 { if p.FoodLeft <= 0 {
if p.FoodLeft--; p.FoodLeft < -StarveTime { if p.FoodLeft--; p.FoodLeft < -StarveTime {
@@ -138,29 +148,36 @@ func (g *RogueGame) stomach(int) {
if g.NoCommand != 0 || g.rnd(5) != 0 { if g.NoCommand != 0 || g.rnd(5) != 0 {
return return
} }
g.NoCommand += g.rnd(8) + 4 g.NoCommand += g.rnd(8) + 4
p.HungryState = 3 p.HungryState = 3
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("%s", g.chooseStr( g.addmsgf("%s", g.chooseStr(
"the munchies overpower your motor capabilities. ", "the munchies overpower your motor capabilities. ",
"you feel too weak from lack of food. ")) "you feel too weak from lack of food. "))
} }
g.msg("%s", g.chooseStr("You freak out", "You faint")) g.msg("%s", g.chooseStr("You freak out", "You faint"))
} else { } else {
oldfood := p.FoodLeft oldfood := p.FoodLeft
amulet := 0 amulet := 0
if g.HasAmulet { if g.HasAmulet {
amulet = 1 amulet = 1
} }
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
if p.FoodLeft < MoreTime && oldfood >= MoreTime { if p.FoodLeft < MoreTime && oldfood >= MoreTime {
p.HungryState = 2 p.HungryState = 2
g.msg("%s", g.chooseStr( 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")) "you are starting to feel weak"))
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime { } else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
p.HungryState = 1 p.HungryState = 1
if g.Options.Terse { if g.Options.Terse {
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry")) g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
} else { } else {
@@ -169,8 +186,10 @@ func (g *RogueGame) stomach(int) {
} }
} }
} }
if p.HungryState != origHungry { if p.HungryState != origHungry {
p.Flags.Clear(Awake) p.Flags.Clear(Awake)
g.Running = false g.Running = false
g.ToDeath = false g.ToDeath = false
g.Count = 0 g.Count = 0
@@ -193,16 +212,18 @@ func (g *RogueGame) comeDown(int) {
// undo the things // undo the things
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Kind.Glyph()) g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Kind.Glyph())
} }
} }
// undo the monsters // undo the monsters
seemonst := p.On(SenseMonsters) seemonst := p.On(SenseMonsters)
for _, tp := range g.Level.Monsters { for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X) g.move(tp.Pos.Y, tp.Pos.X)
if g.cansee(tp.Pos.Y, tp.Pos.X) {
if g.canSee(tp.Pos.Y, tp.Pos.X) {
if !tp.On(Invisible) || p.On(CanSeeInvisible) { if !tp.On(Invisible) || p.On(CanSeeInvisible) {
g.addch(tp.Disguise) g.addch(tp.Disguise)
} else { } else {
@@ -214,6 +235,7 @@ func (g *RogueGame) comeDown(int) {
g.standend() g.standend()
} }
} }
g.msg("Everything looks SO boring now.") g.msg("Everything looks SO boring now.")
} }
@@ -226,29 +248,31 @@ func (g *RogueGame) visuals(int) {
} }
// change the things // change the things
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) { if g.canSee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing()) g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing())
} }
} }
// change the stairs // change the stairs
if !g.SeenStairs && g.cansee(g.Level.Stairs.Y, g.Level.Stairs.X) { if !g.SeenStairs && g.canSee(g.Level.Stairs.Y, g.Level.Stairs.X) {
g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing()) g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing())
} }
// change the monsters // change the monsters
seemonst := p.On(SenseMonsters) seemonst := p.On(SenseMonsters)
for _, tp := range g.Level.Monsters { for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X) g.move(tp.Pos.Y, tp.Pos.X)
if g.seeMonst(tp) { if g.seeMonst(tp) {
if tp.Type == 'X' && tp.Disguise != 'X' { if tp.Type == 'X' && tp.Disguise != 'X' {
g.addch(g.rndThing()) g.addch(g.rndThing())
} else { } else {
g.addch(byte(g.rnd(26) + 'A')) g.addch(g.randomMonsterLetter())
} }
} else if seemonst { } else if seemonst {
g.standout() g.standout()
g.addch(byte(g.rnd(26) + 'A')) g.addch(g.randomMonsterLetter())
g.standend() g.standend()
} }
} }

View File

@@ -25,20 +25,26 @@ type DiceSpec []DiceRoll
// a single 0x0 attack, as it did in C. // a single 0x0 attack, as it did in C.
func ParseDice(s string) DiceSpec { func ParseDice(s string) DiceSpec {
var spec DiceSpec var spec DiceSpec
for s != "" { for s != "" {
count := cAtoi(s) count := cAtoi(s)
xi := strings.IndexByte(s, 'x') xi := strings.IndexByte(s, 'x')
if xi < 0 { if xi < 0 {
break break
} }
s = s[xi+1:] s = s[xi+1:]
spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)}) spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)})
si := strings.IndexByte(s, '/') si := strings.IndexByte(s, '/')
if si < 0 { if si < 0 {
break break
} }
s = s[si+1:] s = s[si+1:]
} }
return spec 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. // String renders the spec back in the classic "NxM/NxM" form.
func (d DiceSpec) String() string { func (d DiceSpec) String() string {
var sb strings.Builder var sb strings.Builder
for i, r := range d { for i, r := range d {
if i > 0 { if i > 0 {
sb.WriteByte('/') sb.WriteByte('/')
} }
fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides) fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides)
} }
return sb.String() 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 // The bestiary and weapon tables must parse to at least one attack each so
// every creature and weapon actually swings. // every creature and weapon actually swings.
func TestTablesHaveDice(t *testing.T) { func TestTablesHaveDice(t *testing.T) {
for i, m := range monsterTable { data := newGameData()
for i, m := range data.monsterTable {
if len(m.Stats.Dmg) == 0 { if len(m.Stats.Dmg) == 0 {
t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name) t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name)
} }
} }
for w, iw := range initWeaps {
for w, iw := range data.initWeaps {
if len(iw.dam) == 0 || len(iw.hrl) == 0 { if len(iw.dam) == 0 || len(iw.hrl) == 0 {
t.Errorf("weapon %v has empty dice", WeaponKind(w)) t.Errorf("weapon %d has empty dice", w)
} }
} }
} }

View File

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

View File

@@ -4,43 +4,6 @@ import "strconv"
// fight.c — all the fighting gets done here. // fight.c — all the fighting gets done here.
// hNames are the strings for hitting; the first four are used when the
// player strikes, the second four for monsters (fight.c h_names).
var hNames = [8]string{
" scored an excellent hit on ",
" hit ",
" have injured ",
" swing and hit ",
" scored an excellent hit on ",
" hit ",
" has injured ",
" swings and hits ",
}
// mNames are the strings for missing (fight.c m_names).
var mNames = [8]string{
" miss",
" swing and miss",
" barely miss",
" don't hit",
" misses",
" swings and misses",
" barely misses",
" doesn't hit",
}
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
var strPlus = [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
}
// addDam adjusts damage done due to strength (fight.c add_dam).
var addDam = [32]int{
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
}
// setMname returns the monster name for the given monster (fight.c // setMname returns the monster name for the given monster (fight.c
// set_mname). // set_mname).
func (g *RogueGame) setMname(tp *Monster) string { func (g *RogueGame) setMname(tp *Monster) string {
@@ -48,20 +11,27 @@ func (g *RogueGame) setMname(tp *Monster) string {
if g.Options.Terse { if g.Options.Terse {
return "it" return "it"
} }
return "something" return "something"
} }
var mname string var mname string
if g.Player.On(Hallucinating) { if g.Player.On(Hallucinating) {
ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X)) var idx int
if !isUpper(byte(ch)) {
ch = g.rnd(26) ch := g.mvinch(tp.Pos.Y, tp.Pos.X)
if isUpper(ch) {
idx = int(ch - 'A')
} else { } else {
ch -= 'A' idx = g.rnd(26)
} }
mname = g.Monsters[ch].Name
mname = g.Monsters[idx].Name
} else { } else {
mname = g.Monsters[tp.Type-'A'].Name mname = g.Monsters[tp.Type-'A'].Name
} }
return "the " + mname return "the " + mname
} }
@@ -77,42 +47,51 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
// place. // place.
g.Count = 0 g.Count = 0
g.Quiet = 0 g.Quiet = 0
g.runto(mp) g.runTo(mp)
// Let him know it was really a xeroc (if it was one). // Let him know it was really a xeroc (if it was one).
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(Blind) { if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(Blind) {
tp.Disguise = 'X' tp.Disguise = 'X'
if p.On(Hallucinating) { if p.On(Hallucinating) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, byte(g.rnd(26)+'A')) g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
} }
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!", g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!")) "wait! That's a xeroc!"))
if !thrown { if !thrown {
return false return false
} }
} }
mname := g.setMname(tp) mname := g.setMname(tp)
didHit := false didHit := false
g.HasHit = g.Options.Terse && !g.ToDeath g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) { if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
didHit = false didHit = false
if thrown { if thrown {
g.thunk(weap, mname, g.Options.Terse) g.thunk(weap, mname, g.Options.Terse)
} else { } else {
g.hit("", mname, g.Options.Terse) g.hit("", mname, g.Options.Terse)
} }
if p.On(CanConfuse) { if p.On(CanConfuse) {
didHit = true didHit = true
tp.Flags.Set(Confused) tp.Flags.Set(Confused)
p.Flags.Clear(CanConfuse) p.Flags.Clear(CanConfuse)
g.endmsg() g.endmsg()
g.HasHit = false g.HasHit = false
g.msg("your hands stop glowing %s", g.pickColor("red")) g.msg("your hands stop glowing %s", g.pickColor("red"))
} }
if tp.Stats.HP <= 0 { if tp.Stats.HP <= 0 {
g.killed(tp, true) g.killed(tp, true)
} else if didHit && !p.On(Blind) { } else if didHit && !p.On(Blind) {
g.msg("%s appears confused", mname) g.msg("%s appears confused", mname)
} }
didHit = true didHit = true
} else { } else {
if thrown { if thrown {
@@ -121,40 +100,47 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
g.miss("", mname, g.Options.Terse) g.miss("", mname, g.Options.Terse)
} }
} }
return didHit return didHit
} }
// attack has the monster attack the player (fight.c attack). Returns -1 if // attack has the monster attack the player (fight.c attack). removed
// the monster removed itself from the level during its own attack. // reports that the monster took itself off the level during its own
func (g *RogueGame) attack(mp *Monster) int { // attack (the C -1 return).
func (g *RogueGame) attack(mp *Monster) (removed bool) {
p := &g.Player p := &g.Player
// Since this is an attack, stop running and any healing that was // Since this is an attack, stop running and any healing that was
// going on at the time. // going on at the time.
g.Running = false g.Running = false
g.Count = 0 g.Count = 0
g.Quiet = 0 g.Quiet = 0
if g.ToDeath && !mp.On(Targeted) { if g.ToDeath && !mp.On(Targeted) {
g.ToDeath = false g.ToDeath = false
g.Kamikaze = false g.Kamikaze = false
} }
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) { if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) {
mp.Disguise = 'X' mp.Disguise = 'X'
if p.On(Hallucinating) { 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) mname := g.setMname(mp)
oldhp := p.Stats.HP oldhp := p.Stats.HP
removed := false
if g.rollEm(&mp.Creature, &p.Creature, nil, false) { if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
if mp.Type != 'I' { if mp.Type != 'I' {
if g.HasHit { if g.HasHit {
g.addmsg(". ") g.addmsgf(". ")
} }
g.hit(mname, "", false) g.hit(mname, "", false)
} else if g.HasHit { } else if g.HasHit {
g.endmsg() g.endmsg()
} }
g.HasHit = false g.HasHit = false
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ... g.death(mp.Type) // Bye bye life ...
@@ -163,10 +149,12 @@ func (g *RogueGame) attack(mp *Monster) int {
if oldhp > g.MaxHit { if oldhp > g.MaxHit {
g.MaxHit = oldhp g.MaxHit = oldhp
} }
if p.Stats.HP <= g.MaxHit { if p.Stats.HP <= g.MaxHit {
g.ToDeath = false g.ToDeath = false
} }
} }
if !mp.On(Cancelled) { if !mp.On(Cancelled) {
switch mp.Type { switch mp.Type {
case 'A': case 'A':
@@ -175,13 +163,17 @@ func (g *RogueGame) attack(mp *Monster) int {
case 'I': case 'I':
// The ice monster freezes you // The ice monster freezes you
p.Flags.Clear(Awake) p.Flags.Clear(Awake)
if g.NoCommand == 0 { if g.NoCommand == 0 {
g.addmsg("you are frozen") g.addmsgf("you are frozen")
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg(" by the %s", mname) g.addmsgf(" by the %s", mname)
} }
g.endmsg() g.endmsg()
} }
g.NoCommand += g.rnd(2) + 2 g.NoCommand += g.rnd(2) + 2
if g.NoCommand > BoreLevel { if g.NoCommand > BoreLevel {
g.death('h') g.death('h')
@@ -190,7 +182,8 @@ func (g *RogueGame) attack(mp *Monster) int {
// Rattlesnakes have poisonous bites // Rattlesnakes have poisonous bites
if !g.save(VsPoison) { if !g.save(VsPoison) {
if !p.IsWearing(RingSustainStrength) { if !p.IsWearing(RingSustainStrength) {
g.chgStr(-1) g.changeStrength(-1)
if !g.Options.Terse { if !g.Options.Terse {
g.msg("you feel a bite in your leg and now feel weaker") g.msg("you feel a bite in your leg and now feel weaker")
} else { } else {
@@ -211,36 +204,45 @@ func (g *RogueGame) attack(mp *Monster) int {
if mp.Type == 'W' { if mp.Type == 'W' {
chance = 15 chance = 15
} }
if g.rnd(100) < chance { if g.rnd(100) < chance {
var fewer int var fewer int
if mp.Type == 'W' { if mp.Type == 'W' {
if p.Stats.Exp == 0 { if p.Stats.Exp == 0 {
g.death('W') // All levels gone g.death('W') // All levels gone
} }
if p.Stats.Lvl--; p.Stats.Lvl == 0 { if p.Stats.Lvl--; p.Stats.Lvl == 0 {
p.Stats.Exp = 0 p.Stats.Exp = 0
p.Stats.Lvl = 1 p.Stats.Lvl = 1
} else { } else {
p.Stats.Exp = eLevels[p.Stats.Lvl-1] + 1 p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
} }
fewer = g.roll(1, 10) fewer = g.roll(1, 10)
} else { } else {
fewer = g.roll(1, 3) fewer = g.roll(1, 3)
} }
p.Stats.HP -= fewer p.Stats.HP -= fewer
p.Stats.MaxHP -= fewer p.Stats.MaxHP -= fewer
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
p.Stats.HP = 1 p.Stats.HP = 1
} }
if p.Stats.MaxHP <= 0 { if p.Stats.MaxHP <= 0 {
g.death(mp.Type) g.death(mp.Type)
} }
g.msg("you suddenly feel weaker") g.msg("you suddenly feel weaker")
} }
case 'F': case 'F':
// Venus Flytrap stops the poor guy from moving // Venus Flytrap stops the poor guy from moving
p.Flags.Set(Held) p.Flags.Set(Held)
p.VfHit++ p.VfHit++
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}} g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
if p.Stats.HP--; p.Stats.HP <= 0 { if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F') g.death('F')
@@ -248,15 +250,20 @@ func (g *RogueGame) attack(mp *Monster) int {
case 'L': case 'L':
// Leprechaun steals some gold // Leprechaun steals some gold
lastpurse := p.Purse lastpurse := p.Purse
p.Purse -= g.goldCalc() p.Purse -= g.goldCalc()
if !g.save(VsMagic) { if !g.save(VsMagic) {
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc() p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
} }
if p.Purse < 0 { if p.Purse < 0 {
p.Purse = 0 p.Purse = 0
} }
g.removeMon(mp.Pos, mp, false) g.removeMon(mp.Pos, mp, false)
removed = true removed = true
if p.Purse != lastpurse { if p.Purse != lastpurse {
g.msg("your purse feels lighter") g.msg("your purse feels lighter")
} }
@@ -264,66 +271,79 @@ func (g *RogueGame) attack(mp *Monster) int {
// Nymphs steal a magic item; look through the pack and // Nymphs steal a magic item; look through the pack and
// pick out one we like. // pick out one we like.
var steal *Object var steal *Object
nobj := 0 nobj := 0
for _, obj := range p.Pack { for _, obj := range p.Pack {
if obj != p.CurArmor && obj != p.CurWeapon && if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] && obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
obj.isMagic() { g.isMagic(obj) {
if nobj++; g.rnd(nobj) == 0 { if nobj++; g.rnd(nobj) == 0 {
steal = obj steal = obj
} }
} }
} }
if steal != nil { if steal != nil {
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false) g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
removed = true removed = true
g.leavePack(steal, false, false) g.leavePack(steal, false, false)
g.msg("she stole %s!", g.invName(steal, true)) g.msg("she stole %s!", g.inventoryName(steal, true))
} }
} }
} }
} else if mp.Type != 'I' { } else if mp.Type != 'I' {
if g.HasHit { if g.HasHit {
g.addmsg(". ") g.addmsgf(". ")
g.HasHit = false g.HasHit = false
} }
if mp.Type == 'F' { if mp.Type == 'F' {
p.Stats.HP -= p.VfHit p.Stats.HP -= p.VfHit
if p.Stats.HP <= 0 { if p.Stats.HP <= 0 {
g.death(mp.Type) // Bye bye life ... g.death(mp.Type) // Bye bye life ...
} }
} }
g.miss(mname, "", false) g.miss(mname, "", false)
} }
if g.Options.FightFlush && !g.ToDeath { if g.Options.FightFlush && !g.ToDeath {
g.flushType() g.flushType()
} }
g.Count = 0 g.Count = 0
g.status() g.status()
if removed {
return -1 return removed
}
return 0
} }
// swing returns true if the swing hits (fight.c swing). // swing returns true if the swing hits (fight.c swing).
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool { func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
res := g.rnd(20) res := g.rnd(20)
need := (20 - atLvl) - opArm need := (20 - atLvl) - opArm
return res+wplus >= need return res+wplus >= need
} }
// rollEm rolls several attacks (fight.c roll_em). // rollAttacks rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool { func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player p := &g.Player
att := &thatt.Stats att := &thatt.Stats
def := &thdef.Stats def := &thdef.Stats
var attacks DiceSpec
var hplus, dplus int var (
attacks DiceSpec
hplus, dplus int
)
if weap == nil { if weap == nil {
attacks = att.Dmg attacks = att.Dmg
} else { } else {
hplus = weap.HPlus hplus = weap.HPlus
dplus = weap.DPlus dplus = weap.DPlus
if weap == p.CurWeapon { if weap == p.CurWeapon {
if p.IsRing(Left, RingIncreaseDamage) { if p.IsRing(Left, RingIncreaseDamage) {
@@ -331,12 +351,14 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
} else if p.IsRing(Left, RingDexterity) { } else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Bonus hplus += p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingIncreaseDamage) { if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Bonus dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) { } else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Bonus hplus += p.CurRing[Right].Bonus
} }
} }
attacks = weap.Damage attacks = weap.Damage
if hurl { if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil && if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
@@ -354,29 +376,37 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
if !thdef.Flags.Has(Awake) { if !thdef.Flags.Has(Awake) {
hplus += 4 hplus += 4
} }
defArm := def.ArmorClass defArm := def.ArmorClass
if def == &p.Stats { if def == &p.Stats {
if p.CurArmor != nil { if p.CurArmor != nil {
defArm = p.CurArmor.ArmorClass defArm = p.CurArmor.ArmorClass
} }
if p.IsRing(Left, RingProtection) { if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Bonus defArm -= p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingProtection) { if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Bonus defArm -= p.CurRing[Right].Bonus
} }
} }
didHit := false didHit := false
for _, atk := range attacks { for _, atk := range attacks {
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) { if g.swing(att.Lvl, defArm, hplus+g.data.strPlus[att.Str]) {
proll := g.roll(atk.Count, atk.Sides) proll := g.roll(atk.Count, atk.Sides)
damage := dplus + proll + addDam[att.Str]
damage := dplus + proll + g.data.addDam[att.Str]
if damage > 0 { if damage > 0 {
def.HP -= damage def.HP -= damage
} }
didHit = true didHit = true
} }
} }
return didHit return didHit
} }
@@ -387,7 +417,9 @@ func cAtoi(s string) int {
for i < len(s) && s[i] >= '0' && s[i] <= '9' { for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++ i++
} }
n, _ := strconv.Atoi(s[:i]) n, _ := strconv.Atoi(s[:i])
return n return n
} }
@@ -398,9 +430,11 @@ func prname(mname string, upper bool) string {
if out == "" { if out == "" {
out = "you" out = "you"
} }
if upper { if upper {
out = string(toUpper(out[0])) + out[1:] out = string(toUpper(out[0])) + out[1:]
} }
return out return out
} }
@@ -409,12 +443,15 @@ func (g *RogueGame) thunk(weap *Object, mname string, noend bool) {
if g.ToDeath { if g.ToDeath {
return return
} }
if weap.Kind == KindWeapon { 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 { } else {
g.addmsg("you hit ") g.addmsgf("you hit ")
} }
g.addmsg("%s", mname)
g.addmsgf("%s", mname)
if !noend { if !noend {
g.endmsg() g.endmsg()
} }
@@ -425,7 +462,9 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
if g.ToDeath { if g.ToDeath {
return return
} }
g.addmsg("%s", prname(er, true))
g.addmsgf("%s", prname(er, true))
var s string var s string
if g.Options.Terse { if g.Options.Terse {
s = " hit" s = " hit"
@@ -434,12 +473,16 @@ func (g *RogueGame) hit(er, ee string, noend bool) {
if er != "" { if er != "" {
i += 4 i += 4
} }
s = hNames[i]
s = g.data.hNames[i]
} }
g.addmsg("%s", s)
g.addmsgf("%s", s)
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("%s", prname(ee, false)) g.addmsgf("%s", prname(ee, false))
} }
if !noend { if !noend {
g.endmsg() g.endmsg()
} }
@@ -450,18 +493,24 @@ func (g *RogueGame) miss(er, ee string, noend bool) {
if g.ToDeath { if g.ToDeath {
return return
} }
g.addmsg("%s", prname(er, true))
g.addmsgf("%s", prname(er, true))
i := 0 i := 0
if !g.Options.Terse { if !g.Options.Terse {
i = g.rnd(4) i = g.rnd(4)
} }
if er != "" { if er != "" {
i += 4 i += 4
} }
g.addmsg("%s", mNames[i])
g.addmsgf("%s", g.data.mNames[i])
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg(" %s", prname(ee, false)) g.addmsgf(" %s", prname(ee, false))
} }
if !noend { if !noend {
g.endmsg() g.endmsg()
} }
@@ -472,12 +521,15 @@ func (g *RogueGame) bounce(weap *Object, mname string, noend bool) {
if g.ToDeath { if g.ToDeath {
return return
} }
if weap.Kind == KindWeapon { 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 { } else {
g.addmsg("you missed ") g.addmsgf("you missed ")
} }
g.addmsg("%s", mname)
g.addmsgf("%s", mname)
if !noend { if !noend {
g.endmsg() g.endmsg()
} }
@@ -489,15 +541,19 @@ func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
for _, obj := range pack { for _, obj := range pack {
obj.Pos = tp.Pos obj.Pos = tp.Pos
detachObj(&tp.Pack, obj) detachObj(&tp.Pack, obj)
if waskill { if waskill {
g.fall(obj, false) g.fall(obj, false)
} }
} }
g.Level.SetMonsterAt(mp.Y, mp.X, nil) g.Level.SetMonsterAt(mp.Y, mp.X, nil)
g.mvaddch(mp.Y, mp.X, tp.OldCh) g.mvaddch(mp.Y, mp.X, tp.OldCh)
detachMon(&g.Level.Monsters, tp) g.Level.RemoveMonster(tp)
if tp.On(Targeted) { if tp.On(Targeted) {
g.Kamikaze = false g.Kamikaze = false
g.ToDeath = false g.ToDeath = false
if g.Options.FightFlush { if g.Options.FightFlush {
g.flushType() g.flushType()
@@ -521,33 +577,40 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
if ok { if ok {
tp.Room.Gold = pos tp.Room.Gold = pos
} }
if ok && g.Depth >= g.MaxDepth { if ok && g.Depth >= g.MaxDepth {
gold := newObject() gold := newObject()
gold.Kind = KindGold gold.Kind = KindGold
gold.GoldValue = g.goldCalc() gold.GoldValue = g.goldCalc()
if g.save(VsMagic) { if g.save(VsMagic) {
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc() gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
} }
attachObj(&tp.Pack, gold) attachObj(&tp.Pack, gold)
} }
} }
// Get rid of the monster. // Get rid of the monster.
mname := g.setMname(tp) mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true) g.removeMon(tp.Pos, tp, true)
if pr { if pr {
if g.HasHit { if g.HasHit {
g.addmsg(". Defeated ") g.addmsgf(". Defeated ")
g.HasHit = false g.HasHit = false
} else { } else {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you have ") g.addmsgf("you have ")
} }
g.addmsg("defeated ")
g.addmsgf("defeated ")
} }
g.msg("%s", mname) g.msg("%s", mname)
} }
// Do adjustments if he went up a level // Do adjustments if he went up a level
g.checkLevel() g.checkLevel()
if g.Options.FightFlush { if g.Options.FightFlush {
g.flushType() g.flushType()
} }

View File

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

View File

@@ -110,7 +110,7 @@ type RogueGame struct {
// screen / messages // screen / messages
scr *Screen scr *Screen
Msgs MsgLine Msgs MessageLine
statusCache statusCache statusCache statusCache
invPage invPage // things.c discovery-list pagination statics invPage invPage // things.c discovery-list pagination statics
@@ -138,6 +138,9 @@ type RogueGame struct {
rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C
restored bool // game came from a save file; Run skips setup restored bool // game came from a save file; Run skips setup
// data is the game's copy of the static tables (extern.c and friends).
data *gameData
} }
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item // NewGame builds a game from cfg, seeds the RNG, and randomizes the item
@@ -145,6 +148,7 @@ type RogueGame struct {
// and first level arrive with later porting phases). // and first level arrive with later porting phases).
func NewGame(cfg Config) *RogueGame { func NewGame(cfg Config) *RogueGame {
g := &RogueGame{ g := &RogueGame{
data: newGameData(),
Rng: &Rng{Seed: cfg.Seed}, Rng: &Rng{Seed: cfg.Seed},
Dnum: int(cfg.Seed), Dnum: int(cfg.Seed),
Whoami: cfg.Name, Whoami: cfg.Name,
@@ -165,16 +169,20 @@ func NewGame(cfg Config) *RogueGame {
g.InvDescribe = true g.InvDescribe = true
g.Msgs.SaveMsg = true g.Msgs.SaveMsg = true
g.scr = NewScreen(cfg.Term) g.scr = NewScreen(cfg.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.FileName = cfg.Home + "/rogue.save" g.FileName = cfg.Home + "/rogue.save"
g.rogueOpts = cfg.RogueOpts g.rogueOpts = cfg.RogueOpts
if cfg.Wizard { if cfg.Wizard {
g.Player.Flags.Set(SenseMonsters) g.Player.Flags.Set(SenseMonsters)
} }
if cfg.RogueOpts != "" { if cfg.RogueOpts != "" {
g.ParseOpts(cfg.RogueOpts) g.ParseOpts(cfg.RogueOpts)
} }
g.Monsters = monsterTable g.Monsters = g.data.monsterTable
g.Items.Group = 2 // weapons.c: int group = 2 g.Items.Group = 2 // weapons.c: int group = 2
for i := range g.Level.Passages { for i := range g.Level.Passages {
g.Level.Passages[i].Flags = Gone | Dark g.Level.Passages[i].Flags = Gone | Dark
@@ -186,20 +194,26 @@ func NewGame(cfg Config) *RogueGame {
g.initColors() // set up colors of potions g.initColors() // set up colors of potions
g.initStones() // set up stone settings of rings g.initStones() // set up stone settings of rings
g.initMaterials() // set up materials of wands g.initMaterials() // set up materials of wands
return g return g
} }
// Run plays the game to its end: the back half of main.c main() plus // Run plays the game to its end: the back half of main.c main() plus
// playit(). It returns after death, victory, quitting, or saving. // playit(). It returns after death, victory, quitting, or saving.
func (g *RogueGame) Run() (err error) { func (g *RogueGame) Run() error {
// A gameEnd panic is the port's my_exit(): recovering it here makes
// Run return normally (zero values), restoring the terminal via the
// caller's defers.
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if _, ok := r.(gameEnd); ok { if _, ok := r.(gameEnd); ok {
return // normal game over / save exit return // normal game over / save exit
} }
panic(r) panic(r)
} }
}() }()
if !g.restored { if !g.restored {
g.NewLevel() // draw current level g.NewLevel() // draw current level
// Start up daemons and fuses // Start up daemons and fuses
@@ -208,7 +222,9 @@ func (g *RogueGame) Run() (err error) {
g.Fuse(DSwander, 0, wanderTime(g), After) g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After) g.StartDaemon(DStomach, 0, After)
} }
g.playit() g.playit()
return nil return nil
} }
@@ -226,10 +242,12 @@ func (g *RogueGame) playit() {
} }
g.Oldpos = g.Player.Pos g.Oldpos = g.Player.Pos
g.Oldrp = g.roomin(g.Player.Pos)
g.Oldrp = g.roomIn(g.Player.Pos)
for g.Playing { for g.Playing {
g.command() // command execution g.command() // command execution
} }
g.endit() g.endit()
} }
@@ -242,7 +260,7 @@ func (g *RogueGame) endit() {
func (g *RogueGame) fatal(s string) { func (g *RogueGame) fatal(s string) {
g.mvaddstr(NumLines-2, 0, s) g.mvaddstr(NumLines-2, 0, s)
g.refresh() g.refresh()
g.myExit(0) g.myExit()
} }
// quit has the player make certain, then exits (main.c quit). The final // quit has the player make certain, then exits (main.c quit). The final
@@ -252,17 +270,21 @@ func (g *RogueGame) quit(int) {
if !g.QComm { if !g.QComm {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
} }
oy, ox := g.scr.Std.GetYX() oy, ox := g.scr.Std.GetYX()
g.msg("really quit?") g.msg("really quit?")
if g.readchar() == 'y' { if g.readchar() == 'y' {
g.clear() 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.move(NumLines-1, 0)
g.refresh() g.refresh()
g.score(g.Player.Purse, 1, 0) g.score(g.Player.Purse, 1, 0)
g.myExit(0) g.myExit()
return return
} }
g.move(0, 0) g.move(0, 0)
g.clrtoeol() g.clrtoeol()
g.status() g.status()

View File

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

View File

@@ -10,9 +10,11 @@ import (
// maxMsg is io.c MAXMSG: how much message fits before --More--. // maxMsg is io.c MAXMSG: how much message fits before --More--.
const maxMsg = NumCols - len("--More--") - 1 const maxMsg = NumCols - len("--More--") - 1
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus // MessageLine is the io.c message machinery: the static msgbuf/newpos
// the related globals (mpos, huh, and the message-behavior flags). // pair plus the related globals (mpos, huh, and the message-behavior
type MsgLine struct { // flags). It owns the top line of the screen; attach wires in the
// display and input it needs.
type MessageLine struct {
buf strings.Builder // msgbuf buf strings.Builder // msgbuf
newpos int newpos int
Mpos int // where cursor is on top line Mpos int // where cursor is on top line
@@ -20,52 +22,61 @@ type MsgLine struct {
SaveMsg bool // remember last msg SaveMsg bool // remember last msg
LowerMsg bool // messages should start w/lower case LowerMsg bool // messages should start w/lower case
MsgEsc bool // check for ESC from msg's --More-- MsgEsc bool // check for ESC from msg's --More--
scr *Screen // the top line lives on scr.Std
look func(wakeup bool) // redraw before a --More-- (misc.c look)
readChar func() byte // input for --More-- prompts
} }
// Msg displays a message at the top of the screen (io.c msg). It returns // Msg displays a message at the top of the screen (io.c msg). It returns
// Escape if the player escaped out of a --More--, ^Escape otherwise (the C // Escape if the player escaped out of a --More--, ^Escape otherwise (the
// convention: callers compare against ESCAPE). // C convention: callers compare against ESCAPE).
func (g *RogueGame) msg(format string, a ...any) int { func (m *MessageLine) Msg(format string, a ...any) int {
// if the string is "", just clear the line // if the string is "", just clear the line
if format == "" { if format == "" {
g.move(0, 0) m.scr.Std.Move(0, 0)
g.clrtoeol() m.scr.Std.Clrtoeol()
g.Msgs.Mpos = 0 m.Mpos = 0
return ^Escape return ^Escape
} }
// otherwise add to the message and flush it out // otherwise add to the message and flush it out
g.doadd(format, a...) m.doaddf(format, a...)
return g.endmsg()
return m.End()
} }
// addmsg adds things to the current message (io.c addmsg). // Addf adds things to the current message (io.c addmsg).
func (g *RogueGame) addmsg(format string, a ...any) { func (m *MessageLine) Addf(format string, a ...any) {
g.doadd(format, a...) m.doaddf(format, a...)
} }
// endmsg displays a new msg, giving the player a chance to see the previous // End displays a new msg, giving the player a chance to see the previous
// one if it is up there with the --More-- (io.c endmsg). // one if it is up there with the --More-- (io.c endmsg).
func (g *RogueGame) endmsg() int { func (m *MessageLine) End() int {
m := &g.Msgs
if m.SaveMsg { if m.SaveMsg {
m.Huh = m.buf.String() m.Huh = m.buf.String()
} }
if m.Mpos != 0 { if m.Mpos != 0 {
g.look(false) m.look(false)
g.mvaddstr(0, m.Mpos, "--More--") m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
g.refresh() m.scr.Refresh()
if !m.MsgEsc { if !m.MsgEsc {
g.waitFor(' ') m.waitForSpace()
} else { } else {
for { for {
ch := g.readchar() ch := m.readChar()
if ch == ' ' { if ch == ' ' {
break break
} }
if ch == Escape { if ch == Escape {
m.buf.Reset() m.buf.Reset()
m.Mpos = 0 m.Mpos = 0
m.newpos = 0 m.newpos = 0
return Escape return Escape
} }
} }
@@ -75,29 +86,64 @@ func (g *RogueGame) endmsg() int {
// with a pack addressing character // with a pack addressing character
out := m.buf.String() out := m.buf.String()
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg && 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:] 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.Mpos = m.newpos
m.newpos = 0 m.newpos = 0
m.buf.Reset() m.buf.Reset()
g.refresh() m.scr.Refresh()
return ^Escape return ^Escape
} }
// doadd performs an add onto the message buffer (io.c doadd). // attach wires the message line to its display and input; NewGame and
func (g *RogueGame) doadd(format string, a ...any) { // Restore call it once the screen and game exist.
m := &g.Msgs 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...) s := fmt.Sprintf(format, a...)
if len(s)+m.newpos >= maxMsg { if len(s)+m.newpos >= maxMsg {
g.endmsg() m.End()
} }
m.buf.WriteString(s) m.buf.WriteString(s)
m.newpos = m.buf.Len() m.newpos = m.buf.Len()
} }
// msg, addmsgf, and endmsg are the game-side shorthands for the message
// line; the machinery lives on MessageLine.
func (g *RogueGame) msg(format string, a ...any) int {
return g.Msgs.Msg(format, a...)
}
func (g *RogueGame) addmsgf(format string, a ...any) {
g.Msgs.Addf(format, a...)
}
func (g *RogueGame) endmsg() {
g.Msgs.End()
}
// stepOk returns true if it is ok to step on ch (io.c step_ok). // stepOk returns true if it is ok to step on ch (io.c step_ok).
func stepOk(ch byte) bool { func stepOk(ch byte) bool {
switch ch { switch ch {
@@ -114,8 +160,10 @@ func (g *RogueGame) readchar() byte {
ch := g.scr.term.ReadChar() ch := g.scr.term.ReadChar()
if ch == 3 { // ^C if ch == 3 { // ^C
g.quit(0) g.quit(0)
return 27 return 27
} }
return ch return ch
} }
@@ -133,8 +181,6 @@ type statusCache struct {
init bool init bool
} }
var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"}
// status displays the important stats line, keeping the cursor where it was // status displays the important stats line, keeping the cursor where it was
// (io.c status). // (io.c status).
func (g *RogueGame) status() { func (g *RogueGame) status() {
@@ -146,17 +192,21 @@ func (g *RogueGame) status() {
if p.CurArmor != nil { if p.CurArmor != nil {
temp = p.CurArmor.ArmorClass temp = p.CurArmor.ArmorClass
} }
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp && 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.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg { s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
return return
} }
s.init = true s.init = true
s.arm = temp s.arm = temp
oy, ox := g.scr.Std.GetYX() oy, ox := g.scr.Std.GetYX()
if s.hp != p.Stats.MaxHP { if s.hp != p.Stats.MaxHP {
s.hp = p.Stats.MaxHP s.hp = p.Stats.MaxHP
s.hpwidth = 0 s.hpwidth = 0
for t := p.Stats.MaxHP; t != 0; t /= 10 { for t := p.Stats.MaxHP; t != 0; t /= 10 {
s.hpwidth++ s.hpwidth++
@@ -175,7 +225,7 @@ func (g *RogueGame) status() {
"Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s", "Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s",
g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP, g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP,
p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp, p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp,
hungerStateName[p.HungryState]) g.data.hungerStateName[p.HungryState])
if g.StatMsg { if g.StatMsg {
g.move(0, 0) g.move(0, 0)
g.msg("%s", line) g.msg("%s", line)
@@ -183,6 +233,7 @@ func (g *RogueGame) status() {
g.move(StatLine, 0) g.move(StatLine, 0)
g.addstr(line) g.addstr(line)
} }
g.clrtoeol() g.clrtoeol()
g.move(oy, ox) g.move(oy, ox)
} }
@@ -197,7 +248,11 @@ func (g *RogueGame) waitFor(ch byte) {
} }
} }
} }
for g.readchar() != ch {
for {
if g.readchar() == ch {
return
}
} }
} }
@@ -221,11 +276,13 @@ func toUpper(c byte) byte {
if isLower(c) { if isLower(c) {
return c - 'a' + 'A' return c - 'a' + 'A'
} }
return c return c
} }
func toLower(c byte) byte { func toLower(c byte) byte {
if isUpper(c) { if isUpper(c) {
return c - 'A' + 'a' return c - 'A' + 'a'
} }
return c return c
} }

View File

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

View File

@@ -10,20 +10,24 @@ func (g *RogueGame) look(wakeup bool) {
hero := p.Pos hero := p.Pos
passcount := 0 passcount := 0
rp := p.Room rp := p.Room
if g.Oldpos != hero { if g.Oldpos != hero {
g.eraseLamp(g.Oldpos, g.Oldrp) g.eraseLamp(g.Oldpos, g.Oldrp)
g.Oldpos = hero g.Oldpos = hero
g.Oldrp = rp g.Oldrp = rp
} }
ey := hero.Y + 1 ey := hero.Y + 1
ex := hero.X + 1 ex := hero.X + 1
sx := hero.X - 1 sx := hero.X - 1
sy := hero.Y - 1 sy := hero.Y - 1
sumhero, diffhero := 0, 0 sumhero, diffhero := 0, 0
if g.DoorStop && !g.Firstmove && g.Running { if g.DoorStop && !g.Firstmove && g.Running {
sumhero = hero.Y + hero.X sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X diffhero = hero.Y - hero.X
} }
pp := g.Level.At(hero.Y, hero.X) pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch pch := pp.Ch
pfl := pp.Flags pfl := pp.Flags
@@ -32,10 +36,12 @@ func (g *RogueGame) look(wakeup bool) {
if y <= 0 || y >= NumLines-1 { if y <= 0 || y >= NumLines-1 {
continue continue
} }
for x := sx; x <= ex; x++ { for x := sx; x <= ex; x++ {
if x < 0 || x >= NumCols { if x < 0 || x >= NumCols {
continue continue
} }
if !p.On(Blind) { if !p.On(Blind) {
if y == hero.Y && x == hero.X { if y == hero.Y && x == hero.X {
continue continue
@@ -43,16 +49,19 @@ func (g *RogueGame) look(wakeup bool) {
} }
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
ch := pp.Ch ch := pp.Ch
if ch == ' ' { // nothing need be done with a ' ' if ch == ' ' { // nothing need be done with a ' '
continue continue
} }
fp := &pp.Flags fp := &pp.Flags
if pch != Door && ch != Door { if pch != Door && ch != Door {
if (pfl & FPassage) != (*fp & FPassage) { if (pfl & FPassage) != (*fp & FPassage) {
continue continue
} }
} }
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) { if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
if hero.X != x && hero.Y != y && if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) { !stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
@@ -61,25 +70,30 @@ func (g *RogueGame) look(wakeup bool) {
} }
tp := pp.Monst tp := pp.Monst
if tp == nil {
switch {
case tp == nil:
ch = g.tripCh(y, x, ch) ch = g.tripCh(y, x, ch)
} else if p.On(SenseMonsters) && tp.On(Invisible) { case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove { if g.DoorStop && !g.Firstmove {
g.Running = false g.Running = false
} }
continue continue
} else { default:
if wakeup { if wakeup {
g.wakeMonster(y, x) g.wakeMonster(y, x)
} }
if g.seeMonst(tp) { if g.seeMonst(tp) {
if p.On(Hallucinating) { if p.On(Hallucinating) {
ch = byte(g.rnd(26) + 'A') ch = g.randomMonsterLetter()
} else { } else {
ch = tp.Disguise ch = tp.Disguise
} }
} }
} }
if p.On(Blind) && (y != hero.Y || x != hero.X) { if p.On(Blind) && (y != hero.Y || x != hero.X) {
continue continue
} }
@@ -129,6 +143,7 @@ func (g *RogueGame) look(wakeup bool) {
continue continue
} }
} }
switch ch { switch ch {
case Door: case Door:
if x == hero.X || y == hero.Y { if x == hero.X || y == hero.Y {
@@ -145,9 +160,11 @@ func (g *RogueGame) look(wakeup bool) {
} }
} }
} }
if g.DoorStop && !g.Firstmove && passcount > 1 { if g.DoorStop && !g.Firstmove && passcount > 1 {
g.Running = false g.Running = false
} }
if !g.Running || !g.Options.Jump { if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh) g.mvaddch(hero.Y, hero.X, PlayerCh)
} }
@@ -165,26 +182,30 @@ func (g *RogueGame) tripCh(y, x int, ch byte) byte {
} }
} }
} }
return ch return ch
} }
// eraseLamp erases the area shown by a lamp in a dark room // eraseLamp erases the area shown by a lamp in a dark room
// (misc.c erase_lamp). // (misc.c erase_lamp).
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) { func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
if !(g.Options.SeeFloor && rp.Flags&(Gone|Dark) == Dark && if !g.Options.SeeFloor || rp.Flags&(Gone|Dark) != Dark ||
!g.Player.On(Blind)) { g.Player.On(Blind) {
return return
} }
ey := pos.Y + 1 ey := pos.Y + 1
ex := pos.X + 1 ex := pos.X + 1
sy := pos.Y - 1 sy := pos.Y - 1
for x := pos.X - 1; x <= ex; x++ { for x := pos.X - 1; x <= ex; x++ {
for y := sy; y <= ey; y++ { for y := sy; y <= ey; y++ {
if y == g.Player.Pos.Y && x == g.Player.Pos.X { if y == g.Player.Pos.Y && x == g.Player.Pos.X {
continue continue
} }
g.move(y, x) g.move(y, x)
if g.inch() == Floor { if g.inch() == Floor {
g.addch(' ') g.addch(' ')
} }
@@ -198,53 +219,53 @@ func (g *RogueGame) showFloor() bool {
if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) { if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) {
return g.Options.SeeFloor return g.Options.SeeFloor
} }
return true
}
// findObj finds the unclaimed object at (y, x) (misc.c find_obj). return true
func (g *RogueGame) findObj(y, x int) *Object {
for _, obj := range g.Level.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
} }
// eat lets her try to eat something (misc.c eat). // eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() { func (g *RogueGame) eat() {
obj := g.getItem("eat", KindFood) obj, ok := g.promptPackItem("eat", KindFood)
if obj == nil { if !ok {
return return
} }
if obj.Kind != KindFood { if obj.Kind != KindFood {
if !g.Options.Terse { if !g.Options.Terse {
g.msg("ugh, you would get ill if you ate that") g.msg("ugh, you would get ill if you ate that")
} else { } else {
g.msg("that's Inedible!") g.msg("that's Inedible!")
} }
return return
} }
p := &g.Player p := &g.Player
if p.FoodLeft < 0 { if p.FoodLeft < 0 {
p.FoodLeft = 0 p.FoodLeft = 0
} }
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize { if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
p.FoodLeft = StomachSize p.FoodLeft = StomachSize
} }
p.HungryState = 0 p.HungryState = 0
if obj == p.CurWeapon { if obj == p.CurWeapon {
p.CurWeapon = nil p.CurWeapon = nil
} }
if obj.Which == 1 {
switch {
case obj.Which == 1:
g.msg("my, that was a yummy %s", g.Fruit) g.msg("my, that was a yummy %s", g.Fruit)
} else if g.rnd(100) > 70 { case g.rnd(100) > 70:
p.Stats.Exp++ p.Stats.Exp++
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk")) g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
g.checkLevel() g.checkLevel()
} else { default:
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum")) g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
} }
g.leavePack(obj, false, false) g.leavePack(obj, false, false)
} }
@@ -252,38 +273,46 @@ func (g *RogueGame) eat() {
// check_level). // check_level).
func (g *RogueGame) checkLevel() { func (g *RogueGame) checkLevel() {
p := &g.Player p := &g.Player
var i int var i int
for i = 0; eLevels[i] != 0; i++ { for i = 0; g.data.eLevels[i] != 0; i++ {
if eLevels[i] > p.Stats.Exp { if g.data.eLevels[i] > p.Stats.Exp {
break break
} }
} }
i++ i++
olevel := p.Stats.Lvl olevel := p.Stats.Lvl
p.Stats.Lvl = i p.Stats.Lvl = i
if i > olevel { if i > olevel {
add := g.roll(i-olevel, 10) add := g.roll(i-olevel, 10)
p.Stats.MaxHP += add p.Stats.MaxHP += add
p.Stats.HP += add p.Stats.HP += add
g.msg("welcome to level %d", i) g.msg("welcome to level %d", i)
} }
} }
// chgStr modifies the player's strength, keeping track of the highest it // changeStrength modifies the player's strength, keeping track of the
// has been (misc.c chg_str). // highest it has been (misc.c chg_str).
func (g *RogueGame) chgStr(amt int) { func (g *RogueGame) changeStrength(amt int) {
if amt == 0 { if amt == 0 {
return return
} }
p := &g.Player p := &g.Player
addStr(&p.Stats.Str, amt) addStr(&p.Stats.Str, amt)
comp := p.Stats.Str comp := p.Stats.Str
if p.IsRing(Left, RingAddStrength) { if p.IsRing(Left, RingAddStrength) {
addStr(&comp, -p.CurRing[Left].Bonus) addStr(&comp, -p.CurRing[Left].Bonus)
} }
if p.IsRing(Right, RingAddStrength) { if p.IsRing(Right, RingAddStrength) {
addStr(&comp, -p.CurRing[Right].Bonus) addStr(&comp, -p.CurRing[Right].Bonus)
} }
if comp > p.MaxStats.Str { if comp > p.MaxStats.Str {
p.MaxStats.Str = comp p.MaxStats.Str = comp
} }
@@ -303,24 +332,29 @@ func (g *RogueGame) addHaste(potion bool) bool {
p := &g.Player p := &g.Player
if p.On(Hasted) { if p.On(Hasted) {
g.NoCommand += g.rnd(8) g.NoCommand += g.rnd(8)
p.Flags.Clear(Awake | Hasted) p.Flags.Clear(Awake | Hasted)
g.Extinguish(DNohaste) g.Extinguish(DNohaste)
g.msg("you faint from exhaustion") g.msg("you faint from exhaustion")
return false return false
} }
p.Flags.Set(Hasted) p.Flags.Set(Hasted)
if potion { if potion {
g.Fuse(DNohaste, 0, g.rnd(4)+4, After) g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
} }
return true return true
} }
// aggravate aggravates all the monsters on this level (misc.c aggravate). // aggravate aggravates all the monsters on this level (misc.c aggravate).
func (g *RogueGame) aggravate() { func (g *RogueGame) aggravate() {
// runto() can splice the monster list while we walk it, so iterate a copy. // runTo() can splice the monster list while we walk it, so iterate a copy.
monsters := append([]*Monster(nil), g.Level.Monsters...) monsters := append([]*Monster(nil), g.Level.Monsters...)
for _, mp := range monsters { for _, mp := range monsters {
g.runto(mp.Pos) g.runTo(mp.Pos)
} }
} }
@@ -330,10 +364,12 @@ func vowelstr(str string) string {
if str == "" { if str == "" {
return "" return ""
} }
switch str[0] { switch str[0] {
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U': case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
return "n" return "n"
} }
return "" return ""
} }
@@ -343,21 +379,25 @@ func (g *RogueGame) isCurrent(obj *Object) bool {
if obj == nil { if obj == nil {
return false return false
} }
p := &g.Player p := &g.Player
if obj == p.CurArmor || obj == p.CurWeapon || if obj == p.CurArmor || obj == p.CurWeapon ||
obj == p.CurRing[Left] || obj == p.CurRing[Right] { obj == p.CurRing[Left] || obj == p.CurRing[Right] {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("That's already ") g.addmsgf("That's already ")
} }
g.msg("in use") g.msg("in use")
return true return true
} }
return false return false
} }
// getDir sets up the direction coordinate for use in various "prefix" // promptDirection sets up the direction coordinate for use in various
// commands (misc.c get_dir). // "prefix" commands (misc.c get_dir).
func (g *RogueGame) getDir() bool { func (g *RogueGame) promptDirection() bool {
if g.Again && g.LastDir != 0 { if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt g.Delta = g.lastDelt
g.DirCh = g.LastDir g.DirCh = g.LastDir
@@ -367,8 +407,10 @@ func (g *RogueGame) getDir() bool {
prompt = "which direction? " prompt = "which direction? "
g.msg("%s", prompt) g.msg("%s", prompt)
} }
for { for {
gotit := true gotit := true
switch g.DirCh = g.readchar(); g.DirCh { switch g.DirCh = g.readchar(); g.DirCh {
case 'h', 'H': case 'h', 'H':
g.Delta = Coord{X: -1, Y: 0} g.Delta = Coord{X: -1, Y: 0}
@@ -389,30 +431,38 @@ func (g *RogueGame) getDir() bool {
case Escape: case Escape:
g.LastDir = 0 g.LastDir = 0
g.resetLast() g.resetLast()
return false return false
default: default:
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
g.msg("%s", prompt) g.msg("%s", prompt)
gotit = false gotit = false
} }
if gotit { if gotit {
break break
} }
} }
g.DirCh = toLower(g.DirCh) g.DirCh = toLower(g.DirCh)
g.LastDir = g.DirCh g.LastDir = g.DirCh
g.lastDelt = g.Delta g.lastDelt = g.Delta
} }
if g.Player.On(Confused) && g.rnd(5) == 0 { if g.Player.On(Confused) && g.rnd(5) == 0 {
for { for {
g.Delta.Y = g.rnd(3) - 1 g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1 g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 { if g.Delta.Y != 0 || g.Delta.X != 0 {
break break
} }
} }
} }
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
return true return true
} }
@@ -422,6 +472,7 @@ func (g *RogueGame) callIt(info *ObjInfo) {
info.Guess = "" info.Guess = ""
} else if info.Guess == "" { } else if info.Guess == "" {
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? ")) g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
buf := "" buf := ""
if g.getStr(&buf, g.scr.Std) == Norm { if g.getStr(&buf, g.scr.Std) == Norm {
if buf != "" { if buf != "" {
@@ -431,21 +482,17 @@ func (g *RogueGame) callIt(info *ObjInfo) {
} }
} }
// thingList is misc.c rnd_thing()'s static table.
var thingList = []byte{
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
}
// rndThing picks a random thing appropriate for this level (misc.c // rndThing picks a random thing appropriate for this level (misc.c
// rnd_thing). // rnd_thing).
func (g *RogueGame) rndThing() byte { func (g *RogueGame) rndThing() byte {
var i int var i int
if g.Depth >= AmuletLevel { if g.Depth >= AmuletLevel {
i = g.rnd(len(thingList)) i = g.rnd(len(g.data.thingList))
} else { } else {
i = g.rnd(len(thingList) - 1) i = g.rnd(len(g.data.thingList) - 1)
} }
return thingList[i]
return g.data.thingList[i]
} }
// chooseStr picks the first or second string depending on whether the // chooseStr picks the first or second string depending on whether the
@@ -454,6 +501,7 @@ func (g *RogueGame) chooseStr(ts, ns string) string {
if g.Player.On(Hallucinating) { if g.Player.On(Hallucinating) {
return ts return ts
} }
return ns return ns
} }
@@ -463,8 +511,10 @@ func unctrl(ch byte) string {
if ch < ' ' { if ch < ' ' {
return "^" + string(ch+'@') return "^" + string(ch+'@')
} }
if ch == 0x7f { if ch == 0x7f {
return "^?" return "^?"
} }
return string(ch) return string(ch)
} }

View File

@@ -2,33 +2,24 @@ package game
// monsters.c — monster creation and saving throws. // monsters.c — monster creation and saving throws.
// lvlMons and wandMons list monsters in rough order of vorpalness; zero
// entries in wandMons never wander (monsters.c).
var lvlMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
}
var wandMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
}
// randMonster picks a monster to show up; the lower the level, the meaner // randMonster picks a monster to show up; the lower the level, the meaner
// the monster (monsters.c randmonster). // the monster (monsters.c randmonster).
func (g *RogueGame) randMonster(wander bool) byte { func (g *RogueGame) randMonster(wander bool) byte {
mons := &lvlMons mons := &g.data.lvlMons
if wander { if wander {
mons = &wandMons mons = &g.data.wandMons
} }
for { for {
d := g.Depth + (g.rnd(10) - 6) d := g.Depth + (g.rnd(10) - 6)
if d < 0 { if d < 0 {
d = g.rnd(5) d = g.rnd(5)
} }
if d > 25 { if d > 25 {
d = g.rnd(5) + 21 d = g.rnd(5) + 21
} }
if mons[d] != 0 { if mons[d] != 0 {
return mons[d] 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 // newMonster picks a new monster and adds it to the list (monsters.c
// new_monster). // new_monster).
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) { func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
levAdd := g.Depth - AmuletLevel levAdd := max(g.Depth-AmuletLevel, 0)
if levAdd < 0 {
levAdd = 0 g.Level.AddMonster(tp)
}
attachMon(&g.Level.Monsters, tp)
tp.Type = typ tp.Type = typ
tp.Disguise = typ tp.Disguise = typ
tp.Pos = cp tp.Pos = cp
g.move(cp.Y, cp.X) g.move(cp.Y, cp.X)
tp.OldCh = g.inch() tp.OldCh = g.inch()
tp.Room = g.roomin(cp) tp.Room = g.roomIn(cp)
g.Level.SetMonsterAt(cp.Y, cp.X, tp) g.Level.SetMonsterAt(cp.Y, cp.X, tp)
mp := &g.Monsters[tp.Type-'A'] mp := &g.Monsters[tp.Type-'A']
tp.Stats.Lvl = mp.Stats.Lvl + levAdd tp.Stats.Lvl = mp.Stats.Lvl + levAdd
@@ -58,15 +47,19 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.Stats.Dmg = mp.Stats.Dmg tp.Stats.Dmg = mp.Stats.Dmg
tp.Stats.Str = mp.Stats.Str tp.Stats.Str = mp.Stats.Str
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp) tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
tp.Flags = mp.Flags tp.Flags = mp.Flags
if g.Depth > 29 { if g.Depth > 29 {
tp.Flags.Set(Hasted) tp.Flags.Set(Hasted)
} }
tp.Turn = true tp.Turn = true
tp.Pack = nil tp.Pack = nil
if g.Player.IsWearing(RingAggravateMonsters) { if g.Player.IsWearing(RingAggravateMonsters) {
g.runto(cp) g.runTo(cp)
} }
if typ == 'X' { if typ == 'X' {
tp.Disguise = g.rndThing() tp.Disguise = g.rndThing()
} }
@@ -81,11 +74,13 @@ func expAdd(tp *Monster) int {
} else { } else {
mod = tp.Stats.MaxHP / 6 mod = tp.Stats.MaxHP / 6
} }
if tp.Stats.Lvl > 9 { if tp.Stats.Lvl > 9 {
mod *= 20 mod *= 20
} else if tp.Stats.Lvl > 6 { } else if tp.Stats.Lvl > 6 {
mod *= 4 mod *= 4
} }
return mod return mod
} }
@@ -93,34 +88,42 @@ func expAdd(tp *Monster) int {
// (monsters.c wanderer). // (monsters.c wanderer).
func (g *RogueGame) wanderer() { func (g *RogueGame) wanderer() {
tp := &Monster{} tp := &Monster{}
var cp Coord var cp Coord
for { for {
cp, _ = g.findFloor(nil, 0, true) cp, _ = g.findFloor(true)
if g.roomin(cp) != g.Player.Room { if g.roomIn(cp) != g.Player.Room {
break break
} }
} }
g.newMonster(tp, g.randMonster(true), cp) g.newMonster(tp, g.randMonster(true), cp)
if g.Player.On(SenseMonsters) { if g.Player.On(SenseMonsters) {
g.standout() g.standout()
if !g.Player.On(Hallucinating) { if !g.Player.On(Hallucinating) {
g.addch(tp.Type) g.addch(tp.Type)
} else { } else {
g.addch(byte(g.rnd(26) + 'A')) g.addch(g.randomMonsterLetter())
} }
g.standend() g.standend()
} }
g.runto(tp.Pos)
g.runTo(tp.Pos)
} }
// wakeMonster is what to do when the hero steps next to a monster // wakeMonster is what to do when the hero steps next to a monster
// (monsters.c wake_monster). // (monsters.c wake_monster).
func (g *RogueGame) wakeMonster(y, x int) *Monster { func (g *RogueGame) wakeMonster(y, x int) *Monster {
p := &g.Player p := &g.Player
tp := g.Level.MonsterAt(y, x) tp := g.Level.MonsterAt(y, x)
if tp == nil { if tp == nil {
panic("can't find monster in wake_monster") panic("can't find monster in wake_monster")
} }
ch := tp.Type ch := tp.Type
// Every time he sees a mean monster, it might start chasing him // Every time he sees a mean monster, it might start chasing him
if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) && if !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
@@ -128,24 +131,30 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
tp.Dest = &p.Pos tp.Dest = &p.Pos
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
} }
if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) && if ch == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) { !tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake) {
rp := p.Room rp := p.Room
if (rp != nil && !rp.Flags.Has(Dark)) || if (rp != nil && !rp.Flags.Has(Dark)) ||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
tp.Flags.Set(Found) tp.Flags.Set(Found)
if !g.save(VsMagic) { if !g.save(VsMagic) {
if p.On(Confused) { if p.On(Confused) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration)) g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else { } else {
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After) g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
} }
p.Flags.Set(Confused) p.Flags.Set(Confused)
mname := g.setMname(tp) mname := g.setMname(tp)
g.addmsg("%s", mname) g.addmsgf("%s", mname)
if mname != "it" { if mname != "it" {
g.addmsg("'") g.addmsgf("'")
} }
g.msg("s gaze has confused you") g.msg("s gaze has confused you")
} }
} }
@@ -153,12 +162,14 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
// Let greedy ones guard gold // Let greedy ones guard gold
if tp.On(Greedy) && !tp.On(Awake) { if tp.On(Greedy) && !tp.On(Awake) {
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
if p.Room.GoldVal != 0 { if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold tp.Dest = &p.Room.Gold
} else { } else {
tp.Dest = &p.Pos tp.Dest = &p.Pos
} }
} }
return tp return tp
} }
@@ -174,6 +185,7 @@ func (g *RogueGame) givePack(tp *Monster) {
// save_throw). // save_throw).
func (g *RogueGame) saveThrow(which int, st *Stats) bool { func (g *RogueGame) saveThrow(which int, st *Stats) bool {
need := 14 + which - st.Lvl/2 need := 14 + which - st.Lvl/2
return g.roll(1, 20) >= need return g.roll(1, 20) >= need
} }
@@ -185,9 +197,17 @@ func (g *RogueGame) save(which int) bool {
if p.IsRing(Left, RingProtection) { if p.IsRing(Left, RingProtection) {
which -= p.CurRing[Left].Bonus which -= p.CurRing[Left].Bonus
} }
if p.IsRing(Right, RingProtection) { if p.IsRing(Right, RingProtection) {
which -= p.CurRing[Right].Bonus which -= p.CurRing[Right].Bonus
} }
} }
return g.saveThrow(which, &p.Stats) 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,158 +2,203 @@ package game
// move.c — hero movement commands. // move.c — hero movement commands.
// doRun starts the hero running (move.c do_run). // startRun starts the hero running (move.c do_run).
func (g *RogueGame) doRun(ch byte) { func (g *RogueGame) startRun(ch byte) {
g.Running = true g.Running = true
g.After = false g.After = false
g.RunCh = ch g.RunCh = ch
} }
// doMove checks that a move is legal and handles the consequences — // moveHero checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move). // fighting, picking up, etc. (move.c do_move). The C `goto over`
func (g *RogueGame) doMove(dy, dx int) { // re-check after a passage turn is the retry loop.
func (g *RogueGame) moveHero(dy, dx int) {
p := &g.Player p := &g.Player
g.Firstmove = false g.Firstmove = false
if g.NoMove > 0 { if g.NoMove > 0 {
g.NoMove-- g.NoMove--
g.msg("you are still stuck in the bear trap") g.msg("you are still stuck in the bear trap")
return return
} }
// Do a confused move (maybe) // Do a confused move (maybe)
var nh Coord var nh Coord
if p.On(Confused) && g.rnd(5) != 0 { if p.On(Confused) && g.rnd(5) != 0 {
nh = g.rndmove(&p.Creature) nh = g.randomStep(&p.Creature)
if nh == p.Pos { if nh == p.Pos {
g.After = false g.After = false
g.Running = false g.Running = false
g.ToDeath = false g.ToDeath = false
return return
} }
} else { } else {
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx} nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
} }
over: for {
// Check if he tried to move off the screen or make an illegal diagonal // Check if he tried to move off the screen or make an illegal
// move, and stop him if he did. // diagonal move, and stop him if he did.
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1 hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
var ch byte
var fl PlaceFlags var (
if !hitBound { ch byte
if !g.diagOk(p.Pos, nh) { fl PlaceFlags
g.After = false )
g.Running = false
return if !hitBound {
} if !g.diagOk(p.Pos, nh) {
if g.Running && p.Pos == nh { g.After = false
g.After = false g.Running = false
g.Running = false
} return
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
g.Level.SetChar(nh.Y, nh.X, Trap)
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
} }
} else if p.On(Held) && ch != 'F' {
g.msg("you are being held") if g.Running && p.Pos == nh {
return g.After = false
} g.Running = false
} }
if hitBound {
ch = ' ' // fall into the wall case below fl = *g.Level.FlagsAt(nh.Y, nh.X)
}
switch ch { ch = g.Level.VisibleChar(nh.Y, nh.X)
case ' ', '|', '-': if !fl.Has(FReal) && ch == Floor {
if g.Options.PassGo && g.Running && p.Room.Flags.Has(Gone) && if !p.On(Levitating) {
!p.On(Blind) { ch = Trap
var b1, b2 bool g.Level.SetChar(nh.Y, nh.X, Trap)
switch g.RunCh { g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
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': } else if p.On(Held) && ch != 'F' {
b1 = p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1) g.msg("you are being held")
b2 = p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
if b1 != b2 { return
if b1 { }
g.RunCh = 'h' }
dx = -1
} else { if hitBound {
g.RunCh = 'l' ch = ' ' // fall into the wall case below
dx = 1 }
}
dy = 0 switch ch {
g.turnref() case ' ', '|', '-':
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx} if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
goto over dy, dx = ndy, ndx
g.turnRefresh()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
continue // the C goto over: re-check the turned move
}
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.finishMove(nh, fl)
case Trap:
tr := g.springTrap(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
g.finishMove(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.finishMove(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.springTrap(p.Pos)
}
g.finishMove(nh, fl)
default:
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)
} else {
if ch != Stairs {
g.Take = ch
} }
g.finishMove(nh, fl)
} }
} }
g.Running = false
g.After = false return
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
g.enterRoom(nh)
}
g.moveStuff(nh, fl)
case Trap:
tr := g.beTrapped(nh)
if tr == TrapDoor || tr == TrapTeleport {
return
}
g.moveStuff(nh, fl)
case Passage:
// when you're in a corridor, you don't know if you're in a maze
// room or not, and there ain't no way to find out if you're
// leaving a maze room, so it is necessary to always recalculate
// proom.
p.Room = g.roomin(p.Pos)
g.moveStuff(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.beTrapped(p.Pos)
}
g.moveStuff(nh, fl)
default:
if ch == Stairs {
g.SeenStairs = true
}
g.Running = false
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
g.fight(nh, p.CurWeapon, false)
} else {
if ch != Stairs {
g.Take = ch
}
g.moveStuff(nh, fl)
}
} }
} }
// moveStuff is the move_stuff label in do_move: complete the step. // passageTurn checks whether a runner in a gone-room passage should turn
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) { // the corner instead of stopping at a wall (the PASSGO block of move.c
// do_move). It reports whether to turn and the new deltas, updating RunCh.
func (g *RogueGame) passageTurn(dy, dx int) (bool, int, int) {
p := &g.Player
if !g.Options.PassGo || !g.Running || !p.Room.Flags.Has(Gone) ||
p.On(Blind) {
return false, dy, dx
}
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
}
return true, dy, 0
}
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
}
return true, 0, dx
}
}
return false, dy, dx
}
// finishMove is the move_stuff label in do_move: complete the step.
func (g *RogueGame) finishMove(nh Coord, fl PlaceFlags) {
p := &g.Player p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
if fl.Has(FPassage) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door { if fl.Has(FPassage) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
g.leaveRoom(nh) g.leaveRoom(nh)
} }
p.Pos = nh p.Pos = nh
} }
@@ -161,17 +206,21 @@ func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
// (move.c turn_ok). // (move.c turn_ok).
func (g *RogueGame) turnOk(y, x int) bool { func (g *RogueGame) turnOk(y, x int) bool {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage) return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage)
} }
// turnref decides whether to refresh at a passage turning (move.c turnref). // turnRefresh decides whether to refresh at a passage turning (move.c
func (g *RogueGame) turnref() { // turnref).
func (g *RogueGame) turnRefresh() {
p := &g.Player p := &g.Player
pp := g.Level.At(p.Pos.Y, p.Pos.X) pp := g.Level.At(p.Pos.Y, p.Pos.X)
if !pp.Flags.Has(FSeen) { if !pp.Flags.Has(FSeen) {
if g.Options.Jump { if g.Options.Jump {
g.refresh() g.refresh()
} }
pp.Flags.Set(FSeen) pp.Flags.Set(FSeen)
} }
} }
@@ -182,6 +231,7 @@ func (g *RogueGame) doorOpen(rp *Room) {
if rp.Flags.Has(Gone) { if rp.Flags.Has(Gone) {
return return
} }
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ { 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++ { for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
if isUpper(g.Level.VisibleChar(y, x)) { if isUpper(g.Level.VisibleChar(y, x)) {
@@ -191,18 +241,20 @@ func (g *RogueGame) doorOpen(rp *Room) {
} }
} }
// beTrapped makes him pay for stepping on a trap (move.c be_trapped). // springTrap makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) beTrapped(tc Coord) TrapKind { func (g *RogueGame) springTrap(tc Coord) TrapKind {
p := &g.Player p := &g.Player
if p.On(Levitating) { if p.On(Levitating) {
return TrapRust // anything that's not a door or teleport return TrapRust // anything that's not a door or teleport
} }
g.Running = false g.Running = false
g.Count = 0 g.Count = 0
pp := g.Level.At(tc.Y, tc.X) pp := g.Level.At(tc.Y, tc.X)
pp.Ch = Trap pp.Ch = Trap
tr := TrapKind(pp.Flags & FTrapMask) tr := TrapKind(pp.Flags & FTrapMask)
pp.Flags.Set(FSeen) pp.Flags.Set(FSeen)
switch tr { switch tr {
case TrapDoor: case TrapDoor:
g.Depth++ g.Depth++
@@ -216,17 +268,17 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
case 0: case 0:
g.msg("you are suddenly in a parallel dimension") g.msg("you are suddenly in a parallel dimension")
case 1: case 1:
g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(rainbow))]) g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 2: case 2:
g.msg("you feel a sting in the side of your neck") g.msg("you feel a sting in the side of your neck")
case 3: case 3:
g.msg("multi-colored lines swirl around you, then fade") g.msg("multi-colored lines swirl around you, then fade")
case 4: case 4:
g.msg("a %s light flashes in your eyes", rainbow[g.rnd(len(rainbow))]) g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 5: case 5:
g.msg("a spike shoots past your ear!") g.msg("a spike shoots past your ear!")
case 6: case 6:
g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(rainbow))]) g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 7: case 7:
g.msg("you suddenly feel very thirsty") g.msg("you suddenly feel very thirsty")
case 8: case 8:
@@ -234,10 +286,11 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
case 9: case 9:
g.msg("time now seems to be going slower") g.msg("time now seems to be going slower")
case 10: case 10:
g.msg("you pack turns %s!", rainbow[g.rnd(len(rainbow))]) g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
} }
case TrapSleep: case TrapSleep:
g.NoCommand += g.spread(5) // SLEEPTIME g.NoCommand += g.spread(5) // SLEEPTIME
p.Flags.Clear(Awake) p.Flags.Clear(Awake)
g.msg("a strange white mist envelops you and you fall asleep") g.msg("a strange white mist envelops you and you fall asleep")
case TrapArrow: case TrapArrow:
@@ -271,22 +324,26 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
g.msg("a poisoned dart killed you") g.msg("a poisoned dart killed you")
g.death('d') g.death('d')
} }
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) { if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
g.chgStr(-1) g.changeStrength(-1)
} }
g.msg("a small dart just hit you in the shoulder") g.msg("a small dart just hit you in the shoulder")
} }
case TrapRust: case TrapRust:
g.msg("a gush of water hits you on the head") g.msg("a gush of water hits you on the head")
g.rustArmor(p.CurArmor) g.rustArmor(p.CurArmor)
} }
g.flushType() g.flushType()
return tr return tr
} }
// rndmove moves in a random direction if the monster/person is confused // randomStep moves in a random direction if the monster/person is
// (move.c rndmove). // confused (move.c rndmove).
func (g *RogueGame) rndmove(who *Creature) Coord { func (g *RogueGame) randomStep(who *Creature) Coord {
ret := Coord{ ret := Coord{
Y: who.Pos.Y + g.rnd(3) - 1, Y: who.Pos.Y + g.rnd(3) - 1,
X: who.Pos.X + g.rnd(3) - 1, X: who.Pos.X + g.rnd(3) - 1,
@@ -296,25 +353,32 @@ func (g *RogueGame) rndmove(who *Creature) Coord {
if ret == who.Pos { if ret == who.Pos {
return ret return ret
} }
if !g.diagOk(who.Pos, ret) { if !g.diagOk(who.Pos, ret) {
return who.Pos return who.Pos
} }
ch := g.Level.VisibleChar(ret.Y, ret.X) ch := g.Level.VisibleChar(ret.Y, ret.X)
if !stepOk(ch) { if !stepOk(ch) {
return who.Pos return who.Pos
} }
if ch == Scroll { if ch == Scroll {
var found *Object var found *Object
for _, obj := range g.Level.Objects { for _, obj := range g.Level.Objects {
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X { if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
found = obj found = obj
break break
} }
} }
if found != nil && found.ScrollKind() == ScrollScareMonster { if found != nil && found.ScrollKind() == ScrollScareMonster {
return who.Pos return who.Pos
} }
} }
return ret return ret
} }
@@ -332,6 +396,7 @@ func (g *RogueGame) rustArmor(arm *Object) {
} }
} else { } else {
arm.ArmorClass++ arm.ArmorClass++
if !g.Options.Terse { if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!") g.msg("your armor appears to be weaker now. Oh my!")
} else { } else {

View File

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

View File

@@ -7,24 +7,30 @@ import (
func genLevel(t *testing.T, seed int32) *RogueGame { func genLevel(t *testing.T, seed int32) *RogueGame {
t.Helper() t.Helper()
g := NewGame(Config{Seed: seed}) g := NewGame(Config{Seed: seed})
g.NewLevel() g.NewLevel()
return g return g
} }
// renderMap draws the raw level map (not the screen) as text. // renderMap draws the raw level map (not the screen) as text.
func renderMap(g *RogueGame) string { func renderMap(g *RogueGame) string {
var sb strings.Builder 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) ch := g.Level.Char(y, x)
if m := g.Level.MonsterAt(y, x); m != nil { if m := g.Level.MonsterAt(y, x); m != nil {
ch = m.Type ch = m.Type
} }
sb.WriteByte(ch) sb.WriteByte(ch)
} }
sb.WriteByte('\n') sb.WriteByte('\n')
} }
return sb.String() return sb.String()
} }
@@ -44,9 +50,11 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: hero on unwalkable cell %q", seed, t.Errorf("seed %d: hero on unwalkable cell %q", seed,
g.Level.Char(hp.Y, hp.X)) g.Level.Char(hp.Y, hp.X))
} }
if g.Level.MonsterAt(hp.Y, hp.X) != nil { if g.Level.MonsterAt(hp.Y, hp.X) != nil {
t.Errorf("seed %d: hero standing on a monster", seed) t.Errorf("seed %d: hero standing on a monster", seed)
} }
if g.Player.Room == nil { if g.Player.Room == nil {
t.Errorf("seed %d: hero not in any room", seed) t.Errorf("seed %d: hero not in any room", seed)
} }
@@ -56,6 +64,7 @@ func TestNewLevelInvariants(t *testing.T) {
if !strings.Contains(m, "|") || !strings.Contains(m, "-") { if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
t.Errorf("seed %d: no room walls drawn", seed) t.Errorf("seed %d: no room walls drawn", seed)
} }
if !strings.Contains(m, ".") && !strings.Contains(m, "#") { if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
t.Errorf("seed %d: no floor or passages drawn", seed) t.Errorf("seed %d: no floor or passages drawn", seed)
} }
@@ -66,6 +75,7 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: monster %c not indexed at its position", t.Errorf("seed %d: monster %c not indexed at its position",
seed, mon.Type) seed, mon.Type)
} }
if mon.Room == nil { if mon.Room == nil {
t.Errorf("seed %d: monster %c has no room", seed, mon.Type) t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
} }
@@ -87,10 +97,12 @@ func TestNewLevelInvariants(t *testing.T) {
t.Errorf("seed %d: starting pack has %d items, want 5", t.Errorf("seed %d: starting pack has %d items, want 5",
seed, len(g.Player.Pack)) seed, len(g.Player.Pack))
} }
if g.Player.CurWeapon == nil || if g.Player.CurWeapon == nil ||
g.Player.CurWeapon.WeaponKind() != WeaponMace { g.Player.CurWeapon.WeaponKind() != WeaponMace {
t.Errorf("seed %d: not wielding the starting mace", seed) t.Errorf("seed %d: not wielding the starting mace", seed)
} }
if g.Player.CurArmor == nil || if g.Player.CurArmor == nil ||
g.Player.CurArmor.ArmorKind() != ArmorRingMail { g.Player.CurArmor.ArmorKind() != ArmorRingMail {
t.Errorf("seed %d: not wearing the starting ring mail", seed) t.Errorf("seed %d: not wearing the starting ring mail", seed)
@@ -100,10 +112,12 @@ func TestNewLevelInvariants(t *testing.T) {
func TestNewLevelDeterministic(t *testing.T) { func TestNewLevelDeterministic(t *testing.T) {
a := renderMap(genLevel(t, 12345)) a := renderMap(genLevel(t, 12345))
b := renderMap(genLevel(t, 12345)) b := renderMap(genLevel(t, 12345))
if a != b { if a != b {
t.Error("same seed produced different levels") t.Error("same seed produced different levels")
} }
c := renderMap(genLevel(t, 54321)) c := renderMap(genLevel(t, 54321))
if a == c { if a == c {
t.Error("different seeds produced identical levels") t.Error("different seeds produced identical levels")
@@ -118,6 +132,7 @@ func TestDeeperLevels(t *testing.T) {
for depth := 1; depth <= 30; depth++ { for depth := 1; depth <= 30; depth++ {
g.Depth = depth g.Depth = depth
g.NewLevel() g.NewLevel()
st := g.Level.Stairs st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs { if g.Level.Char(st.Y, st.X) != Stairs {
t.Fatalf("seed %d depth %d: missing staircase", seed, depth) 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). // category (ObjectKind) from its display character (Glyph).
type ObjectKind int type ObjectKind int
// Item categories.
const ( const (
KindNone ObjectKind = iota KindNone ObjectKind = iota
KindPotion KindPotion
@@ -25,35 +26,49 @@ const (
KindRingOrStick ObjectKind = -2 KindRingOrStick ObjectKind = -2
) )
// kindGlyphs maps each kind to the character Rogue draws for it. // Category words shared by ObjectKind.String, the discovery list, and the
var kindGlyphs = [...]byte{ // ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the
KindNone: ' ', // glyph byte constants.)
KindPotion: Potion, const (
KindScroll: Scroll, potionName = "potion"
KindFood: Food, scrollName = "scroll"
KindWeapon: Weapon, ringName = "ring"
KindArmor: Armor, goldName = "gold"
KindRing: Ring, )
KindWand: Stick,
KindAmulet: Amulet,
KindGold: Gold,
}
// Glyph returns the map/display character for this kind of object. // Glyph returns the map/display character for this kind of object.
func (k ObjectKind) Glyph() byte { func (k ObjectKind) Glyph() byte {
if k < 0 || int(k) >= len(kindGlyphs) { switch k {
return ' ' case KindPotion:
return Potion
case KindScroll:
return Scroll
case KindFood:
return Food
case KindWeapon:
return Weapon
case KindArmor:
return Armor
case KindRing:
return Ring
case KindWand:
return Stick
case KindAmulet:
return Amulet
case KindGold:
return Gold
} }
return kindGlyphs[k]
return ' '
} }
// String names the category the way the C type_name() did. // String names the category the way the C type_name() did.
func (k ObjectKind) String() string { func (k ObjectKind) String() string {
switch k { switch k {
case KindPotion: case KindPotion:
return "potion" return potionName
case KindScroll: case KindScroll:
return "scroll" return scrollName
case KindFood: case KindFood:
return "food" return "food"
case KindWeapon: case KindWeapon:
@@ -61,27 +76,44 @@ func (k ObjectKind) String() string {
case KindArmor: case KindArmor:
return "suit of armor" return "suit of armor"
case KindRing: case KindRing:
return "ring" return ringName
case KindWand: case KindWand:
return "wand or staff" return "wand or staff"
case KindAmulet: case KindAmulet:
return "amulet" return "amulet"
case KindGold: case KindGold:
return "gold" return goldName
case KindRingOrStick: case KindRingOrStick:
return "ring, wand or staff" return "ring, wand or staff"
} }
return "bizarre thing" return "bizarre thing"
} }
// objectKindForGlyph is the reverse of Glyph: what category of item does a // objectKindForGlyph is the reverse of Glyph: what category of item does a
// map character denote. Returns KindNone for non-item characters. // map character denote. Returns KindNone for non-item characters.
func objectKindForGlyph(ch byte) ObjectKind { func objectKindForGlyph(ch byte) ObjectKind {
for k, g := range kindGlyphs { switch ch {
if g == ch && ObjectKind(k) != KindNone { case Potion:
return ObjectKind(k) return KindPotion
} case Scroll:
return KindScroll
case Food:
return KindFood
case Weapon:
return KindWeapon
case Armor:
return KindArmor
case Ring:
return KindRing
case Stick:
return KindWand
case Amulet:
return KindAmulet
case Gold:
return KindGold
} }
return KindNone return KindNone
} }
@@ -148,6 +180,7 @@ func detachObj(list *[]*Object, item *Object) {
for i, o := range *list { for i, o := range *list {
if o == item { if o == item {
*list = append((*list)[:i], (*list)[i+1:]...) *list = append((*list)[:i], (*list)[i+1:]...)
return return
} }
} }

View File

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

View File

@@ -7,29 +7,34 @@ package game
func (g *RogueGame) addPack(obj *Object, silent bool) { func (g *RogueGame) addPack(obj *Object, silent bool) {
p := &g.Player p := &g.Player
fromFloor := false fromFloor := false
if obj == nil { if obj == nil {
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil { if obj = g.Level.ObjectAt(p.Pos.Y, p.Pos.X); obj == nil {
return return
} }
fromFloor = true fromFloor = true
} }
// Check for and deal with scare monster scrolls // Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) { if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) { if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage) g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else { } else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor) g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
} }
g.msg("the scroll turns to dust as you pick it up") g.msg("the scroll turns to dust as you pick it up")
return return
} }
if len(p.Pack) == 0 { if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj) p.Pack = append(p.Pack, obj)
obj.PackCh = g.packChar() obj.PackCh = p.nextPackChar()
p.Inpack++ p.Inpack++
} else { } else {
// Walk the pack looking for the insertion point, keeping items of // Walk the pack looking for the insertion point, keeping items of
@@ -38,9 +43,11 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
// after; -1 after a merge means no insertion. // after; -1 after a merge means no insertion.
lp := -1 lp := -1
merged := false merged := false
for i := 0; i < len(p.Pack); i++ { for i := 0; i < len(p.Pack); i++ {
if p.Pack[i].Kind != obj.Kind { if p.Pack[i].Kind != obj.Kind {
lp = i lp = i
continue continue
} }
// found the group of our type: scan for matching subtype // found the group of our type: scan for matching subtype
@@ -49,19 +56,23 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
if i+1 >= len(p.Pack) { if i+1 >= len(p.Pack) {
break break
} }
i++ i++
} }
op := p.Pack[i] op := p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which { if op.Kind == obj.Kind && op.Which == obj.Which {
if op.Kind.MergesInPack() { switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) { if !g.packRoom(fromFloor, obj) {
return return
} }
op.Count++ op.Count++
obj = op obj = op
lp = -1 lp = -1
merged = true merged = true
} else if obj.Group != 0 { case obj.Group != 0:
lp = i lp = i
for p.Pack[i].Kind == obj.Kind && for p.Pack[i].Kind == obj.Kind &&
p.Pack[i].Which == obj.Which && p.Pack[i].Which == obj.Which &&
@@ -70,24 +81,29 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
if i+1 >= len(p.Pack) { if i+1 >= len(p.Pack) {
break break
} }
i++ i++
} }
op = p.Pack[i] op = p.Pack[i]
if op.Kind == obj.Kind && op.Which == obj.Which && if op.Kind == obj.Kind && op.Which == obj.Which &&
op.Group == obj.Group { op.Group == obj.Group {
op.Count += obj.Count op.Count += obj.Count
p.Inpack-- p.Inpack--
if !g.packRoom(fromFloor, obj) { if !g.packRoom(fromFloor, obj) {
return return
} }
obj = op obj = op
lp = -1 lp = -1
merged = true merged = true
} }
} else { default:
lp = i lp = i
} }
} }
break break
} }
@@ -95,7 +111,8 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
if !g.packRoom(fromFloor, obj) { if !g.packRoom(fromFloor, obj) {
return return
} }
obj.PackCh = g.packChar()
obj.PackCh = p.nextPackChar()
p.Pack = append(p.Pack[:lp+1], p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...) append([]*Object{obj}, p.Pack[lp+1:]...)...)
} }
@@ -117,9 +134,10 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
// Notify the user // Notify the user
if !silent { if !silent {
if !g.Options.Terse { 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)
} }
} }
@@ -129,90 +147,77 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
p := &g.Player p := &g.Player
if p.Inpack++; p.Inpack > MaxPack { if p.Inpack++; p.Inpack > MaxPack {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("there's ") g.addmsgf("there's ")
} }
g.addmsg("no room")
g.addmsgf("no room")
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg(" in your pack") g.addmsgf(" in your pack")
} }
g.endmsg() g.endmsg()
if fromFloor { if fromFloor {
g.moveMsg(obj) g.moveMsg(obj)
} }
p.Inpack = MaxPack p.Inpack = MaxPack
return false return false
} }
if fromFloor { if fromFloor {
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) { if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage) g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else { } else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor) g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
} }
} }
return true return true
} }
// leavePack takes an item out of the pack (pack.c leave_pack). // leavePack takes an item out of the pack (pack.c leave_pack), keeping
// the repeat-command bookkeeping; the pack surgery is
// Player.removeFromPack.
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object { func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all { if obj.Count > 1 && !all {
g.LastPick = obj g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else { } else {
g.LastPick = nil g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
} }
return nobj
}
// packChar returns the next unused pack character (pack.c pack_char). return g.Player.removeFromPack(obj, newobj, all)
func (g *RogueGame) packChar() byte {
p := &g.Player
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
} }
// inventory lists what is in the pack; returns true if there is something // inventory lists what is in the pack; returns true if there is something
// of the given type (pack.c inventory). // of the given type (pack.c inventory).
func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool { func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.NObjs = 0 g.NObjs = 0
for _, item := range list { for _, item := range list {
if kind != KindNone && kind != item.Kind && if !matchesFilter(kind, item) {
!(kind == KindCallable && item.Kind != KindFood &&
item.Kind != KindAmulet) &&
!(kind == KindRingOrStick &&
(item.Kind == KindRing || item.Kind == KindWand)) {
continue continue
} }
g.NObjs++ g.NObjs++
g.Msgs.MsgEsc = true g.Msgs.MsgEsc = true
line := string(item.PackCh) + ") " + g.invName(item, false)
line := string(item.PackCh) + ") " + g.inventoryName(item, false)
if g.addLine("%s", line) == Escape { if g.addLine("%s", line) == Escape {
g.Msgs.MsgEsc = false g.Msgs.MsgEsc = false
g.msg("") g.msg("")
return true return true
} }
g.Msgs.MsgEsc = false g.Msgs.MsgEsc = false
} }
if g.NObjs == 0 { if g.NObjs == 0 {
if g.Options.Terse { if g.Options.Terse {
if kind == KindNone { if kind == KindNone {
@@ -227,9 +232,12 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.msg("you don't have anything appropriate") g.msg("you don't have anything appropriate")
} }
} }
return false return false
} }
g.endLine() g.endLine()
return true return true
} }
@@ -240,31 +248,53 @@ func (g *RogueGame) pickUp(ch byte) {
return return
} }
obj := g.findObj(p.Pos.Y, p.Pos.X) obj := g.Level.ObjectAt(p.Pos.Y, p.Pos.X)
if g.MoveOn { if g.MoveOn {
g.moveMsg(obj) g.moveMsg(obj)
return return
} }
switch ch { switch ch {
case Gold: case Gold:
if obj == nil { if obj == nil {
return return
} }
g.money(obj.GoldValue) g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj) g.Level.RemoveObject(obj)
p.Room.GoldVal = 0 p.Room.GoldVal = 0
default: default:
g.addPack(nil, false) 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 // moveMsg prints the message if you are just moving onto an object
// (pack.c move_msg). // (pack.c move_msg).
func (g *RogueGame) moveMsg(obj *Object) { func (g *RogueGame) moveMsg(obj *Object) {
if !g.Options.Terse { 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 // pickyInven allows the player to inventory a single item (pack.c
@@ -273,50 +303,68 @@ func (g *RogueGame) pickyInven() {
p := &g.Player p := &g.Player
if len(p.Pack) == 0 { if len(p.Pack) == 0 {
g.msg("you aren't carrying anything") g.msg("you aren't carrying anything")
return return
} }
if len(p.Pack) == 1 { if len(p.Pack) == 1 {
g.msg("a) %s", g.invName(p.Pack[0], false)) g.msg("a) %s", g.inventoryName(p.Pack[0], false))
return return
} }
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: ")) g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
mch := g.readchar() mch := g.readchar()
if mch == Escape { if mch == Escape {
g.msg("") g.msg("")
return return
} }
for _, obj := range p.Pack { for _, obj := range p.Pack {
if mch == obj.PackCh { if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false)) g.msg("%c) %s", mch, g.inventoryName(obj, false))
return return
} }
} }
g.msg("'%s' not in pack", unctrl(mch)) g.msg("'%s' not in pack", unctrl(mch))
} }
// getItem picks something out of a pack for a purpose (pack.c get_item). // promptPackItem picks something out of a pack for a purpose (pack.c
func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object { // get_item); ok reports whether the player chose an item.
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (obj *Object, ok bool) {
p := &g.Player p := &g.Player
if len(p.Pack) == 0 { if len(p.Pack) == 0 {
g.msg("you aren't carrying anything") g.msg("you aren't carrying anything")
return nil
return nil, false
} }
if g.Again { if g.Again {
if g.LastPick != nil { if g.LastPick != nil {
return g.LastPick return g.LastPick, true
} }
g.msg("you ran out") g.msg("you ran out")
return nil
return nil, false
} }
for { for {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("which object do you want to ") g.addmsgf("which object do you want to ")
} }
g.addmsg("%s", purpose)
g.addmsgf("%s", purpose)
if g.Options.Terse { if g.Options.Terse {
g.addmsg(" what") g.addmsgf(" what")
} }
g.msg("? (* for list): ") g.msg("? (* for list): ")
ch := g.readchar() ch := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -325,22 +373,28 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
g.resetLast() g.resetLast()
g.After = false g.After = false
g.msg("") g.msg("")
return nil
return nil, false
} }
g.NObjs = 1 // normal case: person types one char g.NObjs = 1 // normal case: person types one char
if ch == '*' { if ch == '*' {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if !g.inventory(p.Pack, kind) { if !g.inventory(p.Pack, kind) {
g.After = false g.After = false
return nil
return nil, false
} }
continue continue
} }
for _, obj := range p.Pack { for _, obj := range p.Pack {
if obj.PackCh == ch { if obj.PackCh == ch {
return obj return obj, true
} }
} }
g.msg("'%s' is not a valid item", unctrl(ch)) g.msg("'%s' is not a valid item", unctrl(ch))
} }
} }
@@ -350,15 +404,18 @@ func (g *RogueGame) money(value int) {
p := &g.Player p := &g.Player
p.Purse += value p.Purse += value
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) { if p.Room.Flags.Has(Gone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage) g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else { } else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor) g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
} }
if value > 0 { if value > 0 {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you found ") g.addmsgf("you found ")
} }
g.msg("%d gold pieces", value) g.msg("%d gold pieces", value)
} }
} }
@@ -369,9 +426,11 @@ func (g *RogueGame) floorCh() byte {
if g.Player.Room.Flags.Has(Gone) { if g.Player.Room.Flags.Has(Gone) {
return Passage return Passage
} }
if g.showFloor() { if g.showFloor() {
return Floor return Floor
} }
return ' ' return ' '
} }
@@ -382,6 +441,7 @@ func (g *RogueGame) floorAt() byte {
if ch == Floor { if ch == Floor {
ch = g.floorCh() ch = g.floorCh()
} }
return ch return ch
} }
@@ -398,5 +458,6 @@ func (g *RogueGame) chooseTerse(terse, verbose string) string {
if g.Options.Terse { if g.Options.Terse {
return terse return terse
} }
return verbose return verbose
} }

View File

@@ -2,40 +2,32 @@ package game
// passages.c — draw the connecting passages. // passages.c — draw the connecting passages.
// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages. // digPassages draws all the passages on a level (passages.c do_passages).
var rdesConn = [MaxRooms][MaxRooms]bool{ func (g *RogueGame) digPassages() {
{false, true, false, true, false, false, false, false, false}, var (
{true, false, true, false, true, false, false, false, false}, isconn [MaxRooms][MaxRooms]bool
{false, true, false, false, false, true, false, false, false}, ingraph [MaxRooms]bool
{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
// starting with one room, connect it to a random adjacent room and // starting with one room, connect it to a random adjacent room and
// then pick a new room to start with. // then pick a new room to start with.
roomcount := 1 roomcount := 1
r1 := g.rnd(MaxRooms) r1 := g.rnd(MaxRooms)
ingraph[r1] = true ingraph[r1] = true
for { for {
// find a room to connect with // find a room to connect with
j := 0 j := 0
r2 := -1 r2 := -1
for i := 0; i < MaxRooms; i++ {
if rdesConn[r1][i] && !ingraph[i] { for i := range MaxRooms {
if g.data.rdesConn[r1][i] && !ingraph[i] {
if j++; g.rnd(j) == 0 { if j++; g.rnd(j) == 0 {
r2 = i r2 = i
} }
} }
} }
if j == 0 { if j == 0 {
// if no adjacent rooms are outside the graph, pick a new room // if no adjacent rooms are outside the graph, pick a new room
// to look from // to look from
@@ -48,12 +40,13 @@ func (g *RogueGame) doPassages() {
} else { } else {
// otherwise, connect new room to the graph, and draw a tunnel // otherwise, connect new room to the graph, and draw a tunnel
// to it // to it
ingraph[r2] = true ingraph[r2] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
g.conn(r1, r2) g.connectRooms(r1, r2)
isconn[r1][r2] = true isconn[r1][r2] = true
isconn[r2][r1] = true isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
roomcount++ roomcount++
} }
if roomcount >= MaxRooms { if roomcount >= MaxRooms {
break break
} }
@@ -66,8 +59,9 @@ func (g *RogueGame) doPassages() {
// find an adjacent room not already connected // find an adjacent room not already connected
j := 0 j := 0
r2 := -1 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] && !isconn[r1][i] {
if j++; g.rnd(j) == 0 { if j++; g.rnd(j) == 0 {
r2 = i r2 = i
} }
@@ -75,19 +69,23 @@ func (g *RogueGame) doPassages() {
} }
// if there is one, connect it and look for the next added passage // if there is one, connect it and look for the next added passage
if j != 0 { if j != 0 {
g.conn(r1, r2) g.connectRooms(r1, r2)
isconn[r1][r2] = true isconn[r1][r2] = true
isconn[r2][r1] = true isconn[r2][r1] = true //nolint:gosec // G602: rnd(MaxRooms) bounded
} }
} }
g.passnum()
g.numberPassages()
} }
// conn draws a corridor from a room in a certain direction (passages.c // connectRooms draws a corridor from a room in a certain direction
// conn). // (passages.c conn).
func (g *RogueGame) conn(r1, r2 int) { func (g *RogueGame) connectRooms(r1, r2 int) {
var rm int var (
var direc byte rm int
direc byte
)
if r1 < r2 { if r1 < r2 {
rm = r1 rm = r1
if r1+1 == r2 { if r1+1 == r2 {
@@ -103,42 +101,52 @@ func (g *RogueGame) conn(r1, r2 int) {
direc = 'd' direc = 'd'
} }
} }
rpf := &g.Level.Rooms[rm] rpf := &g.Level.Rooms[rm]
// Set up the movement variables, in two cases: first drawing one down. // Set up the movement variables, in two cases: first drawing one down.
var rpt *Room var (
var del, turnDelta, spos, epos Coord rpt *Room
var distance, turnDistance int del, turnDelta, spos, epos Coord
distance, turnDistance int
)
if direc == 'd' { if direc == 'd' {
rmt := rm + 3 // room # of dest rmt := rm + 3 // room # of dest
rpt = &g.Level.Rooms[rmt] // room pointer of dest rpt = &g.Level.Rooms[rmt] // room pointer of dest
del = Coord{X: 0, Y: 1} // direction of move del = Coord{X: 0, Y: 1} // direction of move
spos = rpf.Pos // start of move spos = rpf.Pos // start of move
epos = rpt.Pos // end of move epos = rpt.Pos // end of move
if !rpf.Flags.Has(Gone) { // if not gone pick door pos if !rpf.Flags.Has(Gone) { // if not gone pick door pos
for { for {
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1 spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1 spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !(rpf.Flags.Has(Maze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage)) { if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break break
} }
} }
} }
if !rpt.Flags.Has(Gone) { if !rpt.Flags.Has(Gone) {
for { for {
epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1 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)) { if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break break
} }
} }
} }
distance = abs(spos.Y-epos.Y) - 1 // distance to move distance = abs(spos.Y-epos.Y) - 1 // distance to move
turnDelta.Y = 0 // direction to turn
turnDelta.Y = 0 // direction to turn
if spos.X < epos.X { if spos.X < epos.X {
turnDelta.X = 1 turnDelta.X = 1
} else { } else {
turnDelta.X = -1 turnDelta.X = -1
} }
turnDistance = abs(spos.X - epos.X) // how far to turn turnDistance = abs(spos.X - epos.X) // how far to turn
} else { // setup for moving right } else { // setup for moving right
rmt := rm + 1 rmt := rm + 1
@@ -146,29 +154,34 @@ func (g *RogueGame) conn(r1, r2 int) {
del = Coord{X: 1, Y: 0} del = Coord{X: 1, Y: 0}
spos = rpf.Pos spos = rpf.Pos
epos = rpt.Pos epos = rpt.Pos
if !rpf.Flags.Has(Gone) { if !rpf.Flags.Has(Gone) {
for { for {
spos.X = rpf.Pos.X + rpf.Max.X - 1 spos.X = rpf.Pos.X + rpf.Max.X - 1
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 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)) { if !rpf.Flags.Has(Maze) || g.Level.FlagsAt(spos.Y, spos.X).Has(FPassage) {
break break
} }
} }
} }
if !rpt.Flags.Has(Gone) { if !rpt.Flags.Has(Gone) {
for { for {
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1 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)) { if !rpt.Flags.Has(Maze) || g.Level.FlagsAt(epos.Y, epos.X).Has(FPassage) {
break break
} }
} }
} }
distance = abs(spos.X-epos.X) - 1 distance = abs(spos.X-epos.X) - 1
if spos.Y < epos.Y { if spos.Y < epos.Y {
turnDelta.Y = 1 turnDelta.Y = 1
} else { } else {
turnDelta.Y = -1 turnDelta.Y = -1
} }
turnDelta.X = 0 turnDelta.X = 0
turnDistance = abs(spos.Y - epos.Y) turnDistance = abs(spos.Y - epos.Y)
} }
@@ -180,12 +193,13 @@ func (g *RogueGame) conn(r1, r2 int) {
if !rpf.Flags.Has(Gone) { if !rpf.Flags.Has(Gone) {
g.door(rpf, spos) g.door(rpf, spos)
} else { } else {
g.putpass(spos) g.putPassage(spos)
} }
if !rpt.Flags.Has(Gone) { if !rpt.Flags.Has(Gone) {
g.door(rpt, epos) g.door(rpt, epos)
} else { } else {
g.putpass(epos) g.putPassage(epos)
} }
// Get ready to move... // Get ready to move...
curr := spos curr := spos
@@ -196,27 +210,31 @@ func (g *RogueGame) conn(r1, r2 int) {
// Check if we are at the turn place, if so do the turn // Check if we are at the turn place, if so do the turn
if distance == turnSpot { if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- { for ; turnDistance > 0; turnDistance-- {
g.putpass(curr) g.putPassage(curr)
curr.X += turnDelta.X curr.X += turnDelta.X
curr.Y += turnDelta.Y curr.Y += turnDelta.Y
} }
} }
// Continue digging along // Continue digging along
g.putpass(curr) g.putPassage(curr)
distance-- distance--
} }
curr.X += del.X curr.X += del.X
curr.Y += del.Y curr.Y += del.Y
if curr != epos { if curr != epos {
g.msg("warning, connectivity problem on this level") g.msg("warning, connectivity problem on this level")
} }
} }
// putpass adds a passage character or secret passage here (passages.c // putPassage adds a passage character or secret passage here (passages.c
// putpass). // putpass).
func (g *RogueGame) putpass(cp Coord) { func (g *RogueGame) putPassage(cp Coord) {
pp := g.Level.At(cp.Y, cp.X) pp := g.Level.At(cp.Y, cp.X)
pp.Flags.Set(FPassage) pp.Flags.Set(FPassage)
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 { if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
pp.Flags.Clear(FReal) pp.Flags.Clear(FReal)
} else { } else {
@@ -240,6 +258,7 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
} else { } else {
pp.Ch = '|' pp.Ch = '|'
} }
pp.Flags.Clear(FReal) pp.Flags.Clear(FReal)
} else { } else {
pp.Ch = Door pp.Ch = Door
@@ -250,7 +269,7 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
// (passages.c add_pass). // (passages.c add_pass).
func (g *RogueGame) addPass() { func (g *RogueGame) addPass() {
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ { for x := range NumCols {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
if pp.Flags.Has(FPassage) || pp.Ch == Door || if pp.Flags.Has(FPassage) || pp.Ch == Door ||
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) { (!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) {
@@ -258,19 +277,24 @@ func (g *RogueGame) addPass() {
if pp.Flags.Has(FPassage) { if pp.Flags.Has(FPassage) {
ch = Passage ch = Passage
} }
pp.Flags.Set(FSeen) pp.Flags.Set(FSeen)
g.move(y, x) g.move(y, x)
if pp.Monst != nil {
switch {
case pp.Monst != nil:
pp.Monst.OldCh = pp.Ch pp.Monst.OldCh = pp.Ch
} else if pp.Flags.Has(FReal) { case pp.Flags.Has(FReal):
g.addch(ch) g.addch(ch)
} else { default:
g.standout() g.standout()
if pp.Flags.Has(FPassage) { if pp.Flags.Has(FPassage) {
g.addch(Passage) g.addch(Passage)
} else { } else {
g.addch(Door) g.addch(Door)
} }
g.standend() g.standend()
} }
} }
@@ -278,32 +302,36 @@ func (g *RogueGame) addPass() {
} }
} }
// passnum assigns a number to each passageway (passages.c passnum). // numberPassages assigns a number to each passageway (passages.c passnum).
func (g *RogueGame) passnum() { func (g *RogueGame) numberPassages() {
g.pnum = 0 g.pnum = 0
g.newpnum = false g.newpnum = false
for i := range g.Level.Passages { for i := range g.Level.Passages {
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0] g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
} }
for i := range g.Level.Rooms { for i := range g.Level.Rooms {
rp := &g.Level.Rooms[i] rp := &g.Level.Rooms[i]
for j := range rp.Exits { for j := range rp.Exits {
g.newpnum = true g.newpnum = true
g.numpass(rp.Exits[j].Y, rp.Exits[j].X) g.numberPassage(rp.Exits[j].Y, rp.Exits[j].X)
} }
} }
} }
// numpass numbers a passageway square and its brethren (passages.c // numberPassage numbers a passageway square and its brethren (passages.c
// numpass). // numpass).
func (g *RogueGame) numpass(y, x int) { func (g *RogueGame) numberPassage(y, x int) {
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 { if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
return return
} }
fp := g.Level.FlagsAt(y, x) fp := g.Level.FlagsAt(y, x)
if fp.Has(FPassNum) { if fp.Has(FPassNum) {
return return
} }
if g.newpnum { if g.newpnum {
g.pnum++ g.pnum++
g.newpnum = false g.newpnum = false
@@ -317,12 +345,13 @@ func (g *RogueGame) numpass(y, x int) {
} else if !fp.Has(FPassage) { } else if !fp.Has(FPassage) {
return return
} }
*fp |= PlaceFlags(g.pnum)
*fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13
// recurse on the surrounding places // recurse on the surrounding places
g.numpass(y+1, x) g.numberPassage(y+1, x)
g.numpass(y-1, x) g.numberPassage(y-1, x)
g.numpass(y, x+1) g.numberPassage(y, x+1)
g.numpass(y, x-1) g.numberPassage(y, x-1)
} }
// abs is C abs() for ints. // abs is C abs() for ints.
@@ -330,5 +359,6 @@ func abs(n int) int {
if n < 0 { if n < 0 {
return -n return -n
} }
return n return n
} }

View File

@@ -13,56 +13,43 @@ type pact struct {
straight string straight string
} }
// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic
// (it names the fruit) and is computed in doPot.
var pActions = [NumPotionTypes]pact{
PotionConfusion: {Confused, DUnconfuse, HuhDuration,
"what a tripy feeling!",
"wait, what's going on here. Huh? What? Who?"},
PotionLSD: {Hallucinating, DComeDown, SeeDuration,
"Oh, wow! Everything seems so cosmic!",
"Oh, wow! Everything seems so cosmic!"},
PotionSeeInvisible: {CanSeeInvisible, DUnsee, SeeDuration, "", ""},
PotionBlindness: {Blind, DSight, SeeDuration,
"oh, bummer! Everything is dark! Help!",
"a cloak of darkness falls around you"},
PotionLevitation: {Levitating, DLand, HealTime,
"oh, wow! You're floating in the air!",
"you start to float in the air"},
}
// quaff drinks a potion from the pack (potions.c quaff). // quaff drinks a potion from the pack (potions.c quaff).
func (g *RogueGame) quaff() { func (g *RogueGame) quaff() {
p := &g.Player p := &g.Player
obj := g.getItem("quaff", KindPotion) obj, ok := g.promptPackItem("quaff", KindPotion)
// Make certain that it is something that we want to drink // Make certain that it is something that we want to drink
if obj == nil { if !ok {
return return
} }
if obj.Kind != KindPotion { if obj.Kind != KindPotion {
if !g.Options.Terse { if !g.Options.Terse {
g.msg("yuk! Why would you want to drink that?") g.msg("yuk! Why would you want to drink that?")
} else { } else {
g.msg("that's undrinkable") g.msg("that's undrinkable")
} }
return return
} }
if obj == p.CurWeapon { if obj == p.CurWeapon {
p.CurWeapon = nil p.CurWeapon = nil
} }
// Calculate the effect it has on the poor guy. // Calculate the effect it has on the poor guy.
trip := p.On(Hallucinating) trip := p.On(Hallucinating)
g.leavePack(obj, false, false) g.leavePack(obj, false, false)
switch obj.PotionKind() { switch obj.PotionKind() {
case PotionConfusion: case PotionConfusion:
g.doPot(PotionConfusion, !trip) g.applyPotionFuse(PotionConfusion, !trip)
case PotionPoison: case PotionPoison:
g.Items.Potions[PotionPoison].Know = true g.Items.Potions[PotionPoison].Know = true
if p.IsWearing(RingSustainStrength) { if p.IsWearing(RingSustainStrength) {
g.msg("you feel momentarily sick") g.msg("you feel momentarily sick")
} else { } else {
g.chgStr(-(g.rnd(3) + 1)) g.changeStrength(-(g.rnd(3) + 1))
g.msg("you feel very sick now") g.msg("you feel very sick now")
g.comeDown(0) g.comeDown(0)
} }
@@ -72,15 +59,17 @@ func (g *RogueGame) quaff() {
p.Stats.MaxHP++ p.Stats.MaxHP++
p.Stats.HP = p.Stats.MaxHP p.Stats.HP = p.Stats.MaxHP
} }
g.sight(0) g.sight(0)
g.msg("you begin to feel better") g.msg("you begin to feel better")
case PotionGainStrength: case PotionGainStrength:
g.Items.Potions[PotionGainStrength].Know = true g.Items.Potions[PotionGainStrength].Know = true
g.chgStr(1) g.changeStrength(1)
g.msg("you feel stronger, now. What bulging muscles!") g.msg("you feel stronger, now. What bulging muscles!")
case PotionDetectMonsters: case PotionDetectMonsters:
p.Flags.Set(SenseMonsters) p.Flags.Set(SenseMonsters)
g.Fuse(DTurnSee, 1, HuhDuration, After) g.Fuse(DTurnSee, 1, HuhDuration, After)
if !g.turnSee(false) { if !g.turnSee(false) {
g.msg("you have a %s feeling for a moment, then it passes", g.msg("you have a %s feeling for a moment, then it passes",
g.chooseStr("normal", "strange")) g.chooseStr("normal", "strange"))
@@ -88,24 +77,30 @@ func (g *RogueGame) quaff() {
case PotionDetectMagic: case PotionDetectMagic:
// Potion of magic detection. Show the potions and scrolls // Potion of magic detection. Show the potions and scrolls
show := false show := false
if len(g.Level.Objects) > 0 { if len(g.Level.Objects) > 0 {
g.scr.Hw.Clear() g.scr.Hw.Clear()
for _, tp := range g.Level.Objects { for _, tp := range g.Level.Objects {
if tp.isMagic() { if g.isMagic(tp) {
show = true show = true
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic) g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
g.Items.Potions[PotionDetectMagic].Know = true g.Items.Potions[PotionDetectMagic].Know = true
} }
} }
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
for _, tp := range mp.Pack { for _, tp := range mp.Pack {
if tp.isMagic() { if g.isMagic(tp) {
show = true show = true
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic) g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
} }
} }
} }
} }
if show { if show {
g.Items.Potions[PotionDetectMagic].Know = true g.Items.Potions[PotionDetectMagic].Know = true
g.showWin("You sense the presence of magic on this level.--More--") g.showWin("You sense the presence of magic on this level.--More--")
@@ -118,16 +113,21 @@ func (g *RogueGame) quaff() {
if p.On(SenseMonsters) { if p.On(SenseMonsters) {
g.turnSee(false) g.turnSee(false)
} }
g.StartDaemon(DVisuals, 0, Before) g.StartDaemon(DVisuals, 0, Before)
g.SeenStairs = g.seenStairs() g.SeenStairs = g.seenStairs()
} }
g.doPot(PotionLSD, true)
g.applyPotionFuse(PotionLSD, true)
case PotionSeeInvisible: case PotionSeeInvisible:
show := p.On(CanSeeInvisible) show := p.On(CanSeeInvisible)
g.doPot(PotionSeeInvisible, false)
g.applyPotionFuse(PotionSeeInvisible, false)
if !show { if !show {
g.invisOn() g.invisOn()
} }
g.sight(0) g.sight(0)
case PotionRaiseLevel: case PotionRaiseLevel:
g.Items.Potions[PotionRaiseLevel].Know = true g.Items.Potions[PotionRaiseLevel].Know = true
@@ -139,14 +139,17 @@ func (g *RogueGame) quaff() {
if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 { if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 {
p.Stats.MaxHP++ p.Stats.MaxHP++
} }
p.Stats.MaxHP++ p.Stats.MaxHP++
p.Stats.HP = p.Stats.MaxHP p.Stats.HP = p.Stats.MaxHP
} }
g.sight(0) g.sight(0)
g.comeDown(0) g.comeDown(0)
g.msg("you begin to feel much better") g.msg("you begin to feel much better")
case PotionHaste: case PotionHaste:
g.Items.Potions[PotionHaste].Know = true g.Items.Potions[PotionHaste].Know = true
g.After = false g.After = false
if g.addHaste(true) { if g.addHaste(true) {
g.msg("you feel yourself moving much faster") g.msg("you feel yourself moving much faster")
@@ -155,24 +158,30 @@ func (g *RogueGame) quaff() {
if p.IsRing(Left, RingAddStrength) { if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus) addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
} }
if p.IsRing(Right, RingAddStrength) { if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus) addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
} }
if p.Stats.Str < p.MaxStats.Str { if p.Stats.Str < p.MaxStats.Str {
p.Stats.Str = p.MaxStats.Str p.Stats.Str = p.MaxStats.Str
} }
if p.IsRing(Left, RingAddStrength) { if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Left].Bonus) addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
} }
if p.IsRing(Right, RingAddStrength) { if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Right].Bonus) addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
} }
g.msg("hey, this tastes great. It make you feel warm all over") g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness: case PotionBlindness:
g.doPot(PotionBlindness, true) g.applyPotionFuse(PotionBlindness, true)
case PotionLevitation: case PotionLevitation:
g.doPot(PotionLevitation, true) g.applyPotionFuse(PotionLevitation, true)
} }
g.status() g.status()
// Throw the item away // Throw the item away
g.callIt(&g.Items.Potions[obj.Which]) g.callIt(&g.Items.Potions[obj.Which])
@@ -181,17 +190,18 @@ func (g *RogueGame) quaff() {
// raiseLevel: the guy just magically went up a level (potions.c // raiseLevel: the guy just magically went up a level (potions.c
// raise_level). // raise_level).
func (g *RogueGame) raiseLevel() { func (g *RogueGame) raiseLevel() {
g.Player.Stats.Exp = eLevels[g.Player.Stats.Lvl-1] + 1 g.Player.Stats.Exp = g.data.eLevels[g.Player.Stats.Lvl-1] + 1
g.checkLevel() g.checkLevel()
} }
// doPot does a potion with standard setup: it uses a fuse and turns on a // applyPotionFuse does a potion with standard setup: it uses a fuse and
// flag (potions.c do_pot). // turns on a flag (potions.c do_pot).
func (g *RogueGame) doPot(kind PotionKind, knowit bool) { func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
pp := &pActions[kind] pp := &g.data.pActions[kind]
if !g.Items.Potions[kind].Know { if !g.Items.Potions[kind].Know {
g.Items.Potions[kind].Know = knowit g.Items.Potions[kind].Know = knowit
} }
t := g.spread(pp.time) t := g.spread(pp.time)
if !g.Player.On(pp.flags) { if !g.Player.On(pp.flags) {
g.Player.Flags.Set(pp.flags) g.Player.Flags.Set(pp.flags)
@@ -200,24 +210,28 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
} else { } else {
g.Lengthen(pp.daemon, t) g.Lengthen(pp.daemon, t)
} }
high, straight := pp.high, pp.straight high, straight := pp.high, pp.straight
if kind == PotionSeeInvisible { if kind == PotionSeeInvisible {
s := fmt.Sprintf("this potion tastes like %s juice", g.Fruit) s := fmt.Sprintf("this potion tastes like %s juice", g.Fruit)
high, straight = s, s high, straight = s, s
} }
g.msg("%s", g.chooseStr(high, straight)) g.msg("%s", g.chooseStr(high, straight))
} }
// isMagic reports whether an object radiates magic (potions.c is_magic). // isMagic reports whether an object radiates magic (potions.c is_magic).
func (o *Object) isMagic() bool { func (g *RogueGame) isMagic(o *Object) bool {
switch o.Kind { switch o.Kind {
case KindArmor: case KindArmor:
return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which] return o.Flags.Has(Protected) || o.ArmorClass != g.data.aClass[o.Which]
case KindWeapon: case KindWeapon:
return o.HPlus != 0 || o.DPlus != 0 return o.HPlus != 0 || o.DPlus != 0
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet: case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:
return true return true
} }
return false return false
} }
@@ -225,6 +239,7 @@ func (o *Object) isMagic() bool {
// invis_on). // invis_on).
func (g *RogueGame) invisOn() { func (g *RogueGame) invisOn() {
g.Player.Flags.Set(CanSeeInvisible) g.Player.Flags.Set(CanSeeInvisible)
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
if mp.On(Invisible) && g.seeMonst(mp) && !g.Player.On(Hallucinating) { if mp.On(Invisible) && g.seeMonst(mp) && !g.Player.On(Hallucinating) {
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise) g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
@@ -236,8 +251,10 @@ func (g *RogueGame) invisOn() {
// turn_see). // turn_see).
func (g *RogueGame) turnSee(turnOff bool) bool { func (g *RogueGame) turnSee(turnOff bool) bool {
addNew := false addNew := false
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
g.move(mp.Pos.Y, mp.Pos.X) g.move(mp.Pos.Y, mp.Pos.X)
canSee := g.seeMonst(mp) canSee := g.seeMonst(mp)
if turnOff { if turnOff {
if !canSee { if !canSee {
@@ -247,22 +264,27 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
if !canSee { if !canSee {
g.standout() g.standout()
} }
if !g.Player.On(Hallucinating) { if !g.Player.On(Hallucinating) {
g.addch(mp.Type) g.addch(mp.Type)
} else { } else {
g.addch(byte(g.rnd(26) + 'A')) g.addch(g.randomMonsterLetter())
} }
if !canSee { if !canSee {
g.standend() g.standend()
addNew = true addNew = true
} }
} }
} }
if turnOff { if turnOff {
g.Player.Flags.Clear(SenseMonsters) g.Player.Flags.Clear(SenseMonsters)
} else { } else {
g.Player.Flags.Set(SenseMonsters) g.Player.Flags.Set(SenseMonsters)
} }
return addNew return addNew
} }
@@ -271,9 +293,11 @@ func (g *RogueGame) turnSee(turnOff bool) bool {
func (g *RogueGame) seenStairs() bool { func (g *RogueGame) seenStairs() bool {
st := g.Level.Stairs st := g.Level.Stairs
g.move(st.Y, st.X) g.move(st.Y, st.X)
if g.inch() == Stairs { // it's on the map if g.inch() == Stairs { // it's on the map
return true return true
} }
if g.Player.Pos == st { // it's under him if g.Player.Pos == st { // it's under him
return true return true
} }
@@ -282,9 +306,11 @@ func (g *RogueGame) seenStairs() bool {
if g.seeMonst(tp) && tp.On(Awake) { // visible and awake: if g.seeMonst(tp) && tp.On(Awake) { // visible and awake:
return true // it must have moved there return true // it must have moved there
} }
if g.Player.On(SenseMonsters) && tp.OldCh == Stairs { if g.Player.On(SenseMonsters) && tp.OldCh == Stairs {
return true return true
} }
} }
return false return false
} }

View File

@@ -7,17 +7,19 @@ import "fmt"
// ringOn puts a ring on a hand (rings.c ring_on). // ringOn puts a ring on a hand (rings.c ring_on).
func (g *RogueGame) ringOn() { func (g *RogueGame) ringOn() {
p := &g.Player p := &g.Player
obj := g.getItem("put on", KindRing) obj, ok := g.promptPackItem("put on", KindRing)
// Make certain that it is something that we want to wear // Make certain that it is something that we want to wear
if obj == nil { if !ok {
return return
} }
if obj.Kind != KindRing { if obj.Kind != KindRing {
if !g.Options.Terse { if !g.Options.Terse {
g.msg("it would be difficult to wrap that around a finger") g.msg("it would be difficult to wrap that around a finger")
} else { } else {
g.msg("not a ring") g.msg("not a ring")
} }
return return
} }
@@ -27,6 +29,7 @@ func (g *RogueGame) ringOn() {
} }
var ring int var ring int
switch { switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil: case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if ring = g.gethand(); ring < 0 { if ring = g.gethand(); ring < 0 {
@@ -42,14 +45,16 @@ func (g *RogueGame) ringOn() {
} else { } else {
g.msg("wearing two") g.msg("wearing two")
} }
return return
} }
p.CurRing[ring] = obj p.CurRing[ring] = obj
// Calculate the effect it has on the poor guy. // Calculate the effect it has on the poor guy.
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(obj.Bonus) g.changeStrength(obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.invisOn() g.invisOn()
case RingAggravateMonsters: case RingAggravateMonsters:
@@ -57,15 +62,18 @@ func (g *RogueGame) ringOn() {
} }
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("you are now wearing ") g.addmsgf("you are now wearing ")
} }
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh)
g.msg("%s (%c)", g.inventoryName(obj, true), obj.PackCh)
} }
// ringOff takes off a ring (rings.c ring_off). // ringOff takes off a ring (rings.c ring_off).
func (g *RogueGame) ringOff() { func (g *RogueGame) ringOff() {
p := &g.Player p := &g.Player
var ring int var ring int
switch { switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil: case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if g.Options.Terse { if g.Options.Terse {
@@ -73,6 +81,7 @@ func (g *RogueGame) ringOff() {
} else { } else {
g.msg("you aren't wearing any rings") g.msg("you aren't wearing any rings")
} }
return return
case p.CurRing[Left] == nil: case p.CurRing[Left] == nil:
ring = Right ring = Right
@@ -83,14 +92,18 @@ func (g *RogueGame) ringOff() {
return return
} }
} }
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
obj := p.CurRing[ring] obj := p.CurRing[ring]
if obj == nil { if obj == nil {
g.msg("not wearing such a ring") g.msg("not wearing such a ring")
return return
} }
if g.dropCheck(obj) { if g.dropCheck(obj) {
g.msg("was wearing %s(%c)", g.invName(obj, true), obj.PackCh) g.msg("was wearing %s(%c)", g.inventoryName(obj, true), obj.PackCh)
} }
} }
@@ -102,17 +115,22 @@ func (g *RogueGame) gethand() int {
} else { } else {
g.msg("left hand or right hand? ") g.msg("left hand or right hand? ")
} }
c := g.readchar() c := g.readchar()
if c == Escape { if c == Escape {
return -1 return -1
} }
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if c == 'l' || c == 'L' { if c == 'l' || c == 'L' {
return Left return Left
} }
if c == 'r' || c == 'R' { if c == 'r' || c == 'R' {
return Right return Right
} }
if g.Options.Terse { if g.Options.Terse {
g.msg("L or R") g.msg("L or R")
} else { } else {
@@ -121,25 +139,6 @@ func (g *RogueGame) gethand() int {
} }
} }
// ringUses is the rings.c ring_eat static uses[] table: how much food each
// ring type uses up per turn (negative = a 1-in-n chance of 1).
var ringUses = [NumRingTypes]int{
1, // R_PROTECT
1, // R_ADDSTR
1, // R_SUSTSTR
-3, // R_SEARCH
-5, // R_SEEINVIS
0, // R_NOP
0, // R_AGGR
-3, // R_ADDHIT
-3, // R_ADDDAM
2, // R_REGEN
-2, // R_DIGEST
0, // R_TELEPORT
1, // R_STEALTH
1, // R_SUSTARM
}
// ringEat reports how much food the ring on the given hand uses up // ringEat reports how much food the ring on the given hand uses up
// (rings.c ring_eat). // (rings.c ring_eat).
func (g *RogueGame) ringEat(hand int) int { func (g *RogueGame) ringEat(hand int) int {
@@ -147,7 +146,8 @@ func (g *RogueGame) ringEat(hand int) int {
if ring == nil { if ring == nil {
return 0 return 0
} }
eat := ringUses[ring.RingKind()]
eat := g.data.ringUses[ring.RingKind()]
if eat < 0 { if eat < 0 {
if g.rnd(-eat) == 0 { if g.rnd(-eat) == 0 {
eat = 1 eat = 1
@@ -155,20 +155,25 @@ func (g *RogueGame) ringEat(hand int) int {
eat = 0 eat = 0
} }
} }
if ring.RingKind() == RingSlowDigestion { if ring.RingKind() == RingSlowDigestion {
eat = -eat eat = -eat
} }
return eat return eat
} }
// ringNum prints ring bonuses (rings.c ring_num). // ringNum prints ring bonuses (rings.c ring_num). The unused game
func ringNum(g *RogueGame, obj *Object) string { // parameter keeps the nameit prfunc signature.
func ringNum(_ *RogueGame, obj *Object) string {
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
return "" return ""
} }
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity: case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring)) return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
} }
return "" return ""
} }

View File

@@ -11,67 +11,65 @@ import (
// that Run recovers, so the terminal is restored by normal unwinding. // that Run recovers, so the terminal is restored by normal unwinding.
// gameEnd is the sentinel carried by the panic that replaces my_exit(). // gameEnd is the sentinel carried by the panic that replaces my_exit().
type gameEnd struct{ status int } type gameEnd struct{}
// myExit leaves the process properly (main.c my_exit): it unwinds to Run. // myExit leaves the process properly (main.c my_exit): it unwinds to Run.
func (g *RogueGame) myExit(st int) { // Every C caller exited with status 0; abnormal exits panic for real.
func (g *RogueGame) myExit() {
g.Playing = false g.Playing = false
panic(gameEnd{status: st})
}
var ripArt = []string{ panic(gameEnd{})
" __________",
" / \\",
" / REST \\",
" / IN \\",
" / PEACE \\",
" / \\",
" | |",
" | |",
" | killed by a |",
" | |",
" | 1980 |",
" *| * * * | *",
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
} }
// death does something really fun when he dies (rip.c death). // death does something really fun when he dies (rip.c death).
func (g *RogueGame) death(monst byte) { func (g *RogueGame) death(monst byte) {
p := &g.Player p := &g.Player
p.Purse -= p.Purse / 10 p.Purse -= p.Purse / 10
g.clear() g.clear()
killer := g.killname(monst, false) killer := g.killname(monst, false)
if !g.Options.Tombstone { 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' { if monst != 's' && monst != 'h' {
g.printw("a%s ", vowelstr(killer)) g.printw("a%s ", vowelstr(killer))
} }
g.printw("%s with %d gold", killer, p.Purse) g.printw("%s with %d gold", killer, p.Purse)
} else { } else {
year := time.Now().Year() year := time.Now().Year()
for i, line := range ripArt {
for i, line := range g.data.ripArt {
g.scr.Std.MvAddStr(8+i, 0, line) g.scr.Std.MvAddStr(8+i, 0, line)
} }
g.mvaddstr(17, center(killer), killer) g.mvaddstr(17, center(killer), killer)
if monst == 's' || monst == 'h' { if monst == 's' || monst == 'h' {
g.mvaddch(16, 32, ' ') g.mvaddch(16, 32, ' ')
} else { } else {
g.mvaddstr(16, 33, vowelstr(killer)) g.mvaddstr(16, 33, vowelstr(killer))
} }
g.mvaddstr(14, center(g.Whoami), g.Whoami) g.mvaddstr(14, center(g.Whoami), g.Whoami)
au := fmt.Sprintf("%d Au", p.Purse) au := fmt.Sprintf("%d Au", p.Purse)
g.mvaddstr(15, center(au), au) g.mvaddstr(15, center(au), au)
g.mvaddstr(18, 26, fmt.Sprintf("%4d", year)) g.mvaddstr(18, 26, fmt.Sprintf("%4d", year))
} }
g.mvaddstr(NumLines-1, 0, "[Press return to continue]") g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
g.refresh() g.refresh()
flags := 0 flags := 0
if g.HasAmulet { if g.HasAmulet {
flags = 3 flags = 3
} }
g.score(p.Purse, flags, monst) g.score(p.Purse, flags, monst)
g.waitFor('\n') g.waitFor('\n')
g.myExit(0) g.myExit()
} }
// center returns the column to center the given string on the tombstone // center returns the column to center the given string on the tombstone
@@ -85,6 +83,7 @@ func (g *RogueGame) totalWinner() {
p := &g.Player p := &g.Player
g.clear() g.clear()
g.standout() g.standout()
banner := []string{ banner := []string{
" ", " ",
" @ @ @ @ @ @@@ @ @ ", " @ @ @ @ @ @@@ @ @ ",
@@ -100,6 +99,7 @@ func (g *RogueGame) totalWinner() {
for i, line := range banner { for i, line := range banner {
g.scr.Std.MvAddStr(i, 0, line) g.scr.Std.MvAddStr(i, 0, line)
} }
g.standend() g.standend()
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the") 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") g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at")
@@ -110,11 +110,14 @@ func (g *RogueGame) totalWinner() {
g.clear() g.clear()
g.mvaddstr(0, 0, " Worth Item") g.mvaddstr(0, 0, " Worth Item")
g.move(1, 0) g.move(1, 0)
oldpurse := p.Purse oldpurse := p.Purse
line := 1 line := 1
for _, obj := range p.Pack { for _, obj := range p.Pack {
worth := 0 worth := 0
it := &g.Items it := &g.Items
switch obj.Kind { switch obj.Kind {
case KindFood: case KindFood:
worth = 2 * obj.Count worth = 2 * obj.Count
@@ -125,25 +128,30 @@ func (g *RogueGame) totalWinner() {
case KindArmor: case KindArmor:
worth = it.Armors[obj.Which].Worth worth = it.Armors[obj.Which].Worth
worth += (9 - obj.ArmorClass) * 100 worth += (9 - obj.ArmorClass) * 100
worth += 10 * (aClass[obj.Which] - obj.ArmorClass) worth += 10 * (g.data.aClass[obj.Which] - obj.ArmorClass)
obj.Flags.Set(Known) obj.Flags.Set(Known)
case KindScroll: case KindScroll:
op := &it.Scrolls[obj.Which] op := &it.Scrolls[obj.Which]
worth = op.Worth * obj.Count worth = op.Worth * obj.Count
if !op.Know { if !op.Know {
worth /= 2 worth /= 2
} }
op.Know = true op.Know = true
case KindPotion: case KindPotion:
op := &it.Potions[obj.Which] op := &it.Potions[obj.Which]
worth = op.Worth * obj.Count worth = op.Worth * obj.Count
if !op.Know { if !op.Know {
worth /= 2 worth /= 2
} }
op.Know = true op.Know = true
case KindRing: case KindRing:
op := &it.Rings[obj.Which] op := &it.Rings[obj.Which]
worth = op.Worth worth = op.Worth
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage || if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity { obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
if obj.Bonus > 0 { if obj.Bonus > 0 {
@@ -152,67 +160,74 @@ func (g *RogueGame) totalWinner() {
worth = 10 worth = 10
} }
} }
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
worth /= 2 worth /= 2
} }
obj.Flags.Set(Known) obj.Flags.Set(Known)
op.Know = true op.Know = true
case KindWand: case KindWand:
op := &it.Sticks[obj.Which] op := &it.Sticks[obj.Which]
worth = op.Worth worth = op.Worth
worth += 20 * obj.Charges worth += 20 * obj.Charges
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
worth /= 2 worth /= 2
} }
obj.Flags.Set(Known) obj.Flags.Set(Known)
op.Know = true op.Know = true
case KindAmulet: case KindAmulet:
worth = 1000 worth = 1000
} }
if worth < 0 { if worth < 0 {
worth = 0 worth = 0
} }
g.scr.Std.MvPrintw(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.invName(obj, false)) g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.inventoryName(obj, false))
line++ line++
p.Purse += worth p.Purse += worth
} }
g.scr.Std.MvPrintw(line, 0, " %5d Gold Pieces ", oldpurse)
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh() g.refresh()
g.score(p.Purse, 2, ' ') g.score(p.Purse, 2, ' ')
g.myExit(0) g.myExit()
}
// killnameTable is the rip.c nlist[]: special death causes.
var killnameTable = []helpEntry{
{'a', "arrow", true},
{'b', "bolt", true},
{'d', "dart", true},
{'h', "hypothermia", false},
{'s', "starvation", false},
} }
// killname converts a code to a monster name (rip.c killname). // killname converts a code to a monster name (rip.c killname).
func (g *RogueGame) killname(monst byte, doart bool) string { func (g *RogueGame) killname(monst byte, doart bool) string {
var sp string var (
var article bool sp string
article bool
)
if isUpper(monst) { if isUpper(monst) {
sp = g.Monsters[monst-'A'].Name sp = g.Monsters[monst-'A'].Name
article = true article = true
} else { } else {
sp = "Wally the Wonder Badger" sp = "Wally the Wonder Badger"
article = false article = false
for _, hp := range killnameTable {
for _, hp := range g.data.killnameTable {
if hp.Ch == monst { if hp.Ch == monst {
sp = hp.Desc sp = hp.Desc
article = hp.Print article = hp.Print
break break
} }
} }
} }
if doart && article { if doart && article {
return "a" + vowelstr(sp) + " " + sp return "a" + vowelstr(sp) + " " + sp
} }
return sp return sp
} }
@@ -224,13 +239,16 @@ func (g *RogueGame) DeathDemo() {
if _, ok := r.(gameEnd); ok { if _, ok := r.(gameEnd); ok {
return return
} }
panic(r) panic(r)
} }
}() }()
dnum := g.rnd(100) dnum := g.rnd(100)
for dnum--; dnum > 0; dnum-- { for dnum--; dnum > 0; dnum-- {
g.rnd(100) g.rnd(100)
} }
g.Player.Purse = g.rnd(100) + 1 g.Player.Purse = g.rnd(100) + 1
g.Depth = g.rnd(100) + 1 g.Depth = g.rnd(100) + 1
g.death(g.deathMonst()) g.death(g.deathMonst())
@@ -245,5 +263,6 @@ func (g *RogueGame) deathMonst() byte {
'Y', 'Z', 'a', 'b', 'h', 'd', 's', 'Y', 'Z', 'a', 'b', 'h', 'd', 's',
' ', // generates the "Wally the Wonder Badger" message ' ', // generates the "Wally the Wonder Badger" message
} }
return poss[g.rnd(len(poss))] return poss[g.rnd(len(poss))]
} }

View File

@@ -11,21 +11,17 @@ type Rng struct {
Seed int32 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). // Rnd picks a very random number in [0, rng) (main.c rnd).
func (r *Rng) Rnd(rng int) int { func (r *Rng) Rnd(rng int) int {
if rng == 0 { if rng == 0 {
return 0 return 0
} }
v := r.next() v := r.next()
if v < 0 { if v < 0 {
v = -v v = -v
} }
return v % rng return v % rng
} }
@@ -35,9 +31,17 @@ func (r *Rng) Roll(number, sides int) int {
for ; number > 0; number-- { for ; number > 0; number-- {
dtotal += r.Rnd(sides) + 1 dtotal += r.Rnd(sides) + 1
} }
return dtotal 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 // 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. // sources reads rnd(x), and keeping that shape makes cross-checking easy.
func (g *RogueGame) rnd(rng int) int { return g.Rng.Rnd(rng) } 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 { if got := r.Rnd(0); got != 0 {
t.Fatalf("rnd(0) = %d, want 0", got) t.Fatalf("rnd(0) = %d, want 0", got)
} }
if r.Seed != 42 { if r.Seed != 42 {
t.Fatalf("rnd(0) stepped the generator: seed = %d, want 42", r.Seed) 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 { if got := g.spread(1); got != 1 {
t.Errorf("spread(1) = %d, want 1", got) t.Errorf("spread(1) = %d, want 1", got)
} }
if got := g.spread(2); got != 2 { if got := g.spread(2); got != 2 {
t.Errorf("spread(2) = %d, want 2", got) t.Errorf("spread(2) = %d, want 2", got)
} }

View File

@@ -17,10 +17,11 @@ type mazeState struct {
const goldGrp = 1 const goldGrp = 1
// doRooms creates rooms and corridors with a connectivity graph (rooms.c // digRooms creates rooms and corridors with a connectivity graph (rooms.c
// do_rooms). // do_rooms).
func (g *RogueGame) doRooms() { func (g *RogueGame) digRooms() {
var bsze Coord // maximum room size var bsze Coord // maximum room size
bsze.X = NumCols / 3 bsze.X = NumCols / 3
bsze.Y = NumLines / 3 bsze.Y = NumLines / 3
// Clear things for a new level // Clear things for a new level
@@ -32,14 +33,15 @@ func (g *RogueGame) doRooms() {
} }
// Put the gone rooms, if any, on the level // Put the gone rooms, if any, on the level
leftOut := g.rnd(4) leftOut := g.rnd(4)
for i := 0; i < leftOut; i++ { for range leftOut {
g.Level.Rooms[g.rndRoom()].Flags.Set(Gone) g.Level.Rooms[g.randomRoom()].Flags.Set(Gone)
} }
// dig and populate all the rooms on the level // dig and populate all the rooms on the level
for i := range g.Level.Rooms { for i := range g.Level.Rooms {
rp := &g.Level.Rooms[i] rp := &g.Level.Rooms[i]
// Find upper left corner of box that this room goes in // Find upper left corner of box that this room goes in
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y} top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
if rp.Flags.Has(Gone) { if rp.Flags.Has(Gone) {
// Place a gone room. Make certain that there is a blank line // Place a gone room. Make certain that there is a blank line
// for passage drawing. // for passage drawing.
@@ -47,16 +49,19 @@ func (g *RogueGame) doRooms() {
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1 rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1 rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
rp.Max.X = -NumCols rp.Max.X = -NumCols
rp.Max.Y = -NumLines rp.Max.Y = -NumLines
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 { if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
break break
} }
} }
continue continue
} }
// set room type // set room type
if g.rnd(10) < g.Depth-1 { if g.rnd(10) < g.Depth-1 {
rp.Flags.Set(Dark) // dark room rp.Flags.Set(Dark) // dark room
if g.rnd(15) == 0 { if g.rnd(15) == 0 {
rp.Flags = Maze // maze room rp.Flags = Maze // maze room
} }
@@ -64,10 +69,12 @@ func (g *RogueGame) doRooms() {
// Find a place and size for a random room // Find a place and size for a random room
if rp.Flags.Has(Maze) { if rp.Flags.Has(Maze) {
rp.Max.X = bsze.X - 1 rp.Max.X = bsze.X - 1
rp.Max.Y = bsze.Y - 1 rp.Max.Y = bsze.Y - 1
if rp.Pos.X = top.X; rp.Pos.X == 1 { if rp.Pos.X = top.X; rp.Pos.X == 1 {
rp.Pos.X = 0 rp.Pos.X = 0
} }
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 { if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
rp.Pos.Y++ rp.Pos.Y++
rp.Max.Y-- rp.Max.Y--
@@ -77,12 +84,14 @@ func (g *RogueGame) doRooms() {
rp.Max.X = g.rnd(bsze.X-4) + 4 rp.Max.X = g.rnd(bsze.X-4) + 4
rp.Max.Y = g.rnd(bsze.Y-4) + 4 rp.Max.Y = g.rnd(bsze.Y-4) + 4
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X) rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y) rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
if rp.Pos.Y != 0 { if rp.Pos.Y != 0 {
break break
} }
} }
} }
g.drawRoom(rp) g.drawRoom(rp)
// Put the gold in // Put the gold in
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) { if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
@@ -92,16 +101,18 @@ func (g *RogueGame) doRooms() {
rp.Gold, _ = g.findFloorIn(rp, 0, false) rp.Gold, _ = g.findFloorIn(rp, 0, false)
gold.Pos = rp.Gold gold.Pos = rp.Gold
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold) g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
gold.Flags = Stackable gold.Flags = Stackable
gold.Group = goldGrp gold.Group = goldGrp
gold.Kind = KindGold gold.Kind = KindGold
attachObj(&g.Level.Objects, gold) g.Level.AddObject(gold)
} }
// Put the monster in // Put the monster in
prob := 25 prob := 25
if rp.GoldVal > 0 { if rp.GoldVal > 0 {
prob = 80 prob = 80
} }
if g.rnd(100) < prob { if g.rnd(100) < prob {
tp := &Monster{} tp := &Monster{}
mp, _ := g.findFloorIn(rp, 0, true) mp, _ := g.findFloorIn(rp, 0, true)
@@ -115,9 +126,11 @@ func (g *RogueGame) doRooms() {
// rooms; for maze rooms, draws the maze (rooms.c draw_room). // rooms; for maze rooms, draws the maze (rooms.c draw_room).
func (g *RogueGame) drawRoom(rp *Room) { func (g *RogueGame) drawRoom(rp *Room) {
if rp.Flags.Has(Maze) { if rp.Flags.Has(Maze) {
g.doMaze(rp) g.digMaze(rp)
return return
} }
g.vert(rp, rp.Pos.X) // Draw left side g.vert(rp, rp.Pos.X) // Draw left side
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
g.horiz(rp, rp.Pos.Y) // Draw top g.horiz(rp, rp.Pos.Y) // Draw top
@@ -145,8 +158,8 @@ func (g *RogueGame) horiz(rp *Room, starty int) {
} }
} }
// doMaze digs a maze (rooms.c do_maze). // digMaze digs a maze (rooms.c do_maze).
func (g *RogueGame) doMaze(rp *Room) { func (g *RogueGame) digMaze(rp *Room) {
m := &g.maze m := &g.maze
for y := range m.maze { for y := range m.maze {
for x := range m.maze[y] { for x := range m.maze[y] {
@@ -162,7 +175,7 @@ func (g *RogueGame) doMaze(rp *Room) {
starty := (g.rnd(rp.Max.Y) / 2) * 2 starty := (g.rnd(rp.Max.Y) / 2) * 2
startx := (g.rnd(rp.Max.X) / 2) * 2 startx := (g.rnd(rp.Max.X) / 2) * 2
pos := Coord{Y: starty + m.starty, X: startx + m.startx} pos := Coord{Y: starty + m.starty, X: startx + m.startx}
g.putpass(pos) g.putPassage(pos)
g.dig(starty, startx) g.dig(starty, startx)
} }
@@ -173,26 +186,34 @@ func (g *RogueGame) dig(y, x int) {
for { for {
cnt := 0 cnt := 0
var nexty, nextx int var nexty, nextx int
for _, cp := range del { for _, cp := range del {
newy := y + cp.Y newy := y + cp.Y
newx := x + cp.X newx := x + cp.X
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx { if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
continue continue
} }
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) { if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
continue continue
} }
if cnt++; g.rnd(cnt) == 0 { if cnt++; g.rnd(cnt) == 0 {
nexty = newy nexty = newy
nextx = newx nextx = newx
} }
} }
if cnt == 0 { if cnt == 0 {
return return
} }
g.accntMaze(y, x, nexty, nextx)
g.accntMaze(nexty, nextx, y, x) g.accountMaze(y, x, nexty, nextx)
g.accountMaze(nexty, nextx, y, x)
var pos Coord var pos Coord
if nexty == y { if nexty == y {
pos.Y = y + m.starty pos.Y = y + m.starty
@@ -209,18 +230,19 @@ func (g *RogueGame) dig(y, x int) {
pos.Y = nexty + m.starty - 1 pos.Y = nexty + m.starty - 1
} }
} }
g.putpass(pos)
g.putPassage(pos)
pos.Y = nexty + m.starty pos.Y = nexty + m.starty
pos.X = nextx + m.startx pos.X = nextx + m.startx
g.putpass(pos) g.putPassage(pos)
g.dig(nexty, nextx) g.dig(nexty, nextx)
} }
} }
// accntMaze accounts for maze exits (rooms.c accnt_maze). // accountMaze accounts for maze exits (rooms.c accnt_maze).
func (g *RogueGame) accntMaze(y, x, ny, nx int) { func (g *RogueGame) accountMaze(y, x, ny, nx int) {
sp := &g.maze.maze[y][x] 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 { if sp.exits[i].Y == ny && sp.exits[i].X == nx {
return return
} }
@@ -233,18 +255,21 @@ func (g *RogueGame) accntMaze(y, x, ny, nx int) {
} }
} }
// rndPos picks a random spot in a room (rooms.c rnd_pos). // randomPos picks a random spot in a room (rooms.c rnd_pos).
func (g *RogueGame) rndPos(rp *Room) Coord { func (g *RogueGame) randomPos(rp *Room) Coord {
var cp Coord var cp Coord
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1 cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1 cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
return cp return cp
} }
// findFloor finds a valid floor spot, picking a new random room each time // findFloor finds a valid floor spot, picking a new random room each time
// around the loop (rooms.c find_floor with rp == NULL). // around the loop; it retries forever (rooms.c find_floor with rp == NULL
func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) { // — every such C call site passed FALSE for the limit).
return g.findFloorImpl(rp, limit, monst, rp == nil) 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 // findFloorIn finds a valid floor spot in this room (rooms.c find_floor
@@ -261,6 +286,7 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
compchar = Passage compchar = Passage
} }
} }
cnt := limit cnt := limit
for { for {
if limit != 0 { if limit != 0 {
@@ -268,14 +294,18 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
return Coord{}, false return Coord{}, false
} }
} }
if pickroom { if pickroom {
rp = &g.Level.Rooms[g.rndRoom()] rp = &g.Level.Rooms[g.randomRoom()]
compchar = Floor compchar = Floor
if rp.Flags.Has(Maze) { if rp.Flags.Has(Maze) {
compchar = Passage compchar = Passage
} }
} }
cp := g.rndPos(rp)
cp := g.randomPos(rp)
pp := g.Level.At(cp.Y, cp.X) pp := g.Level.At(cp.Y, cp.X)
if monst { if monst {
if pp.Monst == nil && stepOk(pp.Ch) { if pp.Monst == nil && stepOk(pp.Ch) {
@@ -291,14 +321,17 @@ func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Co
// enter_room). // enter_room).
func (g *RogueGame) enterRoom(cp Coord) { func (g *RogueGame) enterRoom(cp Coord) {
p := &g.Player p := &g.Player
rp := g.roomin(cp) rp := g.roomIn(cp)
p.Room = rp p.Room = rp
g.doorOpen(rp) g.doorOpen(rp)
if !rp.Flags.Has(Dark) && !p.On(Blind) { if !rp.Flags.Has(Dark) && !p.On(Blind) {
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ { for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
g.move(y, rp.Pos.X) g.move(y, rp.Pos.X)
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ { for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
tp := g.Level.MonsterAt(y, x) tp := g.Level.MonsterAt(y, x)
ch := g.Level.Char(y, x) ch := g.Level.Char(y, x)
if tp == nil { if tp == nil {
if g.inch() != ch { if g.inch() != ch {
@@ -335,6 +368,7 @@ func (g *RogueGame) leaveRoom(cp Coord) {
} }
var floor byte var floor byte
switch { switch {
case rp.Flags.Has(Gone): case rp.Flags.Has(Gone):
floor = Passage floor = Passage
@@ -348,6 +382,7 @@ func (g *RogueGame) leaveRoom(cp Coord) {
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ { for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ { for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
g.move(y, x) g.move(y, x)
switch ch := g.inch(); ch { switch ch := g.inch(); ch {
case Floor: case Floor:
if floor == ' ' { if floor == ' ' {
@@ -361,8 +396,10 @@ func (g *RogueGame) leaveRoom(cp Coord) {
g.standout() g.standout()
g.addch(ch) g.addch(ch)
g.standend() g.standend()
break break
} }
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
if pp.Ch == Door { if pp.Ch == Door {
g.addch(Door) g.addch(Door)
@@ -373,5 +410,6 @@ func (g *RogueGame) leaveRoom(cp Coord) {
} }
} }
} }
g.doorOpen(rp) g.doorOpen(rp)
} }

View File

@@ -9,21 +9,27 @@ import (
// moves, a rest, an inventory, then Q-quit answered yes. // moves, a rest, an inventory, then Q-quit answered yes.
func TestRunScriptedSession(t *testing.T) { func TestRunScriptedSession(t *testing.T) {
tt := &testTerm{input: []byte("hjkl.i Qy")} tt := &testTerm{input: []byte("hjkl.i Qy")}
g := NewGame(Config{Seed: 99, Term: tt}) g := NewGame(Config{Seed: 99, Term: tt})
if err := g.Run(); err != nil {
err := g.Run()
if err != nil {
t.Fatalf("Run: %v", err) t.Fatalf("Run: %v", err)
} }
if g.Playing { if g.Playing {
t.Error("still playing after quit") t.Error("still playing after quit")
} }
// After quitting, the scoreboard is the last thing shown (in C it went // After quitting, the scoreboard is the last thing shown (in C it went
// to stdout after endwin; here it is drawn on the screen). // to stdout after endwin; here it is drawn on the screen).
found := false found := false
for y := 0; y < NumLines; y++ {
for y := range NumLines {
if strings.Contains(g.scr.Std.Line(y), "Top Ten") { if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
found = true found = true
} }
} }
if !found { if !found {
t.Error("score list not on screen after quit") t.Error("score list not on screen after quit")
} }
@@ -36,17 +42,23 @@ func TestRunManyTurns(t *testing.T) {
// Spaces between commands double as answers to any --More-- prompts; // Spaces between commands double as answers to any --More-- prompts;
// without them a single prompt would swallow the rest of the script // without them a single prompt would swallow the rest of the script
// (wait_for eats everything that isn't a space). // (wait_for eats everything that isn't a space).
var script []byte moves := []byte("h j k l y u b n s .")
moves := []byte("h h j j k k l l y u b n s s . . ") script := make([]byte, 0, len(moves)*200+7)
for range 200 { for range 200 {
script = append(script, moves...) script = append(script, moves...)
} }
script = append(script, " Q y Qy"...) script = append(script, " Q y Qy"...)
tt := &testTerm{input: script} tt := &testTerm{input: script}
g := NewGame(Config{Seed: 31337, Term: tt}) g := NewGame(Config{Seed: 31337, Term: tt})
if err := g.Run(); err != nil {
err := g.Run()
if err != nil {
t.Fatalf("Run: %v", err) t.Fatalf("Run: %v", err)
} }
if g.Playing { if g.Playing {
t.Error("session did not end") t.Error("session did not end")
} }
@@ -65,9 +77,12 @@ func TestRunDownStairs(t *testing.T) {
g.StartDaemon(DDoctor, 0, After) g.StartDaemon(DDoctor, 0, After)
g.Fuse(DSwander, 0, wanderTime(g), After) g.Fuse(DSwander, 0, wanderTime(g), After)
g.StartDaemon(DStomach, 0, After) g.StartDaemon(DStomach, 0, After)
if err := g.Run(); err != nil {
err := g.Run()
if err != nil {
t.Fatalf("Run: %v", err) t.Fatalf("Run: %v", err)
} }
if g.Depth != 2 { if g.Depth != 2 {
t.Errorf("depth = %d after descending, want 2", g.Depth) t.Errorf("depth = %d after descending, want 2", g.Depth)
} }
@@ -79,26 +94,33 @@ func TestSaveCommandRoundTrip(t *testing.T) {
// The C get_str caps input at MAXINP=50 characters, so the save path // The C get_str caps input at MAXINP=50 characters, so the save path
// must be short: work from the temp directory. // must be short: work from the temp directory.
t.Chdir(t.TempDir()) t.Chdir(t.TempDir())
path := "cmd.save" path := "cmd.save"
// 'S' with no default file name goes straight to the name prompt. // 'S' with no default file name goes straight to the name prompt.
script := "S" + path + "\n" script := "S" + path + "\n"
tt := &testTerm{input: []byte(script)} tt := &testTerm{input: []byte(script)}
g := NewGame(Config{Seed: 55, Term: tt}) g := NewGame(Config{Seed: 55, Term: tt})
g.FileName = "" // force the name prompt g.FileName = "" // force the name prompt
if err := g.Run(); err != nil {
t.Fatalf("Run: %v", err) runErr := g.Run()
if runErr != nil {
t.Fatalf("Run: %v", runErr)
} }
h, err := Restore(path, Config{Term: &testTerm{}}) h, err := Restore(path, Config{Term: &testTerm{}})
if err != nil { if err != nil {
t.Fatalf("Restore: %v", err) t.Fatalf("Restore: %v", err)
} }
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse { if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
t.Error("restored game does not match saved game") t.Error("restored game does not match saved game")
} }
// The restored game must be playable. // The restored game must be playable.
h.scr.term.(*testTerm).input = []byte("..Qy") setInput(t, h, []byte("..Qy")...)
if err := h.Run(); err != nil {
t.Fatalf("restored Run: %v", err) restoredErr := h.Run()
if restoredErr != nil {
t.Fatalf("restored Run: %v", restoredErr)
} }
} }

View File

@@ -2,6 +2,7 @@ package game
import ( import (
"encoding/gob" "encoding/gob"
"errors"
"fmt" "fmt"
"os" "os"
) )
@@ -131,16 +132,19 @@ func (g *RogueGame) roomIdx(rp *Room) int {
if rp == nil { if rp == nil {
return -1 return -1
} }
for i := range g.Level.Rooms { for i := range g.Level.Rooms {
if rp == &g.Level.Rooms[i] { if rp == &g.Level.Rooms[i] {
return i return i
} }
} }
for i := range g.Level.Passages { for i := range g.Level.Passages {
if rp == &g.Level.Passages[i] { if rp == &g.Level.Passages[i] {
return 100 + i return 100 + i
} }
} }
return -1 return -1
} }
@@ -152,6 +156,7 @@ func (g *RogueGame) roomAt(i int) *Room {
case i >= 0: case i >= 0:
return &g.Level.Rooms[i] return &g.Level.Rooms[i]
} }
return nil return nil
} }
@@ -160,11 +165,13 @@ func (g *RogueGame) packIdx(obj *Object) int {
if obj == nil { if obj == nil {
return -1 return -1
} }
for i, o := range g.Player.Pack { for i, o := range g.Player.Pack {
if o == obj { if o == obj {
return i return i
} }
} }
return -1 return -1
} }
@@ -266,9 +273,11 @@ func (g *RogueGame) snapshot() *SaveState {
for _, o := range m.Pack { for _, o := range m.Pack {
sc.Pack = append(sc.Pack, *o) sc.Pack = append(sc.Pack, *o)
} }
st.Monsters = append(st.Monsters, sc) st.Monsters = append(st.Monsters, sc)
ref := destRef{} ref := destRef{}
switch { switch {
case m.Dest == nil: case m.Dest == nil:
case m.Dest == &p.Pos: case m.Dest == &p.Pos:
@@ -279,6 +288,7 @@ func (g *RogueGame) snapshot() *SaveState {
ref = destRef{Kind: 2, Idx: mi} ref = destRef{Kind: 2, Idx: mi}
} }
} }
if ref.Kind == 0 { if ref.Kind == 0 {
for _, oo := range g.Level.Objects { for _, oo := range g.Level.Objects {
if m.Dest == &oo.Pos { if m.Dest == &oo.Pos {
@@ -286,6 +296,7 @@ func (g *RogueGame) snapshot() *SaveState {
} }
} }
} }
if ref.Kind == 0 { if ref.Kind == 0 {
for ri := range g.Level.Rooms { for ri := range g.Level.Rooms {
if m.Dest == &g.Level.Rooms[ri].Gold { if m.Dest == &g.Level.Rooms[ri].Gold {
@@ -294,8 +305,10 @@ func (g *RogueGame) snapshot() *SaveState {
} }
} }
} }
st.Dests = append(st.Dests, ref) st.Dests = append(st.Dests, ref)
} }
return st return st
} }
@@ -371,15 +384,18 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.Flags = sp.Body.Flags p.Flags = sp.Body.Flags
p.Stats = sp.Body.Stats p.Stats = sp.Body.Stats
p.Room = g.roomAt(sp.Body.RoomIdx) p.Room = g.roomAt(sp.Body.RoomIdx)
p.Pack = nil p.Pack = nil
for i := range sp.Body.Pack { for i := range sp.Body.Pack {
o := sp.Body.Pack[i] o := sp.Body.Pack[i]
p.Pack = append(p.Pack, &o) p.Pack = append(p.Pack, &o)
} }
pick := func(i int) *Object { pick := func(i int) *Object {
if i < 0 || i >= len(p.Pack) { if i < 0 || i >= len(p.Pack) {
return nil return nil
} }
return p.Pack[i] return p.Pack[i]
} }
p.CurArmor = pick(sp.CurArmor) p.CurArmor = pick(sp.CurArmor)
@@ -400,6 +416,7 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Monsters = nil g.Level.Monsters = nil
for i := range st.Monsters { for i := range st.Monsters {
sc := &st.Monsters[i] sc := &st.Monsters[i]
m := &Monster{Creature: Creature{ m := &Monster{Creature: Creature{
Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise, Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise,
OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats, OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats,
@@ -409,11 +426,14 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
o := sc.Pack[j] o := sc.Pack[j]
m.Pack = append(m.Pack, &o) m.Pack = append(m.Pack, &o)
} }
g.Level.Monsters = append(g.Level.Monsters, m) g.Level.Monsters = append(g.Level.Monsters, m)
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m) g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
} }
for i, ref := range st.Dests { for i, ref := range st.Dests {
m := g.Level.Monsters[i] m := g.Level.Monsters[i]
switch ref.Kind { switch ref.Kind {
case 1: case 1:
m.Dest = &p.Pos m.Dest = &p.Pos
@@ -429,119 +449,167 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.scr.Std.SetContents(st.Screen) g.scr.Std.SetContents(st.Screen)
} }
// saveGame implements the "save game" command (save.c save_game). The C // saveGame implements the "save game" command (save.c save_game). The
// goto over/gotfile flow becomes the useDefault flag. // labeled prompt loop and the useDefault flag stand in for the C
// goto over/gotfile flow.
func (g *RogueGame) saveGame() { func (g *RogueGame) saveGame() {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
over:
useDefault := false
if g.FileName != "" {
var c byte
for {
g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0
if c == Escape {
g.msg("")
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
}
}
prompt:
for { for {
var buf string useDefault := false
if useDefault {
buf = g.FileName if g.FileName != "" {
useDefault = false var c byte
} else {
g.Msgs.Mpos = 0
g.msg("file name: ")
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
return
}
g.Msgs.Mpos = 0
}
// test to see if the file exists
if _, err := os.Stat(buf); err == nil {
for { for {
g.msg("File exists. Do you wish to overwrite it?") g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
c := g.readchar()
if c == Escape { if c == Escape {
g.msg("") g.msg("")
return return
} }
if c == 'y' || c == 'Y' {
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
break break
} }
if c == 'n' || c == 'N' {
goto over g.msg("please answer Y or N")
} }
g.msg("Please answer Y or N")
if c == 'y' || c == 'Y' {
g.addstr("Yes\n")
g.refresh()
useDefault = true
} }
g.msg("file name: %s", buf)
os.Remove(g.FileName)
} }
g.FileName = buf
if err := g.saveFile(g.FileName); err != nil { for {
g.msg("%s", err.Error()) var buf string
continue 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("")
return
}
g.Msgs.Mpos = 0
}
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr == nil {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
c := g.readchar()
if c == Escape {
g.msg("")
return
}
if c == 'y' || c == 'Y' {
break
}
if c == 'n' || c == 'N' {
continue prompt // the C goto over: start again
}
g.msg("Please answer Y or N")
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
}
g.FileName = buf
err := g.saveFile(g.FileName)
if err != nil {
g.msg("%s", err.Error())
continue
}
break prompt
} }
break
} }
g.myExit(0)
g.myExit()
} }
// 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 { 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 { if err != nil {
return err return err
} }
defer f.Close()
if err := gob.NewEncoder(f).Encode(g.snapshot()); err != nil { encErr := gob.NewEncoder(f).Encode(g.snapshot())
os.Remove(path) closeErr := f.Close()
return err
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) return os.Chmod(path, 0o400)
} }
// AutoSave silently saves to the current file name; used on SIGHUP/SIGTERM // 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() { func (g *RogueGame) AutoSave() {
if g.FileName != "" { if g.FileName != "" {
os.Remove(g.FileName) _ = os.Remove(g.FileName)
g.saveFile(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 // Restore restores a saved game from a file (save.c restore). The file is
// deleted, as in C, to defeat restarting from the same save. // deleted, as in C, to defeat restarting from the same save.
func Restore(path string, cfg Config) (*RogueGame, error) { func Restore(path string, cfg Config) (*RogueGame, error) {
f, err := os.Open(path) f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer func() { _ = f.Close() }() // read-only handle
var st SaveState 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 { if st.Version != saveFormatVersion {
return nil, fmt.Errorf("sorry, saved game is out of date") return nil, ErrSaveOutOfDate
} }
g := &RogueGame{ g := &RogueGame{
data: newGameData(),
Rng: &Rng{}, Rng: &Rng{},
Playing: true, Playing: true,
ScorePath: cfg.ScorePath, ScorePath: cfg.ScorePath,
@@ -550,11 +618,14 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
restored: true, restored: true,
} }
g.scr = NewScreen(cfg.Term) g.scr = NewScreen(cfg.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.applySnapshot(&st) g.applySnapshot(&st)
// defeat multiple restarting from the same place // defeat multiple restarting from the same place
if err := os.Remove(path); err != nil { rmErr := os.Remove(path)
return nil, fmt.Errorf("cannot unlink file: %w", err) if rmErr != nil {
return nil, fmt.Errorf("cannot unlink file: %w", rmErr)
} }
return g, nil return g, nil
} }

View File

@@ -15,6 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
g.HasAmulet = true g.HasAmulet = true
g.Items.Potions[PotionHealing].Know = true g.Items.Potions[PotionHealing].Know = true
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???" g.Items.Scrolls[ScrollMagicMapping].Guess = "map???"
g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive
if len(g.Level.Monsters) > 0 { if len(g.Level.Monsters) > 0 {
g.Level.Monsters[0].Flags.Set(Awake) g.Level.Monsters[0].Flags.Set(Awake)
@@ -22,8 +23,10 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
} }
path := filepath.Join(t.TempDir(), "rogue.save") 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, Config{Term: &testTerm{}})
@@ -31,7 +34,8 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Fatalf("Restore: %v", err) 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)") t.Error("save file not deleted on restore (C anti-restart rule)")
} }
@@ -39,36 +43,46 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
t.Errorf("player state lost: purse=%d food=%d", t.Errorf("player state lost: purse=%d food=%d",
h.Player.Purse, h.Player.FoodLeft) h.Player.Purse, h.Player.FoodLeft)
} }
if !h.HasAmulet { if !h.HasAmulet {
t.Error("amulet flag lost") t.Error("amulet flag lost")
} }
if !h.Items.Potions[PotionHealing].Know { if !h.Items.Potions[PotionHealing].Know {
t.Error("potion identification lost") t.Error("potion identification lost")
} }
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" { if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
t.Error("scroll guess lost") t.Error("scroll guess lost")
} }
if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" { if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" {
t.Error("mutated bestiary lost") t.Error("mutated bestiary lost")
} }
if h.Rng.Seed != g.Rng.Seed { if h.Rng.Seed != g.Rng.Seed {
t.Error("RNG state lost") t.Error("RNG state lost")
} }
if renderMap(h) != renderMap(g) { if renderMap(h) != renderMap(g) {
t.Error("restored level map differs") t.Error("restored level map differs")
} }
if len(h.Level.Monsters) != len(g.Level.Monsters) { if len(h.Level.Monsters) != len(g.Level.Monsters) {
t.Fatalf("monster count %d != %d", t.Fatalf("monster count %d != %d",
len(h.Level.Monsters), len(g.Level.Monsters)) len(h.Level.Monsters), len(g.Level.Monsters))
} }
if len(g.Level.Monsters) > 0 { if len(g.Level.Monsters) > 0 {
m := h.Level.Monsters[0] m := h.Level.Monsters[0]
if m.Dest != &h.Player.Pos { if m.Dest != &h.Player.Pos {
t.Error("monster chase target not re-aliased to the hero") t.Error("monster chase target not re-aliased to the hero")
} }
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m { if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
t.Error("map monster index not rebuilt") t.Error("map monster index not rebuilt")
} }
if m.Room == nil { if m.Room == nil {
t.Error("monster room pointer not rebuilt") t.Error("monster room pointer not rebuilt")
} }
@@ -81,13 +95,17 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
len(st.Player.Body.Pack)) len(st.Player.Body.Pack))
t.Logf("restored: CurWeapon=%p pack has %d items", h.Player.CurWeapon, t.Logf("restored: CurWeapon=%p pack has %d items", h.Player.CurWeapon,
len(h.Player.Pack)) len(h.Player.Pack))
found := false found := false
for i, o := range h.Player.Pack { for i, o := range h.Player.Pack {
t.Logf(" pack[%d]=%p type=%v which=%d", i, o, o.Kind, o.Which) t.Logf(" pack[%d]=%p type=%v which=%d", i, o, o.Kind, o.Which)
if o == h.Player.CurWeapon { if o == h.Player.CurWeapon {
found = true found = true
} }
} }
if !found { if !found {
t.Error("restored CurWeapon is not aliased into the pack") t.Error("restored CurWeapon is not aliased into the pack")
} }
@@ -98,15 +116,24 @@ func TestRestoreRejectsWrongVersion(t *testing.T) {
path := filepath.Join(t.TempDir(), "rogue.save") path := filepath.Join(t.TempDir(), "rogue.save")
st := g.snapshot() st := g.snapshot()
st.Version = "0.0.0" st.Version = "0.0.0"
f, err := os.Create(path)
f, err := os.Create(path) //nolint:gosec // G304: test temp path
if err != nil { if err != nil {
t.Fatal(err) 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, Config{})
if restoreErr == nil {
t.Error("restore accepted an out-of-date save") t.Error("restore accepted an out-of-date save")
} }
} }

View File

@@ -25,29 +25,28 @@ type ScoreEnt struct {
Time int64 Time int64
} }
var scoreReasons = [4]string{
"killed",
"quit",
"A total winner",
"killed with Amulet",
}
// rdScore reads the scoreboard file (save.c rd_score). // rdScore reads the scoreboard file (save.c rd_score).
func (g *RogueGame) rdScore() []ScoreEnt { func (g *RogueGame) rdScore() []ScoreEnt {
topTen := make([]ScoreEnt, numScores) topTen := make([]ScoreEnt, numScores)
if g.ScorePath == "" { if g.ScorePath == "" {
return topTen return topTen
} }
f, err := os.Open(g.ScorePath) f, err := os.Open(g.ScorePath)
if err != nil { if err != nil {
return topTen return topTen
} }
defer f.Close() defer func() { _ = f.Close() }() // read-only handle
var onDisk []ScoreEnt 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) copy(topTen, onDisk)
return topTen return topTen
} }
@@ -57,29 +56,45 @@ func (g *RogueGame) wrScore(topTen []ScoreEnt) {
return return
} }
// lock_sc/unlock_sc: exclusive-create lock file with stale takeover. // 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" lock := g.ScorePath + ".lck"
for range 5 { 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 { if err == nil {
lf.Close() _ = lf.Close()
defer os.Remove(lock)
defer func() { _ = os.Remove(lock) }()
break break
} }
if fi, serr := os.Stat(lock); serr == nil &&
time.Since(fi.ModTime()) > 10*time.Second { fi, statErr := os.Stat(lock)
os.Remove(lock) if statErr == nil && time.Since(fi.ModTime()) > staleLockAge {
_ = os.Remove(lock)
continue continue
} }
time.Sleep(time.Second) 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 { if err != nil {
return 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 // score figures the score and posts it (rip.c score). flags -1 means just
// display the list (the -s command line option). // display the list (the -s command line option).
func (g *RogueGame) score(amount, flags int, monst byte) { func (g *RogueGame) score(amount, flags int, monst byte) {
@@ -92,38 +107,47 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
topTen := g.rdScore() topTen := g.rdScore()
// Insert her in list if need be // Insert her in list if need be
ins := -1 ins := -1
if !g.NoScore && flags >= 0 { if !g.NoScore && flags >= 0 {
uid := os.Getuid() uid := os.Getuid()
scp := len(topTen) scp := len(topTen)
for i := range topTen { for i := range topTen {
if amount > topTen[i].Score { if amount > topTen[i].Score {
scp = i scp = i
break break
} else if !g.AllScore && flags != 2 && } else if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 { topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid // only one score per nowin uid
scp = len(topTen) scp = len(topTen)
break break
} }
} }
if scp < len(topTen) { if scp < len(topTen) {
sc2 := len(topTen) - 1 sc2 := len(topTen) - 1
if flags != 2 && !g.AllScore { if flags != 2 && !g.AllScore {
for i := scp; i < len(topTen); i++ { for i := scp; i < len(topTen); i++ {
if topTen[i].UID == uid && topTen[i].Flags != 2 { if topTen[i].UID == uid && topTen[i].Flags != 2 {
sc2 = i sc2 = i
break break
} }
} }
} }
for sc2 > scp { for sc2 > scp {
topTen[sc2] = topTen[sc2-1] topTen[sc2] = topTen[sc2-1]
sc2-- sc2--
} }
lvl := g.Depth lvl := g.Depth
if flags == 2 { if flags == 2 {
lvl = g.MaxDepth lvl = g.MaxDepth
} }
topTen[scp] = ScoreEnt{ topTen[scp] = ScoreEnt{
UID: uid, UID: uid,
Score: amount, Score: amount,
@@ -142,43 +166,53 @@ func (g *RogueGame) score(amount, flags int, monst byte) {
if g.AllScore { if g.AllScore {
label = "Scores" label = "Scores"
} }
lines := []string{ lines := []string{
fmt.Sprintf("Top Ten %s:", label), fmt.Sprintf("Top Ten %s:", label),
" Score Name", " Score Name",
} }
highlight := -1 highlight := -1
for i := range topTen { for i := range topTen {
scp := &topTen[i] scp := &topTen[i]
if scp.Score == 0 { if scp.Score == 0 {
break break
} }
line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1, line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1,
scp.Score, scp.Name, scoreReasons[scp.Flags], scp.Level) scp.Score, scp.Name, g.data.scoreReasons[scp.Flags], scp.Level)
if scp.Flags == 0 || scp.Flags == 3 { if scp.Flags == 0 || scp.Flags == 3 {
line += fmt.Sprintf(" by %s", g.killname(scp.Monster, true)) line += " by " + g.killname(scp.Monster, true)
} }
line += "." line += "."
if i == ins { if i == ins {
highlight = len(lines) highlight = len(lines)
} }
lines = append(lines, line) lines = append(lines, line)
} }
if g.scr != nil && g.scr.term != nil { if g.scr != nil && g.scr.term != nil {
g.clear() g.clear()
for i, line := range lines { for i, line := range lines {
if i == highlight { if i == highlight {
g.standout() g.standout()
} }
g.mvaddstr(i, 0, line) g.mvaddstr(i, 0, line)
if i == highlight { if i == highlight {
g.standend() g.standend()
} }
} }
g.refresh() g.refresh()
} else { } else {
for _, line := range lines { for _, line := range lines {
fmt.Println(line) _, _ = fmt.Fprintln(os.Stdout, line) // CLI output
} }
} }

View File

@@ -37,26 +37,28 @@ type Window struct {
func NewWindow(rows, cols int) *Window { func NewWindow(rows, cols int) *Window {
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)} w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
w.Clear() w.Clear()
return w return w
} }
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
// Move positions the cursor (curses move/wmove). // Move positions the cursor (curses move/wmove).
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x } func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
// GetYX reports the cursor position (curses getyx). // 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). // AddCh writes a character at the cursor and advances it (curses addch).
func (w *Window) AddCh(ch byte) { func (w *Window) AddCh(ch byte) {
if ch == '\n' { if ch == '\n' {
w.cy, w.cx = w.cy+1, 0 w.cy, w.cx = w.cy+1, 0
return return
} }
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols { if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return return
} }
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout} *w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
if w.cx++; w.cx >= w.cols { if w.cx++; w.cx >= w.cols {
w.cx = 0 w.cx = 0
@@ -68,7 +70,7 @@ func (w *Window) AddCh(ch byte) {
// AddStr writes a string at the cursor (curses addstr). // AddStr writes a string at the cursor (curses addstr).
func (w *Window) AddStr(s string) { func (w *Window) AddStr(s string) {
for i := 0; i < len(s); i++ { for i := range len(s) {
w.AddCh(s[i]) w.AddCh(s[i])
} }
} }
@@ -85,15 +87,15 @@ func (w *Window) MvAddStr(y, x int, s string) {
w.AddStr(s) w.AddStr(s)
} }
// Printw writes formatted text at the cursor (curses printw). // Printwf writes formatted text at the cursor (curses printw).
func (w *Window) Printw(format string, a ...any) { func (w *Window) Printwf(format string, a ...any) {
w.AddStr(fmt.Sprintf(format, a...)) w.AddStr(fmt.Sprintf(format, a...))
} }
// MvPrintw moves then writes formatted text (curses mvprintw). // MvPrintwf moves then writes formatted text (curses mvprintw).
func (w *Window) MvPrintw(y, x int, format string, a ...any) { func (w *Window) MvPrintwf(y, x int, format string, a ...any) {
w.Move(y, x) w.Move(y, x)
w.Printw(format, a...) w.Printwf(format, a...)
} }
// Inch returns the character under the cursor (curses inch, sans // Inch returns the character under the cursor (curses inch, sans
@@ -102,12 +104,14 @@ func (w *Window) Inch() byte {
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols { if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return ' ' return ' '
} }
return w.at(w.cy, w.cx).ch return w.at(w.cy, w.cx).ch
} }
// MvInch moves then reads (curses mvinch). // MvInch moves then reads (curses mvinch).
func (w *Window) MvInch(y, x int) byte { func (w *Window) MvInch(y, x int) byte {
w.Move(y, x) w.Move(y, x)
return w.Inch() return w.Inch()
} }
@@ -120,6 +124,7 @@ func (w *Window) Clear() {
for i := range w.cells { for i := range w.cells {
w.cells[i] = cell{ch: ' '} w.cells[i] = cell{ch: ' '}
} }
w.cy, w.cx = 0, 0 w.cy, w.cx = 0, 0
} }
@@ -128,6 +133,7 @@ func (w *Window) Clrtoeol() {
if w.cy < 0 || w.cy >= w.rows { if w.cy < 0 || w.cy >= w.rows {
return return
} }
for x := w.cx; x < w.cols; x++ { for x := w.cx; x < w.cols; x++ {
*w.at(w.cy, x) = cell{ch: ' '} *w.at(w.cy, x) = cell{ch: ' '}
} }
@@ -138,13 +144,14 @@ func (w *Window) CopyFrom(src *Window) {
copy(w.cells, src.cells) copy(w.cells, src.cells)
} }
// Size reports the window dimensions. // Size reports the window dimensions as rows, columns.
func (w *Window) Size() (rows, cols int) { return w.rows, w.cols } func (w *Window) Size() (int, int) { return w.rows, w.cols }
// CellAt reports the character and standout attribute at a position; used // CellAt reports the character and standout attribute at a position; used
// by Terminal implementations to render the window. // 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) c := w.at(y, x)
return c.ch, c.standout return c.ch, c.standout
} }
@@ -155,6 +162,7 @@ func (w *Window) Contents() []byte {
for i, c := range w.cells { for i, c := range w.cells {
out[i] = c.ch out[i] = c.ch
} }
return out return out
} }
@@ -171,12 +179,16 @@ func (w *Window) SetContents(data []byte) {
// victory screens. // victory screens.
func (w *Window) Line(y int) string { func (w *Window) Line(y int) string {
buf := make([]byte, w.cols) buf := make([]byte, w.cols)
for x := 0; x < w.cols; x++ { for x := range w.cols {
buf[x] = w.at(y, x).ch buf[x] = w.at(y, x).ch
} }
return string(buf) 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 // Screen bundles the two windows the game draws on with the device that
// shows them. // shows them.
type Screen struct { type Screen struct {
@@ -217,7 +229,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) { func (g *RogueGame) mvaddstr(y, x int, s string) {
g.scr.Std.MvAddStr(y, x, s) 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) 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) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
func (g *RogueGame) standout() { g.scr.Std.Standout(true) } 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. // scrolls.c — read a scroll and let it happen.
// idType maps identify scrolls to the kind of item they identify
// (scrolls.c static id_type).
var idType = [ScrollIdentifyRingOrStick + 1]ObjectKind{
ScrollIdentifyPotion: KindPotion,
ScrollIdentifyScroll: KindScroll,
ScrollIdentifyWeapon: KindWeapon,
ScrollIdentifyArmor: KindArmor,
ScrollIdentifyRingOrStick: KindRingOrStick,
}
// readScroll reads a scroll from the pack and does the appropriate thing // readScroll reads a scroll from the pack and does the appropriate thing
// (scrolls.c read_scroll). // (scrolls.c read_scroll).
func (g *RogueGame) readScroll() { func (g *RogueGame) readScroll() {
p := &g.Player p := &g.Player
obj := g.getItem("read", KindScroll)
if obj == nil { obj, ok := g.promptPackItem("read", KindScroll)
if !ok {
return return
} }
if obj.Kind != KindScroll { if obj.Kind != KindScroll {
if !g.Options.Terse { if !g.Options.Terse {
g.msg("there is nothing on it to read") g.msg("there is nothing on it to read")
} else { } else {
g.msg("nothing to read") g.msg("nothing to read")
} }
return return
} }
// Calculate the effect it has on the poor guy. // Calculate the effect it has on the poor guy.
@@ -50,30 +43,39 @@ func (g *RogueGame) readScroll() {
// Hold monster scroll. Stop all monsters within two spaces from // Hold monster scroll. Stop all monsters within two spaces from
// chasing after the hero. // chasing after the hero.
held := 0 held := 0
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ { for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
if x < 0 || x >= NumCols { if x < 0 || x >= NumCols {
continue continue
} }
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ { for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
if y < 0 || y > NumLines-1 { if y < 0 || y > NumLines-1 {
continue continue
} }
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) { if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
mp.Flags.Clear(Awake) mp.Flags.Clear(Awake)
mp.Flags.Set(Held) mp.Flags.Set(Held)
held++ held++
} }
} }
} }
if held > 0 { if held > 0 {
g.addmsg("the monster") g.addmsgf("the monster")
if held > 1 { if held > 1 {
g.addmsg("s around you") g.addmsgf("s around you")
} }
g.addmsg(" freeze")
g.addmsgf(" freeze")
if held == 1 { if held == 1 {
g.addmsg("s") g.addmsgf("s")
} }
g.endmsg() g.endmsg()
g.Items.Scrolls[ScrollHoldMonster].Know = true g.Items.Scrolls[ScrollHoldMonster].Know = true
} else { } else {
@@ -83,13 +85,16 @@ func (g *RogueGame) readScroll() {
// Scroll which makes you fall asleep // Scroll which makes you fall asleep
g.Items.Scrolls[ScrollSleep].Know = true g.Items.Scrolls[ScrollSleep].Know = true
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
p.Flags.Clear(Awake) p.Flags.Clear(Awake)
g.msg("you fall asleep") g.msg("you fall asleep")
case ScrollCreateMonster: case ScrollCreateMonster:
// Create a monster: first look in a circle around him, next try // Create a monster: first look in a circle around him, next try
// his room, otherwise give up // his room, otherwise give up
i := 0 i := 0
var mp Coord var mp Coord
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ { for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ { for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
// Don't put a monster on top of the player. // Don't put a monster on top of the player.
@@ -99,16 +104,18 @@ func (g *RogueGame) readScroll() {
// Or anything else nasty // Or anything else nasty
if ch := g.Level.VisibleChar(y, x); stepOk(ch) { if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
if ch == Scroll { if ch == Scroll {
if fo := g.findObj(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster { if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
continue continue
} }
} }
if i++; g.rnd(i) == 0 { if i++; g.rnd(i) == 0 {
mp = Coord{Y: y, X: x} mp = Coord{Y: y, X: x}
} }
} }
} }
} }
if i == 0 { if i == 0 {
g.msg("you hear a faint cry of anguish in the distance") g.msg("you hear a faint cry of anguish in the distance")
} else { } else {
@@ -119,17 +126,18 @@ func (g *RogueGame) readScroll() {
// Identify, let him figure something out // Identify, let him figure something out
g.Items.Scrolls[obj.Which].Know = true g.Items.Scrolls[obj.Which].Know = true
g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name) g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name)
g.whatis(true, idType[obj.ScrollKind()]) g.whatis(true, g.data.idType[obj.ScrollKind()])
case ScrollMagicMapping: case ScrollMagicMapping:
// Scroll of magic mapping. // Scroll of magic mapping.
g.Items.Scrolls[ScrollMagicMapping].Know = true g.Items.Scrolls[ScrollMagicMapping].Know = true
g.msg("oh, now this scroll has a map on it") g.msg("oh, now this scroll has a map on it")
// take all the things we want to keep hidden out of the window // take all the things we want to keep hidden out of the window
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ { for x := range NumCols {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
ch := pp.Ch ch := pp.Ch
pass := false pass := false
switch ch { switch ch {
case Door, Stairs: case Door, Stairs:
case '-', '|': case '-', '|':
@@ -168,13 +176,17 @@ func (g *RogueGame) readScroll() {
ch = ' ' ch = ' '
} }
} }
if pass { if pass {
if !pp.Flags.Has(FReal) { if !pp.Flags.Has(FReal) {
pp.Ch = Passage pp.Ch = Passage
} }
pp.Flags.Set(FSeen | FReal) pp.Flags.Set(FSeen | FReal)
ch = Passage ch = Passage
} }
if ch != ' ' { if ch != ' ' {
if tp := pp.Monst; tp != nil { if tp := pp.Monst; tp != nil {
tp.OldCh = ch tp.OldCh = ch
@@ -190,13 +202,17 @@ func (g *RogueGame) readScroll() {
case ScrollFoodDetection: case ScrollFoodDetection:
// Food detection // Food detection
found := false found := false
g.scr.Hw.Clear() g.scr.Hw.Clear()
for _, fo := range g.Level.Objects { for _, fo := range g.Level.Objects {
if fo.Kind == KindFood { if fo.Kind == KindFood {
found = true found = true
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food) g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
} }
} }
if found { if found {
g.Items.Scrolls[ScrollFoodDetection].Know = true g.Items.Scrolls[ScrollFoodDetection].Know = true
g.showWin("Your nose tingles and you smell food.--More--") g.showWin("Your nose tingles and you smell food.--More--")
@@ -206,7 +222,9 @@ func (g *RogueGame) readScroll() {
case ScrollTeleportation: case ScrollTeleportation:
// Scroll of teleportation: make him disappear and reappear // Scroll of teleportation: make him disappear and reappear
curRoom := p.Room curRoom := p.Room
g.teleport() g.teleport()
if curRoom != p.Room { if curRoom != p.Room {
g.Items.Scrolls[ScrollTeleportation].Know = true g.Items.Scrolls[ScrollTeleportation].Know = true
} }
@@ -215,11 +233,13 @@ func (g *RogueGame) readScroll() {
g.msg("you feel a strange sense of loss") g.msg("you feel a strange sense of loss")
} else { } else {
p.CurWeapon.Flags.Clear(Cursed) p.CurWeapon.Flags.Clear(Cursed)
if g.rnd(2) == 0 { if g.rnd(2) == 0 {
p.CurWeapon.HPlus++ p.CurWeapon.HPlus++
} else { } else {
p.CurWeapon.DPlus++ p.CurWeapon.DPlus++
} }
g.msg("your %s glows %s for a moment", g.msg("your %s glows %s for a moment",
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue")) g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
} }
@@ -248,6 +268,7 @@ func (g *RogueGame) readScroll() {
g.msg("you feel a strange sense of loss") g.msg("you feel a strange sense of loss")
} }
} }
g.look(true) // put the result of the scroll on the screen g.look(true) // put the result of the scroll on the screen
g.status() g.status()

View File

@@ -4,22 +4,34 @@ import "fmt"
// sticks.c — zap wands and staffs. // sticks.c — zap wands and staffs.
// The two ws_type strings a stick can be made as.
const (
wandName = "wand"
staffName = "staff"
)
// doZap performs a zap with a wand (sticks.c do_zap). // doZap performs a zap with a wand (sticks.c do_zap).
func (g *RogueGame) doZap() { func (g *RogueGame) doZap() {
p := &g.Player p := &g.Player
obj := g.getItem("zap with", KindWand)
if obj == nil { obj, ok := g.promptPackItem("zap with", KindWand)
if !ok {
return return
} }
if obj.Kind != KindWand { if obj.Kind != KindWand {
g.After = false g.After = false
g.msg("you can't zap with that!") g.msg("you can't zap with that!")
return return
} }
if obj.Charges == 0 { if obj.Charges == 0 {
g.msg("nothing happens") g.msg("nothing happens")
return return
} }
switch obj.WandKind() { switch obj.WandKind() {
case WandLight: case WandLight:
// Reddy Kilowatt wand. Light up the room // Reddy Kilowatt wand. Light up the room
@@ -30,10 +42,12 @@ func (g *RogueGame) doZap() {
p.Room.Flags.Clear(Dark) p.Room.Flags.Clear(Dark)
// Light the room and put the player back up // Light the room and put the player back up
g.enterRoom(p.Pos) g.enterRoom(p.Pos)
g.addmsg("the room is lit") g.addmsgf("the room is lit")
if !g.Options.Terse { 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() g.endmsg()
} }
case WandDrainLife: case WandDrainLife:
@@ -42,42 +56,53 @@ func (g *RogueGame) doZap() {
// passage) // passage)
if p.Stats.HP < 2 { if p.Stats.HP < 2 {
g.msg("you are too weak to use it") g.msg("you are too weak to use it")
return return
} }
g.drain() g.drain()
case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation: case WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation:
y := p.Pos.Y y := p.Pos.Y
x := p.Pos.X x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) { for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y y += g.Delta.Y
x += g.Delta.X x += g.Delta.X
} }
if tp := g.Level.MonsterAt(y, x); tp != nil { if tp := g.Level.MonsterAt(y, x); tp != nil {
monster := tp.Type monster := tp.Type
if monster == 'F' { if monster == 'F' {
p.Flags.Clear(Held) p.Flags.Clear(Held)
} }
switch obj.WandKind() { switch obj.WandKind() {
case WandInvisibility: case WandInvisibility:
tp.Flags.Set(Invisible) tp.Flags.Set(Invisible)
if g.cansee(y, x) {
if g.canSee(y, x) {
g.mvaddch(y, x, tp.OldCh) g.mvaddch(y, x, tp.OldCh)
} }
case WandPolymorph: case WandPolymorph:
pp := tp.Pack pp := tp.Pack
detachMon(&g.Level.Monsters, tp) g.Level.RemoveMonster(tp)
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.mvaddch(y, x, g.Level.Char(y, x)) g.mvaddch(y, x, g.Level.Char(y, x))
} }
oldch := tp.OldCh oldch := tp.OldCh
g.Delta.Y = y g.Delta.Y = y
g.Delta.X = x g.Delta.X = x
monster = byte(g.rnd(26) + 'A') monster = g.randomMonsterLetter()
g.newMonster(tp, monster, g.Delta) g.newMonster(tp, monster, g.Delta)
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.mvaddch(y, x, monster) g.mvaddch(y, x, monster)
} }
tp.OldCh = oldch tp.OldCh = oldch
tp.Pack = pp tp.Pack = pp
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.Items.Sticks[WandPolymorph].Know = true g.Items.Sticks[WandPolymorph].Know = true
@@ -85,15 +110,17 @@ func (g *RogueGame) doZap() {
case WandCancellation: case WandCancellation:
tp.Flags.Set(Cancelled) tp.Flags.Set(Cancelled)
tp.Flags.Clear(Invisible | CanConfuse) tp.Flags.Clear(Invisible | CanConfuse)
tp.Disguise = tp.Type tp.Disguise = tp.Type
if g.seeMonst(tp) { if g.seeMonst(tp) {
g.mvaddch(y, x, tp.Disguise) g.mvaddch(y, x, tp.Disguise)
} }
case WandTeleportAway, WandTeleportTo: case WandTeleportAway, WandTeleportTo:
var newPos Coord var newPos Coord
if obj.WandKind() == WandTeleportAway { if obj.WandKind() == WandTeleportAway {
for { for {
newPos, _ = g.findFloor(nil, 0, true) newPos, _ = g.findFloor(true)
if newPos != p.Pos { if newPos != p.Pos {
break break
} }
@@ -102,6 +129,7 @@ func (g *RogueGame) doZap() {
newPos.Y = p.Pos.Y + g.Delta.Y newPos.Y = p.Pos.Y + g.Delta.Y
newPos.X = p.Pos.X + g.Delta.X newPos.X = p.Pos.X + g.Delta.X
} }
tp.Dest = &p.Pos tp.Dest = &p.Pos
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
g.relocate(tp, newPos) g.relocate(tp, newPos)
@@ -114,26 +142,31 @@ func (g *RogueGame) doZap() {
bolt.HurlDmg = dice("1x4") bolt.HurlDmg = dice("1x4")
bolt.HPlus = 100 bolt.HPlus = 100
bolt.DPlus = 1 bolt.DPlus = 1
bolt.Flags = Missile bolt.Flags = Missile
if p.CurWeapon != nil { if p.CurWeapon != nil {
bolt.Launch = WeaponKind(p.CurWeapon.Which) bolt.Launch = WeaponKind(p.CurWeapon.Which)
} }
g.doMotion(bolt, g.Delta.Y, g.Delta.X) g.doMotion(bolt, g.Delta.Y, g.Delta.X)
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil && if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
!g.saveThrow(VsMagic, &tp.Stats) { !g.saveThrow(VsMagic, &tp.Stats) {
g.hitMonster(bolt.Pos, bolt) g.hitMonster(bolt.Pos, bolt)
} else if g.Options.Terse { } else if g.Options.Terse {
g.msg("missle vanishes") g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
} else { } 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: case WandHasteMonster, WandSlowMonster:
y := p.Pos.Y y := p.Pos.Y
x := p.Pos.X x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) { for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y y += g.Delta.Y
x += g.Delta.X x += g.Delta.X
} }
if tp := g.Level.MonsterAt(y, x); tp != nil { if tp := g.Level.MonsterAt(y, x); tp != nil {
if obj.WandKind() == WandHasteMonster { if obj.WandKind() == WandHasteMonster {
if tp.On(Slowed) { if tp.On(Slowed) {
@@ -147,14 +180,17 @@ func (g *RogueGame) doZap() {
} else { } else {
tp.Flags.Set(Slowed) tp.Flags.Set(Slowed)
} }
tp.Turn = true tp.Turn = true
} }
g.Delta.Y = y g.Delta.Y = y
g.Delta.X = x g.Delta.X = x
g.runto(g.Delta) g.runTo(g.Delta)
} }
case WandLightning, WandFire, WandCold: case WandLightning, WandFire, WandCold:
var name string var name string
switch obj.WandKind() { switch obj.WandKind() {
case WandLightning: case WandLightning:
name = "bolt" name = "bolt"
@@ -163,10 +199,12 @@ func (g *RogueGame) doZap() {
default: default:
name = "ice" name = "ice"
} }
g.fireBolt(p.Pos, &g.Delta, name) g.fireBolt(p.Pos, &g.Delta, name)
g.Items.Sticks[obj.Which].Know = true g.Items.Sticks[obj.Which].Know = true
case WandNothing: case WandNothing:
} }
obj.Charges-- obj.Charges--
} }
@@ -178,8 +216,11 @@ func (g *RogueGame) drain() {
if g.Level.Char(p.Pos.Y, p.Pos.X) == Door { 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] corp = &g.Level.Passages[*g.Level.FlagsAt(p.Pos.Y, p.Pos.X)&FPassNum]
} }
inpass := p.Room.Flags.Has(Gone) inpass := p.Room.Flags.Has(Gone)
var drainee []*Monster var drainee []*Monster
for _, mp := range g.Level.Monsters { for _, mp := range g.Level.Monsters {
if mp.Room == p.Room || mp.Room == corp || if mp.Room == p.Room || mp.Room == corp ||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door && (inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
@@ -187,11 +228,14 @@ func (g *RogueGame) drain() {
drainee = append(drainee, mp) drainee = append(drainee, mp)
} }
} }
cnt := len(drainee) cnt := len(drainee)
if cnt == 0 { if cnt == 0 {
g.msg("you have a tingling feeling") g.msg("you have a tingling feeling")
return return
} }
p.Stats.HP /= 2 p.Stats.HP /= 2
cnt = p.Stats.HP / cnt cnt = p.Stats.HP / cnt
// Now zot all of the monsters // Now zot all of the monsters
@@ -199,7 +243,7 @@ func (g *RogueGame) drain() {
if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 { if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 {
g.killed(mp, g.seeMonst(mp)) g.killed(mp, g.seeMonst(mp))
} else { } else {
g.runto(mp.Pos) g.runTo(mp.Pos)
} }
} }
} }
@@ -217,7 +261,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
bolt.HPlus = 100 bolt.HPlus = 100
bolt.DPlus = 0 bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name g.Items.Weapons[WeaponFlame].Name = name
var dirch byte var dirch byte
switch dir.Y + dir.X { switch dir.Y + dir.X {
case 0: case 0:
dirch = '/' dirch = '/'
@@ -230,10 +276,12 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
case 2, -2: case 2, -2:
dirch = '\\' dirch = '\\'
} }
pos := start pos := start
hitHero := !fromHero hitHero := !fromHero
used := false used := false
changed := false changed := false
var spotpos []Coord var spotpos []Coord
for len(spotpos) < BoltLength && !used { for len(spotpos) < BoltLength && !used {
pos.Y += dir.Y pos.Y += dir.Y
@@ -241,6 +289,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
spotpos = append(spotpos, pos) spotpos = append(spotpos, pos)
ch := g.Level.VisibleChar(pos.Y, pos.X) ch := g.Level.VisibleChar(pos.Y, pos.X)
bounce := false bounce := false
switch ch { switch ch {
case Door: case Door:
// this code is necessary if the hero is on a door and he // this code is necessary if the hero is on a door and he
@@ -252,37 +301,47 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
case '|', '-', ' ': case '|', '-', ' ':
bounce = true bounce = true
} }
if bounce { if bounce {
if !changed { if !changed {
hitHero = !hitHero hitHero = !hitHero
} }
changed = false changed = false
dir.Y = -dir.Y dir.Y = -dir.Y
dir.X = -dir.X dir.X = -dir.X
spotpos = spotpos[:len(spotpos)-1] spotpos = spotpos[:len(spotpos)-1]
g.msg("the %s bounces", name) g.msg("the %s bounces", name)
continue continue
} }
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil { if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
hitHero = true hitHero = true
changed = !changed changed = !changed
tp.OldCh = g.Level.Char(pos.Y, pos.X) tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) { if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos bolt.Pos = pos
used = true used = true
if tp.Type == 'D' && name == "flame" { if tp.Type == 'D' && name == "flame" {
g.addmsg("the flame bounces") g.addmsgf("the flame bounces")
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg(" off the dragon") g.addmsgf(" off the dragon")
} }
g.endmsg() g.endmsg()
} else { } else {
g.hitMonster(pos, bolt) g.hitMonster(pos, bolt)
} }
} else if ch != 'M' || tp.Disguise == 'M' { } else if ch != 'M' || tp.Disguise == 'M' {
if fromHero { if fromHero {
g.runto(pos) g.runTo(pos)
} }
if g.Options.Terse { if g.Options.Terse {
g.msg("%s misses", name) g.msg("%s misses", name)
} else { } else {
@@ -292,6 +351,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
} else if hitHero && pos == p.Pos { } else if hitHero && pos == p.Pos {
hitHero = false hitHero = false
changed = !changed changed = !changed
if !g.save(VsMagic) { if !g.save(VsMagic) {
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 { if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
if fromHero { if fromHero {
@@ -300,7 +360,9 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
g.death(g.Level.MonsterAt(start.Y, start.X).Type) g.death(g.Level.MonsterAt(start.Y, start.X).Type)
} }
} }
used = true used = true
if g.Options.Terse { if g.Options.Terse {
g.msg("the %s hits", name) g.msg("the %s hits", name)
} else { } else {
@@ -310,6 +372,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
g.msg("the %s whizzes by you", name) g.msg("the %s whizzes by you", name)
} }
} }
g.mvaddch(pos.Y, pos.X, dirch) g.mvaddch(pos.Y, pos.X, dirch)
g.refresh() g.refresh()
} }
@@ -321,11 +384,12 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
// fixStick sets up a new wand or staff (sticks.c fix_stick). // fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) { func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" { if g.Items.WandType[cur.Which] == staffName {
cur.Damage = dice("2x3") cur.Damage = dice("2x3")
} else { } else {
cur.Damage = dice("1x1") cur.Damage = dice("1x1")
} }
cur.HurlDmg = dice("1x1") cur.HurlDmg = dice("1x1")
switch cur.WandKind() { switch cur.WandKind() {
@@ -342,8 +406,10 @@ func chargeStr(g *RogueGame, obj *Object) string {
if !obj.Flags.Has(Known) { if !obj.Flags.Has(Known) {
return "" return ""
} }
if g.Options.Terse { if g.Options.Terse {
return fmt.Sprintf(" [%d]", obj.Charges) return fmt.Sprintf(" [%d]", obj.Charges)
} }
return fmt.Sprintf(" [%d charges]", obj.Charges) return fmt.Sprintf(" [%d charges]", obj.Charges)
} }

View File

@@ -1,247 +1,11 @@
package game package game
// This file is the immutable data from extern.c and init.c: tables that are // tables.go — the static data tables the C game kept as file-scope globals
// never written after program start. Per-game mutable copies (the ObjInfo // (extern.c, init.c, and assorted per-file statics). The port gathers them
// tables, whose probabilities are re-summed and whose Know/Guess fields // into gameData: every RogueGame carries its own copy, so the package holds
// change during play) are cloned into RogueGame.Items by NewGame. // no package-level state. Per-game mutable copies (the ObjInfo tables,
// whose probabilities are re-summed and whose Know/Guess fields change
// initStats is the C INIT_STATS: the player's starting statistics. // during play) are cloned into RogueGame.Items by NewGame.
var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12}
// aClass is extern.c a_class[]: armor class for each armor type.
var aClass = [NumArmorTypes]int{
8, // LEATHER
7, // RING_MAIL
7, // STUDDED_LEATHER
6, // SCALE_MAIL
5, // CHAIN_MAIL
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{
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{
"a trapdoor",
"an arrow trap",
"a sleeping gas trap",
"a beartrap",
"a teleport trap",
"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"}
// 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{
/* 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}},
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}},
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}},
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}},
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, dice("%%%x0"), 0}},
{"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, dice("4x3/3x5"), 0}},
{"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}},
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}},
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}},
{"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}},
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}},
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}},
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}},
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}},
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}},
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}},
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}},
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}},
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, dice("1x8/1x8/2x6"), 0}},
{"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, dice("1x9/1x9/2x9"), 0}},
{"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}},
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}},
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
{"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{
{Prob: 26}, // potion
{Prob: 36}, // scroll
{Prob: 16}, // food
{Prob: 7}, // weapon
{Prob: 7}, // armor
{Prob: 4}, // ring
{Prob: 4}, // stick
}
var 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},
{Name: "scale mail", Prob: 13, Worth: 30},
{Name: "chain mail", Prob: 12, Worth: 75},
{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{
{Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", Prob: 8, Worth: 5},
{Name: "poison", Prob: 8, Worth: 5},
{Name: "gain strength", Prob: 13, Worth: 150},
{Name: "see invisible", Prob: 3, Worth: 100},
{Name: "healing", Prob: 13, Worth: 130},
{Name: "monster detection", Prob: 6, Worth: 130},
{Name: "magic detection", Prob: 6, Worth: 105},
{Name: "raise level", Prob: 2, Worth: 250},
{Name: "extra healing", Prob: 5, Worth: 200},
{Name: "haste self", Prob: 5, Worth: 190},
{Name: "restore strength", Prob: 13, Worth: 130},
{Name: "blindness", Prob: 5, Worth: 5},
{Name: "levitation", Prob: 6, Worth: 75},
}
var baseRingInfo = [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", Prob: 9, Worth: 400},
{Name: "sustain strength", Prob: 5, Worth: 280},
{Name: "searching", Prob: 10, Worth: 420},
{Name: "see invisible", Prob: 10, Worth: 310},
{Name: "adornment", Prob: 1, Worth: 10},
{Name: "aggravate monster", Prob: 10, Worth: 10},
{Name: "dexterity", Prob: 8, Worth: 440},
{Name: "increase damage", Prob: 8, Worth: 400},
{Name: "regeneration", Prob: 4, Worth: 460},
{Name: "slow digestion", Prob: 9, Worth: 240},
{Name: "teleportation", Prob: 5, Worth: 30},
{Name: "stealth", Prob: 7, Worth: 470},
{Name: "maintain armor", Prob: 5, Worth: 380},
}
var baseScrInfo = [NumScrollTypes]ObjInfo{
{Name: "monster confusion", Prob: 7, Worth: 140},
{Name: "magic mapping", Prob: 4, Worth: 150},
{Name: "hold monster", Prob: 2, Worth: 180},
{Name: "sleep", Prob: 3, Worth: 5},
{Name: "enchant armor", Prob: 7, Worth: 160},
{Name: "identify potion", Prob: 10, Worth: 80},
{Name: "identify scroll", Prob: 10, Worth: 80},
{Name: "identify weapon", Prob: 6, Worth: 80},
{Name: "identify armor", Prob: 7, Worth: 100},
{Name: "identify ring, wand or staff", Prob: 10, Worth: 115},
{Name: "scare monster", Prob: 3, Worth: 200},
{Name: "food detection", Prob: 2, Worth: 60},
{Name: "teleportation", Prob: 5, Worth: 165},
{Name: "enchant weapon", Prob: 8, Worth: 150},
{Name: "create monster", Prob: 4, Worth: 75},
{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{
{Name: "mace", Prob: 11, Worth: 8},
{Name: "long sword", Prob: 11, Worth: 15},
{Name: "short bow", Prob: 12, Worth: 15},
{Name: "arrow", Prob: 12, Worth: 1},
{Name: "dagger", Prob: 8, Worth: 3},
{Name: "two handed sword", Prob: 10, Worth: 75},
{Name: "dart", Prob: 12, Worth: 2},
{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{
{Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5},
{Name: "lightning", Prob: 3, Worth: 330},
{Name: "fire", Prob: 3, Worth: 330},
{Name: "cold", Prob: 3, Worth: 330},
{Name: "polymorph", Prob: 15, Worth: 310},
{Name: "magic missile", Prob: 10, Worth: 170},
{Name: "haste monster", Prob: 10, Worth: 5},
{Name: "slow monster", Prob: 11, Worth: 350},
{Name: "drain life", Prob: 9, Worth: 300},
{Name: "nothing", Prob: 1, Worth: 5},
{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{
"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{
"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",
"eng", "er", "ere", "erk", "esh", "evs", "fa", "fid", "fri", "fu",
"gan", "gar", "glen", "gop", "gre", "ha", "hyd", "i", "ing", "ip",
"ish", "it", "ite", "iv", "jo", "kho", "kli", "klis", "la", "lech",
"mar", "me", "mi", "mic", "mik", "mon", "mung", "mur", "nej",
"nelg", "nep", "ner", "nes", "nes", "nih", "nin", "o", "od", "ood",
"org", "orn", "ox", "oxy", "pay", "ple", "plu", "po", "pot",
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
"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{
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
{"germanium", 225}, {"granite", 5}, {"garnet", 50},
{"jade", 150}, {"kryptonite", 300}, {"lapis lazuli", 50},
{"moonstone", 50}, {"obsidian", 15}, {"onyx", 60},
{"opal", 200}, {"pearl", 220}, {"peridot", 63},
{"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{
"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{
"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. // helpEntry is rogue.h struct h_list.
type helpEntry struct { type helpEntry struct {
@@ -250,73 +14,580 @@ type helpEntry struct {
Print bool Print bool
} }
// helpStr is extern.c helpstr[]: the '?' command help text. // gameData is the bundle of static tables, built by newGameData and hung on
var helpStr = []helpEntry{ // RogueGame as g.data. Nothing in it mutates during play: the one table the
{'?', " prints help", true}, // C code writes to (the venus flytrap damage hack) operates on the game's
{'/', " identify object", true}, // Monsters copy, not on this template.
{'h', " left", true}, type gameData struct {
{'j', " down", true}, // initStats is the C INIT_STATS: the player's starting statistics.
{'k', " up", true}, initStats Stats
{'l', " right", true},
{'y', " up & left", true}, // aClass is extern.c a_class[]: armor class for each armor type.
{'u', " up & right", true}, aClass [NumArmorTypes]int
{'b', " down & left", true},
{'n', " down & right", true}, // eLevels is extern.c e_levels[]: experience thresholds per level; the
{'H', " run left", false}, // zero terminates the table as in C.
{'J', " run down", false}, eLevels []int
{'K', " run up", false},
{'L', " run right", false}, // trName is extern.c tr_name[]: names of the traps.
{'Y', " run up & left", false}, trName [NumTrapTypes]string
{'U', " run up & right", false},
{'B', " run down & left", false}, // invTName is extern.c inv_t_name[]: the inventory style names.
{'N', " run down & right", false}, invTName []string
{CTRL('H'), " run left until adjacent", false},
{CTRL('J'), " run down until adjacent", false}, // monsterTable is extern.c monsters[26]: all monster kinds, indexed by
{CTRL('K'), " run up until adjacent", false}, // letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C)
{CTRL('L'), " run right until adjacent", false}, // is rolled from the level at creation time.
{CTRL('Y'), " run up & left until adjacent", false}, monsterTable [26]MonsterKind
{CTRL('U'), " run up & right until adjacent", false},
{CTRL('B'), " run down & left until adjacent", false}, // Base ObjInfo tables (extern.c). These are templates: NewGame copies
{CTRL('N'), " run down & right until adjacent", false}, // them into ItemLore before initProbs converts Prob to cumulative form.
{0, " <SHIFT><dir>: run that way", true}, baseThings [NumThings]ObjInfo
{0, " <CTRL><dir>: run till adjacent", true}, baseArmInfo [NumArmorTypes]ObjInfo
{'f', "<dir> fight till death or near death", true}, basePotInfo [NumPotionTypes]ObjInfo
{'t', "<dir> throw something", true}, baseRingInfo [NumRingTypes]ObjInfo
{'m', "<dir> move onto without picking up", true}, baseScrInfo [NumScrollTypes]ObjInfo
{'z', "<dir> zap a wand in a direction", true}, baseWeapInfo [NumWeaponTypes + 1]ObjInfo
{'^', "<dir> identify trap type", true}, baseWsInfo [NumWandTypes]ObjInfo
{'s', " search for trap/secret door", true},
{'>', " go down a staircase", true}, // rainbow is init.c rainbow[]: the possible potion colors.
{'<', " go up a staircase", true}, rainbow []string
{'.', " rest for a turn", true},
{',', " pick something up", true}, // sylls is init.c sylls[]: syllables for generated scroll names.
{'i', " inventory", true}, sylls []string
{'I', " inventory single item", true},
{'q', " quaff potion", true}, // stoneTable is init.c stones[]: ring stones and their worth.
{'r', " read scroll", true}, stoneTable []Stone
{'e', " eat food", true},
{'w', " wield a weapon", true}, // woods is init.c wood[]: what staffs are made of.
{'W', " wear armor", true}, woods []string
{'T', " take armor off", true},
{'P', " put on ring", true}, // metals is init.c metal[]: what wands are made of.
{'R', " remove ring", true}, metals []string
{'d', " drop object", true},
{'c', " call object", true}, // helpStr is extern.c helpstr[]: the '?' command help text.
{'a', " repeat last command", true}, helpStr []helpEntry
{')', " print current weapon", true},
{']', " print current armor", true}, // hNames are the strings for hitting; the first four are used when the
{'=', " print current rings", true}, // player strikes, the second four for monsters (fight.c h_names).
{'@', " print current stats", true}, hNames [8]string
{'D', " recall what's been discovered", true},
{'o', " examine/set options", true}, // mNames are the strings for missing (fight.c m_names).
{CTRL('R'), " redraw screen", true}, mNames [8]string
{CTRL('P'), " repeat last message", true},
{Escape, " cancel command", true}, // strPlus adjusts hit probabilities due to strength (fight.c str_plus).
{'S', " save game", true}, strPlus [32]int
{'Q', " quit", true},
{'!', " shell escape", true}, // addDam adjusts damage done due to strength (fight.c add_dam).
{'F', "<dir> fight till either of you dies", true}, addDam [32]int
{'v', " print version number", true},
// 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
}
// 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
6, // SCALE_MAIL
5, // CHAIN_MAIL
4, // SPLINT_MAIL
4, // BANDED_MAIL
3, // PLATE_MAIL
},
eLevels: []int{
10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000,
50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0,
},
trName: [NumTrapTypes]string{
"a trapdoor",
"an arrow trap",
"a sleeping gas trap",
"a beartrap",
"a teleport trap",
"a poison dart trap",
"a rust trap",
"a mysterious trap",
},
invTName: []string{"Overwrite", "Slow", "Clear"},
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}},
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}},
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}},
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}},
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, dice("%%%x0"), 0}},
{"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, dice("4x3/3x5"), 0}},
{"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}},
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}},
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}},
{"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}},
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}},
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}},
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}},
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}},
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}},
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}},
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}},
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}},
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, dice("1x8/1x8/2x6"), 0}},
{"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, dice("1x9/1x9/2x9"), 0}},
{"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}},
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}},
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
},
baseThings: [NumThings]ObjInfo{
{Prob: 26}, // potion
{Prob: 36}, // scroll
{Prob: 16}, // food
{Prob: 7}, // weapon
{Prob: 7}, // armor
{Prob: 4}, // ring
{Prob: 4}, // stick
},
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},
{Name: "scale mail", Prob: 13, Worth: 30},
{Name: "chain mail", Prob: 12, Worth: 75},
{Name: "splint mail", Prob: 10, Worth: 80},
{Name: "banded mail", Prob: 10, Worth: 90},
{Name: "plate mail", Prob: 5, Worth: 150},
},
basePotInfo: [NumPotionTypes]ObjInfo{
{Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", Prob: 8, Worth: 5},
{Name: "poison", Prob: 8, Worth: 5},
{Name: "gain strength", Prob: 13, Worth: 150},
{Name: "see invisible", Prob: 3, Worth: 100},
{Name: "healing", Prob: 13, Worth: 130},
{Name: "monster detection", Prob: 6, Worth: 130},
{Name: "magic detection", Prob: 6, Worth: 105},
{Name: "raise level", Prob: 2, Worth: 250},
{Name: "extra healing", Prob: 5, Worth: 200},
{Name: "haste self", Prob: 5, Worth: 190},
{Name: "restore strength", Prob: 13, Worth: 130},
{Name: "blindness", Prob: 5, Worth: 5},
{Name: "levitation", Prob: 6, Worth: 75},
},
baseRingInfo: [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", Prob: 9, Worth: 400},
{Name: "sustain strength", Prob: 5, Worth: 280},
{Name: "searching", Prob: 10, Worth: 420},
{Name: "see invisible", Prob: 10, Worth: 310},
{Name: "adornment", Prob: 1, Worth: 10},
{Name: "aggravate monster", Prob: 10, Worth: 10},
{Name: "dexterity", Prob: 8, Worth: 440},
{Name: "increase damage", Prob: 8, Worth: 400},
{Name: "regeneration", Prob: 4, Worth: 460},
{Name: "slow digestion", Prob: 9, Worth: 240},
{Name: "teleportation", Prob: 5, Worth: 30},
{Name: "stealth", Prob: 7, Worth: 470},
{Name: "maintain armor", Prob: 5, Worth: 380},
},
baseScrInfo: [NumScrollTypes]ObjInfo{
{Name: "monster confusion", Prob: 7, Worth: 140},
{Name: "magic mapping", Prob: 4, Worth: 150},
{Name: "hold monster", Prob: 2, Worth: 180},
{Name: "sleep", Prob: 3, Worth: 5},
{Name: "enchant armor", Prob: 7, Worth: 160},
{Name: "identify potion", Prob: 10, Worth: 80},
{Name: "identify scroll", Prob: 10, Worth: 80},
{Name: "identify weapon", Prob: 6, Worth: 80},
{Name: "identify armor", Prob: 7, Worth: 100},
{Name: "identify ring, wand or staff", Prob: 10, Worth: 115},
{Name: "scare monster", Prob: 3, Worth: 200},
{Name: "food detection", Prob: 2, Worth: 60},
{Name: "teleportation", Prob: 5, Worth: 165},
{Name: "enchant weapon", Prob: 8, Worth: 150},
{Name: "create monster", Prob: 4, Worth: 75},
{Name: "remove curse", Prob: 7, Worth: 105},
{Name: "aggravate monsters", Prob: 3, Worth: 20},
{Name: "protect armor", Prob: 2, Worth: 250},
},
baseWeapInfo: [NumWeaponTypes + 1]ObjInfo{
{Name: "mace", Prob: 11, Worth: 8},
{Name: "long sword", Prob: 11, Worth: 15},
{Name: "short bow", Prob: 12, Worth: 15},
{Name: "arrow", Prob: 12, Worth: 1},
{Name: "dagger", Prob: 8, Worth: 3},
{Name: "two handed sword", Prob: 10, Worth: 75},
{Name: "dart", Prob: 12, Worth: 2},
{Name: "shuriken", Prob: 12, Worth: 5},
{Name: "spear", Prob: 12, Worth: 5},
{}, // DO NOT REMOVE: fake entry for dragon's breath
},
baseWsInfo: [NumWandTypes]ObjInfo{
{Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5},
{Name: "lightning", Prob: 3, Worth: 330},
{Name: "fire", Prob: 3, Worth: 330},
{Name: "cold", Prob: 3, Worth: 330},
{Name: "polymorph", Prob: 15, Worth: 310},
{Name: "magic missile", Prob: 10, Worth: 170},
{Name: "haste monster", Prob: 10, Worth: 5},
{Name: "slow monster", Prob: 11, Worth: 350},
{Name: "drain life", Prob: 9, Worth: 300},
{Name: "nothing", Prob: 1, Worth: 5},
{Name: "teleport away", Prob: 6, Worth: 340},
{Name: "teleport to", Prob: 6, Worth: 50},
{Name: "cancellation", Prob: 5, Worth: 280},
},
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",
},
//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",
"eng", "er", "ere", "erk", "esh", "evs", "fa", "fid", "fri", "fu",
"gan", "gar", "glen", "gop", "gre", "ha", "hyd", "i", "ing", "ip",
"ish", "it", "ite", "iv", "jo", "kho", "kli", "klis", "la", "lech",
"mar", "me", "mi", "mic", "mik", "mon", "mung", "mur", "nej",
"nelg", "nep", "ner", "nes", "nes", "nih", "nin", "o", "od", "ood",
"org", "orn", "ox", "oxy", "pay", "ple", "plu", "po", "pot",
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
"zok", "zon", "zum",
},
stoneTable: []Stone{
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
{"germanium", 225}, {"granite", 5}, {"garnet", 50},
{"jade", 150}, {"kryptonite", 300}, {"lapis lazuli", 50},
{"moonstone", 50}, {"obsidian", 15}, {"onyx", 60},
{"opal", 200}, {"pearl", 220}, {"peridot", 63},
{"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200},
{"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70},
{"taaffeite", 300}, {"zircon", 80},
},
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: []string{
"aluminum", "beryllium", "bone", "brass", "bronze", "copper",
"electrum", "gold", "iron", "lead", "magnesium", "mercury",
"nickel", "pewter", "platinum", "steel", "silver", "silicon",
"tin", "titanium", "tungsten", "zinc",
},
helpStr: []helpEntry{
{'?', " prints help", true},
{'/', " identify object", true},
{'h', " left", true},
{'j', " down", true},
{'k', " up", true},
{'l', " right", true},
{'y', " up & left", true},
{'u', " up & right", true},
{'b', " down & left", true},
{'n', " down & right", true},
{'H', " run left", false},
{'J', " run down", false},
{'K', " run up", false},
{'L', " run right", false},
{'Y', " run up & left", false},
{'U', " run up & right", false},
{'B', " run down & left", false},
{'N', " run down & right", false},
{CTRL('H'), " run left until adjacent", false},
{CTRL('J'), " run down until adjacent", false},
{CTRL('K'), " run up until adjacent", false},
{CTRL('L'), " run right until adjacent", false},
{CTRL('Y'), " run up & left until adjacent", false},
{CTRL('U'), " run up & right until adjacent", false},
{CTRL('B'), " run down & left until adjacent", false},
{CTRL('N'), " run down & right until adjacent", false},
{0, " <SHIFT><dir>: run that way", true},
{0, " <CTRL><dir>: run till adjacent", true},
{'f', "<dir> fight till death or near death", true},
{'t', "<dir> throw something", true},
{'m', "<dir> move onto without picking up", true},
{'z', "<dir> zap a wand in a direction", true},
{'^', "<dir> identify trap type", true},
{'s', " search for trap/secret door", true},
{'>', " go down a staircase", true},
{'<', " go up a staircase", true},
{'.', " rest for a turn", true},
{',', " pick something up", true},
{'i', " inventory", true},
{'I', " inventory single item", true},
{'q', " quaff potion", true},
{'r', " read scroll", true},
{'e', " eat food", true},
{'w', " wield a weapon", true},
{'W', " wear armor", true},
{'T', " take armor off", true},
{'P', " put on ring", true},
{'R', " remove ring", true},
{'d', " drop object", true},
{'c', " call object", true},
{'a', " repeat last command", true},
{')', " print current weapon", true},
{']', " print current armor", true},
{'=', " print current rings", true},
{'@', " print current stats", true},
{'D', " recall what's been discovered", true},
{'o', " examine/set options", true},
{CTRL('R'), " redraw screen", true},
{CTRL('P'), " repeat last message", true},
{Escape, " cancel command", true},
{'S', " save game", true},
{'Q', " quit", true},
{'!', " 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",
},
}
} }
// Version strings (vers.c). The encstr/statlist XOR keys are not ported: // Version strings (vers.c). The encstr/statlist XOR keys are not ported:

View File

@@ -9,16 +9,20 @@ func TestProbabilitiesSumTo100(t *testing.T) {
for _, oi := range info { for _, oi := range info {
s += oi.Prob s += oi.Prob
} }
return s return s
} }
data := newGameData()
tables := map[string][]ObjInfo{ tables := map[string][]ObjInfo{
"things": baseThings[:], "things": data.baseThings[:],
"potions": basePotInfo[:], "potions": data.basePotInfo[:],
"scrolls": baseScrInfo[:], "scrolls": data.baseScrInfo[:],
"rings": baseRingInfo[:], "rings": data.baseRingInfo[:],
"sticks": baseWsInfo[:], "sticks": data.baseWsInfo[:],
"weapons": baseWeapInfo[:NumWeaponTypes], // excludes the flame entry "weapons": data.baseWeapInfo[:NumWeaponTypes], // excludes the flame entry
"armor": baseArmInfo[:], "armor": data.baseArmInfo[:],
} }
for name, tab := range tables { for name, tab := range tables {
if s := sum(tab); s != 100 { if s := sum(tab); s != 100 {
@@ -29,10 +33,12 @@ func TestProbabilitiesSumTo100(t *testing.T) {
func TestInitProbsCumulative(t *testing.T) { func TestInitProbsCumulative(t *testing.T) {
g := NewGame(Config{Seed: 1}) g := NewGame(Config{Seed: 1})
last := g.Items.Potions[NumPotionTypes-1].Prob last := g.Items.Potions[NumPotionTypes-1].Prob
if last != 100 { if last != 100 {
t.Errorf("cumulative potion probability ends at %d, want 100", last) t.Errorf("cumulative potion probability ends at %d, want 100", last)
} }
for i := PotionKind(1); i < NumPotionTypes; i++ { for i := PotionKind(1); i < NumPotionTypes; i++ {
if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob { if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob {
t.Errorf("potion probs not nondecreasing at %d", i) t.Errorf("potion probs not nondecreasing at %d", i)
@@ -43,27 +49,34 @@ func TestInitProbsCumulative(t *testing.T) {
func TestNewGameRandomizesAppearances(t *testing.T) { func TestNewGameRandomizesAppearances(t *testing.T) {
g := NewGame(Config{Seed: 12345}) g := NewGame(Config{Seed: 12345})
seen := map[string]bool{} seen := map[string]bool{}
for i, c := range g.Items.PotColors { for i, c := range g.Items.PotColors {
if c == "" { if c == "" {
t.Fatalf("potion %d has no color", i) t.Fatalf("potion %d has no color", i)
} }
if seen[c] { if seen[c] {
t.Errorf("potion color %q assigned twice", c) t.Errorf("potion color %q assigned twice", c)
} }
seen[c] = true seen[c] = true
} }
for i, n := range g.Items.ScrNames { for i, n := range g.Items.ScrNames {
if n == "" { if n == "" {
t.Fatalf("scroll %d has no name", i) t.Fatalf("scroll %d has no name", i)
} }
if len(n) > MaxNameLen+1 { if len(n) > MaxNameLen+1 {
t.Errorf("scroll name %q longer than C buffer allows", n) t.Errorf("scroll name %q longer than C buffer allows", n)
} }
} }
for i := range g.Items.WandType { for i := range g.Items.WandType {
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" { if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" {
t.Errorf("stick %d has type %q", i, g.Items.WandType[i]) t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
} }
if g.Items.WandMade[i] == "" { if g.Items.WandMade[i] == "" {
t.Errorf("stick %d has no material", i) t.Errorf("stick %d has no material", i)
} }
@@ -77,10 +90,12 @@ func TestNewGameRandomizesAppearances(t *testing.T) {
} }
func TestMonsterTable(t *testing.T) { func TestMonsterTable(t *testing.T) {
if monsterTable[0].Name != "aquator" || monsterTable[25].Name != "zombie" { data := newGameData()
if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" {
t.Error("monster table order broken") t.Error("monster table order broken")
} }
if monsterTable['D'-'A'].Name != "dragon" {
if data.monsterTable['D'-'A'].Name != "dragon" {
t.Error("letter indexing broken") t.Error("letter indexing broken")
} }
} }

View File

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

View File

@@ -9,15 +9,17 @@ import (
// invName returns the name of something as it would appear in an inventory // invName returns the name of something as it would appear in an inventory
// (things.c inv_name). // (things.c inv_name).
func (g *RogueGame) invName(obj *Object, drop bool) string { func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
var pb strings.Builder var pb strings.Builder
which := obj.Which which := obj.Which
it := &g.Items it := &g.Items
switch obj.Kind { switch obj.Kind {
case KindPotion: case KindPotion:
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr) g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr)
case KindRing: case KindRing:
g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum) g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
case KindWand: case KindWand:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr) g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case KindScroll: case KindScroll:
@@ -26,12 +28,15 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
} else { } else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count) fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
} }
op := &it.Scrolls[which] op := &it.Scrolls[which]
if op.Know {
switch {
case op.Know:
fmt.Fprintf(&pb, "of %s", op.Name) fmt.Fprintf(&pb, "of %s", op.Name)
} else if op.Guess != "" { case op.Guess != "":
fmt.Fprintf(&pb, "called %s", op.Guess) fmt.Fprintf(&pb, "called %s", op.Guess)
} else { default:
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which]) fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
} }
case KindFood: case KindFood:
@@ -50,33 +55,40 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
} }
case KindWeapon: case KindWeapon:
sp := it.Weapons[which].Name sp := it.Weapons[which].Name
if obj.Count > 1 { if obj.Count > 1 {
fmt.Fprintf(&pb, "%d ", obj.Count) fmt.Fprintf(&pb, "%d ", obj.Count)
} else { } else {
fmt.Fprintf(&pb, "A%s ", vowelstr(sp)) fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
} }
if obj.Flags.Has(Known) { if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp) fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else { } else {
pb.WriteString(sp) pb.WriteString(sp)
} }
if obj.Count > 1 { if obj.Count > 1 {
pb.WriteString("s") pb.WriteString("s")
} }
if obj.Label != "" { if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label) fmt.Fprintf(&pb, " called %s", obj.Label)
} }
case KindArmor: case KindArmor:
sp := it.Armors[which].Name sp := it.Armors[which].Name
if obj.Flags.Has(Known) { if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp) fmt.Fprintf(&pb, "%s %s [", num(g.data.aClass[which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse { if !g.Options.Terse {
pb.WriteString("protection ") pb.WriteString("protection ")
} }
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass) fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
} else { } else {
pb.WriteString(sp) pb.WriteString(sp)
} }
if obj.Label != "" { if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label) fmt.Fprintf(&pb, " called %s", obj.Label)
} }
@@ -87,20 +99,25 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
} }
out := pb.String() out := pb.String()
if g.InvDescribe { if g.InvDescribe {
p := &g.Player p := &g.Player
if obj == p.CurArmor { if obj == p.CurArmor {
out += " (being worn)" out += " (being worn)"
} }
if obj == p.CurWeapon { if obj == p.CurWeapon {
out += " (weapon in hand)" out += " (weapon in hand)"
} }
if obj == p.CurRing[Left] {
switch obj {
case p.CurRing[Left]:
out += " (on left hand)" out += " (on left hand)"
} else if obj == p.CurRing[Right] { case p.CurRing[Right]:
out += " (on right hand)" out += " (on right hand)"
} }
} }
if out != "" { if out != "" {
if drop && isUpper(out[0]) { if drop && isUpper(out[0]) {
out = string(toLower(out[0])) + out[1:] out = string(toLower(out[0])) + out[1:]
@@ -108,6 +125,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
out = string(toUpper(out[0])) + out[1:] out = string(toUpper(out[0])) + out[1:]
} }
} }
return out return out
} }
@@ -115,29 +133,36 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
// leavePack/detach vocabulary collision). // leavePack/detach vocabulary collision).
func (g *RogueGame) dropIt() { func (g *RogueGame) dropIt() {
p := &g.Player p := &g.Player
ch := g.Level.Char(p.Pos.Y, p.Pos.X) ch := g.Level.Char(p.Pos.Y, p.Pos.X)
if ch != Floor && ch != Passage { if ch != Floor && ch != Passage {
g.After = false g.After = false
g.msg("there is something there already") g.msg("there is something there already")
return return
} }
obj := g.getItem("drop", KindNone)
if obj == nil { obj, ok := g.promptPackItem("drop", KindNone)
if !ok {
return return
} }
if !g.dropCheck(obj) { if !g.dropCheck(obj) {
return return
} }
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack()) obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
// Link it into the level object list // Link it into the level object list
attachObj(&g.Level.Objects, obj) g.Level.AddObject(obj)
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph()) g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph())
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped) g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
obj.Pos = p.Pos obj.Pos = p.Pos
if obj.Kind == KindAmulet { if obj.Kind == KindAmulet {
g.HasAmulet = false g.HasAmulet = false
} }
g.msg("dropped %s", g.invName(obj, true))
g.msg("dropped %s", g.inventoryName(obj, true))
} }
// dropCheck does special checks for dropping or unwielding|unwearing| // dropCheck does special checks for dropping or unwielding|unwearing|
@@ -146,34 +171,43 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
if obj == nil { if obj == nil {
return true return true
} }
p := &g.Player p := &g.Player
if obj != p.CurArmor && obj != p.CurWeapon && if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] { obj != p.CurRing[Left] && obj != p.CurRing[Right] {
return true return true
} }
if obj.Flags.Has(Cursed) { if obj.Flags.Has(Cursed) {
g.msg("you can't. It appears to be cursed") g.msg("you can't. It appears to be cursed")
return false return false
} }
if obj == p.CurWeapon {
switch obj {
case p.CurWeapon:
p.CurWeapon = nil p.CurWeapon = nil
} else if obj == p.CurArmor { case p.CurArmor:
g.wasteTime() g.wasteTime()
p.CurArmor = nil p.CurArmor = nil
} else { default:
hand := Right hand := Right
if obj == p.CurRing[Left] { if obj == p.CurRing[Left] {
hand = Left hand = Left
} }
p.CurRing[hand] = nil p.CurRing[hand] = nil
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.chgStr(-obj.Bonus) g.changeStrength(-obj.Bonus)
case RingSeeInvisible: case RingSeeInvisible:
g.unsee(0) g.unsee(0)
g.Extinguish(DUnsee) g.Extinguish(DUnsee)
} }
} }
return true return true
} }
@@ -193,6 +227,7 @@ func (g *RogueGame) newThing() *Object {
} else { } else {
kind = pickOne(g, g.Items.Things[:]) kind = pickOne(g, g.Items.Things[:])
} }
switch kind { switch kind {
case 0: case 0:
cur.Kind = KindPotion cur.Kind = KindPotion
@@ -202,6 +237,7 @@ func (g *RogueGame) newThing() *Object {
cur.Which = pickOne(g, g.Items.Scrolls[:]) cur.Which = pickOne(g, g.Items.Scrolls[:])
case 2: case 2:
cur.Kind = KindFood cur.Kind = KindFood
g.Player.NoFood = 0 g.Player.NoFood = 0
if g.rnd(10) != 0 { if g.rnd(10) != 0 {
cur.Which = 0 cur.Which = 0
@@ -210,6 +246,7 @@ func (g *RogueGame) newThing() *Object {
} }
case 3: case 3:
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes]))) g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
if r := g.rnd(100); r < 10 { if r := g.rnd(100); r < 10 {
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
cur.HPlus -= g.rnd(3) + 1 cur.HPlus -= g.rnd(3) + 1
@@ -219,7 +256,8 @@ func (g *RogueGame) newThing() *Object {
case 4: case 4:
cur.Kind = KindArmor cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:]) cur.Which = pickOne(g, g.Items.Armors[:])
cur.ArmorClass = aClass[cur.Which]
cur.ArmorClass = g.data.aClass[cur.Which]
if r := g.rnd(100); r < 20 { if r := g.rnd(100); r < 20 {
cur.Flags.Set(Cursed) cur.Flags.Set(Cursed)
cur.ArmorClass += g.rnd(3) + 1 cur.ArmorClass += g.rnd(3) + 1
@@ -228,6 +266,7 @@ func (g *RogueGame) newThing() *Object {
} }
case 5: case 5:
cur.Kind = KindRing cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:]) cur.Which = pickOne(g, g.Items.Rings[:])
switch cur.RingKind() { switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage: case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
@@ -243,6 +282,7 @@ func (g *RogueGame) newThing() *Object {
cur.Which = pickOne(g, g.Items.Sticks[:]) cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur) g.fixStick(cur)
} }
return cur return cur
} }
@@ -255,6 +295,7 @@ func pickOne(g *RogueGame, info []ObjInfo) int {
return idx return idx
} }
} }
return 0 // bad pick_one: C resets to the start of the table return 0 // bad pick_one: C resets to the start of the table
} }
@@ -272,20 +313,27 @@ type invPage struct {
// (things.c discovered). // (things.c discovered).
func (g *RogueGame) discovered() { func (g *RogueGame) discovered() {
var ch byte var ch byte
for { for {
discList := false discList := false
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("for ") g.addmsgf("for ")
} }
g.addmsg("what type")
g.addmsgf("what type")
if !g.Options.Terse { 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)") g.msg("? (* for all)")
ch = g.readchar() ch = g.readchar()
switch ch { switch ch {
case Escape: case Escape:
g.msg("") g.msg("")
return return
case Potion, Scroll, Ring, Stick, '*': case Potion, Scroll, Ring, Stick, '*':
discList = true discList = true
@@ -297,10 +345,12 @@ func (g *RogueGame) discovered() {
Potion, Scroll, Ring, Stick) Potion, Scroll, Ring, Stick)
} }
} }
if discList { if discList {
break break
} }
} }
if ch == '*' { if ch == '*' {
g.printDisc(Potion) g.printDisc(Potion)
g.addLine("") g.addLine("")
@@ -320,6 +370,7 @@ func (g *RogueGame) discovered() {
// (things.c print_disc). // (things.c print_disc).
func (g *RogueGame) printDisc(typ byte) { func (g *RogueGame) printDisc(typ byte) {
var info []ObjInfo var info []ObjInfo
switch typ { switch typ {
case Scroll: case Scroll:
info = g.Items.Scrolls[:] info = g.Items.Scrolls[:]
@@ -330,18 +381,23 @@ func (g *RogueGame) printDisc(typ byte) {
case Stick: case Stick:
info = g.Items.Sticks[:] info = g.Items.Sticks[:]
} }
order := make([]int, len(info)) order := make([]int, len(info))
g.setOrder(order) g.setOrder(order)
obj := Object{Count: 1} obj := Object{Count: 1}
numFound := 0 numFound := 0
for i := range info { for i := range info {
if info[order[i]].Know || info[order[i]].Guess != "" { if info[order[i]].Know || info[order[i]].Guess != "" {
obj.Kind = objectKindForGlyph(typ) obj.Kind = objectKindForGlyph(typ)
obj.Which = order[i] obj.Which = order[i]
g.addLine("%s", g.invName(&obj, false)) g.addLine("%s", g.inventoryName(&obj, false))
numFound++ numFound++
} }
} }
if numFound == 0 { if numFound == 0 {
g.addLine("%s", g.nothing(typ)) g.addLine("%s", g.nothing(typ))
} }
@@ -353,6 +409,7 @@ func (g *RogueGame) setOrder(order []int) {
for i := range order { for i := range order {
order[i] = i order[i] = i
} }
for i := len(order); i > 0; i-- { for i := len(order); i > 0; i-- {
r := g.rnd(i) r := g.rnd(i)
order[i-1], order[r] = order[r], order[i-1] order[i-1], order[r] = order[r], order[i-1]
@@ -368,6 +425,7 @@ func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage pg := &g.invPage
prompt := "--Press space to continue--" prompt := "--Press space to continue--"
isFlush := format == flushSentinel isFlush := format == flushSentinel
var line string var line string
if !isFlush { if !isFlush {
line = fmt.Sprintf(format, a...) line = fmt.Sprintf(format, a...)
@@ -375,36 +433,43 @@ func (g *RogueGame) addLine(format string, a ...any) int {
if pg.lineCnt == 0 { if pg.lineCnt == 0 {
g.scr.Hw.Clear() g.scr.Hw.Clear()
if g.Options.InvType == InvSlow { if g.Options.InvType == InvSlow {
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
} }
} }
if g.Options.InvType == InvSlow { if g.Options.InvType == InvSlow {
if !isFlush && line != "" { if !isFlush && line != "" {
if g.msg("%s", line) == Escape { if g.msg("%s", line) == Escape {
return Escape return Escape
} }
} }
pg.lineCnt++ pg.lineCnt++
} else { } else {
if !pg.init { if !pg.init {
pg.maxlen = len(prompt) pg.maxlen = len(prompt)
pg.init = true pg.init = true
} }
if pg.lineCnt >= NumLines-1 || isFlush { if pg.lineCnt >= NumLines-1 || isFlush {
if g.Options.InvType == InvOver && isFlush && !pg.newpage { if g.Options.InvType == InvOver && isFlush && !pg.newpage {
// Overlay the accumulated list in a box at the top right // Overlay the accumulated list in a box at the top right
// of the screen, prompt, and restore what was beneath. // of the screen, prompt, and restore what was beneath.
g.msg("") g.msg("")
g.refresh() g.refresh()
saved := NewWindow(NumLines, NumCols) saved := NewWindow(NumLines, NumCols)
saved.CopyFrom(g.scr.Std) saved.CopyFrom(g.scr.Std)
lx := NumCols - pg.maxlen - 2 lx := NumCols - pg.maxlen - 2
for y := 0; y <= pg.lineCnt; y++ { for y := 0; y <= pg.lineCnt; y++ {
for x := 0; x <= pg.maxlen; x++ { for x := 0; x <= pg.maxlen; x++ {
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x)) g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
} }
} }
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt) g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
g.refresh() g.refresh()
g.waitFor(' ') g.waitFor(' ')
@@ -417,19 +482,24 @@ func (g *RogueGame) addLine(format string, a ...any) int {
g.scr.Hw.Clear() g.scr.Hw.Clear()
g.refresh() g.refresh()
} }
pg.newpage = true pg.newpage = true
pg.lineCnt = 0 pg.lineCnt = 0
pg.maxlen = len(prompt) pg.maxlen = len(prompt)
} }
if !isFlush && !(pg.lineCnt == 0 && line == "") {
if !isFlush && (pg.lineCnt != 0 || line != "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line) g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++ pg.lineCnt++
if pg.maxlen < len(line) { if pg.maxlen < len(line) {
pg.maxlen = len(line) pg.maxlen = len(line)
} }
pg.lastLine = line pg.lastLine = line
} }
} }
return ^Escape return ^Escape
} }
@@ -447,6 +517,7 @@ func (g *RogueGame) endLine() {
g.flushLine() g.flushLine()
} }
} }
pg.lineCnt = 0 pg.lineCnt = 0
pg.newpage = false pg.newpage = false
} }
@@ -459,20 +530,24 @@ func (g *RogueGame) nothing(typ byte) string {
} else { } else {
out = "Haven't discovered anything" out = "Haven't discovered anything"
} }
if typ != '*' { if typ != '*' {
var tystr string var tystr string
switch typ { switch typ {
case Potion: case Potion:
tystr = "potion" tystr = potionName
case Scroll: case Scroll:
tystr = "scroll" tystr = scrollName
case Ring: case Ring:
tystr = "ring" tystr = ringName
case Stick: case Stick:
tystr = "stick" tystr = "stick"
} }
out += fmt.Sprintf(" about any %ss", tystr) out += fmt.Sprintf(" about any %ss", tystr)
} }
return out return out
} }
@@ -480,20 +555,22 @@ func (g *RogueGame) nothing(typ byte) string {
// (things.c nameit). // (things.c nameit).
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string, func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
op *ObjInfo, prfunc func(*RogueGame, *Object) string) { op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
if op.Know || op.Guess != "" { switch {
case op.Know || op.Guess != "":
if obj.Count == 1 { if obj.Count == 1 {
fmt.Fprintf(pb, "A %s ", typ) fmt.Fprintf(pb, "A %s ", typ)
} else { } else {
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ) fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
} }
if op.Know { if op.Know {
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which) fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
} else { } else {
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which) 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) fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
} else { default:
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ) fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
} }
} }
@@ -505,13 +582,17 @@ func nullstr(*RogueGame, *Object) string { return "" }
// pr_list). // pr_list).
func (g *RogueGame) prList() { func (g *RogueGame) prList() {
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg("for ") g.addmsgf("for ")
} }
g.addmsg("what type")
g.addmsgf("what type")
if !g.Options.Terse { if !g.Options.Terse {
g.addmsg(" of object do you want a list") g.addmsgf(" of object do you want a list")
} }
g.msg("? ") g.msg("? ")
ch := g.readchar() ch := g.readchar()
switch ch { switch ch {
case Potion: case Potion:
@@ -533,14 +614,17 @@ func (g *RogueGame) prList() {
// (things.c pr_spec). // (things.c pr_spec).
func (g *RogueGame) prSpec(info []ObjInfo) { func (g *RogueGame) prSpec(info []ObjInfo) {
lastprob := 0 lastprob := 0
i := byte('0') i := byte('0')
for idx := range info { for idx := range info {
if i == '9'+1 { if i == '9'+1 {
i = 'a' i = 'a'
} }
g.addLine("%c: %s (%d%%)", i, info[idx].Name, info[idx].Prob-lastprob) g.addLine("%c: %s (%d%%)", i, info[idx].Name, info[idx].Prob-lastprob)
lastprob = info[idx].Prob lastprob = info[idx].Prob
i++ i++
} }
g.endLine() g.endLine()
} }

View File

@@ -90,22 +90,29 @@ const (
VsMagic = 3 VsMagic = 3
) )
// Flags for rooms (rogue.h) // RoomFlags are the room state bits (rogue.h room flags).
type RoomFlags int16 type RoomFlags int16
// Room state bits (rogue.h ISDARK/ISGONE/ISMAZE).
const ( const (
Dark RoomFlags = 1 << iota // room is dark Dark RoomFlags = 1 << iota // room is dark
Gone // room is gone (a corridor) Gone // room is gone (a corridor)
Maze // room is a maze 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) Set(b RoomFlags) { *f |= b } func (f *RoomFlags) Has(b RoomFlags) bool { return *f&b != 0 }
func (f *RoomFlags) Clear(b RoomFlags) { *f &^= b }
// Flags for objects (rogue.h) // 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 }
// ObjFlags are the object state bits (rogue.h object flags).
type ObjFlags int32 type ObjFlags int32
// Object state bits (rogue.h).
const ( const (
Cursed ObjFlags = 1 << iota // ISCURSED: object is cursed Cursed ObjFlags = 1 << iota // ISCURSED: object is cursed
Known // ISKNOW: player knows details about the object Known // ISKNOW: player knows details about the object
@@ -115,15 +122,22 @@ const (
Protected // ISPROT: armor is permanently protected 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) Set(b ObjFlags) { *f |= b } func (f *ObjFlags) Has(b ObjFlags) bool { return *f&b != 0 }
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
// Flags for creatures (rogue.h). The C bit collisions are deliberate and // Set turns the given bits on.
// preserved: one name of each pair applies to monsters, the other to the func (f *ObjFlags) Set(b ObjFlags) { *f |= b }
// hero, and they never coexist on one creature.
// Clear turns the given bits off.
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
// 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 type CreatureFlags int32
// Creature state bits (rogue.h).
const ( const (
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: creature can see invisible creatures CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: creature can see invisible creatures
@@ -146,13 +160,20 @@ const (
Slowed CreatureFlags = 0o100000 // ISSLOW: creature has been slowed 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) Set(b CreatureFlags) { *f |= b } func (f *CreatureFlags) Has(b CreatureFlags) bool { return *f&b != 0 }
func (f *CreatureFlags) Clear(b CreatureFlags) { *f &^= b }
// Flags for the level map (rogue.h) // 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 }
// 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 type PlaceFlags uint8
// Map cell bits (rogue.h).
const ( const (
FPassage PlaceFlags = 0x80 // F_PASS: is a passageway FPassage PlaceFlags = 0x80 // F_PASS: is a passageway
FSeen PlaceFlags = 0x40 // have seen this spot before FSeen PlaceFlags = 0x40 // have seen this spot before
@@ -163,14 +184,20 @@ const (
FTrapMask PlaceFlags = 0x07 // F_TMASK: trap number mask 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) Set(b PlaceFlags) { *f |= b } func (f *PlaceFlags) Has(b PlaceFlags) bool { return *f&b != 0 }
func (f *PlaceFlags) Clear(b PlaceFlags) { *f &^= b }
// 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 // TrapKind identifies a trap (rogue.h trap types). The kind is stored in
// the low bits of a map cell's PlaceFlags (FTrapMask). // the low bits of a map cell's PlaceFlags (FTrapMask).
type TrapKind int type TrapKind int
// Trap kinds (rogue.h T_* constants).
const ( const (
TrapDoor TrapKind = 0 TrapDoor TrapKind = 0
TrapArrow TrapKind = 1 TrapArrow TrapKind = 1
@@ -183,15 +210,6 @@ const (
NumTrapTypes = 8 NumTrapTypes = 8
) )
// String returns the trap's display name, article included, as the C
// tr_name table had it.
func (t TrapKind) String() string {
if t < 0 || t >= NumTrapTypes {
return "a bizarre trap"
}
return trName[t]
}
// PotionKind identifies a potion (rogue.h potion types). // PotionKind identifies a potion (rogue.h potion types).
type PotionKind int type PotionKind int
@@ -214,14 +232,6 @@ const (
NumPotionTypes NumPotionTypes
) )
// String returns the potion's true name ("healing", "haste self", ...).
func (p PotionKind) String() string {
if p < 0 || p >= NumPotionTypes {
return "strange potion"
}
return basePotInfo[p].Name
}
// ScrollKind identifies a scroll (rogue.h scroll types). // ScrollKind identifies a scroll (rogue.h scroll types).
type ScrollKind int type ScrollKind int
@@ -248,14 +258,6 @@ const (
NumScrollTypes NumScrollTypes
) )
// String returns the scroll's true name ("magic mapping", ...).
func (s ScrollKind) String() string {
if s < 0 || s >= NumScrollTypes {
return "strange scroll"
}
return baseScrInfo[s].Name
}
// WeaponKind identifies a weapon (rogue.h weapon types). // WeaponKind identifies a weapon (rogue.h weapon types).
type WeaponKind int type WeaponKind int
@@ -270,17 +272,12 @@ const (
WeaponDart WeaponDart
WeaponShuriken WeaponShuriken
WeaponSpear WeaponSpear
WeaponFlame // fake entry for dragon breath (ick) WeaponFlame // fake entry for dragon breath (ick)
NumWeaponTypes = WeaponFlame
) )
// String returns the weapon's name ("mace", "two handed sword", ...). // NumWeaponTypes counts the real weapons; the flame pseudo-weapon sits
func (w WeaponKind) String() string { // just past them in the tables (C's MAXWEAPONS == FLAME).
if w < 0 || w > WeaponFlame { const NumWeaponTypes = WeaponFlame
return "strange weapon"
}
return baseWeapInfo[w].Name
}
// ArmorKind identifies a suit of armor (rogue.h armor types). // ArmorKind identifies a suit of armor (rogue.h armor types).
type ArmorKind int type ArmorKind int
@@ -298,14 +295,6 @@ const (
NumArmorTypes NumArmorTypes
) )
// String returns the armor's name ("ring mail", "plate mail", ...).
func (a ArmorKind) String() string {
if a < 0 || a >= NumArmorTypes {
return "strange armor"
}
return baseArmInfo[a].Name
}
// RingKind identifies a ring (rogue.h ring types). // RingKind identifies a ring (rogue.h ring types).
type RingKind int type RingKind int
@@ -328,14 +317,6 @@ const (
NumRingTypes NumRingTypes
) )
// String returns the ring's true name ("add strength", "stealth", ...).
func (r RingKind) String() string {
if r < 0 || r >= NumRingTypes {
return "strange ring"
}
return baseRingInfo[r].Name
}
// WandKind identifies a wand or staff (rogue.h rod/wand/staff types). // WandKind identifies a wand or staff (rogue.h rod/wand/staff types).
type WandKind int type WandKind int
@@ -358,14 +339,6 @@ const (
NumWandTypes NumWandTypes
) )
// String returns the wand/staff's true name ("lightning", ...).
func (w WandKind) String() string {
if w < 0 || w >= NumWandTypes {
return "strange stick"
}
return baseWsInfo[w].Name
}
// Coord is a position on the level (rogue.h coord). A value type: the C // Coord is a position on the level (rogue.h coord). A value type: the C
// ce(a,b) macro is plain == here. // ce(a,b) macro is plain == here.
type Coord struct { type Coord struct {
@@ -425,6 +398,7 @@ func CTRL(c byte) byte { return c & 0o37 }
func distance(y1, x1, y2, x2 int) int { func distance(y1, x1, y2, x2 int) int {
dx := x2 - x1 dx := x2 - x1
dy := y2 - y1 dy := y2 - y1
return dx*dx + dy*dy return dx*dx + dy*dy
} }
@@ -439,5 +413,6 @@ func sign(nm int) int {
case nm > 0: case nm > 0:
return 1 return 1
} }
return 0 return 0
} }

View File

@@ -9,13 +9,15 @@ const noWeapon WeaponKind = -1
// missile fires a missile in a given direction (weapons.c missile). // missile fires a missile in a given direction (weapons.c missile).
func (g *RogueGame) missile(ydelta, xdelta int) { func (g *RogueGame) missile(ydelta, xdelta int) {
// Get which thing we are hurling // Get which thing we are hurling
obj := g.getItem("throw", KindWeapon) obj, ok := g.promptPackItem("throw", KindWeapon)
if obj == nil { if !ok {
return return
} }
if !g.dropCheck(obj) || g.isCurrent(obj) { if !g.dropCheck(obj) || g.isCurrent(obj) {
return return
} }
obj = g.leavePack(obj, true, false) obj = g.leavePack(obj, true, false)
g.doMotion(obj, ydelta, xdelta) g.doMotion(obj, ydelta, xdelta)
// AHA! Here it has hit something. If it is a wall or a door, or if // AHA! Here it has hit something. If it is a wall or a door, or if
@@ -34,25 +36,29 @@ func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
obj.Pos = p.Pos obj.Pos = p.Pos
for { for {
// Erase the old one // Erase the old one
if obj.Pos != p.Pos && g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse { if obj.Pos != p.Pos && g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch == Floor && !g.showFloor() { if ch == Floor && !g.showFloor() {
ch = ' ' ch = ' '
} }
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch) g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
} }
// Get the new position // Get the new position
obj.Pos.Y += ydelta obj.Pos.Y += ydelta
obj.Pos.X += xdelta obj.Pos.X += xdelta
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X) ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
if stepOk(ch) && ch != Door { if stepOk(ch) && ch != Door {
// It hasn't hit anything yet, so display it if it's alright. // 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 { if g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph()) g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
g.refresh() g.refresh()
} }
continue continue
} }
break break
} }
} }
@@ -62,22 +68,27 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
if fpos, ok := g.fallpos(obj.Pos); ok { if fpos, ok := g.fallpos(obj.Pos); ok {
pp := g.Level.At(fpos.Y, fpos.X) pp := g.Level.At(fpos.Y, fpos.X)
pp.Ch = obj.Kind.Glyph() pp.Ch = obj.Kind.Glyph()
obj.Pos = fpos obj.Pos = fpos
if g.cansee(fpos.Y, fpos.X) { if g.canSee(fpos.Y, fpos.X) {
if pp.Monst != nil { if pp.Monst != nil {
pp.Monst.OldCh = obj.Kind.Glyph() pp.Monst.OldCh = obj.Kind.Glyph()
} else { } else {
g.mvaddch(fpos.Y, fpos.X, obj.Kind.Glyph()) g.mvaddch(fpos.Y, fpos.X, obj.Kind.Glyph())
} }
} }
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
return return
} }
if pr { if pr {
if g.HasHit { if g.HasHit {
g.endmsg() g.endmsg()
g.HasHit = false g.HasHit = false
} }
g.msg("the %s vanishes as it hits the ground", g.msg("the %s vanishes as it hits the ground",
g.Items.Weapons[obj.Which].Name) g.Items.Weapons[obj.Which].Name)
} }
@@ -92,56 +103,58 @@ func (g *RogueGame) hitMonster(mp Coord, obj *Object) bool {
// wield pulls out a certain weapon (weapons.c wield). // wield pulls out a certain weapon (weapons.c wield).
func (g *RogueGame) wield() { func (g *RogueGame) wield() {
p := &g.Player p := &g.Player
oweapon := p.CurWeapon oweapon := p.CurWeapon
if !g.dropCheck(p.CurWeapon) { if !g.dropCheck(p.CurWeapon) {
p.CurWeapon = oweapon p.CurWeapon = oweapon
return return
} }
p.CurWeapon = oweapon p.CurWeapon = oweapon
obj := g.getItem("wield", KindWeapon)
if obj == nil { obj, ok := g.promptPackItem("wield", KindWeapon)
if !ok {
g.After = false g.After = false
return return
} }
if obj.Kind == KindArmor { if obj.Kind == KindArmor {
g.msg("you can't wield armor") g.msg("you can't wield armor")
g.After = false g.After = false
return
}
if g.isCurrent(obj) {
g.After = false
return return
} }
sp := g.invName(obj, true) if g.isCurrent(obj) {
p.CurWeapon = obj g.After = false
if !g.Options.Terse {
g.addmsg("you are now ") 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) g.msg("wielding %s (%c)", sp, obj.PackCh)
} }
// initWeaps is the weapons.c init_dam[] table. // weaponSetup is one row of the weapons.c init_dam[] table (see
var initWeaps = [NumWeaponTypes]struct { // gameData.initWeaps).
type weaponSetup struct {
dam DiceSpec // damage when wielded dam DiceSpec // damage when wielded
hrl DiceSpec // damage when thrown hrl DiceSpec // damage when thrown
launch WeaponKind // launching weapon launch WeaponKind // launching weapon
flags ObjFlags flags ObjFlags
}{
{dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
{dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
{dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
{dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
{dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
{dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
{dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
{dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
{dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
} }
// initWeapon sets up a new weapon (weapons.c init_weapon). // initWeapon sets up a new weapon (weapons.c init_weapon).
func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) { func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
iwp := &initWeaps[which] iwp := &g.data.initWeaps[which]
weap.Kind = KindWeapon weap.Kind = KindWeapon
weap.Which = int(which) weap.Which = int(which)
weap.Damage = iwp.dam weap.Damage = iwp.dam
@@ -149,16 +162,19 @@ func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
weap.Launch = iwp.launch weap.Launch = iwp.launch
weap.Flags = iwp.flags weap.Flags = iwp.flags
weap.HPlus = 0 weap.HPlus = 0
weap.DPlus = 0 weap.DPlus = 0
if which == WeaponDagger {
switch {
case which == WeaponDagger:
weap.Count = g.rnd(4) + 2 weap.Count = g.rnd(4) + 2
weap.Group = g.Items.Group weap.Group = g.Items.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.Count = g.rnd(8) + 8
weap.Group = g.Items.Group weap.Group = g.Items.Group
g.Items.Group++ g.Items.Group++
} else { default:
weap.Count = 1 weap.Count = 1
weap.Group = 0 weap.Group = 0
} }
@@ -170,6 +186,7 @@ func num(n1, n2 int, typ byte) string {
if typ == Weapon { if typ == Weapon {
out += fmt.Sprintf(",%+d", n2) out += fmt.Sprintf(",%+d", n2)
} }
return out return out
} }
@@ -177,7 +194,9 @@ func num(n1, n2 int, typ byte) string {
// (weapons.c fallpos). // (weapons.c fallpos).
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) { func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
var newpos Coord var newpos Coord
cnt := 0 cnt := 0
for y := pos.Y - 1; y <= pos.Y+1; y++ { for y := pos.Y - 1; y <= pos.Y+1; y++ {
for x := pos.X - 1; x <= pos.X+1; x++ { for x := pos.X - 1; x <= pos.X+1; x++ {
// check to make certain the spot is empty, if it is, put the // check to make certain the spot is empty, if it is, put the
@@ -187,6 +206,7 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
y < 0 || x < 0 { y < 0 || x < 0 {
continue continue
} }
ch := g.Level.Char(y, x) ch := g.Level.Char(y, x)
if ch == Floor || ch == Passage { if ch == Floor || ch == Passage {
if cnt++; g.rnd(cnt) == 0 { if cnt++; g.rnd(cnt) == 0 {
@@ -196,5 +216,6 @@ func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
} }
} }
} }
return newpos, cnt != 0 return newpos, cnt != 0
} }

View File

@@ -8,50 +8,60 @@ package game
// create_obj). // create_obj).
func (g *RogueGame) createObj() { func (g *RogueGame) createObj() {
obj := newObject() obj := newObject()
g.msg("type of item: ") g.msg("type of item: ")
obj.Kind = objectKindForGlyph(g.readchar()) obj.Kind = objectKindForGlyph(g.readchar())
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
g.msg("which %c do you want? (0-f)", obj.Kind.Glyph()) g.msg("which %c do you want? (0-f)", obj.Kind.Glyph())
ch := g.readchar() ch := g.readchar()
if isDigit(ch) { if isDigit(ch) {
obj.Which = int(ch - '0') obj.Which = int(ch - '0')
} else { } else {
obj.Which = int(ch-'a') + 10 obj.Which = int(ch-'a') + 10
} }
obj.Group = 0 obj.Group = 0
obj.Count = 1 obj.Count = 1
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
switch {
case obj.Kind == KindWeapon || obj.Kind == KindArmor: switch obj.Kind {
case KindWeapon, KindArmor:
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
bless := g.readchar() bless := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if bless == '-' { if bless == '-' {
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
} }
if obj.Kind == KindWeapon { if obj.Kind == KindWeapon {
g.initWeapon(obj, WeaponKind(obj.Which)) g.initWeapon(obj, WeaponKind(obj.Which))
if bless == '-' { if bless == '-' {
obj.HPlus -= g.rnd(3) + 1 obj.HPlus -= g.rnd(3) + 1
} }
if bless == '+' { if bless == '+' {
obj.HPlus += g.rnd(3) + 1 obj.HPlus += g.rnd(3) + 1
} }
} else { } else {
obj.ArmorClass = aClass[obj.Which] obj.ArmorClass = g.data.aClass[obj.Which]
if bless == '-' { if bless == '-' {
obj.ArmorClass += g.rnd(3) + 1 obj.ArmorClass += g.rnd(3) + 1
} }
if bless == '+' { if bless == '+' {
obj.ArmorClass -= g.rnd(3) + 1 obj.ArmorClass -= g.rnd(3) + 1
} }
} }
case obj.Kind == KindRing: case KindRing:
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage: case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
bless := g.readchar() bless := g.readchar()
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
if bless == '-' { if bless == '-' {
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
obj.Bonus = -1 obj.Bonus = -1
@@ -61,15 +71,17 @@ func (g *RogueGame) createObj() {
case RingAggravateMonsters, RingTeleportation: case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed) obj.Flags.Set(Cursed)
} }
case obj.Kind == KindWand: case KindWand:
g.fixStick(obj) g.fixStick(obj)
case obj.Kind == KindGold: case KindGold:
g.msg("how much?") g.msg("how much?")
buf := "" buf := ""
if g.getStr(&buf, g.scr.Std) == Norm { if g.getStr(&buf, g.scr.Std) == Norm {
obj.GoldValue = cAtoi(buf) obj.GoldValue = cAtoi(buf)
} }
} }
g.addPack(obj, false) g.addPack(obj, false)
} }
@@ -77,18 +89,22 @@ func (g *RogueGame) createObj() {
func (g *RogueGame) showMap() { func (g *RogueGame) showMap() {
hw := g.scr.Hw hw := g.scr.Hw
hw.Clear() hw.Clear()
for y := 1; y < NumLines-1; y++ { for y := 1; y < NumLines-1; y++ {
for x := 0; x < NumCols; x++ { for x := range NumCols {
real := g.Level.FlagsAt(y, x).Has(FReal) isReal := g.Level.FlagsAt(y, x).Has(FReal)
if !real { if !isReal {
hw.Standout(true) hw.Standout(true)
} }
hw.MvAddCh(y, x, g.Level.Char(y, x)) hw.MvAddCh(y, x, g.Level.Char(y, x))
if !real {
if !isReal {
hw.Standout(false) hw.Standout(false)
} }
} }
} }
g.showWin("---More (level map)---") g.showWin("---More (level map)---")
} }
@@ -97,27 +113,35 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
p := &g.Player p := &g.Player
if len(p.Pack) == 0 { if len(p.Pack) == 0 {
g.msg("you don't have anything in your pack to identify") g.msg("you don't have anything in your pack to identify")
return return
} }
var obj *Object var obj *Object
for { for {
obj = g.getItem("identify", kind) obj, _ = g.promptPackItem("identify", kind)
if !insist { if !insist {
break break
} }
if g.NObjs == 0 { if g.NObjs == 0 {
return return
} }
if obj == nil { if obj == nil {
g.msg("you must identify something") g.msg("you must identify something")
} else if kind != KindNone && obj.Kind != kind &&
!(kind == KindRingOrStick && continue
(obj.Kind == KindRing || obj.Kind == KindWand)) {
g.msg("you must identify a %s", kind)
} else {
break
} }
if !matchesFilter(kind, obj) {
g.msg("you must identify a %s", kind)
continue
}
break
} }
if obj == nil { if obj == nil {
@@ -136,7 +160,8 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
case KindRing: case KindRing:
setKnow(obj, g.Items.Rings[:]) setKnow(obj, g.Items.Rings[:])
} }
g.msg("%s", g.invName(obj, false))
g.msg("%s", g.inventoryName(obj, false))
} }
// setKnow sets things up when we really know what a thing is (wizard.c // setKnow sets things up when we really know what a thing is (wizard.c
@@ -154,15 +179,18 @@ func setKnow(obj *Object, info []ObjInfo) {
func (g *RogueGame) teleport() { func (g *RogueGame) teleport() {
p := &g.Player p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt()) g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
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) g.leaveRoom(p.Pos)
p.Pos = c p.Pos = c
g.enterRoom(p.Pos) g.enterRoom(p.Pos)
} else { } else {
p.Pos = c p.Pos = c
g.look(true) g.look(true)
} }
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh) g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
// turn off ISHELD in case teleportation was done while fighting a // turn off ISHELD in case teleportation was done while fighting a
// Flytrap // Flytrap
@@ -171,6 +199,7 @@ func (g *RogueGame) teleport() {
p.VfHit = 0 p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0") g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
} }
g.NoMove = 0 g.NoMove = 0
g.Count = 0 g.Count = 0
g.Running = false g.Running = false

View File

@@ -5,6 +5,8 @@
package term package term
import ( import (
"context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -21,6 +23,9 @@ type Tcell struct {
last *game.Window // last rendered window, for resize redraws 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 // New initializes the terminal. The screen must be at least 80x24, as the
// C game required. // C game required.
func New() (*Tcell, error) { func New() (*Tcell, error) {
@@ -28,16 +33,22 @@ func New() (*Tcell, error) {
if err != nil { if err != nil {
return nil, err 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() w, h := s.Size()
if h < game.NumLines || w < game.NumCols { if h < game.NumLines || w < game.NumCols {
s.Fini() 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() s.HideCursor()
return &Tcell{screen: s}, nil return &Tcell{screen: s}, nil
} }
@@ -49,17 +60,21 @@ func (t *Tcell) Fini() {
// Render blits a game window to the terminal (curses refresh). // Render blits a game window to the terminal (curses refresh).
func (t *Tcell) Render(w *game.Window) { func (t *Tcell) Render(w *game.Window) {
t.last = w t.last = w
rows, cols := w.Size() rows, cols := w.Size()
for y := 0; y < rows; y++ { for y := range rows {
for x := 0; x < cols; x++ { for x := range cols {
ch, standout := w.CellAt(y, x) ch, standout := w.CellAt(y, x)
style := tcell.StyleDefault style := tcell.StyleDefault
if standout { if standout {
style = style.Reverse(true) style = style.Reverse(true)
} }
t.screen.SetContent(x, y, rune(ch), nil, style) t.screen.SetContent(x, y, rune(ch), nil, style)
} }
} }
t.screen.Show() t.screen.Show()
} }
@@ -105,8 +120,9 @@ func (t *Tcell) ReadChar() byte {
return 3 return 3
default: default:
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ { if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
return byte(ev.Key()) return byte(ev.Key()) //nolint:gosec // G115: 1..26 fits
} }
if r := ev.Rune(); r > 0 && r < 0x80 { if r := ev.Rune(); r > 0 && r < 0x80 {
return byte(r) return byte(r)
} }
@@ -118,18 +134,29 @@ func (t *Tcell) ReadChar() byte {
// ShellEscape suspends the screen and runs the user's shell (main.c // ShellEscape suspends the screen and runs the user's shell (main.c
// shell + md_shellescape). // shell + md_shellescape).
func (t *Tcell) ShellEscape() { func (t *Tcell) ShellEscape() {
if err := t.screen.Suspend(); err != nil { err := t.screen.Suspend()
if err != nil {
return return
} }
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")
if shell == "" { if shell == "" {
shell = "/bin/sh" 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.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Run() _ = cmd.Run() // best effort: the shell is the user's business
t.screen.Resume()
resumeErr := t.screen.Resume()
if resumeErr != nil {
panic(resumeErr) // terminal resume failure is unrecoverable
}
} }