diff --git a/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/GameDescriptor.kt b/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/GameDescriptor.kt index a721c30..50960ab 100644 --- a/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/GameDescriptor.kt +++ b/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/GameDescriptor.kt @@ -52,12 +52,17 @@ interface GameDescriptor { val botNames: List get() = DEFAULT_BOT_NAMES /** - * A fresh bot strategy for this room. Called once per room and shared by every bot-played seat, - * each with its own seeded [kotlin.random.Random], so the strategy must be stateless. + * A fresh bot strategy for this room's [config]. Called once per room and shared by every + * bot-played seat, each with its own seeded [kotlin.random.Random], so the strategy must be + * stateless. + * + * Takes the config because a bot is a function of the rules it plays under: a house rule that + * adds a card or a bid changes what a correct move is, and a bot built for the wrong ruleset + * misplays rather than failing loudly. * * Prefer a cheap heuristic bot: it runs on the server's single vCPU for every abandoned seat. */ - fun bot(): Strategy + fun bot(config: C): Strategy /** The rules for a lobby's negotiated [config] — the game's house-rule toggles applied. */ fun rulesFor(config: C): GameRules diff --git a/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/Room.kt b/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/Room.kt index 2a6e43a..496f308 100644 --- a/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/Room.kt +++ b/cardkit-server/src/main/kotlin/io/github/rotundtapir/cardkit/server/Room.kt @@ -65,10 +65,13 @@ class Room( ) { private val logger = LoggerFactory.getLogger("room") private val commands = Channel>(Channel.UNLIMITED) - private val bot: Strategy = descriptor.bot() private var lobbyConfig = initialConfig + // Built from the config the room was created with. Only the timeouts can change later + // (RoomCommand.Configure), never the house rules, so the bot never goes stale. + private val bot: Strategy = descriptor.bot(initialConfig) + // Read cross-thread (isPlaying/resumedState from the Ktor handler & /health), written on the actor. @Volatile private var phase = RoomPhase.LOBBY diff --git a/cardkit-server/src/test/kotlin/io/github/rotundtapir/cardkit/server/ToyGame.kt b/cardkit-server/src/test/kotlin/io/github/rotundtapir/cardkit/server/ToyGame.kt index a5ef10a..01f1753 100644 --- a/cardkit-server/src/test/kotlin/io/github/rotundtapir/cardkit/server/ToyGame.kt +++ b/cardkit-server/src/test/kotlin/io/github/rotundtapir/cardkit/server/ToyGame.kt @@ -110,7 +110,7 @@ object ToyDescriptor : GameDescriptor { override val stateSerializer = ToyState.serializer() override val configSerializer = ToyConfig.serializer() - override fun bot(): Strategy = + override fun bot(config: ToyConfig): Strategy = Strategy { view, random -> view.legalActions.ifEmpty { listOf(ToyAction.Move(1)) }.random(random) } override fun rulesFor(config: ToyConfig): GameRules = ToyRules(config.target)