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
// needs the get_str line editor).
// lookScan carries the state of one look() glance while it examines the
// nine squares around the hero.
type lookScan struct {
hero Coord
pch byte // map character under the hero
pfl PlaceFlags // map flags under the hero
wakeup bool
doorStop bool // door-stop checking applies (mid-run)
sy, sx, ey, ex int
sumhero, diffhero int
passcount int
}
// look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) {
p := &g.Player
hero := p.Pos
passcount := 0
rp := p.Room
if g.Oldpos != hero {
@@ -17,151 +29,28 @@ func (g *RogueGame) look(wakeup bool) {
g.Oldrp = rp
}
ey := hero.Y + 1
ex := hero.X + 1
sx := hero.X - 1
sy := hero.Y - 1
s := lookScan{
hero: hero,
wakeup: wakeup,
sy: hero.Y - 1,
sx: hero.X - 1,
ey: hero.Y + 1,
ex: hero.X + 1,
}
sumhero, diffhero := 0, 0
if g.DoorStop && !g.Firstmove && g.Running {
sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X
s.doorStop = g.DoorStop && !g.Firstmove
if s.doorStop && g.Running {
s.sumhero = hero.Y + hero.X
s.diffhero = hero.Y - hero.X
}
pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch
pfl := pp.Flags
s.pch = pp.Ch
s.pfl = pp.Flags
for y := sy; y <= ey; y++ {
if y <= 0 || y >= NumLines-1 {
continue
}
g.lookAround(&s)
for x := sx; x <= ex; x++ {
if x < 0 || x >= NumCols {
continue
}
if !p.On(Blind) {
if y == hero.Y && x == hero.X {
continue
}
}
pp := g.Level.At(y, x)
ch := pp.Ch
if ch == ' ' { // nothing need be done with a ' '
continue
}
fp := &pp.Flags
if pch != Door && ch != Door {
if (pfl & FPassage) != (*fp & FPassage) {
continue
}
}
if (fp.Has(FPassage) || ch == Door) && (pfl.Has(FPassage) || pch == Door) {
if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
continue
}
}
tp := pp.Monst
switch {
case tp == nil:
ch = g.tripCh(y, x, ch)
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
}
continue
default:
if wakeup {
g.wakeMonster(y, x)
}
if g.seeMonst(tp) {
if p.On(Hallucinating) {
ch = g.randomMonsterLetter()
} else {
ch = tp.Disguise
}
}
}
if p.On(Blind) && (y != hero.Y || x != hero.X) {
continue
}
g.move(y, x)
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
ch = ' '
}
if tp != nil || ch != g.inch() {
g.addch(ch)
}
if g.DoorStop && !g.Firstmove && g.Running {
switch g.RunCh {
case 'h':
if x == ex {
continue
}
case 'j':
if y == sy {
continue
}
case 'k':
if y == ey {
continue
}
case 'l':
if x == sx {
continue
}
case 'y':
if (y+x)-sumhero >= 1 {
continue
}
case 'u':
if (y-x)-diffhero >= 1 {
continue
}
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
}
}
switch ch {
case Door:
if x == hero.X || y == hero.Y {
g.Running = false
}
case Passage:
if x == hero.X || y == hero.Y {
passcount++
}
case Floor, '|', '-', ' ':
default:
g.Running = false
}
}
}
}
if g.DoorStop && !g.Firstmove && passcount > 1 {
if s.doorStop && s.passcount > 1 {
g.Running = false
}
@@ -170,6 +59,192 @@ func (g *RogueGame) look(wakeup bool) {
}
}
// lookAround runs the nine-square scan of look().
func (g *RogueGame) lookAround(s *lookScan) {
for y := s.sy; y <= s.ey; y++ {
if y <= 0 || y >= NumLines-1 {
continue
}
for x := s.sx; x <= s.ex; x++ {
if x < 0 || x >= NumCols {
continue
}
g.lookCell(s, y, x)
}
}
}
// lookCell examines one square around the hero: visibility rules, trip
// and monster rendering, drawing, and run-stop checks (the loop body of
// misc.c look).
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
pp := g.Level.At(y, x)
if g.lookSkips(s, pp, y, x) {
return
}
tp := pp.Monst
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
if skip {
return
}
if !g.lookShow(s, tp, ch, y, x) {
return
}
if s.doorStop && g.Running {
g.lookRunCheck(s, ch, y, x)
}
}
// lookSkips reports whether look ignores this square entirely: the
// hero's own square when sighted, blank rock, passage squares of
// another network, and diagonals the hero could not step to (the guard
// chain of the misc.c look loop).
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
return true
}
if pp.Ch == ' ' { // nothing need be done with a ' '
return true
}
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)
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
ch = ' '
}
if tp != nil || ch != g.inch() {
g.addch(ch)
}
return true
}
// lookCellChar picks what the square shows: trip rendering for empty
// squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop).
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
p := &g.Player
switch {
case tp == nil:
return g.tripCh(y, x, ch), false
case p.On(SenseMonsters) && tp.On(Invisible):
if g.DoorStop && !g.Firstmove {
g.Running = false
}
return ch, true
default:
if s.wakeup {
g.wakeMonster(y, x)
}
if g.seeMonst(tp) {
if p.On(Hallucinating) {
return g.randomMonsterLetter(), false
}
return tp.Disguise, false
}
return ch, false
}
}
// lookRunCheck decides whether what this square shows should stop a run
// (the DoorStop tail of the misc.c look loop). Squares on the running
// edge are ignored.
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
if s.atRunEdge(g.RunCh, y, x) {
return
}
switch ch {
case Door:
if x == s.hero.X || y == s.hero.Y {
g.Running = false
}
case Passage:
if x == s.hero.X || y == s.hero.Y {
s.passcount++
}
case Floor, '|', '-', ' ':
default:
g.Running = false
}
}
// atRunEdge reports whether (y, x) sits on the leading edge of the run
// direction, where door-stop checking does not apply (the first RunCh
// switch of the misc.c look loop).
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
switch runCh {
case 'h':
return x == s.ex
case 'j':
return y == s.sy
case 'k':
return y == s.ey
case 'l':
return x == s.sx
case 'y':
return (y+x)-s.sumhero >= 1
case 'u':
return (y-x)-s.diffhero >= 1
case 'n':
return (y+x)-s.sumhero <= -1
case 'b':
return (y-x)-s.diffhero <= -1
}
return false
}
// tripCh returns the character for this space, taking into account whether
// or not the player is tripping (misc.c trip_ch).
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
@@ -409,40 +484,22 @@ func (g *RogueGame) promptDirection() bool {
}
for {
gotit := true
switch g.DirCh = g.readchar(); g.DirCh {
case 'h', 'H':
g.Delta = Coord{X: -1, Y: 0}
case 'j', 'J':
g.Delta = Coord{X: 0, Y: 1}
case 'k', 'K':
g.Delta = Coord{X: 0, Y: -1}
case 'l', 'L':
g.Delta = Coord{X: 1, Y: 0}
case 'y', 'Y':
g.Delta = Coord{X: -1, Y: -1}
case 'u', 'U':
g.Delta = Coord{X: 1, Y: -1}
case 'b', 'B':
g.Delta = Coord{X: -1, Y: 1}
case 'n', 'N':
g.Delta = Coord{X: 1, Y: 1}
case Escape:
g.DirCh = g.readchar()
if g.DirCh == Escape {
g.LastDir = 0
g.resetLast()
return false
default:
g.Msgs.Mpos = 0
g.msg("%s", prompt)
gotit = false
}
if gotit {
if d, ok := deltaFor(g.DirCh); ok {
g.Delta = d
break
}
g.Msgs.Mpos = 0
g.msg("%s", prompt)
}
g.DirCh = toLower(g.DirCh)
@@ -451,14 +508,7 @@ func (g *RogueGame) promptDirection() bool {
}
if g.Player.On(Confused) && g.rnd(5) == 0 {
for {
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
break
}
}
g.confuseDirection()
}
g.Msgs.Mpos = 0
@@ -466,6 +516,44 @@ func (g *RogueGame) promptDirection() bool {
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).
func (g *RogueGame) callIt(info *ObjInfo) {
if info.Know {