208 lines
9 KiB
Bash
Executable file
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
|