Merge refactor/object-fields (refactor step 3 + module rename)

This commit is contained in:
2026-07-06 23:10:39 +02:00
32 changed files with 293 additions and 210 deletions

View File

@@ -6,7 +6,7 @@ This document has two parts:
codebase (every file, every function, every type, every global) as it exists
on the `modern-rogue` branch.
- **Part 2 — The Go Port Design**: the object-oriented architecture for the Go
port (`sneak.berlin/go/rogue`), including how every C construct maps onto Go
port (`git.eeqj.de/sneak/rgoue`), including how every C construct maps onto Go
types and methods.
---
@@ -887,7 +887,7 @@ Declares `rd_score(SCORE*)` / `wr_score(SCORE*)` (implemented in save.c).
# Part 2 — The Go Port Design
Module: **`sneak.berlin/go/rogue`**
Module: **`git.eeqj.de/sneak/rgoue`**
## 1. Goals and principles
@@ -929,7 +929,7 @@ branches; this branch is Go-only. The tcell wrapper ended up in its own
`term` package so that `game` itself has no third-party imports:
```
go.mod module sneak.berlin/go/rogue
go.mod module git.eeqj.de/sneak/rgoue
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
game/ package game — the port, one .go file per .c file
game.go RogueGame struct, NewGame, Run (main.c)

42
TODO.md
View File

@@ -29,14 +29,19 @@ Refactor ground rules:
# Next Step
Refactor step 3 (branch refactor/object-fields): un-overload Object
fields — split Object.Arm into ArmorClass/Charges/GoldValue; parse
damage dice ("1x4/1x2") once into a DiceSpec type at table definition
instead of per swing; save-format update with round-trip test
migration.
Adopt the house Go linting standards: copy .golangci.yml from the
prompts repo and bring game/, term/, and cmd/ lint-clean (the port is
greenfield code, so no exemptions apply).
# Completed Steps
- 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue
(go.mod, term/ and cmd/ imports, ARCHITECTURE.md, version string).
- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split
into ArmorClass/Charges/GoldValue/Bonus (rings); Stats.Arm →
ArmorClass; damage strings parsed once into DiceSpec at table
definition (ParseDice keeps C roll_em parse semantics, incl. "%%%x0"
and "000x0" edge cases, regression-tested); save format 5.4.4-go3.
- 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc):
ObjectKind separates item category from map glyph (Object.Type byte
→ Kind ObjectKind with Glyph()); PotionKind/ScrollKind/RingKind/
@@ -71,48 +76,45 @@ migration.
# Future Steps
1. Adopt the house Go linting standards: copy .golangci.yml from the
prompts repo and bring game/, term/, and cmd/ lint-clean (the port
is greenfield code, so no exemptions apply).
2. Refactor step 4: method renames, movement/world subsystem
1. Refactor step 4: method renames, movement/world subsystem
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
doRooms/doPassages/doMaze→digRooms/digPassages/digMaze,
chgStr→changeStrength, ...); remove the goto/label flows in doMove,
dispatch, and saveGame in favor of loops and helpers.
3. Refactor step 5: method renames, items/combat/UI subsystems
2. Refactor step 5: method renames, items/combat/UI subsystems
(invName→inventoryName, rollEm→rollAttacks, doPot→applyPotionFuse,
getItem→promptPackItem returning (obj, ok), getDir→promptDirection);
int status codes (attack returning -1) become named results. Two or
three commits, one subsystem each.
4. Refactor step 6: extract types from the god object — MessageLine
3. Refactor step 6: extract types from the god object — MessageLine
owns the msg/addmsg/endmsg machinery; pack/inventory operations move
onto *Player; monster/object list management and map queries
consolidate onto *Level; RogueGame keeps turn orchestration and
cross-system effects only.
5. Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
4. Refactor step 7: effects dispatch — the giant quaff/readScroll/doZap
switches become per-kind handler tables of small named methods,
keeping effect order and RNG call sequence identical.
6. Refactor step 8: constructor and style pass per the house
5. Refactor step 8: constructor and style pass per the house
styleguide — game.New(game.Params{...}) replacing NewGame(Config);
replace the gameEnd panic unwind with error-based turn results where
feasible; 77-column wrap sweep.
7. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
6. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
post-refactor names; add the C name → Go name rename table.
8. Playtest hardening pass: play several full games with the tcell
7. Playtest hardening pass: play several full games with the tcell
binary and extend run_test.go to script a deeper multi-level
playthrough (descend past level 5, use potions, scrolls, zapping,
save/restore). Fix any panics, message mismatches, or divergences
from the C behavior that this uncovers, with regression tests.
9. Verify the seed-compatibility claim against the C reference on
8. Verify the seed-compatibility claim against the C reference on
c-master: same seed, same dungeon, same item tables, for several
seeds.
10. Broaden unit test coverage where playtesting finds thin spots
9. Broaden unit test coverage where playtesting finds thin spots
(rings, sticks, wizard commands).
11. Tag a release once a full game (Amulet retrieval and score entry)
10. Tag a release once a full game (Amulet retrieval and score entry)
completes without defects.
12. Full-terminal-size support (deferred by explicit decision
11. Full-terminal-size support (deferred by explicit decision
2026-07-06): per-game dungeon dimensions instead of the 80x24
constants; open design questions are resize policy, gameplay
tuning at larger sizes, and a --classic 80x24 mode.
13. Note: this repo is exempt from the standard policy scaffold. Do not
12. Note: this repo is exempt from the standard policy scaffold. Do not
add Makefile, Dockerfile, or REPO_POLICIES.md.

View File

@@ -13,8 +13,8 @@ import (
"syscall"
"time"
"sneak.berlin/go/rogue/game"
"sneak.berlin/go/rogue/term"
"git.eeqj.de/sneak/rgoue/game"
"git.eeqj.de/sneak/rgoue/term"
)
func main() {

View File

@@ -410,7 +410,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
g.turnSee(p.On(SenseMonsters))
case CTRL('~'):
if item := g.getItem("charge", KindWand); item != nil {
item.SetCharges(10000)
item.Charges = 10000
}
case CTRL('I'):
for range 9 {
@@ -427,7 +427,7 @@ func (g *RogueGame) wizardCommand(ch byte) {
obj = newObject()
obj.Kind = KindArmor
obj.Which = int(ArmorPlateMail)
obj.Arm = -5
obj.ArmorClass = -5
obj.Flags.Set(Known)
obj.Count = 1
p.CurArmor = obj

58
game/dice.go Normal file
View File

@@ -0,0 +1,58 @@
package game
import (
"fmt"
"strings"
)
// Rogue expresses damage as strings like "1x4/3x6": one attack rolling
// 1d4 and a second rolling 3d6. The C code re-parsed these with atoi on
// every swing (fight.c roll_em); the port parses them once, at table
// definition or item creation, into a DiceSpec.
// DiceRoll is one attack's dice: Count rolls of a Sides-sided die.
type DiceRoll struct {
Count int
Sides int
}
// DiceSpec is the sequence of attacks a damage string described.
type DiceSpec []DiceRoll
// ParseDice parses a C damage string with the exact semantics of the
// roll_em loop: leading digits (C atoi) before and after each 'x', attacks
// separated by '/'. Junk like the bestiary's "%%%x0" placeholder parses as
// a single 0x0 attack, as it did in C.
func ParseDice(s string) DiceSpec {
var spec DiceSpec
for s != "" {
count := cAtoi(s)
xi := strings.IndexByte(s, 'x')
if xi < 0 {
break
}
s = s[xi+1:]
spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)})
si := strings.IndexByte(s, '/')
if si < 0 {
break
}
s = s[si+1:]
}
return spec
}
// dice is the table-definition shorthand for ParseDice.
func dice(s string) DiceSpec { return ParseDice(s) }
// String renders the spec back in the classic "NxM/NxM" form.
func (d DiceSpec) String() string {
var sb strings.Builder
for i, r := range d {
if i > 0 {
sb.WriteByte('/')
}
fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides)
}
return sb.String()
}

45
game/dice_test.go Normal file
View File

@@ -0,0 +1,45 @@
package game
import "testing"
// ParseDice must keep the exact semantics of the C roll_em parse loop,
// including its junk-tolerant edges: the bestiary placeholder "%%%x0" and
// the flytrap reset "000x0" both mean a single 0x0 attack.
func TestParseDice(t *testing.T) {
cases := []struct {
in string
want string
len int
}{
{"1x4", "1x4", 1},
{"1x2/1x5/1x5", "1x2/1x5/1x5", 3},
{"2x12/2x4", "2x12/2x4", 2},
{"0x0", "0x0", 1},
{"000x0", "0x0", 1},
{"%%%x0", "0x0", 1},
{"", "", 0},
{"3", "", 0}, // no 'x': C parses nothing
}
for _, c := range cases {
got := ParseDice(c.in)
if len(got) != c.len || got.String() != c.want {
t.Errorf("ParseDice(%q) = %v (len %d), want %q (len %d)",
c.in, got, len(got), c.want, c.len)
}
}
}
// The bestiary and weapon tables must parse to at least one attack each so
// every creature and weapon actually swings.
func TestTablesHaveDice(t *testing.T) {
for i, m := range monsterTable {
if len(m.Stats.Dmg) == 0 {
t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name)
}
}
for w, iw := range initWeaps {
if len(iw.dam) == 0 || len(iw.hrl) == 0 {
t.Errorf("weapon %v has empty dice", WeaponKind(w))
}
}
}

View File

@@ -72,11 +72,11 @@ func TestReadEnchantArmor(t *testing.T) {
ch := give(g, scr)
g.scr.term.(*testTerm).input = []byte{ch}
before := g.Player.CurArmor.Arm
before := g.Player.CurArmor.ArmorClass
g.readScroll()
if g.Player.CurArmor.Arm != before-1 {
if g.Player.CurArmor.ArmorClass != before-1 {
t.Errorf("enchant armor: AC %d -> %d, want %d",
before, g.Player.CurArmor.Arm, before-1)
before, g.Player.CurArmor.ArmorClass, before-1)
}
}
@@ -139,12 +139,12 @@ func TestZapSlowMonster(t *testing.T) {
g.scr.term.(*testTerm).input = []byte{ch}
g.Delta = Coord{X: 1, Y: 0} // aim at the monster
charges := stick.Charges()
charges := stick.Charges
g.doZap()
if !tp.On(Slowed) {
t.Error("slow monster wand did not slow")
}
if stick.Charges() != charges-1 {
if stick.Charges != charges-1 {
t.Error("zap did not use a charge")
}
}

View File

@@ -1,10 +1,6 @@
package game
import (
"fmt"
"strconv"
"strings"
)
import "strconv"
// fight.c — all the fighting gets done here.
@@ -245,7 +241,7 @@ func (g *RogueGame) attack(mp *Monster) int {
// Venus Flytrap stops the poor guy from moving
p.Flags.Set(Held)
p.VfHit++
g.Monsters['F'-'A'].Stats.Dmg = fmt.Sprintf("%dx1", p.VfHit)
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F')
}
@@ -322,34 +318,34 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
p := &g.Player
att := &thatt.Stats
def := &thdef.Stats
var cp string
var attacks DiceSpec
var hplus, dplus int
if weap == nil {
cp = att.Dmg
attacks = att.Dmg
} else {
hplus = weap.HPlus
dplus = weap.DPlus
if weap == p.CurWeapon {
if p.IsRing(Left, RingIncreaseDamage) {
dplus += p.CurRing[Left].Arm
dplus += p.CurRing[Left].Bonus
} else if p.IsRing(Left, RingDexterity) {
hplus += p.CurRing[Left].Arm
hplus += p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingIncreaseDamage) {
dplus += p.CurRing[Right].Arm
dplus += p.CurRing[Right].Bonus
} else if p.IsRing(Right, RingDexterity) {
hplus += p.CurRing[Right].Arm
hplus += p.CurRing[Right].Bonus
}
}
cp = weap.Damage
attacks = weap.Damage
if hurl {
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
WeaponKind(p.CurWeapon.Which) == weap.Launch {
cp = weap.HurlDmg
attacks = weap.HurlDmg
hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 {
cp = weap.HurlDmg
attacks = weap.HurlDmg
}
}
}
@@ -358,40 +354,28 @@ func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool
if !thdef.Flags.Has(Awake) {
hplus += 4
}
defArm := def.Arm
defArm := def.ArmorClass
if def == &p.Stats {
if p.CurArmor != nil {
defArm = p.CurArmor.Arm
defArm = p.CurArmor.ArmorClass
}
if p.IsRing(Left, RingProtection) {
defArm -= p.CurRing[Left].Arm
defArm -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
defArm -= p.CurRing[Right].Arm
defArm -= p.CurRing[Right].Bonus
}
}
didHit := false
for cp != "" {
ndice := cAtoi(cp)
xi := strings.IndexByte(cp, 'x')
if xi < 0 {
break
}
cp = cp[xi+1:]
nsides := cAtoi(cp)
for _, atk := range attacks {
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) {
proll := g.roll(ndice, nsides)
proll := g.roll(atk.Count, atk.Sides)
damage := dplus + proll + addDam[att.Str]
if damage > 0 {
def.HP -= damage
}
didHit = true
}
si := strings.IndexByte(cp, '/')
if si < 0 {
break
}
cp = cp[si+1:]
}
return didHit
}
@@ -531,7 +515,7 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
case 'F':
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
@@ -540,9 +524,9 @@ func (g *RogueGame) killed(tp *Monster, pr bool) {
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Kind = KindGold
gold.SetGoldVal(g.goldCalc())
gold.GoldValue = g.goldCalc()
if g.save(VsMagic) {
gold.Arm += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}

View File

@@ -23,8 +23,8 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster {
func TestRollEmParsesMultiAttackDice(t *testing.T) {
g := mkGame(t, 42)
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: "1x4/1x4/1x4"}}
def := &Creature{Stats: Stats{Arm: 10, HP: 1000}}
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: dice("1x4/1x4/1x4")}}
def := &Creature{Stats: Stats{ArmorClass: 10, HP: 1000}}
def.Flags.Set(Awake)
// With attacker level 20 vs armor 10, swing always hits
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of

View File

@@ -20,7 +20,7 @@ func (g *RogueGame) initPlayer() {
obj = newObject()
obj.Kind = KindArmor
obj.Which = int(ArmorRingMail)
obj.Arm = aClass[ArmorRingMail] - 1
obj.ArmorClass = aClass[ArmorRingMail] - 1
obj.Flags.Set(Known)
obj.Count = 1
p.CurArmor = obj

View File

@@ -142,9 +142,9 @@ func (g *RogueGame) status() {
p := &g.Player
// If nothing has changed since the last status, don't bother.
temp := p.Stats.Arm
temp := p.Stats.ArmorClass
if p.CurArmor != nil {
temp = p.CurArmor.Arm
temp = p.CurArmor.ArmorClass
}
if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&

View File

@@ -279,10 +279,10 @@ func (g *RogueGame) chgStr(amt int) {
addStr(&p.Stats.Str, amt)
comp := p.Stats.Str
if p.IsRing(Left, RingAddStrength) {
addStr(&comp, -p.CurRing[Left].Arm)
addStr(&comp, -p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&comp, -p.CurRing[Right].Arm)
addStr(&comp, -p.CurRing[Right].Bonus)
}
if comp > p.MaxStats.Str {
p.MaxStats.Str = comp

View File

@@ -54,7 +54,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
tp.Stats.HP = tp.Stats.MaxHP
tp.Stats.Arm = mp.Stats.Arm - levAdd
tp.Stats.ArmorClass = mp.Stats.ArmorClass - levAdd
tp.Stats.Dmg = mp.Stats.Dmg
tp.Stats.Str = mp.Stats.Str
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
@@ -183,10 +183,10 @@ func (g *RogueGame) save(which int) bool {
p := &g.Player
if which == VsMagic {
if p.IsRing(Left, RingProtection) {
which -= p.CurRing[Left].Arm
which -= p.CurRing[Left].Bonus
}
if p.IsRing(Right, RingProtection) {
which -= p.CurRing[Right].Arm
which -= p.CurRing[Right].Bonus
}
}
return g.saveThrow(which, &p.Stats)

View File

@@ -241,7 +241,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
p.Flags.Clear(Awake)
g.msg("a strange white mist envelops you and you fall asleep")
case TrapArrow:
if g.swing(p.Stats.Lvl-1, p.Stats.Arm, 1) {
if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) {
p.Stats.HP -= g.roll(1, 6)
if p.Stats.HP <= 0 {
g.msg("an arrow killed you")
@@ -263,7 +263,7 @@ func (g *RogueGame) beTrapped(tc Coord) TrapKind {
g.teleport()
g.mvaddch(tc.Y, tc.X, Trap)
case TrapDart:
if !g.swing(p.Stats.Lvl+1, p.Stats.Arm, 1) {
if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) {
g.msg("a small dart whizzes by your ear and vanishes")
} else {
p.Stats.HP -= g.roll(1, 4)
@@ -322,7 +322,7 @@ func (g *RogueGame) rndmove(who *Creature) Coord {
// aren't wearing a magic ring (move.c rust_armor).
func (g *RogueGame) rustArmor(arm *Object) {
if arm == nil || arm.Kind != KindArmor || arm.ArmorKind() == ArmorLeather ||
arm.Arm >= 9 {
arm.ArmorClass >= 9 {
return
}
@@ -331,7 +331,7 @@ func (g *RogueGame) rustArmor(arm *Object) {
g.msg("the rust vanishes instantly")
}
} else {
arm.Arm++
arm.ArmorClass++
if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!")
} else {

View File

@@ -111,9 +111,9 @@ func (g *RogueGame) putThings() {
if g.Depth >= AmuletLevel && !g.HasAmulet {
obj := newObject()
attachObj(&g.Level.Objects, obj)
obj.Damage = "0x0"
obj.HurlDmg = "0x0"
obj.Arm = 11
obj.Damage = dice("0x0")
obj.HurlDmg = dice("0x0")
obj.ArmorClass = 11
obj.Kind = KindAmulet
// Put it somewhere
obj.Pos, _ = g.findFloor(nil, 0, false)

View File

@@ -99,13 +99,16 @@ type Object struct {
Text string // what it says if you read it
Launch WeaponKind // what you need to launch it (noWeapon if none)
PackCh byte // what character it is in the pack
Damage string // damage if used like sword
HurlDmg string // damage if thrown
Damage DiceSpec // damage if used like sword
HurlDmg DiceSpec // damage if thrown
Count int // count for plural objects
Which int // which object of a type it is (index for the Kind's table)
HPlus int // plusses to hit
DPlus int // plusses to damage
Arm int // armor protection — overloaded as in C (see Charges/GoldVal)
ArmorClass int // armor protection (armor only)
Charges int // charges remaining (wands and staffs only)
GoldValue int // worth (gold piles only)
Bonus int // magic bonus (rings only: protection, add strength, ...)
Flags ObjFlags
Group int // group number for this object
Label string // label for object
@@ -117,18 +120,6 @@ func newObject() *Object {
return &Object{Launch: noWeapon}
}
// 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 }
// PotionKind returns Which as a potion kind; valid only for KindPotion.
func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) }

View File

@@ -250,7 +250,7 @@ func (g *RogueGame) pickUp(ch byte) {
if obj == nil {
return
}
g.money(obj.GoldVal())
g.money(obj.GoldValue)
detachObj(&g.Level.Objects, obj)
p.Room.GoldVal = 0
default:

View File

@@ -153,19 +153,19 @@ func (g *RogueGame) quaff() {
}
case PotionRestoreStrength:
if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Left].Arm)
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, -p.CurRing[Right].Arm)
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
}
if p.Stats.Str < p.MaxStats.Str {
p.Stats.Str = p.MaxStats.Str
}
if p.IsRing(Left, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Left].Arm)
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
}
if p.IsRing(Right, RingAddStrength) {
addStr(&p.Stats.Str, p.CurRing[Right].Arm)
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
}
g.msg("hey, this tastes great. It make you feel warm all over")
case PotionBlindness:
@@ -212,7 +212,7 @@ func (g *RogueGame) doPot(kind PotionKind, knowit bool) {
func (o *Object) isMagic() bool {
switch o.Kind {
case KindArmor:
return o.Flags.Has(Protected) || o.Arm != aClass[o.Which]
return o.Flags.Has(Protected) || o.ArmorClass != aClass[o.Which]
case KindWeapon:
return o.HPlus != 0 || o.DPlus != 0
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:

View File

@@ -49,7 +49,7 @@ func (g *RogueGame) ringOn() {
// Calculate the effect it has on the poor guy.
switch obj.RingKind() {
case RingAddStrength:
g.chgStr(obj.Arm)
g.chgStr(obj.Bonus)
case RingSeeInvisible:
g.invisOn()
case RingAggravateMonsters:
@@ -168,7 +168,7 @@ func ringNum(g *RogueGame, obj *Object) string {
}
switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring))
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
}
return ""
}

View File

@@ -124,8 +124,8 @@ func (g *RogueGame) totalWinner() {
obj.Flags.Set(Known)
case KindArmor:
worth = it.Armors[obj.Which].Worth
worth += (9 - obj.Arm) * 100
worth += 10 * (aClass[obj.Which] - obj.Arm)
worth += (9 - obj.ArmorClass) * 100
worth += 10 * (aClass[obj.Which] - obj.ArmorClass)
obj.Flags.Set(Known)
case KindScroll:
op := &it.Scrolls[obj.Which]
@@ -146,8 +146,8 @@ func (g *RogueGame) totalWinner() {
worth = op.Worth
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
if obj.Arm > 0 {
worth += obj.Arm * 100
if obj.Bonus > 0 {
worth += obj.Bonus * 100
} else {
worth = 10
}
@@ -160,7 +160,7 @@ func (g *RogueGame) totalWinner() {
case KindWand:
op := &it.Sticks[obj.Which]
worth = op.Worth
worth += 20 * obj.Charges()
worth += 20 * obj.Charges
if !obj.Flags.Has(Known) {
worth /= 2
}

View File

@@ -88,7 +88,7 @@ func (g *RogueGame) doRooms() {
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
gold := newObject()
rp.GoldVal = g.goldCalc()
gold.SetGoldVal(rp.GoldVal)
gold.GoldValue = rp.GoldVal
rp.Gold, _ = g.findFloorIn(rp, 0, false)
gold.Pos = rp.Gold
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)

View File

@@ -13,8 +13,10 @@ import (
// saveFormatVersion identifies the snapshot layout; it changes whenever a
// field rename or retype would make gob silently mis-decode an older
// save ("go2": Object.Type became Kind ObjectKind).
const saveFormatVersion = Release + "-go2"
// save ("go2": Object.Type became Kind ObjectKind; "go3": Object.Arm split
// into ArmorClass/Charges/GoldValue/Bonus, damage strings became
// DiceSpec).
const saveFormatVersion = Release + "-go3"
// destRef encodes a monster's chase target (state.c rs_write_thing's
// (kind, index) pairs).

View File

@@ -15,7 +15,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
g.HasAmulet = true
g.Items.Potions[PotionHealing].Know = true
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???"
g.Monsters['F'-'A'].Stats.Dmg = "3x1" // mutated bestiary must survive
g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive
if len(g.Level.Monsters) > 0 {
g.Level.Monsters[0].Flags.Set(Awake)
g.Level.Monsters[0].Dest = &g.Player.Pos
@@ -48,7 +48,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
t.Error("scroll guess lost")
}
if h.Monsters['F'-'A'].Stats.Dmg != "3x1" {
if h.Monsters['F'-'A'].Stats.Dmg.String() != "3x1" {
t.Error("mutated bestiary lost")
}
if h.Rng.Seed != g.Rng.Seed {

View File

@@ -42,7 +42,7 @@ func (g *RogueGame) readScroll() {
g.msg("your hands begin to glow %s", g.pickColor("red"))
case ScrollEnchantArmor:
if p.CurArmor != nil {
p.CurArmor.Arm--
p.CurArmor.ArmorClass--
p.CurArmor.Flags.Clear(Cursed)
g.msg("your armor glows %s for a moment", g.pickColor("silver"))
}

View File

@@ -16,7 +16,7 @@ func (g *RogueGame) doZap() {
g.msg("you can't zap with that!")
return
}
if obj.Charges() == 0 {
if obj.Charges == 0 {
g.msg("nothing happens")
return
}
@@ -111,7 +111,7 @@ func (g *RogueGame) doZap() {
g.Items.Sticks[WandMagicMissile].Know = true
bolt := newObject()
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
bolt.HurlDmg = "1x4"
bolt.HurlDmg = dice("1x4")
bolt.HPlus = 100
bolt.DPlus = 1
bolt.Flags = Missile
@@ -167,7 +167,7 @@ func (g *RogueGame) doZap() {
g.Items.Sticks[obj.Which].Know = true
case WandNothing:
}
obj.SetCharges(obj.Charges() - 1)
obj.Charges--
}
// drain does the drain-hit-points-from-player schtick (sticks.c drain).
@@ -213,7 +213,7 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
bolt := newObject()
bolt.Kind = KindWeapon
bolt.Which = int(WeaponFlame)
bolt.HurlDmg = "6x6"
bolt.HurlDmg = dice("6x6")
bolt.HPlus = 100
bolt.DPlus = 0
g.Items.Weapons[WeaponFlame].Name = name
@@ -322,17 +322,17 @@ func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
// fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) {
if g.Items.WandType[cur.Which] == "staff" {
cur.Damage = "2x3"
cur.Damage = dice("2x3")
} else {
cur.Damage = "1x1"
cur.Damage = dice("1x1")
}
cur.HurlDmg = "1x1"
cur.HurlDmg = dice("1x1")
switch cur.WandKind() {
case WandLight:
cur.SetCharges(g.rnd(10) + 10)
cur.Charges = g.rnd(10) + 10
default:
cur.SetCharges(g.rnd(5) + 3)
cur.Charges = g.rnd(5) + 3
}
}
@@ -343,7 +343,7 @@ func chargeStr(g *RogueGame, obj *Object) string {
return ""
}
if g.Options.Terse {
return fmt.Sprintf(" [%d]", obj.Charges())
return fmt.Sprintf(" [%d]", obj.Charges)
}
return fmt.Sprintf(" [%d charges]", obj.Charges())
return fmt.Sprintf(" [%d charges]", obj.Charges)
}

View File

@@ -6,7 +6,7 @@ package game
// 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}
var initStats = Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12}
// aClass is extern.c a_class[]: armor class for each armor type.
var aClass = [NumArmorTypes]int{
@@ -47,32 +47,32 @@ var invTName = []string{"Overwrite", "Slow", "Clear"}
// rolled from the level at creation time.
var monsterTable = [26]MonsterKind{
/* Name CARRY FLAGS str exp lvl arm hp dmg */
{"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, "0x0/0x0", 0}},
{"bat", 0, Flying, Stats{10, 1, 1, 3, 1, "1x2", 0}},
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, "1x2/1x5/1x5", 0}},
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, "1x8/1x8/3x10", 0}},
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, "1x2", 0}},
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, "%%%x0", 0}},
{"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, "4x3/3x5", 0}},
{"hobgoblin", 0, Mean, 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, Mean | Flying, Stats{10, 1, 1, 7, 1, "1x4", 0}},
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, "1x1", 0}},
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, "3x4/3x4/2x5", 0}},
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, "0x0", 0}},
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, "1x8", 0}},
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, "4x4", 0}},
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, "1x5/1x5", 0}},
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, "1x6", 0}},
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, "1x3", 0}},
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, "1x8/1x8/2x6", 0}},
{"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, "1x9/1x9/2x9", 0}},
{"vampire", 20, Regenerates | Mean, 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, Mean, Stats{10, 6, 2, 8, 1, "1x8", 0}},
{"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}},
{"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}},
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}},
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}},
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}},
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, dice("%%%x0"), 0}},
{"griffin", 20, Mean | Flying | Regenerates, Stats{10, 2000, 13, 2, 1, dice("4x3/3x5"), 0}},
{"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}},
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}},
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}},
{"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}},
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}},
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}},
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}},
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}},
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}},
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}},
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}},
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}},
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, dice("1x8/1x8/2x6"), 0}},
{"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, dice("1x9/1x9/2x9"), 0}},
{"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}},
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}},
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
}
// Base ObjInfo tables (extern.c). These are templates: NewGame copies them
@@ -323,5 +323,5 @@ var helpStr = []helpEntry{
// 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)"
Version = "rogue (git.eeqj.de/sneak/rgoue port of rogueforge 5.4.4)"
)

View File

@@ -69,11 +69,11 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
case KindArmor:
sp := it.Armors[which].Name
if obj.Flags.Has(Known) {
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp)
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.ArmorClass, 0, Armor), sp)
if !g.Options.Terse {
pb.WriteString("protection ")
}
fmt.Fprintf(&pb, "%d]", 10-obj.Arm)
fmt.Fprintf(&pb, "%d]", 10-obj.ArmorClass)
} else {
pb.WriteString(sp)
}
@@ -83,7 +83,7 @@ func (g *RogueGame) invName(obj *Object, drop bool) string {
case KindAmulet:
pb.WriteString("The Amulet of Yendor")
case KindGold:
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal())
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
}
out := pb.String()
@@ -168,7 +168,7 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
p.CurRing[hand] = nil
switch obj.RingKind() {
case RingAddStrength:
g.chgStr(-obj.Arm)
g.chgStr(-obj.Bonus)
case RingSeeInvisible:
g.unsee(0)
g.Extinguish(DUnsee)
@@ -180,9 +180,9 @@ func (g *RogueGame) dropCheck(obj *Object) bool {
// newThing returns a new random thing for the dungeon (things.c new_thing).
func (g *RogueGame) newThing() *Object {
cur := newObject()
cur.Damage = "0x0"
cur.HurlDmg = "0x0"
cur.Arm = 11
cur.Damage = dice("0x0")
cur.HurlDmg = dice("0x0")
cur.ArmorClass = 11
cur.Count = 1
// Decide what kind of object it will be; if we haven't had food for a
@@ -219,20 +219,20 @@ func (g *RogueGame) newThing() *Object {
case 4:
cur.Kind = KindArmor
cur.Which = pickOne(g, g.Items.Armors[:])
cur.Arm = aClass[cur.Which]
cur.ArmorClass = aClass[cur.Which]
if r := g.rnd(100); r < 20 {
cur.Flags.Set(Cursed)
cur.Arm += g.rnd(3) + 1
cur.ArmorClass += g.rnd(3) + 1
} else if r < 28 {
cur.Arm -= g.rnd(3) + 1
cur.ArmorClass -= g.rnd(3) + 1
}
case 5:
cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:])
switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
if cur.Arm = g.rnd(3); cur.Arm == 0 {
cur.Arm = -1
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
cur.Bonus = -1
cur.Flags.Set(Cursed)
}
case RingAggravateMonsters, RingTeleportation:

View File

@@ -377,9 +377,9 @@ type Stats struct {
Str int // strength (s_str; 3..31)
Exp int // experience
Lvl int // level of mastery
Arm int // armor class
ArmorClass int // armor class (s_arm)
HP int // hit points (s_hpt)
Dmg string // damage dice, e.g. "1x4/1x2"
Dmg DiceSpec // damage dice, e.g. "1x4/1x2"
MaxHP int
}

View File

@@ -123,20 +123,20 @@ func (g *RogueGame) wield() {
// initWeaps is the weapons.c init_dam[] table.
var initWeaps = [NumWeaponTypes]struct {
dam string // damage when wielded
hrl string // damage when thrown
dam DiceSpec // damage when wielded
hrl DiceSpec // damage when thrown
launch WeaponKind // launching weapon
flags ObjFlags
}{
{"2x4", "1x3", noWeapon, 0}, // WeaponMace
{"3x4", "1x2", noWeapon, 0}, // Long sword
{"1x1", "1x1", noWeapon, 0}, // WeaponBow
{"1x1", "2x3", WeaponBow, Stackable | Missile}, // WeaponArrow
{"1x6", "1x4", noWeapon, Missile}, // WeaponDagger
{"4x4", "1x2", noWeapon, 0}, // 2h sword
{"1x1", "1x3", noWeapon, Stackable | Missile}, // WeaponDart
{"1x2", "2x4", noWeapon, Stackable | Missile}, // Shuriken
{"2x3", "1x6", noWeapon, Missile}, // WeaponSpear
{dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
{dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
{dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
{dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
{dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
{dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
{dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
{dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
{dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
}
// initWeapon sets up a new weapon (weapons.c init_weapon).

View File

@@ -38,12 +38,12 @@ func (g *RogueGame) createObj() {
obj.HPlus += g.rnd(3) + 1
}
} else {
obj.Arm = aClass[obj.Which]
obj.ArmorClass = aClass[obj.Which]
if bless == '-' {
obj.Arm += g.rnd(3) + 1
obj.ArmorClass += g.rnd(3) + 1
}
if bless == '+' {
obj.Arm -= g.rnd(3) + 1
obj.ArmorClass -= g.rnd(3) + 1
}
}
case obj.Kind == KindRing:
@@ -54,9 +54,9 @@ func (g *RogueGame) createObj() {
g.Msgs.Mpos = 0
if bless == '-' {
obj.Flags.Set(Cursed)
obj.Arm = -1
obj.Bonus = -1
} else {
obj.Arm = g.rnd(2) + 1
obj.Bonus = g.rnd(2) + 1
}
case RingAggravateMonsters, RingTeleportation:
obj.Flags.Set(Cursed)
@@ -67,7 +67,7 @@ func (g *RogueGame) createObj() {
g.msg("how much?")
buf := ""
if g.getStr(&buf, g.scr.Std) == Norm {
obj.SetGoldVal(cAtoi(buf))
obj.GoldValue = cAtoi(buf)
}
}
g.addPack(obj, false)
@@ -169,7 +169,7 @@ func (g *RogueGame) teleport() {
if p.On(Held) {
p.Flags.Clear(Held)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
}
g.NoMove = 0
g.Count = 0

5
go.mod
View File

@@ -1,10 +1,11 @@
module sneak.berlin/go/rogue
module git.eeqj.de/sneak/rgoue
go 1.25.7
require github.com/gdamore/tcell/v2 v2.13.10
require (
github.com/gdamore/encoding v1.0.1 // indirect
github.com/gdamore/tcell/v2 v2.13.10 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.38.0 // indirect

View File

@@ -11,7 +11,7 @@ import (
"github.com/gdamore/tcell/v2"
"sneak.berlin/go/rogue/game"
"git.eeqj.de/sneak/rgoue/game"
)
// Tcell renders game Windows on a tcell screen and turns key events into