#!/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 # # 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 } game=${2:?usage: compare.sh } 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