package game import ( "path/filepath" "strings" "testing" ) // fortify makes the hero effectively immortal for a crash-sweep drive: // game-over now calls myExit and os.Exit(0) (step 8), which would kill the // test binary, so every death vector is neutralized. Re-applied each turn // because combat, digestion, freezing, and level drain chip away at these. func fortify(g *RogueGame) { p := &g.Player p.Stats.HP = 30000 // survive combat, arrow/dart traps, bolts p.Stats.MaxHP = 30000 // survive vampire max-hp drain p.Stats.Exp = 30000 // survive wraith level drain (death when exp hits 0) p.FoodLeft = 30000 // never starve g.NoCommand = 0 // never freeze to death (ice monster / sleep trap) g.NoMove = 0 // never stay stuck in a bear trap } // driveTurns runs the game's per-turn loop up to n times, doing the same // first-level and pre-play setup Run() does. Run() itself no longer // returns — game-over exits the process — so tests drive command() // directly, with short scripts that avoid quitting, saving, or playing // long enough to starve, any of which would exit the test binary. func driveTurns(t *testing.T, g *RogueGame, n int) { t.Helper() g.startLevel() g.prePlay() for range n { g.command() } } // TestRunDownStairs stands the hero on the staircase and descends via the // '>' command through the real turn loop, then checks the level changed. func TestRunDownStairs(t *testing.T) { // '>' is a free action (After=false), so it is followed by a paying // rest ('.') to end the command() call; without a paying action the // turn loop would spin forever on the auto-fed prompt input. tt := &testTerm{input: []byte(">.")} g := New(Params{Seed: 7, Term: tt}) g.NewLevel() g.Player.Pos = g.Level.Stairs // stand on the stairs g.restored = true // keep startLevel from regenerating g.Daemons = DaemonList{} // and give it a fresh daemon table g.StartDaemon(DRunners, 0, After) g.StartDaemon(DDoctor, 0, After) g.Fuse(DSwander, 0, wanderTime(g), After) g.StartDaemon(DStomach, 0, After) driveTurns(t, g, 1) if g.Depth != 2 { t.Errorf("depth = %d after descending, want 2", g.Depth) } } // TestScoreRendersList checks that the scoreboard is drawn on the screen // (in C it went to stdout after endwin; here it stays on the screen). The // quit and death paths that normally show it now exit the process, so the // display is exercised through score() directly. func TestScoreRendersList(t *testing.T) { g := New(Params{Seed: 1, Term: &testTerm{}}) g.Player.Purse = 100 g.score(g.Player.Purse, 1, 0) // flags 1 = quit; posts the top-ten list found := false for y := range NumLines { if strings.Contains(g.scr.Std.Line(y), "Top Ten") { found = true } } if !found { t.Error("score list not on screen") } } // TestDeepPlaythrough drives a fortified hero through the real command loop: // quaff/read/zap on the first level, then descend through the staircase to // depth 8, saving and restoring mid-way. It is a crash sweep of the turn // engine, deep level generation, item effects, and mid-game save/restore. // The hero is fortified so no death exits the process (step 8), and the fixed // seed keeps it deterministic. func TestDeepPlaythrough(t *testing.T) { g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}}) g.startLevel() g.prePlay() fortify(g) // Stock and use one of each consumable through the command dispatch. pot := give(g, &Object{Kind: KindPotion, Which: int(PotionHealing)}) scr := give(g, &Object{Kind: KindScroll, Which: int(ScrollMagicMapping)}) wand := newObject() wand.Kind = KindWand wand.Which = int(WandLight) wand.Charges = 5 zap := give(g, wand) setInput(t, g, 'q', pot) // quaff healing g.command() setInput(t, g, 'r', scr) // read magic mapping g.command() setInput(t, g, 'z', 'h', zap) // zap the light wand west g.command() fortify(g) // Each consumable identifies itself on use, confirming the q/r/z // commands actually ran through dispatch (not aborted on a bad prompt). if !g.Items.Potions[PotionHealing].Know { t.Error("quaff command did not identify the healing potion") } if !g.Items.Scrolls[ScrollMagicMapping].Know { t.Error("read command did not identify the magic-mapping scroll") } if !g.Items.Sticks[WandLight].Know { t.Error("zap command did not identify the light wand") } const wantDepth = 8 for g.Depth < wantDepth { g.Player.Pos = g.Level.Stairs // stand on the stairs setInput(t, g, '>', '.') // '>' descends (free), '.' pays the turn g.command() fortify(g) if g.Depth == 4 { g = saveAndRestore(t, g) fortify(g) } } if g.Depth != wantDepth { t.Errorf("depth = %d after descending, want %d", g.Depth, wantDepth) } if g.Player.Stats.HP <= 0 { t.Error("hero died during the playthrough") } } // TestTurnLoopCrashSweep mashes movement, search, and rest through the real // turn loop for many turns on several seeds, exercising combat, monster AI, // and traps. The hero is fortified each turn so nothing exits the process, // and the fixed seeds keep it deterministic; the point is to surface panics. func TestTurnLoopCrashSweep(t *testing.T) { // A generous mix of movement, search, and rest. The spaces between // commands double as answers to any --More-- prompt (wait_for eats // everything up to a space); without them one prompt would swallow the // rest of the script. The script is long enough that the bounded drive // never exhausts it (which would spin on the auto-fed prompt input). script := []byte(strings.Repeat("h j k l y u b n s . ", 400)) for _, seed := range []int32{1, 99, 2026, 31337} { g := New(Params{Seed: seed, Term: &testTerm{input: script}}) g.startLevel() g.prePlay() for range 200 { fortify(g) g.command() } if g.Player.Stats.HP <= 0 { t.Errorf("seed %d: hero died despite fortify", seed) } } } // saveAndRestore snapshots the game to a file, restores it, checks the key // state survived, and returns the restored game ready to keep playing. func saveAndRestore(t *testing.T, g *RogueGame) *RogueGame { t.Helper() path := filepath.Join(t.TempDir(), "deep.save") saveErr := g.saveFile(path) if saveErr != nil { t.Fatalf("saveFile: %v", saveErr) } h, err := Restore(path, Params{Wizard: true, Term: &testTerm{}}) if err != nil { t.Fatalf("Restore: %v", err) } if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse { t.Errorf("restored game diverged: depth %d/%d purse %d/%d", h.Depth, g.Depth, h.Player.Purse, g.Player.Purse) } return h }