feat(SAC_LSTM_Bot): training harness (#49)

sac_train.sh orchestrates chunked self-play via tools/training_runner/
RunTraining.java: weighted opponent sampling per chunk, deterministic
eval (SACLSTM_EVAL_MODE=1) every N chunks with win-rate tracking, best
checkpoint (weights/sac_best.zip) by eval score, crash-restart loop on
the runner's liveness detection.

Supporting changes:
- integration.nim: opponentKey() keys the NewBattle buffer-clear rule on
  getBotName(id) with numeric-id fallback (#49 Q14 follow-up);
  bumpRoundCounter() emits the per-round liveness signal.
- SAC_LSTM_Bot.nim: onRoundEnded -> bumpRoundCounter().
- RunTraining.java: BOT_NAME env parameterizes result matching
  (default PPO_Bot, unchanged behavior for PPO).
- Launch packaging: root SAC_LSTM_Bot.json + .sh for the booter;
  src json name aligned to 'SAC_LSTM_Bot' so self-reported identity
  matches the booted identity (mismatch = runner connect timeout).
This commit is contained in:
2026-08-21 21:51:27 +02:00
parent 7104645f5d
commit df256b4d3e
8 changed files with 222 additions and 21 deletions
+10 -8
View File
@@ -18,9 +18,10 @@ import java.util.logging.Logger;
* to the same log file via PPOB_LOG_FILE — the shell wrapper stitches them.
*
* Usage (env vars):
* PPO_BOT_DIR — path to PPO_Bot dir
* PPO_BOT_DIR — path to the bot dir (any Tank Royale bot)
* SAMPLE_BOTS_DIR — path to sample bots archive
* PPOB_LOG_FILE — path to training_log.jsonl (appended)
* BOT_NAME — bot name to match in round results (default: PPO_Bot)
* TRAINING_OPPONENT — opponent bot name (default: Target)
* TRAINING_ROUNDS — number of rounds to run (CLI arg or env var)
*
@@ -39,8 +40,9 @@ public class RunTraining {
: System.getenv().getOrDefault("TRAINING_OPPONENT", "Target");
int totalRounds = args.length > 1 ? Integer.parseInt(args[1])
: Integer.parseInt(System.getenv().getOrDefault("TRAINING_ROUNDS", "100"));
String botName = System.getenv().getOrDefault("BOT_NAME", "PPO_Bot");
System.out.printf("Training: PPO_Bot vs %s for %d rounds%n", opponent, totalRounds);
System.out.printf("Training: %s vs %s for %d rounds%n", botName, opponent, totalRounds);
System.out.printf("Log: %s%n", logFile);
// Dead-bot guard: the runner keeps listing a crashed PPO_Bot in the
@@ -81,7 +83,7 @@ public class RunTraining {
boolean win = false;
boolean found = false;
for (var r : event.getResults()) {
if (r.getName().equals("PPO_Bot")) {
if (r.getName().equals(botName)) {
found = true;
totalScore = r.getTotalScore();
win = r.getRank() == 1;
@@ -99,9 +101,9 @@ public class RunTraining {
frozenRounds[0]++;
long frozenMs = System.currentTimeMillis() - lastAdvanceMs[0];
if (frozenRounds[0] >= 10 && frozenMs >= 10_000) {
System.err.printf("PPO_Bot round_counter frozen at %d for %d "
System.err.printf("%s round_counter frozen at %d for %d "
+ "harness rounds / %.0fs — process dead, aborting for restart%n",
ctr, frozenRounds[0], frozenMs / 1000.0);
botName, ctr, frozenRounds[0], frozenMs / 1000.0);
System.exit(1);
}
} else {
@@ -110,7 +112,7 @@ public class RunTraining {
lastAdvanceMs[0] = System.currentTimeMillis();
}
if (!found) {
System.err.println("PPO_Bot missing from round " + round
System.err.println(botName + " missing from round " + round
+ " results — process died, aborting battle for restart");
System.exit(1);
}
@@ -147,10 +149,10 @@ public class RunTraining {
endCounter = readCounter(counterPath);
}
if (endCounter < expectedEnd) {
System.err.printf("PPO_Bot round_counter %d < expected %d (start+%d) at battle "
System.err.printf("%s round_counter %d < expected %d (start+%d) at battle "
+ "end (waited 60s) — %d rounds never trained (corpse?), aborting for "
+ "restart%n",
endCounter, expectedEnd, totalRounds, expectedEnd - endCounter);
botName, endCounter, expectedEnd, totalRounds, expectedEnd - endCounter);
System.exit(1);
}
System.out.printf("Counter check passed: %d == expected %d%n", endCounter, expectedEnd);