From 4651bb796f07a21bd013d9521b2dfe2e1078cebb Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 23 Jun 2026 10:37:28 +0200 Subject: [PATCH] fix(official-bots): play only own tournament games with correct color The tournament stream broadcasts a gameStart per color for every pairing in the round, without a player id. The bot latched the first color it saw and played games it was not part of, submitting moves for the wrong color that the server rejected. Now it fetches game detail and matches its botId against white/black to resolve its real color, skipping games it is not in. Co-Authored-By: Claude Opus 4.8 --- .../bot/service/TournamentBotGamePlayer.scala | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala index 98961c2..6677b6d 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala @@ -283,12 +283,35 @@ class TournamentBotGamePlayer: forEachLine(response.readEntity(classOf[InputStream])): line => parse(line).foreach: node => if node.path("type").asText() == "gameStart" then - onGameStart(cfg, node.path("gameId").asText(), node.path("color").asText()) + onGameStart(cfg, node.path("gameId").asText()) - private def onGameStart(cfg: TournamentBotConfig, gameId: String, color: String): Unit = - if gameId.nonEmpty && color.nonEmpty && activeGames.add(gameId) then - workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId, color) }) - () + private def onGameStart(cfg: TournamentBotConfig, gameId: String): Unit = + if gameId.isEmpty then () + else + resolveColor(cfg, gameId) match + case None => log.debugf("Ignoring game %s — bot %s is not a participant", gameId, cfg.botId) + case Some(color) => + if activeGames.add(gameId) then + workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId, color) }) + () + + private def resolveColor(cfg: TournamentBotConfig, gameId: String): Option[String] = + fetchGame(cfg, gameId).flatMap { node => + val whiteId = node.path("white").path("id").asText() + val blackId = node.path("black").path("id").asText() + if whiteId == cfg.botId then Some("white") + else if blackId == cfg.botId then Some("black") + else None + } + + private def fetchGame(cfg: TournamentBotConfig, gameId: String): Option[JsonNode] = + Try { + val response = authed(cfg, target(cfg).path("game").path(gameId)).get() + try + if response.getStatus == 200 then Some(objectMapper.readTree(response.readEntity(classOf[String]))) + else { log.warnf("Game detail %s returned status %d", gameId, response.getStatus); None } + finally response.close() + }.getOrElse(None) private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit = Try {