summaryrefslogtreecommitdiff
path: root/sw-engine
diff options
context:
space:
mode:
Diffstat (limited to 'sw-engine')
-rw-r--r--sw-engine/build.gradle.kts16
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt187
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Player.kt28
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Settings.kt29
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt81
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt31
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt19
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Board.kt98
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Military.kt33
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Science.kt56
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Table.kt58
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt64
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Decks.kt38
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Hands.kt37
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt83
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt38
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/GameDefinition.kt92
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/CardDefinition.kt25
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.kt34
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.kt34
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.kt29
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.kt38
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.kt47
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt58
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.kt26
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.kt30
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt26
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.kt55
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.kt36
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Discount.kt19
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Effect.kt15
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/EndGameEffect.kt9
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/GoldIncrease.kt8
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.kt14
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.kt8
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncrease.kt14
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncrease.kt8
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgress.kt9
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbility.kt39
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.kt10
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMove.kt23
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CardFromHandMove.kt15
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CopyGuildMove.kt37
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/DiscardMove.kt18
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt33
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayCardMove.kt20
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.kt25
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt121
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt66
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt29
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt87
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt24
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/score/Score.kt23
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt62
-rw-r--r--sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt31
-rw-r--r--sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json1462
-rw-r--r--sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/global_rules.json4
-rw-r--r--sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json473
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt123
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/BoardsKtTest.kt84
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/TableTest.kt71
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/BoardTest.kt211
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/MilitaryTest.kt57
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.kt45
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/ScienceTest.kt114
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardBackTest.kt14
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardTest.kt42
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/DecksTest.kt104
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.kt14
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandsTest.kt127
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt162
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/GameDefinitionTest.kt20
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.kt96
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.kt147
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.kt192
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializerTest.kt207
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.kt56
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.kt80
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.kt99
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.kt157
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.kt142
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/DiscountTest.kt69
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.kt43
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.kt44
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.kt73
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.kt29
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgressTest.kt49
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.kt93
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMoveTest.kt82
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculatorTest.kt137
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ProductionTest.kt292
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactionsTest.kt27
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourcesTest.kt435
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/TradingRulesTest.kt126
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/test/TestUtils.kt137
-rw-r--r--sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/wonders/WonderTest.kt34
96 files changed, 8166 insertions, 0 deletions
diff --git a/sw-engine/build.gradle.kts b/sw-engine/build.gradle.kts
new file mode 100644
index 00000000..e85d396f
--- /dev/null
+++ b/sw-engine/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+ id("org.jlleitschuh.gradle.ktlint") version "7.1.0"
+}
+
+dependencies {
+ implementation(project(":sw-common-model"))
+ implementation(kotlin("stdlib-jdk8"))
+ implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
+ testImplementation(kotlin("test"))
+ testImplementation(kotlin("test-junit"))
+}
+
+tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
+ kotlinOptions.jvmTarget = "1.8"
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt
new file mode 100644
index 00000000..266a57a5
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Game.kt
@@ -0,0 +1,187 @@
+package org.luxons.sevenwonders.game
+
+import org.luxons.sevenwonders.game.api.Action
+import org.luxons.sevenwonders.game.cards.HandCard
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.api.PlayerTurnInfo
+import org.luxons.sevenwonders.game.api.toApiTable
+import org.luxons.sevenwonders.game.api.toPlayedMove
+import org.luxons.sevenwonders.game.api.toTableCard
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Decks
+import org.luxons.sevenwonders.game.cards.Hands
+import org.luxons.sevenwonders.game.data.LAST_AGE
+import org.luxons.sevenwonders.game.effects.SpecialAbility
+import org.luxons.sevenwonders.game.moves.Move
+import org.luxons.sevenwonders.game.moves.resolve
+import org.luxons.sevenwonders.game.score.ScoreBoard
+import org.luxons.sevenwonders.game.api.Table as ApiTable
+
+class Game internal constructor(
+ val id: Long,
+ private val settings: Settings,
+ boards: List<Board>,
+ private val decks: Decks
+) {
+ private val table: Table = Table(boards)
+ private val players: List<Player> = boards.map { SimplePlayer(it.playerIndex, table) }
+ private val discardedCards: MutableList<Card> = mutableListOf()
+ private val preparedMoves: MutableMap<Int, Move> = mutableMapOf()
+ private var currentTurnInfo: List<PlayerTurnInfo> = emptyList()
+ private var hands: Hands = Hands(emptyList())
+
+ init {
+ startNewAge()
+ }
+
+ private fun startNewAge() {
+ table.increaseCurrentAge()
+ hands = decks.deal(table.currentAge, players.size)
+ startNewTurn()
+ }
+
+ private fun startNewTurn() {
+ currentTurnInfo = players.map { createPlayerTurnInfo(it) }
+ }
+
+ private fun createPlayerTurnInfo(player: Player): PlayerTurnInfo {
+ val hand = hands.createHand(player)
+ val action = determineAction(hand, player.board)
+ val neighbourGuildCards = table.getNeighbourGuildCards(player.index).map { it.toTableCard(null) }
+
+ return PlayerTurnInfo(
+ playerIndex = player.index,
+ table = table.toApiTable(),
+ action = action,
+ hand = hand,
+ preparedMove = preparedMoves[player.index]?.toPlayedMove(),
+ neighbourGuildCards = neighbourGuildCards
+ )
+ }
+
+ /**
+ * Returns information for each player about the current turn.
+ */
+ fun getCurrentTurnInfo(): Collection<PlayerTurnInfo> = currentTurnInfo
+
+ private fun determineAction(hand: List<HandCard>, board: Board): Action = when {
+ endOfGameReached() && board.hasSpecial(SpecialAbility.COPY_GUILD) -> determineCopyGuildAction(board)
+ 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].
+ *
+ * @return the back of the card that is prepared on the table
+ */
+ fun prepareMove(playerIndex: Int, move: PlayerMove): CardBack {
+ val card = decks.getCard(table.currentAge, move.cardName)
+ val context = PlayerContext(playerIndex, table, hands[playerIndex])
+ val resolvedMove = move.type.resolve(move, card, context)
+ preparedMoves[playerIndex] = resolvedMove
+ return card.back
+ }
+
+ /**
+ * Returns true if all players that had to do something have [prepared their move][prepareMove]. This means we are
+ * ready to [play the current turn][playTurn].
+ */
+ fun allPlayersPreparedTheirMove(): Boolean {
+ val nbExpectedMoves = currentTurnInfo.count { it.action !== Action.WAIT }
+ return preparedMoves.size == nbExpectedMoves
+ }
+
+ /**
+ * Plays all the [prepared moves][prepareMove] for the current turn. An exception will be thrown if some players
+ * had not prepared their moves (unless these players had nothing to do). To avoid this, please check if everyone
+ * is ready using [allPlayersPreparedTheirMove].
+ */
+ fun playTurn(): ApiTable {
+ makeMoves()
+ if (endOfAgeReached()) {
+ executeEndOfAgeEvents()
+ if (!endOfGameReached()) {
+ startNewAge()
+ }
+ } else {
+ rotateHandsIfRelevant()
+ startNewTurn()
+ }
+ return table.toApiTable()
+ }
+
+ private fun makeMoves() {
+ val moves = getMovesToPerform()
+
+ // all cards from this turn need to be placed before executing any effect
+ // because effects depending on played cards need to take the ones from the current turn into account too
+ placePreparedCards(moves)
+
+ // same goes for the discarded cards during the last turn, which should be available for special actions
+ if (hands.maxOneCardRemains()) {
+ discardLastCardsOfHands()
+ }
+
+ activatePlayedCards(moves)
+
+ table.lastPlayedMoves = moves
+ preparedMoves.clear()
+ }
+
+ private fun getMovesToPerform(): List<Move> =
+ currentTurnInfo.filter { it.action !== Action.WAIT }.map { getMoveToPerformFor(it.playerIndex) }
+
+ private fun getMoveToPerformFor(playerIndex: Int) =
+ preparedMoves[playerIndex] ?: throw MissingPreparedMoveException(playerIndex)
+
+ private fun endOfAgeReached(): Boolean = hands.isEmpty
+
+ private fun executeEndOfAgeEvents() = table.resolveMilitaryConflicts()
+
+ private fun endOfGameReached(): Boolean = endOfAgeReached() && table.currentAge == LAST_AGE
+
+ private fun rotateHandsIfRelevant() {
+ // we don't rotate hands if some player can play his last card (with the special ability)
+ if (!hands.maxOneCardRemains()) {
+ hands = hands.rotate(table.handRotationDirection)
+ }
+ }
+
+ private fun placePreparedCards(playedMoves: List<Move>) {
+ playedMoves.forEach { move ->
+ move.place(discardedCards, settings)
+ hands = hands.remove(move.playerContext.index, move.card)
+ }
+ }
+
+ private fun discardLastCardsOfHands() =
+ table.boards.filterNot { it.hasSpecial(SpecialAbility.PLAY_LAST_CARD) }.forEach { discardHand(it.playerIndex) }
+
+ private fun discardHand(playerIndex: Int) {
+ val hand = hands[playerIndex]
+ discardedCards.addAll(hand)
+ hands = hands.discardHand(playerIndex)
+ }
+
+ private fun activatePlayedCards(playedMoves: List<Move>) =
+ playedMoves.forEach { it.activate(discardedCards, settings) }
+
+ /**
+ * Computes the score for all players.
+ */
+ fun computeScore(): ScoreBoard = ScoreBoard(table.boards.map { it.computeScore(players[it.playerIndex]) })
+
+ private class MissingPreparedMoveException internal constructor(playerIndex: Int) :
+ IllegalStateException("Player $playerIndex has not prepared his move")
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Player.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Player.kt
new file mode 100644
index 00000000..2c82b6ff
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Player.kt
@@ -0,0 +1,28 @@
+package org.luxons.sevenwonders.game
+
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.cards.Card
+
+internal interface Player {
+ val index: Int
+ val board: Board
+ fun getBoard(relativePosition: RelativeBoardPosition): Board
+}
+
+internal data class SimplePlayer(
+ override val index: Int,
+ private val table: Table
+) : Player {
+ override val board = table.getBoard(index)
+ override fun getBoard(relativePosition: RelativeBoardPosition) = table.getBoard(index, relativePosition)
+}
+
+internal data class PlayerContext(
+ override val index: Int,
+ private val table: Table,
+ val hand: List<Card>
+) : Player by SimplePlayer(index, table) {
+ val currentAge = table.currentAge
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Settings.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Settings.kt
new file mode 100644
index 00000000..ad6cb105
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/Settings.kt
@@ -0,0 +1,29 @@
+package org.luxons.sevenwonders.game
+
+import org.luxons.sevenwonders.game.api.CustomizableSettings
+import org.luxons.sevenwonders.game.api.WonderSide
+import org.luxons.sevenwonders.game.api.WonderSidePickMethod
+import kotlin.random.Random
+
+internal class Settings(
+ val nbPlayers: Int,
+ customSettings: CustomizableSettings = CustomizableSettings()
+) {
+ val random: Random = customSettings.randomSeedForTests?.let { Random(it) } ?: Random
+ val timeLimitInSeconds: Int = customSettings.timeLimitInSeconds
+ val initialGold: Int = customSettings.initialGold
+ val discardedCardGold: Int = customSettings.discardedCardGold
+ val defaultTradingCost: Int = customSettings.defaultTradingCost
+ val pointsPer3Gold: Int = customSettings.pointsPer3Gold
+ val lostPointsPerDefeat: Int = customSettings.lostPointsPerDefeat
+ val wonPointsPerVictoryPerAge: Map<Int, Int> = customSettings.wonPointsPerVictoryPerAge
+
+ private val wonderSidePickMethod: WonderSidePickMethod = customSettings.wonderSidePickMethod
+ private var lastPickedSide: WonderSide? = null
+
+ fun pickWonderSide(): WonderSide {
+ val newSide = wonderSidePickMethod.pickSide(random, lastPickedSide)
+ lastPickedSide = newSide
+ return newSide
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt
new file mode 100644
index 00000000..5dff8636
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Boards.kt
@@ -0,0 +1,81 @@
+package org.luxons.sevenwonders.game.api
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Military
+import org.luxons.sevenwonders.game.boards.Science
+import org.luxons.sevenwonders.game.boards.ScienceType
+import org.luxons.sevenwonders.game.cards.Requirements
+import org.luxons.sevenwonders.game.cards.TableCard
+import org.luxons.sevenwonders.game.moves.Move
+import org.luxons.sevenwonders.game.moves.MoveType
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.Resources
+import org.luxons.sevenwonders.game.wonders.ApiWonder
+import org.luxons.sevenwonders.game.wonders.ApiWonderStage
+import org.luxons.sevenwonders.game.boards.Board as InternalBoard
+import org.luxons.sevenwonders.game.wonders.Wonder as InternalWonder
+import org.luxons.sevenwonders.game.wonders.WonderStage as InternalWonderStage
+
+internal fun InternalBoard.toApiBoard(player: Player, lastMove: Move?): Board = Board(
+ playerIndex = playerIndex,
+ wonder = wonder.toApiWonder(player, lastMove),
+ production = production.toApiProduction(),
+ publicProduction = publicProduction.toApiProduction(),
+ science = science.toApiScience(),
+ military = military.toApiMilitary(),
+ playedCards = getPlayedCards().map { it.toTableCard(lastMove) }.toColumns(),
+ gold = gold
+)
+
+internal fun List<TableCard>.toColumns(): List<List<TableCard>> {
+ val cardsByColor = this.groupBy { it.color }
+ val (resourceCardsCols, otherCols) = cardsByColor.values.partition { it[0].color.isResource }
+ val resourceCardsCol = resourceCardsCols.flatten()
+ val otherColsSorted = otherCols.sortedBy { it[0].color }
+ if (resourceCardsCol.isEmpty()) {
+ return otherColsSorted // we want only non-empty columns
+ }
+ return listOf(resourceCardsCol) + otherColsSorted
+}
+
+internal fun InternalWonder.toApiWonder(player: Player, lastMove: Move?): ApiWonder =
+ ApiWonder(
+ name = name,
+ initialResource = initialResource,
+ stages = stages.map { it.toApiWonderStage(lastBuiltStage == it, lastMove) },
+ image = image,
+ nbBuiltStages = nbBuiltStages,
+ buildability = computeBuildabilityBy(player)
+ )
+
+internal fun InternalWonderStage.toApiWonderStage(
+ isLastBuiltStage: Boolean,
+ lastMove: Move?
+): ApiWonderStage = ApiWonderStage(
+ cardBack = cardBack,
+ isBuilt = isBuilt,
+ requirements = requirements.toApiRequirements(),
+ builtDuringLastMove = lastMove?.type == MoveType.UPGRADE_WONDER && isLastBuiltStage
+)
+
+internal fun Production.toApiProduction(): ApiProduction = ApiProduction(
+ fixedResources = getFixedResources().toCountedResourcesList(),
+ alternativeResources = getAlternativeResources()
+)
+
+internal fun Requirements.toApiRequirements(): ApiRequirements = ApiRequirements(
+ gold = gold,
+ resources = resources.toCountedResourcesList()
+)
+
+internal fun Resources.toCountedResourcesList(): List<ApiCountedResource> =
+ quantities.map { (type, count) -> ApiCountedResource(count, type) }.sortedBy { it.type }
+
+internal fun Military.toApiMilitary(): ApiMilitary = ApiMilitary(nbShields, totalPoints, nbDefeatTokens)
+
+internal fun Science.toApiScience(): ApiScience = ApiScience(
+ jokers = jokers,
+ nbWheels = getQuantity(ScienceType.WHEEL),
+ nbCompasses = getQuantity(ScienceType.COMPASS),
+ nbTablets = getQuantity(ScienceType.TABLET)
+)
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt
new file mode 100644
index 00000000..7587d69a
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Cards.kt
@@ -0,0 +1,31 @@
+package org.luxons.sevenwonders.game.api
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.HandCard
+import org.luxons.sevenwonders.game.cards.TableCard
+import org.luxons.sevenwonders.game.moves.Move
+
+internal fun Card.toTableCard(lastMove: Move? = null): TableCard =
+ TableCard(
+ name = name,
+ color = color,
+ requirements = requirements.toApiRequirements(),
+ chainParent = chainParent,
+ chainChildren = chainChildren,
+ image = image,
+ back = back,
+ playedDuringLastMove = lastMove != null && this.name == lastMove.card.name
+ )
+
+internal fun Card.toHandCard(player: Player): HandCard =
+ HandCard(
+ name = name,
+ color = color,
+ requirements = requirements.toApiRequirements(),
+ chainParent = chainParent,
+ chainChildren = chainChildren,
+ image = image,
+ back = back,
+ playability = computePlayabilityBy(player)
+ )
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt
new file mode 100644
index 00000000..de6e587d
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/api/Table.kt
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.game.api
+
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.moves.Move
+import org.luxons.sevenwonders.game.boards.Table as InternalTable
+
+internal fun InternalTable.toApiTable(): Table = Table(
+ boards = boards.mapIndexed { i, b -> b.toApiBoard(SimplePlayer(i, this), lastPlayedMoves.getOrNull(i)) },
+ currentAge = currentAge,
+ handRotationDirection = handRotationDirection,
+ lastPlayedMoves = lastPlayedMoves.map { it.toPlayedMove() }
+)
+
+internal fun Move.toPlayedMove(): PlayedMove = PlayedMove(
+ playerIndex = playerContext.index,
+ type = type,
+ card = card.toTableCard(this),
+ transactions = transactions
+)
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Board.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Board.kt
new file mode 100644
index 00000000..a43b8a3c
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Board.kt
@@ -0,0 +1,98 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.Age
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.effects.SpecialAbility
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.TradingRules
+import org.luxons.sevenwonders.game.resources.mutableResourcesOf
+import org.luxons.sevenwonders.game.score.PlayerScore
+import org.luxons.sevenwonders.game.score.ScoreCategory
+import org.luxons.sevenwonders.game.wonders.Wonder
+
+internal class Board(val wonder: Wonder, val playerIndex: Int, settings: Settings) {
+
+ val production = Production(mutableResourcesOf(wonder.initialResource))
+ val publicProduction = Production(mutableResourcesOf(wonder.initialResource))
+ val science = Science()
+ val military: Military = Military(settings.lostPointsPerDefeat, settings.wonPointsPerVictoryPerAge)
+ val tradingRules: TradingRules = TradingRules(settings.defaultTradingCost)
+
+ private val pointsPer3Gold: Int = settings.pointsPer3Gold
+
+ private val playedCards: MutableList<Card> = mutableListOf()
+ private val specialAbilities: MutableSet<SpecialAbility> = hashSetOf()
+ private val consumedFreeCards: MutableMap<Age, Boolean> = mutableMapOf()
+
+ var gold: Int = settings.initialGold
+ private set
+
+ var copiedGuild: Card? = null
+ internal set(copiedGuild) {
+ if (copiedGuild!!.color !== Color.PURPLE) {
+ throw IllegalArgumentException("The given card '$copiedGuild' is not a Guild card")
+ }
+ field = copiedGuild
+ }
+
+ fun getPlayedCards(): List<Card> = playedCards
+
+ fun addCard(card: Card) {
+ playedCards.add(card)
+ }
+
+ fun getNbCardsOfColor(colorFilter: List<Color>): Int = playedCards.count { colorFilter.contains(it.color) }
+
+ fun isPlayed(cardName: String): Boolean = playedCards.count { it.name == cardName } > 0
+
+ fun addGold(amount: Int) {
+ this.gold += amount
+ }
+
+ fun removeGold(amount: Int) {
+ if (gold < amount) {
+ throw InsufficientFundsException(gold, amount)
+ }
+ this.gold -= amount
+ }
+
+ fun addSpecial(specialAbility: SpecialAbility) {
+ specialAbilities.add(specialAbility)
+ }
+
+ fun hasSpecial(specialAbility: SpecialAbility): Boolean = specialAbilities.contains(specialAbility)
+
+ fun canPlayFreeCard(age: Age): Boolean =
+ hasSpecial(SpecialAbility.ONE_FREE_PER_AGE) && !consumedFreeCards.getOrDefault(age, false)
+
+ fun consumeFreeCard(age: Age) {
+ consumedFreeCards[age] = true
+ }
+
+ fun computeScore(player: Player): PlayerScore = PlayerScore(
+ boardGold = gold,
+ pointsByCategory = mapOf(
+ ScoreCategory.CIVIL to computePointsForCards(player, Color.BLUE),
+ ScoreCategory.MILITARY to military.totalPoints,
+ ScoreCategory.SCIENCE to science.computePoints(),
+ ScoreCategory.TRADE to computePointsForCards(player, Color.YELLOW),
+ ScoreCategory.GUILD to computePointsForCards(player, Color.PURPLE),
+ ScoreCategory.WONDER to wonder.computePoints(player),
+ ScoreCategory.GOLD to computeGoldPoints()
+ )
+ )
+
+ private fun computePointsForCards(player: Player, color: Color): Int =
+ playedCards.filter { it.color === color }
+ .flatMap { it.effects }
+ .map { it.computePoints(player) }
+ .sum()
+
+ private fun computeGoldPoints(): Int = gold / 3 * pointsPer3Gold
+
+ internal class InsufficientFundsException(current: Int, required: Int) :
+ IllegalStateException("Current balance is $current gold, but $required are required")
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Military.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Military.kt
new file mode 100644
index 00000000..98404d94
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Military.kt
@@ -0,0 +1,33 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.luxons.sevenwonders.game.api.Age
+
+internal class Military(
+ private val lostPointsPerDefeat: Int,
+ private val wonPointsPerVictoryPerAge: Map<Age, Int>
+) {
+ var nbShields = 0
+ private set
+
+ var totalPoints = 0
+ private set
+
+ var nbDefeatTokens = 0
+ private set
+
+ internal fun addShields(nbShields: Int) {
+ this.nbShields += nbShields
+ }
+
+ internal fun victory(age: Age) {
+ val wonPoints = wonPointsPerVictoryPerAge[age] ?: throw UnknownAgeException(age)
+ totalPoints += wonPoints
+ }
+
+ internal fun defeat() {
+ totalPoints -= lostPointsPerDefeat
+ nbDefeatTokens++
+ }
+
+ internal class UnknownAgeException(unknownAge: Age) : IllegalArgumentException(unknownAge.toString())
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Science.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Science.kt
new file mode 100644
index 00000000..a152c7db
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Science.kt
@@ -0,0 +1,56 @@
+package org.luxons.sevenwonders.game.boards
+
+enum class ScienceType {
+ COMPASS,
+ WHEEL,
+ TABLET
+}
+
+internal class Science {
+
+ private val quantities: MutableMap<ScienceType, Int> = mutableMapOf()
+
+ var jokers: Int = 0
+ private set
+
+ fun size(): Int = quantities.values.sum() + jokers
+
+ fun add(type: ScienceType, quantity: Int) {
+ quantities.merge(type, quantity) { x, y -> x + y }
+ }
+
+ fun addJoker(quantity: Int) {
+ jokers += quantity
+ }
+
+ fun addAll(science: Science) {
+ science.quantities.forEach { type, quantity -> this.add(type, quantity) }
+ jokers += science.jokers
+ }
+
+ fun getQuantity(type: ScienceType): Int = quantities.getOrDefault(type, 0)
+
+ fun computePoints(): Int {
+ val values = ScienceType.values().map(::getQuantity).toMutableList()
+ return computePoints(values, jokers)
+ }
+
+ private fun computePoints(values: MutableList<Int>, jokers: Int): Int {
+ if (jokers == 0) {
+ return computePointsNoJoker(values)
+ }
+ var maxPoints = 0
+ for (i in values.indices) {
+ values[i]++
+ maxPoints = Math.max(maxPoints, computePoints(values, jokers - 1))
+ values[i]--
+ }
+ return maxPoints
+ }
+
+ private fun computePointsNoJoker(values: List<Int>): Int {
+ val independentSquaresSum = values.map { i -> i * i }.sum()
+ val nbGroupsOfAll = values.min() ?: 0
+ return independentSquaresSum + nbGroupsOfAll * 7
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Table.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Table.kt
new file mode 100644
index 00000000..168649f4
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/boards/Table.kt
@@ -0,0 +1,58 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.luxons.sevenwonders.game.api.Age
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.cards.HandRotationDirection
+import org.luxons.sevenwonders.game.moves.Move
+
+/**
+ * The table contains what is visible by all the players in the game: the boards and their played cards, and the
+ * players' information.
+ */
+internal class Table(val boards: List<Board>) {
+
+ val nbPlayers: Int = boards.size
+
+ var currentAge: Age = 0
+ private set
+
+ val handRotationDirection: HandRotationDirection
+ get() = HandRotationDirection.forAge(currentAge)
+
+ var lastPlayedMoves: List<Move> = emptyList()
+ internal set
+
+ fun getBoard(playerIndex: Int): Board = boards[playerIndex]
+
+ fun getBoard(playerIndex: Int, position: RelativeBoardPosition): Board =
+ boards[position.getIndexFrom(playerIndex, nbPlayers)]
+
+ fun increaseCurrentAge() {
+ this.currentAge++
+ }
+
+ fun resolveMilitaryConflicts() {
+ repeat(nbPlayers) {
+ val board1 = getBoard(it)
+ val board2 = getBoard(it, RelativeBoardPosition.RIGHT)
+ resolveConflict(board1, board2)
+ }
+ }
+
+ private fun resolveConflict(board1: Board, board2: Board) {
+ val shields1 = board1.military.nbShields
+ val shields2 = board2.military.nbShields
+ if (shields1 < shields2) {
+ board1.military.defeat()
+ board2.military.victory(currentAge)
+ } else if (shields1 > shields2) {
+ board1.military.victory(currentAge)
+ board2.military.defeat()
+ }
+ }
+
+ fun getNeighbourGuildCards(playerIndex: Int): List<Card> = neighboursPositions()
+ .flatMap { getBoard(playerIndex, it).getPlayedCards() }
+ .filter { it.color == Color.PURPLE }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt
new file mode 100644
index 00000000..b983aa0e
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Cards.kt
@@ -0,0 +1,64 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.effects.Effect
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.noTransactions
+
+internal data class Card(
+ val name: String,
+ val color: Color,
+ val requirements: Requirements,
+ val effects: List<Effect>,
+ val chainParent: String?,
+ val chainChildren: List<String>,
+ val image: String,
+ val back: CardBack
+) {
+ fun computePlayabilityBy(player: Player): CardPlayability = when {
+ isAlreadyOnBoard(player.board) -> Playability.incompatibleWithBoard() // cannot play twice the same card
+ isParentOnBoard(player.board) -> Playability.chainable()
+ else -> Playability.requirementDependent(requirements.assess(player))
+ }
+
+ fun isPlayableOnBoardWith(board: Board, transactions: ResourceTransactions) =
+ isChainableOn(board) || requirements.areMetWithHelpBy(board, transactions)
+
+ private fun isChainableOn(board: Board): Boolean = !isAlreadyOnBoard(board) && isParentOnBoard(board)
+
+ private fun isAlreadyOnBoard(board: Board): Boolean = board.isPlayed(name)
+
+ private fun isParentOnBoard(board: Board): Boolean = chainParent != null && board.isPlayed(chainParent)
+
+ fun applyTo(player: Player, transactions: ResourceTransactions) {
+ if (!isChainableOn(player.board)) {
+ requirements.pay(player, transactions)
+ }
+ effects.forEach { it.applyTo(player) }
+ }
+}
+
+private object Playability {
+
+ internal fun incompatibleWithBoard(): CardPlayability = CardPlayability(
+ isPlayable = false,
+ playabilityLevel = PlayabilityLevel.INCOMPATIBLE_WITH_BOARD
+ )
+
+ internal fun chainable(): CardPlayability = CardPlayability(
+ isPlayable = true,
+ isChainable = true,
+ minPrice = 0,
+ cheapestTransactions = setOf(noTransactions()),
+ playabilityLevel = PlayabilityLevel.CHAINABLE
+ )
+
+ internal fun requirementDependent(satisfaction: RequirementsSatisfaction): CardPlayability = CardPlayability(
+ isPlayable = satisfaction.satisfied,
+ isChainable = false,
+ minPrice = satisfaction.minPrice,
+ cheapestTransactions = satisfaction.cheapestTransactions,
+ playabilityLevel = satisfaction.level
+ )
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Decks.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Decks.kt
new file mode 100644
index 00000000..f3c5d471
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Decks.kt
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.cards
+
+internal fun List<Card>.deal(nbPlayers: Int): Hands {
+ val hands: Map<Int, List<Card>> = this.withIndex()
+ .groupBy { (index, _) -> index % nbPlayers }
+ .mapValues { it.value.map { (_, cards) -> cards } }
+
+ val allHands = List(nbPlayers) { i -> hands[i] ?: emptyList() }
+ return Hands(allHands)
+}
+
+internal class Decks(private val cardsPerAge: Map<Int, List<Card>>) {
+
+ @Throws(Decks.CardNotFoundException::class)
+ fun getCard(age: Int, cardName: String): Card =
+ getDeck(age).firstOrNull { c -> c.name == cardName } ?: throw CardNotFoundException(cardName)
+
+ fun deal(age: Int, nbPlayers: Int): Hands {
+ val deck = getDeck(age)
+ validateNbCards(deck, nbPlayers)
+ return deck.deal(nbPlayers)
+ }
+
+ private fun getDeck(age: Int): List<Card> {
+ return cardsPerAge[age] ?: throw IllegalArgumentException("No deck found for age $age")
+ }
+
+ private fun validateNbCards(deck: List<Card>, nbPlayers: Int) {
+ if (nbPlayers == 0) {
+ throw IllegalArgumentException("Cannot deal cards between 0 players")
+ }
+ if (deck.size % nbPlayers != 0) {
+ throw IllegalArgumentException("Cannot deal ${deck.size} cards evenly between $nbPlayers players")
+ }
+ }
+
+ inner class CardNotFoundException(message: String) : RuntimeException(message)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Hands.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Hands.kt
new file mode 100644
index 00000000..19490b9c
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Hands.kt
@@ -0,0 +1,37 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.api.toHandCard
+
+internal class Hands(private val hands: List<List<Card>>) {
+
+ val isEmpty: Boolean = this.hands.all(List<Card>::isEmpty)
+
+ operator fun get(playerIndex: Int): List<Card> {
+ return hands[playerIndex]
+ }
+
+ fun discardHand(playerIndex: Int): Hands {
+ val mutatedHands = hands.toMutableList()
+ mutatedHands[playerIndex] = emptyList()
+ return Hands(mutatedHands)
+ }
+
+ fun remove(playerIndex: Int, card: Card): Hands {
+ val mutatedHands = hands.toMutableList()
+ mutatedHands[playerIndex] = hands[playerIndex] - card
+ return Hands(mutatedHands)
+ }
+
+ fun createHand(player: Player): List<HandCard> = hands[player.index].map { c -> c.toHandCard(player) }
+
+ fun rotate(direction: HandRotationDirection): Hands {
+ val newHands = when (direction) {
+ HandRotationDirection.RIGHT -> hands.takeLast(1) + hands.dropLast(1)
+ HandRotationDirection.LEFT -> hands.drop(1) + hands.take(1)
+ }
+ return Hands(newHands)
+ }
+
+ fun maxOneCardRemains(): Boolean = hands.map { it.size }.max() ?: 0 <= 1
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt
new file mode 100644
index 00000000..27f73109
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/Requirements.kt
@@ -0,0 +1,83 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.Resources
+import org.luxons.sevenwonders.game.resources.asResources
+import org.luxons.sevenwonders.game.resources.bestSolution
+import org.luxons.sevenwonders.game.resources.emptyResources
+import org.luxons.sevenwonders.game.resources.execute
+
+data class Requirements internal constructor(
+ val gold: Int = 0,
+ val resources: Resources = emptyResources()
+) {
+ /**
+ * Returns information about the extent to which the given [player] meets these requirements, either on its own or
+ * by buying resources to neighbours.
+ */
+ internal fun assess(player: Player): RequirementsSatisfaction {
+ if (player.board.gold < gold) {
+ return RequirementsSatisfaction.missingRequiredGold(gold)
+ }
+ if (resources.isEmpty()) {
+ if (gold > 0) {
+ return RequirementsSatisfaction.enoughGold(gold)
+ }
+ return RequirementsSatisfaction.noRequirements()
+ }
+ if (producesRequiredResources(player.board)) {
+ if (gold > 0) {
+ return RequirementsSatisfaction.enoughResourcesAndGold(gold)
+ }
+ return RequirementsSatisfaction.enoughResources()
+ }
+ return satisfactionWithPotentialHelp(player)
+ }
+
+ private fun satisfactionWithPotentialHelp(player: Player): RequirementsSatisfaction {
+ val (minPriceForResources, possibleTransactions) = bestSolution(resources, player)
+ val minPrice = minPriceForResources + gold
+ if (possibleTransactions.isEmpty()) {
+ return RequirementsSatisfaction.unavailableResources()
+ }
+ if (player.board.gold < minPrice) {
+ return RequirementsSatisfaction.missingGoldForResources(minPrice, possibleTransactions)
+ }
+ return RequirementsSatisfaction.metWithHelp(minPrice, possibleTransactions)
+ }
+
+ /**
+ * Returns whether the given board meets these requirements, if the specified resources are bought from neighbours.
+ *
+ * @param board the board to check
+ * @param boughtResources the resources the player intends to buy
+ *
+ * @return true if the given board meets these requirements
+ */
+ internal fun areMetWithHelpBy(board: Board, boughtResources: ResourceTransactions): Boolean {
+ if (!hasRequiredGold(board, boughtResources)) {
+ return false
+ }
+ return producesRequiredResources(board) || producesRequiredResourcesWithHelp(board, boughtResources)
+ }
+
+ private fun hasRequiredGold(board: Board, resourceTransactions: ResourceTransactions): Boolean {
+ val resourcesPrice = board.tradingRules.computeCost(resourceTransactions)
+ return board.gold >= gold + resourcesPrice
+ }
+
+ private fun producesRequiredResources(board: Board): Boolean = board.production.contains(resources)
+
+ private fun producesRequiredResourcesWithHelp(board: Board, transactions: ResourceTransactions): Boolean {
+ val totalBoughtResources = transactions.asResources()
+ val remainingResources = resources - totalBoughtResources
+ return board.production.contains(remainingResources)
+ }
+
+ internal fun pay(player: Player, transactions: ResourceTransactions) {
+ player.board.removeGold(gold)
+ transactions.execute(player)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt
new file mode 100644
index 00000000..87b35723
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/cards/RequirementsSatisfaction.kt
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.noTransactions
+
+internal data class RequirementsSatisfaction(
+ val satisfied: Boolean,
+ val level: PlayabilityLevel,
+ val minPrice: Int,
+ val cheapestTransactions: Set<ResourceTransactions>
+) {
+ companion object {
+
+ internal fun noRequirements() =
+ RequirementsSatisfaction(true, PlayabilityLevel.NO_REQUIREMENTS, 0, setOf(noTransactions()))
+
+ internal fun enoughResources() =
+ RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_RESOURCES, 0, setOf(noTransactions()))
+
+ internal fun enoughGold(minPrice: Int) =
+ RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD, minPrice, setOf(noTransactions()))
+
+ internal fun enoughResourcesAndGold(minPrice: Int) =
+ RequirementsSatisfaction(true, PlayabilityLevel.ENOUGH_GOLD_AND_RES, minPrice, setOf(noTransactions()))
+
+ internal fun metWithHelp(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) =
+ RequirementsSatisfaction(true, PlayabilityLevel.REQUIRES_HELP, minPrice, cheapestTransactions)
+
+ internal fun missingRequiredGold(minPrice: Int) =
+ RequirementsSatisfaction(false, PlayabilityLevel.MISSING_REQUIRED_GOLD, minPrice, emptySet())
+
+ internal fun missingGoldForResources(minPrice: Int, cheapestTransactions: Set<ResourceTransactions>) =
+ RequirementsSatisfaction(false, PlayabilityLevel.MISSING_GOLD_FOR_RES, minPrice, cheapestTransactions)
+
+ internal fun unavailableResources() =
+ RequirementsSatisfaction(false, PlayabilityLevel.UNAVAILABLE_RESOURCES, Int.MAX_VALUE, emptySet())
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/GameDefinition.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/GameDefinition.kt
new file mode 100644
index 00000000..43c48aa7
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/GameDefinition.kt
@@ -0,0 +1,92 @@
+package org.luxons.sevenwonders.game.data
+
+import com.github.salomonbrys.kotson.typeToken
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.luxons.sevenwonders.game.Game
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.Age
+import org.luxons.sevenwonders.game.api.CustomizableSettings
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.data.definitions.DecksDefinition
+import org.luxons.sevenwonders.game.data.definitions.WonderDefinition
+import org.luxons.sevenwonders.game.data.serializers.NumericEffectSerializer
+import org.luxons.sevenwonders.game.data.serializers.ProductionIncreaseSerializer
+import org.luxons.sevenwonders.game.data.serializers.ProductionSerializer
+import org.luxons.sevenwonders.game.data.serializers.ResourceTypeSerializer
+import org.luxons.sevenwonders.game.data.serializers.ResourceTypesSerializer
+import org.luxons.sevenwonders.game.data.serializers.ResourcesSerializer
+import org.luxons.sevenwonders.game.data.serializers.ScienceProgressSerializer
+import org.luxons.sevenwonders.game.effects.GoldIncrease
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease
+import org.luxons.sevenwonders.game.effects.ScienceProgress
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.Resources
+
+internal const val LAST_AGE: Age = 3
+
+internal data class GlobalRules(
+ val minPlayers: Int,
+ val maxPlayers: Int
+)
+
+class GameDefinition internal constructor(
+ rules: GlobalRules,
+ private val wonders: List<WonderDefinition>,
+ private val decksDefinition: DecksDefinition
+) {
+ val minPlayers: Int = rules.minPlayers
+ val maxPlayers: Int = rules.maxPlayers
+
+ fun initGame(id: Long, customSettings: CustomizableSettings, nbPlayers: Int): Game {
+ val settings = Settings(nbPlayers, customSettings)
+ val boards = assignBoards(settings, nbPlayers)
+ val decks = decksDefinition.prepareDecks(settings.nbPlayers, settings.random)
+ return Game(id, settings, boards, decks)
+ }
+
+ private fun assignBoards(settings: Settings, nbPlayers: Int): List<Board> {
+ return wonders.shuffled(settings.random)
+ .take(nbPlayers)
+ .mapIndexed { i, wDef -> Board(wDef.create(settings.pickWonderSide()), i, settings) }
+ }
+
+ companion object {
+
+ fun load(): GameDefinition {
+ val gson: Gson = createGson()
+ val rules = loadJson("global_rules.json", GlobalRules::class.java, gson)
+ val wonders = loadJson("wonders.json", Array<WonderDefinition>::class.java, gson)
+ val decksDefinition = loadJson("cards.json", DecksDefinition::class.java, gson)
+ return GameDefinition(rules, wonders.toList(), decksDefinition)
+ }
+ }
+}
+
+private fun <T> loadJson(filename: String, clazz: Class<T>, gson: Gson): T {
+ val packageAsPath = GameDefinition::class.java.`package`.name.replace('.', '/')
+ val resourcePath = "/$packageAsPath/$filename"
+ val resource = GameDefinition::class.java.getResource(resourcePath)
+ val json = resource.readText()
+ return gson.fromJson(json, clazz)
+}
+
+private fun createGson(): Gson {
+ return GsonBuilder().disableHtmlEscaping()
+ .registerTypeAdapter<Resources>(ResourcesSerializer())
+ .registerTypeAdapter<ResourceType>(ResourceTypeSerializer())
+ .registerTypeAdapter<List<ResourceType>>(ResourceTypesSerializer())
+ .registerTypeAdapter<Production>(ProductionSerializer())
+ .registerTypeAdapter<ProductionIncrease>(ProductionIncreaseSerializer())
+ .registerTypeAdapter<MilitaryReinforcements>(NumericEffectSerializer())
+ .registerTypeAdapter<RawPointsIncrease>(NumericEffectSerializer())
+ .registerTypeAdapter<GoldIncrease>(NumericEffectSerializer())
+ .registerTypeAdapter<ScienceProgress>(ScienceProgressSerializer())
+ .create()
+}
+
+private inline fun <reified T : Any> GsonBuilder.registerTypeAdapter(typeAdapter: Any): GsonBuilder =
+ this.registerTypeAdapter(typeToken<T>(), typeAdapter)
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/CardDefinition.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/CardDefinition.kt
new file mode 100644
index 00000000..0b7b9229
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/CardDefinition.kt
@@ -0,0 +1,25 @@
+package org.luxons.sevenwonders.game.data.definitions
+
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.cards.Requirements
+
+internal class CardDefinition(
+ private val name: String,
+ private val color: Color,
+ private val requirements: Requirements?,
+ private val effect: EffectsDefinition,
+ private val chainParent: String?,
+ private val chainChildren: List<String>?,
+ private val image: String,
+ private val countPerNbPlayer: Map<Int, Int>
+) {
+ fun create(back: CardBack, nbPlayers: Int): List<Card> = List(countPerNbPlayer[nbPlayers] ?: 0) { create(back) }
+
+ fun create(back: CardBack): Card {
+ val reqs = requirements ?: Requirements()
+ val children = chainChildren ?: emptyList()
+ return Card(name, color, reqs, effect.create(), chainParent, children, image, back)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.kt
new file mode 100644
index 00000000..b090547d
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.kt
@@ -0,0 +1,34 @@
+package org.luxons.sevenwonders.game.data.definitions
+
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Decks
+import kotlin.random.Random
+
+internal class DeckDefinition(
+ val cards: List<CardDefinition>,
+ val backImage: String
+) {
+ fun create(nbPlayers: Int): List<Card> = cards.flatMap { it.create(CardBack(backImage), nbPlayers) }
+}
+
+internal class DecksDefinition(
+ private val age1: DeckDefinition,
+ private val age2: DeckDefinition,
+ private val age3: DeckDefinition,
+ private val guildCards: List<CardDefinition>
+) {
+ fun prepareDecks(nbPlayers: Int, random: Random) = Decks(
+ mapOf(
+ 1 to age1.create(nbPlayers).shuffled(random),
+ 2 to age2.create(nbPlayers).shuffled(random),
+ 3 to (age3.create(nbPlayers) + pickGuildCards(nbPlayers, random)).shuffled(random)
+ )
+ )
+
+ private fun pickGuildCards(nbPlayers: Int, random: Random): List<Card> {
+ val back = CardBack(age3.backImage)
+ val guild = guildCards.map { it.create(back) }.shuffled(random)
+ return guild.subList(0, nbPlayers + 2)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.kt
new file mode 100644
index 00000000..4d0348ea
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.kt
@@ -0,0 +1,34 @@
+package org.luxons.sevenwonders.game.data.definitions
+
+import org.luxons.sevenwonders.game.effects.BonusPerBoardElement
+import org.luxons.sevenwonders.game.effects.Discount
+import org.luxons.sevenwonders.game.effects.Effect
+import org.luxons.sevenwonders.game.effects.GoldIncrease
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease
+import org.luxons.sevenwonders.game.effects.ScienceProgress
+import org.luxons.sevenwonders.game.effects.SpecialAbility
+import org.luxons.sevenwonders.game.effects.SpecialAbilityActivation
+
+internal class EffectsDefinition(
+ private val gold: GoldIncrease? = null,
+ private val military: MilitaryReinforcements? = null,
+ private val science: ScienceProgress? = null,
+ private val discount: Discount? = null,
+ private val perBoardElement: BonusPerBoardElement? = null,
+ private val production: ProductionIncrease? = null,
+ private val points: RawPointsIncrease? = null,
+ private val action: SpecialAbility? = null
+) {
+ fun create(): List<Effect> = mutableListOf<Effect>().apply {
+ gold?.let { add(it) }
+ military?.let { add(it) }
+ science?.let { add(it) }
+ discount?.let { add(it) }
+ perBoardElement?.let { add(it) }
+ production?.let { add(it) }
+ points?.let { add(it) }
+ action?.let { add(SpecialAbilityActivation(it)) }
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.kt
new file mode 100644
index 00000000..fb0eccda
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.kt
@@ -0,0 +1,29 @@
+package org.luxons.sevenwonders.game.data.definitions
+
+import org.luxons.sevenwonders.game.api.WonderSide
+import org.luxons.sevenwonders.game.cards.Requirements
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.wonders.Wonder
+import org.luxons.sevenwonders.game.wonders.WonderStage
+
+internal class WonderDefinition(
+ private val name: String,
+ private val sides: Map<WonderSide, WonderSideDefinition>
+) {
+ fun create(wonderSide: WonderSide): Wonder = sides[wonderSide]!!.createWonder(name)
+}
+
+internal class WonderSideDefinition(
+ private val initialResource: ResourceType,
+ private val stages: List<WonderStageDefinition>,
+ private val image: String
+) {
+ fun createWonder(name: String): Wonder = Wonder(name, initialResource, stages.map { it.create() }, image)
+}
+
+internal class WonderStageDefinition(
+ private val requirements: Requirements,
+ private val effects: EffectsDefinition
+) {
+ fun create(): WonderStage = WonderStage(requirements, effects.create())
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.kt
new file mode 100644
index 00000000..9a9a006e
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.kt
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.effects.Effect
+import org.luxons.sevenwonders.game.effects.GoldIncrease
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease
+import java.lang.reflect.Type
+
+internal class NumericEffectSerializer : JsonSerializer<Effect>, JsonDeserializer<Effect> {
+
+ override fun serialize(effect: Effect, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
+ val value: Int = when (effect) {
+ is MilitaryReinforcements -> effect.count
+ is GoldIncrease -> effect.amount
+ is RawPointsIncrease -> effect.points
+ else -> throw IllegalArgumentException("Unknown numeric effect " + effect.javaClass.name)
+ }
+ return JsonPrimitive(value)
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Effect {
+ val value = json.asInt
+ return when (typeOfT) {
+ MilitaryReinforcements::class.java -> MilitaryReinforcements(value)
+ GoldIncrease::class.java -> GoldIncrease(value)
+ RawPointsIncrease::class.java -> RawPointsIncrease(value)
+ else -> throw IllegalArgumentException("Unknown numeric effet " + typeOfT.typeName)
+ }
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.kt
new file mode 100644
index 00000000..0970a968
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.kt
@@ -0,0 +1,47 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.resources.Production
+import java.lang.reflect.Type
+
+internal class ProductionIncreaseSerializer : JsonSerializer<ProductionIncrease>, JsonDeserializer<ProductionIncrease> {
+
+ override fun serialize(
+ productionIncrease: ProductionIncrease,
+ typeOfSrc: Type,
+ context: JsonSerializationContext
+ ): JsonElement {
+ val production = productionIncrease.production
+ val json = context.serialize(production)
+ return if (!json.isJsonNull && !productionIncrease.isSellable) { JsonPrimitive("(${json.asString})") } else json
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(
+ json: JsonElement,
+ typeOfT: Type,
+ context: JsonDeserializationContext
+ ): ProductionIncrease {
+ var prodJson = json
+
+ var resourcesStr = prodJson.asString
+ val isSellable = !resourcesStr.startsWith("(")
+ if (!isSellable) {
+ resourcesStr = unwrapBrackets(resourcesStr)
+ prodJson = JsonPrimitive(resourcesStr)
+ }
+ val production = context.deserialize<Production>(prodJson, Production::class.java)
+ return ProductionIncrease(production, isSellable)
+ }
+
+ private fun unwrapBrackets(str: String): String {
+ return str.substring(1, str.length - 1)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt
new file mode 100644
index 00000000..b766dd31
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.kt
@@ -0,0 +1,58 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonParseException
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.Resources
+import java.lang.reflect.Type
+
+internal class ProductionSerializer : JsonSerializer<Production>, JsonDeserializer<Production> {
+
+ override fun serialize(production: Production, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
+ val fixedResources = production.getFixedResources()
+ val choices = production.getAlternativeResources()
+ return when {
+ fixedResources.isEmpty() -> serializeAsChoice(choices, context)
+ choices.isEmpty() -> serializeAsResources(fixedResources, context)
+ else -> throw IllegalArgumentException("Cannot serialize a production with mixed fixed resources and choices")
+ }
+ }
+
+ private fun serializeAsChoice(choices: Set<Set<ResourceType>>, context: JsonSerializationContext): JsonElement {
+ if (choices.isEmpty()) {
+ return JsonNull.INSTANCE
+ }
+ if (choices.size > 1) {
+ throw IllegalArgumentException("Cannot serialize a production with more than one choice")
+ }
+ val str = choices.flatMap { it }.map { it.symbol }.joinToString("/")
+ return context.serialize(str)
+ }
+
+ private fun serializeAsResources(fixedResources: Resources, context: JsonSerializationContext): JsonElement {
+ return context.serialize(fixedResources)
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Production {
+ val resourcesStr = json.asString
+ val production = Production()
+ if (resourcesStr.contains("/")) {
+ production.addChoice(*createChoice(resourcesStr))
+ } else {
+ val fixedResources = context.deserialize<Resources>(json, Resources::class.java)
+ production.addAll(fixedResources)
+ }
+ return production
+ }
+
+ private fun createChoice(choiceStr: String): Array<ResourceType> {
+ return choiceStr.split("/").map { ResourceType.fromSymbol(it) }.toTypedArray()
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.kt
new file mode 100644
index 00000000..99c364c5
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.kt
@@ -0,0 +1,26 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import java.lang.reflect.Type
+
+internal class ResourceTypeSerializer : JsonSerializer<ResourceType>, JsonDeserializer<ResourceType> {
+
+ override fun serialize(type: ResourceType, typeOfSrc: Type, context: JsonSerializationContext): JsonElement =
+ JsonPrimitive(type.symbol)
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ResourceType {
+ val str = json.asString
+ if (str.isEmpty()) {
+ throw IllegalArgumentException("Empty string is not a valid resource level")
+ }
+ return ResourceType.fromSymbol(str[0])
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.kt
new file mode 100644
index 00000000..b4784689
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.kt
@@ -0,0 +1,30 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import java.lang.reflect.Type
+
+internal class ResourceTypesSerializer : JsonSerializer<List<ResourceType>>, JsonDeserializer<List<ResourceType>> {
+
+ override fun serialize(
+ resources: List<ResourceType>,
+ typeOfSrc: Type,
+ context: JsonSerializationContext
+ ): JsonElement {
+ val s = resources.map { it.symbol }.joinToString("")
+ return JsonPrimitive(s)
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(
+ json: JsonElement,
+ typeOfT: Type,
+ context: JsonDeserializationContext
+ ): List<ResourceType> = json.asString.map { ResourceType.fromSymbol(it) }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt
new file mode 100644
index 00000000..1b1373ed
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.kt
@@ -0,0 +1,26 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.Resources
+import org.luxons.sevenwonders.game.resources.toResources
+import java.lang.reflect.Type
+
+internal class ResourcesSerializer : JsonSerializer<Resources>, JsonDeserializer<Resources> {
+
+ override fun serialize(resources: Resources, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
+ val s = resources.toList().map { it.symbol }.joinToString("")
+ return if (s.isEmpty()) JsonNull.INSTANCE else JsonPrimitive(s)
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Resources =
+ json.asString.map { ResourceType.fromSymbol(it) to 1 }.toResources()
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.kt
new file mode 100644
index 00000000..d6dc9ae3
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.kt
@@ -0,0 +1,55 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import org.luxons.sevenwonders.game.boards.Science
+import org.luxons.sevenwonders.game.boards.ScienceType
+import org.luxons.sevenwonders.game.effects.ScienceProgress
+import java.lang.reflect.Type
+
+internal class ScienceProgressSerializer : JsonSerializer<ScienceProgress>, JsonDeserializer<ScienceProgress> {
+
+ override fun serialize(
+ scienceProgress: ScienceProgress,
+ typeOfSrc: Type,
+ context: JsonSerializationContext
+ ): JsonElement {
+ val science = scienceProgress.science
+
+ if (science.size() > 1) {
+ throw UnsupportedOperationException("Cannot serialize science containing more than one element")
+ }
+
+ for (type in ScienceType.values()) {
+ val quantity = science.getQuantity(type)
+ if (quantity == 1) {
+ return context.serialize(type)
+ }
+ }
+
+ return if (science.jokers == 1) JsonPrimitive("any") else JsonNull.INSTANCE
+ }
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ScienceProgress {
+ val s = json.asString
+ val science = Science()
+ if ("any" == s) {
+ science.addJoker(1)
+ } else {
+ science.add(deserializeScienceType(json, context), 1)
+ }
+ return ScienceProgress(science)
+ }
+
+ private fun deserializeScienceType(json: JsonElement, context: JsonDeserializationContext): ScienceType {
+ return context.deserialize<ScienceType>(json, ScienceType::class.java)
+ ?: throw IllegalArgumentException("Invalid science level " + json.asString)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.kt
new file mode 100644
index 00000000..04dbf9be
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.kt
@@ -0,0 +1,36 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.cards.Color
+
+enum class BoardElementType {
+ CARD,
+ BUILT_WONDER_STAGES,
+ DEFEAT_TOKEN
+}
+
+internal data class BonusPerBoardElement(
+ val boards: List<RelativeBoardPosition>,
+ val type: BoardElementType,
+ val gold: Int = 0,
+ val points: Int = 0,
+ val colors: List<Color>? = null // only relevant if type=CARD
+) : Effect {
+
+ override fun applyTo(player: Player) = player.board.addGold(gold * nbMatchingElementsFor(player))
+
+ override fun computePoints(player: Player): Int = points * nbMatchingElementsFor(player)
+
+ private fun nbMatchingElementsFor(player: Player): Int = boards
+ .map(player::getBoard)
+ .map(::nbMatchingElementsIn)
+ .sum()
+
+ private fun nbMatchingElementsIn(board: Board): Int = when (type) {
+ BoardElementType.CARD -> board.getNbCardsOfColor(colors!!)
+ BoardElementType.BUILT_WONDER_STAGES -> board.wonder.nbBuiltStages
+ BoardElementType.DEFEAT_TOKEN -> board.military.nbDefeatTokens
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Discount.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Discount.kt
new file mode 100644
index 00000000..981ad9bd
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Discount.kt
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.resources.Provider
+import org.luxons.sevenwonders.game.resources.ResourceType
+
+internal data class Discount(
+ val resourceTypes: List<ResourceType> = emptyList(),
+ val providers: List<Provider> = emptyList(),
+ val discountedPrice: Int = 1
+) : InstantOwnBoardEffect() {
+
+ public override fun applyTo(board: Board) {
+ val rules = board.tradingRules
+ for (type in resourceTypes) {
+ providers.forEach { rules.setCost(type, it, discountedPrice) }
+ }
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Effect.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Effect.kt
new file mode 100644
index 00000000..55744669
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/Effect.kt
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+
+/**
+ * Represents an effect than can be applied to a player's board when playing a card or building his wonder. The effect
+ * may affect (or depend on) the adjacent boards. It can have an instantaneous effect on the board, or be postponed to
+ * the end of game where point calculations take place.
+ */
+internal interface Effect {
+
+ fun applyTo(player: Player)
+
+ fun computePoints(player: Player): Int
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/EndGameEffect.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/EndGameEffect.kt
new file mode 100644
index 00000000..b4e7a683
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/EndGameEffect.kt
@@ -0,0 +1,9 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+
+internal abstract class EndGameEffect : Effect {
+
+ // EndGameEffects don't do anything when applied to the board, they simply give more points in the end
+ override fun applyTo(player: Player) = Unit
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/GoldIncrease.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/GoldIncrease.kt
new file mode 100644
index 00000000..f6e37841
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/GoldIncrease.kt
@@ -0,0 +1,8 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.boards.Board
+
+internal data class GoldIncrease(val amount: Int) : InstantOwnBoardEffect() {
+
+ public override fun applyTo(board: Board) = board.addGold(amount)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.kt
new file mode 100644
index 00000000..0b50656c
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.kt
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+
+internal abstract class InstantOwnBoardEffect : Effect {
+
+ override fun applyTo(player: Player) = applyTo(player.board)
+
+ protected abstract fun applyTo(board: Board)
+
+ // InstantEffects are only important when applied to the board, they don't give extra points in the end
+ override fun computePoints(player: Player): Int = 0
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.kt
new file mode 100644
index 00000000..a168943c
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.kt
@@ -0,0 +1,8 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.boards.Board
+
+internal data class MilitaryReinforcements(val count: Int) : InstantOwnBoardEffect() {
+
+ public override fun applyTo(board: Board) = board.military.addShields(count)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncrease.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncrease.kt
new file mode 100644
index 00000000..4bd4a27f
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncrease.kt
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.resources.Production
+
+internal data class ProductionIncrease(val production: Production, val isSellable: Boolean) : InstantOwnBoardEffect() {
+
+ public override fun applyTo(board: Board) {
+ board.production.addAll(production)
+ if (isSellable) {
+ board.publicProduction.addAll(production)
+ }
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncrease.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncrease.kt
new file mode 100644
index 00000000..a47686da
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncrease.kt
@@ -0,0 +1,8 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+
+internal data class RawPointsIncrease(val points: Int) : EndGameEffect() {
+
+ override fun computePoints(player: Player): Int = points
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgress.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgress.kt
new file mode 100644
index 00000000..462330db
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgress.kt
@@ -0,0 +1,9 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.Science
+
+internal class ScienceProgress(val science: Science) : InstantOwnBoardEffect() {
+
+ public override fun applyTo(board: Board) = board.science.addAll(science)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbility.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbility.kt
new file mode 100644
index 00000000..d271134f
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbility.kt
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+
+enum class SpecialAbility {
+
+ /**
+ * The player can play the last card of each age instead of discarding it. This card can be played by paying its
+ * cost, discarded to gain 3 coins or used in the construction of his or her Wonder.
+ */
+ PLAY_LAST_CARD,
+
+ /**
+ * Once per age, a player can construct a building from his or her hand for free.
+ */
+ ONE_FREE_PER_AGE,
+
+ /**
+ * The player can look at all cards discarded since the beginning of the game, pick one and build it for free.
+ */
+ PLAY_DISCARDED,
+
+ /**
+ * The player can, at the end of the game, "copy" a Guild of his or her choice (purple card), built by one of his or
+ * her two neighboring cities.
+ */
+ COPY_GUILD {
+ override fun computePoints(player: Player): Int {
+ val copiedGuild = player.board.copiedGuild
+ ?: throw IllegalStateException("The copied Guild has not been chosen, cannot compute points")
+ return copiedGuild.effects.stream().mapToInt { it.computePoints(player) }.sum()
+ }
+ };
+
+ internal fun apply(board: Board) = board.addSpecial(this)
+
+ internal open fun computePoints(player: Player): Int = 0
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.kt
new file mode 100644
index 00000000..66521679
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.kt
@@ -0,0 +1,10 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.luxons.sevenwonders.game.Player
+
+internal data class SpecialAbilityActivation(val specialAbility: SpecialAbility) : Effect {
+
+ override fun applyTo(player: Player) = specialAbility.apply(player.board)
+
+ override fun computePoints(player: Player): Int = specialAbility.computePoints(player)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMove.kt
new file mode 100644
index 00000000..c4508401
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMove.kt
@@ -0,0 +1,23 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+
+internal class BuildWonderMove(move: PlayerMove, card: Card, player: PlayerContext) :
+ CardFromHandMove(move, card, player) {
+
+ private val wonder = player.board.wonder
+
+ init {
+ if (!wonder.isNextStageBuildable(playerContext.board, transactions)) {
+ throw InvalidMoveException(this, "all levels are already built, or the given resources are insufficient")
+ }
+ }
+
+ override fun place(discardedCards: MutableList<Card>, settings: Settings) = wonder.placeCard(card.back)
+
+ override fun activate(discardedCards: List<Card>, settings: Settings) =
+ wonder.activateLastBuiltStage(playerContext, transactions)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CardFromHandMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CardFromHandMove.kt
new file mode 100644
index 00000000..17d612af
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CardFromHandMove.kt
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+
+internal abstract class CardFromHandMove(move: PlayerMove, card: Card, player: PlayerContext) :
+ Move(move, card, player) {
+
+ init {
+ if (!player.hand.contains(card)) {
+ throw InvalidMoveException(this, "card '${card.name}' not in hand")
+ }
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CopyGuildMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CopyGuildMove.kt
new file mode 100644
index 00000000..3a7fe792
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/CopyGuildMove.kt
@@ -0,0 +1,37 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.effects.SpecialAbility
+
+internal class CopyGuildMove(move: PlayerMove, card: Card, player: PlayerContext) : Move(move, card, player) {
+
+ init {
+ val board = player.board
+ if (!board.hasSpecial(SpecialAbility.COPY_GUILD)) {
+ throw InvalidMoveException(this, "no ability to copy guild cards")
+ }
+ if (card.color !== Color.PURPLE) {
+ throw InvalidMoveException(this, "card '${card.name}' is not a guild card")
+ }
+ val leftNeighbourHasIt = neighbourHasTheCard(RelativeBoardPosition.LEFT)
+ val rightNeighbourHasIt = neighbourHasTheCard(RelativeBoardPosition.RIGHT)
+ if (!leftNeighbourHasIt && !rightNeighbourHasIt) {
+ throw InvalidMoveException(this, "neighbours don't have card '${card.name}'")
+ }
+ }
+
+ private fun neighbourHasTheCard(position: RelativeBoardPosition): Boolean =
+ playerContext.getBoard(position).getPlayedCards().contains(card)
+
+ // nothing special to do here
+ override fun place(discardedCards: MutableList<Card>, settings: Settings) = Unit
+
+ override fun activate(discardedCards: List<Card>, settings: Settings) {
+ playerContext.board.copiedGuild = card
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/DiscardMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/DiscardMove.kt
new file mode 100644
index 00000000..a8cd42a6
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/DiscardMove.kt
@@ -0,0 +1,18 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+
+internal class DiscardMove(move: PlayerMove, card: Card, player: PlayerContext) :
+ CardFromHandMove(move, card, player) {
+
+ override fun place(discardedCards: MutableList<Card>, settings: Settings) {
+ discardedCards.add(card)
+ }
+
+ override fun activate(discardedCards: List<Card>, settings: Settings) {
+ playerContext.board.addGold(settings.discardedCardGold)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt
new file mode 100644
index 00000000..98a96fd9
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/Move.kt
@@ -0,0 +1,33 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+
+internal abstract class Move(
+ val move: PlayerMove,
+ val card: Card,
+ val playerContext: PlayerContext
+) {
+ val type: MoveType = move.type
+
+ val transactions: ResourceTransactions = move.transactions
+
+ abstract fun place(discardedCards: MutableList<Card>, settings: Settings)
+
+ abstract fun activate(discardedCards: List<Card>, settings: Settings)
+}
+
+class InvalidMoveException internal constructor(move: Move, message: String) : IllegalArgumentException(
+ "Player ${move.playerContext.index} cannot perform move ${move.type}: $message"
+)
+
+internal fun MoveType.resolve(move: PlayerMove, card: Card, context: PlayerContext): Move = when (this) {
+ MoveType.PLAY -> PlayCardMove(move, card, context)
+ MoveType.PLAY_FREE -> PlayFreeCardMove(move, card, context)
+ MoveType.UPGRADE_WONDER -> BuildWonderMove(move, card, context)
+ MoveType.DISCARD -> DiscardMove(move, card, context)
+ MoveType.COPY_GUILD -> CopyGuildMove(move, card, context)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayCardMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayCardMove.kt
new file mode 100644
index 00000000..3596b164
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayCardMove.kt
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+
+internal class PlayCardMove(move: PlayerMove, card: Card, player: PlayerContext) :
+ CardFromHandMove(move, card, player) {
+
+ init {
+ if (!card.isPlayableOnBoardWith(player.board, transactions)) {
+ throw InvalidMoveException(this, "requirements not met to play the card ${card.name}")
+ }
+ }
+
+ override fun place(discardedCards: MutableList<Card>, settings: Settings) = playerContext.board.addCard(card)
+
+ override fun activate(discardedCards: List<Card>, settings: Settings) = card.applyTo(playerContext, transactions)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.kt
new file mode 100644
index 00000000..bac185d9
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.kt
@@ -0,0 +1,25 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.cards.Card
+
+internal class PlayFreeCardMove(move: PlayerMove, card: Card, playerContext: PlayerContext) :
+ CardFromHandMove(move, card, playerContext) {
+
+ init {
+ val board = playerContext.board
+ if (!board.canPlayFreeCard(playerContext.currentAge)) {
+ throw InvalidMoveException(this, "no free card available for the current age ${playerContext.currentAge}")
+ }
+ }
+
+ override fun place(discardedCards: MutableList<Card>, settings: Settings) = playerContext.board.addCard(card)
+
+ override fun activate(discardedCards: List<Card>, settings: Settings) {
+ // only apply effects, without paying the cost
+ card.effects.forEach { it.applyTo(playerContext) }
+ playerContext.board.consumeFreeCard(playerContext.currentAge)
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt
new file mode 100644
index 00000000..dea6f2c2
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculator.kt
@@ -0,0 +1,121 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.luxons.sevenwonders.game.Player
+import java.util.EnumSet
+
+internal fun bestSolution(resources: Resources, player: Player): TransactionPlan =
+ BestPriceCalculator(resources, player).computeBestSolution()
+
+data class TransactionPlan(val price: Int, val possibleTransactions: Set<ResourceTransactions>)
+
+private class ResourcePool(
+ val provider: Provider?,
+ private val rules: TradingRules,
+ choices: Set<Set<ResourceType>>
+) {
+ val choices: Set<MutableSet<ResourceType>> = choices.map { it.toMutableSet() }.toSet()
+
+ fun getCost(type: ResourceType): Int = if (provider == null) 0 else rules.getCost(type, provider)
+}
+
+private class BestPriceCalculator(resourcesToPay: Resources, player: Player) {
+
+ private val pools: List<ResourcePool>
+ private val resourcesLeftToPay: MutableResources
+ private val boughtResources: MutableMap<Provider, MutableResources> = HashMap()
+ private var pricePaid: Int = 0
+
+ private var bestSolutions: MutableSet<ResourceTransactions> = mutableSetOf()
+ private var bestPrice: Int = Integer.MAX_VALUE
+
+ init {
+ val board = player.board
+ this.resourcesLeftToPay = resourcesToPay.minus(board.production.getFixedResources()).toMutableResources()
+ this.pools = createResourcePools(player)
+ }
+
+ private fun createResourcePools(player: Player): List<ResourcePool> {
+ // we only take alternative resources here, because fixed resources were already removed for optimization
+ val ownBoardChoices = player.board.production.getAlternativeResources()
+ val ownPool = ResourcePool(null, player.board.tradingRules, ownBoardChoices)
+ val providerPools = Provider.values().map { it.toResourcePoolFor(player) }
+
+ return providerPools + ownPool
+ }
+
+ private fun Provider.toResourcePoolFor(player: Player): ResourcePool {
+ val providerBoard = player.getBoard(boardPosition)
+ val choices = providerBoard.publicProduction.asChoices()
+ return ResourcePool(this, player.board.tradingRules, choices)
+ }
+
+ fun computeBestSolution(): TransactionPlan {
+ computePossibilities()
+ return TransactionPlan(bestPrice, bestSolutions)
+ }
+
+ private fun computePossibilities() {
+ if (resourcesLeftToPay.isEmpty()) {
+ updateBestSolutionIfNeeded()
+ return
+ }
+ for (type in ResourceType.values()) {
+ if (resourcesLeftToPay[type] > 0) {
+ for (pool in pools) {
+ if (pool.provider == null) {
+ computeSelfPossibilities(type, pool)
+ } else {
+ computeNeighbourPossibilities(pool, type, pool.provider)
+ }
+ }
+ }
+ }
+ }
+
+ private fun computeSelfPossibilities(type: ResourceType, pool: ResourcePool) {
+ resourcesLeftToPay.remove(type, 1)
+ computePossibilitiesWhenUsing(type, pool)
+ resourcesLeftToPay.add(type, 1)
+ }
+
+ private fun computeNeighbourPossibilities(pool: ResourcePool, type: ResourceType, provider: Provider) {
+ val cost = pool.getCost(type)
+ resourcesLeftToPay.remove(type, 1)
+ buyOne(provider, type, cost)
+ computePossibilitiesWhenUsing(type, pool)
+ unbuyOne(provider, type, cost)
+ resourcesLeftToPay.add(type, 1)
+ }
+
+ fun buyOne(provider: Provider, type: ResourceType, cost: Int) {
+ boughtResources.getOrPut(provider) { MutableResources() }.add(type, 1)
+ pricePaid += cost
+ }
+
+ fun unbuyOne(provider: Provider, type: ResourceType, cost: Int) {
+ pricePaid -= cost
+ boughtResources.get(provider)!!.remove(type, 1)
+ }
+
+ private fun computePossibilitiesWhenUsing(type: ResourceType, pool: ResourcePool) {
+ for (choice in pool.choices) {
+ if (choice.contains(type)) {
+ val temp = EnumSet.copyOf(choice)
+ choice.clear()
+ computePossibilities()
+ choice.addAll(temp)
+ }
+ }
+ }
+
+ private fun updateBestSolutionIfNeeded() {
+ if (pricePaid > bestPrice) return
+
+ if (pricePaid < bestPrice) {
+ bestPrice = pricePaid
+ bestSolutions.clear()
+ }
+ // avoid mutating the resources from the transactions
+ bestSolutions.add(boughtResources.mapValues { (_, res) -> res.copy() }.toTransactions())
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt
new file mode 100644
index 00000000..66a63463
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Production.kt
@@ -0,0 +1,66 @@
+package org.luxons.sevenwonders.game.resources
+
+import java.util.EnumSet
+
+data class Production internal constructor(
+ private val fixedResources: MutableResources = mutableResourcesOf(),
+ private val alternativeResources: MutableSet<Set<ResourceType>> = mutableSetOf()
+) {
+ fun getFixedResources(): Resources = fixedResources
+
+ fun getAlternativeResources(): Set<Set<ResourceType>> = alternativeResources
+
+ fun addFixedResource(type: ResourceType, quantity: Int) = fixedResources.add(type, quantity)
+
+ fun addChoice(vararg options: ResourceType) {
+ val optionSet = EnumSet.copyOf(options.toList())
+ alternativeResources.add(optionSet)
+ }
+
+ fun addAll(resources: Resources) = fixedResources.add(resources)
+
+ fun addAll(production: Production) {
+ fixedResources.add(production.fixedResources)
+ alternativeResources.addAll(production.getAlternativeResources())
+ }
+
+ internal fun asChoices(): Set<Set<ResourceType>> {
+ val fixedAsChoices = fixedResources.toList().map { EnumSet.of(it) }.toSet()
+ return fixedAsChoices + alternativeResources
+ }
+
+ operator fun contains(resources: Resources): Boolean {
+ if (fixedResources.containsAll(resources)) {
+ return true
+ }
+ return containedInAlternatives(resources - fixedResources)
+ }
+
+ private fun containedInAlternatives(resources: Resources): Boolean =
+ containedInAlternatives(resources.toMutableResources(), alternativeResources)
+
+ private fun containedInAlternatives(
+ resources: MutableResources,
+ alternatives: MutableSet<Set<ResourceType>>
+ ): Boolean {
+ if (resources.isEmpty()) {
+ return true
+ }
+ for (type in ResourceType.values()) {
+ if (resources[type] <= 0) {
+ continue
+ }
+ // return if no alternative produces the resource of this entry
+ val candidate = alternatives.firstOrNull { a -> a.contains(type) } ?: return false
+ resources.remove(type, 1)
+ alternatives.remove(candidate)
+ val remainingAreContainedToo = containedInAlternatives(resources, alternatives)
+ resources.add(type, 1)
+ alternatives.add(candidate)
+ if (remainingAreContainedToo) {
+ return true
+ }
+ }
+ return false
+ }
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt
new file mode 100644
index 00000000..4a3a483c
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactions.kt
@@ -0,0 +1,29 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.api.ApiCountedResource
+import org.luxons.sevenwonders.game.api.toCountedResourcesList
+
+fun Map<Provider, Resources>.toTransactions(): ResourceTransactions =
+ filterValues { !it.isEmpty() }
+ .map { (p, res) -> ResourceTransaction(p, res.toCountedResourcesList()) }
+ .toSet()
+
+fun ResourceTransactions.asResources(): Resources = flatMap { it.resources }.asResources()
+
+fun ResourceTransaction.asResources(): Resources = resources.asResources()
+
+fun List<ApiCountedResource>.asResources(): Resources = map { it.asResources() }.merge()
+
+fun ApiCountedResource.asResources(): Resources = resourcesOf(type to count)
+
+internal fun ResourceTransactions.execute(player: Player) = forEach { it.execute(player) }
+
+internal fun ResourceTransaction.execute(player: Player) {
+ val board = player.board
+ val price = board.tradingRules.computeCost(this)
+ board.removeGold(price)
+ val providerPosition = provider.boardPosition
+ val providerBoard = player.getBoard(providerPosition)
+ providerBoard.addGold(price)
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt
new file mode 100644
index 00000000..6ffda080
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/Resources.kt
@@ -0,0 +1,87 @@
+package org.luxons.sevenwonders.game.resources
+
+fun emptyResources(): Resources = MutableResources()
+
+fun resourcesOf(singleResource: ResourceType): Resources = mapOf(singleResource to 1).toMutableResources()
+
+fun resourcesOf(vararg resources: ResourceType): Resources = mutableResourcesOf(*resources)
+
+fun resourcesOf(vararg resources: Pair<ResourceType, Int>): Resources = mutableResourcesOf(*resources)
+
+fun Iterable<Pair<ResourceType, Int>>.toResources(): Resources = toMutableResources()
+
+/**
+ * Creates [Resources] from a copy of the given map. Future modifications to the input map won't affect the return
+ * resources.
+ */
+fun Map<ResourceType, Int>.toResources(): Resources = toMutableResources()
+
+fun Iterable<Resources>.merge(): Resources = fold(MutableResources()) { r1, r2 -> r1.add(r2); r1 }
+
+internal fun mutableResourcesOf() = MutableResources()
+
+internal fun mutableResourcesOf(vararg resources: ResourceType): MutableResources =
+ resources.map { it to 1 }.toMutableResources()
+
+internal fun mutableResourcesOf(vararg resources: Pair<ResourceType, Int>) = resources.asIterable().toMutableResources()
+
+internal fun Iterable<Pair<ResourceType, Int>>.toMutableResources(): MutableResources =
+ fold(MutableResources()) { mr, (type, qty) -> mr.add(type, qty); mr }
+
+internal fun Map<ResourceType, Int>.toMutableResources(): MutableResources = MutableResources(toMutableMap())
+
+internal fun Resources.toMutableResources(): MutableResources = quantities.toMutableResources()
+
+interface Resources {
+
+ val quantities: Map<ResourceType, Int>
+
+ val size: Int
+ get() = quantities.map { it.value }.sum()
+
+ fun isEmpty(): Boolean = size == 0
+
+ operator fun get(key: ResourceType): Int = quantities.getOrDefault(key, 0)
+
+ fun containsAll(resources: Resources): Boolean = resources.quantities.all { it.value <= this[it.key] }
+
+ operator fun plus(resources: Resources): Resources =
+ ResourceType.values().map { it to this[it] + resources[it] }.toResources()
+
+ /**
+ * Returns new resources containing these resources minus the given [resources]. If the given resources contain
+ * more than these resources contain for a resource type, then the resulting resources will contain none of that
+ * type.
+ */
+ operator fun minus(resources: Resources): Resources =
+ quantities.mapValues { (type, q) -> Math.max(0, q - resources[type]) }.toResources()
+
+ fun toList(): List<ResourceType> = quantities.flatMap { (type, quantity) -> List(quantity) { type } }
+
+ fun copy(): Resources = quantities.toResources()
+}
+
+class MutableResources(
+ override val quantities: MutableMap<ResourceType, Int> = mutableMapOf()
+) : Resources {
+
+ fun add(type: ResourceType, quantity: Int) {
+ quantities.merge(type, quantity) { x, y -> x + y }
+ }
+
+ fun add(resources: Resources) = resources.quantities.forEach { type, quantity -> this.add(type, quantity) }
+
+ fun remove(type: ResourceType, quantity: Int) {
+ if (this[type] < quantity) {
+ throw NoSuchElementException("Can't remove $quantity resources of type $type")
+ }
+ quantities.computeIfPresent(type) { _, oldQty -> oldQty - quantity }
+ }
+
+ override fun equals(other: Any?): Boolean =
+ other is Resources && quantities.filterValues { it > 0 } == other.quantities.filterValues { it > 0 }
+
+ override fun hashCode(): Int = quantities.filterValues { it > 0 }.hashCode()
+
+ override fun toString(): String = "$quantities"
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt
new file mode 100644
index 00000000..a006fadf
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/resources/TradingRules.kt
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.resources
+
+class TradingRules internal constructor(private val defaultCost: Int) {
+
+ private val costs: MutableMap<ResourceType, MutableMap<Provider, Int>> = mutableMapOf()
+
+ fun getCosts(): Map<ResourceType, Map<Provider, Int>> {
+ return costs
+ }
+
+ internal fun getCost(type: ResourceType, provider: Provider): Int =
+ costs.computeIfAbsent(type) { mutableMapOf() }.getOrDefault(provider, defaultCost)
+
+ internal fun setCost(type: ResourceType, provider: Provider, cost: Int) {
+ costs.computeIfAbsent(type) { mutableMapOf() }[provider] = cost
+ }
+
+ internal fun computeCost(transactions: ResourceTransactions): Int = transactions.map { computeCost(it) }.sum()
+
+ internal fun computeCost(transact: ResourceTransaction) = computeCost(transact.asResources(), transact.provider)
+
+ private fun computeCost(resources: Resources, provider: Provider): Int =
+ resources.quantities.map { (type, qty) -> getCost(type, provider) * qty }.sum()
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/score/Score.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/score/Score.kt
new file mode 100644
index 00000000..c1d34d5d
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/score/Score.kt
@@ -0,0 +1,23 @@
+package org.luxons.sevenwonders.game.score
+
+class ScoreBoard(scores: Collection<PlayerScore>) {
+
+ val scores: Collection<PlayerScore> = scores.sortedDescending()
+}
+
+data class PlayerScore(val boardGold: Int, val pointsByCategory: Map<ScoreCategory, Int>) : Comparable<PlayerScore> {
+
+ val totalPoints = pointsByCategory.map { it.value }.sum()
+
+ override fun compareTo(other: PlayerScore) = compareValuesBy(this, other, { it.totalPoints }, { it.boardGold })
+}
+
+enum class ScoreCategory {
+ CIVIL,
+ SCIENCE,
+ MILITARY,
+ TRADE,
+ GUILD,
+ WONDER,
+ GOLD
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt
new file mode 100644
index 00000000..fc2e8676
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/Wonder.kt
@@ -0,0 +1,62 @@
+package org.luxons.sevenwonders.game.wonders
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.PlayabilityLevel
+import org.luxons.sevenwonders.game.cards.RequirementsSatisfaction
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.ResourceType
+
+internal class Wonder(
+ val name: String,
+ val initialResource: ResourceType,
+ val stages: List<WonderStage>,
+ val image: String
+) {
+ val nbBuiltStages: Int
+ get() = stages.count { it.isBuilt }
+
+ private val nextStage: WonderStage
+ get() {
+ if (nbBuiltStages == stages.size) {
+ throw IllegalStateException("This wonder has already reached its maximum level")
+ }
+ return stages[nbBuiltStages]
+ }
+
+ val lastBuiltStage: WonderStage?
+ get() = stages.getOrNull(nbBuiltStages - 1)
+
+ fun computeBuildabilityBy(player: Player): WonderBuildability {
+ if (nbBuiltStages == stages.size) {
+ return Buildability.alreadyBuilt()
+ }
+ return Buildability.requirementDependent(nextStage.requirements.assess(player))
+ }
+
+ fun isNextStageBuildable(board: Board, boughtResources: ResourceTransactions): Boolean =
+ nbBuiltStages < stages.size && nextStage.isBuildable(board, boughtResources)
+
+ fun placeCard(cardBack: CardBack) = nextStage.placeCard(cardBack)
+
+ fun activateLastBuiltStage(player: Player, boughtResources: ResourceTransactions) =
+ lastBuiltStage!!.activate(player, boughtResources)
+
+ fun computePoints(player: Player): Int =
+ stages.filter { it.isBuilt }.flatMap { it.effects }.sumBy { it.computePoints(player) }
+}
+
+private object Buildability {
+
+ fun alreadyBuilt() = WonderBuildability(
+ isBuildable = false, playabilityLevel = PlayabilityLevel.INCOMPATIBLE_WITH_BOARD
+ )
+
+ internal fun requirementDependent(satisfaction: RequirementsSatisfaction) = WonderBuildability(
+ isBuildable = satisfaction.satisfied,
+ minPrice = satisfaction.minPrice,
+ cheapestTransactions = satisfaction.cheapestTransactions,
+ playabilityLevel = satisfaction.level
+ )
+}
diff --git a/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt
new file mode 100644
index 00000000..311e589e
--- /dev/null
+++ b/sw-engine/src/main/kotlin/org/luxons/sevenwonders/game/wonders/WonderStage.kt
@@ -0,0 +1,31 @@
+package org.luxons.sevenwonders.game.wonders
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Requirements
+import org.luxons.sevenwonders.game.effects.Effect
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+
+internal class WonderStage(
+ val requirements: Requirements,
+ val effects: List<Effect>
+) {
+ var cardBack: CardBack? = null
+ private set
+
+ val isBuilt: Boolean
+ get() = cardBack != null
+
+ fun isBuildable(board: Board, boughtResources: ResourceTransactions): Boolean =
+ requirements.areMetWithHelpBy(board, boughtResources)
+
+ fun placeCard(cardBack: CardBack) {
+ this.cardBack = cardBack
+ }
+
+ fun activate(player: Player, boughtResources: ResourceTransactions) {
+ effects.forEach { it.applyTo(player) }
+ requirements.pay(player, boughtResources)
+ }
+}
diff --git a/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
new file mode 100644
index 00000000..bd2d5893
--- /dev/null
+++ b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
@@ -0,0 +1,1462 @@
+{
+ "age1": {
+ "cards": [
+ {
+ "name": "Clay Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "O/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "claypit.png"
+ }, {
+ "name": "Clay Pool",
+ "color": "BROWN",
+ "effect": {
+ "production": "C"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "claypool.png"
+ }, {
+ "name": "Excavation",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "excavation.png"
+ }, {
+ "name": "Forest Cave",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "forestcave.png"
+ }, {
+ "name": "Lumber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lumberyard.png"
+ }, {
+ "name": "Mine",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "mine.png"
+ }, {
+ "name": "Ore Vein",
+ "color": "BROWN",
+ "effect": {
+ "production": "O"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "orevein.png"
+ }, {
+ "name": "Stone Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "S"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stonepit.png"
+ }, {
+ "name": "Timber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/S"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "timberyard.png"
+ }, {
+ "name": "Tree Farm",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "treefarm.png"
+ }, {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ }, {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ }, {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ }, {
+ "name": "East Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "easttradingpost.png"
+ }, {
+ "name": "Marketplace",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "LGP",
+ "providers": [
+ "LEFT_PLAYER", "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "chainChildren": [
+ "Caravansery"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "marketplace.png"
+ }, {
+ "name": "Tavern",
+ "color": "YELLOW",
+ "effect": {
+ "gold": 5
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "tavern.png"
+ }, {
+ "name": "West Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "LEFT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "westtradingpost.png"
+ }, {
+ "name": "Altar",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "chainChildren": [
+ "Temple"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "altar.png"
+ }, {
+ "name": "Baths",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "resources": "S"
+ },
+ "chainChildren": [
+ "Aquaduct"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "baths.png"
+ }, {
+ "name": "Pawnshop",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "pawnshop.png"
+ }, {
+ "name": "Theater",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "chainChildren": [
+ "Statue"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "theater.png"
+ }, {
+ "name": "Apothecary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "resources": "L"
+ },
+ "chainChildren": [
+ "Stables", "Dispensary"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "apothecary.png"
+ }, {
+ "name": "Scriptorium",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "resources": "P"
+ },
+ "chainChildren": [
+ "Courthouse", "Library"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "scriptorium.png"
+ }, {
+ "name": "Workshop",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "resources": "G"
+ },
+ "chainChildren": [
+ "Archery Range", "Laboratory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "workshop.png"
+ }, {
+ "name": "Barracks",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "resources": "O"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "barracks.png"
+ }, {
+ "name": "Guard Tower",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "resources": "C"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "guardtower.png"
+ }, {
+ "name": "Stockade",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "resources": "W"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "stockade.png"
+ }
+ ],
+ "backImage": "age1.png"
+ },
+ "age2": {
+ "cards": [
+ {
+ "name": "Brickyard",
+ "color": "BROWN",
+ "effect": {
+ "production": "CC"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "brickyard.png"
+ }, {
+ "name": "Foundry",
+ "color": "BROWN",
+ "effect": {
+ "production": "OO"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "foundry.png"
+ }, {
+ "name": "Quarry",
+ "color": "BROWN",
+ "effect": {
+ "production": "SS"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "quarry.png"
+ }, {
+ "name": "Sawmill",
+ "color": "BROWN",
+ "effect": {
+ "production": "WW"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "sawmill.png"
+ }, {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ }, {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ }, {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ }, {
+ "name": "Bazar",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF", "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "bazar.png"
+ }, {
+ "name": "Caravansery",
+ "color": "YELLOW",
+ "effect": {
+ "production": "(W/S/O/C)"
+ },
+ "requirements": {
+ "resources": "WW"
+ },
+ "chainParent": "Marketplace",
+ "chainChildren": [
+ "Lighthouse"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "caravansery.png"
+ }, {
+ "name": "Forum",
+ "color": "YELLOW",
+ "effect": {
+ "production": "(G/P/L)"
+ },
+ "requirements": {
+ "resources": "CC"
+ },
+ "chainParent": "East Trading Post",
+ "chainChildren": [
+ "Haven"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "forum.png"
+ }, {
+ "name": "Vineyard",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF", "LEFT", "RIGHT"
+ ],
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "vineyard.png"
+ }, {
+ "name": "Aqueduct",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "resources": "SSS"
+ },
+ "chainParent": "Baths",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "aqueduct.png"
+ }, {
+ "name": "Courthouse",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "resources": "CCL"
+ },
+ "chainParent": "Scriptorium",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "courthouse.png"
+ }, {
+ "name": "Statue",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "resources": "WOO"
+ },
+ "chainParent": "Theater",
+ "chainChildren": [
+ "Gardens"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "statue.png"
+ }, {
+ "name": "Temple",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "resources": "WCG"
+ },
+ "chainParent": "Altar",
+ "chainChildren": [
+ "Pantheon"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "temple.png"
+ }, {
+ "name": "Dispensary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "resources": "OOG"
+ },
+ "chainParent": "Apothecary",
+ "chainChildren": [
+ "Arena", "Lodge"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "dispensary.png"
+ }, {
+ "name": "Laboratory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "resources": "CCP"
+ },
+ "chainParent": "Workshop",
+ "chainChildren": [
+ "Siege Workshop", "Observatory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "laboratory.png"
+ }, {
+ "name": "Library",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "resources": "SSL"
+ },
+ "chainParent": "Scriptorium",
+ "chainChildren": [
+ "Senate", "University"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "library.png"
+ }, {
+ "name": "School",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "resources": "WP"
+ },
+ "chainChildren": [
+ "Academy", "Study"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "school.png"
+ }, {
+ "name": "Archery Range",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "resources": "WWO"
+ },
+ "chainParent": "Workshop",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "archeryrange.png"
+ }, {
+ "name": "Stables",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "resources": "WOC"
+ },
+ "chainParent": "Apothecary",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stables.png"
+ }, {
+ "name": "Training Ground",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "resources": "WOO"
+ },
+ "chainChildren": [
+ "Circus"
+ ],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "trainingground.png"
+ }, {
+ "name": "Walls",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "resources": "SSS"
+ },
+ "chainChildren": [
+ "Fortifications"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "walls.png"
+ }
+ ],
+ "backImage": "age2.png"
+ },
+ "age3": {
+ "cards": [
+ {
+ "name": "Arena",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 3,
+ "points": 1,
+ "type": "BUILT_WONDER_STAGES"
+ }
+ },
+ "requirements": {
+ "resources": "SSO"
+ },
+ "chainParent": "Dispensary",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arena.png"
+ }, {
+ "name": "Chamber of Commerce",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 2,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "CCP"
+ },
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "chamberofcommerce.png"
+ }, {
+ "name": "Haven",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "WOL"
+ },
+ "chainParent": "Forum",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "haven.png"
+ }, {
+ "name": "Lighthouse",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "SG"
+ },
+ "chainParent": "Caravansery",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lighthouse.png"
+ }, {
+ "name": "Gardens",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "resources": "WCC"
+ },
+ "chainParent": "Statue",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "gardens.png"
+ }, {
+ "name": "Palace",
+ "color": "BLUE",
+ "effect": {
+ "points": 8
+ },
+ "requirements": {
+ "resources": "WSOCGPL"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "palace.png"
+ }, {
+ "name": "Pantheon",
+ "color": "BLUE",
+ "effect": {
+ "points": 7
+ },
+ "requirements": {
+ "resources": "OCCGPL"
+ },
+ "chainParent": "Temple",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "pantheon.png"
+ }, {
+ "name": "Senate",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "resources": "WWSO"
+ },
+ "chainParent": "Library",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "senate.png"
+ }, {
+ "name": "Town Hall",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "resources": "SSOG"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "townhall.png"
+ }, {
+ "name": "Academy",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "resources": "SSSG"
+ },
+ "chainParent": "School",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "academy.png"
+ }, {
+ "name": "Lodge",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "resources": "CCPL"
+ },
+ "chainParent": "Dispensary",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lodge.png"
+ }, {
+ "name": "Observatory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "resources": "OOGL"
+ },
+ "chainParent": "Laboratory",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "observatory.png"
+ }, {
+ "name": "Study",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "resources": "WPL"
+ },
+ "chainParent": "School",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "study.png"
+ }, {
+ "name": "University",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "resources": "WWGP"
+ },
+ "chainParent": "Library",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "university.png"
+ }, {
+ "name": "Arsenal",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "resources": "WWOL"
+ },
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arsenal.png"
+ }, {
+ "name": "Circus",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "resources": "SSSO"
+ },
+ "chainParent": "Training Ground",
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "circus.png"
+ }, {
+ "name": "Fortifications",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "resources": "SOOO"
+ },
+ "chainParent": "Walls",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "fortifications.png"
+ }, {
+ "name": "Siege Workshop",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "resources": "WCCC"
+ },
+ "chainParent": "Laboratory",
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "siegeworkshop.png"
+ }
+ ],
+ "backImage": "age3.png"
+ },
+ "guildCards": [
+ {
+ "name": "Builders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "SELF", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "BUILT_WONDER_STAGES"
+ }
+ },
+ "requirements": {
+ "resources": "SSCCG"
+ },
+ "image": "buildersguild.png"
+ }, {
+ "name": "Craftsmens Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "SSOO"
+ },
+ "image": "craftsmensguild.png"
+ }, {
+ "name": "Magistrates Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BLUE"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "WWWSL"
+ },
+ "image": "magistratesguild.png"
+ }, {
+ "name": "Philosophers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREEN"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "CCCPL"
+ },
+ "image": "philosophersguild.png"
+ }, {
+ "name": "Scientists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "science": "any"
+ },
+ "requirements": {
+ "resources": "WWOOP"
+ },
+ "image": "scientistsguild.png"
+ }, {
+ "name": "Shipowners Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN", "GREY", "PURPLE"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "WWWGP"
+ },
+ "image": "shipownersguild.png"
+ }, {
+ "name": "Spies Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "RED"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "CCCG"
+ },
+ "image": "spiesguild.png"
+ }, {
+ "name": "Strategists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "DEFEAT_TOKEN"
+ }
+ },
+ "requirements": {
+ "resources": "SOOL"
+ },
+ "image": "strategistsguild.png"
+ }, {
+ "name": "Traders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "YELLOW"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "GPL"
+ },
+ "image": "tradersguild.png"
+ }, {
+ "name": "Workers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT", "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "resources": "WSOOC"
+ },
+ "image": "workersguild.png"
+ }
+ ]
+}
diff --git a/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/global_rules.json b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/global_rules.json
new file mode 100644
index 00000000..9b486fe6
--- /dev/null
+++ b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/global_rules.json
@@ -0,0 +1,4 @@
+{
+ "minPlayers": 3,
+ "maxPlayers": 7
+}
diff --git a/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
new file mode 100644
index 00000000..fc3ac69d
--- /dev/null
+++ b/sw-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
@@ -0,0 +1,473 @@
+[
+ {
+ "name": "alexandria",
+ "sides": {
+ "A": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OO"
+ },
+ "effects": {
+ "production": "(W/S/O/C)"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "GG"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaA.png"
+ },
+ "B": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "CC"
+ },
+ "effects": {
+ "production": "(W/S/O/C)"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "production": "(G/P/L)"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaB.png"
+ }
+ }
+ },
+ {
+ "name": "babylon",
+ "sides": {
+ "A": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WWW"
+ },
+ "effects": {
+ "science": "any"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "CCCC"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "babylonA.png"
+ },
+ "B": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "CL"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WWG"
+ },
+ "effects": {
+ "action": "PLAY_LAST_CARD"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "CCCP"
+ },
+ "effects": {
+ "science": "any"
+ }
+ }
+ ],
+ "image": "babylonB.png"
+ }
+ }
+ },
+ {
+ "name": "ephesos",
+ "sides": {
+ "A": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 9
+ }
+ },
+ {
+ "requirements": {
+ "resources": "PP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "ephesosA.png"
+ },
+ "B": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 2
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "GPL"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 5
+ }
+ }
+ ],
+ "image": "ephesosB.png"
+ }
+ }
+ },
+ {
+ "name": "gizah",
+ "sides": {
+ "A": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "WWW"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SSSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahA.png"
+ },
+ "B": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SSSSP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahB.png"
+ }
+ }
+ },
+ {
+ "name": "halikarnassus",
+ "sides": {
+ "A": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OOO"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "LL"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "halikarnassusA.png"
+ },
+ "B": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 2,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 1,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "GPL"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ }
+ ],
+ "image": "halikarnassusB.png"
+ }
+ }
+ },
+ {
+ "name": "olympia",
+ "sides": {
+ "A": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "action": "ONE_FREE"
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "olympiaA.png"
+ },
+ "B": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "discount": {
+ "resourceTypes": "WSOC",
+ "providers": [
+ "LEFT_PLAYER",
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ }
+ },
+ {
+ "requirements": {
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OOL"
+ },
+ "effects": {
+ "action": "COPY_GUILD"
+ }
+ }
+ ],
+ "image": "olympiaB.png"
+ }
+ }
+ },
+ {
+ "name": "rhodos",
+ "sides": {
+ "A": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "CCC"
+ },
+ "effects": {
+ "military": 2
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OOOO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "rhodosA.png"
+ },
+ "B": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "resources": "SSS"
+ },
+ "effects": {
+ "gold": 3,
+ "military": 1,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "resources": "OOOO"
+ },
+ "effects": {
+ "gold": 4,
+ "military": 1,
+ "points": 4
+ }
+ }
+ ],
+ "image": "rhodosB.png"
+ }
+ }
+ }
+]
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt
new file mode 100644
index 00000000..a63ccdd6
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/GameTest.kt
@@ -0,0 +1,123 @@
+package org.luxons.sevenwonders.game
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.api.Action
+import org.luxons.sevenwonders.game.cards.HandCard
+import org.luxons.sevenwonders.game.api.PlayedMove
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.api.PlayerTurnInfo
+import org.luxons.sevenwonders.game.cards.TableCard
+import org.luxons.sevenwonders.game.data.GameDefinition
+import org.luxons.sevenwonders.game.data.LAST_AGE
+import org.luxons.sevenwonders.game.moves.MoveType
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.noTransactions
+import org.luxons.sevenwonders.game.test.testCustomizableSettings
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class GameTest {
+
+ @Test
+ fun testFullGame3Players() = playGame(nbPlayers = 3)
+
+ @Test
+ fun testFullGame5Players() = playGame(nbPlayers = 6)
+
+ @Test
+ fun testFullGame7Players() = playGame(nbPlayers = 7)
+
+ private fun playGame(nbPlayers: Int) {
+ val game = createGame(nbPlayers)
+
+ (1..LAST_AGE).forEach { playAge(nbPlayers, game, it) }
+
+ game.computeScore()
+ }
+
+ private fun playAge(nbPlayers: Int, game: Game, age: Int) {
+ repeat(6) {
+ playTurn(nbPlayers, game, age, 7 - it)
+ }
+ }
+
+ private fun createGame(nbPlayers: Int): Game =
+ GameDefinition.load().initGame(0, testCustomizableSettings(), nbPlayers)
+
+ private fun playTurn(nbPlayers: Int, game: Game, ageToCheck: Int, handSize: Int) {
+ val turnInfos = game.getCurrentTurnInfo()
+ assertEquals(nbPlayers, turnInfos.size)
+ turnInfos.forEach {
+ assertEquals(ageToCheck, it.currentAge)
+ assertEquals(handSize, it.hand.size)
+ }
+
+ val moveExpectations = turnInfos.mapNotNull { it.firstAvailableMove() }
+
+ moveExpectations.forEach { game.prepareMove(it.playerIndex, it.moveToSend) }
+ assertTrue(game.allPlayersPreparedTheirMove())
+
+ val table = game.playTurn()
+
+ val expectedMoves = moveExpectations.map { it.expectedPlayedMove }
+ assertEquals(expectedMoves, table.lastPlayedMoves)
+ }
+
+ private fun PlayerTurnInfo.firstAvailableMove(): MoveExpectation? = when (action) {
+ Action.PLAY, Action.PLAY_2, Action.PLAY_LAST -> createPlayCardMove(this)
+ Action.PICK_NEIGHBOR_GUILD -> createPickGuildMove(this)
+ Action.WAIT -> null
+ }
+
+ private fun createPlayCardMove(turnInfo: PlayerTurnInfo): MoveExpectation {
+ val wonderBuildability = turnInfo.wonderBuildability
+ if (wonderBuildability.isBuildable) {
+ val transactions = wonderBuildability.cheapestTransactions.first()
+ return planMove(turnInfo, MoveType.UPGRADE_WONDER, turnInfo.hand.first(), transactions)
+ }
+ val playableCard = turnInfo.hand.firstOrNull { it.playability.isPlayable }
+ return if (playableCard != null) {
+ planMove(turnInfo, MoveType.PLAY, playableCard, playableCard.playability.cheapestTransactions.first())
+ } else {
+ planMove(turnInfo, MoveType.DISCARD, turnInfo.hand.first(), noTransactions())
+ }
+ }
+
+ private fun createPickGuildMove(turnInfo: PlayerTurnInfo): MoveExpectation {
+ val neighbourGuilds = turnInfo.neighbourGuildCards
+
+ // the game should send action WAIT if no guild cards are available around
+ assertFalse(neighbourGuilds.isEmpty())
+ return MoveExpectation(
+ turnInfo.playerIndex,
+ PlayerMove(MoveType.COPY_GUILD, neighbourGuilds.first().name),
+ PlayedMove(turnInfo.playerIndex, MoveType.COPY_GUILD, neighbourGuilds.first(), noTransactions())
+ )
+ }
+
+ data class MoveExpectation(val playerIndex: Int, val moveToSend: PlayerMove, val expectedPlayedMove: PlayedMove)
+
+ private fun planMove(
+ turnInfo: PlayerTurnInfo,
+ moveType: MoveType,
+ card: HandCard,
+ transactions: ResourceTransactions
+ ): MoveExpectation = MoveExpectation(
+ turnInfo.playerIndex,
+ PlayerMove(moveType, card.name, transactions),
+ PlayedMove(turnInfo.playerIndex, moveType, card.toPlayedCard(), transactions)
+ )
+
+ private fun HandCard.toPlayedCard(): TableCard =
+ TableCard(
+ name,
+ color,
+ requirements,
+ chainParent,
+ chainChildren,
+ image,
+ back,
+ true
+ )
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/BoardsKtTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/BoardsKtTest.kt
new file mode 100644
index 00000000..244c30a8
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/BoardsKtTest.kt
@@ -0,0 +1,84 @@
+package org.luxons.sevenwonders.game.api
+
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.cards.TableCard
+import org.luxons.sevenwonders.game.test.testCard
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class BoardsKtTest {
+
+ @Test
+ fun `toColumns on empty list should return no cols`() {
+ val cols = emptyList<TableCard>().toColumns()
+ assertEquals(emptyList<List<TableCard>>(), cols)
+ }
+
+ @Test
+ fun `toColumns with single resource should return a single column`() {
+ val card = testCard(color = Color.BROWN).toTableCard()
+ val cols = listOf(card).toColumns()
+ assertEquals(listOf(listOf(card)), cols)
+ }
+
+ @Test
+ fun `toColumns with single non-resource should return a single column`() {
+ val card = testCard(color = Color.BLUE).toTableCard()
+ val cols = listOf(card).toColumns()
+ assertEquals(listOf(listOf(card)), cols)
+ }
+
+ @Test
+ fun `toColumns with two same-color cards should return a single column`() {
+ val card1 = testCard(color = Color.BLUE).toTableCard()
+ val card2 = testCard(color = Color.BLUE).toTableCard()
+ val cards = listOf(card1, card2)
+ val cols = cards.toColumns()
+ assertEquals(listOf(cards), cols)
+ }
+
+ @Test
+ fun `toColumns with two resource cards should return a single column`() {
+ val card1 = testCard(color = Color.BROWN).toTableCard()
+ val card2 = testCard(color = Color.GREY).toTableCard()
+ val cards = listOf(card1, card2)
+ val cols = cards.toColumns()
+ assertEquals(listOf(cards), cols)
+ }
+
+ @Test
+ fun `toColumns with 2 different non-resource cards should return 2 columns`() {
+ val card1 = testCard(color = Color.BLUE).toTableCard()
+ val card2 = testCard(color = Color.GREEN).toTableCard()
+ val cards = listOf(card1, card2)
+ val cols = cards.toColumns()
+ assertEquals(listOf(listOf(card1), listOf(card2)), cols)
+ }
+
+ @Test
+ fun `toColumns with 1 res and 1 non-res card should return 2 columns`() {
+ val card1 = testCard(color = Color.BROWN).toTableCard()
+ val card2 = testCard(color = Color.GREEN).toTableCard()
+ val cards = listOf(card1, card2)
+ val cols = cards.toColumns()
+ assertEquals(listOf(listOf(card1), listOf(card2)), cols)
+ }
+
+ @Test
+ fun `toColumns should return 1 col for res cards and 1 for each other color`() {
+ val res1 = testCard(color = Color.BROWN).toTableCard()
+ val res2 = testCard(color = Color.BROWN).toTableCard()
+ val res3 = testCard(color = Color.GREY).toTableCard()
+ val blue1 = testCard(color = Color.BLUE).toTableCard()
+ val green1 = testCard(color = Color.GREEN).toTableCard()
+ val green2 = testCard(color = Color.GREEN).toTableCard()
+ val cards = listOf(res1, green1, green2, res2, blue1, res3)
+ val cols = cards.toColumns()
+ val expectedCols = listOf(
+ listOf(res1, res2, res3),
+ listOf(blue1),
+ listOf(green1, green2)
+ )
+ assertEquals(expectedCols, cols)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/TableTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/TableTest.kt
new file mode 100644
index 00000000..19e4e8e8
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/api/TableTest.kt
@@ -0,0 +1,71 @@
+package org.luxons.sevenwonders.game.api
+
+import org.junit.Assume.assumeTrue
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.test.createGuildCards
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class TableTest {
+
+ @Theory
+ fun getBoard_wrapLeft(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 2)
+ val table = testTable(nbPlayers)
+ val last = nbPlayers - 1
+ assertEquals(table.getBoard(last), table.getBoard(0, RelativeBoardPosition.LEFT))
+ assertEquals(table.getBoard(0), table.getBoard(0, RelativeBoardPosition.SELF))
+ assertEquals(table.getBoard(1), table.getBoard(0, RelativeBoardPosition.RIGHT))
+ }
+
+ @Theory
+ fun getBoard_wrapRight(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 2)
+ val table = testTable(nbPlayers)
+ val last = nbPlayers - 1
+ assertEquals(table.getBoard(last - 1), table.getBoard(last, RelativeBoardPosition.LEFT))
+ assertEquals(table.getBoard(last), table.getBoard(last, RelativeBoardPosition.SELF))
+ assertEquals(table.getBoard(0), table.getBoard(last, RelativeBoardPosition.RIGHT))
+ }
+
+ @Theory
+ fun getBoard_noWrap(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 3)
+ val table = testTable(nbPlayers)
+ assertEquals(table.getBoard(0), table.getBoard(1, RelativeBoardPosition.LEFT))
+ assertEquals(table.getBoard(1), table.getBoard(1, RelativeBoardPosition.SELF))
+ assertEquals(table.getBoard(2), table.getBoard(1, RelativeBoardPosition.RIGHT))
+ }
+
+ @Theory
+ fun getNeighbourGuildCards(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 4)
+ val table = testTable(nbPlayers)
+ val guildCards = createGuildCards(4)
+ table.getBoard(0).addCard(guildCards[0])
+ table.getBoard(0).addCard(guildCards[1])
+ table.getBoard(1).addCard(guildCards[2])
+ table.getBoard(2).addCard(guildCards[3])
+
+ val neighbourCards0 = table.getNeighbourGuildCards(0)
+ assertEquals(listOf(guildCards[2]), neighbourCards0)
+
+ val neighbourCards1 = table.getNeighbourGuildCards(1)
+ assertEquals(guildCards - guildCards[2], neighbourCards1)
+
+ val neighbourCards2 = table.getNeighbourGuildCards(2)
+ assertEquals(listOf(guildCards[2]), neighbourCards2)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun nbPlayers(): IntArray = intArrayOf(2, 3, 4, 5, 6, 7, 8)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/BoardTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/BoardTest.kt
new file mode 100644
index 00000000..d1b7c239
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/BoardTest.kt
@@ -0,0 +1,211 @@
+package org.luxons.sevenwonders.game.boards
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.FromDataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.boards.Board.InsufficientFundsException
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease
+import org.luxons.sevenwonders.game.effects.SpecialAbility
+import org.luxons.sevenwonders.game.effects.SpecialAbilityActivation
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.resourcesOf
+import org.luxons.sevenwonders.game.score.ScoreCategory
+import org.luxons.sevenwonders.game.test.addCards
+import org.luxons.sevenwonders.game.test.getDifferentColorFrom
+import org.luxons.sevenwonders.game.test.playCardWithEffect
+import org.luxons.sevenwonders.game.test.singleBoardPlayer
+import org.luxons.sevenwonders.game.test.testBoard
+import org.luxons.sevenwonders.game.test.testCard
+import org.luxons.sevenwonders.game.test.testSettings
+import org.luxons.sevenwonders.game.test.testWonder
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class BoardTest {
+
+ @Theory
+ fun initialGold_respectsSettings(@FromDataPoints("gold") goldAmountInSettings: Int) {
+ val settings = testSettings(initialGold = goldAmountInSettings)
+ val board = Board(testWonder(), 0, settings)
+ assertEquals(goldAmountInSettings, board.gold)
+ }
+
+ @Theory
+ fun initialProduction_containsInitialResource(type: ResourceType) {
+ val board = Board(testWonder(type), 0, testSettings())
+ val resources = resourcesOf(type)
+ assertTrue(board.production.contains(resources))
+ assertTrue(board.publicProduction.contains(resources))
+ }
+
+ @Theory
+ fun removeGold_successfulWhenNotTooMuch(
+ @FromDataPoints("gold") initialGold: Int,
+ @FromDataPoints("gold") goldRemoved: Int
+ ) {
+ assumeTrue(goldRemoved >= 0)
+ assumeTrue(initialGold >= goldRemoved)
+
+ val board = Board(testWonder(), 0, testSettings(initialGold = initialGold))
+ board.removeGold(goldRemoved)
+ assertEquals(initialGold - goldRemoved, board.gold)
+ }
+
+ @Theory
+ fun removeGold_failsWhenTooMuch(
+ @FromDataPoints("gold") initialGold: Int,
+ @FromDataPoints("gold") goldRemoved: Int
+ ) {
+ assumeTrue(goldRemoved >= 0)
+ assumeTrue(initialGold < goldRemoved)
+
+ assertFailsWith<InsufficientFundsException> {
+ val board = Board(testWonder(), 0, testSettings(initialGold = initialGold))
+ board.removeGold(goldRemoved)
+ }
+ }
+
+ @Theory
+ fun getNbCardsOfColor_properCount_singleColor(
+ type: ResourceType,
+ @FromDataPoints("nbCards") nbCards: Int,
+ @FromDataPoints("nbCards") nbOtherCards: Int,
+ color: Color
+ ) {
+ val board = testBoard(initialResource = type)
+ addCards(board, nbCards, nbOtherCards, color)
+ assertEquals(nbCards, board.getNbCardsOfColor(listOf(color)))
+ }
+
+ @Theory
+ fun getNbCardsOfColor_properCount_multiColors(
+ type: ResourceType,
+ @FromDataPoints("nbCards") nbCards1: Int,
+ @FromDataPoints("nbCards") nbCards2: Int,
+ @FromDataPoints("nbCards") nbOtherCards: Int,
+ color1: Color,
+ color2: Color
+ ) {
+ val board = testBoard(initialResource = type)
+ addCards(board, nbCards1, color1)
+ addCards(board, nbCards2, color2)
+ addCards(board, nbOtherCards, getDifferentColorFrom(color1, color2))
+ assertEquals(nbCards1 + nbCards2, board.getNbCardsOfColor(listOf(color1, color2)))
+ }
+
+ @Test
+ fun setCopiedGuild_succeedsOnPurpleCard() {
+ val board = testBoard()
+ val card = testCard(color = Color.PURPLE)
+
+ board.copiedGuild = card
+ assertSame(card, board.copiedGuild)
+ }
+
+ @Theory
+ fun setCopiedGuild_failsOnNonPurpleCard(color: Color) {
+ assumeTrue(color !== Color.PURPLE)
+ val board = testBoard()
+ val card = testCard(color = color)
+
+ assertFailsWith<IllegalArgumentException> {
+ board.copiedGuild = card
+ }
+ }
+
+ @Theory
+ fun hasSpecial(applied: SpecialAbility, tested: SpecialAbility) {
+ val board = testBoard()
+ val special = SpecialAbilityActivation(applied)
+
+ special.applyTo(singleBoardPlayer(board))
+
+ assertEquals(applied === tested, board.hasSpecial(tested))
+ }
+
+ @Test
+ fun canPlayFreeCard() {
+ val board = testBoard()
+ val special = SpecialAbilityActivation(SpecialAbility.ONE_FREE_PER_AGE)
+
+ special.applyTo(singleBoardPlayer(board))
+
+ assertTrue(board.canPlayFreeCard(0))
+ assertTrue(board.canPlayFreeCard(1))
+ assertTrue(board.canPlayFreeCard(2))
+
+ board.consumeFreeCard(0)
+
+ assertFalse(board.canPlayFreeCard(0))
+ assertTrue(board.canPlayFreeCard(1))
+ assertTrue(board.canPlayFreeCard(2))
+
+ board.consumeFreeCard(1)
+
+ assertFalse(board.canPlayFreeCard(0))
+ assertFalse(board.canPlayFreeCard(1))
+ assertTrue(board.canPlayFreeCard(2))
+
+ board.consumeFreeCard(2)
+
+ assertFalse(board.canPlayFreeCard(0))
+ assertFalse(board.canPlayFreeCard(1))
+ assertFalse(board.canPlayFreeCard(2))
+ }
+
+ @Theory
+ fun computePoints_gold(@FromDataPoints("gold") gold: Int) {
+ assumeTrue(gold >= 0)
+ val board = testBoard(initialGold = gold)
+
+ val score = board.computeScore(singleBoardPlayer(board))
+ assertEquals(gold / 3, score.pointsByCategory[ScoreCategory.GOLD])
+ assertEquals(gold / 3, score.totalPoints)
+ }
+
+ @Theory
+ fun computePoints_(@FromDataPoints("gold") gold: Int) {
+ assumeTrue(gold >= 0)
+ val board = testBoard(initialGold = gold)
+
+ val effect = RawPointsIncrease(5)
+ playCardWithEffect(singleBoardPlayer(board), Color.BLUE, effect)
+
+ val score = board.computeScore(singleBoardPlayer(board))
+ assertEquals(gold / 3, score.pointsByCategory[ScoreCategory.GOLD])
+ assertEquals(5, score.pointsByCategory[ScoreCategory.CIVIL])
+ assertEquals(5 + gold / 3, score.totalPoints)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints("gold")
+ fun goldAmounts(): IntArray = intArrayOf(-3, -1, 0, 1, 2, 3)
+
+ @JvmStatic
+ @DataPoints("nbCards")
+ fun nbCards(): IntArray = intArrayOf(0, 1, 2)
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+
+ @JvmStatic
+ @DataPoints
+ fun colors(): Array<Color> = Color.values()
+
+ @JvmStatic
+ @DataPoints
+ fun specialAbilities(): Array<SpecialAbility> = SpecialAbility.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/MilitaryTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/MilitaryTest.kt
new file mode 100644
index 00000000..248d43dd
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/MilitaryTest.kt
@@ -0,0 +1,57 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.FromDataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.boards.Military.UnknownAgeException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@RunWith(Theories::class)
+class MilitaryTest {
+
+ @Theory
+ fun victory_addsCorrectPoints(
+ @FromDataPoints("ages") age: Int,
+ @FromDataPoints("points") nbPointsPerVictory: Int
+ ) {
+ val military = createMilitary(age, nbPointsPerVictory, 0)
+ val initialPoints = military.totalPoints
+
+ military.victory(age)
+ assertEquals(initialPoints + nbPointsPerVictory, military.totalPoints)
+ }
+
+ @Theory
+ fun victory_failsIfUnknownAge(@FromDataPoints("points") nbPointsPerVictory: Int) {
+ val military = createMilitary(0, nbPointsPerVictory, 0)
+ assertFailsWith<UnknownAgeException> {
+ military.victory(1)
+ }
+ }
+
+ @Theory
+ fun defeat_removesCorrectPoints(@FromDataPoints("points") nbPointsLostPerDefeat: Int) {
+ val military = createMilitary(0, 0, nbPointsLostPerDefeat)
+ val initialPoints = military.totalPoints
+
+ military.defeat()
+ assertEquals(initialPoints - nbPointsLostPerDefeat, military.totalPoints)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints("points")
+ fun points(): IntArray = intArrayOf(0, 1, 3, 5)
+
+ @JvmStatic
+ @DataPoints("ages")
+ fun ages(): IntArray = intArrayOf(1, 2, 3)
+
+ private fun createMilitary(age: Int, nbPointsPerVictory: Int, nbPointsPerDefeat: Int): Military =
+ Military(nbPointsPerDefeat, mapOf(age to nbPointsPerVictory))
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.kt
new file mode 100644
index 00000000..2038a676
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.kt
@@ -0,0 +1,45 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.junit.Assume.assumeTrue
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class RelativeBoardPositionTest {
+
+ @Theory
+ fun getIndexFrom_wrapLeft(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 2)
+ val last = nbPlayers - 1
+ assertEquals(last, RelativeBoardPosition.LEFT.getIndexFrom(0, nbPlayers))
+ assertEquals(0, RelativeBoardPosition.SELF.getIndexFrom(0, nbPlayers))
+ assertEquals(1, RelativeBoardPosition.RIGHT.getIndexFrom(0, nbPlayers))
+ }
+
+ @Theory
+ fun getIndexFrom_wrapRight(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 2)
+ val last = nbPlayers - 1
+ assertEquals(last - 1, RelativeBoardPosition.LEFT.getIndexFrom(last, nbPlayers))
+ assertEquals(last, RelativeBoardPosition.SELF.getIndexFrom(last, nbPlayers))
+ assertEquals(0, RelativeBoardPosition.RIGHT.getIndexFrom(last, nbPlayers))
+ }
+
+ @Theory
+ fun getIndexFrom_noWrap(nbPlayers: Int) {
+ assumeTrue(nbPlayers >= 3)
+ assertEquals(0, RelativeBoardPosition.LEFT.getIndexFrom(1, nbPlayers))
+ assertEquals(1, RelativeBoardPosition.SELF.getIndexFrom(1, nbPlayers))
+ assertEquals(2, RelativeBoardPosition.RIGHT.getIndexFrom(1, nbPlayers))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun nbPlayers(): IntArray = intArrayOf(1, 2, 3, 5, 7, 9)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/ScienceTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/ScienceTest.kt
new file mode 100644
index 00000000..80d6773d
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/boards/ScienceTest.kt
@@ -0,0 +1,114 @@
+package org.luxons.sevenwonders.game.boards
+
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.test.createScience
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class ScienceTest {
+
+ @Test
+ fun addAll_empty() {
+ val initial = createScience(3, 4, 5, 1)
+ val empty = Science()
+ initial.addAll(empty)
+ assertEquals(3, initial.getQuantity(ScienceType.COMPASS))
+ assertEquals(4, initial.getQuantity(ScienceType.WHEEL))
+ assertEquals(5, initial.getQuantity(ScienceType.TABLET))
+ assertEquals(1, initial.jokers)
+ }
+
+ @Test
+ fun addAll_noJoker() {
+ val initial = createScience(3, 4, 5, 1)
+ val other = createScience(1, 2, 3, 0)
+ initial.addAll(other)
+ assertEquals(4, initial.getQuantity(ScienceType.COMPASS))
+ assertEquals(6, initial.getQuantity(ScienceType.WHEEL))
+ assertEquals(8, initial.getQuantity(ScienceType.TABLET))
+ assertEquals(1, initial.jokers)
+ }
+
+ @Test
+ fun addAll_withJokers() {
+ val initial = createScience(3, 4, 5, 1)
+ val other = createScience(0, 0, 0, 3)
+ initial.addAll(other)
+ assertEquals(3, initial.getQuantity(ScienceType.COMPASS))
+ assertEquals(4, initial.getQuantity(ScienceType.WHEEL))
+ assertEquals(5, initial.getQuantity(ScienceType.TABLET))
+ assertEquals(4, initial.jokers)
+ }
+
+ @Test
+ fun addAll_mixed() {
+ val initial = createScience(3, 4, 5, 1)
+ val other = createScience(1, 2, 3, 4)
+ initial.addAll(other)
+ assertEquals(4, initial.getQuantity(ScienceType.COMPASS))
+ assertEquals(6, initial.getQuantity(ScienceType.WHEEL))
+ assertEquals(8, initial.getQuantity(ScienceType.TABLET))
+ assertEquals(5, initial.jokers)
+ }
+
+ @Theory
+ fun computePoints_compassesOnly_noJoker(compasses: Int) {
+ val science = createScience(compasses, 0, 0, 0)
+ assertEquals(compasses * compasses, science.computePoints())
+ }
+
+ @Theory
+ fun computePoints_wheelsOnly_noJoker(wheels: Int) {
+ val science = createScience(0, wheels, 0, 0)
+ assertEquals(wheels * wheels, science.computePoints())
+ }
+
+ @Theory
+ fun computePoints_tabletsOnly_noJoker(tablets: Int) {
+ val science = createScience(0, 0, tablets, 0)
+ assertEquals(tablets * tablets, science.computePoints())
+ }
+
+ @Theory
+ fun computePoints_allSameNoJoker(eachSymbol: Int) {
+ val science = createScience(eachSymbol, eachSymbol, eachSymbol, 0)
+ assertEquals(3 * eachSymbol * eachSymbol + 7 * eachSymbol, science.computePoints())
+ }
+
+ @Theory
+ fun computePoints_expectation(expectation: IntArray) {
+ val science = createScience(expectation[0], expectation[1], expectation[2], expectation[3])
+ assertEquals(expectation[4], science.computePoints())
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun quantitiesWithExpectedPoints(): Array<IntArray> = arrayOf(
+ // compasses, wheels, tablets, jokers, expected points
+ intArrayOf(0, 0, 0, 1, 1),
+ intArrayOf(0, 0, 1, 0, 1),
+ intArrayOf(0, 0, 0, 2, 4),
+ intArrayOf(0, 0, 1, 1, 4),
+ intArrayOf(0, 0, 2, 0, 4),
+ intArrayOf(0, 0, 0, 3, 10),
+ intArrayOf(0, 0, 1, 2, 10),
+ intArrayOf(0, 1, 1, 1, 10),
+ intArrayOf(1, 1, 1, 0, 10),
+ intArrayOf(0, 0, 0, 4, 16),
+ intArrayOf(0, 0, 1, 3, 16),
+ intArrayOf(0, 0, 2, 2, 16),
+ intArrayOf(0, 0, 3, 1, 16),
+ intArrayOf(0, 0, 4, 0, 16)
+ )
+
+ @JvmStatic
+ @DataPoints
+ fun quantitiesDataPoints(): IntArray = intArrayOf(0, 1, 3, 5, 8)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardBackTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardBackTest.kt
new file mode 100644
index 00000000..66ff7a0e
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardBackTest.kt
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class CardBackTest {
+
+ @Test
+ fun initializedWithImage() {
+ val imagePath = "whateverimage.png"
+ val (image) = CardBack(imagePath)
+ assertEquals(imagePath, image)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardTest.kt
new file mode 100644
index 00000000..b6fecbd0
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/CardTest.kt
@@ -0,0 +1,42 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.noTransactions
+import org.luxons.sevenwonders.game.test.testCard
+import org.luxons.sevenwonders.game.test.testSettings
+import org.luxons.sevenwonders.game.wonders.Wonder
+import kotlin.test.assertEquals
+
+class CardTest {
+
+ @Test
+ fun playCardCostingMoney() {
+ val initialGold = 3
+ val price = 1
+ val settings = testSettings(3, initialGold)
+
+ val boards = listOf(
+ Board(Wonder("TestWonder", ResourceType.WOOD, emptyList(), ""), 0, settings),
+ Board(Wonder("TestWonder", ResourceType.STONE, emptyList(), ""), 1, settings),
+ Board(Wonder("TestWonder", ResourceType.PAPYRUS, emptyList(), ""), 2, settings)
+ )
+ val table = Table(boards)
+
+ val treeFarmRequirements = Requirements(gold = price)
+ val treeFarmProduction = Production().apply { addChoice(ResourceType.WOOD, ResourceType.CLAY) }
+ val treeFarmEffect = ProductionIncrease(treeFarmProduction, false)
+ val treeFarmCard = testCard("Tree Farm", Color.BROWN, treeFarmRequirements, treeFarmEffect)
+
+ treeFarmCard.applyTo(SimplePlayer(0, table), noTransactions())
+
+ assertEquals(initialGold - price, table.getBoard(0).gold)
+ assertEquals(initialGold, table.getBoard(1).gold)
+ assertEquals(initialGold, table.getBoard(2).gold)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/DecksTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/DecksTest.kt
new file mode 100644
index 00000000..f6c45720
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/DecksTest.kt
@@ -0,0 +1,104 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.cards.Decks.CardNotFoundException
+import org.luxons.sevenwonders.game.test.sampleCards
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class DecksTest {
+
+ @Test
+ fun getCard_failsOnEmptyNameWhenDeckIsEmpty() {
+ val decks = createDecks(0, 0)
+ assertFailsWith<IllegalArgumentException> {
+ decks.getCard(0, "")
+ }
+ }
+
+ @Test
+ fun getCard_failsWhenDeckIsEmpty() {
+ val decks = createDecks(0, 0)
+ assertFailsWith<IllegalArgumentException> {
+ decks.getCard(0, "Any name")
+ }
+ }
+
+ @Test
+ fun getCard_failsWhenCardIsNotFound() {
+ val decks = createDecks(3, 20)
+ assertFailsWith<CardNotFoundException> {
+ decks.getCard(1, "Unknown name")
+ }
+ }
+
+ @Test
+ fun getCard_succeedsWhenCardIsFound() {
+ val decks = createDecks(3, 20)
+ val (name) = decks.getCard(1, "Test Card 3")
+ assertEquals("Test Card 3", name)
+ }
+
+ @Test
+ fun deal_failsOnZeroPlayers() {
+ val decks = createDecks(3, 20)
+ assertFailsWith<IllegalArgumentException> {
+ decks.deal(1, 0)
+ }
+ }
+
+ @Test
+ fun deal_failsOnMissingAge() {
+ val decks = createDecks(2, 0)
+ assertFailsWith<IllegalArgumentException> {
+ decks.deal(4, 10)
+ }
+ }
+
+ @Theory
+ fun deal_failsWhenTooFewPlayers(nbPlayers: Int, nbCards: Int) {
+ assumeTrue(nbCards % nbPlayers != 0)
+ val decks = createDecks(1, nbCards)
+ assertFailsWith<IllegalArgumentException> {
+ decks.deal(1, nbPlayers)
+ }
+ }
+
+ @Theory
+ fun deal_succeedsOnZeroCards(nbPlayers: Int) {
+ val decks = createDecks(1, 0)
+ val hands = decks.deal(1, nbPlayers)
+ repeat(nbPlayers) { i ->
+ assertNotNull(hands[i])
+ assertTrue(hands[i].isEmpty())
+ }
+ }
+
+ @Theory
+ fun deal_evenDistribution(nbPlayers: Int, nbCardsPerPlayer: Int) {
+ val nbCardsPerAge = nbPlayers * nbCardsPerPlayer
+ val decks = createDecks(1, nbCardsPerAge)
+ val hands = decks.deal(1, nbPlayers)
+ repeat(nbPlayers) { i ->
+ assertEquals(nbCardsPerPlayer, hands[i].size)
+ }
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun dataPoints(): IntArray = intArrayOf(1, 2, 3, 5, 10)
+
+ private fun createDecks(nbAges: Int, nbCardsPerAge: Int): Decks =
+ Decks((1..nbAges).map { it to sampleCards(nbCardsPerAge) }.toMap())
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.kt
new file mode 100644
index 00000000..4582c4a1
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.kt
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class HandRotationDirectionTest {
+
+ @Test
+ fun testAgesDirections() {
+ assertEquals(HandRotationDirection.LEFT, HandRotationDirection.forAge(1))
+ assertEquals(HandRotationDirection.RIGHT, HandRotationDirection.forAge(2))
+ assertEquals(HandRotationDirection.LEFT, HandRotationDirection.forAge(3))
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandsTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandsTest.kt
new file mode 100644
index 00000000..c7ff9106
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/HandsTest.kt
@@ -0,0 +1,127 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.FromDataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.test.sampleCards
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class HandsTest {
+
+ @Test
+ fun get_failsOnMissingPlayer() {
+ val hands = createHands(4, 7)
+
+ assertFailsWith<IndexOutOfBoundsException> { hands[5] }
+ }
+
+ @Test
+ fun get_retrievesCorrectCards() {
+ val hand0 = sampleCards(5, 0)
+ val hand1 = sampleCards(10, 5)
+ val hands = Hands(listOf(hand0, hand1))
+ assertEquals(hand0, hands[0])
+ assertEquals(hand1, hands[1])
+ }
+
+ @Theory
+ fun isEmpty_falseWhenAtLeast1_allSame(
+ @FromDataPoints("nbPlayers") nbPlayers: Int,
+ @FromDataPoints("nbCardsPerPlayer") nbCardsPerPlayer: Int
+ ) {
+ assumeTrue(nbCardsPerPlayer >= 1)
+ val hands = createHands(nbPlayers, nbCardsPerPlayer)
+ assertFalse(hands.isEmpty)
+ }
+
+ @Theory
+ fun isEmpty_trueWhenAllEmpty(@FromDataPoints("nbPlayers") nbPlayers: Int) {
+ val hands = createHands(nbPlayers, 0)
+ assertTrue(hands.isEmpty)
+ }
+
+ @Theory
+ fun maxOneCardRemains_falseWhenAtLeast2_allSame(
+ @FromDataPoints("nbPlayers") nbPlayers: Int,
+ @FromDataPoints("nbCardsPerPlayer") nbCardsPerPlayer: Int
+ ) {
+ assumeTrue(nbCardsPerPlayer >= 2)
+ val hands = createHands(nbPlayers, nbCardsPerPlayer)
+ assertFalse(hands.maxOneCardRemains())
+ }
+
+ @Theory
+ fun maxOneCardRemains_trueWhenAtMost1_allSame(
+ @FromDataPoints("nbPlayers") nbPlayers: Int,
+ @FromDataPoints("nbCardsPerPlayer") nbCardsPerPlayer: Int
+ ) {
+ assumeTrue(nbCardsPerPlayer <= 1)
+ val hands = createHands(nbPlayers, nbCardsPerPlayer)
+ assertTrue(hands.maxOneCardRemains())
+ }
+
+ @Theory
+ fun maxOneCardRemains_trueWhenAtMost1_someZero(@FromDataPoints("nbPlayers") nbPlayers: Int) {
+ val hands = createHands(nbPlayers, 1)
+ assertTrue(hands.maxOneCardRemains())
+ }
+
+ @Test
+ fun rotate_movesOfCorrectOffset_right() {
+ val hands = createHands(3, 7)
+ val rotated = hands.rotate(HandRotationDirection.RIGHT)
+ assertEquals(rotated[1], hands[0])
+ assertEquals(rotated[2], hands[1])
+ assertEquals(rotated[0], hands[2])
+ }
+
+ @Test
+ fun rotate_movesOfCorrectOffset_left() {
+ val hands = createHands(3, 7)
+ val rotated = hands.rotate(HandRotationDirection.LEFT)
+ assertEquals(rotated[2], hands[0])
+ assertEquals(rotated[0], hands[1])
+ assertEquals(rotated[1], hands[2])
+ }
+
+ @Test
+ fun createHand_containsAllCards() {
+ val hand0 = sampleCards(5, 0)
+ val hand1 = sampleCards(10, 5)
+ val hands = Hands(listOf(hand0, hand1))
+
+ val table = testTable(2)
+ val hand = hands.createHand(SimplePlayer(0, table))
+
+ assertEquals(hand0.map { it.name }, hand.map { it.name })
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints("nbCardsPerPlayer")
+ fun nbCardsPerPlayer(): IntArray {
+ return intArrayOf(0, 1, 2, 3, 4, 5, 6, 7)
+ }
+
+ @JvmStatic
+ @DataPoints("nbPlayers")
+ fun nbPlayers(): IntArray {
+ return intArrayOf(3, 4, 5, 6, 7)
+ }
+
+ private fun createHands(nbPlayers: Int, nbCardsPerPlayer: Int): Hands {
+ return sampleCards(nbCardsPerPlayer * nbPlayers, 0).deal(nbPlayers)
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt
new file mode 100644
index 00000000..eccca3e7
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/cards/RequirementsTest.kt
@@ -0,0 +1,162 @@
+package org.luxons.sevenwonders.game.cards
+
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.resources.Provider
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.emptyResources
+import org.luxons.sevenwonders.game.resources.noTransactions
+import org.luxons.sevenwonders.game.test.createRequirements
+import org.luxons.sevenwonders.game.test.createTransactions
+import org.luxons.sevenwonders.game.test.singleBoardPlayer
+import org.luxons.sevenwonders.game.test.testBoard
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class RequirementsTest {
+
+ @Test
+ fun getResources_emptyAfterInit() {
+ val (_, resources) = Requirements()
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun setResources_success() {
+ val resources = emptyResources()
+ val requirements = Requirements(0, resources)
+ assertSame(resources, requirements.resources)
+ }
+
+ @Theory
+ fun goldRequirement(boardGold: Int, requiredGold: Int) {
+ val requirements = Requirements(requiredGold)
+
+ val board = testBoard(ResourceType.CLAY, boardGold)
+ val player = singleBoardPlayer(board)
+
+ assertEquals(boardGold >= requiredGold, requirements.areMetWithHelpBy(board, noTransactions()))
+
+ val satisfaction = requirements.assess(player)
+ if (boardGold >= requiredGold) {
+ if (requiredGold == 0) {
+ assertEquals(RequirementsSatisfaction.noRequirements(), satisfaction)
+ } else {
+ assertEquals(RequirementsSatisfaction.enoughGold(requiredGold), satisfaction)
+ }
+ } else {
+ assertEquals(RequirementsSatisfaction.missingRequiredGold(requiredGold), satisfaction)
+ }
+ }
+
+ @Theory
+ fun resourceRequirement_initialResource(initialResource: ResourceType, requiredResource: ResourceType) {
+ val requirements = createRequirements(requiredResource)
+
+ val board = testBoard(initialResource, 0)
+ val player = singleBoardPlayer(board)
+
+ assertEquals(initialResource == requiredResource, requirements.areMetWithHelpBy(board, noTransactions()))
+
+ if (initialResource == requiredResource) {
+ val satisfaction = requirements.assess(player)
+ assertEquals(RequirementsSatisfaction.enoughResources(), satisfaction)
+ }
+ }
+
+ @Theory
+ fun resourceRequirement_ownProduction(
+ initialResource: ResourceType,
+ producedResource: ResourceType,
+ requiredResource: ResourceType
+ ) {
+ assumeTrue(initialResource != requiredResource)
+
+ val requirements = createRequirements(requiredResource)
+
+ val board = testBoard(initialResource, 0)
+ board.production.addFixedResource(producedResource, 1)
+ val player = singleBoardPlayer(board)
+
+ assertEquals(producedResource == requiredResource, requirements.areMetWithHelpBy(board, noTransactions()))
+
+ if (producedResource == requiredResource) {
+ val satisfaction = requirements.assess(player)
+ assertEquals(RequirementsSatisfaction.enoughResources(), satisfaction)
+ }
+ }
+
+ @Theory
+ fun resourceRequirement_boughtResource(
+ initialResource: ResourceType,
+ boughtResource: ResourceType,
+ requiredResource: ResourceType
+ ) {
+ assumeTrue(initialResource != requiredResource)
+
+ val requirements = createRequirements(requiredResource)
+
+ val board = testBoard(initialResource, 2)
+ val neighbourBoard = testBoard(initialResource, 0)
+ neighbourBoard.publicProduction.addFixedResource(boughtResource, 1)
+ val table = Table(listOf(board, neighbourBoard))
+ val player = SimplePlayer(0, table)
+
+ val resources = createTransactions(Provider.RIGHT_PLAYER, boughtResource)
+
+ val neighbourHasResource = boughtResource == requiredResource
+ assertEquals(neighbourHasResource, requirements.areMetWithHelpBy(board, resources))
+
+ val satisfaction = requirements.assess(player)
+ if (neighbourHasResource) {
+ val transactions = setOf(
+ createTransactions(Provider.LEFT_PLAYER, requiredResource),
+ createTransactions(Provider.RIGHT_PLAYER, requiredResource)
+ )
+ assertEquals(RequirementsSatisfaction.metWithHelp(2, transactions), satisfaction)
+ } else {
+ assertEquals(RequirementsSatisfaction.unavailableResources(), satisfaction)
+ }
+ }
+
+ @Theory
+ fun pay_boughtResource(initialResource: ResourceType, requiredResource: ResourceType) {
+ assumeTrue(initialResource != requiredResource)
+
+ val requirements = createRequirements(requiredResource)
+
+ val board = testBoard(initialResource, 2)
+ val neighbourBoard = testBoard(requiredResource, 0)
+ val table = Table(listOf(board, neighbourBoard))
+ val player = SimplePlayer(0, table)
+
+ val transactions = createTransactions(Provider.RIGHT_PLAYER, requiredResource)
+
+ assertTrue(requirements.areMetWithHelpBy(board, transactions))
+ assertTrue(requirements.assess(player).satisfied)
+
+ requirements.pay(player, transactions)
+
+ assertEquals(0, board.gold)
+ assertEquals(2, neighbourBoard.gold)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun goldAmounts(): IntArray = intArrayOf(0, 1, 2, 5)
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/GameDefinitionTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/GameDefinitionTest.kt
new file mode 100644
index 00000000..4317a933
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/GameDefinitionTest.kt
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.data
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.api.CustomizableSettings
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+class GameDefinitionTest {
+
+ @Test
+ fun successfulGameInit() {
+ val gameDefinition = GameDefinition.load()
+ assertNotNull(gameDefinition)
+ assertEquals(3, gameDefinition.minPlayers)
+ assertEquals(7, gameDefinition.maxPlayers)
+
+ val game = gameDefinition.initGame(0, CustomizableSettings(), 7)
+ assertNotNull(game)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.kt
new file mode 100644
index 00000000..0b561938
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.kt
@@ -0,0 +1,96 @@
+package org.luxons.sevenwonders.game.data.definitions
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.api.WonderSidePickMethod
+import java.util.Random
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class WonderSidePickMethodTest {
+
+ private lateinit var random: Random
+
+ private lateinit var random2: Random
+
+ @Before
+ fun setUp() {
+ random = Random(123) // starts with TRUE
+ random2 = Random(123456) // starts with FALSE
+ }
+
+ @Test
+ fun pick_allA() {
+ var side: WonderSide? = null
+ repeat(10) {
+ side = WonderSidePickMethod.ALL_A.pickSide(random, side)
+ assertEquals(WonderSide.A, side)
+ }
+ }
+
+ @Test
+ fun pick_allB() {
+ var side: WonderSide? = null
+ repeat(10) {
+ side = WonderSidePickMethod.ALL_B.pickSide(random, side)
+ assertEquals(WonderSide.B, side)
+ }
+ }
+
+ @Test
+ fun pick_eachRandom() {
+ var side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, null)
+ assertEquals(WonderSide.A, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side)
+ assertEquals(WonderSide.A, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side)
+ assertEquals(WonderSide.A, side)
+ }
+
+ @Test
+ fun pick_eachRandom2() {
+ var side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, null)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side)
+ assertEquals(WonderSide.A, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side)
+ assertEquals(WonderSide.A, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side)
+ assertEquals(WonderSide.B, side)
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side)
+ assertEquals(WonderSide.B, side)
+ }
+
+ @Theory
+ fun pick_allSameRandom_sameAsFirst(firstSide: WonderSide) {
+ var side = firstSide
+ repeat(10) {
+ side = WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random, side)
+ assertEquals(firstSide, side)
+ }
+ }
+
+ @Test
+ fun pick_allSameRandom_firstIsRandom() {
+ assertEquals(WonderSide.A, WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random, null))
+ assertEquals(WonderSide.B, WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random2, null))
+ }
+
+ companion object {
+
+ @DataPoints
+ fun sides(): Array<WonderSide> = WonderSide.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.kt
new file mode 100644
index 00000000..9b44fad2
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.kt
@@ -0,0 +1,147 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.junit.Before
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.effects.GoldIncrease
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease
+import org.luxons.sevenwonders.game.resources.Production
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+@RunWith(Theories::class)
+class NumericEffectSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ gson = GsonBuilder().registerTypeAdapter(MilitaryReinforcements::class.java, NumericEffectSerializer())
+ .registerTypeAdapter(RawPointsIncrease::class.java, NumericEffectSerializer())
+ .registerTypeAdapter(GoldIncrease::class.java, NumericEffectSerializer())
+ // ProductionIncrease is not a numeric effect, it is here for negative testing purpose
+ .registerTypeAdapter(ProductionIncrease::class.java, NumericEffectSerializer()).create()
+ }
+
+ @Test
+ fun serialize_militaryReinforcements_null() {
+ assertEquals("null", gson.toJson(null, MilitaryReinforcements::class.java))
+ }
+
+ @Test
+ fun serialize_rawPointsIncrease_null() {
+ assertEquals("null", gson.toJson(null, RawPointsIncrease::class.java))
+ }
+
+ @Test
+ fun serialize_goldIncrease_null() {
+ assertEquals("null", gson.toJson(null, GoldIncrease::class.java))
+ }
+
+ @Test
+ fun serialize_failOnUnknownType() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.toJson(ProductionIncrease(Production(), false))
+ }
+ }
+
+ @Theory
+ fun serialize_militaryReinforcements(count: Int) {
+ val reinforcements = MilitaryReinforcements(count)
+ assertEquals(count.toString(), gson.toJson(reinforcements))
+ }
+
+ @Theory
+ fun serialize_rawPointsIncrease(count: Int) {
+ val points = RawPointsIncrease(count)
+ assertEquals(count.toString(), gson.toJson(points))
+ }
+
+ @Theory
+ fun serialize_goldIncrease(count: Int) {
+ val goldIncrease = GoldIncrease(count)
+ assertEquals(count.toString(), gson.toJson(goldIncrease))
+ }
+
+ @Theory
+ fun deserialize_militaryReinforcements(count: Int) {
+ val reinforcements = MilitaryReinforcements(count)
+ assertEquals(reinforcements, gson.fromJson<MilitaryReinforcements>(count.toString()))
+ }
+
+ @Theory
+ fun deserialize_rawPointsIncrease(count: Int) {
+ val points = RawPointsIncrease(count)
+ assertEquals(points, gson.fromJson<RawPointsIncrease>(count.toString()))
+ }
+
+ @Theory
+ fun deserialize_goldIncrease(count: Int) {
+ val goldIncrease = GoldIncrease(count)
+ assertEquals(goldIncrease, gson.fromJson<GoldIncrease>(count.toString()))
+ }
+
+ @Test
+ fun deserialize_militaryReinforcements_failOnEmptyString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<MilitaryReinforcements>("\"\"")
+ }
+ }
+
+ @Test
+ fun deserialize_rawPointsIncrease_failOnEmptyString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<RawPointsIncrease>("\"\"")
+ }
+ }
+
+ @Test
+ fun deserialize_goldIncrease_failOnEmptyString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<GoldIncrease>("\"\"")
+ }
+ }
+
+ @Test
+ fun deserialize_militaryReinforcements_failOnNonNumericString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<MilitaryReinforcements>("\"abc\"")
+ }
+ }
+
+ @Test
+ fun deserialize_rawPointsIncrease_failOnNonNumericString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<RawPointsIncrease>("\"abc\"")
+ }
+ }
+
+ @Test
+ fun deserialize_goldIncrease_failOnNonNumericString() {
+ assertFailsWith<NumberFormatException> {
+ gson.fromJson<GoldIncrease>("\"abc\"")
+ }
+ }
+
+ @Test
+ fun deserialize_failOnUnknownType() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<ProductionIncrease>("\"2\"")
+ }
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun dataPoints(): IntArray = intArrayOf(-2, -1, 0, 1, 2, 5)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.kt
new file mode 100644
index 00000000..31d695e8
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.kt
@@ -0,0 +1,192 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.effects.ProductionIncrease
+import org.luxons.sevenwonders.game.resources.MutableResources
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.Resources
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+
+class ProductionIncreaseSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ val resourceTypeList = object : TypeToken<List<ResourceType>>() {}.type
+ gson = GsonBuilder().registerTypeAdapter(Resources::class.java, ResourcesSerializer())
+ .registerTypeAdapter(MutableResources::class.java, ResourcesSerializer())
+ .registerTypeAdapter(ResourceType::class.java, ResourceTypeSerializer())
+ .registerTypeAdapter(resourceTypeList, ResourceTypesSerializer())
+ .registerTypeAdapter(Production::class.java, ProductionSerializer())
+ .registerTypeAdapter(ProductionIncrease::class.java, ProductionIncreaseSerializer())
+ .create()
+ }
+
+ private fun create(sellable: Boolean, wood: Int, stone: Int, clay: Int): ProductionIncrease {
+ val production = Production()
+ if (wood > 0) {
+ production.addFixedResource(ResourceType.WOOD, wood)
+ }
+ if (stone > 0) {
+ production.addFixedResource(ResourceType.STONE, stone)
+ }
+ if (clay > 0) {
+ production.addFixedResource(ResourceType.CLAY, clay)
+ }
+ return ProductionIncrease(production, sellable)
+ }
+
+ private fun createChoice(sellable: Boolean, vararg types: ResourceType): ProductionIncrease {
+ val production = Production()
+ production.addChoice(*types)
+ return ProductionIncrease(production, sellable)
+ }
+
+ @Test
+ fun serialize_nullAsNull() {
+ assertEquals("null", gson.toJson(null, ProductionIncrease::class.java))
+ }
+
+ @Test
+ fun serialize_emptyProdIncreaseAsNull() {
+ val prodIncrease = ProductionIncrease(Production(), false)
+ assertEquals("null", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_singleType() {
+ val prodIncrease = create(true, 1, 0, 0)
+ assertEquals("\"W\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_mixedTypes() {
+ val prodIncrease = create(true, 1, 1, 1)
+ assertEquals("\"WSC\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_mixedTypes_notSellable() {
+ val prodIncrease = create(false, 1, 1, 1)
+ assertEquals("\"(WSC)\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice2() {
+ val prodIncrease = createChoice(true, ResourceType.WOOD, ResourceType.CLAY)
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice3() {
+ val prodIncrease = createChoice(true, ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY)
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice3_notSellable() {
+ val prodIncrease = createChoice(false, ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY)
+ assertEquals("\"(W/O/C)\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice2_unordered() {
+ val prodIncrease = createChoice(true, ResourceType.CLAY, ResourceType.WOOD)
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice3_unordered() {
+ val prodIncrease = createChoice(true, ResourceType.WOOD, ResourceType.CLAY, ResourceType.ORE)
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_failIfMultipleChoices() {
+ val prodIncrease = createChoice(true, ResourceType.WOOD, ResourceType.CLAY)
+ prodIncrease.production.addChoice(ResourceType.ORE, ResourceType.GLASS)
+ assertFailsWith<IllegalArgumentException> {
+ gson.toJson(prodIncrease)
+ }
+ }
+
+ @Test
+ fun serialize_failIfMixedFixedAndChoices() {
+ val prodIncrease = create(true, 1, 0, 0)
+ prodIncrease.production.addChoice(ResourceType.WOOD, ResourceType.CLAY)
+ assertFailsWith<IllegalArgumentException> {
+ gson.toJson(prodIncrease)
+ }
+ }
+
+ @Test
+ fun deserialize_nullFromNull() {
+ assertNull(gson.fromJson("null", ProductionIncrease::class.java))
+ }
+
+ @Test
+ fun deserialize_emptyList() {
+ val prodIncrease = ProductionIncrease(Production(), true)
+ assertEquals(prodIncrease, gson.fromJson("\"\""))
+ }
+
+ @Test
+ fun deserialize_failOnGarbageString() {
+ assertFailsWith(IllegalArgumentException::class) {
+ gson.fromJson<ProductionIncrease>("\"this is garbage\"")
+ }
+ }
+
+ @Test
+ fun deserialize_failOnGarbageStringWithSlashes() {
+ assertFailsWith(IllegalArgumentException::class) {
+ gson.fromJson<ProductionIncrease>("\"this/is/garbage\"")
+ }
+ }
+
+ @Test
+ fun deserialize_singleType() {
+ val prodIncrease = create(true, 1, 0, 0)
+ assertEquals(prodIncrease, gson.fromJson("\"W\""))
+ }
+
+ @Test
+ fun deserialize_multipleTimesSameType_notSellable() {
+ val prodIncrease = create(false, 3, 0, 0)
+ assertEquals(prodIncrease, gson.fromJson("\"(WWW)\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes() {
+ val prodIncrease = create(true, 1, 1, 1)
+ assertEquals(prodIncrease, gson.fromJson("\"WCS\""))
+ }
+
+ @Test
+ fun deserialize_choice2() {
+ val prodIncrease = createChoice(true, ResourceType.WOOD, ResourceType.CLAY)
+ assertEquals(prodIncrease, gson.fromJson("\"W/C\""))
+ }
+
+ @Test
+ fun deserialize_choice3_notSellable() {
+ val prodIncrease = createChoice(false, ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY)
+ assertEquals(prodIncrease, gson.fromJson("\"(W/O/C)\""))
+ }
+
+ @Test
+ fun deserialize_failOnMultipleResourcesInChoice() {
+ assertFailsWith(IllegalArgumentException::class) {
+ gson.fromJson<ProductionIncrease>("\"W/SS/C\"")
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializerTest.kt
new file mode 100644
index 00000000..265087ba
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ProductionSerializerTest.kt
@@ -0,0 +1,207 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.MutableResources
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.Resources
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+
+class ProductionSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ val resourceTypeList = object : TypeToken<List<ResourceType>>() {}.type
+ gson = GsonBuilder().registerTypeAdapter(Resources::class.java, ResourcesSerializer())
+ .registerTypeAdapter(MutableResources::class.java, ResourcesSerializer())
+ .registerTypeAdapter(ResourceType::class.java, ResourceTypeSerializer())
+ .registerTypeAdapter(resourceTypeList, ResourceTypesSerializer())
+ .registerTypeAdapter(Production::class.java, ProductionSerializer()).create()
+ }
+
+ private fun create(wood: Int, stone: Int, clay: Int): Production {
+ val production = Production()
+ if (wood > 0) {
+ production.addFixedResource(ResourceType.WOOD, wood)
+ }
+ if (stone > 0) {
+ production.addFixedResource(ResourceType.STONE, stone)
+ }
+ if (clay > 0) {
+ production.addFixedResource(ResourceType.CLAY, clay)
+ }
+ return production
+ }
+
+ private fun createChoice(vararg types: ResourceType): Production {
+ val production = Production()
+ production.addChoice(*types)
+ return production
+ }
+
+ @Test
+ fun serialize_nullAsNull() {
+ assertEquals("null", gson.toJson(null, Production::class.java))
+ }
+
+ @Test
+ fun serialize_emptyProdIncreaseAsNull() {
+ val prodIncrease = Production()
+ assertEquals("null", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_singleType() {
+ val prodIncrease = create(1, 0, 0)
+ assertEquals("\"W\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_multipleTimesSameType() {
+ val prodIncrease = create(3, 0, 0)
+ assertEquals("\"WWW\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_mixedTypes() {
+ val prodIncrease = create(1, 1, 1)
+ assertEquals("\"WSC\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_mixedTypesMultiple() {
+ val prodIncrease = create(2, 1, 2)
+ assertEquals("\"WWSCC\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice2() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY)
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice3() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY)
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice2_unordered() {
+ val prodIncrease = createChoice(ResourceType.CLAY, ResourceType.WOOD)
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_choice3_unordered() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY, ResourceType.ORE)
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease))
+ }
+
+ @Test
+ fun serialize_failIfMultipleChoices() {
+ val production = createChoice(ResourceType.WOOD, ResourceType.CLAY)
+ production.addChoice(ResourceType.ORE, ResourceType.GLASS)
+ assertFailsWith<IllegalArgumentException> {
+ gson.toJson(production)
+ }
+ }
+
+ @Test
+ fun serialize_failIfMixedFixedAndChoices() {
+ val production = create(1, 0, 0)
+ production.addChoice(ResourceType.WOOD, ResourceType.CLAY)
+ assertFailsWith<IllegalArgumentException> {
+ gson.toJson(production)
+ }
+ }
+
+ @Test
+ fun deserialize_nullFromNull() {
+ assertNull(gson.fromJson("null", Production::class.java))
+ }
+
+ @Test
+ fun deserialize_emptyList() {
+ val prodIncrease = Production()
+ assertEquals(prodIncrease, gson.fromJson("\"\""))
+ }
+
+ @Test
+ fun deserialize_failOnGarbageString() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<Production>("\"this is garbage\"")
+ }
+ }
+
+ @Test
+ fun deserialize_failOnGarbageStringWithSlashes() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<Production>("\"this/is/garbage\"")
+ }
+ }
+
+ @Test
+ fun deserialize_singleType() {
+ val prodIncrease = create(1, 0, 0)
+ assertEquals(prodIncrease, gson.fromJson("\"W\""))
+ }
+
+ @Test
+ fun deserialize_multipleTimesSameType() {
+ val prodIncrease = create(3, 0, 0)
+ assertEquals(prodIncrease, gson.fromJson("\"WWW\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes() {
+ val prodIncrease = create(1, 1, 1)
+ assertEquals(prodIncrease, gson.fromJson("\"WCS\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes_unordered() {
+ val prodIncrease = create(1, 3, 2)
+ assertEquals(prodIncrease, gson.fromJson("\"SCWCSS\""))
+ }
+
+ @Test
+ fun deserialize_choice2() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY)
+ assertEquals(prodIncrease, gson.fromJson("\"W/C\""))
+ }
+
+ @Test
+ fun deserialize_choice3() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY)
+ assertEquals(prodIncrease, gson.fromJson("\"W/O/C\""))
+ }
+
+ @Test
+ fun deserialize_choice2_unordered() {
+ val prodIncrease = createChoice(ResourceType.CLAY, ResourceType.WOOD)
+ assertEquals(prodIncrease, gson.fromJson("\"W/C\""))
+ }
+
+ @Test
+ fun deserialize_choice3_unordered() {
+ val prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY, ResourceType.ORE)
+ assertEquals(prodIncrease, gson.fromJson("\"W/O/C\""))
+ }
+
+ @Test
+ fun deserialize_failOnMultipleResourcesInChoice() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<Production>("\"W/SS/C\"")
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.kt
new file mode 100644
index 00000000..f2b07e84
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.kt
@@ -0,0 +1,56 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.ResourceType
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+
+class ResourceTypeSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ gson = GsonBuilder().registerTypeAdapter(ResourceType::class.java, ResourceTypeSerializer()).create()
+ }
+
+ @Test
+ fun serialize_useSymbolForEachType() {
+ ResourceType.values().forEach { type ->
+ val expectedJson = "\"" + type.symbol + "\""
+ assertEquals(expectedJson, gson.toJson(type))
+ }
+ }
+
+ @Test
+ fun deserialize_useSymbolForEachType() {
+ ResourceType.values().forEach { type ->
+ val typeInJson = "\"" + type.symbol + "\""
+ assertEquals(type, gson.fromJson(typeInJson))
+ }
+ }
+
+ @Test
+ fun deserialize_nullFromNull() {
+ assertNull(gson.fromJson("null", ResourceType::class.java))
+ }
+
+ @Test
+ fun deserialize_failsOnEmptyString() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<ResourceType>("\"\"")
+ }
+ }
+
+ @Test
+ fun deserialize_failsOnGarbageString() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<ResourceType>("\"thisisgarbage\"")
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.kt
new file mode 100644
index 00000000..8c1b421d
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.kt
@@ -0,0 +1,80 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.github.salomonbrys.kotson.typeToken
+import com.github.salomonbrys.kotson.typedToJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.ResourceType
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class ResourceTypesSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ gson = GsonBuilder().registerTypeAdapter(typeToken<List<ResourceType>>(), ResourceTypesSerializer()).create()
+ }
+
+ @Test
+ fun serialize_null() {
+ assertEquals("null", gson.toJson(null, typeToken<List<ResourceType>>()))
+ }
+
+ @Test
+ fun serialize_emptyList() {
+ val types = emptyList<ResourceType>()
+ assertEquals("\"\"", gson.typedToJson(types))
+ }
+
+ @Test
+ fun serialize_singleType() {
+ val types = listOf(ResourceType.WOOD)
+ assertEquals("\"W\"", gson.typedToJson(types))
+ }
+
+ @Test
+ fun serialize_multipleTimesSameType() {
+ val types = List(3) { ResourceType.WOOD }
+ assertEquals("\"WWW\"", gson.typedToJson(types))
+ }
+
+ @Test
+ fun serialize_mixedTypes() {
+ val types = listOf(ResourceType.WOOD, ResourceType.CLAY, ResourceType.STONE)
+ assertEquals("\"WCS\"", gson.typedToJson(types))
+ }
+
+ @Test
+ fun deserialize_null() {
+ assertNull(gson.fromJson("null", typeToken<List<ResourceType>>()))
+ }
+
+ @Test
+ fun deserialize_emptyList() {
+ val types = emptyList<ResourceType>()
+ assertEquals(types, gson.fromJson("\"\""))
+ }
+
+ @Test
+ fun deserialize_singleType() {
+ val types = listOf(ResourceType.WOOD)
+ assertEquals(types, gson.fromJson("\"W\""))
+ }
+
+ @Test
+ fun deserialize_multipleTimesSameType() {
+ val types = List(3) { ResourceType.WOOD }
+ assertEquals(types, gson.fromJson("\"WWW\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes() {
+ val types = listOf(ResourceType.WOOD, ResourceType.CLAY, ResourceType.STONE)
+ assertEquals(types, gson.fromJson("\"WCS\""))
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.kt
new file mode 100644
index 00000000..c146a948
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.kt
@@ -0,0 +1,99 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.MutableResources
+import org.luxons.sevenwonders.game.resources.ResourceType.CLAY
+import org.luxons.sevenwonders.game.resources.ResourceType.STONE
+import org.luxons.sevenwonders.game.resources.ResourceType.WOOD
+import org.luxons.sevenwonders.game.resources.Resources
+import org.luxons.sevenwonders.game.resources.emptyResources
+import org.luxons.sevenwonders.game.resources.resourcesOf
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class ResourcesSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ gson = GsonBuilder()
+ .registerTypeAdapter(Resources::class.java, ResourcesSerializer())
+ .registerTypeAdapter(MutableResources::class.java, ResourcesSerializer())
+ .create()
+ }
+
+ @Test
+ fun serialize_null() {
+ assertEquals("null", gson.toJson(null, Resources::class.java))
+ }
+
+ @Test
+ fun serialize_emptyResourcesToNull() {
+ val resources = emptyResources()
+ assertEquals("null", gson.toJson(resources))
+ }
+
+ @Test
+ fun serialize_singleType() {
+ val resources = resourcesOf(WOOD)
+ assertEquals("\"W\"", gson.toJson(resources))
+ }
+
+ @Test
+ fun serialize_multipleTimesSameType() {
+ val resources = resourcesOf(WOOD to 3)
+ assertEquals("\"WWW\"", gson.toJson(resources))
+ }
+
+ @Test
+ fun serialize_mixedTypes() {
+ val resources = resourcesOf(WOOD, STONE, CLAY)
+ assertEquals("\"WSC\"", gson.toJson(resources))
+ }
+
+ @Test
+ fun serialize_mixedTypes_unordered() {
+ val resources = resourcesOf(CLAY to 1, WOOD to 2, CLAY to 1, STONE to 1)
+ assertEquals("\"CCWWS\"", gson.toJson(resources))
+ }
+
+ @Test
+ fun deserialize_null() {
+ assertNull(gson.fromJson("null", Resources::class.java))
+ }
+
+ @Test
+ fun deserialize_emptyList() {
+ val resources = emptyResources()
+ assertEquals(resources, gson.fromJson("\"\""))
+ }
+
+ @Test
+ fun deserialize_singleType() {
+ val resources = resourcesOf(WOOD)
+ assertEquals(resources, gson.fromJson("\"W\""))
+ }
+
+ @Test
+ fun deserialize_multipleTimesSameType() {
+ val resources = resourcesOf(WOOD to 3)
+ assertEquals(resources, gson.fromJson("\"WWW\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes() {
+ val resources = resourcesOf(WOOD, CLAY, STONE)
+ assertEquals(resources, gson.fromJson("\"WCS\""))
+ }
+
+ @Test
+ fun deserialize_mixedTypes_unordered() {
+ val resources = resourcesOf(WOOD to 1, CLAY to 2, STONE to 3)
+ assertEquals(resources, gson.fromJson("\"SCWCSS\""))
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.kt
new file mode 100644
index 00000000..95d72517
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.kt
@@ -0,0 +1,157 @@
+package org.luxons.sevenwonders.game.data.serializers
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.boards.ScienceType
+import org.luxons.sevenwonders.game.effects.ScienceProgress
+import org.luxons.sevenwonders.game.test.createScienceProgress
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+
+private const val TABLET_STR = "\"TABLET\""
+private const val COMPASS_STR = "\"COMPASS\""
+private const val WHEEL_STR = "\"WHEEL\""
+private const val JOKER_STR = "\"any\""
+
+class ScienceProgressSerializerTest {
+
+ private lateinit var gson: Gson
+
+ @Before
+ fun setUp() {
+ gson = GsonBuilder().registerTypeAdapter(ScienceProgress::class.java, ScienceProgressSerializer()).create()
+ }
+
+ @Test
+ fun serialize_emptyToNull() {
+ val progress = createScienceProgress(0, 0, 0, 0)
+ val json = gson.toJson(progress)
+ assertEquals("null", json)
+ }
+
+ @Test
+ fun serialize_oneCompass() {
+ val progress = createScienceProgress(1, 0, 0, 0)
+ val json = gson.toJson(progress)
+ assertEquals(COMPASS_STR, json)
+ }
+
+ @Test
+ fun serialize_oneWheel() {
+ val progress = createScienceProgress(0, 1, 0, 0)
+ val json = gson.toJson(progress)
+ assertEquals(WHEEL_STR, json)
+ }
+
+ @Test
+ fun serialize_oneTablet() {
+ val progress = createScienceProgress(0, 0, 1, 0)
+ val json = gson.toJson(progress)
+ assertEquals(TABLET_STR, json)
+ }
+
+ @Test
+ fun serialize_oneJoker() {
+ val progress = createScienceProgress(0, 0, 0, 1)
+ val json = gson.toJson(progress)
+ assertEquals(JOKER_STR, json)
+ }
+
+ @Test
+ fun serialize_failOnMultipleCompasses() {
+ assertFailsWith<UnsupportedOperationException> {
+ val progress = createScienceProgress(2, 0, 0, 0)
+ gson.toJson(progress)
+ }
+ }
+
+ @Test
+ fun serialize_failOnMultipleWheels() {
+ assertFailsWith<UnsupportedOperationException> {
+ val progress = createScienceProgress(0, 2, 0, 0)
+ gson.toJson(progress)
+ }
+ }
+
+ @Test
+ fun serialize_failOnMultipleTablets() {
+ assertFailsWith<UnsupportedOperationException> {
+ val progress = createScienceProgress(0, 0, 2, 0)
+ gson.toJson(progress)
+ }
+ }
+
+ @Test
+ fun serialize_failOnMultipleJokers() {
+ assertFailsWith<UnsupportedOperationException> {
+ val progress = createScienceProgress(0, 0, 0, 2)
+ gson.toJson(progress)
+ }
+ }
+
+ @Test
+ fun serialize_failOnMixedElements() {
+ assertFailsWith<UnsupportedOperationException> {
+ val progress = createScienceProgress(1, 1, 0, 0)
+ gson.toJson(progress)
+ }
+ }
+
+ @Test
+ fun deserialize_failOnEmptyString() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<ScienceProgress>("\"\"")
+ }
+ }
+
+ @Test
+ fun deserialize_failOnGarbageString() {
+ assertFailsWith<IllegalArgumentException> {
+ gson.fromJson<ScienceProgress>("thisisgarbage")
+ }
+ }
+
+ @Test
+ fun deserialize_compass() {
+ val progress = gson.fromJson<ScienceProgress>(COMPASS_STR)
+ assertNotNull(progress.science)
+ assertEquals(1, progress.science.getQuantity(ScienceType.COMPASS))
+ assertEquals(0, progress.science.getQuantity(ScienceType.WHEEL))
+ assertEquals(0, progress.science.getQuantity(ScienceType.TABLET))
+ assertEquals(0, progress.science.jokers)
+ }
+
+ @Test
+ fun deserialize_wheel() {
+ val progress = gson.fromJson<ScienceProgress>(WHEEL_STR)
+ assertNotNull(progress.science)
+ assertEquals(0, progress.science.getQuantity(ScienceType.COMPASS))
+ assertEquals(1, progress.science.getQuantity(ScienceType.WHEEL))
+ assertEquals(0, progress.science.getQuantity(ScienceType.TABLET))
+ assertEquals(0, progress.science.jokers)
+ }
+
+ @Test
+ fun deserialize_tablet() {
+ val progress = gson.fromJson<ScienceProgress>(TABLET_STR)
+ assertNotNull(progress.science)
+ assertEquals(0, progress.science.getQuantity(ScienceType.COMPASS))
+ assertEquals(0, progress.science.getQuantity(ScienceType.WHEEL))
+ assertEquals(1, progress.science.getQuantity(ScienceType.TABLET))
+ assertEquals(0, progress.science.jokers)
+ }
+
+ @Test
+ fun deserialize_joker() {
+ val progress = gson.fromJson<ScienceProgress>(JOKER_STR)
+ assertNotNull(progress.science)
+ assertEquals(0, progress.science.getQuantity(ScienceType.COMPASS))
+ assertEquals(0, progress.science.getQuantity(ScienceType.WHEEL))
+ assertEquals(0, progress.science.getQuantity(ScienceType.TABLET))
+ assertEquals(1, progress.science.jokers)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.kt
new file mode 100644
index 00000000..700eddb1
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.kt
@@ -0,0 +1,142 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.Before
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.test.addCards
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class BonusPerBoardElementTest {
+
+ private lateinit var table: Table
+ private lateinit var player0: Player
+
+ @Before
+ fun setUp() {
+ table = testTable(4)
+ player0 = SimplePlayer(0, table)
+ }
+
+ @Theory
+ fun computePoints_countsCards(
+ boardPosition: RelativeBoardPosition,
+ nbCards: Int,
+ nbOtherCards: Int,
+ points: Int,
+ gold: Int,
+ color: Color
+ ) {
+ val board = table.getBoard(0, boardPosition)
+ addCards(board, nbCards, nbOtherCards, color)
+
+ val bonus = BonusPerBoardElement(listOf(boardPosition), BoardElementType.CARD, gold, points, listOf(color))
+
+ assertEquals(nbCards * points, bonus.computePoints(player0))
+ }
+
+ @Theory
+ fun computePoints_countsDefeatTokens(
+ boardPosition: RelativeBoardPosition,
+ nbDefeatTokens: Int,
+ points: Int,
+ gold: Int
+ ) {
+ val board = table.getBoard(0, boardPosition)
+ repeat(nbDefeatTokens) {
+ board.military.defeat()
+ }
+
+ val bonus = BonusPerBoardElement(listOf(boardPosition), BoardElementType.DEFEAT_TOKEN, gold, points, listOf())
+
+ assertEquals(nbDefeatTokens * points, bonus.computePoints(player0))
+ }
+
+ @Theory
+ fun computePoints_countsWonderStages(boardPosition: RelativeBoardPosition, nbStages: Int, points: Int, gold: Int) {
+ val board = table.getBoard(0, boardPosition)
+ repeat(nbStages) {
+ board.wonder.placeCard(CardBack(""))
+ }
+
+ val bonus =
+ BonusPerBoardElement(listOf(boardPosition), BoardElementType.BUILT_WONDER_STAGES, gold, points, listOf())
+
+ assertEquals(nbStages * points, bonus.computePoints(player0))
+ }
+
+ @Theory
+ fun apply_countsCards(
+ boardPosition: RelativeBoardPosition,
+ nbCards: Int,
+ nbOtherCards: Int,
+ points: Int,
+ gold: Int,
+ color: Color
+ ) {
+ val board = table.getBoard(0, boardPosition)
+ addCards(board, nbCards, nbOtherCards, color)
+
+ val bonus = BonusPerBoardElement(listOf(boardPosition), BoardElementType.CARD, gold, points, listOf(color))
+
+ val selfBoard = table.getBoard(0)
+ val initialGold = selfBoard.gold
+ bonus.applyTo(player0)
+ assertEquals(initialGold + nbCards * gold, selfBoard.gold)
+ }
+
+ @Theory
+ fun apply_countsDefeatTokens(boardPosition: RelativeBoardPosition, nbDefeatTokens: Int, points: Int, gold: Int) {
+ val board = table.getBoard(0, boardPosition)
+ repeat(nbDefeatTokens) {
+ board.military.defeat()
+ }
+
+ val bonus = BonusPerBoardElement(listOf(boardPosition), BoardElementType.DEFEAT_TOKEN, gold, points, listOf())
+
+ val selfBoard = table.getBoard(0)
+ val initialGold = selfBoard.gold
+ bonus.applyTo(player0)
+ assertEquals(initialGold + nbDefeatTokens * gold, selfBoard.gold)
+ }
+
+ @Theory
+ fun apply_countsWonderStages(boardPosition: RelativeBoardPosition, nbStages: Int, points: Int, gold: Int) {
+ val board = table.getBoard(0, boardPosition)
+ repeat(nbStages) {
+ board.wonder.placeCard(CardBack(""))
+ }
+
+ val bonus =
+ BonusPerBoardElement(listOf(boardPosition), BoardElementType.BUILT_WONDER_STAGES, gold, points, listOf())
+
+ val selfBoard = table.getBoard(0)
+ val initialGold = selfBoard.gold
+ bonus.applyTo(player0)
+ assertEquals(initialGold + nbStages * gold, selfBoard.gold)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun values(): IntArray = intArrayOf(0, 1, 2, 3)
+
+ @JvmStatic
+ @DataPoints
+ fun colors(): Array<Color> = Color.values()
+
+ @JvmStatic
+ @DataPoints
+ fun positions(): Array<RelativeBoardPosition> = RelativeBoardPosition.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/DiscountTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/DiscountTest.kt
new file mode 100644
index 00000000..d92c8d24
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/DiscountTest.kt
@@ -0,0 +1,69 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.Assume
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.resources.Provider
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.test.createTransactions
+import org.luxons.sevenwonders.game.test.testBoard
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class DiscountTest {
+
+ @Theory
+ fun apply_givesDiscountedPrice(discountedPrice: Int, discountedType: ResourceType, provider: Provider) {
+ val board = testBoard(ResourceType.CLAY, 3)
+ val discount = Discount(listOf(discountedType), listOf(provider), discountedPrice)
+ discount.applyTo(board)
+
+ val transactions = createTransactions(provider, discountedType)
+ assertEquals(discountedPrice, board.tradingRules.computeCost(transactions))
+ }
+
+ @Theory
+ fun apply_doesNotAffectOtherResources(
+ discountedPrice: Int,
+ discountedType: ResourceType,
+ provider: Provider,
+ otherType: ResourceType,
+ otherProvider: Provider
+ ) {
+ Assume.assumeTrue(otherProvider != provider)
+ Assume.assumeTrue(otherType != discountedType)
+
+ val board = testBoard(ResourceType.CLAY, 3)
+ val discount = Discount(listOf(discountedType), listOf(provider), discountedPrice)
+ discount.applyTo(board)
+
+ // this is the default in the settings used by TestUtilsKt.testBoard()
+ val normalPrice = 2
+
+ val fromOtherType = createTransactions(provider, otherType)
+ assertEquals(normalPrice, board.tradingRules.computeCost(fromOtherType))
+
+ val fromOtherProvider = createTransactions(otherProvider, discountedType)
+ assertEquals(normalPrice, board.tradingRules.computeCost(fromOtherProvider))
+
+ val fromOtherProviderAndType = createTransactions(otherProvider, otherType)
+ assertEquals(normalPrice, board.tradingRules.computeCost(fromOtherProviderAndType))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun discountedPrices(): IntArray = intArrayOf(0, 1, 2)
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+
+ @JvmStatic
+ @DataPoints
+ fun providers(): Array<Provider> = Provider.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.kt
new file mode 100644
index 00000000..993cc273
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.kt
@@ -0,0 +1,43 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.test.testBoard
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class GoldIncreaseTest {
+
+ @Theory
+ fun apply_increaseGoldWithRightAmount(initialAmount: Int, goldIncreaseAmount: Int, type: ResourceType) {
+ val board = testBoard(type, initialAmount)
+ val goldIncrease = GoldIncrease(goldIncreaseAmount)
+
+ goldIncrease.applyTo(board)
+
+ assertEquals(initialAmount + goldIncreaseAmount, board.gold)
+ }
+
+ @Theory
+ fun computePoints_isAlwaysZero(gold: Int) {
+ val goldIncrease = GoldIncrease(gold)
+ val player = SimplePlayer(0, testTable(5))
+ assertEquals(0, goldIncrease.computePoints(player))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun goldAmounts(): IntArray = intArrayOf(-5, -1, 0, 1, 2, 5, 10)
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.kt
new file mode 100644
index 00000000..0d5765da
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.kt
@@ -0,0 +1,44 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.test.testBoard
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class MilitaryReinforcementsTest {
+
+ @Theory
+ fun apply_increaseGoldWithRightAmount(initialShields: Int, additionalShields: Int, type: ResourceType) {
+ val board = testBoard(type)
+ board.military.addShields(initialShields)
+
+ val reinforcements = MilitaryReinforcements(additionalShields)
+ reinforcements.applyTo(board)
+
+ assertEquals(initialShields + additionalShields, board.military.nbShields)
+ }
+
+ @Theory
+ fun computePoints_isAlwaysZero(shields: Int) {
+ val reinforcements = MilitaryReinforcements(shields)
+ val player = SimplePlayer(0, testTable(5))
+ assertEquals(0, reinforcements.computePoints(player))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun shieldCounts(): IntArray = intArrayOf(0, 1, 2, 3, 5)
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.kt
new file mode 100644
index 00000000..c016ccc9
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.kt
@@ -0,0 +1,73 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.resourcesOf
+import org.luxons.sevenwonders.game.test.fixedProduction
+import org.luxons.sevenwonders.game.test.testBoard
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class ProductionIncreaseTest {
+
+ @Theory
+ fun apply_boardContainsAddedResourceType(
+ initialType: ResourceType,
+ addedType: ResourceType,
+ extraType: ResourceType
+ ) {
+ val board = testBoard(initialType)
+ val effect = ProductionIncrease(fixedProduction(addedType), false)
+
+ effect.applyTo(board)
+
+ val resources = resourcesOf(initialType, addedType)
+ assertTrue(board.production.contains(resources))
+ assertFalse(board.publicProduction.contains(resources))
+
+ val moreResources = resourcesOf(initialType, addedType, extraType)
+ assertFalse(board.production.contains(moreResources))
+ assertFalse(board.publicProduction.contains(moreResources))
+ }
+
+ @Theory
+ fun apply_boardContainsAddedResourceType_sellable(
+ initialType: ResourceType,
+ addedType: ResourceType,
+ extraType: ResourceType
+ ) {
+ val board = testBoard(initialType)
+ val effect = ProductionIncrease(fixedProduction(addedType), true)
+
+ effect.applyTo(board)
+
+ val resources = resourcesOf(initialType, addedType)
+ assertTrue(board.production.contains(resources))
+ assertTrue(board.publicProduction.contains(resources))
+
+ val moreResources = resourcesOf(initialType, addedType, extraType)
+ assertFalse(board.production.contains(moreResources))
+ assertFalse(board.publicProduction.contains(moreResources))
+ }
+
+ @Theory
+ fun computePoints_isAlwaysZero(addedType: ResourceType) {
+ val effect = ProductionIncrease(fixedProduction(addedType), false)
+ val player = SimplePlayer(0, testTable(5))
+ assertEquals(0, effect.computePoints(player))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.kt
new file mode 100644
index 00000000..9cb10562
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.kt
@@ -0,0 +1,29 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class RawPointsIncreaseTest {
+
+ @Theory
+ fun computePoints_equalsNbOfPoints(points: Int) {
+ val rawPointsIncrease = RawPointsIncrease(points)
+ val player = SimplePlayer(0, testTable(5))
+ assertEquals(points, rawPointsIncrease.computePoints(player))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun points(): IntArray {
+ return intArrayOf(0, 1, 2, 3, 5)
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgressTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgressTest.kt
new file mode 100644
index 00000000..7e566a8c
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/ScienceProgressTest.kt
@@ -0,0 +1,49 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.boards.ScienceType
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.test.createScience
+import org.luxons.sevenwonders.game.test.createScienceProgress
+import org.luxons.sevenwonders.game.test.testBoard
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class ScienceProgressTest {
+
+ @Theory
+ fun apply_initContainsAddedScience(
+ initCompasses: Int,
+ initWheels: Int,
+ initTablets: Int,
+ initJokers: Int,
+ compasses: Int,
+ wheels: Int,
+ tablets: Int,
+ jokers: Int
+ ) {
+ val board = testBoard(ResourceType.ORE)
+ val initialScience = createScience(initCompasses, initWheels, initTablets, initJokers)
+ board.science.addAll(initialScience)
+
+ val effect = createScienceProgress(compasses, wheels, tablets, jokers)
+ effect.applyTo(board)
+
+ assertEquals(initCompasses + compasses, board.science.getQuantity(ScienceType.COMPASS))
+ assertEquals(initWheels + wheels, board.science.getQuantity(ScienceType.WHEEL))
+ assertEquals(initTablets + tablets, board.science.getQuantity(ScienceType.TABLET))
+ assertEquals(initJokers + jokers, board.science.jokers)
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun elementsCount(): IntArray {
+ return intArrayOf(0, 1, 2)
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.kt
new file mode 100644
index 00000000..aae3be8e
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.kt
@@ -0,0 +1,93 @@
+package org.luxons.sevenwonders.game.effects
+
+import org.junit.Assume
+import org.junit.Test
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.test.createGuildCard
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+@RunWith(Theories::class)
+class SpecialAbilityActivationTest {
+
+ @Theory
+ fun apply_addsAbility(ability: SpecialAbility) {
+ val effect = SpecialAbilityActivation(ability)
+ val player = SimplePlayer(0, testTable(5))
+
+ effect.applyTo(player)
+
+ assertTrue(player.board.hasSpecial(ability))
+ }
+
+ @Theory
+ fun computePoints_zeroExceptForCopyGuild(ability: SpecialAbility) {
+ Assume.assumeTrue(ability !== SpecialAbility.COPY_GUILD)
+
+ val effect = SpecialAbilityActivation(ability)
+ val player = SimplePlayer(0, testTable(5))
+
+ assertEquals(0, effect.computePoints(player))
+ }
+
+ @Theory
+ internal fun computePoints_copiedGuild(guildCard: Card, neighbour: RelativeBoardPosition) {
+ val effect = SpecialAbilityActivation(SpecialAbility.COPY_GUILD)
+ val player = SimplePlayer(0, testTable(5))
+
+ val neighbourBoard = player.getBoard(neighbour)
+ neighbourBoard.addCard(guildCard)
+
+ player.board.copiedGuild = guildCard
+
+ val directPointsFromGuildCard = guildCard.effects.stream().mapToInt { e -> e.computePoints(player) }.sum()
+ assertEquals(directPointsFromGuildCard, effect.computePoints(player))
+ }
+
+ @Test
+ fun computePoints_copyGuild_failWhenNoChosenGuild() {
+ val effect = SpecialAbilityActivation(SpecialAbility.COPY_GUILD)
+ val player = SimplePlayer(0, testTable(5))
+ assertFailsWith<IllegalStateException> {
+ effect.computePoints(player)
+ }
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun abilities(): Array<SpecialAbility> = SpecialAbility.values()
+
+ @JvmStatic
+ @DataPoints
+ fun neighbours(): Array<RelativeBoardPosition> =
+ arrayOf(RelativeBoardPosition.LEFT, RelativeBoardPosition.RIGHT)
+
+ @JvmStatic
+ @DataPoints
+ internal fun guilds(): Array<Card> {
+ val bonus = BonusPerBoardElement(
+ listOf(RelativeBoardPosition.LEFT, RelativeBoardPosition.RIGHT),
+ BoardElementType.CARD,
+ points = 1,
+ colors = listOf(Color.GREY, Color.BROWN)
+ )
+ val bonus2 = BonusPerBoardElement(
+ listOf(RelativeBoardPosition.LEFT, RelativeBoardPosition.SELF, RelativeBoardPosition.RIGHT),
+ BoardElementType.BUILT_WONDER_STAGES,
+ points = 1
+ )
+ return arrayOf(createGuildCard(1, bonus), createGuildCard(2, bonus2))
+ }
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMoveTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMoveTest.kt
new file mode 100644
index 00000000..21b92872
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/moves/BuildWonderMoveTest.kt
@@ -0,0 +1,82 @@
+package org.luxons.sevenwonders.game.moves
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.test.createMove
+import org.luxons.sevenwonders.game.test.sampleCards
+import org.luxons.sevenwonders.game.test.testCard
+import org.luxons.sevenwonders.game.test.testSettings
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+class BuildWonderMoveTest {
+
+ @Test
+ fun init_failsWhenCardNotInHand() {
+ val table = testTable(3)
+ val hand = sampleCards(7)
+ val playerContext = PlayerContext(0, table, hand)
+ val anotherCard = testCard("Card that is not in the hand")
+
+ assertFailsWith<InvalidMoveException> {
+ createMove(playerContext, anotherCard, MoveType.UPGRADE_WONDER)
+ }
+ }
+
+ @Test
+ fun init_failsWhenWonderIsCompletelyBuilt() {
+ val settings = testSettings(3)
+ val table = testTable(settings)
+ val hand = sampleCards(7)
+
+ fillPlayerWonderLevels(settings, table, hand)
+
+ // should fail because the wonder is already full
+ assertFailsWith<InvalidMoveException> {
+ buildOneWonderLevel(settings, table, hand, 4)
+ }
+ }
+
+ private fun fillPlayerWonderLevels(settings: Settings, table: Table, hand: List<Card>) {
+ try {
+ val nbLevels = table.getBoard(0).wonder.stages.size
+ repeat(nbLevels) {
+ buildOneWonderLevel(settings, table, hand, it)
+ }
+ } catch (e: InvalidMoveException) {
+ fail("Building wonder levels should not fail before being full")
+ }
+ }
+
+ private fun buildOneWonderLevel(settings: Settings, table: Table, hand: List<Card>, cardIndex: Int) {
+ val card = hand[cardIndex]
+ val playerContext = PlayerContext(0, table, hand)
+ val move = createMove(playerContext, card, MoveType.UPGRADE_WONDER)
+ move.place(mutableListOf(), settings)
+ move.activate(emptyList(), settings)
+ }
+
+ @Test
+ fun place_increasesWonderLevel() {
+ val settings = testSettings(3)
+ val table = testTable(settings)
+ val hand = sampleCards(7)
+ val cardToUse = hand[0]
+ val playerContext = PlayerContext(0, table, hand)
+ val move = createMove(playerContext, cardToUse, MoveType.UPGRADE_WONDER)
+
+ val initialStage = table.getBoard(0).wonder.nbBuiltStages
+
+ move.place(mutableListOf(), settings)
+
+ val newStage = table.getBoard(0).wonder.nbBuiltStages
+
+ // we need to see the level increase before activation so that other players
+ assertEquals(initialStage + 1, newStage)
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculatorTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculatorTest.kt
new file mode 100644
index 00000000..b4c3b886
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/BestPriceCalculatorTest.kt
@@ -0,0 +1,137 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.SimplePlayer
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.resources.Provider.LEFT_PLAYER
+import org.luxons.sevenwonders.game.resources.Provider.RIGHT_PLAYER
+import org.luxons.sevenwonders.game.resources.ResourceType.CLAY
+import org.luxons.sevenwonders.game.resources.ResourceType.GLASS
+import org.luxons.sevenwonders.game.resources.ResourceType.ORE
+import org.luxons.sevenwonders.game.resources.ResourceType.STONE
+import org.luxons.sevenwonders.game.resources.ResourceType.WOOD
+import org.luxons.sevenwonders.game.test.createTransaction
+import org.luxons.sevenwonders.game.test.createTransactions
+import org.luxons.sevenwonders.game.test.testBoard
+import org.luxons.sevenwonders.game.test.testTable
+import kotlin.test.assertEquals
+
+class BestPriceCalculatorTest {
+
+ private fun solutions(price: Int, vararg resourceTransactions: ResourceTransactions) =
+ TransactionPlan(price, setOf(*resourceTransactions))
+
+ @Test
+ fun bestPrice_0forEmptyResources() {
+ val table = testTable(3)
+ val player0 = SimplePlayer(0, table)
+ val emptyResources = emptyResources()
+ val emptyTransactions = noTransactions()
+ assertEquals(solutions(0, emptyTransactions), bestSolution(emptyResources, player0))
+ }
+
+ @Test
+ fun bestPrice_fixedResources_defaultCost() {
+ val left = testBoard(STONE)
+ val main = testBoard(STONE)
+ val right = testBoard(WOOD)
+ val table = Table(listOf(main, right, left))
+
+ val player0 = SimplePlayer(0, table)
+ val player1 = SimplePlayer(1, table)
+ val player2 = SimplePlayer(2, table)
+
+ val resources = resourcesOf(STONE, STONE)
+
+ val stoneLeftSingle = createTransaction(LEFT_PLAYER, STONE)
+ val stoneRightSingle = createTransaction(RIGHT_PLAYER, STONE)
+
+ val stoneLeft = createTransactions(stoneLeftSingle)
+ val stoneRight = createTransactions(stoneRightSingle)
+ val stoneLeftAndRight = createTransactions(stoneLeftSingle, stoneRightSingle)
+
+ assertEquals(solutions(2, stoneLeft), bestSolution(resources, player0))
+ assertEquals(solutions(4, stoneLeftAndRight), bestSolution(resources, player1))
+ assertEquals(solutions(2, stoneRight), bestSolution(resources, player2))
+ }
+
+ @Test
+ fun bestPrice_fixedResources_overridenCost() {
+ val main = testBoard(STONE)
+ main.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1)
+
+ val left = testBoard(WOOD)
+ val right = testBoard(WOOD)
+ val opposite = testBoard(GLASS)
+ val table = Table(listOf(main, right, opposite, left))
+
+ val player0 = SimplePlayer(0, table)
+ val player1 = SimplePlayer(1, table)
+ val player2 = SimplePlayer(2, table)
+ val player3 = SimplePlayer(3, table)
+
+ val resources = resourcesOf(WOOD)
+
+ val woodLeft = createTransactions(LEFT_PLAYER, WOOD)
+ val woodRight = createTransactions(RIGHT_PLAYER, WOOD)
+
+ assertEquals(solutions(1, woodRight), bestSolution(resources, player0))
+ assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1))
+ assertEquals(solutions(2, woodLeft, woodRight), bestSolution(resources, player2))
+ assertEquals(solutions(0, noTransactions()), bestSolution(resources, player3))
+ }
+
+ @Test
+ fun bestPrice_mixedResources_overridenCost() {
+ val left = testBoard(WOOD)
+
+ val main = testBoard(STONE)
+ main.tradingRules.setCost(WOOD, RIGHT_PLAYER, 1)
+
+ val right = testBoard(ORE)
+ right.production.addChoice(WOOD, CLAY)
+ right.publicProduction.addChoice(WOOD, CLAY)
+
+ val table = Table(listOf(main, right, left))
+
+ val player0 = SimplePlayer(0, table)
+ val player1 = SimplePlayer(1, table)
+ val player2 = SimplePlayer(2, table)
+
+ val resources = resourcesOf(WOOD)
+ val woodRight = createTransactions(RIGHT_PLAYER, WOOD)
+
+ assertEquals(solutions(1, woodRight), bestSolution(resources, player0))
+ assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1))
+ assertEquals(solutions(0, noTransactions()), bestSolution(resources, player2))
+ }
+
+ @Test
+ fun bestPrice_chooseCheapest() {
+ val left = testBoard(WOOD)
+
+ val main = testBoard(WOOD)
+ main.production.addChoice(CLAY, ORE)
+ main.tradingRules.setCost(CLAY, RIGHT_PLAYER, 1)
+
+ val right = testBoard(WOOD)
+ right.production.addFixedResource(ORE, 1)
+ right.production.addFixedResource(CLAY, 1)
+ right.publicProduction.addFixedResource(ORE, 1)
+ right.publicProduction.addFixedResource(CLAY, 1)
+
+ val table = Table(listOf(main, right, left))
+
+ val player0 = SimplePlayer(0, table)
+ val player1 = SimplePlayer(1, table)
+ val player2 = SimplePlayer(2, table)
+
+ val resources = resourcesOf(ORE, CLAY)
+ val oreAndClayLeft = createTransactions(LEFT_PLAYER, ORE, CLAY)
+ val clayRight = createTransactions(RIGHT_PLAYER, CLAY)
+
+ assertEquals(solutions(1, clayRight), bestSolution(resources, player0))
+ assertEquals(solutions(0, noTransactions()), bestSolution(resources, player1))
+ assertEquals(solutions(4, oreAndClayLeft), bestSolution(resources, player2))
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ProductionTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ProductionTest.kt
new file mode 100644
index 00000000..0e865921
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ProductionTest.kt
@@ -0,0 +1,292 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.junit.Before
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.ResourceType.CLAY
+import org.luxons.sevenwonders.game.resources.ResourceType.GLASS
+import org.luxons.sevenwonders.game.resources.ResourceType.LOOM
+import org.luxons.sevenwonders.game.resources.ResourceType.ORE
+import org.luxons.sevenwonders.game.resources.ResourceType.PAPYRUS
+import org.luxons.sevenwonders.game.resources.ResourceType.STONE
+import org.luxons.sevenwonders.game.resources.ResourceType.WOOD
+import java.util.EnumSet
+import java.util.HashSet
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ProductionTest {
+
+ private lateinit var emptyResources: Resources
+ private lateinit var resources1Wood: Resources
+ private lateinit var resources1Stone: Resources
+ private lateinit var resources1Stone1Wood: Resources
+ private lateinit var resources2Stones: Resources
+ private lateinit var resources2Stones3Clay: Resources
+
+ @Before
+ fun init() {
+ emptyResources = emptyResources()
+ resources1Wood = resourcesOf(WOOD)
+ resources1Stone = resourcesOf(STONE)
+ resources1Stone1Wood = resourcesOf(STONE to 1, WOOD to 1)
+ resources2Stones = resourcesOf(STONE to 2)
+ resources2Stones3Clay = resourcesOf(STONE to 2, CLAY to 3)
+ }
+
+ @Test
+ fun contains_newProductionContainsEmpty() {
+ val production = Production()
+ assertTrue(production.contains(emptyResources))
+ }
+
+ @Test
+ fun contains_singleFixedResource_noneAtAll() {
+ val production = Production()
+ assertFalse(production.contains(resources2Stones))
+ }
+
+ @Test
+ fun contains_singleFixedResource_notEnough() {
+ val production = Production()
+ production.addFixedResource(STONE, 1)
+ assertFalse(production.contains(resources2Stones))
+ }
+
+ @Test
+ fun contains_singleFixedResource_justEnough() {
+ val production = Production()
+ production.addFixedResource(STONE, 2)
+ assertTrue(production.contains(resources2Stones))
+ }
+
+ @Test
+ fun contains_singleFixedResource_moreThanEnough() {
+ val production = Production()
+ production.addFixedResource(STONE, 3)
+ assertTrue(production.contains(resources2Stones))
+ }
+
+ @Test
+ fun contains_singleFixedResource_moreThanEnough_amongOthers() {
+ val production = Production()
+ production.addFixedResource(STONE, 3)
+ production.addFixedResource(CLAY, 2)
+ assertTrue(production.contains(resources2Stones))
+ }
+
+ @Test
+ fun contains_multipleFixedResources_notEnoughOfOne() {
+ val production = Production()
+ production.addFixedResource(STONE, 3)
+ production.addFixedResource(CLAY, 1)
+ assertFalse(production.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun contains_multipleFixedResources_notEnoughOfBoth() {
+ val production = Production()
+ production.addFixedResource(STONE, 1)
+ production.addFixedResource(CLAY, 1)
+ assertFalse(production.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun contains_multipleFixedResources_moreThanEnough() {
+ val production = Production()
+ production.addFixedResource(STONE, 3)
+ production.addFixedResource(CLAY, 5)
+ assertTrue(production.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun contains_singleChoice_containsEmpty() {
+ val production = Production()
+ production.addChoice(STONE, CLAY)
+ assertTrue(production.contains(emptyResources))
+ }
+
+ @Test
+ fun contains_singleChoice_enough() {
+ val production = Production()
+ production.addChoice(STONE, WOOD)
+ assertTrue(production.contains(resources1Wood))
+ assertTrue(production.contains(resources1Stone))
+ }
+
+ @Test
+ fun contains_multipleChoices_notBoth() {
+ val production = Production()
+ production.addChoice(STONE, CLAY)
+ production.addChoice(STONE, CLAY)
+ production.addChoice(STONE, CLAY)
+ assertFalse(production.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun contains_multipleChoices_enough() {
+ val production = Production()
+ production.addChoice(STONE, ORE)
+ production.addChoice(STONE, WOOD)
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun contains_multipleChoices_enoughReverseOrder() {
+ val production = Production()
+ production.addChoice(STONE, WOOD)
+ production.addChoice(STONE, ORE)
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun contains_multipleChoices_moreThanEnough() {
+ val production = Production()
+ production.addChoice(LOOM, GLASS, PAPYRUS)
+ production.addChoice(STONE, ORE)
+ production.addChoice(STONE, WOOD)
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun contains_mixedFixedAndChoice_enough() {
+ val production = Production()
+ production.addFixedResource(WOOD, 1)
+ production.addChoice(STONE, WOOD)
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun addAll_empty() {
+ val production = Production()
+ production.addAll(emptyResources)
+ assertTrue(production.contains(emptyResources))
+ }
+
+ @Test
+ fun addAll_singleResource() {
+ val production = Production()
+ production.addAll(resources1Stone)
+ assertTrue(production.contains(resources1Stone))
+ }
+
+ @Test
+ fun addAll_multipleResources() {
+ val production = Production()
+ production.addAll(resources2Stones3Clay)
+ assertTrue(production.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun addAll_production_multipleFixedResources() {
+ val production = Production()
+ production.addAll(resources2Stones3Clay)
+
+ val production2 = Production()
+ production2.addAll(production)
+
+ assertTrue(production2.contains(resources2Stones3Clay))
+ }
+
+ @Test
+ fun addAll_production_multipleChoices() {
+ val production = Production()
+ production.addChoice(STONE, WOOD)
+ production.addChoice(STONE, ORE)
+
+ val production2 = Production()
+ production2.addAll(production)
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun addAll_production_mixedFixedResourcesAndChoices() {
+ val production = Production()
+ production.addFixedResource(WOOD, 1)
+ production.addChoice(STONE, WOOD)
+
+ val production2 = Production()
+ production2.addAll(production)
+
+ assertTrue(production.contains(resources1Stone1Wood))
+ }
+
+ @Test
+ fun asChoices_empty() {
+ val production = Production()
+ assertTrue(production.asChoices().isEmpty())
+ }
+
+ @Test
+ fun asChoices_onlyChoices() {
+ val production = Production()
+ production.addChoice(STONE, WOOD)
+ production.addChoice(STONE, ORE)
+ production.addChoice(CLAY, LOOM, GLASS)
+ assertEquals(production.getAlternativeResources(), production.asChoices())
+ }
+
+ @Test
+ fun asChoices_onlyFixed() {
+ val production = Production()
+ production.addFixedResource(WOOD, 1)
+ production.addFixedResource(CLAY, 2)
+
+ val expected = HashSet<Set<ResourceType>>()
+ expected.add(EnumSet.of(WOOD))
+ expected.add(EnumSet.of(CLAY))
+ expected.add(EnumSet.of(CLAY))
+
+ assertEquals(expected, production.asChoices())
+ }
+
+ @Test
+ fun asChoices_mixed() {
+ val production = Production()
+ production.addChoice(STONE, ORE)
+ production.addChoice(CLAY, LOOM, GLASS)
+ production.addFixedResource(WOOD, 1)
+ production.addFixedResource(CLAY, 2)
+
+ val expected = HashSet<Set<ResourceType>>()
+ expected.add(EnumSet.of(STONE, ORE))
+ expected.add(EnumSet.of(CLAY, LOOM, GLASS))
+ expected.add(EnumSet.of(WOOD))
+ expected.add(EnumSet.of(CLAY))
+ expected.add(EnumSet.of(CLAY))
+
+ assertEquals(expected, production.asChoices())
+ }
+
+ @Test
+ fun equals_trueWhenSame() {
+ val production = Production()
+ assertEquals(production, production)
+ }
+
+ @Test
+ fun equals_trueWhenSameContent() {
+ val production1 = Production()
+ val production2 = Production()
+ assertTrue(production1 == production2)
+ production1.addFixedResource(GLASS, 1)
+ production2.addFixedResource(GLASS, 1)
+ assertTrue(production1 == production2)
+ production1.addChoice(ORE, WOOD)
+ production2.addChoice(ORE, WOOD)
+ assertTrue(production1 == production2)
+ }
+
+ @Test
+ fun hashCode_sameWhenSameContent() {
+ val production1 = Production()
+ val production2 = Production()
+ assertEquals(production1.hashCode(), production2.hashCode())
+ production1.addFixedResource(GLASS, 1)
+ production2.addFixedResource(GLASS, 1)
+ assertEquals(production1.hashCode(), production2.hashCode())
+ production1.addChoice(ORE, WOOD)
+ production2.addChoice(ORE, WOOD)
+ assertEquals(production1.hashCode(), production2.hashCode())
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactionsTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactionsTest.kt
new file mode 100644
index 00000000..7e6d7816
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourceTransactionsTest.kt
@@ -0,0 +1,27 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.ResourceType.CLAY
+import org.luxons.sevenwonders.game.resources.ResourceType.WOOD
+import org.luxons.sevenwonders.game.test.createTransaction
+import kotlin.test.assertEquals
+
+class ResourceTransactionsTest {
+
+ @Test
+ fun toTransactions() {
+ val transactionMap = mapOf(
+ Provider.LEFT_PLAYER to (1 of WOOD) + (1 of CLAY),
+ Provider.RIGHT_PLAYER to (1 of WOOD)
+ )
+
+ val expectedNormalized = setOf(
+ createTransaction(Provider.LEFT_PLAYER, WOOD, CLAY),
+ createTransaction(Provider.RIGHT_PLAYER, WOOD)
+ )
+
+ assertEquals(expectedNormalized, transactionMap.toTransactions().toSet())
+ }
+
+ private infix fun Int.of(type: ResourceType): Resources = resourcesOf(type to this)
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourcesTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourcesTest.kt
new file mode 100644
index 00000000..634a25c7
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/ResourcesTest.kt
@@ -0,0 +1,435 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.resources.ResourceType.CLAY
+import org.luxons.sevenwonders.game.resources.ResourceType.GLASS
+import org.luxons.sevenwonders.game.resources.ResourceType.LOOM
+import org.luxons.sevenwonders.game.resources.ResourceType.ORE
+import org.luxons.sevenwonders.game.resources.ResourceType.PAPYRUS
+import org.luxons.sevenwonders.game.resources.ResourceType.STONE
+import org.luxons.sevenwonders.game.resources.ResourceType.WOOD
+import java.util.NoSuchElementException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ResourcesTest {
+
+ @Test
+ fun init_shouldBeEmpty() {
+ val resources = emptyResources()
+ for (resourceType in ResourceType.values()) {
+ assertEquals(0, resources[resourceType])
+ }
+ assertEquals(0, resources.size)
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun add_zero() {
+ val resources = mutableResourcesOf()
+ resources.add(CLAY, 0)
+ assertEquals(0, resources[CLAY])
+ assertEquals(0, resources.size)
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun add_simple() {
+ val resources = mutableResourcesOf()
+ resources.add(WOOD, 3)
+ assertEquals(3, resources[WOOD])
+ assertEquals(3, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun add_multipleCallsStacked() {
+ val resources = mutableResourcesOf()
+ resources.add(ORE, 3)
+ resources.add(ORE, 2)
+ assertEquals(5, resources[ORE])
+ assertEquals(5, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun add_interlaced() {
+ val resources = mutableResourcesOf()
+ resources.add(GLASS, 3)
+ resources.add(STONE, 1)
+ resources.add(WOOD, 4)
+ resources.add(GLASS, 2)
+ assertEquals(5, resources[GLASS])
+ assertEquals(10, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun plus_zero() {
+ val resources = resourcesOf(CLAY to 2)
+ val resourcesPlusZero = resources + emptyResources()
+ val zeroPlusResources = emptyResources() + resources
+
+ assertEquals(2, resourcesPlusZero[CLAY])
+ assertEquals(2, resourcesPlusZero.size)
+ assertEquals(2, zeroPlusResources[CLAY])
+ assertEquals(2, zeroPlusResources.size)
+ }
+
+ @Test
+ fun plus_sameResource() {
+ val resources1 = resourcesOf(WOOD to 1)
+ val resources2 = resourcesOf(WOOD to 3)
+ val sum = resources1 + resources2
+
+ assertEquals(1, resources1.size)
+ assertEquals(3, resources2.size)
+ assertEquals(4, sum[WOOD])
+ assertEquals(4, sum.size)
+ }
+
+ @Test
+ fun plus_differentemptyResources() {
+ val resources1 = resourcesOf(WOOD to 1)
+ val resources2 = resourcesOf(ORE to 3)
+ val sum = resources1 + resources2
+
+ assertEquals(1, resources1.size)
+ assertEquals(3, resources2.size)
+ assertEquals(1, sum[WOOD])
+ assertEquals(3, sum[ORE])
+ assertEquals(4, sum.size)
+ }
+
+ @Test
+ fun plus_overlappingemptyResources() {
+ val resources1 = resourcesOf(WOOD to 1)
+ val resources2 = resourcesOf(WOOD to 2, ORE to 4)
+ val sum = resources1 + resources2
+
+ assertEquals(1, resources1.size)
+ assertEquals(6, resources2.size)
+ assertEquals(3, sum[WOOD])
+ assertEquals(4, sum[ORE])
+ assertEquals(7, sum.size)
+ }
+
+ @Test
+ fun remove_some() {
+ val resources = mutableResourcesOf(WOOD to 3)
+ resources.remove(WOOD, 2)
+ assertEquals(1, resources[WOOD])
+ assertEquals(1, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun remove_all() {
+ val resources = mutableResourcesOf(WOOD to 3)
+ resources.remove(WOOD, 3)
+ assertEquals(0, resources[WOOD])
+ assertEquals(0, resources.size)
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun remove_tooMany() {
+ val resources = mutableResourcesOf(WOOD to 2)
+
+ assertFailsWith<NoSuchElementException> {
+ resources.remove(WOOD, 3)
+ }
+ }
+
+ @Test
+ fun addAll_empty() {
+ val resources = mutableResourcesOf(STONE to 1, CLAY to 3)
+
+ val emptyResources = emptyResources()
+
+ resources.add(emptyResources)
+ assertEquals(1, resources[STONE])
+ assertEquals(3, resources[CLAY])
+ assertEquals(0, resources[ORE])
+ assertEquals(0, resources[GLASS])
+ assertEquals(0, resources[LOOM])
+ assertEquals(4, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun addAll_zeros() {
+ val resources = mutableResourcesOf(STONE to 1, CLAY to 3)
+
+ val emptyResources = resourcesOf(STONE to 0, CLAY to 0)
+
+ resources.add(emptyResources)
+ assertEquals(1, resources[STONE])
+ assertEquals(3, resources[CLAY])
+ assertEquals(0, resources[ORE])
+ assertEquals(0, resources[GLASS])
+ assertEquals(0, resources[LOOM])
+ assertEquals(4, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun addAll_same() {
+ val resources = mutableResourcesOf(STONE to 1, CLAY to 3)
+ val resources2 = resourcesOf(STONE to 2, CLAY to 6)
+
+ resources.add(resources2)
+ assertEquals(3, resources[STONE])
+ assertEquals(9, resources[CLAY])
+ assertEquals(0, resources[ORE])
+ assertEquals(0, resources[GLASS])
+ assertEquals(0, resources[LOOM])
+ assertEquals(12, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun addAll_overlap() {
+ val resources = mutableResourcesOf(STONE to 1, CLAY to 3)
+ val resources2 = resourcesOf(CLAY to 6, ORE to 4)
+
+ resources.add(resources2)
+ assertEquals(1, resources[STONE])
+ assertEquals(9, resources[CLAY])
+ assertEquals(4, resources[ORE])
+ assertEquals(0, resources[GLASS])
+ assertEquals(0, resources[LOOM])
+ assertEquals(14, resources.size)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun contains_emptyContainsEmpty() {
+ val emptyResources = emptyResources()
+ val emptyResources2 = emptyResources()
+ assertTrue(emptyResources.containsAll(emptyResources2))
+ }
+
+ @Test
+ fun contains_singleTypeContainsEmpty() {
+ val resources = resourcesOf(STONE to 1)
+ val emptyResources = emptyResources()
+
+ assertTrue(resources.containsAll(emptyResources))
+ }
+
+ @Test
+ fun contains_multipleTypesContainsEmpty() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+ val emptyResources = emptyResources()
+
+ assertTrue(resources.containsAll(emptyResources))
+ }
+
+ @Test
+ fun contains_self() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+
+ assertTrue(resources.containsAll(resources))
+ }
+
+ @Test
+ fun contains_allOfEachType() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+ val resources2 = resourcesOf(STONE to 1, CLAY to 3)
+
+ assertTrue(resources.containsAll(resources2))
+ }
+
+ @Test
+ fun contains_someOfEachType() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(STONE to 1, CLAY to 3)
+
+ assertTrue(resources.containsAll(resources2))
+ }
+
+ @Test
+ fun contains_someOfSomeTypes() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(CLAY to 3)
+
+ assertTrue(resources.containsAll(resources2))
+ }
+
+ @Test
+ fun contains_allOfSomeTypes() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(CLAY to 4)
+
+ assertTrue(resources.containsAll(resources2))
+ }
+
+ @Test
+ fun minus_empty() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+ val emptyResources = emptyResources()
+
+ val diff = resources.minus(emptyResources)
+ assertEquals(1, diff[STONE])
+ assertEquals(3, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_self() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+
+ val diff = resources.minus(resources)
+ assertEquals(0, diff[STONE])
+ assertEquals(0, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_allOfEachType() {
+ val resources = resourcesOf(STONE to 1, CLAY to 3)
+ val resources2 = resourcesOf(STONE to 1, CLAY to 3)
+
+ val diff = resources.minus(resources2)
+ assertEquals(0, diff[STONE])
+ assertEquals(0, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_someOfEachType() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(STONE to 1, CLAY to 3)
+
+ val diff = resources.minus(resources2)
+ assertEquals(1, diff[STONE])
+ assertEquals(1, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_someOfSomeTypes() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(CLAY to 3)
+
+ val diff = resources.minus(resources2)
+ assertEquals(2, diff[STONE])
+ assertEquals(1, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_allOfSomeTypes() {
+ val resources = resourcesOf(STONE to 2, CLAY to 4)
+ val resources2 = resourcesOf(CLAY to 4)
+
+ val diff = resources.minus(resources2)
+ assertEquals(2, diff[STONE])
+ assertEquals(0, diff[CLAY])
+ assertEquals(0, diff[ORE])
+ assertEquals(0, diff[GLASS])
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_tooMuchOfExistingType() {
+ val resources = resourcesOf(CLAY to 4)
+ val resources2 = resourcesOf(CLAY to 5)
+
+ val diff = resources.minus(resources2)
+ assertEquals(0, diff[CLAY])
+ }
+
+ @Test
+ fun minus_someOfAnAbsentType() {
+ val resources = emptyResources()
+ val resources2 = resourcesOf(LOOM to 5)
+
+ val diff = resources.minus(resources2)
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun minus_someOfATypeWithZero() {
+ val resources = resourcesOf(LOOM to 0)
+ val resources2 = resourcesOf(LOOM to 5)
+
+ val diff = resources.minus(resources2)
+ assertEquals(0, diff[LOOM])
+ }
+
+ @Test
+ fun isEmpty_noElement() {
+ val resources = emptyResources()
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun isEmpty_singleZeroElement() {
+ val resources = resourcesOf(LOOM to 0)
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun isEmpty_multipleZeroElements() {
+ val resources = resourcesOf(WOOD to 0, ORE to 0, LOOM to 0)
+ assertTrue(resources.isEmpty())
+ }
+
+ @Test
+ fun isEmpty_singleElementMoreThanZero() {
+ val resources = resourcesOf(LOOM to 3)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun isEmpty_mixedZeroAndNonZeroElements() {
+ val resources = resourcesOf(WOOD to 0, LOOM to 3)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun isEmpty_mixedZeroAndNonZeroElements_reverseOrder() {
+ val resources = resourcesOf(ORE to 3, PAPYRUS to 0)
+ assertFalse(resources.isEmpty())
+ }
+
+ @Test
+ fun equals_trueWhenSame() {
+ val resources = emptyResources()
+ assertEquals(resources, resources)
+ }
+
+ @Test
+ fun equals_trueWhenSameContent() {
+ val resources1 = mutableResourcesOf()
+ val resources2 = mutableResourcesOf()
+ assertTrue(resources1 == resources2)
+ resources1.add(GLASS, 1)
+ resources2.add(GLASS, 1)
+ assertTrue(resources1 == resources2)
+ }
+
+ @Test
+ fun hashCode_sameWhenSameContent() {
+ val resources1 = mutableResourcesOf()
+ val resources2 = mutableResourcesOf()
+ assertEquals(resources1.hashCode(), resources2.hashCode())
+ resources1.add(GLASS, 1)
+ resources2.add(GLASS, 1)
+ assertEquals(resources1.hashCode(), resources2.hashCode())
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/TradingRulesTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/TradingRulesTest.kt
new file mode 100644
index 00000000..38953529
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/resources/TradingRulesTest.kt
@@ -0,0 +1,126 @@
+package org.luxons.sevenwonders.game.resources
+
+import org.junit.Assume.assumeTrue
+import org.junit.experimental.theories.DataPoints
+import org.junit.experimental.theories.Theories
+import org.junit.experimental.theories.Theory
+import org.junit.runner.RunWith
+import org.luxons.sevenwonders.game.test.createTransaction
+import org.luxons.sevenwonders.game.test.createTransactions
+import kotlin.test.assertEquals
+
+@RunWith(Theories::class)
+class TradingRulesTest {
+
+ @Theory
+ fun setCost_overridesCost(
+ defaultCost: Int,
+ overriddenCost: Int,
+ overriddenProvider: Provider,
+ provider: Provider,
+ type: ResourceType
+ ) {
+ assumeTrue(defaultCost != overriddenCost)
+ assumeTrue(overriddenProvider != provider)
+
+ val rules = TradingRules(defaultCost)
+ rules.setCost(type, overriddenProvider, overriddenCost)
+
+ assertEquals(overriddenCost, rules.getCost(type, overriddenProvider))
+ assertEquals(defaultCost, rules.getCost(type, provider))
+ }
+
+ @Theory
+ fun computeCost_zeroForNoResources(defaultCost: Int) {
+ val rules = TradingRules(defaultCost)
+ assertEquals(0, rules.computeCost(noTransactions()))
+ }
+
+ @Theory
+ fun computeCost_defaultCostWhenNoOverride(defaultCost: Int, provider: Provider, type: ResourceType) {
+ val rules = TradingRules(defaultCost)
+ val transactions = createTransactions(provider, type)
+ assertEquals(defaultCost, rules.computeCost(transactions))
+ }
+
+ @Theory
+ fun computeCost_twiceDefaultFor2Resources(defaultCost: Int, provider: Provider, type: ResourceType) {
+ val rules = TradingRules(defaultCost)
+ val transactions = createTransactions(provider, type, type)
+ assertEquals(2 * defaultCost, rules.computeCost(transactions))
+ }
+
+ @Theory
+ fun computeCost_overriddenCost(defaultCost: Int, overriddenCost: Int, provider: Provider, type: ResourceType) {
+ val rules = TradingRules(defaultCost)
+ rules.setCost(type, provider, overriddenCost)
+ val transactions = createTransactions(provider, type)
+ assertEquals(overriddenCost, rules.computeCost(transactions))
+ }
+
+ @Theory
+ fun computeCost_defaultCostWhenOverrideOnOtherProviderOrType(
+ defaultCost: Int,
+ overriddenCost: Int,
+ overriddenProvider: Provider,
+ overriddenType: ResourceType,
+ provider: Provider,
+ type: ResourceType
+ ) {
+ assumeTrue(overriddenProvider != provider || overriddenType != type)
+ val rules = TradingRules(defaultCost)
+ rules.setCost(overriddenType, overriddenProvider, overriddenCost)
+ val transactions = createTransactions(provider, type)
+ assertEquals(defaultCost, rules.computeCost(transactions))
+ }
+
+ @Theory
+ fun computeCost_oneDefaultAndOneOverriddenType(
+ defaultCost: Int,
+ overriddenCost: Int,
+ overriddenType: ResourceType,
+ provider: Provider,
+ type: ResourceType
+ ) {
+ assumeTrue(overriddenType != type)
+ val rules = TradingRules(defaultCost)
+ rules.setCost(overriddenType, provider, overriddenCost)
+ val transactions = createTransactions(provider, overriddenType, type)
+ assertEquals(defaultCost + overriddenCost, rules.computeCost(transactions))
+ }
+
+ @Theory
+ fun computeCost_oneDefaultAndOneOverriddenProvider(
+ defaultCost: Int,
+ overriddenCost: Int,
+ overriddenProvider: Provider,
+ provider: Provider,
+ type: ResourceType
+ ) {
+ assumeTrue(overriddenProvider != provider)
+ val rules = TradingRules(defaultCost)
+ rules.setCost(type, overriddenProvider, overriddenCost)
+
+ val boughtResources = createTransactions(
+ createTransaction(provider, type),
+ createTransaction(overriddenProvider, type)
+ )
+
+ assertEquals(defaultCost + overriddenCost, rules.computeCost(boughtResources))
+ }
+
+ companion object {
+
+ @JvmStatic
+ @DataPoints
+ fun costs(): IntArray = intArrayOf(0, 1, 2)
+
+ @JvmStatic
+ @DataPoints
+ fun providers(): Array<Provider> = Provider.values()
+
+ @JvmStatic
+ @DataPoints
+ fun resourceTypes(): Array<ResourceType> = ResourceType.values()
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/test/TestUtils.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/test/TestUtils.kt
new file mode 100644
index 00000000..78386b3d
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/test/TestUtils.kt
@@ -0,0 +1,137 @@
+package org.luxons.sevenwonders.game.test
+
+import org.luxons.sevenwonders.game.Player
+import org.luxons.sevenwonders.game.PlayerContext
+import org.luxons.sevenwonders.game.Settings
+import org.luxons.sevenwonders.game.api.CustomizableSettings
+import org.luxons.sevenwonders.game.api.PlayerMove
+import org.luxons.sevenwonders.game.boards.Board
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition
+import org.luxons.sevenwonders.game.boards.Science
+import org.luxons.sevenwonders.game.boards.ScienceType
+import org.luxons.sevenwonders.game.boards.Table
+import org.luxons.sevenwonders.game.cards.Card
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.cards.Color
+import org.luxons.sevenwonders.game.cards.Requirements
+import org.luxons.sevenwonders.game.effects.Effect
+import org.luxons.sevenwonders.game.effects.ScienceProgress
+import org.luxons.sevenwonders.game.moves.Move
+import org.luxons.sevenwonders.game.moves.MoveType
+import org.luxons.sevenwonders.game.resources.Production
+import org.luxons.sevenwonders.game.resources.Provider
+import org.luxons.sevenwonders.game.resources.ResourceTransaction
+import org.luxons.sevenwonders.game.resources.ResourceTransactions
+import org.luxons.sevenwonders.game.resources.ResourceType
+import org.luxons.sevenwonders.game.resources.noTransactions
+import org.luxons.sevenwonders.game.resources.resourcesOf
+import org.luxons.sevenwonders.game.wonders.Wonder
+import org.luxons.sevenwonders.game.wonders.WonderStage
+
+private const val SEED: Long = 42
+
+internal fun testCustomizableSettings(initialGold: Int = 0): CustomizableSettings =
+ CustomizableSettings(randomSeedForTests = SEED).copy(initialGold = initialGold)
+
+internal fun testSettings(nbPlayers: Int = 5, initialGold: Int = 0): Settings =
+ Settings(nbPlayers, testCustomizableSettings(initialGold))
+
+internal fun testTable(nbPlayers: Int = 5): Table = testTable(testSettings(nbPlayers))
+
+internal fun testTable(settings: Settings): Table =
+ Table(testBoards(settings.nbPlayers, settings))
+
+private fun testBoards(count: Int, settings: Settings): List<Board> = List(count) { testBoard(settings) }
+
+internal fun testBoard(
+ initialResource: ResourceType = ResourceType.WOOD,
+ initialGold: Int = 0,
+ vararg production: ResourceType
+): Board {
+ val settings = testSettings(initialGold = initialGold)
+ val board = testBoard(settings, initialResource)
+ board.production.addAll(fixedProduction(*production))
+ return board
+}
+
+private fun testBoard(settings: Settings, initialResource: ResourceType = ResourceType.WOOD): Board =
+ Board(testWonder(initialResource), 0, settings)
+
+internal fun testWonder(initialResource: ResourceType = ResourceType.WOOD): Wonder {
+ val stage1 = WonderStage(Requirements(), emptyList())
+ val stage2 = WonderStage(Requirements(), emptyList())
+ val stage3 = WonderStage(Requirements(), emptyList())
+ return Wonder("Test Wonder ${initialResource.symbol}", initialResource, listOf(stage1, stage2, stage3), "")
+}
+
+internal fun fixedProduction(vararg producedTypes: ResourceType): Production =
+ Production().apply { addAll(resourcesOf(*producedTypes)) }
+
+internal fun createTransactions(provider: Provider, vararg resources: ResourceType): ResourceTransactions =
+ createTransactions(createTransaction(provider, *resources))
+
+internal fun createTransactions(vararg transactions: ResourceTransaction): ResourceTransactions = transactions.toSet()
+
+internal fun createTransaction(provider: Provider, vararg resources: ResourceType): ResourceTransaction =
+ ResourceTransaction(provider, resourcesOf(*resources))
+
+internal fun createRequirements(vararg types: ResourceType): Requirements = Requirements(resources = resourcesOf(*types))
+
+internal fun sampleCards(nbCards: Int, fromIndex: Int = 0, color: Color = Color.BLUE): List<Card> =
+ List(nbCards) { i -> testCard("Test Card ${fromIndex + i}", color) }
+
+internal fun createGuildCards(count: Int): List<Card> = List(count) { createGuildCard(it) }
+
+internal fun createGuildCard(num: Int, effect: Effect? = null): Card =
+ testCard("Test Guild $num", Color.PURPLE, effect = effect)
+
+internal fun testCard(
+ name: String = "Test Card",
+ color: Color = Color.BLUE,
+ requirements: Requirements = Requirements(),
+ effect: Effect? = null
+): Card {
+ val effects = if (effect == null) emptyList() else listOf(effect)
+ return Card(name, color, requirements, effects, null, emptyList(), "path/to/card/image", CardBack("image-III"))
+}
+
+internal fun addCards(board: Board, nbCardsOfColor: Int, nbOtherCards: Int, color: Color) {
+ addCards(board, nbCardsOfColor, color)
+ addCards(board, nbOtherCards, getDifferentColorFrom(color))
+}
+
+internal fun addCards(board: Board, nbCards: Int, color: Color) {
+ sampleCards(nbCards, color = color).forEach { board.addCard(it) }
+}
+
+internal fun getDifferentColorFrom(vararg colors: Color): Color =
+ Color.values().firstOrNull { it !in colors } ?: throw IllegalArgumentException("All colors are forbidden!")
+
+internal fun createScienceProgress(compasses: Int, wheels: Int, tablets: Int, jokers: Int): ScienceProgress =
+ ScienceProgress(createScience(compasses, wheels, tablets, jokers))
+
+internal fun createScience(compasses: Int, wheels: Int, tablets: Int, jokers: Int): Science = Science().apply {
+ add(ScienceType.COMPASS, compasses)
+ add(ScienceType.WHEEL, wheels)
+ add(ScienceType.TABLET, tablets)
+ addJoker(jokers)
+}
+
+internal fun playCardWithEffect(player: Player, color: Color, effect: Effect) {
+ val card = testCard(color = color, effect = effect)
+ player.board.addCard(card)
+ card.applyTo(player, noTransactions())
+}
+
+internal fun createMove(context: PlayerContext, card: Card, type: MoveType): Move =
+ type.resolve(PlayerMove(type, card.name), card, context)
+
+internal fun singleBoardPlayer(board: Board): Player = object : Player {
+ override val index = 0
+ override val board = board
+ override fun getBoard(relativePosition: RelativeBoardPosition): Board = when (relativePosition) {
+ RelativeBoardPosition.LEFT -> throw RuntimeException("No LEFT board")
+ RelativeBoardPosition.SELF -> this.board
+ RelativeBoardPosition.RIGHT -> throw RuntimeException("No RIGHT board")
+ }
+}
diff --git a/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/wonders/WonderTest.kt b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/wonders/WonderTest.kt
new file mode 100644
index 00000000..491d13fb
--- /dev/null
+++ b/sw-engine/src/test/kotlin/org/luxons/sevenwonders/game/wonders/WonderTest.kt
@@ -0,0 +1,34 @@
+package org.luxons.sevenwonders.game.wonders
+
+import org.junit.Test
+import org.luxons.sevenwonders.game.cards.CardBack
+import org.luxons.sevenwonders.game.test.testWonder
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class WonderTest {
+
+ @Test
+ fun buildLevel_increasesNbBuiltStages() {
+ val wonder = testWonder()
+ assertEquals(0, wonder.nbBuiltStages)
+ wonder.placeCard(CardBack("img"))
+ assertEquals(1, wonder.nbBuiltStages)
+ wonder.placeCard(CardBack("img"))
+ assertEquals(2, wonder.nbBuiltStages)
+ wonder.placeCard(CardBack("img"))
+ assertEquals(3, wonder.nbBuiltStages)
+ }
+
+ @Test
+ fun buildLevel_failsIfFull() {
+ val wonder = testWonder()
+ wonder.placeCard(CardBack("img"))
+ wonder.placeCard(CardBack("img"))
+ wonder.placeCard(CardBack("img"))
+
+ assertFailsWith(IllegalStateException::class) {
+ wonder.placeCard(CardBack("img"))
+ }
+ }
+}
bgstack15