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
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:
@@ -18,6 +18,7 @@ import (
|
||||
"scrabble/backend/internal/adminconsole"
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/robot"
|
||||
)
|
||||
|
||||
// adminPageSize is the page size of the admin console's paginated lists.
|
||||
@@ -248,16 +249,58 @@ func (s *Server) consoleGameDetail(c *gin.Context) {
|
||||
MoveCount: g.MoveCount, CreatedAt: fmtTime(g.CreatedAt), UpdatedAt: fmtTime(g.UpdatedAt),
|
||||
FinishedAt: fmtTimePtr(g.FinishedAt),
|
||||
}
|
||||
// Resolve seats and detect robot seats; capture the human opponent's timezone, which
|
||||
// anchors the robot's sleep window for the next-move ETA.
|
||||
oppTZ := ""
|
||||
for _, seat := range g.Seats {
|
||||
row := adminconsole.SeatRow{Seat: seat.Seat, AccountID: seat.AccountID.String(), Score: seat.Score, HintsUsed: seat.HintsUsed, Winner: seat.IsWinner}
|
||||
if acc, err := s.accounts.GetByID(ctx, seat.AccountID); err == nil {
|
||||
acc, accErr := s.accounts.GetByID(ctx, seat.AccountID)
|
||||
if accErr == nil {
|
||||
row.DisplayName = acc.DisplayName
|
||||
}
|
||||
if isRobot, _ := s.accounts.IsRobot(ctx, seat.AccountID); isRobot {
|
||||
row.IsRobot = true
|
||||
view.HasRobot = true
|
||||
} else if accErr == nil {
|
||||
oppTZ = acc.TimeZone
|
||||
}
|
||||
view.Seats = append(view.Seats, row)
|
||||
}
|
||||
// For each robot seat, surface the game's deterministic play-to-win intent and — while
|
||||
// it is that robot's turn — the scheduled next-move ETA, both derived from the bag seed.
|
||||
if view.HasRobot {
|
||||
view.RobotTargetPct = robot.PlayToWinTargetPercent
|
||||
if seed, turnStartedAt, schedErr := s.games.RobotSchedule(ctx, g.ID); schedErr == nil {
|
||||
now := time.Now().UTC()
|
||||
for i := range view.Seats {
|
||||
if !view.Seats[i].IsRobot {
|
||||
continue
|
||||
}
|
||||
if robot.PlayToWin(seed) {
|
||||
view.Seats[i].RobotIntent = "play to win"
|
||||
} else {
|
||||
view.Seats[i].RobotIntent = "play to lose"
|
||||
}
|
||||
if g.Status == game.StatusActive && g.ToMove == view.Seats[i].Seat {
|
||||
view.Seats[i].NextMove = robotETA(robot.NextMoveAt(seed, g.MoveCount, turnStartedAt, oppTZ), now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.renderConsole(c, "game_detail", "games", "Game", view)
|
||||
}
|
||||
|
||||
// robotETA formats a robot's scheduled next-move instant as an absolute UTC time plus a
|
||||
// relative estimate, e.g. "≈ 14:37 UTC (in ~7 min)"; a past instant reads "(due now)".
|
||||
func robotETA(at, now time.Time) string {
|
||||
mins := int(at.Sub(now).Round(time.Minute).Minutes())
|
||||
rel := fmt.Sprintf("in ~%d min", mins)
|
||||
if mins <= 0 {
|
||||
rel = "due now"
|
||||
}
|
||||
return fmt.Sprintf("≈ %s UTC (%s)", at.UTC().Format("15:04"), rel)
|
||||
}
|
||||
|
||||
// consoleComplaints renders the paginated complaint review queue.
|
||||
func (s *Server) consoleComplaints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
Reference in New Issue
Block a user