2026-05-10 17:02:46 -07:00
|
|
|
#!/usr/bin/env bash
|
2026-05-17 03:43:32 -07:00
|
|
|
# Generic external-player JSON-Lines harness for Magic Civilization.
|
2026-05-10 17:02:46 -07:00
|
|
|
#
|
2026-05-17 03:43:32 -07:00
|
|
|
# Spawns Godot headless with the `player_api_main` scene and pipes
|
|
|
|
|
# stdin/stdout. Any client that speaks the JSON-Lines wire protocol
|
|
|
|
|
# documented in `src/game/engine/docs/PLAYER_API.md` can drive a game
|
|
|
|
|
# slot through this harness — Claude Code via the
|
|
|
|
|
# `tooling/claude-player-mcp/` adapter, an OpenSpiel/RL trainer via a
|
|
|
|
|
# Python `subprocess.Popen`, a smoke-test shell script, etc.
|
2026-05-10 17:02:46 -07:00
|
|
|
#
|
2026-05-17 03:43:32 -07:00
|
|
|
# Env vars (see PLAYER_API.md for the full schema):
|
2026-05-17 03:51:07 -07:00
|
|
|
# CP_SEED, CP_PLAYERS, CP_PLAYER_SLOT, CP_MAP_SIZE, CP_MAP_TYPE,
|
2026-05-10 17:02:46 -07:00
|
|
|
# CP_OMNISCIENT, CP_TIMEOUT_SEC, CP_LOG_FILE.
|
|
|
|
|
#
|
2026-05-17 03:51:07 -07:00
|
|
|
# `CP_PLAYER_SLOT` is the env-var name the harness has used since p2-67;
|
2026-05-17 03:43:32 -07:00
|
|
|
# it identifies the externally-controlled player slot — kept as-is for
|
|
|
|
|
# backward compatibility with existing clients. Despite the name it is
|
|
|
|
|
# not Claude-specific.
|
|
|
|
|
#
|
2026-05-10 17:02:46 -07:00
|
|
|
# Exit code 0 on clean shutdown (shutdown request received or stdin EOF).
|
|
|
|
|
# Non-zero on protocol error or harness crash.
|
|
|
|
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
|
|
2026-05-19 11:28:16 -07:00
|
|
|
# Wrap godot in heavy-tests.slice on Linux so a fleet of player_api workers
|
|
|
|
|
# (typical RL training spawns 16-64) cannot starve sshd / interactive work.
|
|
|
|
|
# See scripts/run/heavy-prefix.sh and ~/.config/systemd/user/heavy-tests.slice.
|
|
|
|
|
# shellcheck source=run/heavy-prefix.sh
|
|
|
|
|
source "${SCRIPT_DIR}/run/heavy-prefix.sh"
|
|
|
|
|
|
2026-05-10 17:02:46 -07:00
|
|
|
# Defaults — adapter overrides via env.
|
|
|
|
|
: "${CP_SEED:=42}"
|
|
|
|
|
: "${CP_PLAYERS:=2}"
|
2026-05-17 03:51:07 -07:00
|
|
|
: "${CP_PLAYER_SLOT:=0}"
|
2026-05-10 17:02:46 -07:00
|
|
|
: "${CP_MAP_SIZE:=duel}"
|
|
|
|
|
: "${CP_MAP_TYPE:=continents}"
|
|
|
|
|
: "${CP_OMNISCIENT:=0}"
|
|
|
|
|
: "${CP_TIMEOUT_SEC:=60}"
|
|
|
|
|
: "${CP_LOG_FILE:=}"
|
|
|
|
|
|
|
|
|
|
# Pure-headless mode — no Wayland needed. JSON-Lines speaks to stdout.
|
2026-05-17 02:06:12 -07:00
|
|
|
#
|
|
|
|
|
# Linux uses flatpak Godot (matches the rest of the apricot pipeline).
|
|
|
|
|
# macOS uses the locally-installed `godot` binary (Homebrew); a parallel
|
|
|
|
|
# flatpak runtime just for this harness is silly when native Godot 4
|
|
|
|
|
# works directly. Env-var passthrough is automatic for the native path.
|
2026-05-17 03:51:07 -07:00
|
|
|
export CP_SEED CP_PLAYERS CP_PLAYER_SLOT CP_MAP_SIZE CP_MAP_TYPE \
|
2026-05-17 23:59:31 -07:00
|
|
|
CP_OMNISCIENT CP_TIMEOUT_SEC CP_LOG_FILE CP_VICTORY_MODE \
|
2026-06-03 04:06:43 -07:00
|
|
|
CP_PLAYER_CONTROLLERS CP_PLAYER_SLOTS \
|
|
|
|
|
MC_DATA_ROOT MC_LEARNED_POLICY_PATH
|
2026-05-17 02:06:12 -07:00
|
|
|
|
|
|
|
|
case "$(uname -s)" in
|
|
|
|
|
Darwin)
|
|
|
|
|
GODOT_BIN="${GODOT_BIN:-godot}"
|
|
|
|
|
if ! command -v "$GODOT_BIN" >/dev/null 2>&1; then
|
|
|
|
|
echo "ERROR: no godot binary on PATH (set GODOT_BIN or 'brew install godot')." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
exec "$GODOT_BIN" \
|
|
|
|
|
--path "$PROJECT_DIR/src/game" \
|
|
|
|
|
--headless \
|
|
|
|
|
--rendering-method gl_compatibility \
|
2026-05-17 03:43:32 -07:00
|
|
|
res://engine/scenes/headless/player_api_main.tscn
|
2026-05-17 02:06:12 -07:00
|
|
|
;;
|
|
|
|
|
*)
|
2026-06-09 19:51:48 -07:00
|
|
|
# Build the flatpak invocation once (DRY across the slice / no-slice
|
|
|
|
|
# dispatch below).
|
|
|
|
|
FLATPAK_CMD=(flatpak run --user
|
|
|
|
|
--env=CP_SEED="$CP_SEED"
|
|
|
|
|
--env=CP_PLAYERS="$CP_PLAYERS"
|
|
|
|
|
--env=CP_PLAYER_SLOT="$CP_PLAYER_SLOT"
|
|
|
|
|
--env=CP_MAP_SIZE="$CP_MAP_SIZE"
|
|
|
|
|
--env=CP_MAP_TYPE="$CP_MAP_TYPE"
|
|
|
|
|
--env=CP_OMNISCIENT="$CP_OMNISCIENT"
|
|
|
|
|
--env=CP_TIMEOUT_SEC="$CP_TIMEOUT_SEC"
|
|
|
|
|
--env=CP_LOG_FILE="$CP_LOG_FILE"
|
|
|
|
|
--env=CP_VICTORY_MODE="${CP_VICTORY_MODE:-}"
|
|
|
|
|
--env=CP_PLAYER_CONTROLLERS="${CP_PLAYER_CONTROLLERS:-}"
|
|
|
|
|
--env=CP_PLAYER_SLOTS="${CP_PLAYER_SLOTS:-}"
|
|
|
|
|
--env=MC_DATA_ROOT="${MC_DATA_ROOT:-}"
|
|
|
|
|
--env=MC_LEARNED_POLICY_PATH="${MC_LEARNED_POLICY_PATH:-}"
|
|
|
|
|
org.godotengine.Godot
|
|
|
|
|
--path "$PROJECT_DIR/src/game"
|
|
|
|
|
--headless
|
|
|
|
|
--rendering-method gl_compatibility
|
|
|
|
|
res://engine/scenes/headless/player_api_main.tscn)
|
|
|
|
|
|
|
|
|
|
# CP_NO_SLICE=1 runs Godot as a DIRECT child (no per-process systemd
|
|
|
|
|
# scope) so the spawning client reaps the whole process group on
|
|
|
|
|
# shutdown via os.killpg. The default `heavy_exec` path wraps each
|
|
|
|
|
# Godot in its own `systemd-run --scope` unit (commit 8cb522c3c,
|
|
|
|
|
# 2026-05-26) which orphans the Godot when the client kills only the
|
|
|
|
|
# script PID — leaking one Godot per episode reset (gen0 contention
|
|
|
|
|
# death, 2026-06-09; duel-v4 predates the scope wrapping and so never
|
|
|
|
|
# leaked). RL trainers that contain resources at their OWN unit level
|
|
|
|
|
# (CPUWeight/MemoryHigh on the train service) set CP_NO_SLICE=1 to get
|
|
|
|
|
# back the deterministically-reaped model. Interactive / batch callers
|
|
|
|
|
# leave it unset to keep the slice's sshd-preemption guarantee.
|
|
|
|
|
if [ "${CP_NO_SLICE:-0}" = "1" ]; then
|
|
|
|
|
exec "${FLATPAK_CMD[@]}"
|
|
|
|
|
fi
|
|
|
|
|
heavy_exec "player-api-$$" "${FLATPAK_CMD[@]}"
|
2026-05-17 02:06:12 -07:00
|
|
|
;;
|
|
|
|
|
esac
|