go: port foundation — types, RNG, tables, daemon scheduler
Module sneak.berlin/go/rogue, package game: - types.go: all rogue.h constants, flag types, Coord/Stats/Room/ObjInfo - creature.go/object.go: the THING union split into Creature (Player/ Monster) and Object; slices replace the C linked lists - level.go: Place map with the C column-major layout and chat/flat/moat/ winat access methods - tables.go: extern.c + init.c data (bestiary, item tables, name pools) - rng.go: the exact C LCG (seed*11109+13849), golden-tested against a compiled C reference for seed compatibility - init.go: per-game item appearance randomization (init.c) - daemon.go/daemons.go: scheduler with DaemonID replacing function pointers (ids match the C save format's mapping) - game.go: RogueGame holds all former globals; NewGame constructor
This commit is contained in:
65
game/creature.go
Normal file
65
game/creature.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package game
|
||||
|
||||
// Creature is the _t arm of the C THING union: the player or a monster.
|
||||
type Creature struct {
|
||||
Pos Coord // position
|
||||
Turn bool // if slowed, is it a turn to move
|
||||
Type byte // what it is: 'A'..'Z' for monsters, '@' for the player
|
||||
Disguise byte // what mimic looks like
|
||||
OldCh byte // character that was where it was
|
||||
Dest *Coord // where it is running to — aliases live coords (hero pos, room gold, another monster's pos)
|
||||
Flags CreatureFlags
|
||||
Stats Stats
|
||||
Room *Room // current room for thing
|
||||
Pack []*Object // what the thing is carrying
|
||||
}
|
||||
|
||||
// Monster is a hostile creature on the level.
|
||||
type Monster struct {
|
||||
Creature
|
||||
}
|
||||
|
||||
// Player embeds Creature and owns the player-only state that was global
|
||||
// in the C sources (cur_armor, purse, food_left, ...).
|
||||
type Player struct {
|
||||
Creature
|
||||
CurArmor *Object // what he is wearing
|
||||
CurWeapon *Object // which weapon he is wielding
|
||||
CurRing [2]*Object // which rings are being worn (Left/Right)
|
||||
PackUsed [26]bool // is the character used in the pack?
|
||||
Inpack int // number of things in pack
|
||||
Purse int // how much gold he has
|
||||
FoodLeft int // amount of food in hero's stomach
|
||||
HungryState int // how hungry is he (0..3)
|
||||
NoFood int // number of levels without food
|
||||
MaxStats Stats // the maximum for the player
|
||||
VfHit int // number of times the flytrap has hit
|
||||
}
|
||||
|
||||
// On is the C on(thing, flag) macro.
|
||||
func (c *Creature) On(f CreatureFlags) bool { return c.Flags.Has(f) }
|
||||
|
||||
// IsRing is the C ISRING(hand, ring) macro.
|
||||
func (p *Player) IsRing(hand, ring int) bool {
|
||||
return p.CurRing[hand] != nil && p.CurRing[hand].Which == ring
|
||||
}
|
||||
|
||||
// IsWearing is the C ISWEARING(ring) macro: is the ring on either hand.
|
||||
func (p *Player) IsWearing(ring int) bool {
|
||||
return p.IsRing(Left, ring) || p.IsRing(Right, ring)
|
||||
}
|
||||
|
||||
// attachMon pushes a monster onto the front of a list (list.c attach).
|
||||
func attachMon(list *[]*Monster, item *Monster) {
|
||||
*list = append([]*Monster{item}, *list...)
|
||||
}
|
||||
|
||||
// detachMon removes a monster (by identity) from a list (list.c detach).
|
||||
func detachMon(list *[]*Monster, item *Monster) {
|
||||
for i, m := range *list {
|
||||
if m == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
138
game/daemon.go
Normal file
138
game/daemon.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package game
|
||||
|
||||
// daemon.c — the daemon/fuse scheduler.
|
||||
//
|
||||
// C identifies daemons and fuses by function pointer; the port uses DaemonID
|
||||
// (which the C save format itself resorted to when serializing d_list), and
|
||||
// dispatches through one switch in daemons.go.
|
||||
|
||||
// DaemonID names a daemon/fuse callback. The numeric values match the
|
||||
// function-pointer-to-int mapping in state.c rs_write_daemons.
|
||||
type DaemonID int
|
||||
|
||||
const (
|
||||
DNone DaemonID = 0
|
||||
DRollwand DaemonID = 1
|
||||
DDoctor DaemonID = 2
|
||||
DStomach DaemonID = 3
|
||||
DRunners DaemonID = 4
|
||||
DSwander DaemonID = 5
|
||||
DNohaste DaemonID = 6
|
||||
DUnconfuse DaemonID = 7
|
||||
DUnsee DaemonID = 8
|
||||
DSight DaemonID = 9
|
||||
// Fuses beyond the C save map (state.c never saved these; the Go save
|
||||
// format handles them uniformly).
|
||||
DVisuals DaemonID = 10
|
||||
DComeDown DaemonID = 11
|
||||
DLand DaemonID = 12
|
||||
)
|
||||
|
||||
// Scheduling phases and slot states (daemon.c).
|
||||
const (
|
||||
dEmpty = 0
|
||||
Before = 1 // run before the player's command
|
||||
After = 2 // run after the player's command
|
||||
daemonTime = -1 // d_time sentinel: a recurring daemon, not a fuse
|
||||
)
|
||||
|
||||
// delayedAction is rogue.h struct delayed_action.
|
||||
type delayedAction struct {
|
||||
Type int // dEmpty, Before or After
|
||||
Func DaemonID
|
||||
Arg int
|
||||
Time int // daemonTime for daemons; remaining turns for fuses
|
||||
}
|
||||
|
||||
// DaemonList is the C d_list[MAXDAEMONS] plus the file statics that ride
|
||||
// along with the daemon system.
|
||||
type DaemonList struct {
|
||||
List [MaxDaemons]delayedAction
|
||||
Between int // daemons.c rollwand static `between`
|
||||
}
|
||||
|
||||
// dSlot finds an empty slot in the daemon/fuse list (daemon.c d_slot).
|
||||
func (g *RogueGame) dSlot() *delayedAction {
|
||||
for i := range g.Daemons.List {
|
||||
if g.Daemons.List[i].Type == dEmpty {
|
||||
return &g.Daemons.List[i]
|
||||
}
|
||||
}
|
||||
panic("ran out of fuse slots") // C: debug message in MASTER, NULL deref otherwise
|
||||
}
|
||||
|
||||
// findSlot finds a particular slot in the table (daemon.c find_slot).
|
||||
func (g *RogueGame) findSlot(f DaemonID) *delayedAction {
|
||||
for i := range g.Daemons.List {
|
||||
d := &g.Daemons.List[i]
|
||||
if d.Type != dEmpty && d.Func == f {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartDaemon starts a recurring daemon (daemon.c start_daemon).
|
||||
func (g *RogueGame) StartDaemon(f DaemonID, arg, typ int) {
|
||||
dev := g.dSlot()
|
||||
dev.Type = typ
|
||||
dev.Func = f
|
||||
dev.Arg = arg
|
||||
dev.Time = daemonTime
|
||||
}
|
||||
|
||||
// KillDaemon removes a daemon from the list (daemon.c kill_daemon).
|
||||
func (g *RogueGame) KillDaemon(f DaemonID) {
|
||||
if dev := g.findSlot(f); dev != nil {
|
||||
dev.Type = dEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// DoDaemons runs all the daemons that are active with the current flag
|
||||
// (daemon.c do_daemons).
|
||||
func (g *RogueGame) DoDaemons(flag int) {
|
||||
for i := range g.Daemons.List {
|
||||
dev := &g.Daemons.List[i]
|
||||
if dev.Type == flag && dev.Time == daemonTime {
|
||||
g.runDaemon(dev.Func, dev.Arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fuse starts a countdown fuse (daemon.c fuse).
|
||||
func (g *RogueGame) Fuse(f DaemonID, arg, time, typ int) {
|
||||
wire := g.dSlot()
|
||||
wire.Type = typ
|
||||
wire.Func = f
|
||||
wire.Arg = arg
|
||||
wire.Time = time
|
||||
}
|
||||
|
||||
// Lengthen extends a fuse's countdown (daemon.c lengthen).
|
||||
func (g *RogueGame) Lengthen(f DaemonID, xtime int) {
|
||||
if wire := g.findSlot(f); wire != nil {
|
||||
wire.Time += xtime
|
||||
}
|
||||
}
|
||||
|
||||
// Extinguish puts out a fuse (daemon.c extinguish).
|
||||
func (g *RogueGame) Extinguish(f DaemonID) {
|
||||
if wire := g.findSlot(f); wire != nil {
|
||||
wire.Type = dEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// DoFuses decrements all fuses in the given phase and fires any that burn
|
||||
// down (daemon.c do_fuses).
|
||||
func (g *RogueGame) DoFuses(flag int) {
|
||||
for i := range g.Daemons.List {
|
||||
wire := &g.Daemons.List[i]
|
||||
if wire.Type == flag && wire.Time > 0 {
|
||||
wire.Time--
|
||||
if wire.Time == 0 {
|
||||
wire.Type = dEmpty
|
||||
g.runDaemon(wire.Func, wire.Arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
game/daemons.go
Normal file
17
game/daemons.go
Normal file
@@ -0,0 +1,17 @@
|
||||
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.
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
panic("daemon not yet ported")
|
||||
}
|
||||
}
|
||||
155
game/game.go
Normal file
155
game/game.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package game
|
||||
|
||||
// ItemLore is the per-game item identity state: the randomized appearance
|
||||
// names and the seven mutable ObjInfo tables (extern.c/init.c).
|
||||
type ItemLore struct {
|
||||
PotColors [MaxPotions]string // p_colors: colors of the potions
|
||||
ScrNames [MaxScrolls]string // s_names: names of the scrolls
|
||||
RingStones [MaxRings]string // r_stones: stone settings of the rings
|
||||
WandMade [MaxSticks]string // ws_made: what sticks are made of
|
||||
WandType [MaxSticks]string // ws_type: "wand" or "staff"
|
||||
|
||||
Things [NumThings]ObjInfo
|
||||
Potions [MaxPotions]ObjInfo
|
||||
Scrolls [MaxScrolls]ObjInfo
|
||||
Rings [MaxRings]ObjInfo
|
||||
Sticks [MaxSticks]ObjInfo
|
||||
Weapons [MaxWeapons + 1]ObjInfo
|
||||
Armors [MaxArmors]ObjInfo
|
||||
|
||||
Group int // group number for the next stack of missiles (weapons.c `group`)
|
||||
}
|
||||
|
||||
// Options are the user-settable game options (options.c optlist).
|
||||
type Options struct {
|
||||
Terse bool // terse: shorter messages
|
||||
FightFlush bool // flush: flush typeahead during battle
|
||||
Jump bool // jump: show position only at end of run
|
||||
SeeFloor bool // seefloor: show the lamp-illuminated floor
|
||||
PassGo bool // passgo: follow turnings in passageways
|
||||
Tombstone bool // tombstone: print tombstone when killed
|
||||
InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// RogueGame is one complete game of Rogue: every piece of state that was a
|
||||
// global (or file-scope static) in the C sources, plus the terminal it is
|
||||
// played on. Construct with NewGame, then call Run.
|
||||
//
|
||||
// The struct grows with the port; fields appear in the phase that ports the
|
||||
// code owning them.
|
||||
type RogueGame struct {
|
||||
// identity / RNG
|
||||
Rng *Rng
|
||||
Dnum int // dungeon number
|
||||
Whoami string // name of player
|
||||
Fruit string // favorite fruit
|
||||
Home string // user's home directory
|
||||
FileName string // save file name
|
||||
Wizard bool // true if allows wizard commands
|
||||
NoScore bool // was a wizard sometime
|
||||
|
||||
// the world
|
||||
Player Player
|
||||
Level Level
|
||||
Depth int // C `level`: what level she is on
|
||||
MaxDepth int // C `max_level`: deepest player has gone
|
||||
HasAmulet bool // C `amulet`: he found the amulet
|
||||
SeenStairs bool // have seen the stairs (for lsd)
|
||||
|
||||
// turn/command engine
|
||||
Playing bool // true until he quits
|
||||
After bool // true if we want after daemons
|
||||
Again bool // repeating the last command
|
||||
Count int // number of times to repeat command
|
||||
NoCommand int // number of turns asleep
|
||||
NoMove int // number of turns held in place
|
||||
Quiet int // number of quiet turns
|
||||
Running bool // true if player is running
|
||||
RunCh byte // direction player is running
|
||||
DoorStop bool // stop running when we pass a door
|
||||
Firstmove bool // first move after setting door_stop
|
||||
MoveOn bool // next move shouldn't pick up items
|
||||
ToDeath bool // fighting is to the death!
|
||||
Kamikaze bool // to_death really to DEATH
|
||||
HasHit bool // has a "hit" message pending in msg
|
||||
MaxHit int // max damage done to her in to_death
|
||||
Take byte // thing she is taking
|
||||
Delta Coord
|
||||
DirCh byte
|
||||
LastComm byte
|
||||
LastDir byte
|
||||
LLastComm byte
|
||||
LLastDir byte
|
||||
LastPick *Object
|
||||
LLastPick *Object
|
||||
|
||||
// daemons/fuses
|
||||
Daemons DaemonList
|
||||
|
||||
// UI state (the Screen itself arrives with the UI phase)
|
||||
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?
|
||||
InShell bool // true if executing a shell
|
||||
Oldpos Coord // position before last look() call
|
||||
Oldrp *Room
|
||||
NObjs int // # items listed in inventory() call
|
||||
|
||||
// options and item identity
|
||||
Options Options
|
||||
Items ItemLore
|
||||
|
||||
// scores
|
||||
LastScore int
|
||||
AllScore bool
|
||||
ScorePath string
|
||||
}
|
||||
|
||||
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item
|
||||
// appearance tables (the front half of main.c main(); the player roll-up
|
||||
// and first level arrive with later porting phases).
|
||||
func NewGame(cfg Config) *RogueGame {
|
||||
g := &RogueGame{
|
||||
Rng: &Rng{Seed: cfg.Seed},
|
||||
Dnum: int(cfg.Seed),
|
||||
Whoami: cfg.Name,
|
||||
Fruit: "slime-mold",
|
||||
Home: cfg.Home,
|
||||
Wizard: cfg.Wizard,
|
||||
NoScore: cfg.Wizard,
|
||||
Playing: true,
|
||||
Depth: 1,
|
||||
ScorePath: cfg.ScorePath,
|
||||
LastScore: -1,
|
||||
}
|
||||
g.Options = Options{
|
||||
SeeFloor: true,
|
||||
Tombstone: true,
|
||||
InvType: InvOver,
|
||||
}
|
||||
g.InvDescribe = true
|
||||
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.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
|
||||
}
|
||||
128
game/init.go
Normal file
128
game/init.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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).
|
||||
|
||||
// initColors initializes the potion color scheme for this game
|
||||
// (init.c init_colors).
|
||||
func (g *RogueGame) initColors() {
|
||||
used := make([]bool, len(rainbow))
|
||||
for i := 0; i < MaxPotions; i++ {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(rainbow))
|
||||
if !used[j] {
|
||||
break
|
||||
}
|
||||
}
|
||||
used[j] = true
|
||||
g.Items.PotColors[i] = rainbow[j]
|
||||
}
|
||||
}
|
||||
|
||||
// initNames generates the names of the various scrolls (init.c init_names).
|
||||
func (g *RogueGame) initNames() {
|
||||
for i := 0; i < MaxScrolls; i++ {
|
||||
var cp strings.Builder
|
||||
nwords := g.rnd(3) + 2
|
||||
for ; nwords > 0; nwords-- {
|
||||
nsyl := g.rnd(3) + 1
|
||||
for ; nsyl > 0; nsyl-- {
|
||||
sp := sylls[g.rnd(len(sylls))]
|
||||
if cp.Len()+len(sp) > MaxNameLen {
|
||||
break
|
||||
}
|
||||
cp.WriteString(sp)
|
||||
}
|
||||
cp.WriteByte(' ')
|
||||
}
|
||||
g.Items.ScrNames[i] = strings.TrimSuffix(cp.String(), " ")
|
||||
}
|
||||
}
|
||||
|
||||
// initStones initializes the ring stone setting scheme for this game
|
||||
// (init.c init_stones).
|
||||
func (g *RogueGame) initStones() {
|
||||
used := make([]bool, len(stoneTable))
|
||||
for i := 0; i < MaxRings; i++ {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(stoneTable))
|
||||
if !used[j] {
|
||||
break
|
||||
}
|
||||
}
|
||||
used[j] = true
|
||||
g.Items.RingStones[i] = stoneTable[j].Name
|
||||
g.Items.Rings[i].Worth += stoneTable[j].Value
|
||||
}
|
||||
}
|
||||
|
||||
// initMaterials initializes the construction materials for wands and staffs
|
||||
// (init.c init_materials).
|
||||
func (g *RogueGame) initMaterials() {
|
||||
used := make([]bool, len(woods))
|
||||
metused := make([]bool, len(metals))
|
||||
for i := 0; i < MaxSticks; i++ {
|
||||
var str string
|
||||
for {
|
||||
if g.rnd(2) == 0 {
|
||||
j := g.rnd(len(metals))
|
||||
if !metused[j] {
|
||||
g.Items.WandType[i] = "wand"
|
||||
str = metals[j]
|
||||
metused[j] = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
j := g.rnd(len(woods))
|
||||
if !used[j] {
|
||||
g.Items.WandType[i] = "staff"
|
||||
str = woods[j]
|
||||
used[j] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Items.WandMade[i] = str
|
||||
}
|
||||
}
|
||||
|
||||
// sumProbs sums up the probabilities for items appearing, converting the
|
||||
// per-item weights into a cumulative distribution (init.c sumprobs).
|
||||
func sumProbs(info []ObjInfo) {
|
||||
for i := 1; i < len(info); i++ {
|
||||
info[i].Prob += info[i-1].Prob
|
||||
}
|
||||
}
|
||||
|
||||
// initProbs copies the base tables into the game and initializes the
|
||||
// probabilities for the various items (init.c init_probs).
|
||||
func (g *RogueGame) initProbs() {
|
||||
g.Items.Things = baseThings
|
||||
g.Items.Potions = basePotInfo
|
||||
g.Items.Scrolls = baseScrInfo
|
||||
g.Items.Rings = baseRingInfo
|
||||
g.Items.Sticks = baseWsInfo
|
||||
g.Items.Weapons = baseWeapInfo
|
||||
g.Items.Armors = baseArmInfo
|
||||
|
||||
sumProbs(g.Items.Things[:])
|
||||
sumProbs(g.Items.Potions[:])
|
||||
sumProbs(g.Items.Scrolls[:])
|
||||
sumProbs(g.Items.Rings[:])
|
||||
sumProbs(g.Items.Sticks[:])
|
||||
sumProbs(g.Items.Weapons[:MaxWeapons]) // C sums MAXWEAPONS, excluding the flame entry
|
||||
sumProbs(g.Items.Armors[:])
|
||||
}
|
||||
|
||||
// pickColor returns the given color, or a random one if the hero is
|
||||
// hallucinating (init.c pick_color).
|
||||
func (g *RogueGame) pickColor(col string) string {
|
||||
if g.Player.On(IsHalu) {
|
||||
return rainbow[g.rnd(len(rainbow))]
|
||||
}
|
||||
return col
|
||||
}
|
||||
73
game/level.go
Normal file
73
game/level.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package game
|
||||
|
||||
// Place describes a spot on the level map (rogue.h PLACE).
|
||||
type Place struct {
|
||||
Ch byte // the base map character
|
||||
Flags PlaceFlags
|
||||
Monst *Monster
|
||||
}
|
||||
|
||||
// Level is the current dungeon level: the map and everything on it. It is
|
||||
// reset in place by NewLevel, matching the C reuse of the global arrays.
|
||||
type Level struct {
|
||||
Places [MaxLines * MaxCols]Place
|
||||
Rooms [MaxRooms]Room
|
||||
Passages [MaxPass]Room // one pseudo-room per passage network
|
||||
Objects []*Object // lvl_obj: objects on this level
|
||||
Monsters []*Monster // mlist: monsters on the level
|
||||
Stairs Coord // location of the staircase
|
||||
NTraps int // number of traps on this level
|
||||
}
|
||||
|
||||
// At returns the map cell at (y, x); the C INDEX(y,x) macro, including its
|
||||
// column-major (x<<5)+y layout.
|
||||
func (l *Level) At(y, x int) *Place {
|
||||
return &l.Places[(x<<5)+y]
|
||||
}
|
||||
|
||||
// Char is the chat(y,x) macro: the base map character at a spot.
|
||||
func (l *Level) Char(y, x int) byte { return l.At(y, x).Ch }
|
||||
|
||||
// SetChar updates the base map character at a spot.
|
||||
func (l *Level) SetChar(y, x int, ch byte) { l.At(y, x).Ch = ch }
|
||||
|
||||
// FlagsAt is the flat(y,x) macro, returned as a pointer so ported
|
||||
// read-modify-write sites keep their shape.
|
||||
func (l *Level) FlagsAt(y, x int) *PlaceFlags { return &l.At(y, x).Flags }
|
||||
|
||||
// MonsterAt is the moat(y,x) macro: the monster standing at a spot, if any.
|
||||
func (l *Level) MonsterAt(y, x int) *Monster { return l.At(y, x).Monst }
|
||||
|
||||
// SetMonsterAt places (or clears, with nil) the monster at a spot.
|
||||
func (l *Level) SetMonsterAt(y, x int, m *Monster) { l.At(y, x).Monst = m }
|
||||
|
||||
// VisibleChar is the winat(y,x) macro: what is apparently at a spot — a
|
||||
// monster's disguise if one stands there, else the map character.
|
||||
func (l *Level) VisibleChar(y, x int) byte {
|
||||
if m := l.MonsterAt(y, x); m != nil {
|
||||
return m.Disguise
|
||||
}
|
||||
return l.Char(y, x)
|
||||
}
|
||||
|
||||
// reset clears the per-level state ahead of NewLevel drawing a fresh level.
|
||||
func (l *Level) reset() {
|
||||
for i := range l.Places {
|
||||
l.Places[i] = Place{}
|
||||
}
|
||||
for i := range l.Rooms {
|
||||
l.Rooms[i] = Room{}
|
||||
}
|
||||
for i := range l.Passages {
|
||||
l.Passages[i] = Room{Flags: IsGone | IsDark}
|
||||
}
|
||||
l.Objects = nil
|
||||
l.Monsters = nil
|
||||
l.Stairs = Coord{}
|
||||
l.NTraps = 0
|
||||
}
|
||||
|
||||
// goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth.
|
||||
func (g *RogueGame) goldCalc() int {
|
||||
return g.rnd(50+10*g.Depth) + 2
|
||||
}
|
||||
54
game/object.go
Normal file
54
game/object.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package game
|
||||
|
||||
// Object is the _o arm of the C THING union: anything that can lie on the
|
||||
// floor or ride in a pack.
|
||||
type Object struct {
|
||||
Type byte // what kind of object it is (Potion, Scroll, Weapon, ...)
|
||||
Pos Coord // where it lives on the screen
|
||||
Text string // what it says if you read it
|
||||
Launch int // what you need to launch it (weapon index, -1 none)
|
||||
PackCh byte // what character it is in the pack
|
||||
Damage string // damage if used like sword
|
||||
HurlDmg string // damage if thrown
|
||||
Count int // count for plural objects
|
||||
Which int // which object of a type it is
|
||||
HPlus int // plusses to hit
|
||||
DPlus int // plusses to damage
|
||||
Arm int // armor protection — overloaded as in C (see Charges/GoldVal)
|
||||
Flags ObjFlags
|
||||
Group int // group number for this object
|
||||
Label string // label for object
|
||||
}
|
||||
|
||||
// newObject is list.c new_item() for objects: a zeroed Object with the
|
||||
// same non-zero defaults the C code relies on.
|
||||
func newObject() *Object {
|
||||
return &Object{Launch: -1}
|
||||
}
|
||||
|
||||
// Charges is the o_charges alias for Arm on wands and staffs.
|
||||
func (o *Object) Charges() int { return o.Arm }
|
||||
|
||||
// SetCharges stores a charge count in the overloaded Arm field.
|
||||
func (o *Object) SetCharges(n int) { o.Arm = n }
|
||||
|
||||
// GoldVal is the o_goldval alias for Arm on gold piles.
|
||||
func (o *Object) GoldVal() int { return o.Arm }
|
||||
|
||||
// SetGoldVal stores a gold value in the overloaded Arm field.
|
||||
func (o *Object) SetGoldVal(n int) { o.Arm = n }
|
||||
|
||||
// attachObj is list.c attach(): push item onto the front of a list.
|
||||
func attachObj(list *[]*Object, item *Object) {
|
||||
*list = append([]*Object{item}, *list...)
|
||||
}
|
||||
|
||||
// detachObj is list.c detach(): remove item (by identity) from a list.
|
||||
func detachObj(list *[]*Object, item *Object) {
|
||||
for i, o := range *list {
|
||||
if o == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
51
game/rng.go
Normal file
51
game/rng.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package game
|
||||
|
||||
// Rng is the original Rogue linear congruential generator. The C RN macro is
|
||||
//
|
||||
// #define RN (((seed = seed*11109+13849) >> 16) & 0xffff)
|
||||
//
|
||||
// with `seed` a C int; Seed is int32 so the multiplication wraps exactly the
|
||||
// way 32-bit C arithmetic does. A given seed therefore produces the same
|
||||
// dungeon as the C game.
|
||||
type Rng struct {
|
||||
Seed int32
|
||||
}
|
||||
|
||||
// next steps the generator and returns the next raw value (the RN macro).
|
||||
func (r *Rng) next() int {
|
||||
r.Seed = r.Seed*11109 + 13849
|
||||
return int(r.Seed>>16) & 0xffff
|
||||
}
|
||||
|
||||
// Rnd picks a very random number in [0, rng) (main.c rnd).
|
||||
func (r *Rng) Rnd(rng int) int {
|
||||
if rng == 0 {
|
||||
return 0
|
||||
}
|
||||
v := r.next()
|
||||
if v < 0 {
|
||||
v = -v
|
||||
}
|
||||
return v % rng
|
||||
}
|
||||
|
||||
// Roll rolls a number of dice (main.c roll).
|
||||
func (r *Rng) Roll(number, sides int) int {
|
||||
dtotal := 0
|
||||
for ; number > 0; number-- {
|
||||
dtotal += r.Rnd(sides) + 1
|
||||
}
|
||||
return dtotal
|
||||
}
|
||||
|
||||
// rnd is the ported code's spelling of C rnd(): every call site in the C
|
||||
// sources reads rnd(x), and keeping that shape makes cross-checking easy.
|
||||
func (g *RogueGame) rnd(rng int) int { return g.Rng.Rnd(rng) }
|
||||
|
||||
// roll is C roll().
|
||||
func (g *RogueGame) roll(number, sides int) int { return g.Rng.Roll(number, sides) }
|
||||
|
||||
// spread gives a fuzzy number centered on nm (misc.c spread).
|
||||
func (g *RogueGame) spread(nm int) int {
|
||||
return nm - nm/20 + g.rnd(nm/10)
|
||||
}
|
||||
64
game/rng_test.go
Normal file
64
game/rng_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// The golden values below were produced by the original C generator
|
||||
// (seed = seed*11109+13849; rnd(range) = abs(RN) % range) compiled and run
|
||||
// on this machine. They lock in seed compatibility with the C game.
|
||||
func TestRndMatchesCImplementation(t *testing.T) {
|
||||
golden := map[int32][]int{
|
||||
1: {0, 30, 79, 7, 87, 1, 23, 7, 57, 98},
|
||||
12345: {92, 92, 45, 98, 24, 39, 92, 67, 3, 7},
|
||||
2026: {43, 90, 6, 2, 43, 34, 20, 5, 67, 71},
|
||||
1751808000: {61, 51, 56, 99, 68, 45, 88, 50, 46, 88},
|
||||
}
|
||||
for seed, want := range golden {
|
||||
r := &Rng{Seed: seed}
|
||||
for i, w := range want {
|
||||
if got := r.Rnd(100); got != w {
|
||||
t.Errorf("seed %d: rnd(100) call %d = %d, want %d", seed, i, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollMatchesCImplementation(t *testing.T) {
|
||||
golden := map[int32][]int{
|
||||
1: {6, 8, 10},
|
||||
12345: {10, 10, 15},
|
||||
2026: {10, 10, 13},
|
||||
1751808000: {7, 9, 9},
|
||||
}
|
||||
for seed, want := range golden {
|
||||
r := &Rng{Seed: seed}
|
||||
for i, w := range want {
|
||||
if got := r.Roll(3, 6); got != w {
|
||||
t.Errorf("seed %d: roll(3,6) call %d = %d, want %d", seed, i, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rnd(0) must return 0 without stepping the generator: the C macro
|
||||
// short-circuits before evaluating RN, and BEFORE/AFTER depend on it.
|
||||
func TestRndZeroDoesNotStep(t *testing.T) {
|
||||
r := &Rng{Seed: 42}
|
||||
if got := r.Rnd(0); got != 0 {
|
||||
t.Fatalf("rnd(0) = %d, want 0", got)
|
||||
}
|
||||
if r.Seed != 42 {
|
||||
t.Fatalf("rnd(0) stepped the generator: seed = %d, want 42", r.Seed)
|
||||
}
|
||||
}
|
||||
|
||||
// spread(1)==1 and spread(2)==2 deterministically; the C BEFORE/AFTER
|
||||
// constants rely on this.
|
||||
func TestSpreadSmallValues(t *testing.T) {
|
||||
g := &RogueGame{Rng: &Rng{Seed: 7}}
|
||||
if got := g.spread(1); got != 1 {
|
||||
t.Errorf("spread(1) = %d, want 1", got)
|
||||
}
|
||||
if got := g.spread(2); got != 2 {
|
||||
t.Errorf("spread(2) = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
327
game/tables.go
Normal file
327
game/tables.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package game
|
||||
|
||||
// This file is the immutable data from extern.c and init.c: tables that are
|
||||
// never written after program start. Per-game mutable copies (the ObjInfo
|
||||
// tables, whose probabilities are re-summed and whose Know/Guess fields
|
||||
// change during play) are cloned into RogueGame.Items by NewGame.
|
||||
|
||||
// initStats is the C INIT_STATS: the player's starting statistics.
|
||||
var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, Arm: 10, HP: 12, Dmg: "1x4", MaxHP: 12}
|
||||
|
||||
// aClass is extern.c a_class[]: armor class for each armor type.
|
||||
var aClass = [MaxArmors]int{
|
||||
8, // LEATHER
|
||||
7, // RING_MAIL
|
||||
7, // STUDDED_LEATHER
|
||||
6, // SCALE_MAIL
|
||||
5, // CHAIN_MAIL
|
||||
4, // SPLINT_MAIL
|
||||
4, // BANDED_MAIL
|
||||
3, // PLATE_MAIL
|
||||
}
|
||||
|
||||
// eLevels is extern.c e_levels[]: experience thresholds per level; the
|
||||
// zero terminates the table as in C.
|
||||
var eLevels = []int{
|
||||
10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000,
|
||||
50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0,
|
||||
}
|
||||
|
||||
// trName is extern.c tr_name[]: names of the traps.
|
||||
var trName = [NTraps]string{
|
||||
"a trapdoor",
|
||||
"an arrow trap",
|
||||
"a sleeping gas trap",
|
||||
"a beartrap",
|
||||
"a teleport trap",
|
||||
"a poison dart trap",
|
||||
"a rust trap",
|
||||
"a mysterious trap",
|
||||
}
|
||||
|
||||
// invTName is extern.c inv_t_name[]: the inventory style names.
|
||||
var invTName = []string{"Overwrite", "Slow", "Clear"}
|
||||
|
||||
// monsterTable is extern.c monsters[26]: all monster kinds, indexed by
|
||||
// letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C) is
|
||||
// rolled from the level at creation time.
|
||||
var monsterTable = [26]MonsterKind{
|
||||
/* Name CARRY FLAGS str exp lvl arm hp dmg */
|
||||
{"aquator", 0, IsMean, Stats{10, 20, 5, 2, 1, "0x0/0x0", 0}},
|
||||
{"bat", 0, IsFly, Stats{10, 1, 1, 3, 1, "1x2", 0}},
|
||||
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, "1x2/1x5/1x5", 0}},
|
||||
{"dragon", 100, IsMean, Stats{10, 5000, 10, -1, 1, "1x8/1x8/3x10", 0}},
|
||||
{"emu", 0, IsMean, Stats{10, 2, 1, 7, 1, "1x2", 0}},
|
||||
{"venus flytrap", 0, IsMean, Stats{10, 80, 8, 3, 1, "%%%x0", 0}},
|
||||
{"griffin", 20, IsMean | IsFly | IsRegen, Stats{10, 2000, 13, 2, 1, "4x3/3x5", 0}},
|
||||
{"hobgoblin", 0, IsMean, Stats{10, 3, 1, 5, 1, "1x8", 0}},
|
||||
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, "0x0", 0}},
|
||||
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, "2x12/2x4", 0}},
|
||||
{"kestrel", 0, IsMean | IsFly, Stats{10, 1, 1, 7, 1, "1x4", 0}},
|
||||
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, "1x1", 0}},
|
||||
{"medusa", 40, IsMean, Stats{10, 200, 8, 2, 1, "3x4/3x4/2x5", 0}},
|
||||
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, "0x0", 0}},
|
||||
{"orc", 15, IsGreed, Stats{10, 5, 1, 6, 1, "1x8", 0}},
|
||||
{"phantom", 0, IsInvis, Stats{10, 120, 8, 3, 1, "4x4", 0}},
|
||||
{"quagga", 0, IsMean, Stats{10, 15, 3, 3, 1, "1x5/1x5", 0}},
|
||||
{"rattlesnake", 0, IsMean, Stats{10, 9, 2, 3, 1, "1x6", 0}},
|
||||
{"snake", 0, IsMean, Stats{10, 2, 1, 5, 1, "1x3", 0}},
|
||||
{"troll", 50, IsRegen | IsMean, Stats{10, 120, 6, 4, 1, "1x8/1x8/2x6", 0}},
|
||||
{"black unicorn", 0, IsMean, Stats{10, 190, 7, -2, 1, "1x9/1x9/2x9", 0}},
|
||||
{"vampire", 20, IsRegen | IsMean, Stats{10, 350, 8, 1, 1, "1x10", 0}},
|
||||
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, "1x6", 0}},
|
||||
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, "4x4", 0}},
|
||||
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, "1x6/1x6", 0}},
|
||||
{"zombie", 0, IsMean, Stats{10, 6, 2, 8, 1, "1x8", 0}},
|
||||
}
|
||||
|
||||
// Base ObjInfo tables (extern.c). These are templates: NewGame copies them
|
||||
// into ItemLore before initProbs converts Prob to cumulative form.
|
||||
|
||||
var baseThings = [NumThings]ObjInfo{
|
||||
{Prob: 26}, // potion
|
||||
{Prob: 36}, // scroll
|
||||
{Prob: 16}, // food
|
||||
{Prob: 7}, // weapon
|
||||
{Prob: 7}, // armor
|
||||
{Prob: 4}, // ring
|
||||
{Prob: 4}, // stick
|
||||
}
|
||||
|
||||
var baseArmInfo = [MaxArmors]ObjInfo{
|
||||
{Name: "leather armor", Prob: 20, Worth: 20},
|
||||
{Name: "ring mail", Prob: 15, Worth: 25},
|
||||
{Name: "studded leather armor", Prob: 15, Worth: 20},
|
||||
{Name: "scale mail", Prob: 13, Worth: 30},
|
||||
{Name: "chain mail", Prob: 12, Worth: 75},
|
||||
{Name: "splint mail", Prob: 10, Worth: 80},
|
||||
{Name: "banded mail", Prob: 10, Worth: 90},
|
||||
{Name: "plate mail", Prob: 5, Worth: 150},
|
||||
}
|
||||
|
||||
var basePotInfo = [MaxPotions]ObjInfo{
|
||||
{Name: "confusion", Prob: 7, Worth: 5},
|
||||
{Name: "hallucination", Prob: 8, Worth: 5},
|
||||
{Name: "poison", Prob: 8, Worth: 5},
|
||||
{Name: "gain strength", Prob: 13, Worth: 150},
|
||||
{Name: "see invisible", Prob: 3, Worth: 100},
|
||||
{Name: "healing", Prob: 13, Worth: 130},
|
||||
{Name: "monster detection", Prob: 6, Worth: 130},
|
||||
{Name: "magic detection", Prob: 6, Worth: 105},
|
||||
{Name: "raise level", Prob: 2, Worth: 250},
|
||||
{Name: "extra healing", Prob: 5, Worth: 200},
|
||||
{Name: "haste self", Prob: 5, Worth: 190},
|
||||
{Name: "restore strength", Prob: 13, Worth: 130},
|
||||
{Name: "blindness", Prob: 5, Worth: 5},
|
||||
{Name: "levitation", Prob: 6, Worth: 75},
|
||||
}
|
||||
|
||||
var baseRingInfo = [MaxRings]ObjInfo{
|
||||
{Name: "protection", Prob: 9, Worth: 400},
|
||||
{Name: "add strength", Prob: 9, Worth: 400},
|
||||
{Name: "sustain strength", Prob: 5, Worth: 280},
|
||||
{Name: "searching", Prob: 10, Worth: 420},
|
||||
{Name: "see invisible", Prob: 10, Worth: 310},
|
||||
{Name: "adornment", Prob: 1, Worth: 10},
|
||||
{Name: "aggravate monster", Prob: 10, Worth: 10},
|
||||
{Name: "dexterity", Prob: 8, Worth: 440},
|
||||
{Name: "increase damage", Prob: 8, Worth: 400},
|
||||
{Name: "regeneration", Prob: 4, Worth: 460},
|
||||
{Name: "slow digestion", Prob: 9, Worth: 240},
|
||||
{Name: "teleportation", Prob: 5, Worth: 30},
|
||||
{Name: "stealth", Prob: 7, Worth: 470},
|
||||
{Name: "maintain armor", Prob: 5, Worth: 380},
|
||||
}
|
||||
|
||||
var baseScrInfo = [MaxScrolls]ObjInfo{
|
||||
{Name: "monster confusion", Prob: 7, Worth: 140},
|
||||
{Name: "magic mapping", Prob: 4, Worth: 150},
|
||||
{Name: "hold monster", Prob: 2, Worth: 180},
|
||||
{Name: "sleep", Prob: 3, Worth: 5},
|
||||
{Name: "enchant armor", Prob: 7, Worth: 160},
|
||||
{Name: "identify potion", Prob: 10, Worth: 80},
|
||||
{Name: "identify scroll", Prob: 10, Worth: 80},
|
||||
{Name: "identify weapon", Prob: 6, Worth: 80},
|
||||
{Name: "identify armor", Prob: 7, Worth: 100},
|
||||
{Name: "identify ring, wand or staff", Prob: 10, Worth: 115},
|
||||
{Name: "scare monster", Prob: 3, Worth: 200},
|
||||
{Name: "food detection", Prob: 2, Worth: 60},
|
||||
{Name: "teleportation", Prob: 5, Worth: 165},
|
||||
{Name: "enchant weapon", Prob: 8, Worth: 150},
|
||||
{Name: "create monster", Prob: 4, Worth: 75},
|
||||
{Name: "remove curse", Prob: 7, Worth: 105},
|
||||
{Name: "aggravate monsters", Prob: 3, Worth: 20},
|
||||
{Name: "protect armor", Prob: 2, Worth: 250},
|
||||
}
|
||||
|
||||
var baseWeapInfo = [MaxWeapons + 1]ObjInfo{
|
||||
{Name: "mace", Prob: 11, Worth: 8},
|
||||
{Name: "long sword", Prob: 11, Worth: 15},
|
||||
{Name: "short bow", Prob: 12, Worth: 15},
|
||||
{Name: "arrow", Prob: 12, Worth: 1},
|
||||
{Name: "dagger", Prob: 8, Worth: 3},
|
||||
{Name: "two handed sword", Prob: 10, Worth: 75},
|
||||
{Name: "dart", Prob: 12, Worth: 2},
|
||||
{Name: "shuriken", Prob: 12, Worth: 5},
|
||||
{Name: "spear", Prob: 12, Worth: 5},
|
||||
{}, // DO NOT REMOVE: fake entry for dragon's breath
|
||||
}
|
||||
|
||||
var baseWsInfo = [MaxSticks]ObjInfo{
|
||||
{Name: "light", Prob: 12, Worth: 250},
|
||||
{Name: "invisibility", Prob: 6, Worth: 5},
|
||||
{Name: "lightning", Prob: 3, Worth: 330},
|
||||
{Name: "fire", Prob: 3, Worth: 330},
|
||||
{Name: "cold", Prob: 3, Worth: 330},
|
||||
{Name: "polymorph", Prob: 15, Worth: 310},
|
||||
{Name: "magic missile", Prob: 10, Worth: 170},
|
||||
{Name: "haste monster", Prob: 10, Worth: 5},
|
||||
{Name: "slow monster", Prob: 11, Worth: 350},
|
||||
{Name: "drain life", Prob: 9, Worth: 300},
|
||||
{Name: "nothing", Prob: 1, Worth: 5},
|
||||
{Name: "teleport away", Prob: 6, Worth: 340},
|
||||
{Name: "teleport to", Prob: 6, Worth: 50},
|
||||
{Name: "cancellation", Prob: 5, Worth: 280},
|
||||
}
|
||||
|
||||
// rainbow is init.c rainbow[]: the possible potion colors.
|
||||
var rainbow = []string{
|
||||
"amber", "aquamarine", "black", "blue", "brown", "clear", "crimson",
|
||||
"cyan", "ecru", "gold", "green", "grey", "magenta", "orange", "pink",
|
||||
"plaid", "purple", "red", "silver", "tan", "tangerine", "topaz",
|
||||
"turquoise", "vermilion", "violet", "white", "yellow",
|
||||
}
|
||||
|
||||
// sylls is init.c sylls[]: syllables for generated scroll names.
|
||||
var sylls = []string{
|
||||
"a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash",
|
||||
"bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp",
|
||||
"con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el",
|
||||
"eng", "er", "ere", "erk", "esh", "evs", "fa", "fid", "fri", "fu",
|
||||
"gan", "gar", "glen", "gop", "gre", "ha", "hyd", "i", "ing", "ip",
|
||||
"ish", "it", "ite", "iv", "jo", "kho", "kli", "klis", "la", "lech",
|
||||
"mar", "me", "mi", "mic", "mik", "mon", "mung", "mur", "nej",
|
||||
"nelg", "nep", "ner", "nes", "nes", "nih", "nin", "o", "od", "ood",
|
||||
"org", "orn", "ox", "oxy", "pay", "ple", "plu", "po", "pot",
|
||||
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
|
||||
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
|
||||
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
|
||||
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
|
||||
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
|
||||
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
|
||||
"zok", "zon", "zum",
|
||||
}
|
||||
|
||||
// stoneTable is init.c stones[]: ring stones and their worth.
|
||||
var stoneTable = []Stone{
|
||||
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
|
||||
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
|
||||
{"germanium", 225}, {"granite", 5}, {"garnet", 50},
|
||||
{"jade", 150}, {"kryptonite", 300}, {"lapis lazuli", 50},
|
||||
{"moonstone", 50}, {"obsidian", 15}, {"onyx", 60},
|
||||
{"opal", 200}, {"pearl", 220}, {"peridot", 63},
|
||||
{"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200},
|
||||
{"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70},
|
||||
{"taaffeite", 300}, {"zircon", 80},
|
||||
}
|
||||
|
||||
// woods is init.c wood[]: what staffs are made of.
|
||||
var woods = []string{
|
||||
"avocado wood", "balsa", "bamboo", "banyan", "birch", "cedar",
|
||||
"cherry", "cinnibar", "cypress", "dogwood", "driftwood", "ebony",
|
||||
"elm", "eucalyptus", "fall", "hemlock", "holly", "ironwood",
|
||||
"kukui wood", "mahogany", "manzanita", "maple", "oaken",
|
||||
"persimmon wood", "pecan", "pine", "poplar", "redwood", "rosewood",
|
||||
"spruce", "teak", "walnut", "zebrawood",
|
||||
}
|
||||
|
||||
// metals is init.c metal[]: what wands are made of.
|
||||
var metals = []string{
|
||||
"aluminum", "beryllium", "bone", "brass", "bronze", "copper",
|
||||
"electrum", "gold", "iron", "lead", "magnesium", "mercury",
|
||||
"nickel", "pewter", "platinum", "steel", "silver", "silicon",
|
||||
"tin", "titanium", "tungsten", "zinc",
|
||||
}
|
||||
|
||||
// helpEntry is rogue.h struct h_list.
|
||||
type helpEntry struct {
|
||||
Ch byte
|
||||
Desc string
|
||||
Print bool
|
||||
}
|
||||
|
||||
// helpStr is extern.c helpstr[]: the '?' command help text.
|
||||
var helpStr = []helpEntry{
|
||||
{'?', " prints help", true},
|
||||
{'/', " identify object", true},
|
||||
{'h', " left", true},
|
||||
{'j', " down", true},
|
||||
{'k', " up", true},
|
||||
{'l', " right", true},
|
||||
{'y', " up & left", true},
|
||||
{'u', " up & right", true},
|
||||
{'b', " down & left", true},
|
||||
{'n', " down & right", true},
|
||||
{'H', " run left", false},
|
||||
{'J', " run down", false},
|
||||
{'K', " run up", false},
|
||||
{'L', " run right", false},
|
||||
{'Y', " run up & left", false},
|
||||
{'U', " run up & right", false},
|
||||
{'B', " run down & left", false},
|
||||
{'N', " run down & right", false},
|
||||
{CTRL('H'), " run left until adjacent", false},
|
||||
{CTRL('J'), " run down until adjacent", false},
|
||||
{CTRL('K'), " run up until adjacent", false},
|
||||
{CTRL('L'), " run right until adjacent", false},
|
||||
{CTRL('Y'), " run up & left until adjacent", false},
|
||||
{CTRL('U'), " run up & right until adjacent", false},
|
||||
{CTRL('B'), " run down & left until adjacent", false},
|
||||
{CTRL('N'), " run down & right until adjacent", false},
|
||||
{0, " <SHIFT><dir>: run that way", true},
|
||||
{0, " <CTRL><dir>: run till adjacent", true},
|
||||
{'f', "<dir> fight till death or near death", true},
|
||||
{'t', "<dir> throw something", true},
|
||||
{'m', "<dir> move onto without picking up", true},
|
||||
{'z', "<dir> zap a wand in a direction", true},
|
||||
{'^', "<dir> identify trap type", true},
|
||||
{'s', " search for trap/secret door", true},
|
||||
{'>', " go down a staircase", true},
|
||||
{'<', " go up a staircase", true},
|
||||
{'.', " rest for a turn", true},
|
||||
{',', " pick something up", true},
|
||||
{'i', " inventory", true},
|
||||
{'I', " inventory single item", true},
|
||||
{'q', " quaff potion", true},
|
||||
{'r', " read scroll", true},
|
||||
{'e', " eat food", true},
|
||||
{'w', " wield a weapon", true},
|
||||
{'W', " wear armor", true},
|
||||
{'T', " take armor off", true},
|
||||
{'P', " put on ring", true},
|
||||
{'R', " remove ring", true},
|
||||
{'d', " drop object", true},
|
||||
{'c', " call object", true},
|
||||
{'a', " repeat last command", true},
|
||||
{')', " print current weapon", true},
|
||||
{']', " print current armor", true},
|
||||
{'=', " print current rings", true},
|
||||
{'@', " print current stats", true},
|
||||
{'D', " recall what's been discovered", true},
|
||||
{'o', " examine/set options", true},
|
||||
{CTRL('R'), " redraw screen", true},
|
||||
{CTRL('P'), " repeat last message", true},
|
||||
{Escape, " cancel command", true},
|
||||
{'S', " save game", true},
|
||||
{'Q', " quit", true},
|
||||
{'!', " shell escape", true},
|
||||
{'F', "<dir> fight till either of you dies", true},
|
||||
{'v', " print version number", true},
|
||||
}
|
||||
|
||||
// Version strings (vers.c). The encstr/statlist XOR keys are not ported:
|
||||
// the Go save format does not use them.
|
||||
const (
|
||||
Release = "5.4.4"
|
||||
Version = "rogue (sneak.berlin/go/rogue port of rogueforge 5.4.4)"
|
||||
)
|
||||
86
game/tables_test.go
Normal file
86
game/tables_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// badcheck from init.c: every probability table must sum to exactly 100.
|
||||
func TestProbabilitiesSumTo100(t *testing.T) {
|
||||
sum := func(info []ObjInfo) int {
|
||||
s := 0
|
||||
for _, oi := range info {
|
||||
s += oi.Prob
|
||||
}
|
||||
return s
|
||||
}
|
||||
tables := map[string][]ObjInfo{
|
||||
"things": baseThings[:],
|
||||
"potions": basePotInfo[:],
|
||||
"scrolls": baseScrInfo[:],
|
||||
"rings": baseRingInfo[:],
|
||||
"sticks": baseWsInfo[:],
|
||||
"weapons": baseWeapInfo[:MaxWeapons], // excludes the flame entry
|
||||
"armor": baseArmInfo[:],
|
||||
}
|
||||
for name, tab := range tables {
|
||||
if s := sum(tab); s != 100 {
|
||||
t.Errorf("bad percentages for %s: sum = %d, want 100", name, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitProbsCumulative(t *testing.T) {
|
||||
g := NewGame(Config{Seed: 1})
|
||||
last := g.Items.Potions[MaxPotions-1].Prob
|
||||
if last != 100 {
|
||||
t.Errorf("cumulative potion probability ends at %d, want 100", last)
|
||||
}
|
||||
for i := 1; i < MaxPotions; i++ {
|
||||
if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob {
|
||||
t.Errorf("potion probs not nondecreasing at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGameRandomizesAppearances(t *testing.T) {
|
||||
g := NewGame(Config{Seed: 12345})
|
||||
seen := map[string]bool{}
|
||||
for i, c := range g.Items.PotColors {
|
||||
if c == "" {
|
||||
t.Fatalf("potion %d has no color", i)
|
||||
}
|
||||
if seen[c] {
|
||||
t.Errorf("potion color %q assigned twice", c)
|
||||
}
|
||||
seen[c] = true
|
||||
}
|
||||
for i, n := range g.Items.ScrNames {
|
||||
if n == "" {
|
||||
t.Fatalf("scroll %d has no name", i)
|
||||
}
|
||||
if len(n) > MaxNameLen+1 {
|
||||
t.Errorf("scroll name %q longer than C buffer allows", n)
|
||||
}
|
||||
}
|
||||
for i := range g.Items.WandType {
|
||||
if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" {
|
||||
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
|
||||
}
|
||||
if g.Items.WandMade[i] == "" {
|
||||
t.Errorf("stick %d has no material", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Determinism: same seed, same appearances.
|
||||
h := NewGame(Config{Seed: 12345})
|
||||
if h.Items != g.Items {
|
||||
t.Error("two games with the same seed produced different item lore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonsterTable(t *testing.T) {
|
||||
if monsterTable[0].Name != "aquator" || monsterTable[25].Name != "zombie" {
|
||||
t.Error("monster table order broken")
|
||||
}
|
||||
if monsterTable['D'-'A'].Name != "dragon" {
|
||||
t.Error("letter indexing broken")
|
||||
}
|
||||
}
|
||||
374
game/types.go
Normal file
374
game/types.go
Normal file
@@ -0,0 +1,374 @@
|
||||
// Package game is a Go port of Rogue 5.4.4 ("Exploring the Dungeons of
|
||||
// Doom", Michael Toy, Ken Arnold and Glenn Wichman, 1980-1999).
|
||||
//
|
||||
// The port is function-by-function faithful to the C sources kept in the
|
||||
// repository root; ARCHITECTURE.md documents both the original program and
|
||||
// the design of this port. All formerly-global state lives on the RogueGame
|
||||
// type.
|
||||
package game
|
||||
|
||||
// Maximum number of different things (rogue.h)
|
||||
const (
|
||||
MaxRooms = 9
|
||||
MaxThings = 9
|
||||
MaxObj = 9
|
||||
MaxPack = 23
|
||||
MaxTraps = 10
|
||||
AmuletLevel = 26
|
||||
NumThings = 7 // number of types of things
|
||||
MaxPass = 13 // upper limit on number of passages
|
||||
NumLines = 24
|
||||
NumCols = 80
|
||||
StatLine = NumLines - 1
|
||||
BoreLevel = 50
|
||||
|
||||
MaxStr = 1024 // maximum length of strings
|
||||
MaxLines = 32 // maximum number of screen lines used
|
||||
MaxCols = 80 // maximum number of screen columns used
|
||||
)
|
||||
|
||||
// Return values for get functions (rogue.h)
|
||||
const (
|
||||
Norm = 0 // normal exit
|
||||
Quit = 1 // quit option setting
|
||||
Minus = 2 // back up one option
|
||||
)
|
||||
|
||||
// Inventory types (rogue.h)
|
||||
const (
|
||||
InvOver = 0
|
||||
InvSlow = 1
|
||||
InvClear = 2
|
||||
)
|
||||
|
||||
// Things that appear on the screens (rogue.h). These byte values serve both
|
||||
// as map characters and as Object.Type discriminators, exactly as in C.
|
||||
const (
|
||||
Passage byte = '#'
|
||||
Door byte = '+'
|
||||
Floor byte = '.'
|
||||
PlayerCh byte = '@'
|
||||
Trap byte = '^'
|
||||
Stairs byte = '%'
|
||||
Gold byte = '*'
|
||||
Potion byte = '!'
|
||||
Scroll byte = '?'
|
||||
Magic byte = '$'
|
||||
Food byte = ':'
|
||||
Weapon byte = ')'
|
||||
Armor byte = ']'
|
||||
Amulet byte = ','
|
||||
Ring byte = '='
|
||||
Stick byte = '/'
|
||||
)
|
||||
|
||||
// Pseudo-types for item-selection prompts (rogue.h)
|
||||
const (
|
||||
Callable = -1
|
||||
RorS = -2
|
||||
)
|
||||
|
||||
// Various constants (rogue.h)
|
||||
const (
|
||||
HealTime = 30
|
||||
HuhDuration = 20
|
||||
SeeDuration = 850
|
||||
HungerTime = 1300
|
||||
MoreTime = 150
|
||||
StomachSize = 2000
|
||||
StarveTime = 850
|
||||
Escape = 27
|
||||
Left = 0
|
||||
Right = 1
|
||||
BoltLength = 6
|
||||
LampDist = 3
|
||||
MaxDaemons = 20
|
||||
MaxInp = 50 // options.c: max string entered by the user
|
||||
MaxNameLen = 40 // init.c MAXNAME: max chars in a generated scroll name
|
||||
)
|
||||
|
||||
// Save against things (rogue.h)
|
||||
const (
|
||||
VsPoison = 0
|
||||
VsParalyzation = 0
|
||||
VsDeath = 0
|
||||
VsBreath = 2
|
||||
VsMagic = 3
|
||||
)
|
||||
|
||||
// Flags for rooms (rogue.h)
|
||||
type RoomFlags int16
|
||||
|
||||
const (
|
||||
IsDark RoomFlags = 1 << iota // room is dark
|
||||
IsGone // room is gone (a corridor)
|
||||
IsMaze // room is a maze
|
||||
)
|
||||
|
||||
func (f RoomFlags) Has(b RoomFlags) bool { return f&b != 0 }
|
||||
func (f *RoomFlags) Set(b RoomFlags) { *f |= b }
|
||||
func (f *RoomFlags) Clear(b RoomFlags) { *f &^= b }
|
||||
|
||||
// Flags for objects (rogue.h)
|
||||
type ObjFlags int32
|
||||
|
||||
const (
|
||||
IsCursed ObjFlags = 1 << iota // object is cursed
|
||||
IsKnow // player knows details about the object
|
||||
IsMissl // object is a missile type
|
||||
IsMany // object comes in groups
|
||||
ObjIsFound // object has been seen (ISFOUND shares the bit with creatures)
|
||||
IsProt // armor is permanently protected
|
||||
)
|
||||
|
||||
func (f ObjFlags) Has(b ObjFlags) bool { return f&b != 0 }
|
||||
func (f *ObjFlags) Set(b ObjFlags) { *f |= b }
|
||||
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
|
||||
|
||||
// Flags for creatures (rogue.h). The C bit collisions are deliberate and
|
||||
// preserved: one name of each pair applies to monsters, the other to the
|
||||
// hero, and they never coexist on one creature.
|
||||
type CreatureFlags int32
|
||||
|
||||
const (
|
||||
CanHuh CreatureFlags = 0o000001 // creature can confuse
|
||||
CanSee CreatureFlags = 0o000002 // creature can see invisible creatures
|
||||
IsBlind CreatureFlags = 0o000004 // creature is blind
|
||||
IsCanc CreatureFlags = 0o000010 // creature has special qualities cancelled
|
||||
IsLevit CreatureFlags = 0o000010 // hero is levitating
|
||||
IsFound CreatureFlags = 0o000020 // creature has been seen
|
||||
IsGreed CreatureFlags = 0o000040 // creature runs to protect gold
|
||||
IsHaste CreatureFlags = 0o000100 // creature has been hastened
|
||||
IsTarget CreatureFlags = 0o000200 // creature is the target of an 'f' command
|
||||
IsHeld CreatureFlags = 0o000400 // creature has been held
|
||||
IsHuh CreatureFlags = 0o001000 // creature is confused
|
||||
IsInvis CreatureFlags = 0o002000 // creature is invisible
|
||||
IsMean CreatureFlags = 0o004000 // creature can wake when player enters room
|
||||
IsHalu CreatureFlags = 0o004000 // hero is on acid trip
|
||||
IsRegen CreatureFlags = 0o010000 // creature can regenerate
|
||||
IsRun CreatureFlags = 0o020000 // creature is running at the player
|
||||
SeeMonst CreatureFlags = 0o040000 // hero can detect unseen monsters
|
||||
IsFly CreatureFlags = 0o040000 // creature can fly
|
||||
IsSlow CreatureFlags = 0o100000 // creature has been slowed
|
||||
)
|
||||
|
||||
func (f CreatureFlags) Has(b CreatureFlags) bool { return f&b != 0 }
|
||||
func (f *CreatureFlags) Set(b CreatureFlags) { *f |= b }
|
||||
func (f *CreatureFlags) Clear(b CreatureFlags) { *f &^= b }
|
||||
|
||||
// Flags for the level map (rogue.h)
|
||||
type PlaceFlags uint8
|
||||
|
||||
const (
|
||||
FPass PlaceFlags = 0x80 // is a passageway
|
||||
FSeen PlaceFlags = 0x40 // have seen this spot before
|
||||
FDropped PlaceFlags = 0x20 // object was dropped here
|
||||
FLocked PlaceFlags = 0x20 // door is locked
|
||||
FReal PlaceFlags = 0x10 // what you see is what you get
|
||||
FPNum PlaceFlags = 0x0f // passage number mask
|
||||
FTMask PlaceFlags = 0x07 // trap number mask
|
||||
)
|
||||
|
||||
func (f PlaceFlags) Has(b PlaceFlags) bool { return f&b != 0 }
|
||||
func (f *PlaceFlags) Set(b PlaceFlags) { *f |= b }
|
||||
func (f *PlaceFlags) Clear(b PlaceFlags) { *f &^= b }
|
||||
|
||||
// Trap types (rogue.h)
|
||||
const (
|
||||
TDoor = 0
|
||||
TArrow = 1
|
||||
TSleep = 2
|
||||
TBear = 3
|
||||
TTelep = 4
|
||||
TDart = 5
|
||||
TRust = 6
|
||||
TMyst = 7
|
||||
NTraps = 8
|
||||
)
|
||||
|
||||
// Potion types (rogue.h)
|
||||
const (
|
||||
PConfuse = iota
|
||||
PLSD
|
||||
PPoison
|
||||
PStrength
|
||||
PSeeInvis
|
||||
PHealing
|
||||
PMFind
|
||||
PTFind
|
||||
PRaise
|
||||
PXHeal
|
||||
PHaste
|
||||
PRestore
|
||||
PBlind
|
||||
PLevit
|
||||
MaxPotions
|
||||
)
|
||||
|
||||
// Scroll types (rogue.h)
|
||||
const (
|
||||
SConfuse = iota
|
||||
SMap
|
||||
SHold
|
||||
SSleep
|
||||
SArmor
|
||||
SIDPotion
|
||||
SIDScroll
|
||||
SIDWeapon
|
||||
SIDArmor
|
||||
SIDRorS
|
||||
SScare
|
||||
SFDet
|
||||
STelep
|
||||
SEnch
|
||||
SCreate
|
||||
SRemove
|
||||
SAggr
|
||||
SProtect
|
||||
MaxScrolls
|
||||
)
|
||||
|
||||
// Weapon types (rogue.h)
|
||||
const (
|
||||
Mace = iota
|
||||
Sword
|
||||
Bow
|
||||
Arrow
|
||||
Dagger
|
||||
TwoSword
|
||||
Dart
|
||||
Shiraken
|
||||
Spear
|
||||
Flame // fake entry for dragon breath (ick)
|
||||
MaxWeapons = Flame
|
||||
)
|
||||
|
||||
// Armor types (rogue.h)
|
||||
const (
|
||||
Leather = iota
|
||||
RingMail
|
||||
StuddedLeather
|
||||
ScaleMail
|
||||
ChainMail
|
||||
SplintMail
|
||||
BandedMail
|
||||
PlateMail
|
||||
MaxArmors
|
||||
)
|
||||
|
||||
// Ring types (rogue.h)
|
||||
const (
|
||||
RProtect = iota
|
||||
RAddStr
|
||||
RSustStr
|
||||
RSearch
|
||||
RSeeInvis
|
||||
RNop
|
||||
RAggr
|
||||
RAddHit
|
||||
RAddDam
|
||||
RRegen
|
||||
RDigest
|
||||
RTeleport
|
||||
RStealth
|
||||
RSustArm
|
||||
MaxRings
|
||||
)
|
||||
|
||||
// Rod/Wand/Staff types (rogue.h)
|
||||
const (
|
||||
WsLight = iota
|
||||
WsInvis
|
||||
WsElect
|
||||
WsFire
|
||||
WsCold
|
||||
WsPolymorph
|
||||
WsMissile
|
||||
WsHasteM
|
||||
WsSlowM
|
||||
WsDrain
|
||||
WsNop
|
||||
WsTelAway
|
||||
WsTelTo
|
||||
WsCancel
|
||||
MaxSticks
|
||||
)
|
||||
|
||||
// Coord is a position on the level (rogue.h coord). A value type: the C
|
||||
// ce(a,b) macro is plain == here.
|
||||
type Coord struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// Stats describes a fighting being (rogue.h struct stats).
|
||||
type Stats struct {
|
||||
Str int // strength (s_str; 3..31)
|
||||
Exp int // experience
|
||||
Lvl int // level of mastery
|
||||
Arm int // armor class
|
||||
HP int // hit points (s_hpt)
|
||||
Dmg string // damage dice, e.g. "1x4/1x2"
|
||||
MaxHP int
|
||||
}
|
||||
|
||||
// Room describes a room or passage network (rogue.h struct room).
|
||||
type Room struct {
|
||||
Pos Coord // upper left corner
|
||||
Max Coord // size of room
|
||||
Gold Coord // where the gold is
|
||||
GoldVal int // how much the gold is worth
|
||||
Flags RoomFlags
|
||||
Exits []Coord // where the exits are (r_exit[12]/r_nexits)
|
||||
}
|
||||
|
||||
// MonsterKind is a bestiary entry (rogue.h struct monster).
|
||||
type MonsterKind struct {
|
||||
Name string // what to call the monster
|
||||
Carry int // probability of carrying something
|
||||
Flags CreatureFlags
|
||||
Stats Stats // initial stats
|
||||
}
|
||||
|
||||
// ObjInfo describes one object class: its real name, generation
|
||||
// probability, score worth, and per-game identification state
|
||||
// (rogue.h struct obj_info).
|
||||
type ObjInfo struct {
|
||||
Name string
|
||||
Prob int
|
||||
Worth int
|
||||
Guess string
|
||||
Know bool
|
||||
}
|
||||
|
||||
// Stone is a ring stone with its worth (rogue.h STONE).
|
||||
type Stone struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
// CTRL maps a letter to its control character, as the C CTRL() macro.
|
||||
func CTRL(c byte) byte { return c & 0o37 }
|
||||
|
||||
// IsMult reports whether an object type stacks in the pack (rogue.h ISMULT).
|
||||
func IsMult(typ byte) bool { return typ == Potion || typ == Scroll || typ == Food }
|
||||
|
||||
// distance returns the squared distance between two points (chase.c dist).
|
||||
func distance(y1, x1, y2, x2 int) int {
|
||||
dx := x2 - x1
|
||||
dy := y2 - y1
|
||||
return dx*dx + dy*dy
|
||||
}
|
||||
|
||||
// distCp is dist_cp from chase.c.
|
||||
func distCp(c1, c2 Coord) int { return distance(c1.Y, c1.X, c2.Y, c2.X) }
|
||||
|
||||
// sign is misc.c sign(): -1, 0 or 1.
|
||||
func sign(nm int) int {
|
||||
switch {
|
||||
case nm < 0:
|
||||
return -1
|
||||
case nm > 0:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user