feat(@projects/@magic-civilization): add automated play scripts

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-15 16:53:03 -07:00
parent e7e35df50d
commit df3064a6c9
5 changed files with 49 additions and 87 deletions

8
.gitignore vendored
View file

@ -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/

View file

@ -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)

View file

@ -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"

View file

@ -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=$?"

View file

@ -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: <repo>/.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
}