feat(bot): clock-aware time management and stream-driven tournament play
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Tournament bots flagged in 5+3 classical: budgets were fixed and
clock-blind, HybridBot's veto re-search double-spent (up to 4s/move),
and the game loop polled every 1s, burning our clock waiting on the
opponent.
- Bot is now a trait taking a TimeControl (remaining + increment);
apply(ctx) defaults to Unlimited so local/self-play/tests keep their
fixed budgets.
- TimeControl.budget derives a per-move budget from the real clock with
an overhead reserve, a panic mode under 20s, and a hard ceiling, so a
bot can no longer flag from thinking.
- HybridBot splits one budget across main (0.7) and veto (0.3) searches
instead of running two full searches.
- TournamentBotGamePlayer reads the server clock (seconds -> ms) and
plays stream-driven via GET /game/{id}/stream (NDJSON, heartbeat-kept),
so the opponent's move arrives instantly; polling stays as a fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,4 +3,33 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
type Bot = GameContext => Option[Move]
|
||||
/** Remaining wall-clock for the side to move and the Fischer increment, both in milliseconds. [[TimeControl.Unlimited]]
|
||||
* is the sentinel for callers without a real clock (local play, self-play, tests): bots then fall back to their own
|
||||
* fixed budgets.
|
||||
*/
|
||||
final case class TimeControl(remainingMs: Long, incrementMs: Long):
|
||||
def isClocked: Boolean = remainingMs >= 0L
|
||||
def budgetMs: Long = TimeControl.budget(remainingMs, incrementMs)
|
||||
|
||||
object TimeControl:
|
||||
val Unlimited: TimeControl = TimeControl(-1L, 0L)
|
||||
|
||||
private val OverheadMs = 1500L
|
||||
private val PanicMs = 20000L
|
||||
private val MaxBudget = 8000L
|
||||
private val PanicCap = 2500L
|
||||
private val FloorMs = 50L
|
||||
|
||||
def budget(remainingMs: Long, incrementMs: Long): Long =
|
||||
val usable = math.max(0L, remainingMs - OverheadMs)
|
||||
if usable <= 0L then FloorMs
|
||||
else if remainingMs < PanicMs then clamp(usable / 15 + incrementMs / 2, PanicCap)
|
||||
else clamp(usable / 30 + incrementMs * 4 / 5, MaxBudget)
|
||||
|
||||
private def clamp(value: Long, ceiling: Long): Long =
|
||||
math.max(FloorMs, math.min(value, ceiling))
|
||||
|
||||
trait Bot:
|
||||
def move(context: GameContext, time: TimeControl): Option[Move]
|
||||
def apply(context: GameContext): Option[Move] = move(context, TimeControl.Unlimited)
|
||||
def apply(context: GameContext, time: TimeControl): Option[Move] = move(context, time)
|
||||
|
||||
Reference in New Issue
Block a user