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:
@@ -3,7 +3,8 @@
|
||||
## drain-then-train NewBattle/Shutdown handling, trainPass safety below canSample.
|
||||
|
||||
import arraymancer except Linear
|
||||
import std/locks
|
||||
import std/[locks, json, os]
|
||||
import tankroyale_botapi # updateBotNames: seed the vendored id->name table
|
||||
import SAC_LSTM_Bot/integration
|
||||
import SAC_LSTM_Bot/state # STATE_DIM
|
||||
import SAC_LSTM_Bot/actions # ACTION_DIM
|
||||
@@ -46,14 +47,20 @@ block:
|
||||
assert not ok4, "closed channel must report dataAvailable=false"
|
||||
echo "PASS TrainingMsg channel round-trip"
|
||||
|
||||
# ── 2. NewBattle clears only when the opponent changes; Shutdown stops ────────
|
||||
# ── 2. NewBattle clears only when the opponent NAME changes; Shutdown stops ───
|
||||
block:
|
||||
# Seed the API's id->name table (v1.0.1) for name-based identity (#49).
|
||||
updateBotNames(parseJson(
|
||||
"""{"bots":[{"id":7,"name":"Corners"},{"id":8,"name":"Crazy"}]}"""))
|
||||
assert opponentKey(7) == "Corners", "known id resolves to name"
|
||||
assert opponentKey(99) == "99", "unknown id falls back to numeric string"
|
||||
|
||||
var st: TrainState
|
||||
st.trainer = initSACTrainer(STATE_DIM, ACTION_DIM)
|
||||
st.buf = newReplayBuffer(64, STATE_DIM, ACTION_DIM, burnIn = 2, trainWindow = 3)
|
||||
|
||||
assert handleTrainingMsg(st, TrainingMsg(kind: tmkNewBattle, enemyId: 7))
|
||||
assert st.lastEnemyId == 7
|
||||
assert st.lastEnemyKey == "Corners"
|
||||
|
||||
for i in 0 ..< 5:
|
||||
var m = TrainingMsg(kind: tmkTransition)
|
||||
@@ -68,11 +75,24 @@ block:
|
||||
# Opponent changed -> clear.
|
||||
assert handleTrainingMsg(st, TrainingMsg(kind: tmkNewBattle, enemyId: 8))
|
||||
assert st.buf.len == 0, "opponent change must clear"
|
||||
assert st.lastEnemyId == 8
|
||||
assert st.lastEnemyKey == "Crazy"
|
||||
|
||||
# Nameless window (pre-BotListUpdate) is a distinct key.
|
||||
assert handleTrainingMsg(st, TrainingMsg(kind: tmkNewBattle, enemyId: 99))
|
||||
assert st.lastEnemyKey == "99" and st.buf.len == 0
|
||||
|
||||
# Shutdown stops the caller's loop.
|
||||
assert not handleTrainingMsg(st, TrainingMsg(kind: tmkShutdown))
|
||||
echo "PASS drain-then-train NewBattle/Shutdown"
|
||||
echo "PASS name-keyed NewBattle clear / Shutdown"
|
||||
|
||||
# ── 2b. bumpRoundCounter increments per call, cold-starts at 1 ────────────────
|
||||
block:
|
||||
let tmp = getTempDir() / "sac_test_weights_" & $getCurrentProcessId()
|
||||
putEnv("SACLSTM_WEIGHTS_PATH", tmp / "sac_latest.zip")
|
||||
bumpRoundCounter()
|
||||
bumpRoundCounter()
|
||||
assert readFile(tmp / "round_counter.txt") == "2", "counter increments per round"
|
||||
echo "PASS bumpRoundCounter"
|
||||
|
||||
# ── 3. trainPass is a safe no-op below canSample (no steps, no publish) ───────
|
||||
block:
|
||||
|
||||
Reference in New Issue
Block a user