gauntlet: BitBrain vs Pattern across 32 legacy opponents (does not generalize)
The repo's first multi-opponent gun measurement. Adds tools/ab/gauntlet_run.sh (per-opponent A/B over the legacy roster, subject = frozen ModularBot), tools/ab/gauntlet_analyze.py (paired per-opponent deltas, cross-opponent sign test, style split, MDE) and the arm/opponent fixtures. Result: BitBrain does NOT generalize beyond DrussGT. 32 opponents x 2 arms x 3 runs x 5 rounds = 192 battles / 960 rounds, 0 failed, 0 retries: damage/run 214.5 (pattern) vs 210.9 (bb), sign-flip p=0.53; round wins 237/480 vs 239/480, p=0.91. Sign test: bb better on 13/32 opponents (damage). The DrussGT-only penalty does not carry. The owner's 'killer vs regular movers' sub-claim is not supported: regular bucket +1.3 dmg/run vs dodgers -0.2 (MW p=0.85), and the measured movement predictability does not correlate with the delta.
This commit is contained in:
Executable
+317
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# gauntlet_run.sh — run a PER-ADVERSARY A/B gauntlet of N arms against MANY
|
||||
# legacy opponents (the generalization test for the gun claims that have, until
|
||||
# now, always been measured against the single opponent DrussGT).
|
||||
#
|
||||
# tools/ab/gauntlet_run.sh \
|
||||
# --arms tools/ab/arms_gauntlet.txt \
|
||||
# --opponents tools/ab/opponents_gauntlet.txt \
|
||||
# --runs 3 --rounds 3 --conc 5 --outdir /tmp/ab/j117_gauntlet
|
||||
#
|
||||
# Difference from ab_run.sh: ab_run.sh hard-wires exactly ONE adversary
|
||||
# (DrussGT, via run_bridge_battle's legacy default subject). Here the SUBJECT
|
||||
# is the frozen ModularBot itself (so the capture's `subject event counts` are
|
||||
# ModularBot's own fire/hit/taken counts and `firstPlaces` is ModularBot's
|
||||
# name-based round wins) and the ADVERSARY iterates over a list of legacy
|
||||
# champ bot dirs from tools/robocode_shim/robots.json.
|
||||
#
|
||||
# For every (opponent, arm, run) it launches a real Tank Royale battle through
|
||||
# tools/robocode_shim/run_bridge_battle.sh, each in its own process group with
|
||||
# ephemeral ports and an isolated classic data dir.
|
||||
#
|
||||
# Arm file format (same as ab_run.sh):
|
||||
# name | ENV_VAR=value ENV_VAR2=value2 | optional label
|
||||
#
|
||||
# Opponent file format (one per line):
|
||||
# /tmp/tr_bots/<Name> | style-label # or a bare Name (=> /tmp/tr_bots/)
|
||||
# Blank lines and #-comments ignored.
|
||||
#
|
||||
# Output layout (what gauntlet_analyze.py reads):
|
||||
# <outdir>/session.json
|
||||
# <outdir>/<opponent>/<arm>/run<N>.jsonl tick capture
|
||||
# <outdir>/<opponent>/<arm>/run<N>.jsonl.rounds.json round boundaries
|
||||
# <outdir>/<opponent>/<arm>/run<N>.jsonl.results.json cumulative scores
|
||||
# <outdir>/<opponent>/<arm>/run<N>.events.jsonl fire/hit/death
|
||||
# <outdir>/<opponent>/<arm>/run<N>.battle.log capture stdout
|
||||
# <outdir>/<opponent>/<arm>/run<N>.bot.stdout.log ModularBot env
|
||||
#
|
||||
# Cleanup: every job runs in its own process group (setsid); on EXIT/INT/TERM
|
||||
# this script SIGTERMs/SIGKILLs those groups and pkills anything referencing
|
||||
# this session's outdir, so a Ctrl-C leaves no orphan battles.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
RUN_BRIDGE="$REPO/tools/robocode_shim/run_bridge_battle.sh"
|
||||
|
||||
ARMS_FILE=""
|
||||
OPP_FILE=""
|
||||
RUNS=3
|
||||
ROUNDS=3
|
||||
CONC=4
|
||||
OUTDIR=""
|
||||
# A bot can fail to connect within the booter's 30 s window under load
|
||||
# ("Only 1 of 2 bots started"), which is a FALSE NEGATIVE, not a real battle.
|
||||
# Each job is retried until the capture shows real rows AND a subject counter
|
||||
# line, so the analyzer never sees a phantom 0-0 run.
|
||||
MAX_ATTEMPTS="${GAUNTLET_MAX_ATTEMPTS:-4}"
|
||||
|
||||
usage() {
|
||||
sed -n '2,45p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arms) ARMS_FILE="$2"; shift 2;;
|
||||
--opponents) OPP_FILE="$2"; shift 2;;
|
||||
--runs) RUNS="$2"; shift 2;;
|
||||
--rounds) ROUNDS="$2"; shift 2;;
|
||||
--conc) CONC="$2"; shift 2;;
|
||||
--outdir) OUTDIR="$2"; shift 2;;
|
||||
-h|--help) usage 0;;
|
||||
*) echo "unknown argument: $1" >&2; usage 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$ARMS_FILE" ]] || { echo "ERROR: --arms <file> is required" >&2; usage 1; }
|
||||
[[ -n "$OPP_FILE" ]] || { echo "ERROR: --opponents <file> is required" >&2; usage 1; }
|
||||
[[ -f "$ARMS_FILE" ]] || { echo "ERROR: arm file not found: $ARMS_FILE" >&2; exit 1; }
|
||||
[[ -f "$OPP_FILE" ]] || { echo "ERROR: opponent file not found: $OPP_FILE" >&2; exit 1; }
|
||||
[[ -n "$OUTDIR" ]] || { echo "ERROR: --outdir <dir> is required" >&2; exit 1; }
|
||||
[[ "$OUTDIR" != "/" && "$OUTDIR" != "" ]] || { echo "ERROR: refusing outdir '$OUTDIR'" >&2; exit 1; }
|
||||
OUTDIR="$(mkdir -p "$OUTDIR" && cd "$OUTDIR" && pwd)"
|
||||
|
||||
# ── prerequisites ────────────────────────────────────────────────────────────
|
||||
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}"
|
||||
RUNNER_JAR="${TR_RUNNER_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}"
|
||||
BOT_API_JAR="${TR_BOT_API_JAR:-$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar}"
|
||||
|
||||
missing=0
|
||||
for f in "$ROBOCODE_JAR" "$RUNNER_JAR" "$BOT_API_JAR"; do
|
||||
if [[ ! -f "$f" ]]; then echo "ERROR: missing prerequisite: $f" >&2; missing=1; fi
|
||||
done
|
||||
command -v nim >/dev/null 2>&1 || { echo "ERROR: nim not on PATH" >&2; missing=1; }
|
||||
command -v git >/dev/null 2>&1 || { echo "ERROR: git not on PATH" >&2; missing=1; }
|
||||
[[ -f "$RUN_BRIDGE" ]] || { echo "ERROR: missing $RUN_BRIDGE" >&2; missing=1; }
|
||||
(( missing == 0 )) || { echo "ERROR: prerequisites missing — not starting a broken session" >&2; exit 1; }
|
||||
|
||||
# ── parse the arm file into parallel arrays ──────────────────────────────────
|
||||
trim() { local s="$1"; s="${s#"${s%%[![:space:]]*}"}"; s="${s%"${s##*[![:space:]]}"}"; printf '%s' "$s"; }
|
||||
|
||||
ARM_NAMES=(); ARM_ENVS=(); ARM_LABELS=()
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
[[ -z "${line//[[:space:]]/}" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
name="$(trim "${line%%|*}")"
|
||||
rest="${line#*|}"
|
||||
if [[ "$line" == *"|"* ]]; then
|
||||
envspec="$(trim "${rest%%|*}")"
|
||||
label="$(trim "${rest#*|}")"
|
||||
else
|
||||
envspec=""; label=""
|
||||
fi
|
||||
[[ -n "$name" ]] || { echo "ERROR: arm with empty name in $ARMS_FILE" >&2; exit 1; }
|
||||
ARM_NAMES+=("$name"); ARM_ENVS+=("$envspec"); ARM_LABELS+=("$label")
|
||||
done < "$ARMS_FILE"
|
||||
(( ${#ARM_NAMES[@]} > 0 )) || { echo "ERROR: no arms parsed from $ARMS_FILE" >&2; exit 1; }
|
||||
dupes="$(printf '%s\n' "${ARM_NAMES[@]}" | sort | uniq -d)"
|
||||
[[ -z "$dupes" ]] || { echo "ERROR: duplicate arm name(s): $dupes" >&2; exit 1; }
|
||||
|
||||
# ── parse the opponent file ──────────────────────────────────────────────────
|
||||
OPP_DIRS=(); OPP_NAMES=(); OPP_STYLES=()
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
[[ -z "${line//[[:space:]]/}" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
dir="$(trim "${line%%|*}")"
|
||||
style=""
|
||||
if [[ "$line" == *"|"* ]]; then style="$(trim "${line#*|}")"; fi
|
||||
[[ "$dir" == */* ]] || dir="/tmp/tr_bots/$dir"
|
||||
[[ -d "$dir" ]] || { echo "ERROR: opponent bot dir not found: $dir" >&2; exit 1; }
|
||||
nm="$(basename "$dir")"
|
||||
OPP_DIRS+=("$dir"); OPP_NAMES+=("$nm"); OPP_STYLES+=("$style")
|
||||
done < "$OPP_FILE"
|
||||
(( ${#OPP_NAMES[@]} > 0 )) || { echo "ERROR: no opponents parsed from $OPP_FILE" >&2; exit 1; }
|
||||
|
||||
# ── fresh session dir ────────────────────────────────────────────────────────
|
||||
rm -rf "$OUTDIR"
|
||||
mkdir -p "$OUTDIR"
|
||||
WORK="$OUTDIR/.work"; mkdir -p "$WORK"
|
||||
|
||||
# ── build ONE frozen bot from HEAD ───────────────────────────────────────────
|
||||
COMMIT="$(git -C "$REPO" rev-parse HEAD)"
|
||||
FROZEN_DIR="$OUTDIR/frozen/ModularBot"
|
||||
FROZEN_BIN="$FROZEN_DIR/ModularBot_bin"
|
||||
FROZEN_JSON="$FROZEN_DIR/ModularBot.json"
|
||||
mkdir -p "$FROZEN_DIR"
|
||||
BUILDDIR="$WORK/head"
|
||||
mkdir -p "$BUILDDIR"
|
||||
echo "[gauntlet] exporting HEAD ($COMMIT) -> $BUILDDIR"
|
||||
git -C "$REPO" archive HEAD | tar -x -C "$BUILDDIR"
|
||||
|
||||
echo "[gauntlet] building frozen ModularBot (nim c -d:release)…"
|
||||
( cd "$BUILDDIR/ModularBot_garage" && \
|
||||
nim c -d:release --nimcache:"$OUTDIR/nimcache" --out:"$FROZEN_BIN" src/ModularBot.nim )
|
||||
[[ -x "$FROZEN_BIN" ]] || { echo "ERROR: frozen build failed" >&2; exit 1; }
|
||||
BINSHA="$(sha256sum "$FROZEN_BIN" | awk '{print $1}')"
|
||||
echo "[gauntlet] frozen binary sha256=$BINSHA"
|
||||
|
||||
cat > "$FROZEN_JSON" <<'JSON'
|
||||
{
|
||||
"name": "ModularBot",
|
||||
"version": "0.1.0",
|
||||
"authors": ["Davide Cappellini"],
|
||||
"description": "frozen A/B build of ModularBot (gauntlet_run.sh)",
|
||||
"homepage": "",
|
||||
"countryCodes": ["IT"],
|
||||
"gameTypes": ["classic", "1v1"],
|
||||
"platform": "Nim",
|
||||
"programmingLang": "Nim"
|
||||
}
|
||||
JSON
|
||||
|
||||
# ── session.json ─────────────────────────────────────────────────────────────
|
||||
json_esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "commit": "%s",\n' "$COMMIT"
|
||||
printf ' "binary_sha256": "%s",\n' "$BINSHA"
|
||||
printf ' "binary": "frozen/ModularBot/ModularBot_bin",\n'
|
||||
printf ' "rounds": %d,\n' "$ROUNDS"
|
||||
printf ' "runs": %d,\n' "$RUNS"
|
||||
printf ' "conc": %d,\n' "$CONC"
|
||||
printf ' "timestamp": "%s",\n' "$(date -Is)"
|
||||
printf ' "outdir": "%s",\n' "$(json_esc "$OUTDIR")"
|
||||
printf ' "arms": [\n'
|
||||
for i in "${!ARM_NAMES[@]}"; do
|
||||
printf ' {"name": "%s", "env": "%s", "label": "%s"}' \
|
||||
"$(json_esc "${ARM_NAMES[$i]}")" "$(json_esc "${ARM_ENVS[$i]}")" "$(json_esc "${ARM_LABELS[$i]}")"
|
||||
(( i + 1 < ${#ARM_NAMES[@]} )) && printf ',' || true
|
||||
printf '\n'
|
||||
done
|
||||
printf ' ],\n'
|
||||
printf ' "opponents": [\n'
|
||||
for i in "${!OPP_NAMES[@]}"; do
|
||||
printf ' {"name": "%s", "dir": "%s", "style": "%s"}' \
|
||||
"$(json_esc "${OPP_NAMES[$i]}")" "$(json_esc "${OPP_DIRS[$i]}")" "$(json_esc "${OPP_STYLES[$i]}")"
|
||||
(( i + 1 < ${#OPP_NAMES[@]} )) && printf ',' || true
|
||||
printf '\n'
|
||||
done
|
||||
printf ' ]\n}\n'
|
||||
} > "$OUTDIR/session.json"
|
||||
|
||||
# ── one job (run in its own process group) ───────────────────────────────────
|
||||
# exported so the detached `bash -c` subshell can call it; config via env
|
||||
export REPO RUN_BRIDGE ROUNDS OUTDIR WORK FROZEN_BIN FROZEN_JSON
|
||||
|
||||
gauntlet_run_one() {
|
||||
local opponent_dir="$1" opponent="$2" arm="$3" run="$4" envspec="$5"
|
||||
local dir="$OUTDIR/$opponent/$arm"
|
||||
local work="$WORK/$opponent/$arm/run$run"
|
||||
local botdir="$work/bots/ModularBot"
|
||||
mkdir -p "$dir" "$botdir" "$work/data"
|
||||
|
||||
cp "$FROZEN_JSON" "$botdir/ModularBot.json"
|
||||
cat > "$botdir/ModularBot.sh" <<SH
|
||||
#!/bin/sh
|
||||
cd "\$(dirname "\$0")"
|
||||
exec ./ModularBot_bin > "$dir/run$run.bot.stdout.log" 2> "$dir/run$run.bot.stderr.log"
|
||||
SH
|
||||
chmod +x "$botdir/ModularBot.sh"
|
||||
ln -sf "$FROZEN_BIN" "$botdir/ModularBot_bin"
|
||||
|
||||
local attempt=0
|
||||
while :; do
|
||||
attempt=$((attempt + 1))
|
||||
rm -f "$dir/run$run.jsonl" "$dir/run$run.jsonl.rounds.json" \
|
||||
"$dir/run$run.jsonl.results.json" "$dir/run$run.events.jsonl" \
|
||||
"$dir/run$run.status" "$dir/run$run.bot.stdout.log" "$dir/run$run.bot.stderr.log"
|
||||
|
||||
local rc=0
|
||||
# shellcheck disable=SC2086 # envspec is intentionally word-split
|
||||
env $envspec \
|
||||
SHIM_BOTDIR="$botdir" \
|
||||
SHIM_BOT_NAME="ModularBot" \
|
||||
SHIM_DATA="$work/data" \
|
||||
TR_EVENTS_OUT="$dir/run$run.events.jsonl" \
|
||||
timeout 900 "$RUN_BRIDGE" "$opponent_dir" "$ROUNDS" "$dir/run$run.jsonl" \
|
||||
> "$dir/run$run.battle.log" 2>&1 || rc=$?
|
||||
echo "$rc" > "$dir/run$run.status"
|
||||
|
||||
# liveness: a real battle has captured rows and a subject counter line
|
||||
if grep -q "subject event counts" "$dir/run$run.battle.log" 2>/dev/null \
|
||||
&& grep -Eq "rows=[1-9]" "$dir/run$run.battle.log" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
if (( attempt >= MAX_ATTEMPTS )); then
|
||||
echo "[gauntlet] WARNING: ${opponent}/${arm} run${run} never started after ${attempt} attempts" >&2
|
||||
break
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
echo "$attempt" > "$dir/run$run.attempts"
|
||||
return 0
|
||||
}
|
||||
export -f gauntlet_run_one
|
||||
|
||||
# ── cleanup: kill this session's children ────────────────────────────────────
|
||||
JOB_PIDS=()
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
trap - EXIT INT TERM
|
||||
for p in "${JOB_PIDS[@]:-}"; do
|
||||
[[ -n "$p" ]] || continue
|
||||
kill -TERM -- "-$p" 2>/dev/null || kill -TERM "$p" 2>/dev/null || true
|
||||
done
|
||||
sleep 0.5
|
||||
for p in "${JOB_PIDS[@]:-}"; do
|
||||
[[ -n "$p" ]] || continue
|
||||
kill -KILL -- "-$p" 2>/dev/null || true
|
||||
done
|
||||
pkill -f "[r]un_bridge_battle.sh.*$OUTDIR" 2>/dev/null || true
|
||||
pkill -f "[r]obocode_shim.TrBattleCapture.*$OUTDIR" 2>/dev/null || true
|
||||
exit "$rc"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# ── launch the jobs, K at a time ─────────────────────────────────────────────
|
||||
NJOBS=$(( ${#OPP_NAMES[@]} * ${#ARM_NAMES[@]} * RUNS ))
|
||||
echo "[gauntlet] session: ${#OPP_NAMES[@]} opponents × ${#ARM_NAMES[@]} arms × $RUNS runs = $NJOBS battles, rounds=$ROUNDS, conc=$CONC"
|
||||
echo "[gauntlet] outdir: $OUTDIR"
|
||||
RUNNING=0
|
||||
for oi in "${!OPP_NAMES[@]}"; do
|
||||
for ai in "${!ARM_NAMES[@]}"; do
|
||||
for (( r=1; r<=RUNS; r++ )); do
|
||||
setsid bash -c 'gauntlet_run_one "$1" "$2" "$3" "$4" "$5"' _ \
|
||||
"${OPP_DIRS[$oi]}" "${OPP_NAMES[$oi]}" "${ARM_NAMES[$ai]}" "$r" "${ARM_ENVS[$ai]}" &
|
||||
JOB_PIDS+=("$!")
|
||||
RUNNING=$((RUNNING + 1))
|
||||
if (( RUNNING >= CONC )); then
|
||||
wait -n || true
|
||||
RUNNING=$((RUNNING - 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
wait || true
|
||||
|
||||
JOB_PIDS=()
|
||||
|
||||
# ── aggregate status ─────────────────────────────────────────────────────────
|
||||
FAILED=0
|
||||
for oi in "${!OPP_NAMES[@]}"; do
|
||||
for ai in "${!ARM_NAMES[@]}"; do
|
||||
for (( r=1; r<=RUNS; r++ )); do
|
||||
st="$(cat "$OUTDIR/${OPP_NAMES[$oi]}/${ARM_NAMES[$ai]}/run$r.status" 2>/dev/null || echo 999)"
|
||||
if [[ "$st" != "0" ]]; then
|
||||
echo "[gauntlet] FAIL ${OPP_NAMES[$oi]}/${ARM_NAMES[$ai]} run$r (rc=$st)" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo "[gauntlet] done: $(( NJOBS - FAILED )) ok, $FAILED failed"
|
||||
echo "[gauntlet] analyze with: python3 $REPO/tools/ab/gauntlet_analyze.py $OUTDIR"
|
||||
(( FAILED == 0 )) || exit 1
|
||||
Reference in New Issue
Block a user