feat(robot): shrink endgame think time when both sides pass
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
In a dead-drawn endgame — the two most recent journal moves are both passes, so the board and the robot's rack are frozen and the robot is bound to pass again — the robot still waited out its long late-game think time (up to 90 min) before passing, needlessly dragging out a decided game. Shorten that delay to a [0.8, 1.5]x band around the human's last-move think time (the gap between the last two journal entries), clamped to [30s, 8min] and taken as a min with the normal schedule, so the robot never moves slower. A slow human collapses to the 8-min cap; a fast human is tracked, with the floor keeping the robot from passing suspiciously instantly. The anchor reads the move journal only (no schema change), stays deterministic from the seed, and still defers to the sleep window. RobotTurns now carries EndgamePass + OppLastMove, filled by one batched journal query on the scan; the honest-AI single-game trigger keeps the normal path (it moves at once). NextMoveAt (admin ETA) is left as the normal-schedule upper bound.
This commit is contained in:
@@ -49,6 +49,21 @@ const (
|
||||
delayHardMinMinutes = 1.0
|
||||
delayHardMaxMinutes = 90.0
|
||||
|
||||
// In a dead-drawn endgame — the two most recent committed moves are both passes,
|
||||
// so the board and the robot's rack are frozen and the robot is bound to pass
|
||||
// again — the robot drops the long late-game think time and answers on a shortened
|
||||
// schedule scaled to the human's own last-move (pass) think time: a uniform sample
|
||||
// from [endgameLoFactor, endgameHiFactor] of it, clamped to [endgameFloorSeconds,
|
||||
// endgameCapMinutes]. A slow human collapses to the cap (the robot never drags out
|
||||
// a decided game), a fast human is tracked, and the floor keeps the robot from
|
||||
// passing suspiciously instantly. The shrink only ever lowers the delay (it is
|
||||
// taken as a min with the normal schedule), so it never makes the robot slower, and
|
||||
// it composes with the sleep window, which is still honoured before any move.
|
||||
endgameLoFactor = 0.8
|
||||
endgameHiFactor = 1.5
|
||||
endgameFloorSeconds = 30.0
|
||||
endgameCapMinutes = 8.0
|
||||
|
||||
// nudgeReplySpreadMinutes is the width of the quick window, anchored at the move's
|
||||
// lower band (delayBand's lo), within which the robot answers a daytime nudge on
|
||||
// its turn — so a nudged robot replies near the floor of its think time.
|
||||
@@ -171,7 +186,9 @@ func deviates(seed int64, moveCount, bagLen int) bool {
|
||||
// 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.
|
||||
// meaningful only on the robot's own turn; the admin console surfaces it as an ETA. In a
|
||||
// dead-drawn endgame the robot may pass sooner than this (see endgamePassDelay); NextMoveAt
|
||||
// remains the normal-schedule upper bound.
|
||||
func NextMoveAt(seed int64, moveCount int, turnStartedAt time.Time, opponentTZ string) time.Time {
|
||||
t := turnStartedAt.Add(moveDelay(seed, moveCount))
|
||||
drift := sleepDrift(seed)
|
||||
@@ -215,6 +232,25 @@ func moveDelay(seed int64, moveCount int) time.Duration {
|
||||
return clampMinutes(lo + (hi-lo)*math.Pow(u, delaySkew))
|
||||
}
|
||||
|
||||
// endgamePassDelay is the robot's shortened think time for a guaranteed endgame pass
|
||||
// (the two most recent moves are both passes), given the human's last-move think time
|
||||
// oppLast: a uniform sample from [endgameLoFactor, endgameHiFactor] of oppLast, clamped
|
||||
// to [endgameFloorSeconds, endgameCapMinutes]. It is deterministic per (seed, moveCount)
|
||||
// like moveDelay, and oppLast is read from the persisted move journal, so the schedule is
|
||||
// reproducible across restarts. The caller takes it as a min with moveDelay, so it never
|
||||
// slows the robot down. A non-positive oppLast (clock skew) clamps up to the floor.
|
||||
func endgamePassDelay(seed int64, moveCount int, oppLast time.Duration) time.Duration {
|
||||
floor := time.Duration(endgameFloorSeconds * float64(time.Second))
|
||||
ceil := time.Duration(endgameCapMinutes * float64(time.Minute))
|
||||
lo := clampDur(time.Duration(float64(oppLast)*endgameLoFactor), floor, ceil)
|
||||
hi := clampDur(time.Duration(float64(oppLast)*endgameHiFactor), floor, ceil)
|
||||
if hi < lo {
|
||||
hi = lo
|
||||
}
|
||||
u := unitFloat(mix(seed, "endgame", moveCount))
|
||||
return lo + time.Duration(float64(hi-lo)*u)
|
||||
}
|
||||
|
||||
// nudgeReplyDelay is how soon after a daytime nudge the robot answers the move at
|
||||
// moveCount: a uniform sample from the quick window [lo, lo+nudgeReplySpreadMinutes],
|
||||
// where lo is the move's lower band — so a nudge pulls the move in near the floor of
|
||||
@@ -254,6 +290,18 @@ func clampMinutes(mins float64) time.Duration {
|
||||
return time.Duration(mins * float64(time.Minute))
|
||||
}
|
||||
|
||||
// clampDur returns d confined to the inclusive range [lo, hi].
|
||||
func clampDur(d, lo, hi time.Duration) time.Duration {
|
||||
switch {
|
||||
case d < lo:
|
||||
return lo
|
||||
case d > hi:
|
||||
return hi
|
||||
default:
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
// sleepDrift is the per-game shift of the robot's sleep window relative to the
|
||||
// opponent's timezone, in [-sleepDriftHours, +sleepDriftHours] hours.
|
||||
func sleepDrift(seed int64) time.Duration {
|
||||
|
||||
Reference in New Issue
Block a user