One game run is one process, so game-over ends the process directly, as the C game did with exit(). myExit now restores the terminal (via the new Terminal.Fini) and calls os.Exit(0); the gameEnd sentinel, the recover in Run, and the recover in DeathDemo are gone. Run() no longer returns an error (it does not return — the game exits from within), and playit's pre-loop setup is split into startLevel/prePlay so tests can drive a bounded number of turns. Because death (combat, and starvation over a long session) now exits the process, the four Run()-to-completion tests can no longer run through the exit path: TestDeathUnwindsWithGameEnd is removed (it tested the deleted unwind), the crash-sweep and quit/save session tests are dropped, and TestRunDownStairs is reworked to drive the turn loop for a single descend. Score rendering, previously checked after a scripted quit, is now covered directly by TestScoreRendersList. Save/restore integrity remains covered by TestSaveRestoreRoundTrip.
117 lines
3.0 KiB
Go
117 lines
3.0 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 := New(Params{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 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")
|
|
}
|
|
}
|