.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:
- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
the misspell autofix REVERTED where it rewrote authentic C game text
("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
closes explicitly and removes corrupt saves, scoreboard writes are
documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
ErrScreenTooSmall); panics allowed for unrecoverable states per
MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
per-line nolints for provably-bounded conversions (randomMonsterLetter
helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
(recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
(goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
inventory filter untangled into matchesFilter, main.go split so
defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods
Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
416 lines
8.8 KiB
Go
416 lines
8.8 KiB
Go
package game
|
|
|
|
// rooms.c — create the layout for the new level.
|
|
|
|
// spot is the rooms.c SPOT position matrix for maze positions.
|
|
type spot struct {
|
|
nexits int
|
|
exits [4]Coord
|
|
used bool
|
|
}
|
|
|
|
// mazeState is the rooms.c file-scope maze generation state.
|
|
type mazeState struct {
|
|
maxy, maxx, starty, startx int
|
|
maze [NumLines/3 + 1][NumCols/3 + 1]spot
|
|
}
|
|
|
|
const goldGrp = 1
|
|
|
|
// doRooms creates rooms and corridors with a connectivity graph (rooms.c
|
|
// do_rooms).
|
|
func (g *RogueGame) doRooms() {
|
|
var bsze Coord // maximum room size
|
|
|
|
bsze.X = NumCols / 3
|
|
bsze.Y = NumLines / 3
|
|
// Clear things for a new level
|
|
for i := range g.Level.Rooms {
|
|
rp := &g.Level.Rooms[i]
|
|
rp.GoldVal = 0
|
|
rp.Exits = rp.Exits[:0]
|
|
rp.Flags = 0
|
|
}
|
|
// Put the gone rooms, if any, on the level
|
|
leftOut := g.rnd(4)
|
|
for range leftOut {
|
|
g.Level.Rooms[g.rndRoom()].Flags.Set(Gone)
|
|
}
|
|
// dig and populate all the rooms on the level
|
|
for i := range g.Level.Rooms {
|
|
rp := &g.Level.Rooms[i]
|
|
// Find upper left corner of box that this room goes in
|
|
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
|
|
|
|
if rp.Flags.Has(Gone) {
|
|
// Place a gone room. Make certain that there is a blank line
|
|
// for passage drawing.
|
|
for {
|
|
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
|
|
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
|
|
rp.Max.X = -NumCols
|
|
|
|
rp.Max.Y = -NumLines
|
|
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
|
|
break
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
// set room type
|
|
if g.rnd(10) < g.Depth-1 {
|
|
rp.Flags.Set(Dark) // dark room
|
|
|
|
if g.rnd(15) == 0 {
|
|
rp.Flags = Maze // maze room
|
|
}
|
|
}
|
|
// Find a place and size for a random room
|
|
if rp.Flags.Has(Maze) {
|
|
rp.Max.X = bsze.X - 1
|
|
|
|
rp.Max.Y = bsze.Y - 1
|
|
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
|
rp.Pos.X = 0
|
|
}
|
|
|
|
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
|
rp.Pos.Y++
|
|
rp.Max.Y--
|
|
}
|
|
} else {
|
|
for {
|
|
rp.Max.X = g.rnd(bsze.X-4) + 4
|
|
rp.Max.Y = g.rnd(bsze.Y-4) + 4
|
|
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
|
|
|
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
|
if rp.Pos.Y != 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
g.drawRoom(rp)
|
|
// Put the gold in
|
|
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
|
|
gold := newObject()
|
|
rp.GoldVal = g.goldCalc()
|
|
gold.GoldValue = rp.GoldVal
|
|
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
|
gold.Pos = rp.Gold
|
|
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
|
|
|
gold.Flags = Stackable
|
|
gold.Group = goldGrp
|
|
gold.Kind = KindGold
|
|
attachObj(&g.Level.Objects, gold)
|
|
}
|
|
// Put the monster in
|
|
prob := 25
|
|
if rp.GoldVal > 0 {
|
|
prob = 80
|
|
}
|
|
|
|
if g.rnd(100) < prob {
|
|
tp := &Monster{}
|
|
mp, _ := g.findFloorIn(rp, 0, true)
|
|
g.newMonster(tp, g.randMonster(false), mp)
|
|
g.givePack(tp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// drawRoom draws a box around a room and lays down the floor for normal
|
|
// rooms; for maze rooms, draws the maze (rooms.c draw_room).
|
|
func (g *RogueGame) drawRoom(rp *Room) {
|
|
if rp.Flags.Has(Maze) {
|
|
g.doMaze(rp)
|
|
|
|
return
|
|
}
|
|
|
|
g.vert(rp, rp.Pos.X) // Draw left side
|
|
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
|
|
g.horiz(rp, rp.Pos.Y) // Draw top
|
|
g.horiz(rp, rp.Pos.Y+rp.Max.Y-1) // Draw bottom
|
|
|
|
// Put the floor down
|
|
for y := rp.Pos.Y + 1; y < rp.Pos.Y+rp.Max.Y-1; y++ {
|
|
for x := rp.Pos.X + 1; x < rp.Pos.X+rp.Max.X-1; x++ {
|
|
g.Level.SetChar(y, x, Floor)
|
|
}
|
|
}
|
|
}
|
|
|
|
// vert draws a vertical line (rooms.c vert).
|
|
func (g *RogueGame) vert(rp *Room, startx int) {
|
|
for y := rp.Pos.Y + 1; y <= rp.Max.Y+rp.Pos.Y-1; y++ {
|
|
g.Level.SetChar(y, startx, '|')
|
|
}
|
|
}
|
|
|
|
// horiz draws a horizontal line (rooms.c horiz).
|
|
func (g *RogueGame) horiz(rp *Room, starty int) {
|
|
for x := rp.Pos.X; x <= rp.Pos.X+rp.Max.X-1; x++ {
|
|
g.Level.SetChar(starty, x, '-')
|
|
}
|
|
}
|
|
|
|
// doMaze digs a maze (rooms.c do_maze).
|
|
func (g *RogueGame) doMaze(rp *Room) {
|
|
m := &g.maze
|
|
for y := range m.maze {
|
|
for x := range m.maze[y] {
|
|
m.maze[y][x].used = false
|
|
m.maze[y][x].nexits = 0
|
|
}
|
|
}
|
|
|
|
m.maxy = rp.Max.Y
|
|
m.maxx = rp.Max.X
|
|
m.starty = rp.Pos.Y
|
|
m.startx = rp.Pos.X
|
|
starty := (g.rnd(rp.Max.Y) / 2) * 2
|
|
startx := (g.rnd(rp.Max.X) / 2) * 2
|
|
pos := Coord{Y: starty + m.starty, X: startx + m.startx}
|
|
g.putpass(pos)
|
|
g.dig(starty, startx)
|
|
}
|
|
|
|
// dig digs out from around where we are now, if possible (rooms.c dig).
|
|
func (g *RogueGame) dig(y, x int) {
|
|
m := &g.maze
|
|
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
|
|
|
|
for {
|
|
cnt := 0
|
|
|
|
var nexty, nextx int
|
|
|
|
for _, cp := range del {
|
|
newy := y + cp.Y
|
|
|
|
newx := x + cp.X
|
|
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
|
continue
|
|
}
|
|
|
|
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
|
|
continue
|
|
}
|
|
|
|
if cnt++; g.rnd(cnt) == 0 {
|
|
nexty = newy
|
|
nextx = newx
|
|
}
|
|
}
|
|
|
|
if cnt == 0 {
|
|
return
|
|
}
|
|
|
|
g.accntMaze(y, x, nexty, nextx)
|
|
g.accntMaze(nexty, nextx, y, x)
|
|
|
|
var pos Coord
|
|
if nexty == y {
|
|
pos.Y = y + m.starty
|
|
if nextx-x < 0 {
|
|
pos.X = nextx + m.startx + 1
|
|
} else {
|
|
pos.X = nextx + m.startx - 1
|
|
}
|
|
} else {
|
|
pos.X = x + m.startx
|
|
if nexty-y < 0 {
|
|
pos.Y = nexty + m.starty + 1
|
|
} else {
|
|
pos.Y = nexty + m.starty - 1
|
|
}
|
|
}
|
|
|
|
g.putpass(pos)
|
|
pos.Y = nexty + m.starty
|
|
pos.X = nextx + m.startx
|
|
g.putpass(pos)
|
|
g.dig(nexty, nextx)
|
|
}
|
|
}
|
|
|
|
// accntMaze accounts for maze exits (rooms.c accnt_maze).
|
|
func (g *RogueGame) accntMaze(y, x, ny, nx int) {
|
|
sp := &g.maze.maze[y][x]
|
|
for i := range sp.nexits {
|
|
if sp.exits[i].Y == ny && sp.exits[i].X == nx {
|
|
return
|
|
}
|
|
}
|
|
// Note: the C original never increments nexits here (a latent bug it
|
|
// inherits); it stores the exit in the next slot and relies on the
|
|
// slot staying zero-counted. We reproduce the store-without-count.
|
|
if sp.nexits < len(sp.exits) {
|
|
sp.exits[sp.nexits] = Coord{Y: ny, X: nx}
|
|
}
|
|
}
|
|
|
|
// rndPos picks a random spot in a room (rooms.c rnd_pos).
|
|
func (g *RogueGame) rndPos(rp *Room) Coord {
|
|
var cp Coord
|
|
|
|
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
|
|
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
|
|
|
|
return cp
|
|
}
|
|
|
|
// findFloor finds a valid floor spot, picking a new random room each time
|
|
// around the loop; it retries forever (rooms.c find_floor with rp == NULL
|
|
// — every such C call site passed FALSE for the limit).
|
|
func (g *RogueGame) findFloor(monst bool) (Coord, bool) {
|
|
return g.findFloorImpl(nil, 0, monst, true)
|
|
}
|
|
|
|
// findFloorIn finds a valid floor spot in this room (rooms.c find_floor
|
|
// with a specific room).
|
|
func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
|
|
return g.findFloorImpl(rp, limit, monst, false)
|
|
}
|
|
|
|
func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) {
|
|
var compchar byte
|
|
if !pickroom {
|
|
compchar = Floor
|
|
if rp.Flags.Has(Maze) {
|
|
compchar = Passage
|
|
}
|
|
}
|
|
|
|
cnt := limit
|
|
for {
|
|
if limit != 0 {
|
|
if cnt--; cnt == -1 {
|
|
return Coord{}, false
|
|
}
|
|
}
|
|
|
|
if pickroom {
|
|
rp = &g.Level.Rooms[g.rndRoom()]
|
|
|
|
compchar = Floor
|
|
if rp.Flags.Has(Maze) {
|
|
compchar = Passage
|
|
}
|
|
}
|
|
|
|
cp := g.rndPos(rp)
|
|
|
|
pp := g.Level.At(cp.Y, cp.X)
|
|
if monst {
|
|
if pp.Monst == nil && stepOk(pp.Ch) {
|
|
return cp, true
|
|
}
|
|
} else if pp.Ch == compchar {
|
|
return cp, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// enterRoom is the code executed whenever you appear in a room (rooms.c
|
|
// enter_room).
|
|
func (g *RogueGame) enterRoom(cp Coord) {
|
|
p := &g.Player
|
|
rp := g.roomin(cp)
|
|
p.Room = rp
|
|
g.doorOpen(rp)
|
|
|
|
if !rp.Flags.Has(Dark) && !p.On(Blind) {
|
|
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
|
g.move(y, rp.Pos.X)
|
|
|
|
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
|
tp := g.Level.MonsterAt(y, x)
|
|
|
|
ch := g.Level.Char(y, x)
|
|
if tp == nil {
|
|
if g.inch() != ch {
|
|
g.addch(ch)
|
|
} else {
|
|
g.move(y, x+1)
|
|
}
|
|
} else {
|
|
tp.OldCh = ch
|
|
if !g.seeMonst(tp) {
|
|
if p.On(SenseMonsters) {
|
|
g.standout()
|
|
g.addch(tp.Disguise)
|
|
g.standend()
|
|
} else {
|
|
g.addch(ch)
|
|
}
|
|
} else {
|
|
g.addch(tp.Disguise)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
|
|
func (g *RogueGame) leaveRoom(cp Coord) {
|
|
p := &g.Player
|
|
rp := p.Room
|
|
|
|
if rp.Flags.Has(Maze) {
|
|
return
|
|
}
|
|
|
|
var floor byte
|
|
|
|
switch {
|
|
case rp.Flags.Has(Gone):
|
|
floor = Passage
|
|
case !rp.Flags.Has(Dark) || p.On(Blind):
|
|
floor = Floor
|
|
default:
|
|
floor = ' '
|
|
}
|
|
|
|
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPassNum]
|
|
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
|
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
|
g.move(y, x)
|
|
|
|
switch ch := g.inch(); ch {
|
|
case Floor:
|
|
if floor == ' ' {
|
|
g.addch(' ')
|
|
}
|
|
default:
|
|
// to check for monster, we have to strip out the standout
|
|
// bit (our Window returns the bare character already)
|
|
if isUpper(ch) {
|
|
if p.On(SenseMonsters) {
|
|
g.standout()
|
|
g.addch(ch)
|
|
g.standend()
|
|
|
|
break
|
|
}
|
|
|
|
pp := g.Level.At(y, x)
|
|
if pp.Ch == Door {
|
|
g.addch(Door)
|
|
} else {
|
|
g.addch(floor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
g.doorOpen(rp)
|
|
}
|