forge/testScripts/ports/compare.sh
2026-10-06 16:57:52 -05:00

234 lines
11 KiB
Bash
Executable file

#!/bin/bash
# Runs an original game from the test library and its Forge port on the same inputs, headless
# and on the deterministic clock, and says whether their traces agree (FORGE.md section 14.1).
#
# testScripts/ports/compare.sh <family> <game>
#
# SINGE the engine binary (default: the newest build in .builddir)
# SINGE_LIBRARY the test library (default: ~/claude/singetest/ported, where the
# ported originals live; the hypseus families look under its hypseus/)
# SINGE_RUN a directory to run in, with Singe/ and Forge/ beside each other
# (default: a fresh one under /tmp, with forge/ copied in)
#
# Families: actionMax (games 38AmbushAlley, BlueThunder, Hydrosub2021, PopsGhostly, SonicFury);
# karis (a game as directory/script, e.g. TimeGal/Timegal.singe);
# hypseus (a Karis-framework title from the Hypseus library by its folder, e.g.
# Astroboy: the script is the description's namesake under singe/, the video its
# framefile);
# rdg (a map-mode title from the Hypseus library, laid out the same way; the engine's
# deterministic clock gives both runs the same seed for the game's random draws);
# alg (an American Laser Games title, Hypseus layout or the library's);
# videoland (Adventures in Videoland: Rollercoaster, by its folder);
# timetraveler (Hologram Time Traveler, by its folder).
set -u
family=${1:?usage: compare.sh <family> <game>}
game=${2:?usage: compare.sh <family> <game>}
repo=$(cd "$(dirname "$0")/../.." && pwd)
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported}
binary=${SINGE:-$(ls -t "$repo"/.builddir/linux-gcc/*/singe/Singe-v* "$repo"/.builddir/linux/x86_64/singe/Singe-v* 2>/dev/null | head -1)}
run=${SINGE_RUN:-}
if [[ -z "$run" ]]; then
run=$(mktemp -d /tmp/forgePort.XXXXXX)
fi
# The run directory gets Forge fresh from the repository every time, so an edit to Author.singe
# is what the port plays.
mkdir -p "$run"
rm -rf "$run/Forge"
cp -r "$repo/forge" "$run/Forge"
if [[ -z "$binary" || ! -x "$binary" ]]; then
echo "compare.sh: no engine binary; set SINGE" >&2
exit 2
fi
case "$family" in
actionMax)
video="$library/ActionMax/frame_$game.txt"
original="$repo/testScripts/ports/actionMaxOriginal.singe"
port="$repo/testScripts/ports/actionMaxPort.singe"
;;
karis)
# The game's directory and its main script, as "TimeGal/Timegal.singe"; the video is
# what its games.dat names.
export PORT_SCRIPT=${game#*/}
game=${game%%/*}
video=$library/$(grep -o 'VIDEO *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
original="$repo/testScripts/ports/karisOriginal.singe"
port="$repo/testScripts/ports/karisPort.singe"
;;
hypseus)
# A Hypseus title's script hardcodes BASEDIR = "singe", so the run directory gets the
# title's singe/ beside it, and its own copy of Forge.
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported/hypseus}
# The script is the description's namesake, wherever under singe/ the title keeps it
# (Sintel's folder is not named as its title is).
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
if [[ -n "$desc" ]]; then
export PORT_SCRIPT=${desc#"$library/$game/"}
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
else
export PORT_SCRIPT=singe/$game/$game.singe
fi
# The framefile is at the top of the title, or beside the script.
video=$library/$game/$game.txt
[[ -f "$video" ]] || video=$library/$game/${PORT_SCRIPT%.singe}.txt
original="$repo/testScripts/ports/karisOriginal.singe"
port="$repo/testScripts/ports/karisPort.singe"
run=$run/$game
mkdir -p "$run"
rm -rf "$run/Forge"
cp -r "$repo/forge" "$run/Forge"
ln -sfn "$library/$game/singe" "$run/singe"
;;
rdg)
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported/hypseus}
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
export PORT_SCRIPT=${desc#"$library/$game/"}
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
video=$library/$game/$game.txt
[[ -f "$video" ]] || video=$library/$game/${PORT_SCRIPT%.singe}.txt
original="$repo/testScripts/ports/karisOriginal.singe"
port="$repo/testScripts/ports/karisPort.singe"
run=$run/$game
mkdir -p "$run"
rm -rf "$run/Forge"
cp -r "$repo/forge" "$run/Forge"
ln -sfn "$library/$game/singe" "$run/singe"
;;
alg)
# An American Laser Games title from the Hypseus library: the same layout as the
# map-mode ones, its own drivers.
library=${SINGE_LIBRARY:-$HOME/claude/singetest/hypseus}
# The Hypseus layout wins when singe/ holds the description, even beside a Script/ of the
# game's own files (the one-player Johnny Rock HD edition keeps both).
hypseusDesc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
if [[ -z "$hypseusDesc" && -d "$library/$game/Script" ]]; then
# The library's own copy (Script/, Cfg/, Video/ with the framefile beside the video;
# Platoon keeps its Video/ under Script/): the script is the description's namesake,
# the video the framefile in Video/.
desc=$(ls "$library/$game/Script"/*.forge 2>/dev/null | head -1)
export PORT_SCRIPT=Script/$(basename "${desc%.forge}").singe
video=$(ls "$library/$game/Video"/*.txt "$library/$game/Script/Video"/*.txt 2>/dev/null | head -1)
# No framefile: the video itself, when the title keeps one there.
[[ -n "$video" ]] || video=$(ls "$library/$game/Video"/*.m2v "$library/$game/Video"/*.mp4 "$library/$game/Video"/*.mkv "$library/$game/Script/Video"/*.m2v 2>/dev/null | head -1)
else
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
if [[ -n "$desc" ]]; then
export PORT_SCRIPT=${desc#"$library/$game/"}
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
else
export PORT_SCRIPT=singe/$game/$game.singe
fi
video=$library/$game/$game.txt
fi
original="$repo/testScripts/ports/algOriginal.singe"
port="$repo/testScripts/ports/algPort.singe"
run=$run/$game
mkdir -p "$run"
rm -rf "$run/Forge"
cp -r "$repo/forge" "$run/Forge"
ln -sfn "$library/$game/singe" "$run/singe"
;;
videoland|timetraveler)
# A one-off of the library (Adventures in Videoland: Rollercoaster; Hologram Time
# Traveler): its games.dat names the video and the script; the description is beside
# the script; the drivers are the family's own.
library=${SINGE_LIBRARY:-$HOME/claude/singetest}
export PORT_SCRIPT=$(grep -o 'SCRIPT *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
video=$library/$(grep -o 'VIDEO *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
original="$repo/testScripts/ports/${family}Original.singe"
port="$repo/testScripts/ports/${family}Port.singe"
run=$run/$game
mkdir -p "$run"
rm -rf "$run/Forge"
cp -r "$repo/forge" "$run/Forge"
;;
*)
echo "compare.sh: no family called $family" >&2
exit 2
;;
esac
if [[ -z "${video:-}" ]]; then
echo "compare.sh: no video for $family $game in the library; nothing to compare" >&2
exit 2
fi
export SINGE_LIBRARY=$library PORT_GAME=$game SDL_VIDEODRIVER=${SDL_VIDEODRIVER:-offscreen} SDL_AUDIO_DRIVER=dummy
trace() {
local name=$1 script=$2 work
work=$(mktemp -d "$run/$name.XXXXXX")
local pass passes=1
# PORT_TWICE=1 runs each side twice in the same data directory (the original's own files are
# put back only after both), so the second run shows what the first one saved: the
# Persistence row of FORGE.md 14.18. The trace is both runs', the drive told which it is.
[[ "${PORT_TWICE:-}" == "1" ]] && passes=2
: > "$work/trace"
for (( pass = 1; pass <= passes; pass++ )); do
# An hour a run; a run spinning in Lua never sees the quit SIGTERM asks for, so -k kills it.
( cd "$run" && PORT_PASS=$pass timeout -k 30 3600 "$binary" -w -p -k --deterministic -d "$work" -v "$video" "$script" ) > "$work/run$pass.log" 2>&1
(( pass > 1 )) && echo "TRACE run $pass" >> "$work/trace"
grep -a '^TRACE' "$work/run$pass.log" | grep -v 'state 2 from' >> "$work/trace"
done
cp "$work/run1.log" "$work/run.log"
grep -a 'rror\|Author:' "$work/run.log" | head -3 >&2
# The screenshots a drive took (PORT_SHOTS=1) land in the run's data directory; with
# SINGE_SHOTS set they are gathered there, named for the run.
if [[ -n "${SINGE_SHOTS:-}" ]]; then
mkdir -p "$SINGE_SHOTS"
# The engine saves under the game's own folder in the data directory, wherever that is.
find "$work" -name "*.png" -print0 | while IFS= read -r -d '' png; do
cp "$png" "$SINGE_SHOTS/$name-$(basename "$png")"
done
fi
echo "$work/trace"
}
# The originals autosave into their own Cfg folder as they play, which would give the port's
# snapshot and the next run different saves to read: the folder is put back as it was.
cfgDir=""
# One comparison of a title at a time: two would each keep the original's files, then put them
# back over each other. A second waits for the first.
exec 9> "${TMPDIR:-/tmp}/compare-$family-${game//\//_}.lock"
flock 9
if [[ -n "${PORT_SCRIPT:-}" ]]; then
cfgDir=$library/$game/$(dirname "$PORT_SCRIPT")/Cfg
[[ -d "$cfgDir" ]] || cfgDir=$library/$game/Cfg
[[ -d "$cfgDir" ]] || cfgDir=""
fi
if [[ -n "$cfgDir" ]]; then
cfgKeep=$(mktemp -d "$run/cfg.XXXXXX")
cp -r "$cfgDir/." "$cfgKeep/"
fi
# A title that keeps its dips and board in a .cfg beside its script (the gun games, the map-mode
# games) has the original write it at every board: kept and put back the same way, whatever the
# family, or a run leaves its scores in the library for the next description to snapshot.
cfgFiles=""
if [[ -n "${PORT_SCRIPT:-}" ]]; then
cfgFiles=$(ls "$library/$game/$(dirname "$PORT_SCRIPT")"/*.cfg 2>/dev/null)
if [[ -n "$cfgFiles" ]]; then
cfgKeepFiles=$(mktemp -d "$run/cfgfiles.XXXXXX")
cp $cfgFiles "$cfgKeepFiles/"
fi
fi
# The originals' files go back however the comparison ends, a kill included: a run cut short
# would otherwise leave the game's own writes (its board, its pool index) for the next run and
# the description to disagree on.
restoreConfig() {
if [[ -n "$cfgDir" ]]; then
rm -rf "$cfgDir"
cp -r "$cfgKeep" "$cfgDir"
fi
if [[ -n "$cfgFiles" ]]; then
cp "$cfgKeepFiles"/*.cfg "$library/$game/$(dirname "$PORT_SCRIPT")/"
fi
}
trap restoreConfig EXIT
a=$(trace original "$original")
b=$(trace port "$port")
if [[ ! -s "$a" || ! -s "$b" ]]; then
echo "$family $game: a run produced no trace; see $(dirname "$a")/run.log and $(dirname "$b")/run.log" >&2
exit 2
fi
if diff -q "$a" "$b" > /dev/null; then
echo "$family $game: the port plays as the original does ($(wc -l < "$a") trace lines; $(tail -1 "$a"))"
exit 0
fi
echo "$family $game: the traces differ"
diff "$a" "$b" | head -20
exit 1