go: port combat, chase driver, traps, zapping, death and scores
- fight.c in full: fight/attack with all eight special monster attacks (aquator rust, ice freeze, rattlesnake poison, wraith/vampire drains, flytrap hold, leprechaun/nymph theft), swing, roll_em dice parsing (with a C-semantics atoi — Go's strconv rejects '1x4/...'), hit/miss message variants, killed, remove_mon - chase.c completed: runners, move_monst, relocate, do_chase with dragon breath, chase target selection (C's dead oroom comparison in relocate is preserved as-is) - move.c in full: do_move with passgo turning, all eight traps, rndmove, rust_armor - sticks.c completed: do_zap (all 14 sticks), drain, fire_bolt with wall bounces; weapons.c completed: missile/do_motion/fall/wield - rip.c: death tombstone, total_winner, killname; C exit() becomes a gameEnd panic that Run will recover - score.go: top-ten scoreboard as a gob file with lock-file protocol replacing the XOR-encrypted C format - wizard.c: whatis/set_know/teleport; daemons.c stomach - monster bestiary moved to per-game state (C mutates the flytrap damage string during play) Tests: combat math, kill/removal bookkeeping, monster chase pursuit, death unwinding, scripted-input headless terminal.
This commit is contained in:
118
game/fight_test.go
Normal file
118
game/fight_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
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(IsRun)
|
||||
// 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(IsCanc)
|
||||
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(IsRun)
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user