Refactor step 5, combat: rollEm→rollAttacks; attack, moveMonster, and chaseStep return (removed bool) instead of the C -1/0 int codes. Behavior unchanged; suite green.
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.rollAttacks(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")
|
|
}
|
|
}
|