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

193
game/monsters.go Normal file
View File

@@ -0,0 +1,193 @@
package game
// monsters.c — monster creation and saving throws.
// lvlMons and wandMons list monsters in rough order of vorpalness; zero
// entries in wandMons never wander (monsters.c).
var lvlMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
}
var wandMons = [26]byte{
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
}
// randMonster picks a monster to show up; the lower the level, the meaner
// the monster (monsters.c randmonster).
func (g *RogueGame) randMonster(wander bool) byte {
mons := &lvlMons
if wander {
mons = &wandMons
}
for {
d := g.Depth + (g.rnd(10) - 6)
if d < 0 {
d = g.rnd(5)
}
if d > 25 {
d = g.rnd(5) + 21
}
if mons[d] != 0 {
return mons[d]
}
}
}
// newMonster picks a new monster and adds it to the list (monsters.c
// new_monster).
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
levAdd := g.Depth - AmuletLevel
if levAdd < 0 {
levAdd = 0
}
attachMon(&g.Level.Monsters, tp)
tp.Type = typ
tp.Disguise = typ
tp.Pos = cp
g.move(cp.Y, cp.X)
tp.OldCh = g.inch()
tp.Room = g.roomin(cp)
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
mp := &monsterTable[tp.Type-'A']
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
tp.Stats.HP = tp.Stats.MaxHP
tp.Stats.Arm = mp.Stats.Arm - levAdd
tp.Stats.Dmg = mp.Stats.Dmg
tp.Stats.Str = mp.Stats.Str
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
tp.Flags = mp.Flags
if g.Depth > 29 {
tp.Flags.Set(IsHaste)
}
tp.Turn = true
tp.Pack = nil
if g.Player.IsWearing(RAggr) {
g.runto(cp)
}
if typ == 'X' {
tp.Disguise = g.rndThing()
}
}
// expAdd is the experience to add for this monster's level/hit points
// (monsters.c exp_add).
func expAdd(tp *Monster) int {
var mod int
if tp.Stats.Lvl == 1 {
mod = tp.Stats.MaxHP / 8
} else {
mod = tp.Stats.MaxHP / 6
}
if tp.Stats.Lvl > 9 {
mod *= 20
} else if tp.Stats.Lvl > 6 {
mod *= 4
}
return mod
}
// wanderer creates a new wandering monster and aims it at the player
// (monsters.c wanderer).
func (g *RogueGame) wanderer() {
tp := &Monster{}
var cp Coord
for {
cp, _ = g.findFloor(nil, 0, true)
if g.roomin(cp) != g.Player.Room {
break
}
}
g.newMonster(tp, g.randMonster(true), cp)
if g.Player.On(SeeMonst) {
g.standout()
if !g.Player.On(IsHalu) {
g.addch(tp.Type)
} else {
g.addch(byte(g.rnd(26) + 'A'))
}
g.standend()
}
g.runto(tp.Pos)
}
// wakeMonster is what to do when the hero steps next to a monster
// (monsters.c wake_monster).
func (g *RogueGame) wakeMonster(y, x int) *Monster {
p := &g.Player
tp := g.Level.MonsterAt(y, x)
if tp == nil {
panic("can't find monster in wake_monster")
}
ch := tp.Type
// Every time he sees a mean monster, it might start chasing him
if !tp.On(IsRun) && g.rnd(3) != 0 && tp.On(IsMean) && !tp.On(IsHeld) &&
!p.IsWearing(RStealth) && !p.On(IsLevit) {
tp.Dest = &p.Pos
tp.Flags.Set(IsRun)
}
if ch == 'M' && !p.On(IsBlind) && !p.On(IsHalu) &&
!tp.On(IsFound) && !tp.On(IsCanc) && tp.On(IsRun) {
rp := p.Room
if (rp != nil && !rp.Flags.Has(IsDark)) ||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
tp.Flags.Set(IsFound)
if !g.save(VsMagic) {
if p.On(IsHuh) {
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
} else {
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
}
p.Flags.Set(IsHuh)
mname := g.setMname(tp)
g.addmsg("%s", mname)
if mname != "it" {
g.addmsg("'")
}
g.msg("s gaze has confused you")
}
}
}
// Let greedy ones guard gold
if tp.On(IsGreed) && !tp.On(IsRun) {
tp.Flags.Set(IsRun)
if p.Room.GoldVal != 0 {
tp.Dest = &p.Room.Gold
} else {
tp.Dest = &p.Pos
}
}
return tp
}
// givePack gives a pack to a monster if it deserves one (monsters.c
// give_pack).
func (g *RogueGame) givePack(tp *Monster) {
if g.Depth >= g.MaxDepth && g.rnd(100) < monsterTable[tp.Type-'A'].Carry {
attachObj(&tp.Pack, g.newThing())
}
}
// saveThrow sees if a creature saves against something (monsters.c
// save_throw).
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
need := 14 + which - st.Lvl/2
return g.roll(1, 20) >= need
}
// save sees if the hero saves against various nasty things (monsters.c
// save).
func (g *RogueGame) save(which int) bool {
p := &g.Player
if which == VsMagic {
if p.IsRing(Left, RProtect) {
which -= p.CurRing[Left].Arm
}
if p.IsRing(Right, RProtect) {
which -= p.CurRing[Right].Arm
}
}
return g.saveThrow(which, &p.Stats)
}