.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:
- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
the misspell autofix REVERTED where it rewrote authentic C game text
("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
closes explicitly and removes corrupt saves, scoreboard writes are
documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
ErrScreenTooSmall); panics allowed for unrecoverable states per
MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
per-line nolints for provably-bounded conversions (randomMonsterLetter
helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
(recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
(goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
inventory filter untangled into matchesFilter, main.go split so
defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods
Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
135 lines
3.3 KiB
Go
135 lines
3.3 KiB
Go
package game
|
|
|
|
import "testing"
|
|
|
|
// mkGame builds a headless game on a generated level, initializing the
|
|
// look() state the way playit() does before the first command.
|
|
func mkGame(t *testing.T, seed int32) *RogueGame {
|
|
t.Helper()
|
|
|
|
g := NewGame(Config{Seed: seed, Term: &testTerm{}})
|
|
g.NewLevel()
|
|
g.Oldpos = g.Player.Pos
|
|
g.Oldrp = g.roomin(g.Player.Pos)
|
|
|
|
return g
|
|
}
|
|
|
|
// spawnAdjacent puts a monster of the given type next to the hero.
|
|
func spawnAdjacent(g *RogueGame, typ byte) *Monster {
|
|
pos := Coord{X: g.Player.Pos.X + 1, Y: g.Player.Pos.Y}
|
|
tp := &Monster{}
|
|
g.newMonster(tp, typ, pos)
|
|
|
|
return tp
|
|
}
|
|
|
|
func TestRollEmParsesMultiAttackDice(t *testing.T) {
|
|
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}}
|
|
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
|
|
// 1x4 + str bonus 1 each must deal between 6 and 15 damage.
|
|
if !g.rollEm(att, def, nil, false) {
|
|
t.Fatal("attack with guaranteed swing missed")
|
|
}
|
|
|
|
dmg := 1000 - def.Stats.HP
|
|
if dmg < 6 || dmg > 15 {
|
|
t.Errorf("three 1x4+1 attacks dealt %d damage, want 6..15", dmg)
|
|
}
|
|
}
|
|
|
|
func TestFightKillsMonster(t *testing.T) {
|
|
g := mkGame(t, 7)
|
|
tp := spawnAdjacent(g, 'B') // bat: 1 hit die
|
|
tp.Stats.HP = 1
|
|
g.Player.Stats.Lvl = 20 // always hits
|
|
before := len(g.Level.Monsters)
|
|
g.fight(tp.Pos, g.Player.CurWeapon, false)
|
|
|
|
if len(g.Level.Monsters) != before-1 {
|
|
t.Error("monster not removed after fatal fight")
|
|
}
|
|
|
|
if g.Level.MonsterAt(tp.Pos.Y, tp.Pos.X) != nil {
|
|
t.Error("map still records dead monster")
|
|
}
|
|
|
|
if g.Player.Stats.Exp == 0 {
|
|
t.Error("no experience for the kill")
|
|
}
|
|
}
|
|
|
|
func TestAttackHurtsPlayer(t *testing.T) {
|
|
g := mkGame(t, 9)
|
|
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
|
|
tp.Stats.Lvl = 20 // always hits
|
|
tp.Flags.Clear(Cancelled)
|
|
|
|
hpBefore := g.Player.Stats.HP
|
|
g.Player.Stats.HP = 500
|
|
g.Player.Stats.MaxHP = 500
|
|
g.attack(tp)
|
|
|
|
if g.Player.Stats.HP >= 500 {
|
|
t.Errorf("player HP unchanged (%d -> %d)", hpBefore, g.Player.Stats.HP)
|
|
}
|
|
}
|
|
|
|
func TestDeathUnwindsWithGameEnd(t *testing.T) {
|
|
g := mkGame(t, 11)
|
|
|
|
defer func() {
|
|
r := recover()
|
|
if _, ok := r.(gameEnd); !ok {
|
|
t.Fatalf("death did not unwind with gameEnd, got %v", r)
|
|
}
|
|
|
|
if g.Playing {
|
|
t.Error("still playing after death")
|
|
}
|
|
}()
|
|
|
|
g.Options.Tombstone = false
|
|
g.death('K')
|
|
}
|
|
|
|
func TestRunnersChaseHero(t *testing.T) {
|
|
g := mkGame(t, 3)
|
|
// Place a hobgoblin a few squares away in the hero's room and set it
|
|
// running at the hero.
|
|
p := &g.Player
|
|
|
|
pos := Coord{X: p.Pos.X + 3, Y: p.Pos.Y}
|
|
if !stepOk(g.Level.Char(pos.Y, pos.X)) || g.Level.MonsterAt(pos.Y, pos.X) != nil {
|
|
t.Skip("no clear lane on this seed")
|
|
}
|
|
|
|
tp := &Monster{}
|
|
g.newMonster(tp, 'H', pos)
|
|
tp.Flags.Set(Awake)
|
|
tp.Dest = &p.Pos
|
|
d0 := distCp(tp.Pos, p.Pos)
|
|
|
|
g.runners(0)
|
|
|
|
if d1 := distCp(tp.Pos, p.Pos); d1 >= d0 {
|
|
t.Errorf("monster did not close distance: %d -> %d", d0, d1)
|
|
}
|
|
}
|
|
|
|
func TestKilledLeprechaunDropsGoldViaFall(t *testing.T) {
|
|
g := mkGame(t, 21)
|
|
tp := spawnAdjacent(g, 'L')
|
|
tp.Stats.HP = 0
|
|
objsBefore := len(g.Level.Objects)
|
|
g.killed(tp, false)
|
|
// At max depth the leprechaun's hoard falls to the floor.
|
|
if len(g.Level.Objects) <= objsBefore-1 {
|
|
t.Error("level object list shrank unexpectedly")
|
|
}
|
|
}
|