Stage 17 round 5 (L2): robot play-to-win intent + next-move ETA in the admin game card
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 29s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m14s

The admin game detail now shows, per robot seat, the game's deterministic play-to-win
decision (from the bag seed) and — while it is that robot's turn — its scheduled next-move
ETA (sampled think-time delay, deferred past the sleep window), plus a caption with the
~40% global target. Wiring: robot.PlayToWin/NextMoveAt/PlayToWinTargetPercent exports,
account.IsRobot, game RobotSchedule (seed + turn-start). Tests: NextMoveAt invariants
(never early, never in the sleep window), PlayToWin export, and an admin render integration
test asserting the intent + ETA + target appear.
This commit is contained in:
Ilia Denisov
2026-06-07 09:42:23 +02:00
parent 29d1193a0a
commit f916d5e0ca
9 changed files with 198 additions and 4 deletions
+31
View File
@@ -207,6 +207,37 @@ func TestMixDeterministic(t *testing.T) {
}
}
// TestNextMoveAt checks the exported schedule used by the admin ETA: the instant is never
// earlier than the sampled think-time delay, and it never lands while the robot is asleep
// (a delay that would fall in the sleep window is deferred to the wake time).
func TestNextMoveAt(t *testing.T) {
base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for seed := int64(1); seed <= 500; seed++ {
for _, h := range []int{0, 2, 6, 9, 14, 23} { // turn starts across the day
start := base.Add(time.Duration(h) * time.Hour)
at := NextMoveAt(seed, 3, start, "UTC")
if at.Before(start.Add(moveDelay(seed, 3))) {
t.Fatalf("seed %d h %d: ETA %s earlier than the scheduled delay", seed, h, at)
}
if asleep("UTC", sleepDrift(seed), at) {
t.Fatalf("seed %d h %d: ETA %s lands in the sleep window", seed, h, at)
}
}
}
}
// TestPlayToWinExport checks the exported decision matches the internal one and the target.
func TestPlayToWinExport(t *testing.T) {
for seed := int64(1); seed <= 200; seed++ {
if PlayToWin(seed) != playToWin(seed) {
t.Fatalf("PlayToWin(%d) != playToWin", seed)
}
}
if PlayToWinTargetPercent != playToWinPercent {
t.Errorf("PlayToWinTargetPercent = %d, want %d", PlayToWinTargetPercent, playToWinPercent)
}
}
// plays builds candidate plays carrying only the given scores (ranked as passed).
func plays(scores ...int) []engine.MoveRecord {
out := make([]engine.MoveRecord, len(scores))