Merge pull request 'Update golangci-lint to v2.12.2 with canonical config' (#1) from golangci-v2.12.2 into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-08-07 23:24:38 +02:00
45 changed files with 218 additions and 56 deletions

View File

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

13
TODO.md
View File

@@ -34,6 +34,19 @@ wizard commands).
# Completed Steps # 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 - 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 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 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 // The C game computed `lowtime + getpid()` in int; the truncation to
// 32 bits is the same wraparound the C int arithmetic performed. // 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) + return int32(time.Now().Unix()&0x7fffffff) +
int32(os.Getpid()&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 package game
// chase.c — code for one creature to chase another. // 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 // 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 // desire itself. shot means a dragon breathed flame instead of moving
// (the goal loop of chase.c do_chase). // (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 var this Coord
for { for {

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// command.c — read and execute the user commands. // 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) { func (g *RogueGame) callTarget(obj *Object) (*ObjInfo, string, *string, bool) {
it := &g.Items it := &g.Items
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind { switch obj.Kind {
case KindRing: case KindRing:
return &it.Rings[obj.Which], it.RingStones[obj.Which], nil, true 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 // callPrelude resolves the item's current name, reporting a previous
// guess; ok is false when the item is already identified (command.c // guess; ok is false when the item is already identified (command.c
// call). // 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 fromGuess := false
if op != nil { 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. // Creature is the _t arm of the C THING union: the player or a monster.
type Creature struct { type Creature struct {
Pos Coord // position Pos Coord // position
Turn bool // if slowed, is it a turn to move Turn bool // if slowed, is it a turn to move
Type byte // what it is: 'A'..'Z' for monsters, '@' for the player Type byte // what it is: 'A'..'Z' for monsters, '@' for the player
Disguise byte // what mimic looks like Disguise byte // what mimic looks like
OldCh byte // character that was where it was 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) // Dest is where it is running to — aliases live coords (hero pos,
Flags CreatureFlags // room gold, another monster's pos).
Stats Stats Dest *Coord
Room *Room // current room for thing Flags CreatureFlags
Pack []*Object // what the thing is carrying Stats Stats
Room *Room // current room for thing
Pack []*Object // what the thing is carrying
} }
// Monster is a hostile creature on the level. // 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 package game
// daemons.c — the daemon and fuse callbacks, dispatched by DaemonID. // 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 package game
import "testing" import "testing"
@@ -6,6 +7,8 @@ import "testing"
// including its junk-tolerant edges: the bestiary placeholder "%%%x0" and // including its junk-tolerant edges: the bestiary placeholder "%%%x0" and
// the flytrap reset "000x0" both mean a single 0x0 attack. // the flytrap reset "000x0" both mean a single 0x0 attack.
func TestParseDice(t *testing.T) { func TestParseDice(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
in string in string
want 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 // The bestiary and weapon tables must parse to at least one attack each so
// every creature and weapon actually swings. // every creature and weapon actually swings.
func TestTablesHaveDice(t *testing.T) { func TestTablesHaveDice(t *testing.T) {
t.Parallel()
data := newGameData() data := newGameData()
for i, m := range data.monsterTable { for i, m := range data.monsterTable {
if len(m.Stats.Dmg) == 0 { 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 package game
import "testing" import "testing"
@@ -37,6 +38,8 @@ func setInput(t *testing.T, g *RogueGame, input ...byte) {
} }
func TestQuaffHealingPotion(t *testing.T) { func TestQuaffHealingPotion(t *testing.T) {
t.Parallel()
g := mkGameInput(t) g := mkGameInput(t)
pot := newObject() pot := newObject()
pot.Kind = KindPotion pot.Kind = KindPotion
@@ -61,6 +64,8 @@ func TestQuaffHealingPotion(t *testing.T) {
} }
func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) { func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
t.Parallel()
g := mkGameInput(t) g := mkGameInput(t)
pot := newObject() pot := newObject()
pot.Kind = KindPotion pot.Kind = KindPotion
@@ -88,6 +93,8 @@ func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
} }
func TestReadEnchantArmor(t *testing.T) { func TestReadEnchantArmor(t *testing.T) {
t.Parallel()
g := mkGameInput(t) g := mkGameInput(t)
scr := newObject() scr := newObject()
scr.Kind = KindScroll scr.Kind = KindScroll
@@ -105,6 +112,7 @@ func TestReadEnchantArmor(t *testing.T) {
} }
func TestReadHoldMonsterFreezesAdjacent(t *testing.T) { func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
t.Parallel()
// Note: this must not use a greedy monster ('O' orc, ISGREED): the C // Note: this must not use a greedy monster ('O' orc, ISGREED): the C
// wake_monster gold-guarding check has no ISHELD guard, so the // wake_monster gold-guarding check has no ISHELD guard, so the
// look(TRUE) at the end of read_scroll immediately re-wakes greedy // 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 // (orc) held by a scroll is re-woken by the look(TRUE) that read_scroll
// performs, ending up both held and running again. // performs, ending up both held and running again.
func TestHoldScrollGreedyMonsterQuirk(t *testing.T) { func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
t.Parallel()
g := mkGameInput(t) g := mkGameInput(t)
tp := spawnAdjacent(g, 'O') tp := spawnAdjacent(g, 'O')
tp.Flags.Set(Awake) tp.Flags.Set(Awake)
@@ -158,6 +168,8 @@ func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
} }
func TestZapSlowMonster(t *testing.T) { func TestZapSlowMonster(t *testing.T) {
t.Parallel()
g := mkGameInput(t) g := mkGameInput(t)
tp := spawnAdjacent(g, 'Z') tp := spawnAdjacent(g, 'Z')
stick := newObject() stick := newObject()
@@ -183,6 +195,8 @@ func TestZapSlowMonster(t *testing.T) {
} }
func TestParseOpts(t *testing.T) { func TestParseOpts(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1}) g := New(Params{Seed: 1})
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow") 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 package game
import "strconv" import "strconv"

View File

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

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// Place describes a spot on the level map (rogue.h PLACE). // 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 package game
// misc.c — look() display maintenance, direction input, eating, level-ups, // 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 // lookDiagonalBlocked hides diagonal door/passage squares the hero could
// not actually step to (misc.c look). // 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 { if !fp.Has(FPassage) && ch != Door {
return false 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 // lookCellChar picks what the square shows: trip rendering for empty
// squares, waking and disguises for monsters. skip means the square is // squares, waking and disguises for monsters. skip means the square is
// not drawn at all (the monster switch of the look loop). // 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 p := &g.Player
switch { switch {

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// monsters.c — monster creation and saving throws. // 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 package game
// move.c — hero movement commands. // 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 // moveResolve acts on the square the hero stepped at: a wall may turn a
// passage runner (reported with the new deltas); anything else completes // passage runner (reported with the new deltas); anything else completes
// or refuses the move (the switch of move.c do_move). // 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 { switch ch {
case ' ', '|', '-': case ' ', '|', '-':
if turn, ndy, ndx := g.passageTurn(dy, dx); turn { if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
@@ -343,7 +346,8 @@ func (g *RogueGame) trapMystery(Coord) {
case 0: case 0:
g.msg("you are suddenly in a parallel dimension") g.msg("you are suddenly in a parallel dimension")
case 1: 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: case 2:
g.msg("you feel a sting in the side of your neck") g.msg("you feel a sting in the side of your neck")
case 3: case 3:

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// new_level.c — dig and draw a new level. // 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 package game
import ( import (
@@ -35,6 +36,8 @@ func renderMap(g *RogueGame) string {
} }
func TestNewLevelInvariants(t *testing.T) { func TestNewLevelInvariants(t *testing.T) {
t.Parallel()
for _, seed := range []int32{1, 12345, 2026, 99999} { for _, seed := range []int32{1, 12345, 2026, 99999} {
g := genLevel(t, seed) g := genLevel(t, seed)
@@ -142,6 +145,8 @@ func checkStartingKit(t *testing.T, g *RogueGame, seed int32) {
} }
func TestNewLevelDeterministic(t *testing.T) { func TestNewLevelDeterministic(t *testing.T) {
t.Parallel()
a := renderMap(genLevel(t, 12345)) a := renderMap(genLevel(t, 12345))
b := 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 — // TestDeeperLevels exercises generation across many depths and seeds —
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep. // mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
func TestDeeperLevels(t *testing.T) { func TestDeeperLevels(t *testing.T) {
t.Parallel()
for _, seed := range []int32{7, 42, 1000, 31337} { for _, seed := range []int32{7, 42, 1000, 31337} {
g := New(Params{Seed: seed}) g := New(Params{Seed: seed})
for depth := 1; depth <= 30; depth++ { for depth := 1; depth <= 30; depth++ {

View File

@@ -38,6 +38,7 @@ const (
// Glyph returns the map/display character for this kind of object. // Glyph returns the map/display character for this kind of object.
func (k ObjectKind) Glyph() byte { func (k ObjectKind) Glyph() byte {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k { switch k {
case KindPotion: case KindPotion:
return Potion return Potion
@@ -64,6 +65,7 @@ func (k ObjectKind) Glyph() byte {
// String names the category the way the C type_name() did. // String names the category the way the C type_name() did.
func (k ObjectKind) String() string { func (k ObjectKind) String() string {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k { switch k {
case KindPotion: case KindPotion:
return potionName return potionName
@@ -118,6 +120,7 @@ func (k ObjectKind) MergesInPack() bool {
// stringRest names the remaining kinds, including the ring-or-stick // stringRest names the remaining kinds, including the ring-or-stick
// prompt pseudo-kind (the tail of the C type_name switch). // prompt pseudo-kind (the tail of the C type_name switch).
func (k ObjectKind) stringRest() string { func (k ObjectKind) stringRest() string {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch k { switch k {
case KindWand: case KindWand:
return "wand or staff" 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 package game
import "strings" 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/ // getOpt reads a new value for an option (options.c get_bool/get_sf/
// get_inv_t/get_str dispatch). // get_inv_t/get_str dispatch).
func (g *RogueGame) getOpt(op *optDesc) int { func (g *RogueGame) getOpt(op *optDesc) int {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch op.kind { switch op.kind {
case optBool: case optBool:
return g.getBool(op.boolP) 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 // anything else marks the insertion point (the matched-subtype switch
// of pack.c add_pack). It returns the resulting entry, the insert-after // 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. // 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 { switch {
case op.Kind.MergesInPack(): case op.Kind.MergesInPack():
if !g.packRoom(fromFloor, obj) { 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 // packMatchGroup rejoins a grouped missile bundle with its group entry
// (the o_group arm of pack.c add_pack). // (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 p := &g.Player
lp := i lp := i
@@ -333,6 +337,7 @@ func (g *RogueGame) pickUp(ch byte) {
// everything, KindCallable takes anything nameable (not food, not the // everything, KindCallable takes anything nameable (not food, not the
// amulet), KindRingOrStick takes rings and wands. // amulet), KindRingOrStick takes rings and wands.
func matchesFilter(kind ObjectKind, item *Object) bool { func matchesFilter(kind ObjectKind, item *Object) bool {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch kind { switch kind {
case KindNone: case KindNone:
return true return true

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// passages.c — draw the connecting passages. // 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 package game
import "fmt" 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). // isMagic reports whether an object radiates magic (potions.c is_magic).
func (g *RogueGame) isMagic(o *Object) bool { func (g *RogueGame) isMagic(o *Object) bool {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch o.Kind { switch o.Kind {
case KindArmor: case KindArmor:
return o.Flags.Has(Protected) || o.ArmorClass != g.data.aClass[o.Which] 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 p.CurRing[ring] = obj
// Calculate the effect it has on the poor guy. // 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() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.changeStrength(obj.Bonus) g.changeStrength(obj.Bonus)
@@ -171,6 +172,7 @@ func ringNum(_ *RogueGame, obj *Object) string {
return "" return ""
} }
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity: case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring)) 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 package game
import ( import (
@@ -98,8 +99,10 @@ func (g *RogueGame) totalWinner() {
} }
g.standend() g.standend()
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the") g.scr.Std.MvAddStr(10, 0,
g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at") "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.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.mvaddstr(NumLines-1, 0, "--Press space to continue--")
g.refresh() g.refresh()
@@ -132,6 +135,7 @@ func (g *RogueGame) objectWorth(obj *Object) int {
it := &g.Items it := &g.Items
worth := 0 worth := 0
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind { switch obj.Kind {
case KindFood: case KindFood:
worth = 2 * obj.Count 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 package game
// Rng is the original Rogue linear congruential generator. The C RN macro is // 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 package game
import "testing" import "testing"
@@ -6,6 +7,8 @@ import "testing"
// (seed = seed*11109+13849; rnd(range) = abs(RN) % range) compiled and run // (seed = seed*11109+13849; rnd(range) = abs(RN) % range) compiled and run
// on this machine. They lock in seed compatibility with the C game. // on this machine. They lock in seed compatibility with the C game.
func TestRndMatchesCImplementation(t *testing.T) { func TestRndMatchesCImplementation(t *testing.T) {
t.Parallel()
golden := map[int32][]int{ golden := map[int32][]int{
1: {0, 30, 79, 7, 87, 1, 23, 7, 57, 98}, 1: {0, 30, 79, 7, 87, 1, 23, 7, 57, 98},
12345: {92, 92, 45, 98, 24, 39, 92, 67, 3, 7}, 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) { func TestRollMatchesCImplementation(t *testing.T) {
t.Parallel()
golden := map[int32][]int{ golden := map[int32][]int{
1: {6, 8, 10}, 1: {6, 8, 10},
12345: {10, 10, 15}, 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 // rnd(0) must return 0 without stepping the generator: the C macro
// short-circuits before evaluating RN, and BEFORE/AFTER depend on it. // short-circuits before evaluating RN, and BEFORE/AFTER depend on it.
func TestRndZeroDoesNotStep(t *testing.T) { func TestRndZeroDoesNotStep(t *testing.T) {
t.Parallel()
r := &Rng{Seed: 42} r := &Rng{Seed: 42}
if got := r.Rnd(0); got != 0 { if got := r.Rnd(0); got != 0 {
t.Fatalf("rnd(0) = %d, want 0", got) 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 // spread(1)==1 and spread(2)==2 deterministically; the C BEFORE/AFTER
// constants rely on this. // constants rely on this.
func TestSpreadSmallValues(t *testing.T) { func TestSpreadSmallValues(t *testing.T) {
t.Parallel()
g := &RogueGame{Rng: &Rng{Seed: 7}} g := &RogueGame{Rng: &Rng{Seed: 7}}
if got := g.spread(1); got != 1 { if got := g.spread(1); got != 1 {
t.Errorf("spread(1) = %d, want 1", got) 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 package game
// rooms.c — create the layout for the new level. // rooms.c — create the layout for the new level.
@@ -330,7 +331,9 @@ func floorChar(rp *Room) byte {
return Floor 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 var compchar byte
if !pickroom { if !pickroom {
compchar = floorChar(rp) compchar = floorChar(rp)

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game package game
import ( 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 // TestRunDownStairs stands the hero on the staircase and descends via the
// '>' command through the real turn loop, then checks the level changed. // '>' command through the real turn loop, then checks the level changed.
func TestRunDownStairs(t *testing.T) { func TestRunDownStairs(t *testing.T) {
t.Parallel()
// '>' is a free action (After=false), so it is followed by a paying // '>' is a free action (After=false), so it is followed by a paying
// rest ('.') to end the command() call; without a paying action the // rest ('.') to end the command() call; without a paying action the
// turn loop would spin forever on the auto-fed prompt input. // 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 // quit and death paths that normally show it now exit the process, so the
// display is exercised through score() directly. // display is exercised through score() directly.
func TestScoreRendersList(t *testing.T) { func TestScoreRendersList(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1, Term: &testTerm{}}) g := New(Params{Seed: 1, Term: &testTerm{}})
g.Player.Purse = 100 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 // The hero is fortified so no death exits the process (step 8), and the fixed
// seed keeps it deterministic. // seed keeps it deterministic.
func TestDeepPlaythrough(t *testing.T) { func TestDeepPlaythrough(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}}) g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}})
g.startLevel() g.startLevel()
g.prePlay() 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 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. // and the fixed seeds keep it deterministic; the point is to surface panics.
func TestTurnLoopCrashSweep(t *testing.T) { func TestTurnLoopCrashSweep(t *testing.T) {
t.Parallel()
// A generous mix of movement, search, and rest. The spaces between // A generous mix of movement, search, and rest. The spaces between
// commands double as answers to any --More-- prompt (wait_for eats // commands double as answers to any --More-- prompt (wait_for eats
// everything up to a space); without them one prompt would swallow the // 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 package game
import ( import (
@@ -686,6 +687,7 @@ func Restore(path string, params Params) (*RogueGame, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { _ = f.Close() }() // read-only handle defer func() { _ = f.Close() }() // read-only handle
var st SaveState var st SaveState

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
package game package game
import ( 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 // match C byte for byte. The golden is captured from an instrumented build
// of the C game (testdata/README.md). // of the C game (testdata/README.md).
func TestSeedCompatItemTables(t *testing.T) { func TestSeedCompatItemTables(t *testing.T) {
t.Parallel()
golden, err := os.ReadFile("testdata/item_tables.golden") golden, err := os.ReadFile("testdata/item_tables.golden")
if err != nil { if err != nil {
t.Fatalf("read golden: %v", err) 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 package game
import "fmt" import "fmt"
@@ -272,6 +273,7 @@ func slowTarget(tp *Monster) {
func (g *RogueGame) zapBolt(obj *Object) bool { func (g *RogueGame) zapBolt(obj *Object) bool {
var name string var name string
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.WandKind() { switch obj.WandKind() {
case WandLightning: case WandLightning:
name = "bolt" 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 // boltStrikesMonster resolves a bolt arriving on a monster's square (the
// monster arm of the fire_bolt loop). It reports whether the bolt was // monster arm of the fire_bolt loop). It reports whether the bolt was
// used up. // 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) tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) { if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos bolt.Pos = pos
@@ -506,6 +510,7 @@ func (g *RogueGame) fixStick(cur *Object) {
cur.HurlDmg = dice("1x1") cur.HurlDmg = dice("1x1")
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch cur.WandKind() { switch cur.WandKind() {
case WandLight: case WandLight:
cur.Charges = g.rnd(10) + 10 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 package game
// tables.go — the static data tables the C game kept as file-scope globals // 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 //nolint:funlen,maintidx // a single composite literal holding every C data table
func newGameData() *gameData { func newGameData() *gameData {
return &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{ aClass: [NumArmorTypes]int{
8, // LEATHER 8, // LEATHER
@@ -205,7 +209,8 @@ func newGameData() *gameData {
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 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}}, {"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}}, {"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}}, {"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}}, {"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}}, {"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}, {Name: "plate mail", Prob: 5, Worth: 150},
}, },
//nolint:dupl // distinct C data tables that merely share their shape
basePotInfo: [NumPotionTypes]ObjInfo{ basePotInfo: [NumPotionTypes]ObjInfo{
{Name: "confusion", Prob: 7, Worth: 5}, {Name: "confusion", Prob: 7, Worth: 5},
{Name: "hallucination", Prob: 8, Worth: 5}, {Name: "hallucination", Prob: 8, Worth: 5},
@@ -265,6 +271,7 @@ func newGameData() *gameData {
{Name: "levitation", Prob: 6, Worth: 75}, {Name: "levitation", Prob: 6, Worth: 75},
}, },
//nolint:dupl // distinct C data tables that merely share their shape
baseRingInfo: [NumRingTypes]ObjInfo{ baseRingInfo: [NumRingTypes]ObjInfo{
{Name: "protection", Prob: 9, Worth: 400}, {Name: "protection", Prob: 9, Worth: 400},
{Name: "add strength", 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 {}, // DO NOT REMOVE: fake entry for dragon's breath
}, },
//nolint:dupl // distinct C data tables that merely share their shape
baseWsInfo: [NumWandTypes]ObjInfo{ baseWsInfo: [NumWandTypes]ObjInfo{
{Name: "light", Prob: 12, Worth: 250}, {Name: "light", Prob: 12, Worth: 250},
{Name: "invisibility", Prob: 6, Worth: 5}, {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 package game
import "testing" import "testing"
// badcheck from init.c: every probability table must sum to exactly 100. // badcheck from init.c: every probability table must sum to exactly 100.
func TestProbabilitiesSumTo100(t *testing.T) { func TestProbabilitiesSumTo100(t *testing.T) {
t.Parallel()
sum := func(info []ObjInfo) int { sum := func(info []ObjInfo) int {
s := 0 s := 0
for _, oi := range info { for _, oi := range info {
@@ -32,6 +35,8 @@ func TestProbabilitiesSumTo100(t *testing.T) {
} }
func TestInitProbsCumulative(t *testing.T) { func TestInitProbsCumulative(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 1}) g := New(Params{Seed: 1})
last := g.Items.Potions[NumPotionTypes-1].Prob last := g.Items.Potions[NumPotionTypes-1].Prob
@@ -47,6 +52,8 @@ func TestInitProbsCumulative(t *testing.T) {
} }
func TestNewGameRandomizesAppearances(t *testing.T) { func TestNewGameRandomizesAppearances(t *testing.T) {
t.Parallel()
g := New(Params{Seed: 12345}) g := New(Params{Seed: 12345})
checkPotionColors(t, g) checkPotionColors(t, g)
@@ -112,6 +119,8 @@ func checkWandMaterials(t *testing.T, g *RogueGame) {
} }
func TestMonsterTable(t *testing.T) { func TestMonsterTable(t *testing.T) {
t.Parallel()
data := newGameData() data := newGameData()
if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" { if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" {
t.Error("monster table order broken") 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 package game
// testTerm is a headless Terminal for tests: rendering is a no-op and // 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 package game
import ( import (
@@ -15,13 +16,15 @@ func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
which := obj.Which which := obj.Which
it := &g.Items it := &g.Items
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind { switch obj.Kind {
case KindPotion: case KindPotion:
g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr) g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr)
case KindRing: case KindRing:
g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum) g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
case KindWand: 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: case KindScroll:
g.nameScroll(&pb, obj) g.nameScroll(&pb, obj)
case KindFood: case KindFood:
@@ -250,6 +253,7 @@ func (g *RogueGame) dropRing(obj *Object) {
p.CurRing[hand] = nil p.CurRing[hand] = nil
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() { switch obj.RingKind() {
case RingAddStrength: case RingAddStrength:
g.changeStrength(-obj.Bonus) g.changeStrength(-obj.Bonus)
@@ -346,6 +350,7 @@ func (g *RogueGame) newRingThing(cur *Object) {
cur.Kind = KindRing cur.Kind = KindRing
cur.Which = pickOne(g, g.Items.Rings[:]) cur.Which = pickOne(g, g.Items.Rings[:])
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch cur.RingKind() { switch cur.RingKind() {
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage: case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
if cur.Bonus = g.rnd(3); cur.Bonus == 0 { if cur.Bonus = g.rnd(3); cur.Bonus == 0 {

View File

@@ -118,7 +118,7 @@ const (
Known // ISKNOW: player knows details about the object Known // ISKNOW: player knows details about the object
Missile // ISMISL: object is a missile type Missile // ISMISL: object is a missile type
Stackable // ISMANY: object comes in groups 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 Protected // ISPROT: armor is permanently protected
) )
@@ -140,18 +140,18 @@ type CreatureFlags int32
// Creature state bits (rogue.h). // Creature state bits (rogue.h).
const ( const (
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse 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 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 Levitating CreatureFlags = 0o000010 // ISLEVIT: hero is levitating
Found CreatureFlags = 0o000020 // ISFOUND: creature has been seen Found CreatureFlags = 0o000020 // ISFOUND: creature has been seen
Greedy CreatureFlags = 0o000040 // ISGREED: creature runs to protect gold Greedy CreatureFlags = 0o000040 // ISGREED: creature runs to protect gold
Hasted CreatureFlags = 0o000100 // ISHASTE: creature has been hastened 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 Held CreatureFlags = 0o000400 // ISHELD: creature has been held
Confused CreatureFlags = 0o001000 // ISHUH: creature is confused Confused CreatureFlags = 0o001000 // ISHUH: creature is confused
Invisible CreatureFlags = 0o002000 // ISINVIS: creature is invisible 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 Hallucinating CreatureFlags = 0o004000 // ISHALU: hero is on acid trip
Regenerates CreatureFlags = 0o010000 // ISREGEN: creature can regenerate Regenerates CreatureFlags = 0o010000 // ISREGEN: creature can regenerate
Awake CreatureFlags = 0o020000 // ISRUN: creature is running at the player 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. // 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). // distance returns the squared distance between two points (chase.c dist).
func distance(y1, x1, y2, x2 int) int { 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 package game
import "fmt" import "fmt"

View File

@@ -1,3 +1,4 @@
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
package game package game
// wizard.c — special wizard commands, some of which are also non-wizard // wizard.c — special wizard commands, some of which are also non-wizard
@@ -25,6 +26,7 @@ func (g *RogueGame) createObj() {
obj.Count = 1 obj.Count = 1
g.Msgs.Mpos = 0 g.Msgs.Mpos = 0
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind { switch obj.Kind {
case KindWeapon, KindArmor: case KindWeapon, KindArmor:
g.createWeaponArmor(obj) 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 // createRing sets up a wizard-created ring, prompting for a bonus on
// the bonus rings (the ring arm of wizard.c create_obj). // the bonus rings (the ring arm of wizard.c create_obj).
func (g *RogueGame) createRing(obj *Object) { func (g *RogueGame) createRing(obj *Object) {
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.RingKind() { switch obj.RingKind() {
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage: case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
g.msg("blessing? (+,-,n)") g.msg("blessing? (+,-,n)")
@@ -136,6 +139,7 @@ func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
return return
} }
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
switch obj.Kind { switch obj.Kind {
case KindScroll: case KindScroll:
setKnow(obj, g.Items.Scrolls[:]) 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 // motionKey translates the arrow and paging keys to Rogue's movement
// letters (tcell.go ReadChar). // letters (tcell.go ReadChar).
func motionKey(k tcell.Key) (byte, bool) { func motionKey(k tcell.Key) (byte, bool) {
//nolint:exhaustive // translation table: all other keys fall through
switch k { switch k {
case tcell.KeyUp: case tcell.KeyUp:
return 'k', true 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 // editingKey translates the editing and control keys to their C0 codes
// (tcell.go ReadChar). // (tcell.go ReadChar).
func editingKey(k tcell.Key) (byte, bool) { func editingKey(k tcell.Key) (byte, bool) {
//nolint:exhaustive // translation table: all other keys fall through
switch k { switch k {
case tcell.KeyEnter: case tcell.KeyEnter:
return '\n', true return '\n', true
case tcell.KeyEscape: case tcell.KeyEscape:
return game.Escape, true return game.Escape, true
case tcell.KeyBackspace, tcell.KeyBackspace2: case tcell.KeyBackspace, tcell.KeyBackspace2:
return 8, true return '\b', true
case tcell.KeyDelete: case tcell.KeyDelete:
return 0x7f, true return '\x7f', true
case tcell.KeyTab: case tcell.KeyTab:
return '\t', true return '\t', true
case tcell.KeyCtrlC: case tcell.KeyCtrlC:
return 3, true return '\x03', true
} }
return 0, false return 0, false
@@ -188,8 +190,8 @@ func (t *Tcell) ShellEscape() {
"[Entering shell; exit to return to the game]") "[Entering shell; exit to return to the game]")
// The shell session has no deadline by design; Background context. // The shell session has no deadline by design; Background context.
cmd := exec.CommandContext(context.Background(), //nolint:gosec // G204: the user's own $SHELL //nolint:gosec // G204: the user's own $SHELL
shell) cmd := exec.CommandContext(context.Background(), shell)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr