From 5b7e258195d6b0cbd65fb5b9afd091dc9b7f18c8 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 22 Jul 2026 22:11:10 +0700 Subject: [PATCH] Decompose test functions to clear complexity findings (step 7) TestSaveRestoreRoundTrip, TestNewGameRandomizesAppearances, and TestNewLevelInvariants split their assertion blocks into t.Helper() sub-checks. The lint run is now completely clean (0 issues). --- game/newlevel_test.go | 151 +++++++++++++++++++++++++----------------- game/save_test.go | 52 ++++++++++----- game/tables_test.go | 36 ++++++++-- 3 files changed, 157 insertions(+), 82 deletions(-) diff --git a/game/newlevel_test.go b/game/newlevel_test.go index 0d5122e..45f47e1 100644 --- a/game/newlevel_test.go +++ b/game/newlevel_test.go @@ -38,75 +38,106 @@ func TestNewLevelInvariants(t *testing.T) { for _, seed := range []int32{1, 12345, 2026, 99999} { g := genLevel(t, seed) - // The staircase is somewhere real. - st := g.Level.Stairs - if g.Level.Char(st.Y, st.X) != Stairs { - t.Errorf("seed %d: no staircase at recorded stairs position", seed) + checkHeroPlacement(t, g, seed) + checkRoomsDrawn(t, g, seed) + checkMonstersPlaced(t, g, seed) + checkObjectsPlaced(t, g, seed) + checkStartingKit(t, g, seed) + } +} + +// checkHeroPlacement verifies the staircase and hero landed on valid, +// unoccupied cells. +func checkHeroPlacement(t *testing.T, g *RogueGame, seed int32) { + t.Helper() + + // The staircase is somewhere real. + st := g.Level.Stairs + if g.Level.Char(st.Y, st.X) != Stairs { + t.Errorf("seed %d: no staircase at recorded stairs position", seed) + } + + // The hero stands on a walkable, monster-free cell. + hp := g.Player.Pos + if !stepOk(g.Level.Char(hp.Y, hp.X)) { + t.Errorf("seed %d: hero on unwalkable cell %q", seed, + g.Level.Char(hp.Y, hp.X)) + } + + if g.Level.MonsterAt(hp.Y, hp.X) != nil { + t.Errorf("seed %d: hero standing on a monster", seed) + } + + if g.Player.Room == nil { + t.Errorf("seed %d: hero not in any room", seed) + } +} + +// checkRoomsDrawn verifies rooms and floor/passages appear on the map. +func checkRoomsDrawn(t *testing.T, g *RogueGame, seed int32) { + t.Helper() + + m := renderMap(g) + if !strings.Contains(m, "|") || !strings.Contains(m, "-") { + t.Errorf("seed %d: no room walls drawn", seed) + } + + if !strings.Contains(m, ".") && !strings.Contains(m, "#") { + t.Errorf("seed %d: no floor or passages drawn", seed) + } +} + +// checkMonstersPlaced verifies every monster is indexed on the map and +// placed in a room. +func checkMonstersPlaced(t *testing.T, g *RogueGame, seed int32) { + t.Helper() + + for _, mon := range g.Level.Monsters { + if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon { + t.Errorf("seed %d: monster %c not indexed at its position", + seed, mon.Type) } - // The hero stands on a walkable, monster-free cell. - hp := g.Player.Pos - if !stepOk(g.Level.Char(hp.Y, hp.X)) { - t.Errorf("seed %d: hero on unwalkable cell %q", seed, - g.Level.Char(hp.Y, hp.X)) + if mon.Room == nil { + t.Errorf("seed %d: monster %c has no room", seed, mon.Type) } + } +} - if g.Level.MonsterAt(hp.Y, hp.X) != nil { - t.Errorf("seed %d: hero standing on a monster", seed) +// checkObjectsPlaced verifies every level object sits on a cell +// displaying its type (items can share cells only with monsters standing +// on them). +func checkObjectsPlaced(t *testing.T, g *RogueGame, seed int32) { + t.Helper() + + for _, obj := range g.Level.Objects { + ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) + if ch != obj.Kind.Glyph() && + g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil { + t.Errorf("seed %d: object %v at (%d,%d) but map shows %q", + seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch) } + } +} - if g.Player.Room == nil { - t.Errorf("seed %d: hero not in any room", seed) - } +// checkStartingKit verifies the player has her starting kit: food, armor, +// mace, bow, arrows. +func checkStartingKit(t *testing.T, g *RogueGame, seed int32) { + t.Helper() - // Some rooms exist and are drawn. - m := renderMap(g) - if !strings.Contains(m, "|") || !strings.Contains(m, "-") { - t.Errorf("seed %d: no room walls drawn", seed) - } + if len(g.Player.Pack) != 5 { + t.Errorf("seed %d: starting pack has %d items, want 5", + seed, len(g.Player.Pack)) + } - if !strings.Contains(m, ".") && !strings.Contains(m, "#") { - t.Errorf("seed %d: no floor or passages drawn", seed) - } + if g.Player.CurWeapon == nil || + g.Player.CurWeapon.WeaponKind() != WeaponMace { + t.Errorf("seed %d: not wielding the starting mace", seed) + } - // Every monster is indexed on the map and placed in a room. - for _, mon := range g.Level.Monsters { - if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon { - t.Errorf("seed %d: monster %c not indexed at its position", - seed, mon.Type) - } - - if mon.Room == nil { - t.Errorf("seed %d: monster %c has no room", seed, mon.Type) - } - } - - // Every level object sits on a cell displaying its type (items can - // share cells only with monsters standing on them). - for _, obj := range g.Level.Objects { - ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) - if ch != obj.Kind.Glyph() && - g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil { - t.Errorf("seed %d: object %v at (%d,%d) but map shows %q", - seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch) - } - } - - // The player has her starting kit: food, armor, mace, bow, arrows. - if len(g.Player.Pack) != 5 { - t.Errorf("seed %d: starting pack has %d items, want 5", - seed, len(g.Player.Pack)) - } - - if g.Player.CurWeapon == nil || - g.Player.CurWeapon.WeaponKind() != WeaponMace { - t.Errorf("seed %d: not wielding the starting mace", seed) - } - - if g.Player.CurArmor == nil || - g.Player.CurArmor.ArmorKind() != ArmorRingMail { - t.Errorf("seed %d: not wearing the starting ring mail", seed) - } + if g.Player.CurArmor == nil || + g.Player.CurArmor.ArmorKind() != ArmorRingMail { + t.Errorf("seed %d: not wearing the starting ring mail", seed) } } diff --git a/game/save_test.go b/game/save_test.go index 4bbe291..34bad67 100644 --- a/game/save_test.go +++ b/game/save_test.go @@ -39,6 +39,15 @@ func TestSaveRestoreRoundTrip(t *testing.T) { t.Error("save file not deleted on restore (C anti-restart rule)") } + checkRestoredState(t, g, h) + checkRestoredMonsters(t, g, h) + checkEquipmentAliasing(t, g, h) +} + +// checkRestoredState verifies the scalar state survived the round trip. +func checkRestoredState(t *testing.T, g, h *RogueGame) { + t.Helper() + if h.Player.Purse != 123 || h.Player.FoodLeft != 777 { t.Errorf("player state lost: purse=%d food=%d", h.Player.Purse, h.Player.FoodLeft) @@ -67,28 +76,41 @@ func TestSaveRestoreRoundTrip(t *testing.T) { if renderMap(h) != renderMap(g) { t.Error("restored level map differs") } +} + +// checkRestoredMonsters verifies the monster list and its pointer fixups +// survived the round trip. +func checkRestoredMonsters(t *testing.T, g, h *RogueGame) { + t.Helper() if len(h.Level.Monsters) != len(g.Level.Monsters) { t.Fatalf("monster count %d != %d", len(h.Level.Monsters), len(g.Level.Monsters)) } - if len(g.Level.Monsters) > 0 { - m := h.Level.Monsters[0] - if m.Dest != &h.Player.Pos { - t.Error("monster chase target not re-aliased to the hero") - } - - if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m { - t.Error("map monster index not rebuilt") - } - - if m.Room == nil { - t.Error("monster room pointer not rebuilt") - } + if len(g.Level.Monsters) == 0 { + return } - // Equipment aliasing: the wielded mace must be the same *Object as the - // one in the pack. + + m := h.Level.Monsters[0] + if m.Dest != &h.Player.Pos { + t.Error("monster chase target not re-aliased to the hero") + } + + if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m { + t.Error("map monster index not rebuilt") + } + + if m.Room == nil { + t.Error("monster room pointer not rebuilt") + } +} + +// checkEquipmentAliasing verifies the wielded mace is the same *Object as +// the one in the pack. +func checkEquipmentAliasing(t *testing.T, g, h *RogueGame) { + t.Helper() + st := g.snapshot() t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d", st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing, diff --git a/game/tables_test.go b/game/tables_test.go index 5226ac4..313bc3f 100644 --- a/game/tables_test.go +++ b/game/tables_test.go @@ -48,6 +48,22 @@ func TestInitProbsCumulative(t *testing.T) { func TestNewGameRandomizesAppearances(t *testing.T) { g := NewGame(Config{Seed: 12345}) + + checkPotionColors(t, g) + checkScrollNames(t, g) + checkWandMaterials(t, g) + + // Determinism: same seed, same appearances. + h := NewGame(Config{Seed: 12345}) + if h.Items != g.Items { + t.Error("two games with the same seed produced different item lore") + } +} + +// checkPotionColors verifies every potion has a distinct color. +func checkPotionColors(t *testing.T, g *RogueGame) { + t.Helper() + seen := map[string]bool{} for i, c := range g.Items.PotColors { @@ -61,6 +77,12 @@ func TestNewGameRandomizesAppearances(t *testing.T) { seen[c] = true } +} + +// checkScrollNames verifies every scroll has a name within the C buffer +// limit. +func checkScrollNames(t *testing.T, g *RogueGame) { + t.Helper() for i, n := range g.Items.ScrNames { if n == "" { @@ -71,9 +93,15 @@ func TestNewGameRandomizesAppearances(t *testing.T) { t.Errorf("scroll name %q longer than C buffer allows", n) } } +} + +// checkWandMaterials verifies every stick has a wand/staff type and a +// material. +func checkWandMaterials(t *testing.T, g *RogueGame) { + t.Helper() for i := range g.Items.WandType { - if g.Items.WandType[i] != "wand" && g.Items.WandType[i] != "staff" { + if g.Items.WandType[i] != wandName && g.Items.WandType[i] != staffName { t.Errorf("stick %d has type %q", i, g.Items.WandType[i]) } @@ -81,12 +109,6 @@ func TestNewGameRandomizesAppearances(t *testing.T) { t.Errorf("stick %d has no material", i) } } - - // Determinism: same seed, same appearances. - h := NewGame(Config{Seed: 12345}) - if h.Items != g.Items { - t.Error("two games with the same seed produced different item lore") - } } func TestMonsterTable(t *testing.T) {