diff options
author | Joffrey Bion <joffrey.bion@booking.com> | 2020-05-15 03:05:09 +0200 |
---|---|---|
committer | Joffrey Bion <joffrey.bion@booking.com> | 2020-05-15 03:29:28 +0200 |
commit | 07295f3e96a16efc517e5a5e6ae0af4e3d5c5035 (patch) | |
tree | 3d35ea3d249d6f1205700d73996f9d93b6582162 | |
parent | Send score at end of game (diff) | |
download | seven-wonders-07295f3e96a16efc517e5a5e6ae0af4e3d5c5035.tar.gz seven-wonders-07295f3e96a16efc517e5a5e6ae0af4e3d5c5035.tar.bz2 seven-wonders-07295f3e96a16efc517e5a5e6ae0af4e3d5c5035.zip |
Add dumb bots feature
10 files changed, 163 insertions, 4 deletions
diff --git a/settings.gradle.kts b/settings.gradle.kts index e4b3c342..b312e0b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,3 +5,4 @@ include("sw-engine") include("sw-server") include("sw-client") include("sw-ui") +include("sw-bot") diff --git a/sw-bot/build.gradle.kts b/sw-bot/build.gradle.kts new file mode 100644 index 00000000..fd4de5e0 --- /dev/null +++ b/sw-bot/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + kotlin("jvm") + id("org.jlleitschuh.gradle.ktlint") +} + +dependencies { + implementation(project(":sw-client")) + implementation(kotlin("stdlib-jdk8")) + testImplementation(kotlin("test")) + testImplementation(kotlin("test-junit")) +} 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 new file mode 100644 index 00000000..deb7e237 --- /dev/null +++ b/sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt @@ -0,0 +1,88 @@ +package org.luxons.sevenwonders.bot + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import org.luxons.sevenwonders.client.SevenWondersClient +import org.luxons.sevenwonders.client.SevenWondersSession +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.model.resources.noTransactions +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 +) + +@OptIn(ExperimentalTime::class) +class SevenWondersBot( + private val displayName: String, + private val botConfig: BotConfig = BotConfig() +) { + private val client = SevenWondersClient() + + suspend fun play(serverHost: String, gameId: Long) = withTimeout(botConfig.globalTimeout) { + val session = client.connect(serverHost) + botDelay() + session.chooseName(displayName) + botDelay() + session.joinGame(gameId) + botDelay() + session.awaitGameStart(gameId) + + coroutineScope { + launch { + session.watchTurns().first { turn -> + botDelay() + val keepPlaying = session.playTurn(turn) + !keepPlaying + } + } + botDelay() + session.sayReady() // triggers the first turn + } + session.disconnect() + } + + private suspend fun botDelay() { + delay(Random.nextLong(botConfig.minActionDelayMillis, botConfig.maxActionDelayMillis)) + } + + private suspend fun SevenWondersSession.playTurn(turn: PlayerTurnInfo): Boolean { + when (turn.action) { + Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> prepareMove(createPlayCardMove(turn)) + Action.PICK_NEIGHBOR_GUILD -> prepareMove(createPickGuildMove(turn)) + Action.SAY_READY -> sayReady() + Action.WAIT -> Unit + Action.WATCH_SCORE -> return false + } + return true + } +} + +private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { + val wonderBuildability = turnInfo.wonderBuildability + if (wonderBuildability.isBuildable) { + val transactions = wonderBuildability.cheapestTransactions.first() + return PlayerMove(MoveType.UPGRADE_WONDER, turnInfo.hand!!.first().name, transactions) + } + val playableCard = turnInfo.hand!!.firstOrNull { it.playability.isPlayable } + return if (playableCard != null) { + PlayerMove(MoveType.PLAY, playableCard.name, playableCard.playability.cheapestTransactions.first()) + } else { + PlayerMove(MoveType.DISCARD, turnInfo.hand!!.first().name, noTransactions()) + } +} + +private fun createPickGuildMove(turnInfo: PlayerTurnInfo): PlayerMove = + PlayerMove(MoveType.COPY_GUILD, turnInfo.neighbourGuildCards.first().name) diff --git a/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt index 480e9d67..e13ab505 100644 --- a/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt +++ b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt @@ -18,6 +18,7 @@ import org.luxons.sevenwonders.model.PlayerTurnInfo import org.luxons.sevenwonders.model.api.ConnectedPlayer import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.SEVEN_WONDERS_WS_ENDPOINT +import org.luxons.sevenwonders.model.api.actions.AddBotAction import org.luxons.sevenwonders.model.api.actions.ChooseNameAction import org.luxons.sevenwonders.model.api.actions.CreateGameAction import org.luxons.sevenwonders.model.api.actions.JoinGameAction @@ -86,6 +87,10 @@ class SevenWondersSession(private val stompSession: StompSessionWithKxSerializat stompSession.sendEmptyMsg("/app/lobby/leave") } + suspend fun addBot(displayName: String) { + stompSession.convertAndSend("/app/lobby/addBot", AddBotAction(displayName), AddBotAction.serializer()) + } + suspend fun reorderPlayers(players: List<String>) { stompSession.convertAndSend("/app/lobby/reorderPlayers", ReorderPlayersAction(players), ReorderPlayersAction.serializer()) } @@ -133,7 +138,4 @@ class SevenWondersSession(private val stompSession: StompSessionWithKxSerializat suspend fun unprepareMove() { stompSession.sendEmptyMsg("/app/game/unprepareMove") } - -// suspend fun watchGameEnd() { -// } } diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt index 773e703f..a4467667 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt @@ -70,3 +70,14 @@ class UpdateSettingsAction( */ val settings: CustomizableSettings ) + +/** + * The action to add a bot to the game. Can only be called in the lobby by the owner of the game. + */ +@Serializable +class AddBotAction( + /** + * The display name for the bot to add. + */ + val botDisplayName: String +) diff --git a/sw-server/build.gradle.kts b/sw-server/build.gradle.kts index 7d1ee0ae..66e70dbb 100644 --- a/sw-server/build.gradle.kts +++ b/sw-server/build.gradle.kts @@ -10,8 +10,10 @@ apply(plugin = "io.spring.dependency-management") dependencies { implementation(project(":sw-common-model")) implementation(project(":sw-engine")) + implementation(project(":sw-bot")) implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) // required by Spring 5 + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6") implementation("org.springframework.boot:spring-boot-starter-websocket") implementation("org.springframework.boot:spring-boot-starter-security") @@ -32,7 +34,6 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.hildan.jackstomp:jackstomp:2.0.0") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5") } // packages the frontend app within the jar diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt index 403a4696..cdb636fb 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt @@ -1,6 +1,8 @@ package org.luxons.sevenwonders.server.controllers import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.bot.SevenWondersBot +import org.luxons.sevenwonders.model.api.actions.AddBotAction import org.luxons.sevenwonders.model.api.actions.ReorderPlayersAction import org.luxons.sevenwonders.model.api.actions.UpdateSettingsAction import org.luxons.sevenwonders.model.hideHandsAndWaitForReadiness @@ -16,6 +18,8 @@ import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Controller import org.springframework.validation.annotation.Validated import java.security.Principal +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch /** * Handles actions in the game's lobby. The lobby is the place where players gather before a game. @@ -78,6 +82,18 @@ class LobbyController @Autowired constructor( sendLobbyUpdateToPlayers(lobby) } + @MessageMapping("/lobby/addBot") + fun addBot(@Validated action: AddBotAction, principal: Principal) { + val lobby = principal.player.ownedLobby + val bot = SevenWondersBot(action.botDisplayName) + GlobalScope.launch { + bot.play("localhost:8000", lobby.id) + } + + logger.info("Added bot {} to game '{}'", action.botDisplayName, lobby.name) + sendLobbyUpdateToPlayers(lobby) + } + internal fun sendLobbyUpdateToPlayers(lobby: Lobby) { lobby.getPlayers().forEach { template.convertAndSendToUser(it.username, "/queue/lobby/updated", lobby.toDTO()) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt index d015e2a2..2dc36ef9 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt @@ -4,6 +4,7 @@ import com.palantir.blueprintjs.Intent import com.palantir.blueprintjs.bpButton import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.ui.redux.RequestAddBot import org.luxons.sevenwonders.ui.redux.RequestLeaveLobby import org.luxons.sevenwonders.ui.redux.RequestStartGame import org.luxons.sevenwonders.ui.redux.connectStateAndDispatch @@ -20,6 +21,7 @@ interface LobbyStateProps : RProps { interface LobbyDispatchProps : RProps { var startGame: () -> Unit + var addBot: (displayName: String) -> Unit var leaveLobby: () -> Unit } @@ -39,6 +41,7 @@ class LobbyPresenter(props: LobbyProps) : RComponent<LobbyProps, RState>(props) radialPlayerList(currentGame.players, currentPlayer) if (currentPlayer.isGameOwner) { startButton(currentGame, currentPlayer) + addBotButton() } else { leaveButton() } @@ -59,6 +62,25 @@ class LobbyPresenter(props: LobbyProps) : RComponent<LobbyProps, RState>(props) } } + private fun RBuilder.addBotButton() { + bpButton( + large = true, + intent = Intent.NONE, + icon = "plus", + rightIcon = "desktop", + title = "Add a bot to this game", + onClick = { addBot() } + ) { + +"ADD BOT" + } + } + + private fun addBot() { + val name = listOf("Bob", "Jack", "John", "Boris", "HAL", "GLaDOS").random() +// val botName = "\uD83E\uDD16 $name" + props.addBot(name) + } + private fun RBuilder.leaveButton() { bpButton( large = true, @@ -82,6 +104,7 @@ private val lobby = connectStateAndDispatch<LobbyStateProps, LobbyDispatchProps, }, mapDispatchToProps = { dispatch, _ -> startGame = { dispatch(RequestStartGame()) } + addBot = { name -> dispatch(RequestAddBot(name)) } leaveLobby = { dispatch(RequestLeaveLobby()) } } ) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt index eef77585..300693a3 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt @@ -10,6 +10,8 @@ data class RequestCreateGame(val gameName: String) : RAction data class RequestJoinGame(val gameId: Long) : RAction +data class RequestAddBot(val botDisplayName: String) : RAction + data class RequestReorderPlayers(val orderedPlayers: List<String>) : RAction data class RequestUpdateSettings(val settings: CustomizableSettings) : RAction diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt index e2bf82c5..d6d41881 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.ui.redux.EnterGameAction +import org.luxons.sevenwonders.ui.redux.RequestAddBot import org.luxons.sevenwonders.ui.redux.RequestLeaveLobby import org.luxons.sevenwonders.ui.redux.RequestStartGame import org.luxons.sevenwonders.ui.redux.UpdateLobbyAction @@ -19,6 +20,9 @@ suspend fun SwSagaContext.lobbySaga(session: SevenWondersSession) { .map { UpdateLobbyAction(it) } .dispatchAllIn(this) + launch { + onEach<RequestAddBot> { session.addBot(it.botDisplayName) } + } val startGameJob = launch { awaitStartGame(session) } awaitFirst( |