Files
rgoue/game/fight_test.go
sneak e71a2ef3fd Rename constants to descriptive names (refactor step 1)
Pure rename, no behavior change; the full suite (RNG goldens,
generation invariants, scripted sessions, save round trip) is
unchanged and green.

- Creature flags: IsHuh→Confused, IsHalu→Hallucinating,
  CanHuh→CanConfuse, CanSee→CanSeeInvisible, IsRun→Awake,
  SeeMonst→SenseMonsters, IsCanc→Cancelled, IsLevit→Levitating,
  IsBlind/IsGreed/IsHaste/IsTarget/IsHeld/IsInvis/IsMean/IsRegen/
  IsFly/IsSlow → Blind/Greedy/Hasted/Targeted/Held/Invisible/Mean/
  Regenerates/Flying/Slowed, IsFound→Found
- Object flags: IsCursed→Cursed, IsKnow→Known, IsMissl→Missile,
  IsMany→Stackable, ObjIsFound→WasFound, IsProt→Protected
- Room flags: IsDark/IsGone/IsMaze → Dark/Gone/Maze; place flags:
  FPass→FPassage, FPNum→FPassNum, FTMask→FTrapMask
- Trap types: TDoor→TrapDoor ... TMyst→TrapMystery
- Item subtypes: P*→Potion* (PLSD→PotionLSD, PMFind→
  PotionDetectMonsters, ...), S*→Scroll* (SIDRorS→
  ScrollIdentifyRingOrStick, ...), R*→Ring* (RAddHit→RingDexterity,
  RNop→RingAdornment, ...), Ws*→Wand* (WsElect→WandLightning, ...),
  weapons (TwoSword→WeaponTwoHandedSword, Shiraken→WeaponShuriken,
  ...), armor (RingMail→ArmorRingMail, ...)
- Counts: Max{Potions,Scrolls,Rings,Sticks,Weapons,Armors} →
  Num{Potion,Scroll,Ring,Wand,Weapon,Armor}Types; NTraps→NumTrapTypes
- Level.NTraps field → TrapCount (also in the save snapshot)
- Original C constant names preserved as comment breadcrumbs in
  types.go so the lineage stays greppable

TODO.md: step 1 moved to Completed, step 2 (typed kinds) promoted to
Next Step.
2026-07-06 21:28:57 +02:00

119 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: "1x4/1x4/1x4"}}
def := &Creature{Stats: Stats{Arm: 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")
}
}