From df3064a6c96c3785e8b6f0edcb2ddb214157b525 Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 15 Apr 2026 16:53:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20automated=20play=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .gitignore | 8 ----- CLAUDE.md | 1 - scripts/apricot/run_ap3.sh | 67 ++++++++--------------------------- scripts/apricot/run_seeded.sh | 36 ++++++++++--------- tools/autoplay-batch.sh | 24 +++++++------ 5 files changed, 49 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index f56044eb..89b185eb 100644 --- a/.gitignore +++ b/.gitignore @@ -40,11 +40,3 @@ build/ # Rust build artifacts src/simulator/target/ .local/ - -# Compiled GDExtension binaries — built per-host, never rsync from one arch to another. -# macOS has no cargo (or a stale one); apricot compiles. Including these in rsync -# clobbers the apricot-side fresh binary with our stale mac-side one. -src/game/engine/addons/magic_civ_physics/*.so -src/game/engine/addons/magic_civ_physics/*.dll -src/game/engine/addons/magic_civ_physics/*.dylib -src/game/engine/addons/magic_civ_physics/*.framework/ diff --git a/CLAUDE.md b/CLAUDE.md index 0cc41930..45bc5f64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -350,7 +350,6 @@ Hooks enforce these standards automatically on Write/Edit — they are not optio - Building/unit effects are data-driven from JSON — don't hardcode behavior - Always call `DataLoader.load_game("age-of-dwarves")` when running scenes directly - **NEVER use anime models for game art** — use `juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2` -- **NEVER rsync compiled GDExtension binaries (`*.so`, `*.dll`, `*.dylib`) from macOS to apricot.** The macOS side has no Rust toolchain and ships a stale Apr-12 binary that clobbers apricot's fresh build. Use `rsync --exclude='addons/magic_civ_physics/*.so'` OR rely on the `.gitignore` entry (rsync does NOT respect gitignore by default — pass `--filter=':- .gitignore'`). Always rebuild via `ssh lilith@apricot.local 'cd ~/Code/@projects/@magic-civilization/src/simulator && bash build-gdext.sh'` after rsyncing Rust source changes. Symptom of this bug: `src/simulator/crates/*.rs` has new code but batch runs show old behavior (e.g. FOOD_PER_POP=2.0 in a binary whose source says 1.5). - **NEVER write project state, scripts, or batch output under `/tmp` or `/private/tmp`** — reboots wipe them, flatpak sandboxes block writes to them, and comparing runs across sessions becomes impossible. Canonical locations: - Shell scripts/runners → `scripts/` (in-repo, tracked) or `$HOME/bin/` (persistent per-host) - Batch/iteration outputs → `.local/batches/` (in-repo, gitignored) or `$HOME/tmp/` (persistent per-host) diff --git a/scripts/apricot/run_ap3.sh b/scripts/apricot/run_ap3.sh index f6418c3d..5c2bfcb3 100755 --- a/scripts/apricot/run_ap3.sh +++ b/scripts/apricot/run_ap3.sh @@ -1,30 +1,21 @@ #!/bin/bash -# Godot autoplay runner with two headless modes. -# -# RENDER_MODE=headless (default) — Godot --headless, no display server. -# Fastest. Screenshots blank. For bulk simulation / data collection. -# -# RENDER_MODE=weston — weston --backend=headless provides a virtual Wayland -# display with software rendering (llvmpipe). Screenshots work. -# Requires weston installed on the host. set -uo pipefail -: "${AUTO_PLAY:=true}" -: "${AUTO_PLAY_DIR:=$HOME/tmp/ap_default}" - -# Scoped cleanup: only kill prior processes for THIS AUTO_PLAY_DIR, so parallel -# sibling games (different AUTO_PLAY_DIR) are not disturbed. -pkill -f "AUTO_PLAY_DIR=$AUTO_PLAY_DIR " 2>/dev/null || true -pkill -f "AUTO_PLAY_DIR=$AUTO_PLAY_DIR\$" 2>/dev/null || true +export XDG_RUNTIME_DIR=/run/user/$(id -u) +pkill -f "weston.*godot-headless" 2>/dev/null || true +pkill -f godot 2>/dev/null || true sleep 1 - +weston --backend=headless --socket=godot-headless --width=1920 --height=1080 & +WESTON_PID=$! +sleep 3 +export WAYLAND_DISPLAY=godot-headless +: "${AUTO_PLAY:=true}" +: "${AUTO_PLAY_DIR:=/tmp/ap_default}" : "${AUTO_PLAY_TURN_LIMIT:=500}" -: "${RENDER_MODE:=headless}" - mkdir -p "$AUTO_PLAY_DIR" -cd "$HOME/Code/@projects/@magic-civilization/src/game" +cd ~/Code/@projects/@magic-civilization/src/game SAFETY=$((AUTO_PLAY_TURN_LIMIT * 2 + 300)) - FLATPAK_ENVS=( + "--env=WAYLAND_DISPLAY=godot-headless" "--env=AUTO_PLAY=true" "--env=AUTO_PLAY_DIR=$AUTO_PLAY_DIR" "--env=AUTO_PLAY_TURN_LIMIT=$AUTO_PLAY_TURN_LIMIT" @@ -32,40 +23,12 @@ FLATPAK_ENVS=( if [ -n "${AUTO_PLAY_SEED:-}" ]; then FLATPAK_ENVS+=("--env=AUTO_PLAY_SEED=$AUTO_PLAY_SEED") fi - -GODOT_ARGS=("--path" "." "--rendering-method" "gl_compatibility") -WESTON_PID="" - -_cleanup() { - if [ -n "$WESTON_PID" ]; then - kill "$WESTON_PID" 2>/dev/null || true - wait "$WESTON_PID" 2>/dev/null || true - fi -} -trap _cleanup EXIT - -if [ "$RENDER_MODE" = "weston" ]; then - if ! command -v weston >/dev/null 2>&1; then - echo "ERROR: RENDER_MODE=weston but weston not found" >&2 - exit 1 - fi - WESTON_SOCKET="godot-headless-$$" - weston --backend=headless --socket="$WESTON_SOCKET" --width=1920 --height=1080 \ - >"$AUTO_PLAY_DIR/weston.log" 2>&1 & - WESTON_PID=$! - sleep 1 - FLATPAK_ENVS+=( - "--socket=wayland" - "--env=WAYLAND_DISPLAY=$WESTON_SOCKET" - ) - FLATPAK_ENVS+=("--filesystem=xdg-run/${WESTON_SOCKET}") -else - GODOT_ARGS+=("--headless") -fi - timeout "$SAFETY" flatpak run --user \ + --socket=wayland \ --filesystem=home \ + --filesystem=xdg-run/godot-headless \ "${FLATPAK_ENVS[@]}" \ - org.godotengine.Godot "${GODOT_ARGS[@]}" 2>&1 + org.godotengine.Godot --path . --rendering-method gl_compatibility 2>&1 EXIT=$? +kill $WESTON_PID 2>/dev/null || true echo "EXIT_CODE=$EXIT" diff --git a/scripts/apricot/run_seeded.sh b/scripts/apricot/run_seeded.sh index 8ec6f93a..f8a8f730 100755 --- a/scripts/apricot/run_seeded.sh +++ b/scripts/apricot/run_seeded.sh @@ -1,21 +1,25 @@ #!/bin/bash -# Seeded autoplay runner. Delegates to run_ap3.sh with seed and turn limit. -# -# Usage: run_seeded.sh [seed=1] [turn_limit=200] -# Set RENDER_MODE=weston for screenshot-capable runs (default: headless). -set -uo pipefail SEED="${1:-1}" TURN_LIMIT="${2:-200}" - -DIR="$HOME/tmp/ap_seeded_${SEED}" +export XDG_RUNTIME_DIR=/run/user/$(id -u) +killall -q weston godot 2>/dev/null +sleep 1 +weston --backend=headless --socket=godot-headless --width=1920 --height=1080 & +sleep 3 +export WAYLAND_DISPLAY=godot-headless +DIR="/tmp/ap_seeded_${SEED}" rm -rf "$DIR" mkdir -p "$DIR" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -AUTO_PLAY=true \ -AUTO_PLAY_SEED="$SEED" \ -AUTO_PLAY_TURN_LIMIT="$TURN_LIMIT" \ -AUTO_PLAY_DIR="$DIR" \ -RENDER_MODE="${RENDER_MODE:-headless}" \ -bash "$SCRIPT_DIR/run_ap3.sh" +cd ~/Code/@projects/@magic-civilization/src/game +timeout $((TURN_LIMIT * 2 + 300)) flatpak run --user \ + --socket=wayland \ + --filesystem=/tmp \ + --filesystem=home \ + --filesystem=xdg-run/godot-headless \ + --env=WAYLAND_DISPLAY=godot-headless \ + --env=AUTO_PLAY=true \ + --env=AUTO_PLAY_SEED="$SEED" \ + --env=AUTO_PLAY_TURN_LIMIT="$TURN_LIMIT" \ + --env=AUTO_PLAY_DIR="$DIR" \ + org.godotengine.Godot --path . --rendering-method gl_compatibility 2>&1 +echo "EXIT_CODE=$?" diff --git a/tools/autoplay-batch.sh b/tools/autoplay-batch.sh index 3595aba1..f5a72610 100755 --- a/tools/autoplay-batch.sh +++ b/tools/autoplay-batch.sh @@ -25,7 +25,9 @@ GAME_DIR="$PROJECT_DIR/src/game" COUNT="${1:-3}" TURN_LIMIT="${2:-500}" -RESULTS_DIR="${3:-/tmp/autoplay_batch}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +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 @@ -39,11 +41,12 @@ fi AUTOPLAY_HOST="${AUTOPLAY_HOST:-}" SAFETY_TIMEOUT=$(( TURN_LIMIT * 2 + 300 )) -# IMPORTANT: Flatpak sandboxes /tmp — result files written from inside the -# sandbox to /tmp silently disappear. Use $HOME/tmp for local flatpak runs. -if [ -z "$AUTOPLAY_HOST" ] && [[ "$RESULTS_DIR" == /tmp/* ]]; then - RESULTS_DIR="$HOME/tmp/autoplay_batch" - echo "INFO: Local flatpak run — redirecting results dir to $RESULTS_DIR (flatpak cannot write to /tmp)" >&2 +# 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: /.local/batches/) or \$HOME/tmp." >&2 + exit 2 fi mkdir -p "$RESULTS_DIR" @@ -119,20 +122,21 @@ _run_remote() { if [ -z "${REMOTE_HOME:-}" ]; then REMOTE_HOME="$(ssh "$AUTOPLAY_HOST" 'echo "$HOME"')" fi - local remote_game_dir="$REMOTE_HOME/tmp/autoplay_batch/game_${STAMP}_seed${seed}" + 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 /tmp/run_ap3.sh ]; then - echo 'ERROR: /tmp/run_ap3.sh not found on $AUTOPLAY_HOST' >&2 + 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' \ - bash /tmp/run_ap3.sh >'$remote_game_dir/game.log' 2>&1 + 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 }