From eba0457b152d17b83e1eb27ef6de075a56aa02eb Mon Sep 17 00:00:00 2001 From: "Lala, Shahd" Date: Thu, 4 Jun 2026 20:22:38 +0000 Subject: [PATCH] feat: bot vs. player --- .../redis/GameRedisSubscriberManager.scala | 10 +++++++ .../chess/resource/GameResource.scala | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala index 41505e1..f21e162 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala @@ -117,6 +117,16 @@ class GameRedisSubscriberManager: log.debugf("Subscribed to game %s", gameId) catch case ex: Exception => log.warnf(ex, "Redis subscription failed for game %s", gameId) + // Notify the official-bots service to start playing a side of a game. Mirrors + // the event the tournament service publishes; official-bots subscribes to + // ":bot:*:events". + def publishBotGameStart(gameId: String, botId: String, playingAs: String): Unit = + val channel = s"${redisConfig.prefix}:bot:$botId:events" + val payload = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","botAccountId":"$botId"}""" + Try(redis.pubsub(classOf[String]).publish(channel, payload)) match + case scala.util.Failure(ex) => log.warnf(ex, "Failed to publish bot gameStart for game %s", gameId) + case scala.util.Success(_) => () + def unsubscribeGame(gameId: String): Unit = Option(c2sListeners.remove(gameId)).foreach { subscriber => subscriber.unsubscribe(c2sTopic(gameId)).subscribe().`with`(_ => (), _ => ()) diff --git a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala index 211cde5..60d8e1e 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala @@ -25,6 +25,7 @@ import de.nowchess.chess.observer.* import de.nowchess.chess.redis.GameRedisSubscriberManager import de.nowchess.chess.registry.{GameEntry, GameRegistry} import de.nowchess.security.InternalOnly +import jakarta.annotation.security.PermitAll import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject import jakarta.ws.rs.* @@ -179,6 +180,32 @@ class GameResource: ) created(GameDtoMapper.toGameFullDto(entry, ioClient)) + // Player-facing game creation for "play vs bot". Unlike createGame this is not + // internal-only: a logged-in (or anonymous) player creates the game directly, + // and core notifies the official-bots service to play the bot side. + @POST + @Path("/vs-bot") + @PermitAll + @Consumes(Array(MediaType.APPLICATION_JSON)) + @Produces(Array(MediaType.APPLICATION_JSON)) + def createBotGame(body: CreateGameRequestDto): Response = + val req = Option(body).getOrElse(CreateGameRequestDto(None, None, None, None)) + val white = playerInfoFrom(req.white, DefaultWhite) + val black = playerInfoFrom(req.black, DefaultBlack) + val tc = toTimeControl(req.timeControl) + val entry = newEntry(GameContext.initial, white, black, tc, GameMode.Open) + registry.store(entry) + subscriberManager.subscribeGame(entry.gameId) + notifyBotSide(entry) + log.infof("Bot game %s created — white=%s black=%s", entry.gameId, white.displayName, black.displayName) + created(GameDtoMapper.toGameFullDto(entry, ioClient)) + + private def notifyBotSide(entry: GameEntry): Unit = + if entry.black.id.value.startsWith("bot-") then + subscriberManager.publishBotGameStart(entry.gameId, entry.black.id.value, "black") + else if entry.white.id.value.startsWith("bot-") then + subscriberManager.publishBotGameStart(entry.gameId, entry.white.id.value, "white") + @GET @Path("/{gameId}") @Produces(Array(MediaType.APPLICATION_JSON))