From a8feb6c05dd7c16c3314b2355ea82be075d9d45d Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 7 Jul 2026 02:10:58 +0200 Subject: [PATCH] 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. --- .golangci.yml | 3 + MEMORY.md | 5 +- TODO.md | 84 +++-- game/command.go | 38 +- game/dice_test.go | 7 +- game/fight.go | 49 +-- game/game.go | 6 +- game/init.go | 52 +-- game/io.go | 4 +- game/misc.go | 15 +- game/monsters.go | 16 +- game/move.go | 8 +- game/object.go | 76 ++-- game/options.go | 8 +- game/passages.go | 17 +- game/potions.go | 30 +- game/rings.go | 21 +- game/rip.go | 31 +- game/save.go | 1 + game/score.go | 9 +- game/scrolls.go | 12 +- game/sticks.go | 8 +- game/tables.go | 889 +++++++++++++++++++++++++++++--------------- game/tables_test.go | 21 +- game/things.go | 14 +- game/types.go | 64 ---- game/weapons.go | 17 +- game/wizard.go | 2 +- 28 files changed, 796 insertions(+), 711 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3f3df27..547a6a9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,9 @@ linters: - 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 linters-settings: lll: diff --git a/MEMORY.md b/MEMORY.md index c1e4cbd..6486034 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -20,7 +20,10 @@ 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). Line-level //nolint +date. Approved so far: paralleltest (2026-07-06); testpackage and +exhaustive (2026-07-07). Complexity linters (cyclop, gocognit, nestif) +stay enabled and red until refactor step 7 fixes the findings, per +sneak 2026-07-07. mnd findings await a ruling. 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. diff --git a/TODO.md b/TODO.md index fec4bb0..acbef93 100644 --- a/TODO.md +++ b/TODO.md @@ -29,22 +29,34 @@ Refactor ground rules: # Next Step -Finish adopting the house Go linting standards. Done so far (branch -refactor/lint-adoption): .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). Remaining findings -are all in linters awaiting sneak's exception decision: mnd (288), -gochecknoglobals (37), cyclop (36), nestif (30), gocognit (23), -exhaustive (22), goconst (16), testpackage (9). Blocked on that -decision; either disable with approval or scope the fixes. +Refactor step 4: method renames, movement/world subsystem +(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep, +doRooms/doPassages/doMaze→digRooms/digPassages/digMaze, +chgStr→changeStrength, ...); remove the goto/label flows in doMove, +dispatch, and saveGame in favor of loops and helpers. # Completed Steps +- 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. Remaining red: cyclop (36), nestif + (30), gocognit (23) stay until step 7 fixes them per sneak's ruling, + and mnd (289) still awaits a ruling. +- 2026-07-06 Lint adoption bulk (refactor/lint-adoption, 5ba9fe8): + .golangci.yml copied verbatim from the prompts repo (plus the + sneak-approved paralleltest exception, 2026-07-06); ~1,500 findings + fixed (autofix formatting sweep, errcheck/err113/noinlineerr error + handling, forbidigo, funcorder, recvcheck pointer receivers, + goprintffuncname renames msg helpers to *f, revive doc comments, + gocritic switch rewrites, gosec real fixes plus justified nolints, + unparam signature tightening, C-faithful "missle" spellings restored + after misspell autofix changed game text). - 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue (go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string). - 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split @@ -86,45 +98,41 @@ decision; either disable with approval or scope the fixes. # Future Steps -1. Refactor step 4: method renames, movement/world subsystem - (doMove→moveHero, beTrapped→springTrap, rndmove→randomStep, - doRooms/doPassages/doMaze→digRooms/digPassages/digMaze, - chgStr→changeStrength, ...); remove the goto/label flows in doMove, - dispatch, and saveGame in favor of loops and helpers. -2. Refactor step 5: method renames, items/combat/UI subsystems +1. 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 +2. 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 +3. 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 + keeping effect order and RNG call sequence identical. This step also + clears the outstanding cyclop/gocognit/nestif lint findings. +4. Refactor step 8: constructor and style pass per the house styleguide — game.New(game.Params{...}) replacing NewGame(Config); replace the gameEnd panic unwind with error-based turn results where feasible; 77-column wrap sweep. -6. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the +5. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the post-refactor names; add the C name → Go name rename table. -7. Playtest hardening pass: play several full games with the tcell - binary and extend run_test.go to script a deeper multi-level - playthrough (descend past level 5, use potions, scrolls, zapping, - save/restore). Fix any panics, message mismatches, or divergences - from the C behavior that this uncovers, with regression tests. -8. Verify the seed-compatibility claim against the C reference on - c-master: same seed, same dungeon, same item tables, for several - seeds. -9. Broaden unit test coverage where playtesting finds thin spots - (rings, sticks, wizard commands). -10. Tag a release once a full game (Amulet retrieval and score entry) - completes without defects. -11. Full-terminal-size support (deferred by explicit decision +6. Playtest hardening pass: play several full games with the tcell + binary and extend run_test.go to script a deeper multi-level + playthrough (descend past level 5, use potions, scrolls, zapping, + save/restore). Fix any panics, message mismatches, or divergences + from the C behavior that this uncovers, with regression tests. +7. Verify the seed-compatibility claim against the C reference on + c-master: same seed, same dungeon, same item tables, for several + seeds. +8. Broaden unit test coverage where playtesting finds thin spots + (rings, sticks, wizard commands). +9. Tag a release once a full game (Amulet retrieval and score entry) + completes without defects. +10. Full-terminal-size support (deferred by explicit decision 2026-07-06): per-game dungeon dimensions instead of the 80x24 constants; open design questions are resize policy, gameplay tuning at larger sizes, and a --classic 80x24 mode. -12. Note: this repo is exempt from the standard policy scaffold. Do not +11. Note: this repo is exempt from the standard policy scaffold. Do not add Makefile, Dockerfile, or REPO_POLICIES.md. diff --git a/game/command.go b/game/command.go index af4f5ef..3e5d704 100644 --- a/game/command.go +++ b/game/command.go @@ -373,9 +373,9 @@ over: case g.Level.Char(g.Delta.Y, g.Delta.X) != Trap: g.msg("no trap there") case p.On(Hallucinating): - g.msg("%s", trName[g.rnd(NumTrapTypes)]) + g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)]) default: - g.msg("%s", trName[*fp&FTrapMask]) + g.msg("%s", g.data.trName[*fp&FTrapMask]) fp.Set(FSeen) } } @@ -540,9 +540,9 @@ func (g *RogueGame) search() { } if p.On(Hallucinating) { - g.msg("%s", trName[g.rnd(NumTrapTypes)]) + g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)]) } else { - g.msg("%s", trName[*fp&FTrapMask]) + g.msg("%s", g.data.trName[*fp&FTrapMask]) fp.Set(FSeen) } @@ -584,7 +584,7 @@ func (g *RogueGame) help() { if helpch != '*' { g.move(0, 0) - for _, strp := range helpStr { + for _, strp := range g.data.helpStr { if strp.Ch == helpch { g.Msgs.LowerMsg = true g.msg("%s%s", unctrl(strp.Ch), strp.Desc) @@ -602,7 +602,7 @@ func (g *RogueGame) help() { // command mode. numprint := 0 - for _, strp := range helpStr { + for _, strp := range g.data.helpStr { if strp.Print { numprint++ } @@ -622,7 +622,7 @@ func (g *RogueGame) help() { cnt := 0 - for _, strp := range helpStr { + for _, strp := range g.data.helpStr { if !strp.Print { continue } @@ -652,28 +652,6 @@ func (g *RogueGame) help() { 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). func (g *RogueGame) identify() { g.msg("what do you want identified? ") @@ -692,7 +670,7 @@ func (g *RogueGame) identify() { } else { str = "unknown character" - for _, hp := range identList { + for _, hp := range g.data.identList { if hp.Ch == ch { str = hp.Desc diff --git a/game/dice_test.go b/game/dice_test.go index 84bd90b..bca6e9c 100644 --- a/game/dice_test.go +++ b/game/dice_test.go @@ -32,15 +32,16 @@ func TestParseDice(t *testing.T) { // The bestiary and weapon tables must parse to at least one attack each so // every creature and weapon actually swings. func TestTablesHaveDice(t *testing.T) { - for i, m := range monsterTable { + data := newGameData() + for i, m := range data.monsterTable { if len(m.Stats.Dmg) == 0 { t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name) } } - for w, iw := range initWeaps { + for w, iw := range data.initWeaps { if len(iw.dam) == 0 || len(iw.hrl) == 0 { - t.Errorf("weapon %v has empty dice", WeaponKind(w)) + t.Errorf("weapon %d has empty dice", w) } } } diff --git a/game/fight.go b/game/fight.go index 09826b1..6511cc2 100644 --- a/game/fight.go +++ b/game/fight.go @@ -4,43 +4,6 @@ import "strconv" // fight.c — all the fighting gets done here. -// hNames are the strings for hitting; the first four are used when the -// player strikes, the second four for monsters (fight.c h_names). -var hNames = [8]string{ - " scored an excellent hit on ", - " hit ", - " have injured ", - " swing and hit ", - " scored an excellent hit on ", - " hit ", - " has injured ", - " swings and hits ", -} - -// mNames are the strings for missing (fight.c m_names). -var mNames = [8]string{ - " miss", - " swing and miss", - " barely miss", - " don't hit", - " misses", - " swings and misses", - " barely misses", - " doesn't hit", -} - -// strPlus adjusts hit probabilities due to strength (fight.c str_plus). -var strPlus = [32]int{ - -7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, - 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, -} - -// addDam adjusts damage done due to strength (fight.c add_dam). -var addDam = [32]int{ - -7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, - 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, -} - // setMname returns the monster name for the given monster (fight.c // set_mname). func (g *RogueGame) setMname(tp *Monster) string { @@ -254,7 +217,7 @@ func (g *RogueGame) attack(mp *Monster) int { p.Stats.Exp = 0 p.Stats.Lvl = 1 } 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) @@ -314,7 +277,7 @@ func (g *RogueGame) attack(mp *Monster) int { for _, obj := range p.Pack { if obj != p.CurArmor && obj != p.CurWeapon && obj != p.CurRing[Left] && obj != p.CurRing[Right] && - obj.isMagic() { + g.isMagic(obj) { if nobj++; g.rnd(nobj) == 0 { steal = obj } @@ -436,10 +399,10 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool didHit := false 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) - damage := dplus + proll + addDam[att.Str] + damage := dplus + proll + g.data.addDam[att.Str] if damage > 0 { def.HP -= damage } @@ -515,7 +478,7 @@ func (g *RogueGame) hit(er, ee string, noend bool) { i += 4 } - s = hNames[i] + s = g.data.hNames[i] } g.addmsgf("%s", s) @@ -546,7 +509,7 @@ func (g *RogueGame) miss(er, ee string, noend bool) { i += 4 } - g.addmsgf("%s", mNames[i]) + g.addmsgf("%s", g.data.mNames[i]) if !g.Options.Terse { g.addmsgf(" %s", prname(ee, false)) diff --git a/game/game.go b/game/game.go index 3c872fa..6d1cbbf 100644 --- a/game/game.go +++ b/game/game.go @@ -138,6 +138,9 @@ type RogueGame struct { rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C restored bool // game came from a save file; Run skips setup + + // data is the game's copy of the static tables (extern.c and friends). + data *gameData } // NewGame builds a game from cfg, seeds the RNG, and randomizes the item @@ -145,6 +148,7 @@ type RogueGame struct { // and first level arrive with later porting phases). func NewGame(cfg Config) *RogueGame { g := &RogueGame{ + data: newGameData(), Rng: &Rng{Seed: cfg.Seed}, Dnum: int(cfg.Seed), Whoami: cfg.Name, @@ -176,7 +180,7 @@ func NewGame(cfg Config) *RogueGame { g.ParseOpts(cfg.RogueOpts) } - g.Monsters = monsterTable + g.Monsters = g.data.monsterTable g.Items.Group = 2 // weapons.c: int group = 2 for i := range g.Level.Passages { diff --git a/game/init.go b/game/init.go index b89afa5..2dd0671 100644 --- a/game/init.go +++ b/game/init.go @@ -8,7 +8,7 @@ import "strings" // initPlayer rolls her up (init.c init_player). func (g *RogueGame) initPlayer() { p := &g.Player - p.MaxStats = initStats + p.MaxStats = g.data.initStats p.Stats = p.MaxStats p.FoodLeft = HungerTime // Give him some food @@ -20,7 +20,7 @@ func (g *RogueGame) initPlayer() { obj = newObject() obj.Kind = KindArmor obj.Which = int(ArmorRingMail) - obj.ArmorClass = aClass[ArmorRingMail] - 1 + obj.ArmorClass = g.data.aClass[ArmorRingMail] - 1 obj.Flags.Set(Known) obj.Count = 1 p.CurArmor = obj @@ -50,19 +50,19 @@ func (g *RogueGame) initPlayer() { // initColors initializes the potion color scheme for this game // (init.c init_colors). func (g *RogueGame) initColors() { - used := make([]bool, len(rainbow)) + used := make([]bool, len(g.data.rainbow)) for i := range NumPotionTypes { var j int for { - j = g.rnd(len(rainbow)) + j = g.rnd(len(g.data.rainbow)) if !used[j] { break } } used[j] = true - g.Items.PotColors[i] = rainbow[j] + g.Items.PotColors[i] = g.data.rainbow[j] } } @@ -75,7 +75,7 @@ func (g *RogueGame) initNames() { for ; nwords > 0; nwords-- { nsyl := g.rnd(3) + 1 for ; nsyl > 0; nsyl-- { - sp := sylls[g.rnd(len(sylls))] + sp := g.data.sylls[g.rnd(len(g.data.sylls))] if cp.Len()+len(sp) > MaxNameLen { break } @@ -93,47 +93,47 @@ func (g *RogueGame) initNames() { // initStones initializes the ring stone setting scheme for this game // (init.c init_stones). func (g *RogueGame) initStones() { - used := make([]bool, len(stoneTable)) + used := make([]bool, len(g.data.stoneTable)) for i := range NumRingTypes { var j int for { - j = g.rnd(len(stoneTable)) + j = g.rnd(len(g.data.stoneTable)) if !used[j] { break } } used[j] = true - g.Items.RingStones[i] = stoneTable[j].Name - g.Items.Rings[i].Worth += stoneTable[j].Value + g.Items.RingStones[i] = g.data.stoneTable[j].Name + g.Items.Rings[i].Worth += g.data.stoneTable[j].Value } } // initMaterials initializes the construction materials for wands and staffs // (init.c init_materials). func (g *RogueGame) initMaterials() { - used := make([]bool, len(woods)) - metused := make([]bool, len(metals)) + used := make([]bool, len(g.data.woods)) + metused := make([]bool, len(g.data.metals)) for i := range NumWandTypes { var str string for { if g.rnd(2) == 0 { - j := g.rnd(len(metals)) + j := g.rnd(len(g.data.metals)) if !metused[j] { - g.Items.WandType[i] = "wand" - str = metals[j] + g.Items.WandType[i] = wandName + str = g.data.metals[j] metused[j] = true break } } else { - j := g.rnd(len(woods)) + j := g.rnd(len(g.data.woods)) if !used[j] { - g.Items.WandType[i] = "staff" - str = woods[j] + g.Items.WandType[i] = staffName + str = g.data.woods[j] used[j] = true break @@ -156,13 +156,13 @@ func sumProbs(info []ObjInfo) { // initProbs copies the base tables into the game and initializes the // probabilities for the various items (init.c init_probs). func (g *RogueGame) initProbs() { - g.Items.Things = baseThings - g.Items.Potions = basePotInfo - g.Items.Scrolls = baseScrInfo - g.Items.Rings = baseRingInfo - g.Items.Sticks = baseWsInfo - g.Items.Weapons = baseWeapInfo - g.Items.Armors = baseArmInfo + g.Items.Things = g.data.baseThings + g.Items.Potions = g.data.basePotInfo + g.Items.Scrolls = g.data.baseScrInfo + g.Items.Rings = g.data.baseRingInfo + g.Items.Sticks = g.data.baseWsInfo + g.Items.Weapons = g.data.baseWeapInfo + g.Items.Armors = g.data.baseArmInfo sumProbs(g.Items.Things[:]) sumProbs(g.Items.Potions[:]) @@ -177,7 +177,7 @@ func (g *RogueGame) initProbs() { // hallucinating (init.c pick_color). func (g *RogueGame) pickColor(col string) string { if g.Player.On(Hallucinating) { - return rainbow[g.rnd(len(rainbow))] + return g.data.rainbow[g.rnd(len(g.data.rainbow))] } return col diff --git a/game/io.go b/game/io.go index b32f06d..b21fa49 100644 --- a/game/io.go +++ b/game/io.go @@ -146,8 +146,6 @@ type statusCache struct { init bool } -var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"} - // status displays the important stats line, keeping the cursor where it was // (io.c status). func (g *RogueGame) status() { @@ -192,7 +190,7 @@ func (g *RogueGame) status() { "Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s", g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP, p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp, - hungerStateName[p.HungryState]) + g.data.hungerStateName[p.HungryState]) if g.StatMsg { g.move(0, 0) g.msg("%s", line) diff --git a/game/misc.go b/game/misc.go index 2586f13..60f8498 100644 --- a/game/misc.go +++ b/game/misc.go @@ -286,8 +286,8 @@ func (g *RogueGame) checkLevel() { p := &g.Player var i int - for i = 0; eLevels[i] != 0; i++ { - if eLevels[i] > p.Stats.Exp { + for i = 0; g.data.eLevels[i] != 0; i++ { + if g.data.eLevels[i] > p.Stats.Exp { break } } @@ -493,22 +493,17 @@ func (g *RogueGame) callIt(info *ObjInfo) { } } -// thingList is misc.c rnd_thing()'s static table. -var thingList = []byte{ - Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet, -} - // rndThing picks a random thing appropriate for this level (misc.c // rnd_thing). func (g *RogueGame) rndThing() byte { var i int if g.Depth >= AmuletLevel { - i = g.rnd(len(thingList)) + i = g.rnd(len(g.data.thingList)) } else { - i = g.rnd(len(thingList) - 1) + i = g.rnd(len(g.data.thingList) - 1) } - return thingList[i] + return g.data.thingList[i] } // chooseStr picks the first or second string depending on whether the diff --git a/game/monsters.go b/game/monsters.go index 9274a3d..864681f 100644 --- a/game/monsters.go +++ b/game/monsters.go @@ -2,24 +2,12 @@ package game // monsters.c — monster creation and saving throws. -// lvlMons and wandMons list monsters in rough order of vorpalness; zero -// entries in wandMons never wander (monsters.c). -var lvlMons = [26]byte{ - 'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A', - 'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D', -} - -var wandMons = [26]byte{ - 'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A', - 0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0, -} - // randMonster picks a monster to show up; the lower the level, the meaner // the monster (monsters.c randmonster). func (g *RogueGame) randMonster(wander bool) byte { - mons := &lvlMons + mons := &g.data.lvlMons if wander { - mons = &wandMons + mons = &g.data.wandMons } for { diff --git a/game/move.go b/game/move.go index 93a6311..5576597 100644 --- a/game/move.go +++ b/game/move.go @@ -255,17 +255,17 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind { case 0: g.msg("you are suddenly in a parallel dimension") case 1: - g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(rainbow))]) + g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))]) case 2: g.msg("you feel a sting in the side of your neck") case 3: g.msg("multi-colored lines swirl around you, then fade") case 4: - g.msg("a %s light flashes in your eyes", rainbow[g.rnd(len(rainbow))]) + g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))]) case 5: g.msg("a spike shoots past your ear!") case 6: - g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(rainbow))]) + g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))]) case 7: g.msg("you suddenly feel very thirsty") case 8: @@ -273,7 +273,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind { case 9: g.msg("time now seems to be going slower") case 10: - g.msg("you pack turns %s!", rainbow[g.rnd(len(rainbow))]) + g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))]) } case TrapSleep: g.NoCommand += g.spread(5) // SLEEPTIME diff --git a/game/object.go b/game/object.go index 0ba68d3..5991503 100644 --- a/game/object.go +++ b/game/object.go @@ -26,36 +26,49 @@ const ( KindRingOrStick ObjectKind = -2 ) -// kindGlyphs maps each kind to the character Rogue draws for it. -var kindGlyphs = [...]byte{ - KindNone: ' ', - KindPotion: Potion, - KindScroll: Scroll, - KindFood: Food, - KindWeapon: Weapon, - KindArmor: Armor, - KindRing: Ring, - KindWand: Stick, - KindAmulet: Amulet, - KindGold: Gold, -} +// Category words shared by ObjectKind.String, the discovery list, and the +// ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the +// glyph byte constants.) +const ( + potionName = "potion" + scrollName = "scroll" + ringName = "ring" + goldName = "gold" +) // Glyph returns the map/display character for this kind of object. func (k ObjectKind) Glyph() byte { - if k < 0 || int(k) >= len(kindGlyphs) { - return ' ' + switch k { + case KindPotion: + return Potion + case KindScroll: + return Scroll + case KindFood: + return Food + case KindWeapon: + return Weapon + case KindArmor: + return Armor + case KindRing: + return Ring + case KindWand: + return Stick + case KindAmulet: + return Amulet + case KindGold: + return Gold } - return kindGlyphs[k] + return ' ' } // String names the category the way the C type_name() did. func (k ObjectKind) String() string { switch k { case KindPotion: - return "potion" + return potionName case KindScroll: - return "scroll" + return scrollName case KindFood: return "food" case KindWeapon: @@ -63,13 +76,13 @@ func (k ObjectKind) String() string { case KindArmor: return "suit of armor" case KindRing: - return "ring" + return ringName case KindWand: return "wand or staff" case KindAmulet: return "amulet" case KindGold: - return "gold" + return goldName case KindRingOrStick: return "ring, wand or staff" } @@ -80,10 +93,25 @@ func (k ObjectKind) String() string { // objectKindForGlyph is the reverse of Glyph: what category of item does a // map character denote. Returns KindNone for non-item characters. func objectKindForGlyph(ch byte) ObjectKind { - for k, g := range kindGlyphs { - if g == ch && ObjectKind(k) != KindNone { - return ObjectKind(k) - } + switch ch { + case Potion: + return KindPotion + case Scroll: + return KindScroll + case Food: + return KindFood + case Weapon: + return KindWeapon + case Armor: + return KindArmor + case Ring: + return KindRing + case Stick: + return KindWand + case Amulet: + return KindAmulet + case Gold: + return KindGold } return KindNone diff --git a/game/options.go b/game/options.go index 5da903d..15fea9c 100644 --- a/game/options.go +++ b/game/options.go @@ -101,7 +101,7 @@ func (g *RogueGame) putOpt(op *optDesc) { case optBool, optSeeFloor: hw.AddStr(boolStr(*op.boolP)) case optInvT: - hw.AddStr(invTName[*op.intP]) + hw.AddStr(g.data.invTName[*op.intP]) case optStr: hw.AddStr(*op.strP) } @@ -291,7 +291,7 @@ func displayStr(buf []byte) string { func (g *RogueGame) getInvT(ip *int) int { win := g.scr.Hw oy, ox := win.GetYX() - win.AddStr(invTName[*ip]) + win.AddStr(g.data.invTName[*ip]) for { win.Move(oy, ox) @@ -319,7 +319,7 @@ func (g *RogueGame) getInvT(ip *int) int { break } - win.MvPrintwf(oy, ox, "%s\n", invTName[*ip]) + win.MvPrintwf(oy, ox, "%s\n", g.data.invTName[*ip]) return Norm } @@ -383,7 +383,7 @@ func (g *RogueGame) ParseOpts(str string) { w = string(toUpper(w[0])) + w[1:] } - for ti, tn := range invTName { + for ti, tn := range g.data.invTName { if strings.HasPrefix(tn, w) { *op.intP = ti diff --git a/game/passages.go b/game/passages.go index 6706726..39dc04b 100644 --- a/game/passages.go +++ b/game/passages.go @@ -2,19 +2,6 @@ package game // passages.c — draw the connecting passages. -// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages. -var rdesConn = [MaxRooms][MaxRooms]bool{ - {false, true, false, true, false, false, false, false, false}, - {true, false, true, false, true, false, false, false, false}, - {false, true, false, false, false, true, false, false, false}, - {true, false, false, false, true, false, true, false, false}, - {false, true, false, true, false, true, false, true, false}, - {false, false, true, false, true, false, false, false, true}, - {false, false, false, true, false, false, false, true, false}, - {false, false, false, false, true, false, true, false, true}, - {false, false, false, false, false, true, false, true, false}, -} - // doPassages draws all the passages on a level (passages.c do_passages). func (g *RogueGame) doPassages() { var ( @@ -34,7 +21,7 @@ func (g *RogueGame) doPassages() { r2 := -1 for i := range MaxRooms { - if rdesConn[r1][i] && !ingraph[i] { + if g.data.rdesConn[r1][i] && !ingraph[i] { if j++; g.rnd(j) == 0 { r2 = i } @@ -74,7 +61,7 @@ func (g *RogueGame) doPassages() { r2 := -1 for i := range MaxRooms { - if rdesConn[r1][i] && !isconn[r1][i] { + if g.data.rdesConn[r1][i] && !isconn[r1][i] { if j++; g.rnd(j) == 0 { r2 = i } diff --git a/game/potions.go b/game/potions.go index e4723c4..f1ba406 100644 --- a/game/potions.go +++ b/game/potions.go @@ -13,24 +13,6 @@ type pact struct { straight string } -// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic -// (it names the fruit) and is computed in doPot. -var pActions = [NumPotionTypes]pact{ - PotionConfusion: {Confused, DUnconfuse, HuhDuration, - "what a tripy feeling!", - "wait, what's going on here. Huh? What? Who?"}, - PotionLSD: {Hallucinating, DComeDown, SeeDuration, - "Oh, wow! Everything seems so cosmic!", - "Oh, wow! Everything seems so cosmic!"}, - PotionSeeInvisible: {CanSeeInvisible, DUnsee, SeeDuration, "", ""}, - PotionBlindness: {Blind, DSight, SeeDuration, - "oh, bummer! Everything is dark! Help!", - "a cloak of darkness falls around you"}, - PotionLevitation: {Levitating, DLand, HealTime, - "oh, wow! You're floating in the air!", - "you start to float in the air"}, -} - // quaff drinks a potion from the pack (potions.c quaff). func (g *RogueGame) quaff() { p := &g.Player @@ -100,7 +82,7 @@ func (g *RogueGame) quaff() { g.scr.Hw.Clear() for _, tp := range g.Level.Objects { - if tp.isMagic() { + if g.isMagic(tp) { show = true g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic) @@ -110,7 +92,7 @@ func (g *RogueGame) quaff() { for _, mp := range g.Level.Monsters { for _, tp := range mp.Pack { - if tp.isMagic() { + if g.isMagic(tp) { show = true g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic) @@ -208,14 +190,14 @@ func (g *RogueGame) quaff() { // raiseLevel: the guy just magically went up a level (potions.c // raise_level). func (g *RogueGame) raiseLevel() { - g.Player.Stats.Exp = eLevels[g.Player.Stats.Lvl-1] + 1 + g.Player.Stats.Exp = g.data.eLevels[g.Player.Stats.Lvl-1] + 1 g.checkLevel() } // doPot does a potion with standard setup: it uses a fuse and turns on a // flag (potions.c do_pot). func (g *RogueGame) doPot(kind PotionKind, knowit bool) { - pp := &pActions[kind] + pp := &g.data.pActions[kind] if !g.Items.Potions[kind].Know { g.Items.Potions[kind].Know = knowit } @@ -240,10 +222,10 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) { } // isMagic reports whether an object radiates magic (potions.c is_magic). -func (o *Object) isMagic() bool { +func (g *RogueGame) isMagic(o *Object) bool { switch o.Kind { case KindArmor: - return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which] + return o.Flags.Has(Protected) || o.ArmorClass != g.data.aClass[o.Which] case KindWeapon: return o.HPlus != 0 || o.DPlus != 0 case KindPotion, KindScroll, KindWand, KindRing, KindAmulet: diff --git a/game/rings.go b/game/rings.go index c58e893..a43ff93 100644 --- a/game/rings.go +++ b/game/rings.go @@ -139,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 // (rings.c ring_eat). func (g *RogueGame) ringEat(hand int) int { @@ -166,7 +147,7 @@ func (g *RogueGame) ringEat(hand int) int { return 0 } - eat := ringUses[ring.RingKind()] + eat := g.data.ringUses[ring.RingKind()] if eat < 0 { if g.rnd(-eat) == 0 { eat = 1 diff --git a/game/rip.go b/game/rip.go index 7136f7a..6c6f31e 100644 --- a/game/rip.go +++ b/game/rip.go @@ -21,22 +21,6 @@ func (g *RogueGame) myExit() { panic(gameEnd{}) } -var ripArt = []string{ - " __________", - " / \\", - " / REST \\", - " / IN \\", - " / PEACE \\", - " / \\", - " | |", - " | |", - " | killed by a |", - " | |", - " | 1980 |", - " *| * * * | *", - " ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______", -} - // death does something really fun when he dies (rip.c death). func (g *RogueGame) death(monst byte) { p := &g.Player @@ -56,7 +40,7 @@ func (g *RogueGame) death(monst byte) { } else { year := time.Now().Year() - for i, line := range ripArt { + for i, line := range g.data.ripArt { g.scr.Std.MvAddStr(8+i, 0, line) } @@ -144,7 +128,7 @@ func (g *RogueGame) totalWinner() { case KindArmor: worth = it.Armors[obj.Which].Worth worth += (9 - obj.ArmorClass) * 100 - worth += 10 * (aClass[obj.Which] - obj.ArmorClass) + worth += 10 * (g.data.aClass[obj.Which] - obj.ArmorClass) obj.Flags.Set(Known) case KindScroll: op := &it.Scrolls[obj.Which] @@ -216,15 +200,6 @@ func (g *RogueGame) totalWinner() { 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). func (g *RogueGame) killname(monst byte, doart bool) string { var ( @@ -239,7 +214,7 @@ func (g *RogueGame) killname(monst byte, doart bool) string { sp = "Wally the Wonder Badger" article = false - for _, hp := range killnameTable { + for _, hp := range g.data.killnameTable { if hp.Ch == monst { sp = hp.Desc article = hp.Print diff --git a/game/save.go b/game/save.go index 0793fb6..193333b 100644 --- a/game/save.go +++ b/game/save.go @@ -606,6 +606,7 @@ func Restore(path string, cfg Config) (*RogueGame, error) { } g := &RogueGame{ + data: newGameData(), Rng: &Rng{}, Playing: true, ScorePath: cfg.ScorePath, diff --git a/game/score.go b/game/score.go index f3aeecd..8d07364 100644 --- a/game/score.go +++ b/game/score.go @@ -25,13 +25,6 @@ type ScoreEnt struct { Time int64 } -var scoreReasons = [4]string{ - "killed", - "quit", - "A total winner", - "killed with Amulet", -} - // rdScore reads the scoreboard file (save.c rd_score). func (g *RogueGame) rdScore() []ScoreEnt { topTen := make([]ScoreEnt, numScores) @@ -187,7 +180,7 @@ func (g *RogueGame) score(amount, flags int, monst byte) { } line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1, - scp.Score, scp.Name, scoreReasons[scp.Flags], scp.Level) + scp.Score, scp.Name, g.data.scoreReasons[scp.Flags], scp.Level) if scp.Flags == 0 || scp.Flags == 3 { line += " by " + g.killname(scp.Monster, true) } diff --git a/game/scrolls.go b/game/scrolls.go index f924b71..fbcdfd1 100644 --- a/game/scrolls.go +++ b/game/scrolls.go @@ -2,16 +2,6 @@ package game // scrolls.c — read a scroll and let it happen. -// idType maps identify scrolls to the kind of item they identify -// (scrolls.c static id_type). -var idType = [ScrollIdentifyRingOrStick + 1]ObjectKind{ - ScrollIdentifyPotion: KindPotion, - ScrollIdentifyScroll: KindScroll, - ScrollIdentifyWeapon: KindWeapon, - ScrollIdentifyArmor: KindArmor, - ScrollIdentifyRingOrStick: KindRingOrStick, -} - // readScroll reads a scroll from the pack and does the appropriate thing // (scrolls.c read_scroll). func (g *RogueGame) readScroll() { @@ -136,7 +126,7 @@ func (g *RogueGame) readScroll() { // Identify, let him figure something out g.Items.Scrolls[obj.Which].Know = true g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name) - g.whatis(true, idType[obj.ScrollKind()]) + g.whatis(true, g.data.idType[obj.ScrollKind()]) case ScrollMagicMapping: // Scroll of magic mapping. g.Items.Scrolls[ScrollMagicMapping].Know = true diff --git a/game/sticks.go b/game/sticks.go index 417f2ac..835c2b1 100644 --- a/game/sticks.go +++ b/game/sticks.go @@ -4,6 +4,12 @@ import "fmt" // sticks.c — zap wands and staffs. +// The two ws_type strings a stick can be made as. +const ( + wandName = "wand" + staffName = "staff" +) + // doZap performs a zap with a wand (sticks.c do_zap). func (g *RogueGame) doZap() { p := &g.Player @@ -378,7 +384,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) { // fixStick sets up a new wand or staff (sticks.c fix_stick). func (g *RogueGame) fixStick(cur *Object) { - if g.Items.WandType[cur.Which] == "staff" { + if g.Items.WandType[cur.Which] == staffName { cur.Damage = dice("2x3") } else { cur.Damage = dice("1x1") diff --git a/game/tables.go b/game/tables.go index d71a382..1bb644c 100644 --- a/game/tables.go +++ b/game/tables.go @@ -1,247 +1,11 @@ package game -// This file is the immutable data from extern.c and init.c: tables that are -// never written after program start. Per-game mutable copies (the ObjInfo -// tables, whose probabilities are re-summed and whose Know/Guess fields -// change during play) are cloned into RogueGame.Items by NewGame. - -// initStats is the C INIT_STATS: the player's starting statistics. -var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12} - -// 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", - "there", "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", -} +// tables.go — the static data tables the C game kept as file-scope globals +// (extern.c, init.c, and assorted per-file statics). The port gathers them +// into gameData: every RogueGame carries its own copy, so the package holds +// no package-level state. Per-game mutable copies (the ObjInfo tables, +// whose probabilities are re-summed and whose Know/Guess fields change +// during play) are cloned into RogueGame.Items by NewGame. // helpEntry is rogue.h struct h_list. type helpEntry struct { @@ -250,73 +14,580 @@ type helpEntry struct { Print bool } -// helpStr is extern.c helpstr[]: the '?' command help text. -var 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, " : run that way", true}, - {0, " : run till adjacent", true}, - {'f', " fight till death or near death", true}, - {'t', " throw something", true}, - {'m', " move onto without picking up", true}, - {'z', " zap a wand in a direction", true}, - {'^', " 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', " fight till either of you dies", true}, - {'v', " print version number", true}, +// gameData is the bundle of static tables, built by newGameData and hung on +// RogueGame as g.data. Nothing in it mutates during play: the one table the +// C code writes to (the venus flytrap damage hack) operates on the game's +// Monsters copy, not on this template. +type gameData struct { + // initStats is the C INIT_STATS: the player's starting statistics. + initStats Stats + + // aClass is extern.c a_class[]: armor class for each armor type. + aClass [NumArmorTypes]int + + // eLevels is extern.c e_levels[]: experience thresholds per level; the + // zero terminates the table as in C. + eLevels []int + + // trName is extern.c tr_name[]: names of the traps. + trName [NumTrapTypes]string + + // invTName is extern.c inv_t_name[]: the inventory style names. + invTName []string + + // monsterTable is extern.c monsters[26]: all monster kinds, indexed by + // letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C) + // is rolled from the level at creation time. + monsterTable [26]MonsterKind + + // Base ObjInfo tables (extern.c). These are templates: NewGame copies + // them into ItemLore before initProbs converts Prob to cumulative form. + baseThings [NumThings]ObjInfo + baseArmInfo [NumArmorTypes]ObjInfo + basePotInfo [NumPotionTypes]ObjInfo + baseRingInfo [NumRingTypes]ObjInfo + baseScrInfo [NumScrollTypes]ObjInfo + baseWeapInfo [NumWeaponTypes + 1]ObjInfo + baseWsInfo [NumWandTypes]ObjInfo + + // rainbow is init.c rainbow[]: the possible potion colors. + rainbow []string + + // sylls is init.c sylls[]: syllables for generated scroll names. + sylls []string + + // stoneTable is init.c stones[]: ring stones and their worth. + stoneTable []Stone + + // woods is init.c wood[]: what staffs are made of. + woods []string + + // metals is init.c metal[]: what wands are made of. + metals []string + + // helpStr is extern.c helpstr[]: the '?' command help text. + helpStr []helpEntry + + // hNames are the strings for hitting; the first four are used when the + // player strikes, the second four for monsters (fight.c h_names). + hNames [8]string + + // mNames are the strings for missing (fight.c m_names). + mNames [8]string + + // strPlus adjusts hit probabilities due to strength (fight.c str_plus). + strPlus [32]int + + // addDam adjusts damage done due to strength (fight.c add_dam). + addDam [32]int + + // lvlMons and wandMons list monsters in rough order of vorpalness; + // zero entries in wandMons never wander (monsters.c). + lvlMons [26]byte + wandMons [26]byte + + // ringUses is the rings.c ring_eat static uses[] table: how much food + // each ring type uses up per turn (negative = a 1-in-n chance of 1). + ringUses [NumRingTypes]int + + // initWeaps is the weapons.c init_dam[] table. + initWeaps [NumWeaponTypes]weaponSetup + + // pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic + // (it names the fruit) and is computed in doPot. + 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, " : run that way", true}, + {0, " : run till adjacent", true}, + {'f', " fight till death or near death", true}, + {'t', " throw something", true}, + {'m', " move onto without picking up", true}, + {'z', " zap a wand in a direction", true}, + {'^', " 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', " 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: diff --git a/game/tables_test.go b/game/tables_test.go index 07e30e2..5226ac4 100644 --- a/game/tables_test.go +++ b/game/tables_test.go @@ -13,14 +13,16 @@ func TestProbabilitiesSumTo100(t *testing.T) { return s } + data := newGameData() + tables := map[string][]ObjInfo{ - "things": baseThings[:], - "potions": basePotInfo[:], - "scrolls": baseScrInfo[:], - "rings": baseRingInfo[:], - "sticks": baseWsInfo[:], - "weapons": baseWeapInfo[:NumWeaponTypes], // excludes the flame entry - "armor": baseArmInfo[:], + "things": data.baseThings[:], + "potions": data.basePotInfo[:], + "scrolls": data.baseScrInfo[:], + "rings": data.baseRingInfo[:], + "sticks": data.baseWsInfo[:], + "weapons": data.baseWeapInfo[:NumWeaponTypes], // excludes the flame entry + "armor": data.baseArmInfo[:], } for name, tab := range tables { if s := sum(tab); s != 100 { @@ -88,11 +90,12 @@ func TestNewGameRandomizesAppearances(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") } - if monsterTable['D'-'A'].Name != "dragon" { + if data.monsterTable['D'-'A'].Name != "dragon" { t.Error("letter indexing broken") } } diff --git a/game/things.go b/game/things.go index 2cd75a5..0237bf1 100644 --- a/game/things.go +++ b/game/things.go @@ -17,9 +17,9 @@ func (g *RogueGame) invName(obj *Object, drop bool) string { switch obj.Kind { case KindPotion: - g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr) + g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr) case KindRing: - g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum) + g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum) case KindWand: g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr) case KindScroll: @@ -78,7 +78,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string { case KindArmor: sp := it.Armors[which].Name if obj.Flags.Has(Known) { - fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp) + fmt.Fprintf(&pb, "%s %s [", num(g.data.aClass[which]-obj.ArmorClass, 0, Armor), sp) if !g.Options.Terse { pb.WriteString("protection ") @@ -257,7 +257,7 @@ func (g *RogueGame) newThing() *Object { cur.Kind = KindArmor cur.Which = pickOne(g, g.Items.Armors[:]) - cur.ArmorClass = aClass[cur.Which] + cur.ArmorClass = g.data.aClass[cur.Which] if r := g.rnd(100); r < 20 { cur.Flags.Set(Cursed) cur.ArmorClass += g.rnd(3) + 1 @@ -536,11 +536,11 @@ func (g *RogueGame) nothing(typ byte) string { switch typ { case Potion: - tystr = "potion" + tystr = potionName case Scroll: - tystr = "scroll" + tystr = scrollName case Ring: - tystr = "ring" + tystr = ringName case Stick: tystr = "stick" } diff --git a/game/types.go b/game/types.go index c0c7924..d8e8d2b 100644 --- a/game/types.go +++ b/game/types.go @@ -210,16 +210,6 @@ const ( NumTrapTypes = 8 ) -// String returns the trap's display name, article included, as the C -// tr_name table had it. -func (t TrapKind) String() string { - if t < 0 || t >= NumTrapTypes { - return "a bizarre trap" - } - - return trName[t] -} - // PotionKind identifies a potion (rogue.h potion types). type PotionKind int @@ -242,15 +232,6 @@ const ( NumPotionTypes ) -// String returns the potion's true name ("healing", "haste self", ...). -func (p PotionKind) String() string { - if p < 0 || p >= NumPotionTypes { - return "strange potion" - } - - return basePotInfo[p].Name -} - // ScrollKind identifies a scroll (rogue.h scroll types). type ScrollKind int @@ -277,15 +258,6 @@ const ( NumScrollTypes ) -// String returns the scroll's true name ("magic mapping", ...). -func (s ScrollKind) String() string { - if s < 0 || s >= NumScrollTypes { - return "strange scroll" - } - - return baseScrInfo[s].Name -} - // WeaponKind identifies a weapon (rogue.h weapon types). type WeaponKind int @@ -307,15 +279,6 @@ const ( // just past them in the tables (C's MAXWEAPONS == FLAME). const NumWeaponTypes = WeaponFlame -// String returns the weapon's name ("mace", "two handed sword", ...). -func (w WeaponKind) String() string { - if w < 0 || w > WeaponFlame { - return "strange weapon" - } - - return baseWeapInfo[w].Name -} - // ArmorKind identifies a suit of armor (rogue.h armor types). type ArmorKind int @@ -332,15 +295,6 @@ const ( NumArmorTypes ) -// String returns the armor's name ("ring mail", "plate mail", ...). -func (a ArmorKind) String() string { - if a < 0 || a >= NumArmorTypes { - return "strange armor" - } - - return baseArmInfo[a].Name -} - // RingKind identifies a ring (rogue.h ring types). type RingKind int @@ -363,15 +317,6 @@ const ( NumRingTypes ) -// String returns the ring's true name ("add strength", "stealth", ...). -func (r RingKind) String() string { - if r < 0 || r >= NumRingTypes { - return "strange ring" - } - - return baseRingInfo[r].Name -} - // WandKind identifies a wand or staff (rogue.h rod/wand/staff types). type WandKind int @@ -394,15 +339,6 @@ const ( NumWandTypes ) -// String returns the wand/staff's true name ("lightning", ...). -func (w WandKind) String() string { - if w < 0 || w >= NumWandTypes { - return "strange stick" - } - - return baseWsInfo[w].Name -} - // Coord is a position on the level (rogue.h coord). A value type: the C // ce(a,b) macro is plain == here. type Coord struct { diff --git a/game/weapons.go b/game/weapons.go index 63da01a..daa9d61 100644 --- a/game/weapons.go +++ b/game/weapons.go @@ -143,27 +143,18 @@ func (g *RogueGame) wield() { g.msg("wielding %s (%c)", sp, obj.PackCh) } -// initWeaps is the weapons.c init_dam[] table. -var initWeaps = [NumWeaponTypes]struct { +// weaponSetup is one row of the weapons.c init_dam[] table (see +// gameData.initWeaps). +type weaponSetup struct { dam DiceSpec // damage when wielded hrl DiceSpec // damage when thrown launch WeaponKind // launching weapon flags ObjFlags -}{ - {dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace - {dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword - {dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow - {dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow - {dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger - {dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword - {dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart - {dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken - {dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear } // initWeapon sets up a new weapon (weapons.c init_weapon). func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) { - iwp := &initWeaps[which] + iwp := &g.data.initWeaps[which] weap.Kind = KindWeapon weap.Which = int(which) weap.Damage = iwp.dam diff --git a/game/wizard.go b/game/wizard.go index 1914fc8..9688d48 100644 --- a/game/wizard.go +++ b/game/wizard.go @@ -46,7 +46,7 @@ func (g *RogueGame) createObj() { obj.HPlus += g.rnd(3) + 1 } } else { - obj.ArmorClass = aClass[obj.Which] + obj.ArmorClass = g.data.aClass[obj.Which] if bless == '-' { obj.ArmorClass += g.rnd(3) + 1 }