From fec79b939a3043854e4cb99b1880a8ee5d1fadc1 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 7 Jul 2026 03:19:31 +0200 Subject: [PATCH] Decompose chase.go (refactor step 7) chase splits into chaseBestSpot/chaseTry/scareScrollAt with a chaseSearch state struct; chaseStep gains chaseRooms, chaseGoal, dragonBreath/dragonShoots, and chaseTakeObject; runners gains runnerTurn; findDest gains objectClaimed. chase.go is complexity-clean. Behavior and RNG call order unchanged. --- game/chase.go | 465 +++++++++++++++++++++++++++++--------------------- 1 file changed, 266 insertions(+), 199 deletions(-) diff --git a/game/chase.go b/game/chase.go index d686994..b9516cc 100644 --- a/game/chase.go +++ b/game/chase.go @@ -9,26 +9,7 @@ const dragonShot = 5 func (g *RogueGame) runners(int) { list := append([]*Monster(nil), g.Level.Monsters...) for _, tp := range list { - if !tp.On(Held) && tp.On(Awake) { - origPos := tp.Pos - - wastarget := tp.On(Targeted) - if removed := g.moveMonster(tp); removed { - continue - } - - if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 { - if removed := g.moveMonster(tp); removed { - continue - } - } - - if wastarget && origPos != tp.Pos { - tp.Flags.Clear(Targeted) - - g.ToDeath = false - } - } + g.runnerTurn(tp) } if g.HasHit { @@ -37,6 +18,33 @@ func (g *RogueGame) runners(int) { } } +// runnerTurn gives one monster its motion for the turn; flying monsters +// far from the hero move twice (the loop body of chase.c runners). +func (g *RogueGame) runnerTurn(tp *Monster) { + if tp.On(Held) || !tp.On(Awake) { + return + } + + origPos := tp.Pos + + wastarget := tp.On(Targeted) + if removed := g.moveMonster(tp); removed { + return + } + + if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 { + if removed := g.moveMonster(tp); removed { + return + } + } + + if wastarget && origPos != tp.Pos { + tp.Flags.Clear(Targeted) + + g.ToDeath = false + } +} + // moveMonster executes a single turn of running for a monster (chase.c // move_monst). The result reports that the monster died or left the // level (the C -1 return). @@ -87,111 +95,33 @@ func (g *RogueGame) relocate(th *Monster, newLoc Coord) { } } -// 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 +// chaseStep makes one thing chase another (chase.c do_chase). The +// result reports that the chaser died or left the level in the attempt +// (the C -1 return). +func (g *RogueGame) chaseStep(th *Monster) bool { stoprun := false // true means we are there - mindist := 32767 - rer := th.Room // find room of chaser - if th.On(Greedy) && rer.GoldVal == 0 { - th.Dest = &p.Pos // if gold has been taken, run after hero - } + rer, ree, door := g.chaseRooms(th) - var ree *Room // find room of chasee - if th.Dest == &p.Pos { - ree = p.Room - } else { - ree = g.roomIn(*th.Dest) - } - // We don't count doors as inside rooms for this routine - door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door - - var this Coord - - for { - // If the object of our desire is in a different room, and we are - // not in a corridor, run to the door nearest to our goal. - if rer != ree { - for i := range rer.Exits { - curdist := distCp(*th.Dest, rer.Exits[i]) - if curdist < mindist { - this = rer.Exits[i] - mindist = curdist - } - } - - if door { - rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum] - door = false - - continue // the C goto over: redo with the passage as room - } - } else { - this = *th.Dest - // For dragons check and see if (a) the hero is on a straight - // line from it, and (b) that it is within shooting distance, - // but outside of striking range. - if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X || - abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) && - distCp(th.Pos, p.Pos) <= BoltLength*BoltLength && - !th.On(Cancelled) && g.rnd(dragonShot) == 0 { - g.Delta.Y = sign(p.Pos.Y - th.Pos.Y) - - g.Delta.X = sign(p.Pos.X - th.Pos.X) - if g.HasHit { - g.endmsg() - } - - g.fireBolt(th.Pos, &g.Delta, "flame") - g.Running = false - g.Count = 0 - - g.Quiet = 0 - if g.ToDeath && !th.On(Targeted) { - g.ToDeath = false - g.Kamikaze = false - } - - return false - } - } - - break + this, shot := g.chaseGoal(th, rer, ree, door, 32767) + if shot { + return false } // This now contains what we want to run to this time so we run to it. // If we hit it we either want to fight it or stop running - if !g.chase(th, this) { - if this == p.Pos { - return g.attack(th) - } else if this == *th.Dest { - for _, obj := range g.Level.Objects { - if th.Dest == &obj.Pos { - g.Level.RemoveObject(obj) - attachObj(&th.Pack, obj) - - if th.Room.Flags.Has(Gone) { - g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage) - } else { - g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor) - } - - th.Dest = g.findDest(th) - - break - } - } - - if th.Type != 'F' { - stoprun = true - } - } - } else { + if g.chase(th, this) { if th.Type == 'F' { return false } + } else { + switch this { + case g.Player.Pos: + return g.attack(th) + case *th.Dest: + g.chaseTakeObject(th) + + stoprun = th.Type != 'F' + } } g.relocate(th, g.chRet) @@ -203,14 +133,129 @@ func (g *RogueGame) chaseStep(th *Monster) (removed bool) { return false } +// chaseRooms finds the rooms of the chaser and its desire; doors do not +// count as inside rooms here (the setup of chase.c do_chase). +func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) { + p := &g.Player + + rer := th.Room // find room of chaser + if th.On(Greedy) && rer.GoldVal == 0 { + th.Dest = &p.Pos // if gold has been taken, run after hero + } + + var ree *Room // find room of chasee + if th.Dest == &p.Pos { + ree = p.Room + } else { + ree = g.roomIn(*th.Dest) + } + + return rer, ree, g.Level.Char(th.Pos.Y, th.Pos.X) == Door +} + +// chaseGoal picks the spot the chaser runs toward this turn: the +// nearest exit toward its desire when it is in a different room, or the +// desire itself. shot means a dragon breathed flame instead of moving +// (the goal loop of chase.c do_chase). +func (g *RogueGame) chaseGoal(th *Monster, rer, ree *Room, door bool, mindist int) (Coord, bool) { + var this Coord + + for { + // If the object of our desire is in a different room, and we are + // not in a corridor, run to the door nearest to our goal. + if rer == ree { + this = *th.Dest + + return this, g.dragonBreath(th) + } + + for i := range rer.Exits { + curdist := distCp(*th.Dest, rer.Exits[i]) + if curdist < mindist { + this = rer.Exits[i] + mindist = curdist + } + } + + if !door { + return this, false + } + + rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum] + door = false + // the C goto over: redo with the passage as room + } +} + +// dragonBreath checks whether a dragon shoots flame at the hero instead +// of moving, and shoots it (the D block of chase.c do_chase). +func (g *RogueGame) dragonBreath(th *Monster) bool { + if th.Type != 'D' || !g.dragonShoots(th) { + return false + } + + p := &g.Player + + g.Delta.Y = sign(p.Pos.Y - th.Pos.Y) + + g.Delta.X = sign(p.Pos.X - th.Pos.X) + if g.HasHit { + g.endmsg() + } + + g.fireBolt(th.Pos, &g.Delta, "flame") + g.Running = false + g.Count = 0 + + g.Quiet = 0 + if g.ToDeath && !th.On(Targeted) { + g.ToDeath = false + g.Kamikaze = false + } + + return true +} + +// dragonShoots decides whether the dragon takes the shot: the hero is +// on a straight line from it, within shooting distance but outside +// striking range, it is not cancelled, and the shot roll comes up +// (chase.c do_chase). +func (g *RogueGame) dragonShoots(th *Monster) bool { + p := &g.Player + if th.Pos.Y != p.Pos.Y && th.Pos.X != p.Pos.X && + abs(th.Pos.Y-p.Pos.Y) != abs(th.Pos.X-p.Pos.X) { + return false + } + + return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength && + !th.On(Cancelled) && g.rnd(dragonShot) == 0 +} + +// chaseTakeObject has the monster pick up the object it was running to +// (the dest arm of chase.c do_chase). +func (g *RogueGame) chaseTakeObject(th *Monster) { + for _, obj := range g.Level.Objects { + if th.Dest == &obj.Pos { + g.Level.RemoveObject(obj) + attachObj(&th.Pack, obj) + + if th.Room.Flags.Has(Gone) { + g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage) + } else { + g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor) + } + + th.Dest = g.findDest(th) + + break + } + } +} + // chase finds the spot for the chaser to move closer to the chasee // (chase.c chase). Returns true if we want to keep on chasing later, false // if we reach the goal. The chosen spot lands in g.chRet. func (g *RogueGame) chase(tp *Monster, ee Coord) bool { - p := &g.Player - er := tp.Pos - plcnt := 1 - var curdist int // If the thing is confused, let it move randomly. Invisible Stalkers @@ -226,77 +271,98 @@ func (g *RogueGame) chase(tp *Monster, ee Coord) bool { tp.Flags.Clear(Confused) } } else { - // Otherwise, find the empty spot next to the chaser that is - // closest to the chasee. This will eventually hold where we move - // to get closer. If we can't find an empty spot, we stay where we - // are. - curdist = distCp(er, ee) - g.chRet = er + curdist = g.chaseBestSpot(tp, ee) + } - ey := er.Y + 1 - if ey >= NumLines-1 { - ey = NumLines - 2 + return curdist != 0 && g.chRet != g.Player.Pos +} + +// chaseSearch is the scan state while chase looks for the step that +// gets a monster closest to its chasee. +type chaseSearch struct { + er Coord // where the chaser is + ee Coord // where it wants to go + curdist int + plcnt int +} + +// chaseBestSpot finds the empty spot next to the chaser that is closest +// to the chasee, leaving it in g.chRet; if there is none, the chaser +// stays where it is (the search half of chase.c chase). +func (g *RogueGame) chaseBestSpot(tp *Monster, ee Coord) int { + er := tp.Pos + s := chaseSearch{er: er, ee: ee, curdist: distCp(er, ee), plcnt: 1} + g.chRet = er + + ey := er.Y + 1 + if ey >= NumLines-1 { + ey = NumLines - 2 + } + + ex := er.X + 1 + if ex >= NumCols { + ex = NumCols - 1 + } + + for x := er.X - 1; x <= ex; x++ { + if x < 0 { + continue } - ex := er.X + 1 - if ex >= NumCols { - ex = NumCols - 1 - } - - for x := er.X - 1; x <= ex; x++ { - if x < 0 { - continue - } - - for y := er.Y - 1; y <= ey; y++ { - tryp := Coord{X: x, Y: y} - if !g.diagOk(er, tryp) { - continue - } - - ch := g.Level.VisibleChar(y, x) - if stepOk(ch) { - // If it is a scroll, it might be a scare monster - // scroll so we need to look it up to see what type - // it is. - if ch == Scroll { - var found *Object - - for _, obj := range g.Level.Objects { - if y == obj.Pos.Y && x == obj.Pos.X { - found = obj - - break - } - } - - if found != nil && found.ScrollKind() == ScrollScareMonster { - continue - } - } - // It can also be a Xeroc, which we shouldn't step on - if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' { - continue - } - // If we didn't find any scrolls at this place or it - // wasn't a scare scroll, then this place counts - thisdist := distance(y, x, ee.Y, ee.X) - if thisdist < curdist { - plcnt = 1 - g.chRet = tryp - curdist = thisdist - } else if thisdist == curdist { - if plcnt++; g.rnd(plcnt) == 0 { - g.chRet = tryp - curdist = thisdist - } - } - } - } + for y := er.Y - 1; y <= ey; y++ { + g.chaseTry(&s, y, x) } } - return curdist != 0 && g.chRet != p.Pos + return s.curdist +} + +// chaseTry scores one candidate square, reservoir-sampling among ties +// (the scan body of chase.c chase). +func (g *RogueGame) chaseTry(s *chaseSearch, y, x int) { + tryp := Coord{X: x, Y: y} + if !g.diagOk(s.er, tryp) { + return + } + + ch := g.Level.VisibleChar(y, x) + if !stepOk(ch) { + return + } + // If it is a scroll, it might be a scare monster scroll so we need + // to look it up to see what type it is. + if ch == Scroll && g.scareScrollAt(y, x) { + return + } + // It can also be a Xeroc, which we shouldn't step on + if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' { + return + } + // If we didn't find any scrolls at this place or it wasn't a scare + // scroll, then this place counts + thisdist := distance(y, x, s.ee.Y, s.ee.X) + if thisdist < s.curdist { + s.plcnt = 1 + g.chRet = tryp + s.curdist = thisdist + } else if thisdist == s.curdist { + if s.plcnt++; g.rnd(s.plcnt) == 0 { + g.chRet = tryp + s.curdist = thisdist + } + } +} + +// scareScrollAt reports whether the object lying at (y, x) is a scare +// monster scroll (chase.c chase). +func (g *RogueGame) scareScrollAt(y, x int) bool { + for _, obj := range g.Level.Objects { + if y == obj.Pos.Y && x == obj.Pos.X { + return obj.ScrollKind() == ScrollScareMonster + } + } + + return false } // setOldChar sets the oldch character for the monster (chase.c set_oldch). @@ -431,22 +497,23 @@ func (g *RogueGame) findDest(tp *Monster) *Coord { continue } - if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob { - claimed := false - - for _, other := range g.Level.Monsters { - if other.Dest == &obj.Pos { - claimed = true - - break - } - } - - if !claimed { - return &obj.Pos - } + if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob && + !g.objectClaimed(obj) { + return &obj.Pos } } return &g.Player.Pos } + +// objectClaimed reports whether some monster already runs toward this +// object (chase.c find_dest). +func (g *RogueGame) objectClaimed(obj *Object) bool { + for _, other := range g.Level.Monsters { + if other.Dest == &obj.Pos { + return true + } + } + + return false +}