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") } }