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:
@@ -342,6 +342,59 @@ func TestProactiveNudgeGap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndgamePassDelayBoundsAndAnchor checks the shortened endgame think time: it always
|
||||
// lands in [30s, 8min], collapses a slow human to the cap, floors a fast human, tracks a
|
||||
// mid human inside [0.8,1.5]*oppLast, floors a clock-skew negative gap, and is
|
||||
// deterministic per (seed, moveCount).
|
||||
func TestEndgamePassDelayBoundsAndAnchor(t *testing.T) {
|
||||
const floor = 30 * time.Second
|
||||
const ceil = 8 * time.Minute
|
||||
cases := []struct {
|
||||
name string
|
||||
oppLast time.Duration
|
||||
lo, hi time.Duration // expected inclusive output range
|
||||
}{
|
||||
{"clock-skew negative floors", -time.Hour, floor, floor},
|
||||
{"zero floors", 0, floor, floor},
|
||||
{"very fast floors", 3 * time.Second, floor, floor}, // [2.4s,4.5s] → floor
|
||||
{"fast tracks above floor", 30 * time.Second, floor, 45 * time.Second}, // [24s,45s] → [30s,45s]
|
||||
{"mid tracks in band", 2 * time.Minute, 96 * time.Second, 3 * time.Minute}, // [1.6m,3m]
|
||||
{"at cap boundary", 8 * time.Minute, 384 * time.Second, ceil}, // [6.4m,12m] → [6.4m,8m]
|
||||
{"slow caps", 3 * time.Hour, ceil, ceil}, // [2.4h,4.5h] → cap
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for seed := int64(1); seed <= 2000; seed++ {
|
||||
d := endgamePassDelay(seed, 30, c.oppLast)
|
||||
if d < floor || d > ceil {
|
||||
t.Fatalf("oppLast=%s seed=%d: delay %s out of hard [%s,%s]", c.oppLast, seed, d, floor, ceil)
|
||||
}
|
||||
if d < c.lo || d > c.hi {
|
||||
t.Fatalf("oppLast=%s seed=%d: delay %s out of expected [%s,%s]", c.oppLast, seed, d, c.lo, c.hi)
|
||||
}
|
||||
if endgamePassDelay(seed, 30, c.oppLast) != d {
|
||||
t.Fatalf("oppLast=%s seed=%d: not deterministic", c.oppLast, seed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndgamePassDelayShrinksLateGame checks the endgame think time is always shorter than
|
||||
// the normal late-game schedule (band floor 10min vs the 8min cap), so taking the min in the
|
||||
// driver actually speeds the robot up rather than ever slowing it down.
|
||||
func TestEndgamePassDelayShrinksLateGame(t *testing.T) {
|
||||
for seed := int64(1); seed <= 1000; seed++ {
|
||||
for mc := 28; mc <= 40; mc++ {
|
||||
eg := endgamePassDelay(seed, mc, 3*time.Hour) // worst case: a slow human, caps at 8min
|
||||
if nd := moveDelay(seed, mc); eg >= nd {
|
||||
t.Fatalf("seed=%d mc=%d: endgame %s not shorter than normal %s", seed, mc, eg, nd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// plays builds candidate plays carrying only the given scores (ranked as passed).
|
||||
func plays(scores ...int) []engine.MoveRecord {
|
||||
out := make([]engine.MoveRecord, len(scores))
|
||||
|
||||
Reference in New Issue
Block a user