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:
@@ -114,6 +114,40 @@ func playToWin(seed int64) bool {
|
||||
return mix(seed, "win")%100 < playToWinPercent
|
||||
}
|
||||
|
||||
// PlayToWin exposes the once-per-game play-to-win decision for a game's bag seed, for the
|
||||
// admin console (it is deterministic and fixed for the whole game).
|
||||
func PlayToWin(seed int64) bool { return playToWin(seed) }
|
||||
|
||||
// PlayToWinTargetPercent is the configured probability, in percent, that a robot plays to
|
||||
// win in any given game (the admin console shows it alongside the per-game decision).
|
||||
const PlayToWinTargetPercent = playToWinPercent
|
||||
|
||||
// NextMoveAt is the deterministic instant the robot is scheduled to play the move at
|
||||
// moveCount, given when the turn started and the opponent's timezone (which anchors the
|
||||
// robot's sleep window). It is the sampled think-time delay, deferred to the end of the
|
||||
// sleep window when it would otherwise land while the robot is asleep. The driver acts on
|
||||
// a scan tick, so the real move lands at the first scan at or after this instant. It is
|
||||
// meaningful only on the robot's own turn; the admin console surfaces it as an ETA.
|
||||
func NextMoveAt(seed int64, moveCount int, turnStartedAt time.Time, opponentTZ string) time.Time {
|
||||
t := turnStartedAt.Add(moveDelay(seed, moveCount))
|
||||
drift := sleepDrift(seed)
|
||||
if asleep(opponentTZ, drift, t) {
|
||||
t = wakeAfter(opponentTZ, drift, t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// wakeAfter returns the first instant at or after t when the robot is awake — the local
|
||||
// hour reaches sleepEndHour in the opponent's drifted timezone — converted back to UTC.
|
||||
func wakeAfter(opponentTZ string, drift time.Duration, t time.Time) time.Time {
|
||||
local := t.In(loadLocation(opponentTZ)).Add(drift)
|
||||
wake := time.Date(local.Year(), local.Month(), local.Day(), sleepEndHour, 0, 0, 0, local.Location())
|
||||
if !wake.After(local) {
|
||||
wake = wake.Add(24 * time.Hour)
|
||||
}
|
||||
return wake.Add(-drift).UTC()
|
||||
}
|
||||
|
||||
// delayBand returns the lower and upper bounds, in minutes, of the move-delay band
|
||||
// for the move at moveCount. It interpolates linearly with game progress (the move
|
||||
// count over avgGameMoves, capped at 1): early moves sit in a short band and late
|
||||
|
||||
Reference in New Issue
Block a user