Initial attempt at making rogue work with application overrides.
This commit is contained in:
1 parent
088f706cb4
commit
46dc520854
15 files changed
+1836
-111
No files matched your search
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//go:build linux && cgo
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides"
|
||||
"git.stormux.org/storm/fenrir/internal/applicationoverrides/rogue"
|
||||
"git.stormux.org/storm/fenrir/internal/command"
|
||||
"git.stormux.org/storm/fenrir/internal/reader"
|
||||
)
|
||||
|
||||
// Opt in with FENRIR_ROGUE_TEST_BINARY=/usr/bin/rogue. Each game has a private
|
||||
// PTY and working/save directory. SEED disables scoring in classic Rogue.
|
||||
// This exercises real ncurses output, without touching the active reader.
|
||||
func TestRogueLiveOverride(t *testing.T) {
|
||||
program := os.Getenv("FENRIR_ROGUE_TEST_BINARY")
|
||||
if program == "" {
|
||||
t.Skip("set FENRIR_ROGUE_TEST_BINARY to a classic Rogue executable")
|
||||
}
|
||||
for _, rows := range []int{24, 32} {
|
||||
for _, style := range []string{"Clear", "Overwrite", "Slow"} {
|
||||
t.Run(fmt.Sprintf("%d/%s", rows, style), func(t *testing.T) {
|
||||
cols := 80
|
||||
if rows > 24 {
|
||||
cols = 100
|
||||
}
|
||||
master, slave, err := open_pty(rows, cols)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer syscall.Close(master)
|
||||
slaveFile := os.NewFile(uintptr(slave), "private Rogue PTY")
|
||||
defer slaveFile.Close()
|
||||
child := exec.Command(program)
|
||||
child.Dir = t.TempDir()
|
||||
child.Env = append(os.Environ(), "HOME="+child.Dir, "TERM=xterm-256color", "LANG=C", "LC_ALL=C", "SEED=1234", "ROGUEOPTS=name=FenrirFixture,noterse,inven="+style)
|
||||
child.Stdin, child.Stdout, child.Stderr = slaveFile, slaveFile, slaveFile
|
||||
child.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0}
|
||||
if err := child.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = child.Process.Kill(); _ = child.Wait() }()
|
||||
if err := syscall.SetNonblock(master, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, err := new_emulator(rows, cols)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer e.close()
|
||||
a, r := &rogue.Adapter{}, reader.New()
|
||||
active := false
|
||||
var frame applicationoverrides.Frame
|
||||
var output []string
|
||||
pump := func() {
|
||||
var buffer [32768]byte
|
||||
for total := 0; total < 131072; {
|
||||
n, err := syscall.Read(master, buffer[:])
|
||||
if n > 0 {
|
||||
total += n
|
||||
e.feed(buffer[:n])
|
||||
}
|
||||
if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EIO) || n == 0 {
|
||||
break
|
||||
}
|
||||
if err != nil && !errors.Is(err, syscall.EINTR) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if active {
|
||||
s := e.snapshot(true, false)
|
||||
frame = a.Observe(s)
|
||||
for _, message := range r.Update_application(s, frame) {
|
||||
if message.Text != "" {
|
||||
output = append(output, message.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
screenText := func() string { return string(e.snapshot(true, false).Cells) }
|
||||
waitFor := func(label string, predicate func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
pump()
|
||||
if predicate() {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("%s not observed; screen=%q; speech=%q", label, screenText(), output)
|
||||
}
|
||||
send := func(text string) {
|
||||
t.Helper()
|
||||
r.Note_input(true, false, false)
|
||||
if _, err := syscall.Write(master, []byte(text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
waitFor("initial game", func() bool { return strings.Contains(screenText(), "Hp:") && strings.Contains(screenText(), "@") })
|
||||
active = true
|
||||
pump()
|
||||
if !frame.Recognized || !frame.OwnsPresentation {
|
||||
t.Fatal("real game not recognized", screenText())
|
||||
}
|
||||
for index, id := range []command.ID{command.ReadLiveRegion1, command.ReadLiveRegion2, command.ReadLiveRegion3, command.ReadLiveRegion4, command.ReadLiveRegion5, command.ReadLiveRegion6} {
|
||||
result, handled := a.Command(id)
|
||||
if !handled || !strings.HasPrefix(result.Text, frame.Regions[index].Name+": ") {
|
||||
t.Fatal("unnamed region", result)
|
||||
}
|
||||
}
|
||||
initialHealth, _ := a.Command(command.ReadLiveRegion1)
|
||||
send("i")
|
||||
if style == "Slow" {
|
||||
waitFor("slow inventory", func() bool { return strings.Contains(e.snapshot(true, false).Text(0), "a)") })
|
||||
// Five initial items; each intervening message waits for Space.
|
||||
for _, letter := range []string{"b)", "c)", "d)", "e)"} {
|
||||
send(" ")
|
||||
waitFor("next inventory item", func() bool { return strings.Contains(e.snapshot(true, false).Text(0), letter) })
|
||||
}
|
||||
} else {
|
||||
waitFor("inventory page", func() bool { return strings.Contains(screenText(), "--Press space to continue--") })
|
||||
if !frame.Recognized || frame.OwnsPresentation {
|
||||
t.Fatal("inventory interpreted as dungeon")
|
||||
}
|
||||
send(" ")
|
||||
waitFor("return from inventory", func() bool {
|
||||
return frame.OwnsPresentation && !strings.Contains(screenText(), "--Press space to continue--")
|
||||
})
|
||||
}
|
||||
send("?")
|
||||
waitFor("help prompt", func() bool { return strings.Contains(e.snapshot(true, false).Text(0), "Character you want help for") })
|
||||
send("*")
|
||||
waitFor("help page", func() bool { return strings.Contains(screenText(), "--Press space to continue--") })
|
||||
if !frame.Recognized || frame.OwnsPresentation {
|
||||
t.Fatal("help not readable")
|
||||
}
|
||||
send(" ")
|
||||
waitFor("return from help", func() bool { return frame.OwnsPresentation })
|
||||
send("o")
|
||||
waitFor("options", func() bool { return strings.Contains(screenText(), `Terse output ("terse"):`) })
|
||||
if !frame.Recognized || frame.OwnsPresentation {
|
||||
t.Fatal("options not readable")
|
||||
}
|
||||
send("\x1b")
|
||||
waitFor("options exit prompt", func() bool { return strings.Contains(screenText(), "--Press space to continue--") })
|
||||
send(" ")
|
||||
waitFor("return from options", func() bool { return frame.OwnsPresentation })
|
||||
start := len(output)
|
||||
send("v")
|
||||
waitFor("version announcement", func() bool { return strings.Contains(strings.Join(output[start:], " "), "Version 5.4") })
|
||||
t.Log(initialHealth.Text, strings.Join(output[start:], " "))
|
||||
// Move onto an empty adjacent floor cell. Keystrokes must not be
|
||||
// echoed as letters, and the moving @ must not read out a map row.
|
||||
s := e.snapshot(true, false)
|
||||
moved := false
|
||||
for _, step := range []struct {
|
||||
dx, dy int
|
||||
key string
|
||||
}{{1, 0, "l"}, {-1, 0, "h"}, {0, 1, "j"}, {0, -1, "k"}} {
|
||||
x, y := s.X+step.dx, s.Y+step.dy
|
||||
if x < 0 || x >= 80 || y < 1 || y > 22 || s.Line(y)[x] != '.' {
|
||||
continue
|
||||
}
|
||||
start = len(output)
|
||||
send(step.key)
|
||||
waitFor("player movement", func() bool { current := e.snapshot(true, false); return current.X == x && current.Y == y })
|
||||
for _, text := range output[start:] {
|
||||
if text == step.key || strings.Contains(text, "@") {
|
||||
t.Fatal("map or key chatter", text)
|
||||
}
|
||||
}
|
||||
moved = true
|
||||
break
|
||||
}
|
||||
if !moved {
|
||||
t.Fatal("fixture has no adjacent empty floor")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user