diff options
author | Joffrey BION <joffrey.bion@gmail.com> | 2019-05-16 23:48:38 +0200 |
---|---|---|
committer | Joffrey BION <joffrey.bion@gmail.com> | 2019-05-16 23:48:38 +0200 |
commit | 2382a452456e4bdef4584e1046925e372624cb79 (patch) | |
tree | 0e49b2e5d81facb55fb8b08228abeb218a27d466 /sw-engine/src/main/kotlin | |
parent | Remove GRADLE_METADATA feature to avoid breaking frontend build (diff) | |
download | seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.tar.gz seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.tar.bz2 seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.zip |
Rationalize module names
Diffstat (limited to 'sw-engine/src/main/kotlin')
54 files changed, 2177 insertions, 0 deletions
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) + } +} |