From 7c2371766b940742f3986d7904d4c20a4127ea70 Mon Sep 17 00:00:00 2001 From: joffrey-bion Date: Wed, 3 Feb 2021 02:37:38 +0100 Subject: Add auto-game with bots only Resolves: https://github.com/joffrey-bion/seven-wonders/issues/82 --- sw-bot/build.gradle.kts | 2 +- .../org/luxons/sevenwonders/bot/SevenWondersBot.kt | 137 ++++++++++++++------- 2 files changed, 91 insertions(+), 48 deletions(-) (limited to 'sw-bot') diff --git a/sw-bot/build.gradle.kts b/sw-bot/build.gradle.kts index 64a03734..7c44ca16 100644 --- a/sw-bot/build.gradle.kts +++ b/sw-bot/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementation(project(":sw-client")) + api(project(":sw-client")) implementation(kotlin("stdlib-jdk8")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") diff --git a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt index 17619f6a..d1a95d4d 100644 --- a/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt +++ b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt @@ -1,74 +1,119 @@ package org.luxons.sevenwonders.bot -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withTimeout -import org.luxons.sevenwonders.client.SevenWondersClient -import org.luxons.sevenwonders.client.SevenWondersSession -import org.luxons.sevenwonders.client.joinGameAndWaitLobby -import org.luxons.sevenwonders.model.Action -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.client.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.api.ConnectedPlayer +import org.luxons.sevenwonders.model.api.actions.BotConfig import org.luxons.sevenwonders.model.api.actions.Icon import org.luxons.sevenwonders.model.resources.noTransactions +import org.luxons.sevenwonders.model.wonders.AssignedWonder import org.slf4j.LoggerFactory import kotlin.random.Random -import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.hours - -@OptIn(ExperimentalTime::class) -data class BotConfig( - val minActionDelayMillis: Long = 500, - val maxActionDelayMillis: Long = 1000, - val globalTimeout: Duration = 10.hours, -) private val logger = LoggerFactory.getLogger(SevenWondersBot::class.simpleName) -@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class) +suspend fun SevenWondersClient.connectBot( + serverUrl: String, + name: String, + config: BotConfig = BotConfig(), +): SevenWondersBot { + logger.info("Connecting new bot '$name' to $serverUrl") + val session = connect(serverUrl) + val player = session.chooseName(name, Icon("desktop"), isHuman = false) + return SevenWondersBot(player, config, session) +} + +suspend fun SevenWondersClient.connectBots( + serverUrl: String, + names: List, + config: BotConfig = BotConfig(), +): List = names.map { connectBot(serverUrl, it, config) } + +@OptIn(ExperimentalTime::class) class SevenWondersBot( - private val displayName: String, - private val botConfig: BotConfig = BotConfig(), + private val player: ConnectedPlayer, + private val config: BotConfig = BotConfig(), + private val session: SevenWondersSession, ) { - private val client = SevenWondersClient() + suspend fun createGameWithBotFriendsAndAutoPlay( + gameName: String, + otherBots: List, + customWonders: List? = null, + customSettings: Settings? = null, + ): PlayerTurnInfo { + val nJoinerBots = otherBots.size + require(nJoinerBots >= 2) { "At least 2 more bots must join the game" } + require(customWonders == null || customWonders.size == nJoinerBots + 1) { + "Custom wonders don't match the number of players in the game" + } + + val lobby = session.createGameAndWaitLobby(gameName) + otherBots.forEach { + it.session.joinGameAndWaitLobby(lobby.id) + } + + customWonders?.let { session.reassignWonders(it) } + customSettings?.let { session.updateSettings(it) } - suspend fun play(serverUrl: String, gameId: Long) = withTimeout(botConfig.globalTimeout) { - val session = client.connect(serverUrl) - val player = session.chooseName(displayName, Icon("desktop"), isHuman = false) - val gameStartedEvents = session.watchGameStarted() - session.joinGameAndWaitLobby(gameId) - val firstTurn = gameStartedEvents.first() + return withContext(Dispatchers.Default) { + otherBots.forEach { + launch { + val turn = it.session.watchGameStarted().first() + it.autoPlayUntilEnd(turn) + } + } + val firstTurn = session.startGameAndAwaitFirstTurn() + autoPlayUntilEnd(firstTurn) + } + } + + suspend fun joinAndAutoPlay(gameId: Long): PlayerTurnInfo { + val firstTurn = session.joinGameAndWaitFirstTurn(gameId) + return autoPlayUntilEnd(firstTurn) + } + private suspend fun autoPlayUntilEnd(currentTurn: PlayerTurnInfo) = coroutineScope { + val endGameTurnInfo = async { + session.watchTurns().filter { it.action == Action.WATCH_SCORE }.first() + } session.watchTurns() - .onStart { emit(firstTurn) } + .onStart { emit(currentTurn) } .takeWhile { it.action != Action.WATCH_SCORE } - .onCompletion { - session.leaveGame() - session.disconnect() - } + .catch { e -> logger.error("BOT $player: error in turnInfo flow", e) } .collect { turn -> botDelay() val shortTurnDescription = "action ${turn.action}, ${turn.hand?.size ?: 0} cards in hand" logger.info("BOT $player: playing turn ($shortTurnDescription)") - session.playTurn(turn) + session.autoPlayTurn(turn) } + val lastTurn = endGameTurnInfo.await() + logger.info("BOT $player: leaving the game") + session.leaveGame() + session.disconnect() + logger.info("BOT $player: disconnected") + lastTurn } private suspend fun botDelay() { - delay(Random.nextLong(botConfig.minActionDelayMillis, botConfig.maxActionDelayMillis)) + val timeMillis = if (config.minActionDelayMillis == config.maxActionDelayMillis) { + config.minActionDelayMillis + } else { + Random.nextLong(config.minActionDelayMillis, config.maxActionDelayMillis) + } + delay(timeMillis) } +} - private suspend fun SevenWondersSession.playTurn(turn: PlayerTurnInfo) { - when (turn.action) { - Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> prepareMove(createPlayCardMove(turn)) - Action.PLAY_FREE_DISCARDED -> prepareMove(createPlayFreeDiscardedCardMove(turn)) - Action.PICK_NEIGHBOR_GUILD -> prepareMove(createPickGuildMove(turn)) - Action.SAY_READY -> sayReady() - Action.WAIT, Action.WATCH_SCORE -> Unit - } +private suspend fun SevenWondersSession.autoPlayTurn(turn: PlayerTurnInfo) { + when (turn.action) { + Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> prepareMove(createPlayCardMove(turn)) + Action.PLAY_FREE_DISCARDED -> prepareMove(createPlayFreeDiscardedCardMove(turn)) + Action.PICK_NEIGHBOR_GUILD -> prepareMove(createPickGuildMove(turn)) + Action.SAY_READY -> sayReady() + Action.WAIT, Action.WATCH_SCORE -> Unit } } @@ -80,9 +125,7 @@ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { val transactions = wonderBuildability.transactionsOptions.random() return PlayerMove(MoveType.UPGRADE_WONDER, hand.random().name, transactions) } - val playableCard = hand - .filter { it.playability.isPlayable } - .randomOrNull() + val playableCard = hand.filter { it.playability.isPlayable }.randomOrNull() return if (playableCard != null) { PlayerMove(MoveType.PLAY, playableCard.name, playableCard.playability.transactionOptions.random()) } else { -- cgit