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
+39
View File
@@ -167,6 +167,45 @@ func TestConsoleServesAndGuardsCSRF(t *testing.T) {
}
}
// TestConsoleGameDetailRobotSchedule checks the admin game card surfaces a robot seat's
// play-to-win intent and, while it is the robot's turn, its next-move ETA (Stage 17).
func TestConsoleGameDetailRobotSchedule(t *testing.T) {
ctx := context.Background()
svc := newGameService()
robotAcc, err := account.NewStore(testDB).ProvisionRobot(ctx, "robot-admin-"+uuid.NewString(), "Robo Tester")
if err != nil {
t.Fatalf("provision robot: %v", err)
}
human := provisionAccount(t)
// Seat the robot first so it is to move (seat 0), exposing the next-move ETA.
g, err := svc.Create(ctx, game.CreateParams{
Variant: engine.VariantEnglish, Seats: []uuid.UUID{robotAcc.ID, human}, TurnTimeout: 24 * time.Hour, Seed: 7,
})
if err != nil {
t.Fatalf("create: %v", err)
}
srv := server.New(":0", server.Deps{
Logger: zap.NewNop(), Accounts: account.NewStore(testDB), Games: svc, Registry: testRegistry, DictDir: dictDir(),
})
code, body := consoleDo(srv.Handler(), http.MethodGet, "http://admin.test/_gm/games/"+g.ID.String(), "", "")
if code != http.StatusOK {
t.Fatalf("game detail = %d, want 200", code)
}
if !strings.Contains(body, "🤖") {
t.Error("robot seat is not marked in the game detail")
}
if !strings.Contains(body, "play to win") && !strings.Contains(body, "play to lose") {
t.Error("robot play-to-win intent missing")
}
if !strings.Contains(body, "next move") {
t.Error("robot is to move but the next-move ETA is missing")
}
if !strings.Contains(body, "~40%") {
t.Error("robot play-to-win target caption missing")
}
}
// consoleDo issues a request to h, optionally with an Origin header, and returns
// the status and body. Form bodies are sent as application/x-www-form-urlencoded.
func consoleDo(h http.Handler, method, target, body, origin string) (int, string) {