chore(lint): adopt canonical golangci-lint config

Replace .golangci.yml with the shared canonical config. The old
config's top-level linters-settings block was silently ignored under
the v2 schema, so the lll/funlen/cyclop/dupl thresholds now actually
apply. The four repo-specific disables (mnd, exhaustive, paralleltest,
testpackage) move out of the config into targeted in-code nolint
directives carrying their original approval dates, keeping the config
byte-identical to the canonical one.

Fixes surfaced by the stricter settings: t.Parallel() added to all 32
tests, 24 overlong lines wrapped or their comments tightened, tcell
control-code returns rewritten as character literals, dupl markers on
the identically-shaped item data tables, and two wsl_v5 defer cuddles.

No behavior changes. The repo has no golangci-lint version pin (no
Dockerfile or CI; make lint runs the host binary, currently v2.12.2),
so there was nothing to bump.
This commit is contained in:
2026-08-07 20:45:02 +00:00
parent 8ce238dd62
commit 63d1e797e2
45 changed files with 218 additions and 56 deletions

View File

@@ -1,5 +1,9 @@
version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run:
timeout: 5m
modules-download-mode: readonly
@@ -14,25 +18,17 @@ linters:
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
# Repo-specific exceptions approved by sneak (2026-07-06)
- paralleltest # Requires t.Parallel() in every test
# Approved by sneak 2026-07-07
- testpackage # Tests use internal package game to reach unexported state
- exhaustive # C-faithful switches handle only the cases C handled
- mnd # C-faithful gameplay literals; naming them hurts C-greppability
linters-settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

13
TODO.md
View File

@@ -34,6 +34,19 @@ wizard commands).
# Completed Steps
- 2026-08-07 Canonical linter config (`golangci-v2.12.2`): replaced
`.golangci.yml` with the shared canonical config (v2 schema; settings now live
under `linters.settings`, so the `lll`/`funlen`/`cyclop`/`dupl` thresholds
actually apply — the old top-level `linters-settings` block was silently
ignored). The four repo-specific disables (`mnd`, `exhaustive`,
`paralleltest`, `testpackage`) moved out of the config into targeted in-code
`//nolint` directives carrying the original approval dates, so the config
stays byte-identical to canonical. Real fixes: `t.Parallel()` in all 32 tests,
24 long lines wrapped or their comments tightened, control bytes in
`term/tcell.go` as character literals, and two `wsl_v5` defer cuddles. The
repo has no golangci-lint version pin to bump (no Dockerfile or CI;
`make lint` runs the host `golangci-lint`, currently v2.12.2).
- 2026-07-24 Seed compatibility — item tables (seed-compat): instrumented the C
reference on modern-rogue with a DUMP mode (testdata/c_seedcompat.patch) that
forces the RNG seed and prints the per-seed item appearance tables (potion

View File

@@ -128,6 +128,7 @@ func chooseSeed(wizard bool) int32 {
// The C game computed `lowtime + getpid()` in int; the truncation to
// 32 bits is the same wraparound the C int arithmetic performed.
//nolint:mnd // C-faithful: the C int wraparound mask
return int32(time.Now().Unix()&0x7fffffff) +
int32(os.Getpid()&0x7fffffff)
}

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// chase.c — code for one creature to chase another.
@@ -157,7 +158,9 @@ func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) {
// nearest exit toward its desire when it is in a different room, or the
// desire itself. shot means a dragon breathed flame instead of moving
// (the goal loop of chase.c do_chase).
func (g *RogueGame) chaseGoal(th *Monster, rer, ree *Room, door bool, mindist int) (Coord, bool) {
func (g *RogueGame) chaseGoal(
th *Monster, rer, ree *Room, door bool, mindist int,
) (Coord, bool) {
var this Coord
for {

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// command.c — read and execute the user commands.
@@ -794,6 +795,7 @@ func (g *RogueGame) call() {
func (g *RogueGame) callTarget(obj *Object) (*ObjInfo, string, *string, bool) {
it := &g.Items
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind {
case KindRing:
return &it.Rings[obj.Which], it.RingStones[obj.Which], nil, true
@@ -815,7 +817,9 @@ func (g *RogueGame) callTarget(obj *Object) (*ObjInfo, string, *string, bool) {
// callPrelude resolves the item's current name, reporting a previous
// guess; ok is false when the item is already identified (command.c
// call).
func (g *RogueGame) callPrelude(op *ObjInfo, elsewise string, guess *string) (string, *string, bool) {
func (g *RogueGame) callPrelude(
op *ObjInfo, elsewise string, guess *string,
) (string, *string, bool) {
fromGuess := false
if op != nil {

View File

@@ -2,16 +2,18 @@ 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
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 is where it is running to — aliases live coords (hero pos,
// room gold, another monster's pos).
Dest *Coord
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.

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// daemons.c — the daemon and fuse callbacks, dispatched by DaemonID.

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import "testing"
@@ -6,6 +7,8 @@ import "testing"
// 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) {
t.Parallel()
cases := []struct {
in string
want string
@@ -32,6 +35,8 @@ func TestParseDice(t *testing.T) {
// 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) {
t.Parallel()
data := newGameData()
for i, m := range data.monsterTable {
if len(m.Stats.Dmg) == 0 {

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import "testing"
@@ -37,6 +38,8 @@ func setInput(t *testing.T, g *RogueGame, input ...byte) {
}
func TestQuaffHealingPotion(t *testing.T) {
t.Parallel()
g := mkGameInput(t)
pot := newObject()
pot.Kind = KindPotion
@@ -61,6 +64,8 @@ func TestQuaffHealingPotion(t *testing.T) {
}
func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
t.Parallel()
g := mkGameInput(t)
pot := newObject()
pot.Kind = KindPotion
@@ -88,6 +93,8 @@ func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
}
func TestReadEnchantArmor(t *testing.T) {
t.Parallel()
g := mkGameInput(t)
scr := newObject()
scr.Kind = KindScroll
@@ -105,6 +112,7 @@ func TestReadEnchantArmor(t *testing.T) {
}
func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
t.Parallel()
// Note: this must not use a greedy monster ('O' orc, ISGREED): the C
// wake_monster gold-guarding check has no ISHELD guard, so the
// look(TRUE) at the end of read_scroll immediately re-wakes greedy
@@ -133,6 +141,8 @@ func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
// (orc) held by a scroll is re-woken by the look(TRUE) that read_scroll
// performs, ending up both held and running again.
func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
t.Parallel()
g := mkGameInput(t)
tp := spawnAdjacent(g, 'O')
tp.Flags.Set(Awake)
@@ -158,6 +168,8 @@ func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
}
func TestZapSlowMonster(t *testing.T) {
t.Parallel()
g := mkGameInput(t)
tp := spawnAdjacent(g, 'Z')
stick := newObject()
@@ -183,6 +195,8 @@ func TestZapSlowMonster(t *testing.T) {
}
func TestParseOpts(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1})
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "strconv"

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import "testing"
@@ -25,6 +26,8 @@ func spawnAdjacent(g *RogueGame, typ byte) *Monster {
}
func TestRollEmParsesMultiAttackDice(t *testing.T) {
t.Parallel()
g := mkGame(t, 42)
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: dice("1x4/1x4/1x4")}}
def := &Creature{Stats: Stats{ArmorClass: 10, HP: 1000}}
@@ -43,6 +46,8 @@ func TestRollEmParsesMultiAttackDice(t *testing.T) {
}
func TestFightKillsMonster(t *testing.T) {
t.Parallel()
g := mkGame(t, 7)
tp := spawnAdjacent(g, 'B') // bat: 1 hit die
tp.Stats.HP = 1
@@ -64,6 +69,8 @@ func TestFightKillsMonster(t *testing.T) {
}
func TestAttackHurtsPlayer(t *testing.T) {
t.Parallel()
g := mkGame(t, 9)
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
tp.Stats.Lvl = 20 // always hits
@@ -80,6 +87,8 @@ func TestAttackHurtsPlayer(t *testing.T) {
}
func TestRunnersChaseHero(t *testing.T) {
t.Parallel()
g := mkGame(t, 3)
// Place a hobgoblin a few squares away in the hero's room and set it
// running at the hero.
@@ -104,6 +113,8 @@ func TestRunnersChaseHero(t *testing.T) {
}
func TestKilledLeprechaunDropsGoldViaFall(t *testing.T) {
t.Parallel()
g := mkGame(t, 21)
tp := spawnAdjacent(g, 'L')
tp.Stats.HP = 0

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// ItemLore is the per-game item identity state: the randomized appearance

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "strings"
@@ -169,7 +170,8 @@ func (g *RogueGame) initProbs() {
sumProbs(g.Items.Scrolls[:])
sumProbs(g.Items.Rings[:])
sumProbs(g.Items.Sticks[:])
sumProbs(g.Items.Weapons[:NumWeaponTypes]) // C sums MAXWEAPONS, excluding the flame entry
// C sums MAXWEAPONS, excluding the flame entry.
sumProbs(g.Items.Weapons[:NumWeaponTypes])
sumProbs(g.Items.Armors[:])
}

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import (

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// Place describes a spot on the level map (rogue.h PLACE).

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// misc.c — look() display maintenance, direction input, eating, level-ups,
@@ -130,7 +131,9 @@ func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
// not actually step to (misc.c look).
func (g *RogueGame) lookDiagonalBlocked(s *lookScan, fp PlaceFlags, ch byte, y, x int) bool {
func (g *RogueGame) lookDiagonalBlocked(
s *lookScan, fp PlaceFlags, ch byte, y, x int,
) bool {
if !fp.Has(FPassage) && ch != Door {
return false
}
@@ -167,7 +170,9 @@ func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
// lookCellChar picks what the square shows: trip rendering for empty
// squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop).
func (g *RogueGame) lookCellChar(s *lookScan, tp *Monster, y, x int, ch byte) (byte, bool) {
func (g *RogueGame) lookCellChar(
s *lookScan, tp *Monster, y, x int, ch byte,
) (byte, bool) {
p := &g.Player
switch {

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// monsters.c — monster creation and saving throws.

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// move.c — hero movement commands.
@@ -60,7 +61,9 @@ func (g *RogueGame) moveHero(dy, dx int) {
// moveResolve acts on the square the hero stepped at: a wall may turn a
// passage runner (reported with the new deltas); anything else completes
// or refuses the move (the switch of move.c do_move).
func (g *RogueGame) moveResolve(nh Coord, ch byte, fl PlaceFlags, dy, dx int) (bool, int, int) {
func (g *RogueGame) moveResolve(
nh Coord, ch byte, fl PlaceFlags, dy, dx int,
) (bool, int, int) {
switch ch {
case ' ', '|', '-':
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
@@ -343,7 +346,8 @@ func (g *RogueGame) trapMystery(Coord) {
case 0:
g.msg("you are suddenly in a parallel dimension")
case 1:
g.msg("the light in here suddenly seems %s", g.data.rainbow[g.rnd(len(g.data.rainbow))])
g.msg("the light in here suddenly seems %s",
g.data.rainbow[g.rnd(len(g.data.rainbow))])
case 2:
g.msg("you feel a sting in the side of your neck")
case 3:

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// new_level.c — dig and draw a new level.

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import (
@@ -35,6 +36,8 @@ func renderMap(g *RogueGame) string {
}
func TestNewLevelInvariants(t *testing.T) {
t.Parallel()
for _, seed := range []int32{1, 12345, 2026, 99999} {
g := genLevel(t, seed)
@@ -142,6 +145,8 @@ func checkStartingKit(t *testing.T, g *RogueGame, seed int32) {
}
func TestNewLevelDeterministic(t *testing.T) {
t.Parallel()
a := renderMap(genLevel(t, 12345))
b := renderMap(genLevel(t, 12345))
@@ -158,6 +163,8 @@ func TestNewLevelDeterministic(t *testing.T) {
// TestDeeperLevels exercises generation across many depths and seeds —
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
func TestDeeperLevels(t *testing.T) {
t.Parallel()
for _, seed := range []int32{7, 42, 1000, 31337} {
g := New(Params{Seed: seed})
for depth := 1; depth <= 30; depth++ {

View File

@@ -38,6 +38,7 @@ const (
// Glyph returns the map/display character for this kind of object.
func (k ObjectKind) Glyph() byte {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k {
case KindPotion:
return Potion
@@ -64,6 +65,7 @@ func (k ObjectKind) Glyph() byte {
// String names the category the way the C type_name() did.
func (k ObjectKind) String() string {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k {
case KindPotion:
return potionName
@@ -118,6 +120,7 @@ func (k ObjectKind) MergesInPack() bool {
// stringRest names the remaining kinds, including the ring-or-stick
// prompt pseudo-kind (the tail of the C type_name switch).
func (k ObjectKind) stringRest() string {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k {
case KindWand:
return "wand or staff"

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "strings"
@@ -118,6 +119,7 @@ func boolStr(b bool) string {
// getOpt reads a new value for an option (options.c get_bool/get_sf/
// get_inv_t/get_str dispatch).
func (g *RogueGame) getOpt(op *optDesc) int {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch op.kind {
case optBool:
return g.getBool(op.boolP)

View File

@@ -156,7 +156,9 @@ func packScanWhich(pack []*Object, obj *Object, i, lp int) (int, int) {
// anything else marks the insertion point (the matched-subtype switch
// of pack.c add_pack). It returns the resulting entry, the insert-after
// index, whether a merge happened, and ok false when the pack is full.
func (g *RogueGame) packMatch(obj, op *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
func (g *RogueGame) packMatch(
obj, op *Object, i int, fromFloor bool,
) (*Object, int, bool, bool) {
switch {
case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) {
@@ -175,7 +177,9 @@ func (g *RogueGame) packMatch(obj, op *Object, i int, fromFloor bool) (*Object,
// packMatchGroup rejoins a grouped missile bundle with its group entry
// (the o_group arm of pack.c add_pack).
func (g *RogueGame) packMatchGroup(obj *Object, i int, fromFloor bool) (*Object, int, bool, bool) {
func (g *RogueGame) packMatchGroup(
obj *Object, i int, fromFloor bool,
) (*Object, int, bool, bool) {
p := &g.Player
lp := i
@@ -333,6 +337,7 @@ func (g *RogueGame) pickUp(ch byte) {
// everything, KindCallable takes anything nameable (not food, not the
// amulet), KindRingOrStick takes rings and wands.
func matchesFilter(kind ObjectKind, item *Object) bool {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch kind {
case KindNone:
return true

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// passages.c — draw the connecting passages.

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "fmt"
@@ -261,6 +262,7 @@ func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
// isMagic reports whether an object radiates magic (potions.c is_magic).
func (g *RogueGame) isMagic(o *Object) bool {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch o.Kind {
case KindArmor:
return o.Flags.Has(Protected) || o.ArmorClass != g.data.aClass[o.Which]

View File

@@ -33,6 +33,7 @@ func (g *RogueGame) ringOn() {
p.CurRing[ring] = obj
// Calculate the effect it has on the poor guy.
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() {
case RingAddStrength:
g.changeStrength(obj.Bonus)
@@ -171,6 +172,7 @@ func ringNum(_ *RogueGame, obj *Object) string {
return ""
}
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import (
@@ -98,8 +99,10 @@ func (g *RogueGame) totalWinner() {
}
g.standend()
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the")
g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at")
g.scr.Std.MvAddStr(10, 0,
"You have joined the elite ranks of those who have escaped the")
g.scr.Std.MvAddStr(11, 0,
"Dungeons of Doom alive. You journey home and sell all your loot at")
g.scr.Std.MvAddStr(12, 0, "a great profit and are admitted to the Fighters' Guild.")
g.mvaddstr(NumLines-1, 0, "--Press space to continue--")
g.refresh()
@@ -132,6 +135,7 @@ func (g *RogueGame) objectWorth(obj *Object) int {
it := &g.Items
worth := 0
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind {
case KindFood:
worth = 2 * obj.Count

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// Rng is the original Rogue linear congruential generator. The C RN macro is

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import "testing"
@@ -6,6 +7,8 @@ import "testing"
// (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) {
t.Parallel()
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},
@@ -23,6 +26,8 @@ func TestRndMatchesCImplementation(t *testing.T) {
}
func TestRollMatchesCImplementation(t *testing.T) {
t.Parallel()
golden := map[int32][]int{
1: {6, 8, 10},
12345: {10, 10, 15},
@@ -42,6 +47,8 @@ func TestRollMatchesCImplementation(t *testing.T) {
// 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) {
t.Parallel()
r := &Rng{Seed: 42}
if got := r.Rnd(0); got != 0 {
t.Fatalf("rnd(0) = %d, want 0", got)
@@ -55,6 +62,8 @@ func TestRndZeroDoesNotStep(t *testing.T) {
// spread(1)==1 and spread(2)==2 deterministically; the C BEFORE/AFTER
// constants rely on this.
func TestSpreadSmallValues(t *testing.T) {
t.Parallel()
g := &RogueGame{Rng: &Rng{Seed: 7}}
if got := g.spread(1); got != 1 {
t.Errorf("spread(1) = %d, want 1", got)

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// rooms.c — create the layout for the new level.
@@ -330,7 +331,9 @@ func floorChar(rp *Room) byte {
return Floor
}
func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) {
func (g *RogueGame) findFloorImpl(
rp *Room, limit int, monst, pickroom bool,
) (Coord, bool) {
var compchar byte
if !pickroom {
compchar = floorChar(rp)

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import (
@@ -39,6 +40,7 @@ func driveTurns(t *testing.T, g *RogueGame, n int) {
// TestRunDownStairs stands the hero on the staircase and descends via the
// '>' command through the real turn loop, then checks the level changed.
func TestRunDownStairs(t *testing.T) {
t.Parallel()
// '>' is a free action (After=false), so it is followed by a paying
// rest ('.') to end the command() call; without a paying action the
// turn loop would spin forever on the auto-fed prompt input.
@@ -65,6 +67,8 @@ func TestRunDownStairs(t *testing.T) {
// quit and death paths that normally show it now exit the process, so the
// display is exercised through score() directly.
func TestScoreRendersList(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1, Term: &testTerm{}})
g.Player.Purse = 100
@@ -90,6 +94,8 @@ func TestScoreRendersList(t *testing.T) {
// The hero is fortified so no death exits the process (step 8), and the fixed
// seed keeps it deterministic.
func TestDeepPlaythrough(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}})
g.startLevel()
g.prePlay()
@@ -155,6 +161,7 @@ func TestDeepPlaythrough(t *testing.T) {
// and traps. The hero is fortified each turn so nothing exits the process,
// and the fixed seeds keep it deterministic; the point is to surface panics.
func TestTurnLoopCrashSweep(t *testing.T) {
t.Parallel()
// A generous mix of movement, search, and rest. The spaces between
// commands double as answers to any --More-- prompt (wait_for eats
// everything up to a space); without them one prompt would swallow the

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import (
@@ -686,6 +687,7 @@ func Restore(path string, params Params) (*RogueGame, error) {
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }() // read-only handle
var st SaveState

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import (
@@ -8,6 +9,8 @@ import (
)
func TestSaveRestoreRoundTrip(t *testing.T) {
t.Parallel()
g := mkGame(t, 4242)
// Dirty up some state so the round trip is meaningful.
g.Player.Purse = 123
@@ -134,6 +137,8 @@ func checkEquipmentAliasing(t *testing.T, g, h *RogueGame) {
}
func TestRestoreRejectsWrongVersion(t *testing.T) {
t.Parallel()
g := mkGame(t, 1)
path := filepath.Join(t.TempDir(), "rogue.save")
st := g.snapshot()

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import (
@@ -36,6 +37,7 @@ func (g *RogueGame) rdScore() []ScoreEnt {
if err != nil {
return topTen
}
defer func() { _ = f.Close() }() // read-only handle
var onDisk []ScoreEnt

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// scrolls.c — read a scroll and let it happen.
@@ -148,7 +149,8 @@ func (g *RogueGame) createMonsterSpot() (Coord, bool) {
// Or anything else nasty
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
if ch == Scroll {
if fo := g.Level.ObjectAt(y, x); fo != nil && fo.ScrollKind() == ScrollScareMonster {
if fo := g.Level.ObjectAt(y, x); fo != nil &&
fo.ScrollKind() == ScrollScareMonster {
continue
}
}

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import (
@@ -51,6 +52,8 @@ func dumpItemTables(seed int32, g *RogueGame) string {
// match C byte for byte. The golden is captured from an instrumented build
// of the C game (testdata/README.md).
func TestSeedCompatItemTables(t *testing.T) {
t.Parallel()
golden, err := os.ReadFile("testdata/item_tables.golden")
if err != nil {
t.Fatalf("read golden: %v", err)

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "fmt"
@@ -272,6 +273,7 @@ func slowTarget(tp *Monster) {
func (g *RogueGame) zapBolt(obj *Object) bool {
var name string
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.WandKind() {
case WandLightning:
name = "bolt"
@@ -434,7 +436,9 @@ func boltBounces(ch byte, heroPos, pos Coord) bool {
// boltStrikesMonster resolves a bolt arriving on a monster's square (the
// monster arm of the fire_bolt loop). It reports whether the bolt was
// used up.
func (g *RogueGame) boltStrikesMonster(tp *Monster, bolt *Object, pos Coord, ch byte, name string, fromHero bool) bool {
func (g *RogueGame) boltStrikesMonster(
tp *Monster, bolt *Object, pos Coord, ch byte, name string, fromHero bool,
) bool {
tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos
@@ -506,6 +510,7 @@ func (g *RogueGame) fixStick(cur *Object) {
cur.HurlDmg = dice("1x1")
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch cur.WandKind() {
case WandLight:
cur.Charges = g.rnd(10) + 10

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// tables.go — the static data tables the C game kept as file-scope globals
@@ -166,7 +167,10 @@ const ripWall = " | |"
//nolint:funlen,maintidx // a single composite literal holding every C data table
func newGameData() *gameData {
return &gameData{
initStats: Stats{Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12, Dmg: dice("1x4"), MaxHP: 12},
initStats: Stats{
Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12,
Dmg: dice("1x4"), MaxHP: 12,
},
aClass: [NumArmorTypes]int{
8, // LEATHER
@@ -205,7 +209,8 @@ func newGameData() *gameData {
{"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}},
{"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}},
@@ -248,6 +253,7 @@ func newGameData() *gameData {
{Name: "plate mail", Prob: 5, Worth: 150},
},
//nolint:dupl // distinct C data tables that merely share their shape
basePotInfo: [NumPotionTypes]ObjInfo{
{Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", Prob: 8, Worth: 5},
@@ -265,6 +271,7 @@ func newGameData() *gameData {
{Name: "levitation", Prob: 6, Worth: 75},
},
//nolint:dupl // distinct C data tables that merely share their shape
baseRingInfo: [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", Prob: 9, Worth: 400},
@@ -316,6 +323,7 @@ func newGameData() *gameData {
{}, // DO NOT REMOVE: fake entry for dragon's breath
},
//nolint:dupl // distinct C data tables that merely share their shape
baseWsInfo: [NumWandTypes]ObjInfo{
{Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5},

View File

@@ -1,9 +1,12 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
import "testing"
// badcheck from init.c: every probability table must sum to exactly 100.
func TestProbabilitiesSumTo100(t *testing.T) {
t.Parallel()
sum := func(info []ObjInfo) int {
s := 0
for _, oi := range info {
@@ -32,6 +35,8 @@ func TestProbabilitiesSumTo100(t *testing.T) {
}
func TestInitProbsCumulative(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1})
last := g.Items.Potions[NumPotionTypes-1].Prob
@@ -47,6 +52,8 @@ func TestInitProbsCumulative(t *testing.T) {
}
func TestNewGameRandomizesAppearances(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 12345})
checkPotionColors(t, g)
@@ -112,6 +119,8 @@ func checkWandMaterials(t *testing.T, g *RogueGame) {
}
func TestMonsterTable(t *testing.T) {
t.Parallel()
data := newGameData()
if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" {
t.Error("monster table order broken")

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game
// testTerm is a headless Terminal for tests: rendering is a no-op and

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import (
@@ -15,13 +16,15 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
which := obj.Which
it := &g.Items
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind {
case KindPotion:
g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr)
case KindRing:
g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
case KindWand:
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which],
&it.Sticks[which], chargeStr)
case KindScroll:
g.nameScroll(&pb, obj)
case KindFood:
@@ -250,6 +253,7 @@ func (g *RogueGame) dropRing(obj *Object) {
p.CurRing[hand] = nil
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() {
case RingAddStrength:
g.changeStrength(-obj.Bonus)
@@ -346,6 +350,7 @@ func (g *RogueGame) newRingThing(cur *Object) {
cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:])
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {

View File

@@ -118,7 +118,7 @@ const (
Known // ISKNOW: player knows details about the object
Missile // ISMISL: object is a missile type
Stackable // ISMANY: object comes in groups
WasFound // ISFOUND (objects): object has been seen (ISFOUND shares the bit with creatures)
WasFound // ISFOUND (objects): seen; bit shared with creatures
Protected // ISPROT: armor is permanently protected
)
@@ -140,18 +140,18 @@ type CreatureFlags int32
// Creature state bits (rogue.h).
const (
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: creature can see invisible creatures
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: can see invisible creatures
Blind CreatureFlags = 0o000004 // ISBLIND: creature is blind
Cancelled CreatureFlags = 0o000010 // ISCANC: creature has special qualities cancelled
Cancelled CreatureFlags = 0o000010 // ISCANC: special qualities cancelled
Levitating CreatureFlags = 0o000010 // ISLEVIT: hero is levitating
Found CreatureFlags = 0o000020 // ISFOUND: creature has been seen
Greedy CreatureFlags = 0o000040 // ISGREED: creature runs to protect gold
Hasted CreatureFlags = 0o000100 // ISHASTE: creature has been hastened
Targeted CreatureFlags = 0o000200 // ISTARGET: creature is the target of an 'f' command
Targeted CreatureFlags = 0o000200 // ISTARGET: target of an 'f' command
Held CreatureFlags = 0o000400 // ISHELD: creature has been held
Confused CreatureFlags = 0o001000 // ISHUH: creature is confused
Invisible CreatureFlags = 0o002000 // ISINVIS: creature is invisible
Mean CreatureFlags = 0o004000 // ISMEAN: creature can wake when player enters room
Mean CreatureFlags = 0o004000 // ISMEAN: wakes when player enters room
Hallucinating CreatureFlags = 0o004000 // ISHALU: hero is on acid trip
Regenerates CreatureFlags = 0o010000 // ISREGEN: creature can regenerate
Awake CreatureFlags = 0o020000 // ISRUN: creature is running at the player
@@ -392,7 +392,7 @@ type Stone struct {
}
// CTRL maps a letter to its control character, as the C CTRL() macro.
func CTRL(c byte) byte { return c & 0o37 }
func CTRL(c byte) byte { return c & 0o37 } //nolint:mnd // the C CTRL() mask
// distance returns the squared distance between two points (chase.c dist).
func distance(y1, x1, y2, x2 int) int {

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
import "fmt"

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game
// wizard.c — special wizard commands, some of which are also non-wizard
@@ -25,6 +26,7 @@ func (g *RogueGame) createObj() {
obj.Count = 1
g.Msgs.Mpos = 0
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind {
case KindWeapon, KindArmor:
g.createWeaponArmor(obj)
@@ -82,6 +84,7 @@ func (g *RogueGame) createWeaponArmor(obj *Object) {
// createRing sets up a wizard-created ring, prompting for a bonus on
// the bonus rings (the ring arm of wizard.c create_obj).
func (g *RogueGame) createRing(obj *Object) {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)")
@@ -136,6 +139,7 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
return
}
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind {
case KindScroll:
setKnow(obj, g.Items.Scrolls[:])

View File

@@ -128,6 +128,7 @@ func namedKey(k tcell.Key) (byte, bool) {
// motionKey translates the arrow and paging keys to Rogue's movement
// letters (tcell.go ReadChar).
func motionKey(k tcell.Key) (byte, bool) {
//nolint:exhaustive // translation table: all other keys fall through
switch k {
case tcell.KeyUp:
return 'k', true
@@ -153,19 +154,20 @@ func motionKey(k tcell.Key) (byte, bool) {
// editingKey translates the editing and control keys to their C0 codes
// (tcell.go ReadChar).
func editingKey(k tcell.Key) (byte, bool) {
//nolint:exhaustive // translation table: all other keys fall through
switch k {
case tcell.KeyEnter:
return '\n', true
case tcell.KeyEscape:
return game.Escape, true
case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8, true
return '\b', true
case tcell.KeyDelete:
return 0x7f, true
return '\x7f', true
case tcell.KeyTab:
return '\t', true
case tcell.KeyCtrlC:
return 3, true
return '\x03', true
}
return 0, false
@@ -188,8 +190,8 @@ func (t *Tcell) ShellEscape() {
"[Entering shell; exit to return to the game]")
// The shell session has no deadline by design; Background context.
cmd := exec.CommandContext(context.Background(), //nolint:gosec // G204: the user's own $SHELL
shell)
//nolint:gosec // G204: the user's own $SHELL
cmd := exec.CommandContext(context.Background(), shell)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr