diff options
11 files changed, 224 insertions, 159 deletions
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 a0688f20..a10ca152 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 @@ -7,6 +7,7 @@ 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.cards.HandCard import org.luxons.sevenwonders.model.resources.noTransactions import org.luxons.sevenwonders.model.wonders.AssignedWonder import org.slf4j.LoggerFactory @@ -79,19 +80,18 @@ class SevenWondersBot( private suspend fun autoPlayUntilEnd(currentTurn: PlayerTurnInfo) = coroutineScope { val endGameTurnInfo = async { - session.watchTurns().filter { it.action == Action.WATCH_SCORE }.first() + session.watchTurns().filter { it.action is TurnAction.WatchScore }.first() } session.watchTurns() .onStart { session.sayReady() emit(currentTurn) } - .takeWhile { it.action != Action.WATCH_SCORE } + .takeWhile { it.action !is TurnAction.WatchScore } .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)") + logger.info("BOT $player: playing turn (action ${turn.action})") session.autoPlayTurn(turn) } val lastTurn = endGameTurnInfo.await() @@ -113,17 +113,16 @@ class SevenWondersBot( } 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 + when (val action = turn.action) { + is TurnAction.PlayFromHand -> prepareMove(createPlayCardMove(turn, action.hand)) + is TurnAction.PlayFromDiscarded -> prepareMove(createPlayFreeDiscardedCardMove(action)) + is TurnAction.PickNeighbourGuild -> prepareMove(createPickGuildMove(action)) + is TurnAction.SayReady -> sayReady() + is TurnAction.Wait, is TurnAction.WatchScore -> Unit } } -private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { - val hand = turnInfo.hand ?: error("Cannot choose move, no hand in current turn info!") +private fun createPlayCardMove(turnInfo: PlayerTurnInfo, hand: List<HandCard>): PlayerMove { val wonderBuildability = turnInfo.wonderBuildability if (wonderBuildability.isBuildable) { val transactions = wonderBuildability.transactionsOptions.random() @@ -137,10 +136,10 @@ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): PlayerMove { } } -private fun createPlayFreeDiscardedCardMove(turnInfo: PlayerTurnInfo): PlayerMove { - val card = turnInfo.discardedCards?.random() ?: error("No discarded card to play") +private fun createPlayFreeDiscardedCardMove(action: TurnAction.PlayFromDiscarded): PlayerMove { + val card = action.discardedCards.random() return PlayerMove(MoveType.PLAY_FREE_DISCARDED, card.name) } -private fun createPickGuildMove(turnInfo: PlayerTurnInfo): PlayerMove = - PlayerMove(MoveType.COPY_GUILD, turnInfo.neighbourGuildCards.random().name) +private fun createPickGuildMove(action: TurnAction.PickNeighbourGuild): PlayerMove = + PlayerMove(MoveType.COPY_GUILD, action.neighbourGuildCards.random().name) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt index a1b3cb4e..ead54e96 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt @@ -1,71 +1,9 @@ package org.luxons.sevenwonders.model import kotlinx.serialization.Serializable -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.boards.RelativeBoardPosition -import org.luxons.sevenwonders.model.cards.HandCard import org.luxons.sevenwonders.model.cards.TableCard import org.luxons.sevenwonders.model.resources.ResourceTransactions import org.luxons.sevenwonders.model.resources.noTransactions -import org.luxons.sevenwonders.model.score.ScoreBoard -import org.luxons.sevenwonders.model.wonders.WonderBuildability - -enum class Action(val message: String) { - SAY_READY("Click 'READY' when you are ready to receive your next hand."), - PLAY("Pick the card you want to play or discard."), - PLAY_2("Pick the first card you want to play or discard. Note that you have the ability to play these 2 last cards. You will choose how to play the last one during your next turn."), - PLAY_LAST("You have the special ability to play your last card. Choose how you want to play it."), - PLAY_FREE_DISCARDED("Pick a card from the discarded deck, you can play it for free (but you cannot discard for 3 gold coins or upgrade your wonder with it)."), - PICK_NEIGHBOR_GUILD("Choose a Guild card (purple) that you want to copy from one of your neighbours."), - WAIT("Please wait for other players to perform extra actions."), - WATCH_SCORE("The game is over! Look at the scoreboard to see the final ranking!"); - - fun allowsBuildingWonder(): Boolean = when (this) { - PLAY, PLAY_2, PLAY_LAST -> true - else -> false - } - - fun allowsDiscarding(): Boolean = when (this) { - PLAY, PLAY_2, PLAY_LAST -> true - else -> false - } -} - -@Serializable -data class PlayerTurnInfo( - val playerIndex: Int, - val table: TableState, - val action: Action, - val hand: List<HandCard>?, - val neighbourGuildCards: List<HandCard>, - val discardedCards: List<HandCard>?, // only present when the player can actually see them - val scoreBoard: ScoreBoard? = null, -) { - val currentAge: Int = table.currentAge - val message: String = action.message - val wonderBuildability: WonderBuildability = table.boards[playerIndex].wonder.buildability - - val RelativeBoardPosition.index: Int - get() = getIndexFrom(playerIndex, table.boards.size) -} - -fun PlayerTurnInfo.getOwnBoard(): Board = table.boards[playerIndex] - -fun PlayerTurnInfo.getBoard(position: RelativeBoardPosition): Board = table.boards[position.index] - -fun PlayerTurnInfo.getNonNeighbourBoards(): List<Board> { - val nPlayers = table.boards.size - if (nPlayers <= 3) { - return emptyList() - } - val first = (playerIndex + 2) % nPlayers - val last = (playerIndex - 2 + nPlayers) % nPlayers - val range = if (first <= last) first..last else ((first until nPlayers) + (0..last)) - return range.map { table.boards[it] } -} - -// TODO move to server code -fun Collection<PlayerTurnInfo>.hideHandsAndWaitForReadiness() = map { it.copy(action = Action.SAY_READY, hand = null) } @Serializable data class PlayedMove( diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/PlayerState.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/PlayerState.kt new file mode 100644 index 00000000..271b2a99 --- /dev/null +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/PlayerState.kt @@ -0,0 +1,38 @@ +package org.luxons.sevenwonders.model + +import kotlinx.serialization.Serializable +import org.luxons.sevenwonders.model.boards.Board +import org.luxons.sevenwonders.model.boards.RelativeBoardPosition +import org.luxons.sevenwonders.model.wonders.WonderBuildability + +@Serializable +data class PlayerTurnInfo( + val playerIndex: Int, + val table: TableState, + val action: TurnAction, +) { + val currentAge: Int = table.currentAge + val message: String = action.message + val wonderBuildability: WonderBuildability = table.boards[playerIndex].wonder.buildability + + val RelativeBoardPosition.index: Int + get() = getIndexFrom(playerIndex, table.boards.size) +} + +fun PlayerTurnInfo.getOwnBoard(): Board = table.boards[playerIndex] + +fun PlayerTurnInfo.getBoard(position: RelativeBoardPosition): Board = table.boards[position.index] + +fun PlayerTurnInfo.getNonNeighbourBoards(): List<Board> { + val nPlayers = table.boards.size + if (nPlayers <= 3) { + return emptyList() + } + val first = (playerIndex + 2) % nPlayers + val last = (playerIndex - 2 + nPlayers) % nPlayers + val range = if (first <= last) first..last else ((first until nPlayers) + (0..last)) + return range.map { table.boards[it] } +} + +// TODO move to server code +fun Collection<PlayerTurnInfo>.hideHandsAndWaitForReadiness() = map { it.copy(action = TurnAction.SayReady()) } diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/TurnActions.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/TurnActions.kt new file mode 100644 index 00000000..bd94944b --- /dev/null +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/TurnActions.kt @@ -0,0 +1,74 @@ +package org.luxons.sevenwonders.model + +import kotlinx.serialization.Serializable +import org.luxons.sevenwonders.model.cards.HandCard +import org.luxons.sevenwonders.model.score.ScoreBoard + +object ActionMessages { + const val SAY_READY = "Click 'READY' when you are ready to receive your next hand." + const val PLAY = "Pick the card you want to play or discard. The rest of the cards will be passed on to the next player." + const val PLAY_LAST = "Pick the card you want to play or discard. The remaining card will be discarded." + const val PLAY_2 = "Pick the first card you want to play or discard. Note that you have the ability to play these 2 last cards. You will choose how to play the last one during your next turn." + const val PLAY_LAST_SPECIAL = "You have the special ability to play your last card. Choose how you want to play it." + const val PLAY_FREE_DISCARDED = "Pick a card from the discarded deck, you can play it for free (but you cannot discard for 3 gold coins or upgrade your wonder with it)." + const val PICK_NEIGHBOR_GUILD = "Choose a Guild card (purple) that you want to copy from one of your neighbours." + const val WAIT_OTHER_PLAY_LAST = "Another player can play his last card, please wait…" + const val WAIT_OTHER_PICK_GUILD = "Another player is picking a guild card to copy, please wait…" + const val WAIT_OTHER_PLAY_DISCARD = "Another player is playing a free card from the discard pile, please wait…" + const val WATCH_SCORE = "The game is over! Look at the scoreboard to see the final ranking!" +} + +@Serializable +sealed class TurnAction { + abstract val message: String + + @Serializable + data class SayReady( + override val message: String = ActionMessages.SAY_READY, + ) : TurnAction() + + @Serializable + data class PlayFromHand( + override val message: String, + val hand: List<HandCard>, + ) : TurnAction() { + override fun toString(): String = "${super.toString()} (${hand.size} cards)" + } + + @Serializable + data class PlayFromDiscarded( + val discardedCards: List<HandCard>, + ) : TurnAction() { + override val message: String = ActionMessages.PLAY_FREE_DISCARDED + + override fun toString(): String = "${super.toString()} (${discardedCards.size} cards)" + } + + @Serializable + data class PickNeighbourGuild( + val neighbourGuildCards: List<HandCard>, + ) : TurnAction() { + override val message: String = ActionMessages.PICK_NEIGHBOR_GUILD + + override fun toString(): String = "${super.toString()} (${neighbourGuildCards.size} cards)" + } + + @Serializable + data class Wait( + override val message: String, + ) : TurnAction() + + @Serializable + data class WatchScore( + override val message: String = ActionMessages.WATCH_SCORE, + val scoreBoard: ScoreBoard, + ) : TurnAction() { + override fun toString(): String = "${super.toString()} (winner: Player #${scoreBoard.scores[0].playerIndex})" + } + + fun allowsBuildingWonder(): Boolean = this is PlayFromHand + + fun allowsDiscarding(): Boolean = this is PlayFromHand + + override fun toString(): String = this::class.simpleName!! +} diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt index ed58250f..77b67f9d 100644 --- a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt +++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt @@ -42,69 +42,71 @@ class Game internal constructor( } private fun startNewTurn() { - val newTableState = table.toTableState() currentTurnInfo = players.map { player -> val hand = hands.createHand(player) - val action = determineAction(hand, player.board) - createPlayerTurnInfo(player, action, hand, newTableState) + val action = if (hand.isEmpty()) { + TurnAction.Wait(message = ActionMessages.WAIT_OTHER_PLAY_LAST) + } else { + TurnAction.PlayFromHand(message = actionMessage(hand, player), hand = hand) + } + PlayerTurnInfo(playerIndex = player.index, table = table.toTableState(), action = action) } } + private fun actionMessage(hand: List<HandCard>, player: Player) = when { + hand.size == 1 && player.board.hasSpecial(SpecialAbility.PLAY_LAST_CARD) -> ActionMessages.PLAY_LAST_SPECIAL + hand.size == 2 && player.board.hasSpecial(SpecialAbility.PLAY_LAST_CARD) -> ActionMessages.PLAY_2 + hand.size == 2 -> ActionMessages.PLAY_LAST + else -> ActionMessages.PLAY + } + private fun startPlayDiscardedTurn() { val newTableState = table.toTableState() currentTurnInfo = players.map { player -> val action = when { - player.board.hasSpecial(SpecialAbility.PLAY_DISCARDED) -> Action.PLAY_FREE_DISCARDED - else -> Action.WAIT + player.board.hasSpecial(SpecialAbility.PLAY_DISCARDED) -> TurnAction.PlayFromDiscarded( + discardedCards = discardedCards.toHandCards(player, true), + ) + else -> TurnAction.Wait(message = ActionMessages.WAIT_OTHER_PLAY_DISCARD) } - createPlayerTurnInfo(player, action, null, newTableState) + PlayerTurnInfo(playerIndex = player.index, table = newTableState, action = action) } } - private fun createPlayerTurnInfo(player: Player, action: Action, hand: List<HandCard>?, newTableState: TableState) = - PlayerTurnInfo( - playerIndex = player.index, - table = newTableState, - action = action, - hand = hand, - discardedCards = discardedCards.toHandCards(player, true).takeIf { action == Action.PLAY_FREE_DISCARDED }, - neighbourGuildCards = table.getNeighbourGuildCards(player.index).toHandCards(player, true), - ) - - private fun startEndGameTurn() { + private fun startEndGameActionsTurn() { // some player may need to do additional stuff - startNewTurn() - val allDone = currentTurnInfo.all { it.action == Action.WAIT } - if (!allDone) { + currentTurnInfo = players.map { player -> + PlayerTurnInfo( + playerIndex = player.index, + table = table.toTableState(), + action = computeEndGameAction(player), + ) + } + val someSpecialActions = currentTurnInfo.any { it.action !is TurnAction.Wait } + if (someSpecialActions) { return // we play the last turn like a normal one } val scoreBoard = computeScore() currentTurnInfo = currentTurnInfo.map { - it.copy(action = Action.WATCH_SCORE, scoreBoard = scoreBoard) + it.copy(action = TurnAction.WatchScore(message = ActionMessages.WATCH_SCORE, scoreBoard = scoreBoard)) } } + private fun computeEndGameAction(player: Player): TurnAction { + val guilds = table.getNeighbourGuildCards(player.index).toHandCards(player, true) + return when { + player.canCopyGuild() && guilds.isEmpty() -> TurnAction.PickNeighbourGuild(guilds) + else -> TurnAction.Wait(ActionMessages.WAIT_OTHER_PICK_GUILD) + } + } + + private fun Player.canCopyGuild() = board.hasSpecial(SpecialAbility.COPY_GUILD) && board.copiedGuild == null + /** * Returns information for each player about the current turn. */ fun getCurrentTurnInfo(): List<PlayerTurnInfo> = currentTurnInfo - private fun determineAction(hand: List<HandCard>, board: Board): Action = when { - endOfGameReached() -> when { - board.hasSpecial(SpecialAbility.COPY_GUILD) && board.copiedGuild == null -> determineCopyGuildAction(board) - else -> Action.WAIT - } - hand.size == 1 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD) -> Action.PLAY_LAST - hand.size == 2 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD) -> Action.PLAY_2 - hand.isEmpty() -> Action.WAIT - else -> Action.PLAY - } - - private fun determineCopyGuildAction(board: Board): Action { - val neighbourGuildCards = table.getNeighbourGuildCards(board.playerIndex) - return if (neighbourGuildCards.isEmpty()) Action.WAIT else Action.PICK_NEIGHBOR_GUILD - } - /** * Prepares the given [move] for the player at the given [playerIndex]. * @@ -132,7 +134,7 @@ class Game internal constructor( * ready to [play the current turn][playTurn]. */ fun allPlayersPreparedTheirMove(): Boolean { - val nbExpectedMoves = currentTurnInfo.count { it.action !== Action.WAIT } + val nbExpectedMoves = currentTurnInfo.count { it.action !is TurnAction.Wait } return preparedMoves.size == nbExpectedMoves } @@ -145,7 +147,6 @@ class Game internal constructor( fun playTurn() { makeMoves() - // same goes for the discarded cards during the last turn, which should be available for special actions if (hands.maxOneCardRemains()) { discardLastCardsIfRelevant() } @@ -153,9 +154,9 @@ class Game internal constructor( if (shouldStartPlayDiscardedTurn()) { startPlayDiscardedTurn() } else if (endOfAgeReached()) { - executeEndOfAgeEvents() + resolveMilitaryConflicts() if (endOfGameReached()) { - startEndGameTurn() + startEndGameActionsTurn() } else { startNewAge() } @@ -178,14 +179,14 @@ class Game internal constructor( } private fun getMovesToPerform(): List<Move> = - currentTurnInfo.filter { it.action !== Action.WAIT }.map { getMoveToPerformFor(it.playerIndex) } + currentTurnInfo.filter { it.action !is TurnAction.Wait }.map { getMoveToPerformFor(it.playerIndex) } private fun getMoveToPerformFor(playerIndex: Int) = preparedMoves[playerIndex] ?: throw MissingPreparedMoveException(playerIndex) private fun endOfAgeReached(): Boolean = hands.areEmpty - private fun executeEndOfAgeEvents() { + private fun resolveMilitaryConflicts() { // this is necessary because this method is actually called twice in the 3rd age if someone has CPY_GUILD // TODO we should instead manage the game's state machine in a better way to avoid stuff like this if (!militaryConflictsResolved) { diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt index a2e75a5f..09cdd970 100644 --- a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt +++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt @@ -32,9 +32,10 @@ class GameTest { val turnInfos = game.getCurrentTurnInfo() assertEquals(nbPlayers, turnInfos.size) turnInfos.forEach { - assertEquals(Action.WATCH_SCORE, it.action) + val action = it.action + assertTrue(action is TurnAction.WatchScore) - val scoreBoard = it.scoreBoard + val scoreBoard = action.scoreBoard assertNotNull(scoreBoard) assertEquals(nbPlayers, scoreBoard.scores.size) } @@ -51,7 +52,7 @@ class GameTest { } while (!game.getCurrentTurnInfo().first().isStartOfAge(age + 1)) } - private fun PlayerTurnInfo.isStartOfAge(age: Int) = action == Action.WATCH_SCORE || currentAge == age + private fun PlayerTurnInfo.isStartOfAge(age: Int) = action is TurnAction.WatchScore || currentAge == age private fun playTurn(nbPlayers: Int, game: Game, ageToCheck: Int) { val turnInfos = game.getCurrentTurnInfo() @@ -71,30 +72,31 @@ class GameTest { assertEquals(expectedMoves, game.getCurrentTurnInfo()[0].table.lastPlayedMoves) } - private fun PlayerTurnInfo.firstAvailableMove(): MoveExpectation? = when (action) { - Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> createPlayCardMove(this) - Action.PLAY_FREE_DISCARDED -> createPlayFreeDiscardedCardMove(this) - Action.PICK_NEIGHBOR_GUILD -> createPickGuildMove(this) - Action.WAIT, Action.SAY_READY -> null - Action.WATCH_SCORE -> fail("should not have WATCH_SCORE action before end of game") + private fun PlayerTurnInfo.firstAvailableMove(): MoveExpectation? = when (val a = action) { + is TurnAction.PlayFromHand -> createPlayCardMove(this, a.hand) + is TurnAction.PlayFromDiscarded -> createPlayFreeDiscardedCardMove(this, a.discardedCards) + is TurnAction.PickNeighbourGuild -> createPickGuildMove(this, a.neighbourGuildCards) + is TurnAction.SayReady, + is TurnAction.Wait -> null + is TurnAction.WatchScore -> fail("should not have WATCH_SCORE action before end of game") } - private fun createPlayCardMove(turnInfo: PlayerTurnInfo): MoveExpectation { + private fun createPlayCardMove(turnInfo: PlayerTurnInfo, hand: List<HandCard>): MoveExpectation { val wonderBuildability = turnInfo.wonderBuildability if (wonderBuildability.isBuildable) { val transactions = wonderBuildability.transactionsOptions.first() - return planMove(turnInfo, MoveType.UPGRADE_WONDER, turnInfo.hand!!.first(), transactions) + return planMove(turnInfo, MoveType.UPGRADE_WONDER, hand.first(), transactions) } - val playableCard = turnInfo.hand!!.firstOrNull { it.playability.isPlayable } + val playableCard = hand.firstOrNull { it.playability.isPlayable } return if (playableCard != null) { planMove(turnInfo, MoveType.PLAY, playableCard, playableCard.playability.transactionOptions.first()) } else { - planMove(turnInfo, MoveType.DISCARD, turnInfo.hand!!.first(), noTransactions()) + planMove(turnInfo, MoveType.DISCARD, hand.first(), noTransactions()) } } - private fun createPlayFreeDiscardedCardMove(turn: PlayerTurnInfo): MoveExpectation { - val card = turn.discardedCards?.random() ?: error("No discarded card to play") + private fun createPlayFreeDiscardedCardMove(turn: PlayerTurnInfo, discardedCards: List<HandCard>): MoveExpectation { + val card = discardedCards.random() return MoveExpectation( turn.playerIndex, PlayerMove(MoveType.PLAY_FREE_DISCARDED, card.name, noTransactions()), @@ -113,9 +115,7 @@ class GameTest { PlayedMove(turnInfo.playerIndex, moveType, card.toPlayedCard(), transactions), ) - private fun createPickGuildMove(turnInfo: PlayerTurnInfo): MoveExpectation { - val neighbourGuilds = turnInfo.neighbourGuildCards - + private fun createPickGuildMove(turnInfo: PlayerTurnInfo, neighbourGuilds: List<HandCard>): MoveExpectation { // the game should send action WAIT if no guild cards are available around assertFalse(neighbourGuilds.isEmpty()) val card = neighbourGuilds.first() diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/AutoGameController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/AutoGameController.kt index a4aceb96..b6d66e4b 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/AutoGameController.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/AutoGameController.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.withTimeout import org.luxons.sevenwonders.bot.connectBot import org.luxons.sevenwonders.bot.connectBots import org.luxons.sevenwonders.client.SevenWondersClient +import org.luxons.sevenwonders.model.TurnAction import org.luxons.sevenwonders.model.api.AutoGameAction import org.luxons.sevenwonders.model.api.AutoGameResult import org.slf4j.LoggerFactory @@ -48,7 +49,8 @@ class AutoGameController( lastTurn } - val scoreBoard = lastTurn.scoreBoard ?: error("Last turn info doesn't have scoreboard") + val turnAction = lastTurn.action as? TurnAction.WatchScore ?: error("Last turn action should be to watch the score") + val scoreBoard = turnAction.scoreBoard return AutoGameResult(scoreBoard, lastTurn.table) } diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt index df40b93d..7f05688b 100644 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.junit.runner.RunWith import org.luxons.sevenwonders.client.* -import org.luxons.sevenwonders.model.Action +import org.luxons.sevenwonders.model.TurnAction import org.luxons.sevenwonders.model.api.GameListEvent import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.server.test.runAsyncTest @@ -15,7 +15,10 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment import org.springframework.boot.web.server.LocalServerPort import org.springframework.test.context.junit4.SpringRunner -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue @RunWith(SpringRunner::class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -129,13 +132,13 @@ class SevenWondersTest { ).forEach { (session, startEvents) -> launch { val initialReadyTurn = startEvents.first() - assertEquals(Action.SAY_READY, initialReadyTurn.action) - assertNull(initialReadyTurn.hand) + assertTrue(initialReadyTurn.action is TurnAction.SayReady) val turns = session.watchTurns() session.sayReady() val firstActualTurn = turns.first() - assertNotNull(firstActualTurn.hand) + val action = firstActualTurn.action + assertTrue(action is TurnAction.PlayFromHand) session.disconnect() } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index ccbb2958..ed1c14d1 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -61,8 +61,9 @@ private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, RSta +GameStyles.pulsatingRed } } - turnInfo.scoreBoard?.let { - scoreTableOverlay(it, props.players, props.leaveGame) + val action = turnInfo.action + if (action is TurnAction.WatchScore) { + scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) } actionInfo(turnInfo.message) boardComponent(board = board) { @@ -86,7 +87,7 @@ private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, RSta if (card != null && move != null) { preparedMove(card, move) } - if (turnInfo.action == Action.SAY_READY) { + if (turnInfo.action is TurnAction.SayReady) { sayReadyButton() } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt index 8800c92e..9eab3553 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -37,7 +37,7 @@ interface HandProps : RProps { class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { override fun RBuilder.render() { - val hand = props.turnInfo.cardsToPlay() ?: return + val hand = props.turnInfo.action.cardsToPlay() ?: return styledDiv { css { handStyle() @@ -52,11 +52,13 @@ class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { } } - private fun PlayerTurnInfo.cardsToPlay(): List<HandCard>? = when (action) { - Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> hand - Action.PLAY_FREE_DISCARDED -> discardedCards - Action.PICK_NEIGHBOR_GUILD -> neighbourGuildCards - Action.WAIT, Action.WATCH_SCORE, Action.SAY_READY -> null + private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { + is TurnAction.PlayFromHand -> hand + is TurnAction.PlayFromDiscarded -> discardedCards + is TurnAction.PickNeighbourGuild -> neighbourGuildCards + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> null } private fun RBuilder.handCard( @@ -94,15 +96,17 @@ class HandComponent(props: HandProps) : RComponent<HandProps, RState>(props) { bpButtonGroup { val action = props.turnInfo.action when (action) { - Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> { + is TurnAction.PlayFromHand -> { playCardButton(card, HandAction.PLAY) if (props.turnInfo.getOwnBoard().canPlayAnyCardForFree) { playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE) } } - Action.PLAY_FREE_DISCARDED -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED) - Action.PICK_NEIGHBOR_GUILD -> playCardButton(card, HandAction.COPY_GUILD) - else -> error("unsupported action in hand card: $action") + is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED) + is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD) + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> error("unsupported action in hand card: $action") } if (action.allowsBuildingWonder()) { diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt index 91dbf61f..5d345136 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt @@ -3,6 +3,7 @@ package org.luxons.sevenwonders.ui.redux import org.luxons.sevenwonders.model.MoveType import org.luxons.sevenwonders.model.PlayerMove import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.model.TurnAction import org.luxons.sevenwonders.model.api.ConnectedPlayer import org.luxons.sevenwonders.model.api.GameListEvent import org.luxons.sevenwonders.model.api.LobbyDTO @@ -32,10 +33,14 @@ data class GameState( val turnInfo: PlayerTurnInfo?, val preparedCardsByUsername: Map<String, CardBack?> = emptyMap(), val currentPreparedMove: PlayerMove? = null, + // UI val transactionSelector: TransactionSelectorState? = null, ) { val currentPreparedCard: HandCard? - get() = turnInfo?.hand?.firstOrNull { it.name == currentPreparedMove?.cardName } + get() { + val hand = (turnInfo?.action as? TurnAction.PlayFromHand)?.hand + return hand?.firstOrNull { it.name == currentPreparedMove?.cardName } + } } data class TransactionSelectorState( |