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:
2026-07-06 18:46:22 +02:00
parent 45dba95678
commit 7fa2048402
13 changed files with 1535 additions and 0 deletions

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

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module sneak.berlin/go/rogue
go 1.25.7