8 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
25 changed files with 277 additions and 208 deletions

55
TODO.md
View File

@@ -29,14 +29,32 @@ Refactor ground rules:
# Next Step
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.
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. This step also
clears the outstanding cyclop/gocognit/nestif lint findings.
# 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,
@@ -110,36 +128,27 @@ three commits, one subsystem each.
# Future Steps
1. 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.
2. 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. This step also
clears the outstanding cyclop/gocognit/nestif lint findings.
3. Refactor step 8: constructor and style pass per the house
1. 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.
4. 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.
5. 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
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.
6. 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
seeds.
7. Broaden unit test coverage where playtesting finds thin spots
5. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands).
8. 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.
9. 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
constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode.
10. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md.
8. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

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

View File

@@ -13,12 +13,12 @@ func (g *RogueGame) runners(int) {
origPos := tp.Pos
wastarget := tp.On(Targeted)
if g.moveMonster(tp) == -1 {
if removed := g.moveMonster(tp); removed {
continue
}
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if g.moveMonster(tp) == -1 {
if removed := g.moveMonster(tp); removed {
continue
}
}
@@ -38,23 +38,24 @@ func (g *RogueGame) runners(int) {
}
// moveMonster executes a single turn of running for a monster (chase.c
// move_monst). Returns -1 if the monster died or left the level.
func (g *RogueGame) moveMonster(tp *Monster) int {
// move_monst). The result reports that the monster died or left the
// level (the C -1 return).
func (g *RogueGame) moveMonster(tp *Monster) bool {
if !tp.On(Slowed) || tp.Turn {
if g.chaseStep(tp) == -1 {
return -1
if g.chaseStep(tp) {
return true
}
}
if tp.On(Hasted) {
if g.chaseStep(tp) == -1 {
return -1
if g.chaseStep(tp) {
return true
}
}
tp.Turn = !tp.Turn
return 0
return false
}
// relocate makes the monster's new location be the specified one, updating
@@ -86,9 +87,10 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
}
}
// chaseStep makes one thing chase another (chase.c do_chase). Returns -1
// if the chaser died in the attempt.
func (g *RogueGame) chaseStep(th *Monster) int {
// chaseStep makes one thing chase another (chase.c do_chase). removed
// reports that the chaser died or left the level in the attempt (the C
// -1 return).
func (g *RogueGame) chaseStep(th *Monster) (removed bool) {
p := &g.Player
stoprun := false // true means we are there
mindist := 32767
@@ -153,7 +155,7 @@ func (g *RogueGame) chaseStep(th *Monster) int {
g.Kamikaze = false
}
return 0
return false
}
}
@@ -167,7 +169,7 @@ func (g *RogueGame) chaseStep(th *Monster) int {
} else if this == *th.Dest {
for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos {
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
attachObj(&th.Pack, obj)
if th.Room.Flags.Has(Gone) {
@@ -188,7 +190,7 @@ func (g *RogueGame) chaseStep(th *Monster) int {
}
} else {
if th.Type == 'F' {
return 0
return false
}
}
@@ -198,7 +200,7 @@ func (g *RogueGame) chaseStep(th *Monster) int {
th.Flags.Clear(Awake)
}
return 0
return false
}
// chase finds the spot for the chaser to move closer to the chasee

View File

@@ -240,7 +240,7 @@ func (g *RogueGame) dispatch(ch byte) {
g.Kamikaze = true
}
if !g.getDir() {
if !g.promptDirection() {
g.After = false
break
@@ -269,7 +269,7 @@ func (g *RogueGame) dispatch(ch byte) {
continue // the C goto over: fight by running at it
}
case 't':
if !g.getDir() {
if !g.promptDirection() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
@@ -334,7 +334,7 @@ func (g *RogueGame) dispatch(ch byte) {
case 's':
g.search()
case 'z':
if g.getDir() {
if g.promptDirection() {
g.doZap()
} else {
g.After = false
@@ -360,7 +360,7 @@ func (g *RogueGame) dispatch(ch byte) {
g.After = false // "legal" illegal command
case '^':
g.After = false
if g.getDir() {
if g.promptDirection() {
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
@@ -386,7 +386,7 @@ func (g *RogueGame) dispatch(ch byte) {
g.Again = false
case 'm':
g.MoveOn = true
if !g.getDir() {
if !g.promptDirection() {
g.After = false
} else {
ch = g.DirCh
@@ -453,7 +453,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
case CTRL('X'):
g.turnSee(p.On(SenseMonsters))
case CTRL('~'):
if item := g.getItem("charge", KindWand); item != nil {
if item, ok := g.promptPackItem("charge", KindWand); ok {
item.Charges = 10000
}
case CTRL('I'):
@@ -738,9 +738,9 @@ func (g *RogueGame) levitCheck() bool {
// call allows a user to call a potion, scroll, or ring something
// (command.c 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
if obj == nil {
if !ok {
return
}
@@ -820,7 +820,7 @@ func (g *RogueGame) current(cur *Object, how, where string) {
}
g.InvDescribe = false
g.addmsgf("%c) %s", cur.PackCh, g.invName(cur, true))
g.addmsgf("%c) %s", cur.PackCh, g.inventoryName(cur, true))
g.InvDescribe = true
if where != "" {

View File

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

View File

@@ -67,7 +67,7 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
didHit := false
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
if thrown {
@@ -104,9 +104,10 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
return didHit
}
// attack has the monster attack the player (fight.c attack). Returns -1 if
// the monster removed itself from the level during its own attack.
func (g *RogueGame) attack(mp *Monster) int {
// attack has the monster attack the player (fight.c attack). removed
// reports that the monster took itself off the level during its own
// attack (the C -1 return).
func (g *RogueGame) attack(mp *Monster) (removed bool) {
p := &g.Player
// Since this is an attack, stop running and any healing that was
// going on at the time.
@@ -128,9 +129,8 @@ func (g *RogueGame) attack(mp *Monster) int {
mname := g.setMname(mp)
oldhp := p.Stats.HP
removed := false
if g.rollEm(&mp.Creature, &p.Creature, nil, false) {
if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
if mp.Type != 'I' {
if g.HasHit {
g.addmsgf(". ")
@@ -290,7 +290,7 @@ func (g *RogueGame) attack(mp *Monster) int {
removed = true
g.leavePack(steal, false, false)
g.msg("she stole %s!", g.invName(steal, true))
g.msg("she stole %s!", g.inventoryName(steal, true))
}
}
}
@@ -317,11 +317,7 @@ func (g *RogueGame) attack(mp *Monster) int {
g.Count = 0
g.status()
if removed {
return -1
}
return 0
return removed
}
// swing returns true if the swing hits (fight.c swing).
@@ -332,8 +328,8 @@ func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
return res+wplus >= need
}
// rollEm rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool {
// rollAttacks rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player
att := &thatt.Stats
def := &thdef.Stats
@@ -553,7 +549,7 @@ func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
g.Level.SetMonsterAt(mp.Y, mp.X, nil)
g.mvaddch(mp.Y, mp.X, tp.OldCh)
detachMon(&g.Level.Monsters, tp)
g.Level.RemoveMonster(tp)
if tp.On(Targeted) {
g.Kamikaze = false

View File

@@ -32,7 +32,7 @@ func TestRollEmParsesMultiAttackDice(t *testing.T) {
// With attacker level 20 vs armor 10, swing always hits
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of
// 1x4 + str bonus 1 each must deal between 6 and 15 damage.
if !g.rollEm(att, def, nil, false) {
if !g.rollAttacks(att, def, nil, false) {
t.Fatal("attack with guaranteed swing missed")
}

View File

@@ -110,7 +110,7 @@ type RogueGame struct {
// screen / messages
scr *Screen
Msgs MsgLine
Msgs MessageLine
statusCache statusCache
invPage invPage // things.c discovery-list pagination statics
@@ -169,6 +169,7 @@ func NewGame(cfg Config) *RogueGame {
g.InvDescribe = true
g.Msgs.SaveMsg = true
g.scr = NewScreen(cfg.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.FileName = cfg.Home + "/rogue.save"
g.rogueOpts = cfg.RogueOpts

View File

@@ -10,9 +10,11 @@ import (
// maxMsg is io.c MAXMSG: how much message fits before --More--.
const maxMsg = NumCols - len("--More--") - 1
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus
// the related globals (mpos, huh, and the message-behavior flags).
type MsgLine struct {
// MessageLine is the io.c message machinery: the static msgbuf/newpos
// pair plus the related globals (mpos, huh, and the message-behavior
// flags). It owns the top line of the screen; attach wires in the
// display and input it needs.
type MessageLine struct {
buf strings.Builder // msgbuf
newpos int
Mpos int // where cursor is on top line
@@ -20,49 +22,52 @@ type MsgLine struct {
SaveMsg bool // remember last msg
LowerMsg bool // messages should start w/lower case
MsgEsc bool // check for ESC from msg's --More--
scr *Screen // the top line lives on scr.Std
look func(wakeup bool) // redraw before a --More-- (misc.c look)
readChar func() byte // input for --More-- prompts
}
// Msg displays a message at the top of the screen (io.c msg). It returns
// Escape if the player escaped out of a --More--, ^Escape otherwise (the C
// convention: callers compare against ESCAPE).
func (g *RogueGame) msg(format string, a ...any) int {
// Escape if the player escaped out of a --More--, ^Escape otherwise (the
// C convention: callers compare against ESCAPE).
func (m *MessageLine) Msg(format string, a ...any) int {
// if the string is "", just clear the line
if format == "" {
g.move(0, 0)
g.clrtoeol()
g.Msgs.Mpos = 0
m.scr.Std.Move(0, 0)
m.scr.Std.Clrtoeol()
m.Mpos = 0
return ^Escape
}
// otherwise add to the message and flush it out
g.doaddf(format, a...)
m.doaddf(format, a...)
return g.endmsg()
return m.End()
}
// addmsgf adds things to the current message (io.c addmsg).
func (g *RogueGame) addmsgf(format string, a ...any) {
g.doaddf(format, a...)
// Addf adds things to the current message (io.c addmsg).
func (m *MessageLine) Addf(format string, a ...any) {
m.doaddf(format, a...)
}
// endmsg displays a new msg, giving the player a chance to see the previous
// End displays a new msg, giving the player a chance to see the previous
// one if it is up there with the --More-- (io.c endmsg).
func (g *RogueGame) endmsg() int {
m := &g.Msgs
func (m *MessageLine) End() int {
if m.SaveMsg {
m.Huh = m.buf.String()
}
if m.Mpos != 0 {
g.look(false)
g.mvaddstr(0, m.Mpos, "--More--")
g.refresh()
m.look(false)
m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
m.scr.Refresh()
if !m.MsgEsc {
g.waitFor(' ')
m.waitForSpace()
} else {
for {
ch := g.readchar()
ch := m.readChar()
if ch == ' ' {
break
}
@@ -85,30 +90,60 @@ func (g *RogueGame) endmsg() int {
out = string(toUpper(out[0])) + out[1:]
}
g.mvaddstr(0, 0, out)
g.clrtoeol()
m.scr.Std.MvAddStr(0, 0, out)
m.scr.Std.Clrtoeol()
m.Mpos = m.newpos
m.newpos = 0
m.buf.Reset()
g.refresh()
m.scr.Refresh()
return ^Escape
}
// doaddf performs an add onto the message buffer (io.c doadd).
func (g *RogueGame) doaddf(format string, a ...any) {
m := &g.Msgs
// attach wires the message line to its display and input; NewGame and
// Restore call it once the screen and game exist.
func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) {
m.scr = scr
m.look = look
m.readChar = readChar
}
// waitForSpace absorbs input until the player types a space: the
// --More-- acknowledgement (io.c wait_for).
func (m *MessageLine) waitForSpace() {
for {
if m.readChar() == ' ' {
return
}
}
}
// doaddf performs an add onto the message buffer (io.c doadd).
func (m *MessageLine) doaddf(format string, a ...any) {
s := fmt.Sprintf(format, a...)
if len(s)+m.newpos >= maxMsg {
g.endmsg()
m.End()
}
m.buf.WriteString(s)
m.newpos = m.buf.Len()
}
// msg, addmsgf, and endmsg are the game-side shorthands for the message
// line; the machinery lives on MessageLine.
func (g *RogueGame) msg(format string, a ...any) int {
return g.Msgs.Msg(format, a...)
}
func (g *RogueGame) addmsgf(format string, a ...any) {
g.Msgs.Addf(format, a...)
}
func (g *RogueGame) endmsg() {
g.Msgs.End()
}
// stepOk returns true if it is ok to step on ch (io.c step_ok).
func stepOk(ch byte) bool {
switch ch {

View File

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

View File

@@ -223,21 +223,10 @@ func (g *RogueGame) showFloor() bool {
return true
}
// findObj finds the unclaimed object at (y, x) (misc.c find_obj).
func (g *RogueGame) findObj(y, x int) *Object {
for _, obj := range g.Level.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
}
// eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() {
obj := g.getItem("eat", KindFood)
if obj == nil {
obj, ok := g.promptPackItem("eat", KindFood)
if !ok {
return
}
@@ -406,9 +395,9 @@ func (g *RogueGame) isCurrent(obj *Object) bool {
return false
}
// getDir sets up the direction coordinate for use in various "prefix"
// commands (misc.c get_dir).
func (g *RogueGame) getDir() bool {
// promptDirection sets up the direction coordinate for use in various
// "prefix" commands (misc.c get_dir).
func (g *RogueGame) promptDirection() bool {
if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt
g.DirCh = g.LastDir

View File

@@ -31,7 +31,7 @@ func (g *RogueGame) randMonster(wander bool) byte {
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
levAdd := max(g.Depth-AmuletLevel, 0)
attachMon(&g.Level.Monsters, tp)
g.Level.AddMonster(tp)
tp.Type = typ
tp.Disguise = typ
tp.Pos = cp

View File

@@ -105,7 +105,7 @@ func (g *RogueGame) putThings() {
if g.rnd(100) < 36 {
// Pick a new object and link it in the list
obj := g.newThing()
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
// Put it somewhere
obj.Pos, _ = g.findFloor(false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
@@ -115,7 +115,7 @@ func (g *RogueGame) putThings() {
// yet, put it somewhere on the ground
if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject()
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
obj.Damage = dice("0x0")
obj.HurlDmg = dice("0x0")
obj.ArmorClass = 11
@@ -137,7 +137,7 @@ func (g *RogueGame) treasureRoom() {
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
tp := g.newThing()
tp.Pos = mp
attachObj(&g.Level.Objects, tp)
g.Level.AddObject(tp)
g.Level.SetChar(mp.Y, mp.X, tp.Kind.Glyph())
}

View File

@@ -9,7 +9,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
fromFloor := false
if obj == nil {
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil {
if obj = g.Level.ObjectAt(p.Pos.Y, p.Pos.X); obj == nil {
return
}
@@ -18,7 +18,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
// Check for and deal with scare monster scrolls
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster && obj.Flags.Has(WasFound) {
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
@@ -34,7 +34,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = g.packChar()
obj.PackCh = p.nextPackChar()
p.Inpack++
} else {
// Walk the pack looking for the insertion point, keeping items of
@@ -112,7 +112,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
return
}
obj.PackCh = g.packChar()
obj.PackCh = p.nextPackChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
@@ -137,7 +137,7 @@ func (g *RogueGame) addPack(obj *Object, silent bool) {
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)
}
}
@@ -168,7 +168,7 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
}
if fromFloor {
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(Gone) {
@@ -181,46 +181,17 @@ func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
return true
}
// leavePack takes an item out of the pack (pack.c leave_pack).
// leavePack takes an item out of the pack (pack.c leave_pack), keeping
// the repeat-command bookkeeping; the pack surgery is
// Player.removeFromPack.
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// packChar returns the next unused pack character (pack.c pack_char).
func (g *RogueGame) packChar() byte {
p := &g.Player
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
return g.Player.removeFromPack(obj, newobj, all)
}
// inventory lists what is in the pack; returns true if there is something
@@ -236,7 +207,7 @@ func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
g.NObjs++
g.Msgs.MsgEsc = true
line := string(item.PackCh) + ") " + g.invName(item, false)
line := string(item.PackCh) + ") " + g.inventoryName(item, false)
if g.addLine("%s", line) == Escape {
g.Msgs.MsgEsc = false
g.msg("")
@@ -277,7 +248,7 @@ func (g *RogueGame) pickUp(ch byte) {
return
}
obj := g.findObj(p.Pos.Y, p.Pos.X)
obj := g.Level.ObjectAt(p.Pos.Y, p.Pos.X)
if g.MoveOn {
g.moveMsg(obj)
@@ -291,7 +262,7 @@ func (g *RogueGame) pickUp(ch byte) {
}
g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj)
g.Level.RemoveObject(obj)
p.Room.GoldVal = 0
default:
@@ -323,7 +294,7 @@ func (g *RogueGame) moveMsg(obj *Object) {
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
@@ -337,7 +308,7 @@ func (g *RogueGame) pickyInven() {
}
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
}
@@ -354,7 +325,7 @@ func (g *RogueGame) pickyInven() {
for _, obj := range p.Pack {
if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false))
g.msg("%c) %s", mch, g.inventoryName(obj, false))
return
}
@@ -363,23 +334,24 @@ func (g *RogueGame) pickyInven() {
g.msg("'%s' not in pack", unctrl(mch))
}
// getItem picks something out of a pack for a purpose (pack.c get_item).
func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
// promptPackItem picks something out of a pack for a purpose (pack.c
// get_item); ok reports whether the player chose an item.
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (obj *Object, ok bool) {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return nil
return nil, false
}
if g.Again {
if g.LastPick != nil {
return g.LastPick
return g.LastPick, true
}
g.msg("you ran out")
return nil
return nil, false
}
for {
@@ -402,7 +374,7 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
g.After = false
g.msg("")
return nil
return nil, false
}
g.NObjs = 1 // normal case: person types one char
@@ -411,7 +383,7 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
if !g.inventory(p.Pack, kind) {
g.After = false
return nil
return nil, false
}
continue
@@ -419,7 +391,7 @@ func (g *RogueGame) getItem(purpose string, kind ObjectKind) *Object {
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj
return obj, true
}
}

View File

@@ -16,9 +16,9 @@ type pact struct {
// quaff drinks a potion from the pack (potions.c quaff).
func (g *RogueGame) quaff() {
p := &g.Player
obj := g.getItem("quaff", KindPotion)
obj, ok := g.promptPackItem("quaff", KindPotion)
// Make certain that it is something that we want to drink
if obj == nil {
if !ok {
return
}
@@ -43,7 +43,7 @@ func (g *RogueGame) quaff() {
switch obj.PotionKind() {
case PotionConfusion:
g.doPot(PotionConfusion, !trip)
g.applyPotionFuse(PotionConfusion, !trip)
case PotionPoison:
g.Items.Potions[PotionPoison].Know = true
if p.IsWearing(RingSustainStrength) {
@@ -118,11 +118,11 @@ func (g *RogueGame) quaff() {
g.SeenStairs = g.seenStairs()
}
g.doPot(PotionLSD, true)
g.applyPotionFuse(PotionLSD, true)
case PotionSeeInvisible:
show := p.On(CanSeeInvisible)
g.doPot(PotionSeeInvisible, false)
g.applyPotionFuse(PotionSeeInvisible, false)
if !show {
g.invisOn()
@@ -177,9 +177,9 @@ func (g *RogueGame) quaff() {
g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness:
g.doPot(PotionBlindness, true)
g.applyPotionFuse(PotionBlindness, true)
case PotionLevitation:
g.doPot(PotionLevitation, true)
g.applyPotionFuse(PotionLevitation, true)
}
g.status()
@@ -194,9 +194,9 @@ func (g *RogueGame) raiseLevel() {
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) {
// applyPotionFuse does a potion with standard setup: it uses a fuse and
// turns on a flag (potions.c do_pot).
func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
pp := &g.data.pActions[kind]
if !g.Items.Potions[kind].Know {
g.Items.Potions[kind].Know = knowit

View File

@@ -7,9 +7,9 @@ import "fmt"
// ringOn puts a ring on a hand (rings.c ring_on).
func (g *RogueGame) ringOn() {
p := &g.Player
obj := g.getItem("put on", KindRing)
obj, ok := g.promptPackItem("put on", KindRing)
// Make certain that it is something that we want to wear
if obj == nil {
if !ok {
return
}
@@ -65,7 +65,7 @@ func (g *RogueGame) ringOn() {
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).
@@ -103,7 +103,7 @@ func (g *RogueGame) ringOff() {
}
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)
}
}

View File

@@ -189,7 +189,7 @@ func (g *RogueGame) totalWinner() {
}
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.invName(obj, false))
g.inventoryName(obj, false))
line++
p.Purse += worth
}

View File

@@ -105,7 +105,7 @@ func (g *RogueGame) digRooms() {
gold.Flags = Stackable
gold.Group = goldGrp
gold.Kind = KindGold
attachObj(&g.Level.Objects, gold)
g.Level.AddObject(gold)
}
// Put the monster in
prob := 25

View File

@@ -618,6 +618,7 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
restored: true,
}
g.scr = NewScreen(cfg.Term)
g.Msgs.attach(g.scr, g.look, g.readchar)
g.applySnapshot(&st)
// defeat multiple restarting from the same place

View File

@@ -7,8 +7,8 @@ package game
func (g *RogueGame) readScroll() {
p := &g.Player
obj := g.getItem("read", KindScroll)
if obj == nil {
obj, ok := g.promptPackItem("read", KindScroll)
if !ok {
return
}
@@ -104,7 +104,7 @@ func (g *RogueGame) readScroll() {
// Or anything else nasty
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
if ch == Scroll {
if fo := g.findObj(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
continue
}
}

View File

@@ -14,8 +14,8 @@ const (
func (g *RogueGame) doZap() {
p := &g.Player
obj := g.getItem("zap with", KindWand)
if obj == nil {
obj, ok := g.promptPackItem("zap with", KindWand)
if !ok {
return
}
@@ -85,7 +85,7 @@ func (g *RogueGame) doZap() {
}
case WandPolymorph:
pp := tp.Pack
detachMon(&g.Level.Monsters, tp)
g.Level.RemoveMonster(tp)
if g.seeMonst(tp) {
g.mvaddch(y, x, g.Level.Char(y, x))

View File

@@ -94,7 +94,7 @@ type gameData struct {
initWeaps [NumWeaponTypes]weaponSetup
// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic
// (it names the fruit) and is computed in doPot.
// (it names the fruit) and is computed in applyPotionFuse.
pActions [NumPotionTypes]pact
// idType maps identify scrolls to the kind of item they identify

View File

@@ -9,7 +9,7 @@ import (
// invName returns the name of something as it would appear in an inventory
// (things.c inv_name).
func (g *RogueGame) invName(obj *Object, drop bool) string {
func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
var pb strings.Builder
which := obj.Which
@@ -142,8 +142,8 @@ func (g *RogueGame) dropIt() {
return
}
obj := g.getItem("drop", KindNone)
if obj == nil {
obj, ok := g.promptPackItem("drop", KindNone)
if !ok {
return
}
@@ -153,7 +153,7 @@ func (g *RogueGame) dropIt() {
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
// Link it into the level object list
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph())
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
@@ -162,7 +162,7 @@ func (g *RogueGame) dropIt() {
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|
@@ -392,7 +392,7 @@ func (g *RogueGame) printDisc(typ byte) {
if info[order[i]].Know || info[order[i]].Guess != "" {
obj.Kind = objectKindForGlyph(typ)
obj.Which = order[i]
g.addLine("%s", g.invName(&obj, false))
g.addLine("%s", g.inventoryName(&obj, false))
numFound++
}

View File

@@ -9,8 +9,8 @@ const noWeapon WeaponKind = -1
// missile fires a missile in a given direction (weapons.c missile).
func (g *RogueGame) missile(ydelta, xdelta int) {
// Get which thing we are hurling
obj := g.getItem("throw", KindWeapon)
if obj == nil {
obj, ok := g.promptPackItem("throw", KindWeapon)
if !ok {
return
}
@@ -78,7 +78,7 @@ func (g *RogueGame) fall(obj *Object, pr bool) {
}
}
attachObj(&g.Level.Objects, obj)
g.Level.AddObject(obj)
return
}
@@ -113,8 +113,8 @@ func (g *RogueGame) wield() {
p.CurWeapon = oweapon
obj := g.getItem("wield", KindWeapon)
if obj == nil {
obj, ok := g.promptPackItem("wield", KindWeapon)
if !ok {
g.After = false
return
@@ -133,7 +133,7 @@ func (g *RogueGame) wield() {
return
}
sp := g.invName(obj, true)
sp := g.inventoryName(obj, true)
p.CurWeapon = obj
if !g.Options.Terse {

View File

@@ -119,7 +119,7 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
var obj *Object
for {
obj = g.getItem("identify", kind)
obj, _ = g.promptPackItem("identify", kind)
if !insist {
break
@@ -161,7 +161,7 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
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