magicciv/tools/autoplay-batch.sh
Natalie 8ed21325c7 feat(autoplay): add weston rendering support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-16 17:51:24 -07:00

236 lines
8.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# autoplay-batch.sh — Run auto_play N times with different seeds and collect per-game output dirs.
#
# Usage: tools/autoplay-batch.sh [--weston] [count=3] [turn_limit=500] [results_dir]
#
# Output layout:
# <results_dir>/game_<stamp>_seed<N>/
# meta.json
# turn_stats.jsonl
# events.jsonl
# game.log
# weston.log (weston mode only)
# *.save (per-turn saves, if configured)
#
# Environment:
# AUTOPLAY_HOST — If set (e.g. "lilith@apricot.local"), run each game via SSH
# using run_ap3.sh on the remote host and scp results back.
# If unset, run locally via flatpak (Linux only).
# RENDER_MODE — "headless" (default) or "weston". --weston flag sets this.
# headless: Godot --headless, no display, no screenshots.
# weston: weston headless backend, software rendering, screenshots work.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
GAME_DIR="$PROJECT_DIR/src/game"
REPO_ROOT="$PROJECT_DIR"
RENDER_MODE="${RENDER_MODE:-headless}"
# Parse --weston flag before positional args
POSITIONAL=()
for arg in "$@"; do
case "$arg" in
--weston) RENDER_MODE="weston" ;;
--headless) RENDER_MODE="headless" ;;
*) POSITIONAL+=("$arg") ;;
esac
done
set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
COUNT="${1:-3}"
TURN_LIMIT="${2:-500}"
RESULTS_DIR="${3:-$REPO_ROOT/.local/batches/autoplay_batch}"
if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then
echo "ERROR: count must be a positive integer (got '$COUNT')" >&2
exit 2
fi
if ! [[ "$TURN_LIMIT" =~ ^[0-9]+$ ]] || [ "$TURN_LIMIT" -lt 1 ]; then
echo "ERROR: turn_limit must be a positive integer (got '$TURN_LIMIT')" >&2
exit 2
fi
AUTOPLAY_HOST="${AUTOPLAY_HOST:-}"
SAFETY_TIMEOUT=$(( TURN_LIMIT * 2 + 300 ))
# Flatpak sandbox can't write to /tmp. Reject /tmp paths outright instead of
# silently redirecting — persistent output belongs under the repo.
if [[ "$RESULTS_DIR" == /tmp/* ]] || [[ "$RESULTS_DIR" == /private/tmp/* ]]; then
echo "ERROR: results_dir under /tmp is forbidden (wiped on reboot, flatpak sandbox hostile)." >&2
echo " Use a path under the repo (default: <repo>/.local/batches/) or \$HOME/tmp." >&2
exit 2
fi
mkdir -p "$RESULTS_DIR"
STAMP="$(date +%Y%m%d_%H%M%S)"
echo "============================================================"
echo "Autoplay Batch: $COUNT games, turn_limit=$TURN_LIMIT"
echo "Results: $RESULTS_DIR"
echo "Stamp: $STAMP"
if [ -n "$AUTOPLAY_HOST" ]; then
echo "Mode: remote SSH ($AUTOPLAY_HOST)"
else
echo "Mode: local flatpak"
fi
echo "Render: $RENDER_MODE"
echo "Safety timeout: ${SAFETY_TIMEOUT}s per game"
echo "============================================================"
_kill_stale_procs() {
pkill -f "weston.*godot-headless" 2>/dev/null || true
pkill -f "org.godotengine.Godot" 2>/dev/null || true
sleep 0.5
}
_run_local() {
local seed="$1"
local game_dir="$2"
if ! command -v flatpak >/dev/null 2>&1; then
echo "ERROR: flatpak not installed. Set AUTOPLAY_HOST to run on a remote Linux host." >&2
exit 1
fi
_kill_stale_procs
local WESTON_PID=""
local FLATPAK_ENVS=(
"--env=AUTO_PLAY=true"
"--env=AUTO_PLAY_SEED=$seed"
"--env=AUTO_PLAY_TURN_LIMIT=$TURN_LIMIT"
"--env=AUTO_PLAY_DIR=$game_dir"
)
local GODOT_ARGS=("--path" "$GAME_DIR" "--rendering-method" "gl_compatibility")
if [ "$RENDER_MODE" = "weston" ]; then
if ! command -v weston >/dev/null 2>&1; then
echo "ERROR: --weston mode but weston not installed" >&2
exit 1
fi
WESTON_SOCKET="godot-headless-$$"
echo "[seed $seed] Starting weston (headless)..."
weston --backend=headless --socket="$WESTON_SOCKET" --width=1920 --height=1080 \
>"$game_dir/weston.log" 2>&1 &
WESTON_PID=$!
sleep 1
FLATPAK_ENVS+=(
"--socket=wayland"
"--env=WAYLAND_DISPLAY=$WESTON_SOCKET"
"--filesystem=xdg-run/${WESTON_SOCKET}"
)
else
GODOT_ARGS+=("--headless")
fi
echo "[seed $seed] Launching Godot ($RENDER_MODE, timeout ${SAFETY_TIMEOUT}s)..."
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
timeout "$SAFETY_TIMEOUT" flatpak run --user \
--filesystem=home \
"${FLATPAK_ENVS[@]}" \
org.godotengine.Godot "${GODOT_ARGS[@]}" \
>"$game_dir/game.log" 2>&1 || {
local exit_code=$?
echo "[seed $seed] Godot exited with code $exit_code" >&2
}
if [ -n "$WESTON_PID" ]; then
kill "$WESTON_PID" 2>/dev/null || true
wait "$WESTON_PID" 2>/dev/null || true
fi
}
_run_remote() {
local seed="$1"
local game_dir="$2"
echo "[seed $seed] Running via SSH on $AUTOPLAY_HOST..."
# Resolve remote $HOME once so we don't fight quoting rules
if [ -z "${REMOTE_HOME:-}" ]; then
REMOTE_HOME="$(ssh "$AUTOPLAY_HOST" 'echo "$HOME"')"
fi
local remote_game_dir="$REMOTE_HOME/Code/@projects/@magic-civilization/.local/batches/autoplay_batch/game_${STAMP}_seed${seed}"
local remote_runner="$REMOTE_HOME/bin/run_ap3.sh"
ssh "$AUTOPLAY_HOST" "
set -euo pipefail
mkdir -p '$remote_game_dir'
if [ ! -f '$remote_runner' ]; then
echo 'ERROR: $remote_runner not found on $AUTOPLAY_HOST (expected persistent runner in \$HOME/bin)' >&2
exit 1
fi
AUTO_PLAY=true \
AUTO_PLAY_SEED='$seed' \
AUTO_PLAY_TURN_LIMIT='$TURN_LIMIT' \
AUTO_PLAY_DIR='$remote_game_dir' \
RENDER_MODE='$RENDER_MODE' \
bash '$remote_runner' >'$remote_game_dir/game.log' 2>&1
" || {
echo "[seed $seed] SSH run exited with error — see $game_dir/game.log after scp" >&2
}
echo "[seed $seed] Fetching results from $AUTOPLAY_HOST..."
scp -r "$AUTOPLAY_HOST:$remote_game_dir/." "$game_dir/" \
>/dev/null 2>&1 || {
echo "WARNING: scp failed for seed $seed — result may be missing" >&2
}
}
# ── Main loop ────────────────────────────────────────────────────────────────
FAILED_SEEDS=()
for seed in $(seq 1 "$COUNT"); do
game_dir="$RESULTS_DIR/game_${STAMP}_seed${seed}"
mkdir -p "$game_dir"
echo ""
echo "[$(date +%H:%M:%S)] === Game $seed/$COUNT (seed=$seed) ==="
echo "[seed $seed] Output dir: $game_dir"
if [ -n "$AUTOPLAY_HOST" ]; then
_run_remote "$seed" "$game_dir"
else
_run_local "$seed" "$game_dir"
fi
# Check for meta.json + non-empty turn_stats.jsonl as canonical success indicators
meta_ok=false
stats_ok=false
[ -f "$game_dir/meta.json" ] && meta_ok=true
[ -f "$game_dir/turn_stats.jsonl" ] && [ -s "$game_dir/turn_stats.jsonl" ] && stats_ok=true
if $meta_ok && $stats_ok; then
line_count="$(wc -l < "$game_dir/turn_stats.jsonl" | tr -d ' ')"
echo "[seed $seed] OK — meta.json present, turn_stats.jsonl has $line_count line(s)"
else
if ! $meta_ok; then
echo "[seed $seed] MISSING meta.json" >&2
fi
if ! $stats_ok; then
echo "[seed $seed] MISSING or empty turn_stats.jsonl (game may have crashed)" >&2
fi
FAILED_SEEDS+=("$seed")
fi
done
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "============================================================"
PRODUCED=$(( COUNT - ${#FAILED_SEEDS[@]} ))
echo "Batch complete: $PRODUCED/$COUNT games produced turn_stats.jsonl"
echo "Results: $RESULTS_DIR"
echo "============================================================"
if [ ${#FAILED_SEEDS[@]} -gt 0 ]; then
echo "ERROR: No turn_stats.jsonl for seeds: ${FAILED_SEEDS[*]}" >&2
echo " Check game.log in each game dir for details." >&2
exit 1
fi
exit 0