summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sw-bot/src/main/kotlin/org/luxons/sevenwonders/bot/SevenWondersBot.kt31
-rw-r--r--sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/Moves.kt62
-rw-r--r--sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/PlayerState.kt38
-rw-r--r--sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/TurnActions.kt74
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/engine/Game.kt87
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/engine/GameTest.kt36
-rw-r--r--sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/AutoGameController.kt4
-rw-r--r--sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt13
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt7
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt24
-rw-r--r--sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt7
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(
bgstack15