summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--settings.gradle.kts1
-rw-r--r--sw-bot/build.gradle.kts11
-rw-r--r--sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt88
-rw-r--r--sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt8
-rw-r--r--sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt11
-rw-r--r--sw-server/build.gradle.kts3
-rw-r--r--sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt16
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt23
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt2
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt4
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(
bgstack15