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:
231
game/io.go
Normal file
231
game/io.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// io.c — the message line, the status line, and character input.
|
||||
|
||||
// maxMsg is io.c MAXMSG: how much message fits before --More--.
|
||||
const maxMsg = NumCols - len("--More--") - 1
|
||||
|
||||
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus
|
||||
// the related globals (mpos, huh, and the message-behavior flags).
|
||||
type MsgLine struct {
|
||||
buf strings.Builder // msgbuf
|
||||
newpos int
|
||||
Mpos int // where cursor is on top line
|
||||
Huh string // the last message printed
|
||||
SaveMsg bool // remember last msg
|
||||
LowerMsg bool // messages should start w/lower case
|
||||
MsgEsc bool // check for ESC from msg's --More--
|
||||
}
|
||||
|
||||
// Msg displays a message at the top of the screen (io.c msg). It returns
|
||||
// Escape if the player escaped out of a --More--, ^Escape otherwise (the C
|
||||
// convention: callers compare against ESCAPE).
|
||||
func (g *RogueGame) msg(format string, a ...any) int {
|
||||
// if the string is "", just clear the line
|
||||
if format == "" {
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.Msgs.Mpos = 0
|
||||
return ^Escape
|
||||
}
|
||||
// otherwise add to the message and flush it out
|
||||
g.doadd(format, a...)
|
||||
return g.endmsg()
|
||||
}
|
||||
|
||||
// addmsg adds things to the current message (io.c addmsg).
|
||||
func (g *RogueGame) addmsg(format string, a ...any) {
|
||||
g.doadd(format, a...)
|
||||
}
|
||||
|
||||
// endmsg displays a new msg, giving the player a chance to see the previous
|
||||
// one if it is up there with the --More-- (io.c endmsg).
|
||||
func (g *RogueGame) endmsg() int {
|
||||
m := &g.Msgs
|
||||
if m.SaveMsg {
|
||||
m.Huh = m.buf.String()
|
||||
}
|
||||
if m.Mpos != 0 {
|
||||
g.look(false)
|
||||
g.mvaddstr(0, m.Mpos, "--More--")
|
||||
g.refresh()
|
||||
if !m.MsgEsc {
|
||||
g.waitFor(' ')
|
||||
} else {
|
||||
for {
|
||||
ch := g.readchar()
|
||||
if ch == ' ' {
|
||||
break
|
||||
}
|
||||
if ch == Escape {
|
||||
m.buf.Reset()
|
||||
m.Mpos = 0
|
||||
m.newpos = 0
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// All messages should start with uppercase, except ones that start
|
||||
// with a pack addressing character
|
||||
out := m.buf.String()
|
||||
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg &&
|
||||
!(len(out) > 1 && out[1] == ')') {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
g.mvaddstr(0, 0, out)
|
||||
g.clrtoeol()
|
||||
m.Mpos = m.newpos
|
||||
m.newpos = 0
|
||||
m.buf.Reset()
|
||||
g.refresh()
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// doadd performs an add onto the message buffer (io.c doadd).
|
||||
func (g *RogueGame) doadd(format string, a ...any) {
|
||||
m := &g.Msgs
|
||||
s := fmt.Sprintf(format, a...)
|
||||
if len(s)+m.newpos >= maxMsg {
|
||||
g.endmsg()
|
||||
}
|
||||
m.buf.WriteString(s)
|
||||
m.newpos = m.buf.Len()
|
||||
}
|
||||
|
||||
// stepOk returns true if it is ok to step on ch (io.c step_ok).
|
||||
func stepOk(ch byte) bool {
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
return false
|
||||
default:
|
||||
return !isAlpha(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// readchar reads and returns a character, checking for gross input errors
|
||||
// (io.c readchar).
|
||||
func (g *RogueGame) readchar() byte {
|
||||
ch := g.scr.term.ReadChar()
|
||||
if ch == 3 { // ^C
|
||||
g.quit(0)
|
||||
return 27
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// statusCache is the set of static shadow variables in io.c status() that
|
||||
// suppress redundant status-line redraws.
|
||||
type statusCache struct {
|
||||
hpwidth int
|
||||
hungry int
|
||||
lvl int
|
||||
pur int
|
||||
hp int
|
||||
arm int
|
||||
str int
|
||||
exp int
|
||||
init bool
|
||||
}
|
||||
|
||||
var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"}
|
||||
|
||||
// status displays the important stats line, keeping the cursor where it was
|
||||
// (io.c status).
|
||||
func (g *RogueGame) status() {
|
||||
s := &g.statusCache
|
||||
p := &g.Player
|
||||
|
||||
// If nothing has changed since the last status, don't bother.
|
||||
temp := p.Stats.Arm
|
||||
if p.CurArmor != nil {
|
||||
temp = p.CurArmor.Arm
|
||||
}
|
||||
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
|
||||
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
|
||||
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg {
|
||||
return
|
||||
}
|
||||
s.init = true
|
||||
s.arm = temp
|
||||
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
if s.hp != p.Stats.MaxHP {
|
||||
s.hp = p.Stats.MaxHP
|
||||
s.hpwidth = 0
|
||||
for t := p.Stats.MaxHP; t != 0; t /= 10 {
|
||||
s.hpwidth++
|
||||
}
|
||||
}
|
||||
|
||||
// Save current status
|
||||
s.lvl = g.Depth
|
||||
s.pur = p.Purse
|
||||
s.hp = p.Stats.HP
|
||||
s.str = p.Stats.Str
|
||||
s.exp = p.Stats.Exp
|
||||
s.hungry = p.HungryState
|
||||
|
||||
line := fmt.Sprintf(
|
||||
"Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s",
|
||||
g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP,
|
||||
p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp,
|
||||
hungerStateName[p.HungryState])
|
||||
if g.StatMsg {
|
||||
g.move(0, 0)
|
||||
g.msg("%s", line)
|
||||
} else {
|
||||
g.move(StatLine, 0)
|
||||
g.addstr(line)
|
||||
}
|
||||
g.clrtoeol()
|
||||
g.move(oy, ox)
|
||||
}
|
||||
|
||||
// waitFor sits around until the guy types the right key (io.c wait_for).
|
||||
func (g *RogueGame) waitFor(ch byte) {
|
||||
if ch == '\n' {
|
||||
for {
|
||||
c := g.readchar()
|
||||
if c == '\n' || c == '\r' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for g.readchar() != ch {
|
||||
}
|
||||
}
|
||||
|
||||
// showWin displays a window and waits before returning (io.c show_win).
|
||||
func (g *RogueGame) showWin(message string) {
|
||||
g.scr.Hw.MvAddStr(0, 0, message)
|
||||
g.scr.Hw.Move(g.Player.Pos.Y, g.Player.Pos.X)
|
||||
g.scr.RefreshWin(g.scr.Hw)
|
||||
g.waitFor(' ')
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// ASCII helpers standing in for <ctype.h>; the C game only ever handles
|
||||
// 7-bit characters.
|
||||
func isAlpha(c byte) bool { return isUpper(c) || isLower(c) }
|
||||
func isUpper(c byte) bool { return c >= 'A' && c <= 'Z' }
|
||||
func isLower(c byte) bool { return c >= 'a' && c <= 'z' }
|
||||
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
|
||||
func isPrint(c byte) bool { return c >= ' ' && c < 0x7f }
|
||||
func toUpper(c byte) byte {
|
||||
if isLower(c) {
|
||||
return c - 'a' + 'A'
|
||||
}
|
||||
return c
|
||||
}
|
||||
func toLower(c byte) byte {
|
||||
if isUpper(c) {
|
||||
return c - 'A' + 'a'
|
||||
}
|
||||
return c
|
||||
}
|
||||
Reference in New Issue
Block a user