Decompose look and promptDirection (refactor step 7)

look's nine-square scan splits into lookAround/lookCell with a
lookScan state struct and guard helpers (lookSkips,
lookForeignPassage, lookDiagonalBlocked, lookCellChar, lookShow,
lookRunCheck, atRunEdge); promptDirection gains deltaFor and
confuseDirection. misc.go is complexity-clean. Behavior and RNG call
order unchanged.
This commit is contained in:
2026-07-07 03:03:50 +02:00
parent a20f500655
commit 4a248eb392

View File

@@ -4,11 +4,23 @@ package game
// and small utilities. call_it arrives with the scroll/potion phase (it // and small utilities. call_it arrives with the scroll/potion phase (it
// needs the get_str line editor). // needs the get_str line editor).
// lookScan carries the state of one look() glance while it examines the
// nine squares around the hero.
type lookScan struct {
hero Coord
pch byte // map character under the hero
pfl PlaceFlags // map flags under the hero
wakeup bool
doorStop bool // door-stop checking applies (mid-run)
sy, sx, ey, ex int
sumhero, diffhero int
passcount int
}
// look takes a quick glance all around the player (misc.c look). // look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) { func (g *RogueGame) look(wakeup bool) {
p := &g.Player p := &g.Player
hero := p.Pos hero := p.Pos
passcount := 0
rp := p.Room rp := p.Room
if g.Oldpos != hero { if g.Oldpos != hero {
@@ -17,85 +29,126 @@ func (g *RogueGame) look(wakeup bool) {
g.Oldrp = rp g.Oldrp = rp
} }
ey := hero.Y + 1 s := lookScan{
ex := hero.X + 1 hero: hero,
sx := hero.X - 1 wakeup: wakeup,
sy := hero.Y - 1 sy: hero.Y - 1,
sx: hero.X - 1,
ey: hero.Y + 1,
ex: hero.X + 1,
}
sumhero, diffhero := 0, 0 s.doorStop = g.DoorStop && !g.Firstmove
if g.DoorStop && !g.Firstmove && g.Running { if s.doorStop && g.Running {
sumhero = hero.Y + hero.X s.sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X s.diffhero = hero.Y - hero.X
} }
pp := g.Level.At(hero.Y, hero.X) pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch s.pch = pp.Ch
pfl := pp.Flags s.pfl = pp.Flags
for y := sy; y <= ey; y++ { g.lookAround(&s)
if s.doorStop && s.passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
}
// lookAround runs the nine-square scan of look().
func (g *RogueGame) lookAround(s *lookScan) {
for y := s.sy; y <= s.ey; y++ {
if y <= 0 || y >= NumLines-1 { if y <= 0 || y >= NumLines-1 {
continue continue
} }
for x := sx; x <= ex; x++ { for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols { if x < 0 || x >= NumCols {
continue continue
} }
if !p.On(Blind) { g.lookCell(s, y, x)
if y == hero.Y && x == hero.X { }
continue
} }
} }
// lookCell examines one square around the hero: visibility rules, trip
// and monster rendering, drawing, and run-stop checks (the loop body of
// misc.c look).
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
pp := g.Level.At(y, x) pp := g.Level.At(y, x)
if g.lookSkips(s, pp, y, x) {
ch := pp.Ch return
if ch == ' ' { // nothing need be done with a ' '
continue
}
fp := &pp.Flags
if pch != Door && ch != Door {
if (pfl & FPassage) != (*fp & FPassage) {
continue
}
}
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
continue
}
} }
tp := pp.Monst tp := pp.Monst
switch { ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
case tp == nil: if skip {
ch = g.tripCh(y, x, ch) return
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
continue if !g.lookShow(s, tp, ch, y, x) {
default: return
if wakeup {
g.wakeMonster(y, x)
} }
if g.seeMonst(tp) { if s.doorStop && g.Running {
if p.On(Hallucinating) { g.lookRunCheck(s, ch, y, x)
ch = g.randomMonsterLetter()
} else {
ch = tp.Disguise
}
} }
} }
if p.On(Blind) && (y != hero.Y || x != hero.X) { // lookSkips reports whether look ignores this square entirely: the
continue // hero's own square when sighted, blank rock, passage squares of
// another network, and diagonals the hero could not step to (the guard
// chain of the misc.c look loop).
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
return true
}
if pp.Ch == ' ' { // nothing need be done with a ' '
return true
}
return lookForeignPassage(s, pp.Flags, pp.Ch) ||
g.lookDiagonalBlocked(s, pp.Flags, pp.Ch, y, x)
}
// lookForeignPassage hides passage squares belonging to a different
// passage network than the hero's (misc.c look).
func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
if s.pch != Door && ch != Door {
return (s.pfl & FPassage) != (fp & FPassage)
}
return false
}
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
// not actually step to (misc.c look).
func (g *RogueGame) lookDiagonalBlocked(s *lookScan, fp PlaceFlags, ch byte, y, x int) bool {
if !fp.Has(FPassage) && ch != Door {
return false
}
if !s.pfl.Has(FPassage) && s.pch != Door {
return false
}
return s.hero.X != x && s.hero.Y != y &&
!stepOk(g.Level.Char(y, s.hero.X)) && !stepOk(g.Level.Char(s.hero.Y, x))
}
// lookShow draws the square if it changed; it reports false when a
// blind hero cannot see it at all (the draw part of the look loop).
func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
p := &g.Player
if p.On(Blind) && (y != s.hero.Y || x != s.hero.X) {
return false
} }
g.move(y, x) g.move(y, x)
@@ -108,66 +161,88 @@ func (g *RogueGame) look(wakeup bool) {
g.addch(ch) g.addch(ch)
} }
if g.DoorStop && !g.Firstmove && g.Running { return true
switch g.RunCh {
case 'h':
if x == ex {
continue
} }
case 'j':
if y == sy { // lookCellChar picks what the square shows: trip rendering for empty
continue // squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop).
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
p := &g.Player
switch {
case tp == nil:
return g.tripCh(y, x, ch), false
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
} }
case 'k':
if y == ey { return ch, true
continue default:
if s.wakeup {
g.wakeMonster(y, x)
} }
case 'l':
if x == sx { if g.seeMonst(tp) {
continue if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
} }
case 'y':
if (y+x)-sumhero >= 1 { return tp.Disguise, false
continue
} }
case 'u':
if (y-x)-diffhero >= 1 { return ch, false
continue
} }
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
} }
// lookRunCheck decides whether what this square shows should stop a run
// (the DoorStop tail of the misc.c look loop). Squares on the running
// edge are ignored.
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
if s.atRunEdge(g.RunCh, y, x) {
return
} }
switch ch { switch ch {
case Door: case Door:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
g.Running = false g.Running = false
} }
case Passage: case Passage:
if x == hero.X || y == hero.Y { if x == s.hero.X || y == s.hero.Y {
passcount++ s.passcount++
} }
case Floor, '|', '-', ' ': case Floor, '|', '-', ' ':
default: default:
g.Running = false g.Running = false
} }
} }
}
// atRunEdge reports whether (y, x) sits on the leading edge of the run
// direction, where door-stop checking does not apply (the first RunCh
// switch of the misc.c look loop).
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
switch runCh {
case 'h':
return x == s.ex
case 'j':
return y == s.sy
case 'k':
return y == s.ey
case 'l':
return x == s.sx
case 'y':
return (y+x)-s.sumhero >= 1
case 'u':
return (y-x)-s.diffhero >= 1
case 'n':
return (y+x)-s.sumhero <= -1
case 'b':
return (y-x)-s.diffhero <= -1
} }
if g.DoorStop && !g.Firstmove && passcount > 1 { return false
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
} }
// tripCh returns the character for this space, taking into account whether // tripCh returns the character for this space, taking into account whether
@@ -409,40 +484,22 @@ func (g *RogueGame) promptDirection() bool {
} }
for { for {
gotit := true g.DirCh = g.readchar()
if g.DirCh == Escape {
switch g.DirCh = g.readchar(); g.DirCh {
case 'h', 'H':
g.Delta = Coord{X: -1, Y: 0}
case 'j', 'J':
g.Delta = Coord{X: 0, Y: 1}
case 'k', 'K':
g.Delta = Coord{X: 0, Y: -1}
case 'l', 'L':
g.Delta = Coord{X: 1, Y: 0}
case 'y', 'Y':
g.Delta = Coord{X: -1, Y: -1}
case 'u', 'U':
g.Delta = Coord{X: 1, Y: -1}
case 'b', 'B':
g.Delta = Coord{X: -1, Y: 1}
case 'n', 'N':
g.Delta = Coord{X: 1, Y: 1}
case Escape:
g.LastDir = 0 g.LastDir = 0
g.resetLast() g.resetLast()
return false return false
default:
g.Msgs.Mpos = 0
g.msg("%s", prompt)
gotit = false
} }
if gotit { if d, ok := deltaFor(g.DirCh); ok {
g.Delta = d
break break
} }
g.Msgs.Mpos = 0
g.msg("%s", prompt)
} }
g.DirCh = toLower(g.DirCh) g.DirCh = toLower(g.DirCh)
@@ -451,14 +508,7 @@ func (g *RogueGame) promptDirection() bool {
} }
if g.Player.On(Confused) && g.rnd(5) == 0 { if g.Player.On(Confused) && g.rnd(5) == 0 {
for { g.confuseDirection()
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
break
}
}
} }
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
@@ -466,6 +516,44 @@ func (g *RogueGame) promptDirection() bool {
return true return true
} }
// confuseDirection randomizes the chosen direction for a confused hero
// (the ISHUH tail of misc.c get_dir).
func (g *RogueGame) confuseDirection() {
for {
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
return
}
}
}
// deltaFor maps a direction key to its movement delta; ok is false for
// keys that are not directions (the switch of misc.c get_dir).
func deltaFor(ch byte) (Coord, bool) {
switch ch {
case 'h', 'H':
return Coord{X: -1, Y: 0}, true
case 'j', 'J':
return Coord{X: 0, Y: 1}, true
case 'k', 'K':
return Coord{X: 0, Y: -1}, true
case 'l', 'L':
return Coord{X: 1, Y: 0}, true
case 'y', 'Y':
return Coord{X: -1, Y: -1}, true
case 'u', 'U':
return Coord{X: 1, Y: -1}, true
case 'b', 'B':
return Coord{X: -1, Y: 1}, true
case 'n', 'N':
return Coord{X: 1, Y: 1}, true
}
return Coord{}, false
}
// callIt calls an object something after use (misc.c call_it). // callIt calls an object something after use (misc.c call_it).
func (g *RogueGame) callIt(info *ObjInfo) { func (g *RogueGame) callIt(info *ObjInfo) {
if info.Know { if info.Know {