singe/testScripts/ports/compare.sh
2026-09-22 18:32:07 -05:00

208 lines
9 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 assets/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/assets/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/assets/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/assets/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}
if [[ -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)
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/assets/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/assets/Forge" "$run/Forge"
;;
*)
echo "compare.sh: no family called $family" >&2
exit 2
;;
esac
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")
( cd "$run" && timeout 3600 "$binary" -w -p -k --deterministic -d "$work" -v "$video" "$script" ) > "$work/run.log" 2>&1
grep -a '^TRACE' "$work/run.log" | grep -v 'state 2 from' > "$work/trace"
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"
for png in "$work"/*.png; do
[[ -f "$png" ]] && 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=""
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
# An American Laser Games title keeps its dips and board in a .cfg beside its script, which
# the original writes at every board: kept and put back the same way.
cfgFiles=""
if [[ "$family" == "alg" ]]; 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