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

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:
Ilia Denisov
2026-06-19 12:48:39 +02:00
parent 0eb1e0ef47
commit f3768d20f2
10 changed files with 342 additions and 7 deletions
+10 -1
View File
@@ -128,7 +128,16 @@ func (s *Service) TriggerMove(gameID uuid.UUID) {
// the opponent during the current turn pulls the move in to the short reply
// window; otherwise the robot waits out its sampled delay.
func (s *Service) maybeMove(ctx context.Context, rt game.RobotTurn, oppID uuid.UUID, now time.Time) error {
if now.Before(rt.TurnStartedAt.Add(moveDelay(rt.Seed, rt.MoveCount))) {
delay := moveDelay(rt.Seed, rt.MoveCount)
if rt.EndgamePass {
// A dead-drawn endgame (the last two moves are both passes) means the robot is
// bound to pass again: answer on the shortened schedule scaled to the human's
// last move, taken as a min so it is never slower than the normal think time.
if d := endgamePassDelay(rt.Seed, rt.MoveCount, rt.OppLastMove); d < delay {
delay = d
}
}
if now.Before(rt.TurnStartedAt.Add(delay)) {
last, ok, err := s.social.LastNudgeAt(ctx, rt.GameID, oppID)
if err != nil {
return err