go: port dungeon generation, items base, pack, and monster creation

rooms.c, passages.c, new_level.c ported in full: room/maze layout,
corridor spanning tree with extra connections, passage numbering flood
fill, trap/stairs/object placement, treasure rooms. Careful RNG-call
ordering throughout keeps generation seed-faithful to C.

Supporting subsystems this depends on, also ported:
- screen.go: curses replaced by in-memory Window cell buffers behind a
  Terminal interface (tcell arrives with the UI phase; tests run headless)
- io.go: msg/addmsg/endmsg message-line protocol, status line, step_ok
- pack.c in full (add_pack sorting/stacking walk, get_item, inventory)
- things.c in full (inv_name, new_thing, discovery lists, pagination)
- monsters.c in full; chase.c navigation half (roomin/cansee/see_monst/
  find_dest/runto/set_oldch); rings.c, armor.c in full
- weapons.c/sticks.c creation half (init_weapon, fix_stick, num)
- misc.c look() display update, eat, level-up, direction input
- daemons.c callbacks except stomach (needs death()) and the runners
  chase driver (combat phase)
- init.c init_player with the starting kit

Tests: level invariants across seeds, determinism, 30-depth sweep.
This commit is contained in:
2026-07-06 19:05:46 +02:00
parent 7fa2048402
commit a69ef7dc04
21 changed files with 3872 additions and 17 deletions

377
game/rooms.go Normal file
View File

@@ -0,0 +1,377 @@
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 i := 0; i < leftOut; i++ {
g.Level.Rooms[g.rndRoom()].Flags.Set(IsGone)
}
// 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(IsGone) {
// 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(IsDark) // dark room
if g.rnd(15) == 0 {
rp.Flags = IsMaze // maze room
}
}
// Find a place and size for a random room
if rp.Flags.Has(IsMaze) {
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.SetGoldVal(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 = IsMany
gold.Group = goldGrp
gold.Type = Gold
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(IsMaze) {
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(FPass) {
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 := 0; i < sp.nexits; i++ {
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 (rooms.c find_floor with rp == NULL).
func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) {
return g.findFloorImpl(rp, limit, monst, rp == nil)
}
// 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(IsMaze) {
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(IsMaze) {
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(IsDark) && !p.On(IsBlind) {
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(SeeMonst) {
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(IsMaze) {
return
}
var floor byte
switch {
case rp.Flags.Has(IsGone):
floor = Passage
case !rp.Flags.Has(IsDark) || p.On(IsBlind):
floor = Floor
default:
floor = ' '
}
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPNum]
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(SeeMonst) {
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)
}