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

66
game/armor.go Normal file
View File

@@ -0,0 +1,66 @@
package game
// armor.c — misc functions for dealing with armor.
// wear lets the player put armor on (armor.c wear).
func (g *RogueGame) wear() {
p := &g.Player
obj := g.getItem("wear", int(Armor))
if obj == nil {
return
}
if p.CurArmor != nil {
g.addmsg("you are already wearing some")
if !g.Options.Terse {
g.addmsg(". You'll have to take it off first")
}
g.endmsg()
g.After = false
return
}
if obj.Type != Armor {
g.msg("you can't wear that")
return
}
g.wasteTime()
obj.Flags.Set(IsKnow)
sp := g.invName(obj, true)
p.CurArmor = obj
if !g.Options.Terse {
g.addmsg("you are now ")
}
g.msg("wearing %s", sp)
}
// takeOff gets the armor off of the player's back (armor.c take_off).
func (g *RogueGame) takeOff() {
p := &g.Player
obj := p.CurArmor
if obj == nil {
g.After = false
if g.Options.Terse {
g.msg("not wearing armor")
} else {
g.msg("you aren't wearing any armor")
}
return
}
if !g.dropCheck(p.CurArmor) {
return
}
p.CurArmor = nil
if g.Options.Terse {
g.addmsg("was")
} else {
g.addmsg("you used to be")
}
g.msg(" wearing %c) %s", obj.PackCh, g.invName(obj, true))
}
// wasteTime does nothing but let other things happen (armor.c waste_time).
func (g *RogueGame) wasteTime() {
g.DoDaemons(Before)
g.DoFuses(Before)
g.DoDaemons(After)
g.DoFuses(After)
}

138
game/chase.go Normal file
View File

@@ -0,0 +1,138 @@
package game
// chase.c — the navigation and visibility half. The chase driver (runners,
// move_monst, do_chase, chase) arrives with the combat phase, since it
// attacks the player and fires dragon bolts.
// setOldch sets the oldch character for the monster (chase.c set_oldch).
func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
if tp.Pos == cp {
return
}
sch := tp.OldCh
tp.OldCh = g.mvinch(cp.Y, cp.X)
if !g.Player.On(IsBlind) {
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(IsDark) {
tp.OldCh = ' '
} else if distCp(cp, g.Player.Pos) <= LampDist && g.Options.SeeFloor {
tp.OldCh = g.Level.Char(cp.Y, cp.X)
}
}
}
// seeMonst returns true if the hero can see the monster (chase.c
// see_monst).
func (g *RogueGame) seeMonst(mp *Monster) bool {
p := &g.Player
if p.On(IsBlind) {
return false
}
if mp.On(IsInvis) && !p.On(CanSee) {
return false
}
y, x := mp.Pos.Y, mp.Pos.X
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if y != p.Pos.Y && x != p.Pos.X &&
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
return false
}
return true
}
if mp.Room != p.Room {
return false
}
return !mp.Room.Flags.Has(IsDark)
}
// runto sets a monster running after the hero (chase.c runto).
func (g *RogueGame) runto(runner Coord) {
tp := g.Level.MonsterAt(runner.Y, runner.X)
if tp == nil {
return
}
// Start the beastie running
tp.Flags.Set(IsRun)
tp.Flags.Clear(IsHeld)
tp.Dest = g.findDest(tp)
}
// roomin finds what room some coordinates are in; nil means they aren't in
// any room (chase.c roomin).
func (g *RogueGame) roomin(cp Coord) *Room {
fp := *g.Level.FlagsAt(cp.Y, cp.X)
if fp.Has(FPass) {
return &g.Level.Passages[fp&FPNum]
}
for i := range g.Level.Rooms {
rp := &g.Level.Rooms[i]
if cp.X <= rp.Pos.X+rp.Max.X && rp.Pos.X <= cp.X &&
cp.Y <= rp.Pos.Y+rp.Max.Y && rp.Pos.Y <= cp.Y {
return rp
}
}
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
return nil
}
// diagOk checks to see if a diagonal move is legal (chase.c diag_ok).
func (g *RogueGame) diagOk(sp, ep Coord) bool {
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
return false
}
if ep.X == sp.X || ep.Y == sp.Y {
return true
}
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
}
// cansee returns true if the hero can see a certain coordinate (chase.c
// cansee).
func (g *RogueGame) cansee(y, x int) bool {
p := &g.Player
if p.On(IsBlind) {
return false
}
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
if g.Level.FlagsAt(y, x).Has(FPass) {
if y != p.Pos.Y && x != p.Pos.X &&
!stepOk(g.Level.Char(y, p.Pos.X)) &&
!stepOk(g.Level.Char(p.Pos.Y, x)) {
return false
}
}
return true
}
// We can only see if the hero is in the same room as the coordinate
// and the room is lit, or if it is close.
rer := g.roomin(Coord{X: x, Y: y})
return rer == p.Room && !rer.Flags.Has(IsDark)
}
// findDest finds the proper destination for the monster (chase.c
// find_dest).
func (g *RogueGame) findDest(tp *Monster) *Coord {
prob := monsterTable[tp.Type-'A'].Carry
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
return &g.Player.Pos
}
for _, obj := range g.Level.Objects {
if obj.Type == Scroll && obj.Which == SScare {
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
}
}
}
return &g.Player.Pos
}

View File

@@ -1,17 +1,205 @@
package game
// daemons.c — the daemon and fuse callbacks. Each callback is ported in the
// phase that owns its subsystem; runDaemon is the dispatch switch that
// replaces the C function pointers.
// daemons.c — the daemon and fuse callbacks, dispatched by DaemonID.
// stomach() arrives with the endgame phase (starvation calls death()).
// runDaemon invokes the callback named by id (the call through d_func in C).
func (g *RogueGame) runDaemon(id DaemonID, arg int) {
_ = arg
switch id {
case DRollwand:
g.rollwand(arg)
case DDoctor:
g.doctor(arg)
case DSwander:
g.swander(arg)
case DNohaste:
g.nohaste(arg)
case DUnconfuse:
g.unconfuse(arg)
case DUnsee:
g.unsee(arg)
case DSight:
g.sight(arg)
case DVisuals:
g.visuals(arg)
case DComeDown:
g.comeDown(arg)
case DLand:
g.land(arg)
default:
// Callbacks are added to this switch as their subsystems are
// ported; reaching one that isn't here is a porting bug, not a
// game state.
// ported; reaching one that isn't here is a porting bug.
panic("daemon not yet ported")
}
}
// doctor is the healing daemon that restores hit points after rest
// (daemons.c doctor).
func (g *RogueGame) doctor(int) {
p := &g.Player
lv := p.Stats.Lvl
ohp := p.Stats.HP
g.Quiet++
if lv < 8 {
if g.Quiet+(lv<<1) > 20 {
p.Stats.HP++
}
} else if g.Quiet >= 3 {
p.Stats.HP += g.rnd(lv-7) + 1
}
if p.IsRing(Left, RRegen) {
p.Stats.HP++
}
if p.IsRing(Right, RRegen) {
p.Stats.HP++
}
if ohp != p.Stats.HP {
if p.Stats.HP > p.Stats.MaxHP {
p.Stats.HP = p.Stats.MaxHP
}
g.Quiet = 0
}
}
// swander is called when it is time to start rolling for wandering monsters
// (daemons.c swander).
func (g *RogueGame) swander(int) {
g.StartDaemon(DRollwand, 0, Before)
}
// rollwand rolls to see if a wandering monster starts up (daemons.c
// rollwand).
func (g *RogueGame) rollwand(int) {
if g.Daemons.Between++; g.Daemons.Between >= 4 {
if g.roll(1, 6) == 4 {
g.wanderer()
g.KillDaemon(DRollwand)
g.Fuse(DSwander, 0, wanderTime(g), Before)
}
g.Daemons.Between = 0
}
}
// wanderTime is the WANDERTIME macro: spread(70).
func wanderTime(g *RogueGame) int { return g.spread(70) }
// unconfuse releases the poor player from his confusion (daemons.c
// unconfuse).
func (g *RogueGame) unconfuse(int) {
g.Player.Flags.Clear(IsHuh)
g.msg("you feel less %s now", g.chooseStr("trippy", "confused"))
}
// unsee turns off the ability to see invisible (daemons.c unsee).
func (g *RogueGame) unsee(int) {
for _, th := range g.Level.Monsters {
if th.On(IsInvis) && g.seeMonst(th) {
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
}
}
g.Player.Flags.Clear(CanSee)
}
// sight gives the hero his sight back (daemons.c sight).
func (g *RogueGame) sight(int) {
p := &g.Player
if p.On(IsBlind) {
g.Extinguish(DSight)
p.Flags.Clear(IsBlind)
if !p.Room.Flags.Has(IsGone) {
g.enterRoom(p.Pos)
}
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
"the veil of darkness lifts"))
}
}
// nohaste ends the hasting (daemons.c nohaste).
func (g *RogueGame) nohaste(int) {
g.Player.Flags.Clear(IsHaste)
g.msg("you feel yourself slowing down")
}
// comeDown takes the hero down off her acid trip (daemons.c come_down).
func (g *RogueGame) comeDown(int) {
p := &g.Player
if !p.On(IsHalu) {
return
}
g.KillDaemon(DVisuals)
p.Flags.Clear(IsHalu)
if p.On(IsBlind) {
return
}
// undo the things
for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Type)
}
}
// undo the monsters
seemonst := p.On(SeeMonst)
for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X)
if g.cansee(tp.Pos.Y, tp.Pos.X) {
if !tp.On(IsInvis) || p.On(CanSee) {
g.addch(tp.Disguise)
} else {
g.addch(g.Level.Char(tp.Pos.Y, tp.Pos.X))
}
} else if seemonst {
g.standout()
g.addch(tp.Type)
g.standend()
}
}
g.msg("Everything looks SO boring now.")
}
// visuals changes the characters for the player while hallucinating
// (daemons.c visuals).
func (g *RogueGame) visuals(int) {
p := &g.Player
if !g.After || (g.Running && g.Options.Jump) {
return
}
// change the things
for _, tp := range g.Level.Objects {
if g.cansee(tp.Pos.Y, tp.Pos.X) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing())
}
}
// change the stairs
if !g.SeenStairs && g.cansee(g.Level.Stairs.Y, g.Level.Stairs.X) {
g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing())
}
// change the monsters
seemonst := p.On(SeeMonst)
for _, tp := range g.Level.Monsters {
g.move(tp.Pos.Y, tp.Pos.X)
if g.seeMonst(tp) {
if tp.Type == 'X' && tp.Disguise != 'X' {
g.addch(g.rndThing())
} else {
g.addch(byte(g.rnd(26) + 'A'))
}
} else if seemonst {
g.standout()
g.addch(byte(g.rnd(26) + 'A'))
g.standend()
}
}
}
// land lands the hero from a levitation potion (daemons.c land).
func (g *RogueGame) land(int) {
g.Player.Flags.Clear(IsLevit)
g.msg("%s", g.chooseStr("bummer! You've hit the ground",
"you float gently to the ground"))
}

28
game/fight.go Normal file
View File

@@ -0,0 +1,28 @@
package game
// fight.c — combat. setMname arrives first (monster wake-ups need it); the
// combat resolution functions come with the combat phase.
// setMname returns the monster name for the given monster (fight.c
// set_mname).
func (g *RogueGame) setMname(tp *Monster) string {
if !g.seeMonst(tp) && !g.Player.On(SeeMonst) {
if g.Options.Terse {
return "it"
}
return "something"
}
var mname string
if g.Player.On(IsHalu) {
ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X))
if !isUpper(byte(ch)) {
ch = g.rnd(26)
} else {
ch -= 'A'
}
mname = monsterTable[ch].Name
} else {
mname = monsterTable[tp.Type-'A'].Name
}
return "the " + mname
}

View File

@@ -33,12 +33,13 @@ type Options struct {
// Config carries everything needed to construct a game.
type Config struct {
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
Name string // player name (overridden by ROGUEOPTS name=)
RogueOpts string // the ROGUEOPTS environment string
Home string // home directory (save file default location)
ScorePath string // scoreboard file; empty disables scoring
Wizard bool // enable debug commands (implies NoScore)
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
Name string // player name (overridden by ROGUEOPTS name=)
RogueOpts string // the ROGUEOPTS environment string
Home string // home directory (save file default location)
ScorePath string // scoreboard file; empty disables scoring
Wizard bool // enable debug commands (implies NoScore)
Term Terminal // the display/input device; nil runs headless
}
// RogueGame is one complete game of Rogue: every piece of state that was a
@@ -92,11 +93,23 @@ type RogueGame struct {
LLastDir byte
LastPick *Object
LLastPick *Object
lastDelt Coord // misc.c get_dir static last_delt
// dungeon generation working state
maze mazeState // rooms.c maze statics
pnum int // passages.c passnum statics
newpnum bool
// daemons/fuses
Daemons DaemonList
// UI state (the Screen itself arrives with the UI phase)
// screen / messages
scr *Screen
Msgs MsgLine
statusCache statusCache
invPage invPage // things.c discovery-list pagination statics
// UI state
StatMsg bool // should status() print as a msg()
InvDescribe bool // say which way items are being used
QComm bool // are we executing a 'Q' command?
@@ -138,18 +151,58 @@ func NewGame(cfg Config) *RogueGame {
InvType: InvOver,
}
g.InvDescribe = true
g.Msgs.SaveMsg = true
g.scr = NewScreen(cfg.Term)
g.FileName = cfg.Home + "/rogue.save"
if cfg.Wizard {
g.Player.Flags.Set(SeeMonst)
}
// TODO(port): parse cfg.RogueOpts once options.go lands.
g.initProbs() // set up prob tables for objects
// TODO(port): initPlayer() goes exactly here (RNG order!) once pack.go lands.
g.Items.Group = 2 // weapons.c: int group = 2
for i := range g.Level.Passages {
g.Level.Passages[i].Flags = IsGone | IsDark
}
g.initProbs() // set up prob tables for objects
g.initPlayer() // set up initial player stats
g.initNames() // set up names of scrolls
g.initColors() // set up colors of potions
g.initStones() // set up stone settings of rings
g.initMaterials() // set up materials of wands
// TODO(port): new level + daemon start-up once their phases land.
return g
}
// quit has the player make certain, then exits (main.c quit). The final
// scoring display arrives with the endgame phase.
func (g *RogueGame) quit(int) {
// Reset the message position in case we got here via an interrupt
if !g.QComm {
g.Msgs.Mpos = 0
}
oy, ox := g.scr.Std.GetYX()
g.msg("really quit?")
if g.readchar() == 'y' {
g.clear()
g.scr.Std.MvPrintw(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
g.move(NumLines-1, 0)
g.refresh()
// TODO(port): score(purse, 1, 0) once rip.go lands.
g.gameOver()
return
}
g.move(0, 0)
g.clrtoeol()
g.status()
g.move(oy, ox)
g.refresh()
g.Msgs.Mpos = 0
g.Count = 0
g.ToDeath = false
}
// gameOver ends the game loop; it replaces the C exit() calls so that Run
// can unwind normally and restore the terminal.
func (g *RogueGame) gameOver() {
g.Playing = false
}

View File

@@ -3,7 +3,49 @@ package game
import "strings"
// init.c — per-game randomization of item appearances and probability
// tables. initPlayer arrives with the pack/items phase (it needs addPack).
// tables, and the player roll-up.
// initPlayer rolls her up (init.c init_player).
func (g *RogueGame) initPlayer() {
p := &g.Player
p.MaxStats = initStats
p.Stats = p.MaxStats
p.FoodLeft = HungerTime
// Give him some food
obj := newObject()
obj.Type = Food
obj.Count = 1
g.addPack(obj, true)
// And his suit of armor
obj = newObject()
obj.Type = Armor
obj.Which = RingMail
obj.Arm = aClass[RingMail] - 1
obj.Flags.Set(IsKnow)
obj.Count = 1
p.CurArmor = obj
g.addPack(obj, true)
// Give him his weaponry. First a mace.
obj = newObject()
g.initWeapon(obj, Mace)
obj.HPlus = 1
obj.DPlus = 1
obj.Flags.Set(IsKnow)
g.addPack(obj, true)
p.CurWeapon = obj
// Now a +1 bow
obj = newObject()
g.initWeapon(obj, Bow)
obj.HPlus = 1
obj.Flags.Set(IsKnow)
g.addPack(obj, true)
// Now some arrows
obj = newObject()
g.initWeapon(obj, Arrow)
obj.Count = g.rnd(15) + 25
obj.Flags.Set(IsKnow)
g.addPack(obj, true)
}
// initColors initializes the potion color scheme for this game
// (init.c init_colors).

231
game/io.go Normal file
View 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
}

455
game/misc.go Normal file
View File

@@ -0,0 +1,455 @@
package game
// misc.c — look() display maintenance, direction input, eating, level-ups,
// and small utilities. call_it arrives with the scroll/potion phase (it
// needs the get_str line editor).
// look takes a quick glance all around the player (misc.c look).
func (g *RogueGame) look(wakeup bool) {
p := &g.Player
hero := p.Pos
passcount := 0
rp := p.Room
if g.Oldpos != hero {
g.eraseLamp(g.Oldpos, g.Oldrp)
g.Oldpos = hero
g.Oldrp = rp
}
ey := hero.Y + 1
ex := hero.X + 1
sx := hero.X - 1
sy := hero.Y - 1
sumhero, diffhero := 0, 0
if g.DoorStop && !g.Firstmove && g.Running {
sumhero = hero.Y + hero.X
diffhero = hero.Y - hero.X
}
pp := g.Level.At(hero.Y, hero.X)
pch := pp.Ch
pfl := pp.Flags
for y := sy; y <= ey; y++ {
if y <= 0 || y >= NumLines-1 {
continue
}
for x := sx; x <= ex; x++ {
if x < 0 || x >= NumCols {
continue
}
if !p.On(IsBlind) {
if y == hero.Y && x == hero.X {
continue
}
}
pp := g.Level.At(y, x)
ch := pp.Ch
if ch == ' ' { // nothing need be done with a ' '
continue
}
fp := &pp.Flags
if pch != Door && ch != Door {
if (pfl & FPass) != (*fp & FPass) {
continue
}
}
if (fp.Has(FPass) || ch == Door) && (pfl.Has(FPass) || pch == Door) {
if hero.X != x && hero.Y != y &&
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
continue
}
}
tp := pp.Monst
if tp == nil {
ch = g.tripCh(y, x, ch)
} else if p.On(SeeMonst) && tp.On(IsInvis) {
if g.DoorStop && !g.Firstmove {
g.Running = false
}
continue
} else {
if wakeup {
g.wakeMonster(y, x)
}
if g.seeMonst(tp) {
if p.On(IsHalu) {
ch = byte(g.rnd(26) + 'A')
} else {
ch = tp.Disguise
}
}
}
if p.On(IsBlind) && (y != hero.Y || x != hero.X) {
continue
}
g.move(y, x)
if p.Room.Flags.Has(IsDark) && !g.Options.SeeFloor && ch == Floor {
ch = ' '
}
if tp != nil || ch != g.inch() {
g.addch(ch)
}
if g.DoorStop && !g.Firstmove && g.Running {
switch g.RunCh {
case 'h':
if x == ex {
continue
}
case 'j':
if y == sy {
continue
}
case 'k':
if y == ey {
continue
}
case 'l':
if x == sx {
continue
}
case 'y':
if (y+x)-sumhero >= 1 {
continue
}
case 'u':
if (y-x)-diffhero >= 1 {
continue
}
case 'n':
if (y+x)-sumhero <= -1 {
continue
}
case 'b':
if (y-x)-diffhero <= -1 {
continue
}
}
switch ch {
case Door:
if x == hero.X || y == hero.Y {
g.Running = false
}
case Passage:
if x == hero.X || y == hero.Y {
passcount++
}
case Floor, '|', '-', ' ':
default:
g.Running = false
}
}
}
}
if g.DoorStop && !g.Firstmove && passcount > 1 {
g.Running = false
}
if !g.Running || !g.Options.Jump {
g.mvaddch(hero.Y, hero.X, PlayerCh)
}
}
// tripCh returns the character for this space, taking into account whether
// or not the player is tripping (misc.c trip_ch).
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
if g.Player.On(IsHalu) && g.After {
switch ch {
case Floor, ' ', Passage, '-', '|', Door, Trap:
default:
if y != g.Level.Stairs.Y || x != g.Level.Stairs.X || !g.SeenStairs {
ch = g.rndThing()
}
}
}
return ch
}
// eraseLamp erases the area shown by a lamp in a dark room
// (misc.c erase_lamp).
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
if !(g.Options.SeeFloor && rp.Flags&(IsGone|IsDark) == IsDark &&
!g.Player.On(IsBlind)) {
return
}
ey := pos.Y + 1
ex := pos.X + 1
sy := pos.Y - 1
for x := pos.X - 1; x <= ex; x++ {
for y := sy; y <= ey; y++ {
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
continue
}
g.move(y, x)
if g.inch() == Floor {
g.addch(' ')
}
}
}
}
// showFloor reports whether we show the floor in her room at this time
// (misc.c show_floor).
func (g *RogueGame) showFloor() bool {
if g.Player.Room.Flags&(IsGone|IsDark) == IsDark && !g.Player.On(IsBlind) {
return g.Options.SeeFloor
}
return true
}
// findObj finds the unclaimed object at (y, x) (misc.c find_obj).
func (g *RogueGame) findObj(y, x int) *Object {
for _, obj := range g.Level.Objects {
if obj.Pos.Y == y && obj.Pos.X == x {
return obj
}
}
return nil
}
// eat lets her try to eat something (misc.c eat).
func (g *RogueGame) eat() {
obj := g.getItem("eat", int(Food))
if obj == nil {
return
}
if obj.Type != Food {
if !g.Options.Terse {
g.msg("ugh, you would get ill if you ate that")
} else {
g.msg("that's Inedible!")
}
return
}
p := &g.Player
if p.FoodLeft < 0 {
p.FoodLeft = 0
}
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
p.FoodLeft = StomachSize
}
p.HungryState = 0
if obj == p.CurWeapon {
p.CurWeapon = nil
}
if obj.Which == 1 {
g.msg("my, that was a yummy %s", g.Fruit)
} else if g.rnd(100) > 70 {
p.Stats.Exp++
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
g.checkLevel()
} else {
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
}
g.leavePack(obj, false, false)
}
// checkLevel checks to see if the guy has gone up a level (misc.c
// check_level).
func (g *RogueGame) checkLevel() {
p := &g.Player
var i int
for i = 0; eLevels[i] != 0; i++ {
if eLevels[i] > p.Stats.Exp {
break
}
}
i++
olevel := p.Stats.Lvl
p.Stats.Lvl = i
if i > olevel {
add := g.roll(i-olevel, 10)
p.Stats.MaxHP += add
p.Stats.HP += add
g.msg("welcome to level %d", i)
}
}
// chgStr modifies the player's strength, keeping track of the highest it
// has been (misc.c chg_str).
func (g *RogueGame) chgStr(amt int) {
if amt == 0 {
return
}
p := &g.Player
addStr(&p.Stats.Str, amt)
comp := p.Stats.Str
if p.IsRing(Left, RAddStr) {
addStr(&comp, -p.CurRing[Left].Arm)
}
if p.IsRing(Right, RAddStr) {
addStr(&comp, -p.CurRing[Right].Arm)
}
if comp > p.MaxStats.Str {
p.MaxStats.Str = comp
}
}
// addStr performs the actual strength add, checking bounds (misc.c add_str).
func addStr(sp *int, amt int) {
if *sp += amt; *sp < 3 {
*sp = 3
} else if *sp > 31 {
*sp = 31
}
}
// addHaste adds a haste to the player (misc.c add_haste).
func (g *RogueGame) addHaste(potion bool) bool {
p := &g.Player
if p.On(IsHaste) {
g.NoCommand += g.rnd(8)
p.Flags.Clear(IsRun | IsHaste)
g.Extinguish(DNohaste)
g.msg("you faint from exhaustion")
return false
}
p.Flags.Set(IsHaste)
if potion {
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
}
return true
}
// aggravate aggravates all the monsters on this level (misc.c aggravate).
func (g *RogueGame) aggravate() {
// runto() can splice the monster list while we walk it, so iterate a copy.
monsters := append([]*Monster(nil), g.Level.Monsters...)
for _, mp := range monsters {
g.runto(mp.Pos)
}
}
// vowelstr returns "n" if the string starts with a vowel, for "a"/"an"
// (misc.c vowelstr).
func vowelstr(str string) string {
if str == "" {
return ""
}
switch str[0] {
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
return "n"
}
return ""
}
// isCurrent sees if the object is one of the currently used items
// (misc.c is_current).
func (g *RogueGame) isCurrent(obj *Object) bool {
if obj == nil {
return false
}
p := &g.Player
if obj == p.CurArmor || obj == p.CurWeapon ||
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
if !g.Options.Terse {
g.addmsg("That's already ")
}
g.msg("in use")
return true
}
return false
}
// getDir sets up the direction coordinate for use in various "prefix"
// commands (misc.c get_dir).
func (g *RogueGame) getDir() bool {
if g.Again && g.LastDir != 0 {
g.Delta = g.lastDelt
g.DirCh = g.LastDir
} else {
prompt := "direction: "
if !g.Options.Terse {
prompt = "which direction? "
g.msg("%s", prompt)
}
for {
gotit := true
switch g.DirCh = g.readchar(); g.DirCh {
case 'h', 'H':
g.Delta = Coord{X: -1, Y: 0}
case 'j', 'J':
g.Delta = Coord{X: 0, Y: 1}
case 'k', 'K':
g.Delta = Coord{X: 0, Y: -1}
case 'l', 'L':
g.Delta = Coord{X: 1, Y: 0}
case 'y', 'Y':
g.Delta = Coord{X: -1, Y: -1}
case 'u', 'U':
g.Delta = Coord{X: 1, Y: -1}
case 'b', 'B':
g.Delta = Coord{X: -1, Y: 1}
case 'n', 'N':
g.Delta = Coord{X: 1, Y: 1}
case Escape:
g.LastDir = 0
g.resetLast()
return false
default:
g.Msgs.Mpos = 0
g.msg("%s", prompt)
gotit = false
}
if gotit {
break
}
}
g.DirCh = toLower(g.DirCh)
g.LastDir = g.DirCh
g.lastDelt = g.Delta
}
if g.Player.On(IsHuh) && g.rnd(5) == 0 {
for {
g.Delta.Y = g.rnd(3) - 1
g.Delta.X = g.rnd(3) - 1
if g.Delta.Y != 0 || g.Delta.X != 0 {
break
}
}
}
g.Msgs.Mpos = 0
return true
}
// thingList is misc.c rnd_thing()'s static table.
var thingList = []byte{
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
}
// rndThing picks a random thing appropriate for this level (misc.c
// rnd_thing).
func (g *RogueGame) rndThing() byte {
var i int
if g.Depth >= AmuletLevel {
i = g.rnd(len(thingList))
} else {
i = g.rnd(len(thingList) - 1)
}
return thingList[i]
}
// chooseStr picks the first or second string depending on whether the
// player is tripping (misc.c choose_str).
func (g *RogueGame) chooseStr(ts, ns string) string {
if g.Player.On(IsHalu) {
return ts
}
return ns
}
// unctrl gives a printable representation of a character, like curses
// unctrl(): control characters display as ^X.
func unctrl(ch byte) string {
if ch < ' ' {
return "^" + string(ch+'@')
}
if ch == 0x7f {
return "^?"
}
return string(ch)
}

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)
}

19
game/move.go Normal file
View File

@@ -0,0 +1,19 @@
package game
// move.c — hero movement. doorOpen arrives first (room entry needs it);
// the movement commands and traps come with the UI phase.
// doorOpen is called to wake up things in a room that might move when the
// hero enters (move.c door_open).
func (g *RogueGame) doorOpen(rp *Room) {
if rp.Flags.Has(IsGone) {
return
}
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
if isUpper(g.Level.VisibleChar(y, x)) {
g.wakeMonster(y, x)
}
}
}
}

159
game/newlevel.go Normal file
View File

@@ -0,0 +1,159 @@
package game
// new_level.c — dig and draw a new level.
const (
treasRoomChance = 20 // one chance in TREAS_ROOM for a treasure room
maxTreas = 10 // maximum number of treasures in a treasure room
minTreas = 2 // minimum number of treasures in a treasure room
maxTries = 10 // max number of tries to put down a monster
)
// NewLevel digs and draws a new level (new_level.c new_level).
func (g *RogueGame) NewLevel() {
p := &g.Player
p.Flags.Clear(IsHeld) // unhold when you go down just in case
if g.Depth > g.MaxDepth {
g.MaxDepth = g.Depth
}
// Clean things off from last level
for i := range g.Level.Places {
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
}
g.clear()
// Free up the monsters on the last level; the objects and their packs
// go with them (the garbage collector is our free_list).
g.Level.Monsters = nil
g.Level.Objects = nil
g.doRooms() // Draw rooms
g.doPassages() // Draw passages
p.NoFood++
g.putThings() // Place objects (if any)
// Place the traps
if g.rnd(10) < g.Depth {
g.Level.NTraps = g.rnd(g.Depth/4) + 1
if g.Level.NTraps > MaxTraps {
g.Level.NTraps = MaxTraps
}
for i := g.Level.NTraps; i > 0; i-- {
// not only wouldn't it be NICE to have traps in mazes (not
// that we care about being nice), since the trap number is
// stored where the passage number is, we can't actually do it.
var stairs Coord
for {
stairs, _ = g.findFloor(nil, 0, false)
if g.Level.Char(stairs.Y, stairs.X) == Floor {
break
}
}
sp := g.Level.FlagsAt(stairs.Y, stairs.X)
sp.Clear(FReal)
*sp |= PlaceFlags(g.rnd(NTraps))
}
}
// Place the staircase down.
stairs, _ := g.findFloor(nil, 0, false)
g.Level.Stairs = stairs
g.Level.SetChar(stairs.Y, stairs.X, Stairs)
g.SeenStairs = false
for _, tp := range g.Level.Monsters {
tp.Room = g.roomin(tp.Pos)
}
hero, _ := g.findFloor(nil, 0, true)
p.Pos = hero
g.enterRoom(hero)
g.mvaddch(hero.Y, hero.X, PlayerCh)
if p.On(SeeMonst) {
g.turnSee(false)
}
if p.On(IsHalu) {
g.visuals(0)
}
}
// rndRoom picks a room that is really there (new_level.c rnd_room).
func (g *RogueGame) rndRoom() int {
for {
rm := g.rnd(MaxRooms)
if !g.Level.Rooms[rm].Flags.Has(IsGone) {
return rm
}
}
}
// putThings puts potions and scrolls on this level (new_level.c
// put_things).
func (g *RogueGame) putThings() {
// Once you have found the amulet, the only way to get new stuff is to
// go down into the dungeon.
if g.HasAmulet && g.Depth < g.MaxDepth {
return
}
// check for treasure rooms, and if so, put it in.
if g.rnd(treasRoomChance) == 0 {
g.treasRoom()
}
// Do MAXOBJ attempts to put things on a level
for i := 0; i < MaxObj; i++ {
if g.rnd(100) < 36 {
// Pick a new object and link it in the list
obj := g.newThing()
attachObj(&g.Level.Objects, obj)
// Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Type)
}
}
// If he is really deep in the dungeon and he hasn't found the amulet
// yet, put it somewhere on the ground
if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject()
attachObj(&g.Level.Objects, obj)
obj.Damage = "0x0"
obj.HurlDmg = "0x0"
obj.Arm = 11
obj.Type = Amulet
// Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false)
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet)
}
}
// treasRoom adds a treasure room (new_level.c treas_room).
func (g *RogueGame) treasRoom() {
rp := &g.Level.Rooms[g.rndRoom()]
spots := (rp.Max.Y-2)*(rp.Max.X-2) - minTreas
if spots > maxTreas-minTreas {
spots = maxTreas - minTreas
}
numMonst := g.rnd(spots) + minTreas
for nm := numMonst; nm > 0; nm-- {
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
tp := g.newThing()
tp.Pos = mp
attachObj(&g.Level.Objects, tp)
g.Level.SetChar(mp.Y, mp.X, tp.Type)
}
// fill up room with monsters from the next level down
nm := g.rnd(spots) + minTreas
if nm < numMonst+2 {
nm = numMonst + 2
}
spots = (rp.Max.Y - 2) * (rp.Max.X - 2)
if nm > spots {
nm = spots
}
g.Depth++
for ; nm > 0; nm-- {
if mp, ok := g.findFloorIn(rp, maxTries, true); ok {
tp := &Monster{}
g.newMonster(tp, g.randMonster(false), mp)
tp.Flags.Set(IsMean) // no sloughers in THIS room
g.givePack(tp)
}
}
g.Depth--
}

124
game/newlevel_test.go Normal file
View File

@@ -0,0 +1,124 @@
package game
import (
"strings"
"testing"
)
func genLevel(t *testing.T, seed int32) *RogueGame {
t.Helper()
g := NewGame(Config{Seed: seed})
g.NewLevel()
return g
}
// renderMap draws the raw level map (not the screen) as text.
func renderMap(g *RogueGame) string {
var sb strings.Builder
for y := 0; y < NumLines; y++ {
for x := 0; x < NumCols; x++ {
ch := g.Level.Char(y, x)
if m := g.Level.MonsterAt(y, x); m != nil {
ch = m.Type
}
sb.WriteByte(ch)
}
sb.WriteByte('\n')
}
return sb.String()
}
func TestNewLevelInvariants(t *testing.T) {
for _, seed := range []int32{1, 12345, 2026, 99999} {
g := genLevel(t, seed)
// The staircase is somewhere real.
st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs {
t.Errorf("seed %d: no staircase at recorded stairs position", seed)
}
// The hero stands on a walkable, monster-free cell.
hp := g.Player.Pos
if !stepOk(g.Level.Char(hp.Y, hp.X)) {
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
g.Level.Char(hp.Y, hp.X))
}
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
t.Errorf("seed %d: hero standing on a monster", seed)
}
if g.Player.Room == nil {
t.Errorf("seed %d: hero not in any room", seed)
}
// Some rooms exist and are drawn.
m := renderMap(g)
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
t.Errorf("seed %d: no room walls drawn", seed)
}
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
t.Errorf("seed %d: no floor or passages drawn", seed)
}
// Every monster is indexed on the map and placed in a room.
for _, mon := range g.Level.Monsters {
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
t.Errorf("seed %d: monster %c not indexed at its position",
seed, mon.Type)
}
if mon.Room == nil {
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
}
}
// Every level object sits on a cell displaying its type (items can
// share cells only with monsters standing on them).
for _, obj := range g.Level.Objects {
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
if ch != obj.Type && g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil {
t.Errorf("seed %d: object %q at (%d,%d) but map shows %q",
seed, obj.Type, obj.Pos.Y, obj.Pos.X, ch)
}
}
// The player has her starting kit: food, armor, mace, bow, arrows.
if len(g.Player.Pack) != 5 {
t.Errorf("seed %d: starting pack has %d items, want 5",
seed, len(g.Player.Pack))
}
if g.Player.CurWeapon == nil || g.Player.CurWeapon.Which != Mace {
t.Errorf("seed %d: not wielding the starting mace", seed)
}
if g.Player.CurArmor == nil || g.Player.CurArmor.Which != RingMail {
t.Errorf("seed %d: not wearing the starting ring mail", seed)
}
}
}
func TestNewLevelDeterministic(t *testing.T) {
a := renderMap(genLevel(t, 12345))
b := renderMap(genLevel(t, 12345))
if a != b {
t.Error("same seed produced different levels")
}
c := renderMap(genLevel(t, 54321))
if a == c {
t.Error("different seeds produced identical levels")
}
}
// TestDeeperLevels exercises generation across many depths and seeds —
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
func TestDeeperLevels(t *testing.T) {
for _, seed := range []int32{7, 42, 1000, 31337} {
g := NewGame(Config{Seed: seed})
for depth := 1; depth <= 30; depth++ {
g.Depth = depth
g.NewLevel()
st := g.Level.Stairs
if g.Level.Char(st.Y, st.X) != Stairs {
t.Fatalf("seed %d depth %d: missing staircase", seed, depth)
}
}
}
}

400
game/pack.go Normal file
View File

@@ -0,0 +1,400 @@
package game
// pack.c — routines to deal with the pack.
// addPack picks up an object and adds it to the pack; if obj is non-nil use
// it instead of getting it off the ground (pack.c add_pack).
func (g *RogueGame) addPack(obj *Object, silent bool) {
p := &g.Player
fromFloor := false
if obj == nil {
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil {
return
}
fromFloor = true
}
// Check for and deal with scare monster scrolls
if obj.Type == Scroll && obj.Which == SScare && obj.Flags.Has(ObjIsFound) {
detachObj(&g.Level.Objects, obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(IsGone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
g.msg("the scroll turns to dust as you pick it up")
return
}
if len(p.Pack) == 0 {
p.Pack = append(p.Pack, obj)
obj.PackCh = g.packChar()
p.Inpack++
} else {
// Walk the pack looking for the insertion point, keeping items of
// one type together and merging stackable/grouped items — a direct
// translation of the C linked-list walk. lp is the index to insert
// after; -1 after a merge means no insertion.
lp := -1
merged := false
for i := 0; i < len(p.Pack); i++ {
if p.Pack[i].Type != obj.Type {
lp = i
continue
}
// found the group of our type: scan for matching subtype
for p.Pack[i].Type == obj.Type && p.Pack[i].Which != obj.Which {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op := p.Pack[i]
if op.Type == obj.Type && op.Which == obj.Which {
if IsMult(op.Type) {
if !g.packRoom(fromFloor, obj) {
return
}
op.Count++
obj = op
lp = -1
merged = true
} else if obj.Group != 0 {
lp = i
for p.Pack[i].Type == obj.Type &&
p.Pack[i].Which == obj.Which &&
p.Pack[i].Group != obj.Group {
lp = i
if i+1 >= len(p.Pack) {
break
}
i++
}
op = p.Pack[i]
if op.Type == obj.Type && op.Which == obj.Which &&
op.Group == obj.Group {
op.Count += obj.Count
p.Inpack--
if !g.packRoom(fromFloor, obj) {
return
}
obj = op
lp = -1
merged = true
}
} else {
lp = i
}
}
break
}
if !merged && lp != -1 {
if !g.packRoom(fromFloor, obj) {
return
}
obj.PackCh = g.packChar()
p.Pack = append(p.Pack[:lp+1],
append([]*Object{obj}, p.Pack[lp+1:]...)...)
}
}
obj.Flags.Set(ObjIsFound)
// If this was the object of something's desire, that monster will get
// mad and run at the hero.
for _, op := range g.Level.Monsters {
if op.Dest == &obj.Pos {
op.Dest = &p.Pos
}
}
if obj.Type == Amulet {
g.HasAmulet = true
}
// Notify the user
if !silent {
if !g.Options.Terse {
g.addmsg("you now have ")
}
g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh)
}
}
// packRoom sees if there's room in the pack; if not, prints an appropriate
// message (pack.c pack_room).
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
p := &g.Player
if p.Inpack++; p.Inpack > MaxPack {
if !g.Options.Terse {
g.addmsg("there's ")
}
g.addmsg("no room")
if !g.Options.Terse {
g.addmsg(" in your pack")
}
g.endmsg()
if fromFloor {
g.moveMsg(obj)
}
p.Inpack = MaxPack
return false
}
if fromFloor {
detachObj(&g.Level.Objects, obj)
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(IsGone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
}
return true
}
// leavePack takes an item out of the pack (pack.c leave_pack).
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
p := &g.Player
p.Inpack--
nobj := obj
if obj.Count > 1 && !all {
g.LastPick = obj
obj.Count--
if obj.Group != 0 {
p.Inpack++
}
if newobj {
copied := *obj
nobj = &copied
nobj.Count = 1
}
} else {
g.LastPick = nil
p.PackUsed[obj.PackCh-'a'] = false
detachObj(&p.Pack, obj)
}
return nobj
}
// packChar returns the next unused pack character (pack.c pack_char).
func (g *RogueGame) packChar() byte {
p := &g.Player
for i := range p.PackUsed {
if !p.PackUsed[i] {
p.PackUsed[i] = true
return byte(i) + 'a'
}
}
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
}
// inventory lists what is in the pack; returns true if there is something
// of the given type (pack.c inventory).
func (g *RogueGame) inventory(list []*Object, typ int) bool {
g.NObjs = 0
for _, item := range list {
if typ != 0 && typ != int(item.Type) &&
!(typ == Callable && item.Type != Food && item.Type != Amulet) &&
!(typ == RorS && (item.Type == Ring || item.Type == Stick)) {
continue
}
g.NObjs++
g.Msgs.MsgEsc = true
line := string(item.PackCh) + ") " + g.invName(item, false)
if g.addLine("%s", line) == Escape {
g.Msgs.MsgEsc = false
g.msg("")
return true
}
g.Msgs.MsgEsc = false
}
if g.NObjs == 0 {
if g.Options.Terse {
if typ == 0 {
g.msg("empty handed")
} else {
g.msg("nothing appropriate")
}
} else {
if typ == 0 {
g.msg("you are empty handed")
} else {
g.msg("you don't have anything appropriate")
}
}
return false
}
g.endLine()
return true
}
// pickUp adds something to the character's pack (pack.c pick_up).
func (g *RogueGame) pickUp(ch byte) {
p := &g.Player
if p.On(IsLevit) {
return
}
obj := g.findObj(p.Pos.Y, p.Pos.X)
if g.MoveOn {
g.moveMsg(obj)
return
}
switch ch {
case Gold:
if obj == nil {
return
}
g.money(obj.GoldVal())
detachObj(&g.Level.Objects, obj)
p.Room.GoldVal = 0
default:
g.addPack(nil, false)
}
}
// moveMsg prints the message if you are just moving onto an object
// (pack.c move_msg).
func (g *RogueGame) moveMsg(obj *Object) {
if !g.Options.Terse {
g.addmsg("you ")
}
g.msg("moved onto %s", g.invName(obj, true))
}
// pickyInven allows the player to inventory a single item (pack.c
// picky_inven).
func (g *RogueGame) pickyInven() {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return
}
if len(p.Pack) == 1 {
g.msg("a) %s", g.invName(p.Pack[0], false))
return
}
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
g.Msgs.Mpos = 0
mch := g.readchar()
if mch == Escape {
g.msg("")
return
}
for _, obj := range p.Pack {
if mch == obj.PackCh {
g.msg("%c) %s", mch, g.invName(obj, false))
return
}
}
g.msg("'%s' not in pack", unctrl(mch))
}
// getItem picks something out of a pack for a purpose (pack.c get_item).
func (g *RogueGame) getItem(purpose string, typ int) *Object {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you aren't carrying anything")
return nil
}
if g.Again {
if g.LastPick != nil {
return g.LastPick
}
g.msg("you ran out")
return nil
}
for {
if !g.Options.Terse {
g.addmsg("which object do you want to ")
}
g.addmsg("%s", purpose)
if g.Options.Terse {
g.addmsg(" what")
}
g.msg("? (* for list): ")
ch := g.readchar()
g.Msgs.Mpos = 0
// Give the poor player a chance to abort the command
if ch == Escape {
g.resetLast()
g.After = false
g.msg("")
return nil
}
g.NObjs = 1 // normal case: person types one char
if ch == '*' {
g.Msgs.Mpos = 0
if !g.inventory(p.Pack, typ) {
g.After = false
return nil
}
continue
}
for _, obj := range p.Pack {
if obj.PackCh == ch {
return obj
}
}
g.msg("'%s' is not a valid item", unctrl(ch))
}
}
// money adds or subtracts gold from the pack (pack.c money).
func (g *RogueGame) money(value int) {
p := &g.Player
p.Purse += value
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
if p.Room.Flags.Has(IsGone) {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
} else {
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
}
if value > 0 {
if !g.Options.Terse {
g.addmsg("you found ")
}
g.msg("%d gold pieces", value)
}
}
// floorCh returns the appropriate floor character for her room
// (pack.c floor_ch).
func (g *RogueGame) floorCh() byte {
if g.Player.Room.Flags.Has(IsGone) {
return Passage
}
if g.showFloor() {
return Floor
}
return ' '
}
// floorAt returns the character at the hero's position, taking see_floor
// into account (pack.c floor_at).
func (g *RogueGame) floorAt() byte {
ch := g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X)
if ch == Floor {
ch = g.floorCh()
}
return ch
}
// resetLast resets the last command when the current one is aborted
// (pack.c reset_last).
func (g *RogueGame) resetLast() {
g.LastComm = g.LLastComm
g.LastDir = g.LLastDir
g.LastPick = g.LLastPick
}
// chooseTerse picks the terse or verbose variant of a message.
func (g *RogueGame) chooseTerse(terse, verbose string) string {
if g.Options.Terse {
return terse
}
return verbose
}

302
game/passages.go Normal file
View File

@@ -0,0 +1,302 @@
package game
// passages.c — draw the connecting passages.
// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages.
var rdesConn = [MaxRooms][MaxRooms]bool{
{false, true, false, true, false, false, false, false, false},
{true, false, true, false, true, false, false, false, false},
{false, true, false, false, false, true, false, false, false},
{true, false, false, false, true, false, true, false, false},
{false, true, false, true, false, true, false, true, false},
{false, false, true, false, true, false, false, false, true},
{false, false, false, true, false, false, false, true, false},
{false, false, false, false, true, false, true, false, true},
{false, false, false, false, false, true, false, true, false},
}
// doPassages draws all the passages on a level (passages.c do_passages).
func (g *RogueGame) doPassages() {
var isconn [MaxRooms][MaxRooms]bool
var ingraph [MaxRooms]bool
// starting with one room, connect it to a random adjacent room and
// then pick a new room to start with.
roomcount := 1
r1 := g.rnd(MaxRooms)
ingraph[r1] = true
for {
// find a room to connect with
j := 0
r2 := -1
for i := 0; i < MaxRooms; i++ {
if rdesConn[r1][i] && !ingraph[i] {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
if j == 0 {
// if no adjacent rooms are outside the graph, pick a new room
// to look from
for {
r1 = g.rnd(MaxRooms)
if ingraph[r1] {
break
}
}
} else {
// otherwise, connect new room to the graph, and draw a tunnel
// to it
ingraph[r2] = true
g.conn(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true
roomcount++
}
if roomcount >= MaxRooms {
break
}
}
// attempt to add passages to the graph a random number of times so that
// there isn't always just one unique passage through it.
for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
r1 = g.rnd(MaxRooms) // a random room to look from
// find an adjacent room not already connected
j := 0
r2 := -1
for i := 0; i < MaxRooms; i++ {
if rdesConn[r1][i] && !isconn[r1][i] {
if j++; g.rnd(j) == 0 {
r2 = i
}
}
}
// if there is one, connect it and look for the next added passage
if j != 0 {
g.conn(r1, r2)
isconn[r1][r2] = true
isconn[r2][r1] = true
}
}
g.passnum()
}
// conn draws a corridor from a room in a certain direction (passages.c
// conn).
func (g *RogueGame) conn(r1, r2 int) {
var rm int
var direc byte
if r1 < r2 {
rm = r1
if r1+1 == r2 {
direc = 'r'
} else {
direc = 'd'
}
} else {
rm = r2
if r2+1 == r1 {
direc = 'r'
} else {
direc = 'd'
}
}
rpf := &g.Level.Rooms[rm]
// Set up the movement variables, in two cases: first drawing one down.
var rpt *Room
var del, turnDelta, spos, epos Coord
var distance, turnDistance int
if direc == 'd' {
rmt := rm + 3 // room # of dest
rpt = &g.Level.Rooms[rmt] // room pointer of dest
del = Coord{X: 0, Y: 1} // direction of move
spos = rpf.Pos // start of move
epos = rpt.Pos // end of move
if !rpf.Flags.Has(IsGone) { // if not gone pick door pos
for {
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) {
break
}
}
}
if !rpt.Flags.Has(IsGone) {
for {
epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) {
break
}
}
}
distance = abs(spos.Y-epos.Y) - 1 // distance to move
turnDelta.Y = 0 // direction to turn
if spos.X < epos.X {
turnDelta.X = 1
} else {
turnDelta.X = -1
}
turnDistance = abs(spos.X - epos.X) // how far to turn
} else { // setup for moving right
rmt := rm + 1
rpt = &g.Level.Rooms[rmt]
del = Coord{X: 1, Y: 0}
spos = rpf.Pos
epos = rpt.Pos
if !rpf.Flags.Has(IsGone) {
for {
spos.X = rpf.Pos.X + rpf.Max.X - 1
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) {
break
}
}
}
if !rpt.Flags.Has(IsGone) {
for {
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) {
break
}
}
}
distance = abs(spos.X-epos.X) - 1
if spos.Y < epos.Y {
turnDelta.Y = 1
} else {
turnDelta.Y = -1
}
turnDelta.X = 0
turnDistance = abs(spos.Y - epos.Y)
}
turnSpot := g.rnd(distance-1) + 1 // where turn starts
// Draw in the doors on either side of the passage or just put #'s if
// the rooms are gone.
if !rpf.Flags.Has(IsGone) {
g.door(rpf, spos)
} else {
g.putpass(spos)
}
if !rpt.Flags.Has(IsGone) {
g.door(rpt, epos)
} else {
g.putpass(epos)
}
// Get ready to move...
curr := spos
for distance > 0 {
// Move to new position
curr.X += del.X
curr.Y += del.Y
// Check if we are at the turn place, if so do the turn
if distance == turnSpot {
for ; turnDistance > 0; turnDistance-- {
g.putpass(curr)
curr.X += turnDelta.X
curr.Y += turnDelta.Y
}
}
// Continue digging along
g.putpass(curr)
distance--
}
curr.X += del.X
curr.Y += del.Y
if curr != epos {
g.msg("warning, connectivity problem on this level")
}
}
// putpass adds a passage character or secret passage here (passages.c
// putpass).
func (g *RogueGame) putpass(cp Coord) {
pp := g.Level.At(cp.Y, cp.X)
pp.Flags.Set(FPass)
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
pp.Flags.Clear(FReal)
} else {
pp.Ch = Passage
}
}
// door adds a door or possibly a secret door; also enters the door in the
// exits array of the room (passages.c door).
func (g *RogueGame) door(rm *Room, cp Coord) {
rm.Exits = append(rm.Exits, cp)
if rm.Flags.Has(IsMaze) {
return
}
pp := g.Level.At(cp.Y, cp.X)
if g.rnd(10)+1 < g.Depth && g.rnd(5) == 0 {
if cp.Y == rm.Pos.Y || cp.Y == rm.Pos.Y+rm.Max.Y-1 {
pp.Ch = '-'
} else {
pp.Ch = '|'
}
pp.Flags.Clear(FReal)
} else {
pp.Ch = Door
}
}
// passnum assigns a number to each passageway (passages.c passnum).
func (g *RogueGame) passnum() {
g.pnum = 0
g.newpnum = false
for i := range g.Level.Passages {
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
}
for i := range g.Level.Rooms {
rp := &g.Level.Rooms[i]
for j := range rp.Exits {
g.newpnum = true
g.numpass(rp.Exits[j].Y, rp.Exits[j].X)
}
}
}
// numpass numbers a passageway square and its brethren (passages.c
// numpass).
func (g *RogueGame) numpass(y, x int) {
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
return
}
fp := g.Level.FlagsAt(y, x)
if fp.Has(FPNum) {
return
}
if g.newpnum {
g.pnum++
g.newpnum = false
}
// check to see if it is a door or secret door, i.e., a new exit, or a
// numerable type of place
if ch := g.Level.Char(y, x); ch == Door ||
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
rp := &g.Level.Passages[g.pnum]
rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
} else if !fp.Has(FPass) {
return
}
*fp |= PlaceFlags(g.pnum)
// recurse on the surrounding places
g.numpass(y+1, x)
g.numpass(y-1, x)
g.numpass(y, x+1)
g.numpass(y, x-1)
}
// abs is C abs() for ints.
func abs(n int) int {
if n < 0 {
return -n
}
return n
}

85
game/potions.go Normal file
View File

@@ -0,0 +1,85 @@
package game
// potions.c — the visibility-related helpers arrive first; quaff and the
// potion effect dispatch come with the effects phase.
// isMagic reports whether an object radiates magic (potions.c is_magic).
func (o *Object) isMagic() bool {
switch o.Type {
case Armor:
return o.Flags.Has(IsProt) || o.Arm != aClass[o.Which]
case Weapon:
return o.HPlus != 0 || o.DPlus != 0
case Potion, Scroll, Stick, Ring, Amulet:
return true
}
return false
}
// invisOn turns on the ability to see invisible monsters (potions.c
// invis_on).
func (g *RogueGame) invisOn() {
g.Player.Flags.Set(CanSee)
for _, mp := range g.Level.Monsters {
if mp.On(IsInvis) && g.seeMonst(mp) && !g.Player.On(IsHalu) {
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
}
}
}
// turnSee puts on or off seeing monsters on this level (potions.c
// turn_see).
func (g *RogueGame) turnSee(turnOff bool) bool {
addNew := false
for _, mp := range g.Level.Monsters {
g.move(mp.Pos.Y, mp.Pos.X)
canSee := g.seeMonst(mp)
if turnOff {
if !canSee {
g.addch(mp.OldCh)
}
} else {
if !canSee {
g.standout()
}
if !g.Player.On(IsHalu) {
g.addch(mp.Type)
} else {
g.addch(byte(g.rnd(26) + 'A'))
}
if !canSee {
g.standend()
addNew = true
}
}
}
if turnOff {
g.Player.Flags.Clear(SeeMonst)
} else {
g.Player.Flags.Set(SeeMonst)
}
return addNew
}
// seenStairs reports whether the player has seen the stairs (potions.c
// seen_stairs).
func (g *RogueGame) seenStairs() bool {
st := g.Level.Stairs
g.move(st.Y, st.X)
if g.inch() == Stairs { // it's on the map
return true
}
if g.Player.Pos == st { // it's under him
return true
}
// if a monster is on the stairs, this gets hairy
if tp := g.Level.MonsterAt(st.Y, st.X); tp != nil {
if g.seeMonst(tp) && tp.On(IsRun) { // visible and awake:
return true // it must have moved there
}
if g.Player.On(SeeMonst) && tp.OldCh == Stairs {
return true
}
}
return false
}

174
game/rings.go Normal file
View File

@@ -0,0 +1,174 @@
package game
import "fmt"
// rings.c — routines dealing specifically with rings.
// ringOn puts a ring on a hand (rings.c ring_on).
func (g *RogueGame) ringOn() {
p := &g.Player
obj := g.getItem("put on", int(Ring))
// Make certain that it is something that we want to wear
if obj == nil {
return
}
if obj.Type != Ring {
if !g.Options.Terse {
g.msg("it would be difficult to wrap that around a finger")
} else {
g.msg("not a ring")
}
return
}
// find out which hand to put it on
if g.isCurrent(obj) {
return
}
var ring int
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if ring = g.gethand(); ring < 0 {
return
}
case p.CurRing[Left] == nil:
ring = Left
case p.CurRing[Right] == nil:
ring = Right
default:
if !g.Options.Terse {
g.msg("you already have a ring on each hand")
} else {
g.msg("wearing two")
}
return
}
p.CurRing[ring] = obj
// Calculate the effect it has on the poor guy.
switch obj.Which {
case RAddStr:
g.chgStr(obj.Arm)
case RSeeInvis:
g.invisOn()
case RAggr:
g.aggravate()
}
if !g.Options.Terse {
g.addmsg("you are now wearing ")
}
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh)
}
// ringOff takes off a ring (rings.c ring_off).
func (g *RogueGame) ringOff() {
p := &g.Player
var ring int
switch {
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
if g.Options.Terse {
g.msg("no rings")
} else {
g.msg("you aren't wearing any rings")
}
return
case p.CurRing[Left] == nil:
ring = Right
case p.CurRing[Right] == nil:
ring = Left
default:
if ring = g.gethand(); ring < 0 {
return
}
}
g.Msgs.Mpos = 0
obj := p.CurRing[ring]
if obj == nil {
g.msg("not wearing such a ring")
return
}
if g.dropCheck(obj) {
g.msg("was wearing %s(%c)", g.invName(obj, true), obj.PackCh)
}
}
// gethand asks which hand the hero is interested in (rings.c gethand).
func (g *RogueGame) gethand() int {
for {
if g.Options.Terse {
g.msg("left or right ring? ")
} else {
g.msg("left hand or right hand? ")
}
c := g.readchar()
if c == Escape {
return -1
}
g.Msgs.Mpos = 0
if c == 'l' || c == 'L' {
return Left
}
if c == 'r' || c == 'R' {
return Right
}
if g.Options.Terse {
g.msg("L or R")
} else {
g.msg("please type L or R")
}
}
}
// ringUses is the rings.c ring_eat static uses[] table: how much food each
// ring type uses up per turn (negative = a 1-in-n chance of 1).
var ringUses = [MaxRings]int{
1, // R_PROTECT
1, // R_ADDSTR
1, // R_SUSTSTR
-3, // R_SEARCH
-5, // R_SEEINVIS
0, // R_NOP
0, // R_AGGR
-3, // R_ADDHIT
-3, // R_ADDDAM
2, // R_REGEN
-2, // R_DIGEST
0, // R_TELEPORT
1, // R_STEALTH
1, // R_SUSTARM
}
// ringEat reports how much food the ring on the given hand uses up
// (rings.c ring_eat).
func (g *RogueGame) ringEat(hand int) int {
ring := g.Player.CurRing[hand]
if ring == nil {
return 0
}
eat := ringUses[ring.Which]
if eat < 0 {
if g.rnd(-eat) == 0 {
eat = 1
} else {
eat = 0
}
}
if ring.Which == RDigest {
eat = -eat
}
return eat
}
// ringNum prints ring bonuses (rings.c ring_num).
func ringNum(g *RogueGame, obj *Object) string {
if !obj.Flags.Has(IsKnow) {
return ""
}
switch obj.Which {
case RProtect, RAddStr, RAddDam, RAddHit:
return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring))
}
return ""
}

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)
}

198
game/screen.go Normal file
View File

@@ -0,0 +1,198 @@
package game
import "fmt"
// The screen layer replaces curses. Game code draws into Window cell
// buffers (stdscr and the hw scratch window); a Terminal implementation
// blits them to a real device. Tests run with a scripted Terminal (or none
// at all), which is also how the "screen is a data structure" idiom —
// C code reading back what it drew with inch() — stays intact headlessly.
// Terminal is the physical device: a tcell screen in the real game, a
// script in tests.
type Terminal interface {
// Render blits the window to the device.
Render(w *Window)
// ReadChar blocks for the next key, translated to Rogue's input bytes
// (arrows become hjkl, control keys their C0 codes).
ReadChar() byte
}
// cell is one screen position.
type cell struct {
ch byte
standout bool
}
// Window is an in-memory curses window: a cell grid with a cursor and a
// standout attribute.
type Window struct {
rows, cols int
cells []cell
cy, cx int
standout bool
}
// NewWindow returns a cleared window.
func NewWindow(rows, cols int) *Window {
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
w.Clear()
return w
}
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
// Move positions the cursor (curses move/wmove).
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
// GetYX reports the cursor position (curses getyx).
func (w *Window) GetYX() (y, x int) { return w.cy, w.cx }
// AddCh writes a character at the cursor and advances it (curses addch).
func (w *Window) AddCh(ch byte) {
if ch == '\n' {
w.cy, w.cx = w.cy+1, 0
return
}
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return
}
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
if w.cx++; w.cx >= w.cols {
w.cx = 0
if w.cy < w.rows-1 {
w.cy++
}
}
}
// AddStr writes a string at the cursor (curses addstr).
func (w *Window) AddStr(s string) {
for i := 0; i < len(s); i++ {
w.AddCh(s[i])
}
}
// MvAddCh moves then writes (curses mvaddch).
func (w *Window) MvAddCh(y, x int, ch byte) {
w.Move(y, x)
w.AddCh(ch)
}
// MvAddStr moves then writes (curses mvaddstr).
func (w *Window) MvAddStr(y, x int, s string) {
w.Move(y, x)
w.AddStr(s)
}
// Printw writes formatted text at the cursor (curses printw).
func (w *Window) Printw(format string, a ...any) {
w.AddStr(fmt.Sprintf(format, a...))
}
// MvPrintw moves then writes formatted text (curses mvprintw).
func (w *Window) MvPrintw(y, x int, format string, a ...any) {
w.Move(y, x)
w.Printw(format, a...)
}
// Inch returns the character under the cursor (curses inch, sans
// attributes — the C code always strips them with CCHAR).
func (w *Window) Inch() byte {
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
return ' '
}
return w.at(w.cy, w.cx).ch
}
// MvInch moves then reads (curses mvinch).
func (w *Window) MvInch(y, x int) byte {
w.Move(y, x)
return w.Inch()
}
// Standout sets or clears the standout attribute for subsequent writes
// (curses standout/standend).
func (w *Window) Standout(on bool) { w.standout = on }
// Clear blanks the window and homes the cursor (curses clear/wclear).
func (w *Window) Clear() {
for i := range w.cells {
w.cells[i] = cell{ch: ' '}
}
w.cy, w.cx = 0, 0
}
// Clrtoeol blanks from the cursor to the end of the line (curses clrtoeol).
func (w *Window) Clrtoeol() {
if w.cy < 0 || w.cy >= w.rows {
return
}
for x := w.cx; x < w.cols; x++ {
*w.at(w.cy, x) = cell{ch: ' '}
}
}
// CopyFrom copies another window's contents (curses overwrite).
func (w *Window) CopyFrom(src *Window) {
copy(w.cells, src.cells)
}
// Line returns row y as a trimmed string; used by tests and the death/
// victory screens.
func (w *Window) Line(y int) string {
buf := make([]byte, w.cols)
for x := 0; x < w.cols; x++ {
buf[x] = w.at(y, x).ch
}
return string(buf)
}
// Screen bundles the two windows the game draws on with the device that
// shows them.
type Screen struct {
term Terminal
Std *Window // stdscr: the dungeon view
Hw *Window // hw: the scratch window for overlays
}
// NewScreen builds the standard 24x80 game screen.
func NewScreen(term Terminal) *Screen {
return &Screen{
term: term,
Std: NewWindow(NumLines, NumCols),
Hw: NewWindow(NumLines, NumCols),
}
}
// Refresh pushes stdscr to the device (curses refresh).
func (s *Screen) Refresh() {
if s.term != nil {
s.term.Render(s.Std)
}
}
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
func (s *Screen) RefreshWin(w *Window) {
if s.term != nil {
s.term.Render(w)
}
}
// Thin RogueGame wrappers so ported bodies keep their curses shape.
func (g *RogueGame) move(y, x int) { g.scr.Std.Move(y, x) }
func (g *RogueGame) addch(ch byte) { g.scr.Std.AddCh(ch) }
func (g *RogueGame) addstr(s string) { g.scr.Std.AddStr(s) }
func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) }
func (g *RogueGame) mvaddstr(y, x int, s string) {
g.scr.Std.MvAddStr(y, x, s)
}
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printw(f, a...) }
func (g *RogueGame) inch() byte { return g.scr.Std.Inch() }
func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
func (g *RogueGame) standout() { g.scr.Std.Standout(true) }
func (g *RogueGame) standend() { g.scr.Std.Standout(false) }
func (g *RogueGame) clear() { g.scr.Std.Clear() }
func (g *RogueGame) clrtoeol() { g.scr.Std.Clrtoeol() }
func (g *RogueGame) refresh() { g.scr.Refresh() }

35
game/sticks.go Normal file
View File

@@ -0,0 +1,35 @@
package game
import "fmt"
// sticks.c — wand and staff setup and naming. Zapping (do_zap, drain,
// fire_bolt) arrives with the combat phase.
// fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" {
cur.Damage = "2x3"
} else {
cur.Damage = "1x1"
}
cur.HurlDmg = "1x1"
switch cur.Which {
case WsLight:
cur.SetCharges(g.rnd(10) + 10)
default:
cur.SetCharges(g.rnd(5) + 3)
}
}
// chargeStr returns the charge-count suffix for an identified stick
// (sticks.c charge_str).
func chargeStr(g *RogueGame, obj *Object) string {
if !obj.Flags.Has(IsKnow) {
return ""
}
if g.Options.Terse {
return fmt.Sprintf(" [%d]", obj.Charges())
}
return fmt.Sprintf(" [%d charges]", obj.Charges())
}

502
game/things.go Normal file
View File

@@ -0,0 +1,502 @@
package game
import (
"fmt"
"strings"
)
// things.c — item naming, random item creation, and the discovery list.
// invName returns the name of something as it would appear in an inventory
// (things.c inv_name).
func (g *RogueGame) invName(obj *Object, drop bool) string {
var pb strings.Builder
which := obj.Which
it := &g.Items
switch obj.Type {
case Potion:
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr)
case Ring:
g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum)
case Stick:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
case Scroll:
if obj.Count == 1 {
pb.WriteString("A scroll ")
} else {
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
}
op := &it.Scrolls[which]
if op.Know {
fmt.Fprintf(&pb, "of %s", op.Name)
} else if op.Guess != "" {
fmt.Fprintf(&pb, "called %s", op.Guess)
} else {
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
}
case Food:
if which == 1 {
if obj.Count == 1 {
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
} else {
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
}
} else {
if obj.Count == 1 {
pb.WriteString("Some food")
} else {
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
}
}
case Weapon:
sp := it.Weapons[which].Name
if obj.Count > 1 {
fmt.Fprintf(&pb, "%d ", obj.Count)
} else {
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
}
if obj.Flags.Has(IsKnow) {
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
} else {
pb.WriteString(sp)
}
if obj.Count > 1 {
pb.WriteString("s")
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
}
case Armor:
sp := it.Armors[which].Name
if obj.Flags.Has(IsKnow) {
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(&pb, "%d]", 10-obj.Arm)
} else {
pb.WriteString(sp)
}
if obj.Label != "" {
fmt.Fprintf(&pb, " called %s", obj.Label)
}
case Amulet:
pb.WriteString("The Amulet of Yendor")
case Gold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal())
}
out := pb.String()
if g.InvDescribe {
p := &g.Player
if obj == p.CurArmor {
out += " (being worn)"
}
if obj == p.CurWeapon {
out += " (weapon in hand)"
}
if obj == p.CurRing[Left] {
out += " (on left hand)"
} else if obj == p.CurRing[Right] {
out += " (on right hand)"
}
}
if out != "" {
if drop && isUpper(out[0]) {
out = string(toLower(out[0])) + out[1:]
} else if !drop && isLower(out[0]) {
out = string(toUpper(out[0])) + out[1:]
}
}
return out
}
// dropIt puts something down (things.c drop; renamed to avoid the
// leavePack/detach vocabulary collision).
func (g *RogueGame) dropIt() {
p := &g.Player
ch := g.Level.Char(p.Pos.Y, p.Pos.X)
if ch != Floor && ch != Passage {
g.After = false
g.msg("there is something there already")
return
}
obj := g.getItem("drop", 0)
if obj == nil {
return
}
if !g.dropCheck(obj) {
return
}
obj = g.leavePack(obj, true, !IsMult(obj.Type))
// Link it into the level object list
attachObj(&g.Level.Objects, obj)
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Type)
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
obj.Pos = p.Pos
if obj.Type == Amulet {
g.HasAmulet = false
}
g.msg("dropped %s", g.invName(obj, true))
}
// dropCheck does special checks for dropping or unwielding|unwearing|
// unringing (things.c dropcheck).
func (g *RogueGame) dropCheck(obj *Object) bool {
if obj == nil {
return true
}
p := &g.Player
if obj != p.CurArmor && obj != p.CurWeapon &&
obj != p.CurRing[Left] && obj != p.CurRing[Right] {
return true
}
if obj.Flags.Has(IsCursed) {
g.msg("you can't. It appears to be cursed")
return false
}
if obj == p.CurWeapon {
p.CurWeapon = nil
} else if obj == p.CurArmor {
g.wasteTime()
p.CurArmor = nil
} else {
hand := Right
if obj == p.CurRing[Left] {
hand = Left
}
p.CurRing[hand] = nil
switch obj.Which {
case RAddStr:
g.chgStr(-obj.Arm)
case RSeeInvis:
g.unsee(0)
g.Extinguish(DUnsee)
}
}
return true
}
// newThing returns a new random thing for the dungeon (things.c new_thing).
func (g *RogueGame) newThing() *Object {
cur := newObject()
cur.Damage = "0x0"
cur.HurlDmg = "0x0"
cur.Arm = 11
cur.Count = 1
// Decide what kind of object it will be; if we haven't had food for a
// while, let it be food.
var kind int
if g.Player.NoFood > 3 {
kind = 2
} else {
kind = pickOne(g, g.Items.Things[:])
}
switch kind {
case 0:
cur.Type = Potion
cur.Which = pickOne(g, g.Items.Potions[:])
case 1:
cur.Type = Scroll
cur.Which = pickOne(g, g.Items.Scrolls[:])
case 2:
cur.Type = Food
g.Player.NoFood = 0
if g.rnd(10) != 0 {
cur.Which = 0
} else {
cur.Which = 1
}
case 3:
g.initWeapon(cur, pickOne(g, g.Items.Weapons[:MaxWeapons]))
if r := g.rnd(100); r < 10 {
cur.Flags.Set(IsCursed)
cur.HPlus -= g.rnd(3) + 1
} else if r < 15 {
cur.HPlus += g.rnd(3) + 1
}
case 4:
cur.Type = Armor
cur.Which = pickOne(g, g.Items.Armors[:])
cur.Arm = aClass[cur.Which]
if r := g.rnd(100); r < 20 {
cur.Flags.Set(IsCursed)
cur.Arm += g.rnd(3) + 1
} else if r < 28 {
cur.Arm -= g.rnd(3) + 1
}
case 5:
cur.Type = Ring
cur.Which = pickOne(g, g.Items.Rings[:])
switch cur.Which {
case RAddStr, RProtect, RAddHit, RAddDam:
if cur.Arm = g.rnd(3); cur.Arm == 0 {
cur.Arm = -1
cur.Flags.Set(IsCursed)
}
case RAggr, RTeleport:
cur.Flags.Set(IsCursed)
}
case 6:
cur.Type = Stick
cur.Which = pickOne(g, g.Items.Sticks[:])
g.fixStick(cur)
}
return cur
}
// pickOne picks an item out of a list of possible objects using their
// cumulative probabilities (things.c pick_one).
func pickOne(g *RogueGame, info []ObjInfo) int {
i := g.rnd(100)
for idx := range info {
if i < info[idx].Prob {
return idx
}
}
return 0 // bad pick_one: C resets to the start of the table
}
// invPage is the things.c static pagination state for the discovery/
// inventory list windows (line_cnt, newpage, lastfmt/lastarg, maxlen).
type invPage struct {
lineCnt int
newpage bool
lastLine string
maxlen int
init bool
}
// discovered lists what the player has found of a certain type
// (things.c discovered).
func (g *RogueGame) discovered() {
var ch byte
for {
discList := false
if !g.Options.Terse {
g.addmsg("for ")
}
g.addmsg("what type")
if !g.Options.Terse {
g.addmsg(" of object do you want a list")
}
g.msg("? (* for all)")
ch = g.readchar()
switch ch {
case Escape:
g.msg("")
return
case Potion, Scroll, Ring, Stick, '*':
discList = true
default:
if g.Options.Terse {
g.msg("Not a type")
} else {
g.msg("Please type one of %c%c%c%c (ESCAPE to quit)",
Potion, Scroll, Ring, Stick)
}
}
if discList {
break
}
}
if ch == '*' {
g.printDisc(Potion)
g.addLine("")
g.printDisc(Scroll)
g.addLine("")
g.printDisc(Ring)
g.addLine("")
g.printDisc(Stick)
g.endLine()
} else {
g.printDisc(ch)
g.endLine()
}
}
// printDisc prints what we've discovered of the given type
// (things.c print_disc).
func (g *RogueGame) printDisc(typ byte) {
var info []ObjInfo
switch typ {
case Scroll:
info = g.Items.Scrolls[:]
case Potion:
info = g.Items.Potions[:]
case Ring:
info = g.Items.Rings[:]
case Stick:
info = g.Items.Sticks[:]
}
order := make([]int, len(info))
g.setOrder(order)
obj := Object{Count: 1}
numFound := 0
for i := range info {
if info[order[i]].Know || info[order[i]].Guess != "" {
obj.Type = typ
obj.Which = order[i]
g.addLine("%s", g.invName(&obj, false))
numFound++
}
}
if numFound == 0 {
g.addLine("%s", g.nothing(typ))
}
}
// setOrder shuffles the display order for the discovery list
// (things.c set_order).
func (g *RogueGame) setOrder(order []int) {
for i := range order {
order[i] = i
}
for i := len(order); i > 0; i-- {
r := g.rnd(i)
order[i-1], order[r] = order[r], order[i-1]
}
}
// addLine adds a line to the list of discoveries (things.c add_line). A
// format of exactly "\x00" is the C fmt==NULL page-flush sentinel — use
// flushLine() for that.
const flushSentinel = "\x00"
func (g *RogueGame) addLine(format string, a ...any) int {
pg := &g.invPage
prompt := "--Press space to continue--"
isFlush := format == flushSentinel
var line string
if !isFlush {
line = fmt.Sprintf(format, a...)
}
if pg.lineCnt == 0 {
g.scr.Hw.Clear()
if g.Options.InvType == InvSlow {
g.Msgs.Mpos = 0
}
}
if g.Options.InvType == InvSlow {
if !isFlush && line != "" {
if g.msg("%s", line) == Escape {
return Escape
}
}
pg.lineCnt++
} else {
if !pg.init {
pg.maxlen = len(prompt)
pg.init = true
}
if pg.lineCnt >= NumLines-1 || isFlush {
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
// Overlay the accumulated list in a box at the top right
// of the screen, prompt, and restore what was beneath.
g.msg("")
g.refresh()
saved := NewWindow(NumLines, NumCols)
saved.CopyFrom(g.scr.Std)
lx := NumCols - pg.maxlen - 2
for y := 0; y <= pg.lineCnt; y++ {
for x := 0; x <= pg.maxlen; x++ {
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
}
}
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
g.refresh()
g.waitFor(' ')
g.scr.Std.CopyFrom(saved)
g.refresh()
} else {
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
g.scr.RefreshWin(g.scr.Hw)
g.waitFor(' ')
g.scr.Hw.Clear()
g.refresh()
}
pg.newpage = true
pg.lineCnt = 0
pg.maxlen = len(prompt)
}
if !isFlush && !(pg.lineCnt == 0 && line == "") {
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
pg.lineCnt++
if pg.maxlen < len(line) {
pg.maxlen = len(line)
}
pg.lastLine = line
}
}
return ^Escape
}
// flushLine is add_line(NULL): force out the accumulated page.
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
// endLine ends the list of lines (things.c end_line).
func (g *RogueGame) endLine() {
pg := &g.invPage
if g.Options.InvType != InvSlow {
if pg.lineCnt == 1 && !pg.newpage {
g.Msgs.Mpos = 0
g.msg("%s", pg.lastLine)
} else {
g.flushLine()
}
}
pg.lineCnt = 0
pg.newpage = false
}
// nothing builds the "nothing found" message for a type (things.c nothing).
func (g *RogueGame) nothing(typ byte) string {
var out string
if g.Options.Terse {
out = "Nothing"
} else {
out = "Haven't discovered anything"
}
if typ != '*' {
var tystr string
switch typ {
case Potion:
tystr = "potion"
case Scroll:
tystr = "scroll"
case Ring:
tystr = "ring"
case Stick:
tystr = "stick"
}
out += fmt.Sprintf(" about any %ss", tystr)
}
return out
}
// nameit gives the proper name to a potion, stick, or ring
// (things.c nameit).
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
if op.Know || op.Guess != "" {
if obj.Count == 1 {
fmt.Fprintf(pb, "A %s ", typ)
} else {
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
}
if op.Know {
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
} else {
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which)
}
} else if obj.Count == 1 {
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
} else {
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
}
}
// nullstr returns an empty string (things.c nullstr).
func nullstr(*RogueGame, *Object) string { return "" }

86
game/weapons.go Normal file
View File

@@ -0,0 +1,86 @@
package game
import "fmt"
// weapons.c — weapon initialization and naming. The throwing half
// (missile, do_motion, hit_monster) arrives with the combat phase.
const noWeapon = -1
// initWeaps is the weapons.c init_dam[] table.
var initWeaps = [MaxWeapons]struct {
dam string // damage when wielded
hrl string // damage when thrown
launch int // launching weapon
flags ObjFlags
}{
{"2x4", "1x3", noWeapon, 0}, // Mace
{"3x4", "1x2", noWeapon, 0}, // Long sword
{"1x1", "1x1", noWeapon, 0}, // Bow
{"1x1", "2x3", Bow, IsMany | IsMissl}, // Arrow
{"1x6", "1x4", noWeapon, IsMissl}, // Dagger
{"4x4", "1x2", noWeapon, 0}, // 2h sword
{"1x1", "1x3", noWeapon, IsMany | IsMissl}, // Dart
{"1x2", "2x4", noWeapon, IsMany | IsMissl}, // Shuriken
{"2x3", "1x6", noWeapon, IsMissl}, // Spear
}
// initWeapon sets up a new weapon (weapons.c init_weapon).
func (g *RogueGame) initWeapon(weap *Object, which int) {
iwp := &initWeaps[which]
weap.Type = Weapon
weap.Which = which
weap.Damage = iwp.dam
weap.HurlDmg = iwp.hrl
weap.Launch = iwp.launch
weap.Flags = iwp.flags
weap.HPlus = 0
weap.DPlus = 0
if which == Dagger {
weap.Count = g.rnd(4) + 2
weap.Group = g.Items.Group
g.Items.Group++
} else if weap.Flags.Has(IsMany) {
weap.Count = g.rnd(8) + 8
weap.Group = g.Items.Group
g.Items.Group++
} else {
weap.Count = 1
weap.Group = 0
}
}
// num formats a hit/damage or armor bonus string (weapons.c num).
func num(n1, n2 int, typ byte) string {
out := fmt.Sprintf("%+d", n1)
if typ == Weapon {
out += fmt.Sprintf(",%+d", n2)
}
return out
}
// fallpos picks a random empty position around (pos) for a dropped item
// (weapons.c fallpos).
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
var newpos Coord
cnt := 0
for y := pos.Y - 1; y <= pos.Y+1; y++ {
for x := pos.X - 1; x <= pos.X+1; x++ {
// check to make certain the spot is empty, if it is, put the
// object there, set it in the level list and re-draw the room
// if he can see it
if (y == g.Player.Pos.Y && x == g.Player.Pos.X) ||
y < 0 || x < 0 {
continue
}
ch := g.Level.Char(y, x)
if ch == Floor || ch == Passage {
if cnt++; g.rnd(cnt) == 0 {
newpos.Y = y
newpos.X = x
}
}
}
}
return newpos, cnt != 0
}