From 7fa204840227132da29d24511fc3c97c1a975475 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 6 Jul 2026 18:46:22 +0200 Subject: [PATCH] =?UTF-8?q?go:=20port=20foundation=20=E2=80=94=20types,=20?= =?UTF-8?q?RNG,=20tables,=20daemon=20scheduler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- game/creature.go | 65 ++++++++ game/daemon.go | 138 ++++++++++++++++ game/daemons.go | 17 ++ game/game.go | 155 ++++++++++++++++++ game/init.go | 128 +++++++++++++++ game/level.go | 73 +++++++++ game/object.go | 54 +++++++ game/rng.go | 51 ++++++ game/rng_test.go | 64 ++++++++ game/tables.go | 327 ++++++++++++++++++++++++++++++++++++++ game/tables_test.go | 86 ++++++++++ game/types.go | 374 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + 13 files changed, 1535 insertions(+) create mode 100644 game/creature.go create mode 100644 game/daemon.go create mode 100644 game/daemons.go create mode 100644 game/game.go create mode 100644 game/init.go create mode 100644 game/level.go create mode 100644 game/object.go create mode 100644 game/rng.go create mode 100644 game/rng_test.go create mode 100644 game/tables.go create mode 100644 game/tables_test.go create mode 100644 game/types.go create mode 100644 go.mod diff --git a/game/creature.go b/game/creature.go new file mode 100644 index 0000000..2dd0cf0 --- /dev/null +++ b/game/creature.go @@ -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 + } + } +} diff --git a/game/daemon.go b/game/daemon.go new file mode 100644 index 0000000..db4b506 --- /dev/null +++ b/game/daemon.go @@ -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) + } + } + } +} diff --git a/game/daemons.go b/game/daemons.go new file mode 100644 index 0000000..6201e83 --- /dev/null +++ b/game/daemons.go @@ -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") + } +} diff --git a/game/game.go b/game/game.go new file mode 100644 index 0000000..d34bee5 --- /dev/null +++ b/game/game.go @@ -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 +} diff --git a/game/init.go b/game/init.go new file mode 100644 index 0000000..ee51688 --- /dev/null +++ b/game/init.go @@ -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 +} diff --git a/game/level.go b/game/level.go new file mode 100644 index 0000000..45da081 --- /dev/null +++ b/game/level.go @@ -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 +} diff --git a/game/object.go b/game/object.go new file mode 100644 index 0000000..31b963f --- /dev/null +++ b/game/object.go @@ -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 + } + } +} diff --git a/game/rng.go b/game/rng.go new file mode 100644 index 0000000..fc21663 --- /dev/null +++ b/game/rng.go @@ -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) +} diff --git a/game/rng_test.go b/game/rng_test.go new file mode 100644 index 0000000..29364b4 --- /dev/null +++ b/game/rng_test.go @@ -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) + } +} diff --git a/game/tables.go b/game/tables.go new file mode 100644 index 0000000..4f8bd9d --- /dev/null +++ b/game/tables.go @@ -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, " : run that way", true}, + {0, " : run till adjacent", true}, + {'f', " fight till death or near death", true}, + {'t', " throw something", true}, + {'m', " move onto without picking up", true}, + {'z', " zap a wand in a direction", true}, + {'^', " 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', " 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)" +) diff --git a/game/tables_test.go b/game/tables_test.go new file mode 100644 index 0000000..c290365 --- /dev/null +++ b/game/tables_test.go @@ -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") + } +} diff --git a/game/types.go b/game/types.go new file mode 100644 index 0000000..ab25be8 --- /dev/null +++ b/game/types.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9e28abd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module sneak.berlin/go/rogue + +go 1.25.7