summaryrefslogtreecommitdiff
path: root/game-engine/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'game-engine/src/main')
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/Game.java232
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/Settings.java93
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/Action.java20
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java128
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java49
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java45
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java77
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/api/Table.java106
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Board.java175
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java28
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Military.java56
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java28
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Science.java65
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java7
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Card.java128
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java14
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Color.java11
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java65
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java21
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java64
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java131
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java66
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java85
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java38
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java76
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java24
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java66
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java27
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java6
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java31
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java36
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java21
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java48
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java55
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.java78
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java31
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java37
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java64
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java86
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java44
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java15
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java11
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java20
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java54
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java22
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java47
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java26
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java39
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java23
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java56
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java27
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/InvalidMoveException.java8
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/Move.java50
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java39
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java39
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BestPriceCalculator.java102
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java24
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Production.java106
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java18
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java40
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java91
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java43
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java38
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java24
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java11
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java102
-rw-r--r--game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java56
-rw-r--r--game-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json1719
-rw-r--r--game-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json515
74 files changed, 6027 insertions, 0 deletions
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/Game.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/Game.java
new file mode 100644
index 00000000..ed11913b
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/Game.java
@@ -0,0 +1,232 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.api.Action;
+import org.luxons.sevenwonders.game.api.HandCard;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.PlayerTurnInfo;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+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.effects.SpecialAbility;
+import org.luxons.sevenwonders.game.moves.InvalidMoveException;
+import org.luxons.sevenwonders.game.moves.Move;
+import org.luxons.sevenwonders.game.scoring.ScoreBoard;
+
+public class Game {
+
+ private static final int LAST_AGE = 3;
+
+ private final long id;
+
+ private final Settings settings;
+
+ private final int nbPlayers;
+
+ private final Table table;
+
+ private final Decks decks;
+
+ private final List<Card> discardedCards;
+
+ private final Map<Integer, Move> preparedMoves;
+
+ private Map<Integer, PlayerTurnInfo> currentTurnInfo;
+
+ private Hands hands;
+
+ public Game(long id, Settings settings, int nbPlayers, List<Board> boards, Decks decks) {
+ this.id = id;
+ this.settings = settings;
+ this.nbPlayers = nbPlayers;
+ this.table = new Table(boards);
+ this.decks = decks;
+ this.discardedCards = new ArrayList<>();
+ this.currentTurnInfo = new HashMap<>();
+ this.preparedMoves = new HashMap<>();
+ startNewAge();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public Settings getSettings() {
+ return settings;
+ }
+
+ private void startNewAge() {
+ table.increaseCurrentAge();
+ hands = decks.deal(table.getCurrentAge(), table.getNbPlayers());
+ startNewTurn();
+ }
+
+ private void startNewTurn() {
+ currentTurnInfo.clear();
+ for (int i = 0; i < nbPlayers; i++) {
+ currentTurnInfo.put(i, createPlayerTurnInfo(i));
+ }
+ }
+
+ private PlayerTurnInfo createPlayerTurnInfo(int playerIndex) {
+ PlayerTurnInfo pti = new PlayerTurnInfo(playerIndex, table);
+ List<HandCard> hand = hands.createHand(table, playerIndex);
+ pti.setHand(hand);
+ Action action = determineAction(hand, table.getBoard(playerIndex));
+ pti.setAction(action);
+ pti.setMessage(action.getMessage());
+ if (action == Action.PICK_NEIGHBOR_GUILD) {
+ pti.setNeighbourGuildCards(table.getNeighbourGuildCards(playerIndex));
+ }
+ return pti;
+ }
+
+ public Collection<PlayerTurnInfo> getCurrentTurnInfo() {
+ return currentTurnInfo.values();
+ }
+
+ private Action determineAction(List<HandCard> hand, Board board) {
+ if (endOfGameReached() && board.hasSpecial(SpecialAbility.COPY_GUILD)) {
+ return Action.PICK_NEIGHBOR_GUILD;
+ } else if (hand.size() == 1 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ return Action.PLAY_LAST;
+ } else if (hand.size() == 2 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ return Action.PLAY_2;
+ } else if (hand.isEmpty()) {
+ return Action.WAIT;
+ } else {
+ return Action.PLAY;
+ }
+ }
+
+ public CardBack prepareMove(int playerIndex, PlayerMove playerMove) throws InvalidMoveException {
+ Card card = decks.getCard(playerMove.getCardName());
+ Move move = playerMove.getType().resolve(playerIndex, card, playerMove);
+ validate(move);
+ preparedMoves.put(playerIndex, move);
+ return card.getBack();
+ }
+
+ private void validate(Move move) throws InvalidMoveException {
+ List<Card> hand = hands.get(move.getPlayerIndex());
+ move.validate(table, hand);
+ }
+
+ public boolean allPlayersPreparedTheirMove() {
+ long nbExpectedMoves = currentTurnInfo.values().stream().filter(pti -> pti.getAction() != Action.WAIT).count();
+ return preparedMoves.size() == nbExpectedMoves;
+ }
+
+ public Table playTurn() {
+ makeMoves();
+ if (endOfAgeReached()) {
+ executeEndOfAgeEvents();
+ if (!endOfGameReached()) {
+ startNewAge();
+ }
+ } else {
+ rotateHandsIfRelevant();
+ startNewTurn();
+ }
+ return table;
+ }
+
+ private void rotateHandsIfRelevant() {
+ // we don't rotate hands if some player can play his last card (with the special ability)
+ if (!hands.maxOneCardRemains()) {
+ hands.rotate(table.getHandRotationDirection());
+ }
+ }
+
+ private void makeMoves() {
+ List<Move> playedMoves = mapToList(preparedMoves);
+
+ // 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(playedMoves);
+
+ // same goes for the discarded cards during the last turn, which should be available for special actions
+ if (hands.maxOneCardRemains()) {
+ discardLastCardsOfHands();
+ }
+
+ activatePlayedCards(playedMoves);
+
+ table.setLastPlayedMoves(playedMoves);
+ preparedMoves.clear();
+ }
+
+ private static List<Move> mapToList(Map<Integer, Move> movesPerPlayer) {
+ List<Move> moves = new ArrayList<>(movesPerPlayer.size());
+ for (int p = 0; p < movesPerPlayer.size(); p++) {
+ Move move = movesPerPlayer.get(p);
+ if (move == null) {
+ throw new MissingPreparedMoveException(p);
+ }
+ moves.add(move);
+ }
+ return moves;
+ }
+
+ private void placePreparedCards(List<Move> playedMoves) {
+ playedMoves.forEach(move -> {
+ move.place(table, discardedCards, settings);
+ removeFromHand(move.getPlayerIndex(), move.getCard());
+ });
+ }
+
+ private void discardLastCardsOfHands() {
+ for (int i = 0; i < nbPlayers; i++) {
+ Board board = table.getBoard(i);
+ if (!board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ discardHand(i);
+ }
+ }
+ }
+
+ private void discardHand(int playerIndex) {
+ List<Card> hand = hands.get(playerIndex);
+ discardedCards.addAll(hand);
+ hand.clear();
+ }
+
+ private void removeFromHand(int playerIndex, Card card) {
+ hands.get(playerIndex).remove(card);
+ }
+
+ private void activatePlayedCards(List<Move> playedMoves) {
+ playedMoves.forEach(move -> move.activate(table, discardedCards, settings));
+ }
+
+ private boolean endOfAgeReached() {
+ return hands.isEmpty();
+ }
+
+ private void executeEndOfAgeEvents() {
+ table.resolveMilitaryConflicts();
+ }
+
+ private boolean endOfGameReached() {
+ return endOfAgeReached() && table.getCurrentAge() == LAST_AGE;
+ }
+
+ public ScoreBoard computeScore() {
+ ScoreBoard scoreBoard = new ScoreBoard();
+ table.getBoards().stream().map(b -> b.computePoints(table)).forEach(scoreBoard::add);
+ return scoreBoard;
+ }
+
+ private static class MissingPreparedMoveException extends IllegalStateException {
+ MissingPreparedMoveException(int playerIndex) {
+ super("Player " + playerIndex + " is not ready to play");
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/Settings.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/Settings.java
new file mode 100644
index 00000000..f05b0b01
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/Settings.java
@@ -0,0 +1,93 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.Map;
+import java.util.Random;
+
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.data.definitions.WonderSide;
+import org.luxons.sevenwonders.game.data.definitions.WonderSidePickMethod;
+
+public class Settings {
+
+ private final Random random;
+
+ private final int timeLimitInSeconds;
+
+ private final int nbPlayers;
+
+ private final int initialGold;
+
+ private final int discardedCardGold;
+
+ private final int defaultTradingCost;
+
+ private final int pointsPer3Gold;
+
+ private final WonderSidePickMethod wonderSidePickMethod;
+
+ private WonderSide lastPickedSide = null;
+
+ private final int lostPointsPerDefeat;
+
+ private final Map<Integer, Integer> wonPointsPerVictoryPerAge;
+
+ public Settings(int nbPlayers) {
+ this(nbPlayers, new CustomizableSettings());
+ }
+
+ public Settings(int nbPlayers, CustomizableSettings customSettings) {
+ long seed = customSettings.getRandomSeedForTests();
+ this.random = seed > 0 ? new Random(seed) : new Random();
+ this.timeLimitInSeconds = customSettings.getTimeLimitInSeconds();
+ this.nbPlayers = nbPlayers;
+ this.initialGold = customSettings.getInitialGold();
+ this.discardedCardGold = customSettings.getDiscardedCardGold();
+ this.defaultTradingCost = customSettings.getDefaultTradingCost();
+ this.pointsPer3Gold = customSettings.getPointsPer3Gold();
+ this.wonderSidePickMethod = customSettings.getWonderSidePickMethod();
+ this.lostPointsPerDefeat = customSettings.getLostPointsPerDefeat();
+ this.wonPointsPerVictoryPerAge = customSettings.getWonPointsPerVictoryPerAge();
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public int getTimeLimitInSeconds() {
+ return timeLimitInSeconds;
+ }
+
+ public int getNbPlayers() {
+ return nbPlayers;
+ }
+
+ public int getInitialGold() {
+ return initialGold;
+ }
+
+ public int getDiscardedCardGold() {
+ return discardedCardGold;
+ }
+
+ public int getDefaultTradingCost() {
+ return defaultTradingCost;
+ }
+
+ public int getPointsPer3Gold() {
+ return pointsPer3Gold;
+ }
+
+ public WonderSide pickWonderSide() {
+ WonderSide newSide = wonderSidePickMethod.pickSide(getRandom(), lastPickedSide);
+ lastPickedSide = newSide;
+ return newSide;
+ }
+
+ public int getLostPointsPerDefeat() {
+ return lostPointsPerDefeat;
+ }
+
+ public Map<Integer, Integer> getWonPointsPerVictoryPerAge() {
+ return wonPointsPerVictoryPerAge;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Action.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Action.java
new file mode 100644
index 00000000..88e392f9
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Action.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.api;
+
+public enum Action {
+ PLAY("Pick the card you want to play or discard."),
+ PLAY_2("Pick the first card you want to play or discard. Note that you have the ability to play these 2 last cards."
+ + " You will choose how to play the last one during your next turn."),
+ PLAY_LAST("You have the special ability to play your last card. Choose how you want to play it."),
+ PICK_NEIGHBOR_GUILD("Choose a Guild card (purple) that you want to copy from one of your neighbours."),
+ WAIT("Please wait for other players to perform extra actions.");
+
+ private final String message;
+
+ Action(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java
new file mode 100644
index 00000000..2cbf8727
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java
@@ -0,0 +1,128 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.data.definitions.WonderSidePickMethod;
+
+public class CustomizableSettings {
+
+ private long randomSeedForTests = -1;
+
+ private int timeLimitInSeconds = 45;
+
+ private WonderSidePickMethod wonderSidePickMethod = WonderSidePickMethod.EACH_RANDOM;
+
+ private int initialGold = 3;
+
+ private int discardedCardGold = 3;
+
+ private int defaultTradingCost = 2;
+
+ private int pointsPer3Gold = 1;
+
+ private int lostPointsPerDefeat = 1;
+
+ private Map<Integer, Integer> wonPointsPerVictoryPerAge = new HashMap<>();
+
+ public CustomizableSettings() {
+ wonPointsPerVictoryPerAge.put(1, 1);
+ wonPointsPerVictoryPerAge.put(2, 3);
+ wonPointsPerVictoryPerAge.put(3, 5);
+ }
+
+ public long getRandomSeedForTests() {
+ return randomSeedForTests;
+ }
+
+ public void setRandomSeedForTests(long randomSeedForTests) {
+ this.randomSeedForTests = randomSeedForTests;
+ }
+
+ public int getTimeLimitInSeconds() {
+ return timeLimitInSeconds;
+ }
+
+ public void setTimeLimitInSeconds(int timeLimitInSeconds) {
+ this.timeLimitInSeconds = timeLimitInSeconds;
+ }
+
+ public int getInitialGold() {
+ return initialGold;
+ }
+
+ public void setInitialGold(int initialGold) {
+ this.initialGold = initialGold;
+ }
+
+ public int getDiscardedCardGold() {
+ return discardedCardGold;
+ }
+
+ public void setDiscardedCardGold(int discardedCardGold) {
+ this.discardedCardGold = discardedCardGold;
+ }
+
+ public int getDefaultTradingCost() {
+ return defaultTradingCost;
+ }
+
+ public void setDefaultTradingCost(int defaultTradingCost) {
+ this.defaultTradingCost = defaultTradingCost;
+ }
+
+ public int getPointsPer3Gold() {
+ return pointsPer3Gold;
+ }
+
+ public void setPointsPer3Gold(int pointsPer3Gold) {
+ this.pointsPer3Gold = pointsPer3Gold;
+ }
+
+ public WonderSidePickMethod getWonderSidePickMethod() {
+ return wonderSidePickMethod;
+ }
+
+ public void setWonderSidePickMethod(WonderSidePickMethod wonderSidePickMethod) {
+ this.wonderSidePickMethod = wonderSidePickMethod;
+ }
+
+ public int getLostPointsPerDefeat() {
+ return lostPointsPerDefeat;
+ }
+
+ public void setLostPointsPerDefeat(int lostPointsPerDefeat) {
+ this.lostPointsPerDefeat = lostPointsPerDefeat;
+ }
+
+ public Map<Integer, Integer> getWonPointsPerVictoryPerAge() {
+ return wonPointsPerVictoryPerAge;
+ }
+
+ public void setWonPointsPerVictoryPerAge(Map<Integer, Integer> wonPointsPerVictoryPerAge) {
+ this.wonPointsPerVictoryPerAge = wonPointsPerVictoryPerAge;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CustomizableSettings that = (CustomizableSettings) o;
+ return randomSeedForTests == that.randomSeedForTests && timeLimitInSeconds == that.timeLimitInSeconds
+ && initialGold == that.initialGold && discardedCardGold == that.discardedCardGold
+ && defaultTradingCost == that.defaultTradingCost && pointsPer3Gold == that.pointsPer3Gold
+ && lostPointsPerDefeat == that.lostPointsPerDefeat && wonderSidePickMethod == that.wonderSidePickMethod
+ && Objects.equals(wonPointsPerVictoryPerAge, that.wonPointsPerVictoryPerAge);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(randomSeedForTests, timeLimitInSeconds, wonderSidePickMethod, initialGold,
+ discardedCardGold, defaultTradingCost, pointsPer3Gold, lostPointsPerDefeat, wonPointsPerVictoryPerAge);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java
new file mode 100644
index 00000000..a97679c2
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java
@@ -0,0 +1,49 @@
+package org.luxons.sevenwonders.game.api;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+/**
+ * A card with contextual information relative to the hand it is sitting in. The extra information is especially useful
+ * because it frees the client from a painful business logic implementation.
+ */
+public class HandCard {
+
+ private final Card card;
+
+ private final boolean chainable;
+
+ private final boolean free;
+
+ private final boolean playable;
+
+ public HandCard(Card card, Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ this.card = card;
+ this.chainable = card.isChainableOn(board);
+ this.free = card.isFreeFor(board);
+ this.playable = card.isPlayable(table, playerIndex);
+ }
+
+ public Card getCard() {
+ return card;
+ }
+
+ public boolean isChainable() {
+ return chainable;
+ }
+
+ public boolean isFree() {
+ return free;
+ }
+
+ public boolean isPlayable() {
+ return playable;
+ }
+
+ @Override
+ public String toString() {
+ return "HandCard{" + "card=" + card + ", chainable=" + chainable + ", free=" + free + ", playable=" + playable
+ + '}';
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java
new file mode 100644
index 00000000..bad8f852
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java
@@ -0,0 +1,45 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.moves.MoveType;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public class PlayerMove {
+
+ private MoveType type;
+
+ private String cardName;
+
+ private List<BoughtResources> boughtResources = new ArrayList<>();
+
+ public MoveType getType() {
+ return type;
+ }
+
+ public void setType(MoveType type) {
+ this.type = type;
+ }
+
+ public String getCardName() {
+ return cardName;
+ }
+
+ public void setCardName(String cardName) {
+ this.cardName = cardName;
+ }
+
+ public List<BoughtResources> getBoughtResources() {
+ return boughtResources;
+ }
+
+ public void setBoughtResources(List<BoughtResources> boughtResources) {
+ this.boughtResources = boughtResources;
+ }
+
+ @Override
+ public String toString() {
+ return type + " '" + cardName + '\'';
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java
new file mode 100644
index 00000000..3d92d40b
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java
@@ -0,0 +1,77 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class PlayerTurnInfo {
+
+ private final int playerIndex;
+
+ private final Table table;
+
+ private final int currentAge;
+
+ private Action action;
+
+ private List<HandCard> hand;
+
+ private List<Card> neighbourGuildCards;
+
+ private String message;
+
+ public PlayerTurnInfo(int playerIndex, Table table) {
+ this.playerIndex = playerIndex;
+ this.table = table;
+ this.currentAge = table.getCurrentAge();
+ }
+
+ public int getPlayerIndex() {
+ return playerIndex;
+ }
+
+ public Table getTable() {
+ return table;
+ }
+
+ public int getCurrentAge() {
+ return currentAge;
+ }
+
+ public List<HandCard> getHand() {
+ return hand;
+ }
+
+ public void setHand(List<HandCard> hand) {
+ this.hand = hand;
+ }
+
+ public List<Card> getNeighbourGuildCards() {
+ return neighbourGuildCards;
+ }
+
+ public void setNeighbourGuildCards(List<Card> neighbourGuildCards) {
+ this.neighbourGuildCards = neighbourGuildCards;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return "PlayerTurnInfo{" + "playerIndex=" + playerIndex + ", action=" + action + ", hand=" + hand + '}';
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Table.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Table.java
new file mode 100644
index 00000000..82f9055a
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/api/Table.java
@@ -0,0 +1,106 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.boards.Board;
+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.cards.HandRotationDirection;
+import org.luxons.sevenwonders.game.moves.Move;
+import org.luxons.sevenwonders.game.resources.Provider;
+
+/**
+ * The table contains what is visible by all the players in the game: the boards and their played cards, and the
+ * players' information.
+ */
+public class Table {
+
+ private final int nbPlayers;
+
+ private final List<Board> boards;
+
+ private int currentAge = 0;
+
+ private List<Move> lastPlayedMoves;
+
+ public Table(List<Board> boards) {
+ this.nbPlayers = boards.size();
+ this.boards = boards;
+ }
+
+ public int getNbPlayers() {
+ return nbPlayers;
+ }
+
+ public List<Board> getBoards() {
+ return boards;
+ }
+
+ public Board getBoard(int playerIndex) {
+ return boards.get(playerIndex);
+ }
+
+ public Board getBoard(int playerIndex, RelativeBoardPosition position) {
+ return boards.get(position.getIndexFrom(playerIndex, nbPlayers));
+ }
+
+ public List<Move> getLastPlayedMoves() {
+ return lastPlayedMoves;
+ }
+
+ public void setLastPlayedMoves(List<Move> lastPlayedMoves) {
+ this.lastPlayedMoves = lastPlayedMoves;
+ }
+
+ public int getCurrentAge() {
+ return currentAge;
+ }
+
+ public void increaseCurrentAge() {
+ this.currentAge++;
+ }
+
+ public HandRotationDirection getHandRotationDirection() {
+ return HandRotationDirection.forAge(currentAge);
+ }
+
+ public void resolveMilitaryConflicts() {
+ for (int i = 0; i < nbPlayers; i++) {
+ Board board1 = getBoard(i);
+ Board board2 = getBoard((i + 1) % nbPlayers);
+ resolveConflict(board1, board2, currentAge);
+ }
+ }
+
+ private static void resolveConflict(Board board1, Board board2, int age) {
+ int shields1 = board1.getMilitary().getNbShields();
+ int shields2 = board2.getMilitary().getNbShields();
+ if (shields1 < shields2) {
+ board2.getMilitary().victory(age);
+ board1.getMilitary().defeat();
+ } else if (shields1 > shields2) {
+ board1.getMilitary().victory(age);
+ board2.getMilitary().defeat();
+ }
+ }
+
+ public List<Card> getNeighbourGuildCards(int playerIndex) {
+ return getNeighbourBoards(playerIndex).stream()
+ .map(Board::getPlayedCards)
+ .flatMap(List::stream)
+ .filter(c -> c.getColor() == Color.PURPLE)
+ .collect(Collectors.toList());
+ }
+
+ private List<Board> getNeighbourBoards(int playerIndex) {
+ Provider[] providers = Provider.values();
+ List<Board> boards = new ArrayList<>(providers.length);
+ for (Provider provider : providers) {
+ boards.add(getBoard(playerIndex, provider.getBoardPosition()));
+ }
+ return boards;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Board.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Board.java
new file mode 100644
index 00000000..a59bbfae
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Board.java
@@ -0,0 +1,175 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.Table;
+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.scoring.PlayerScore;
+import org.luxons.sevenwonders.game.scoring.ScoreCategory;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+public class Board {
+
+ private final Wonder wonder;
+
+ private final int playerIndex;
+
+ private final List<Card> playedCards = new ArrayList<>();
+
+ private final Production production = new Production();
+
+ private final Production publicProduction = new Production();
+
+ private final Science science = new Science();
+
+ private final TradingRules tradingRules;
+
+ private final Military military;
+
+ private final Set<SpecialAbility> specialAbilities = EnumSet.noneOf(SpecialAbility.class);
+
+ private Map<Integer, Boolean> consumedFreeCards = new HashMap<>();
+
+ private Card copiedGuild;
+
+ private int gold;
+
+ private int pointsPer3Gold;
+
+ public Board(Wonder wonder, int playerIndex, Settings settings) {
+ this.wonder = wonder;
+ this.playerIndex = playerIndex;
+ this.gold = settings.getInitialGold();
+ this.tradingRules = new TradingRules(settings.getDefaultTradingCost());
+ this.military = new Military(settings.getLostPointsPerDefeat(), settings.getWonPointsPerVictoryPerAge());
+ this.pointsPer3Gold = settings.getPointsPer3Gold();
+ this.production.addFixedResource(wonder.getInitialResource(), 1);
+ this.publicProduction.addFixedResource(wonder.getInitialResource(), 1);
+ }
+
+ public Wonder getWonder() {
+ return wonder;
+ }
+
+ public List<Card> getPlayedCards() {
+ return playedCards;
+ }
+
+ public void addCard(Card card) {
+ playedCards.add(card);
+ }
+
+ int getNbCardsOfColor(List<Color> colorFilter) {
+ return (int) playedCards.stream().filter(c -> colorFilter.contains(c.getColor())).count();
+ }
+
+ public boolean isPlayed(String cardName) {
+ return getPlayedCards().stream().map(Card::getName).filter(name -> name.equals(cardName)).count() > 0;
+ }
+
+ public Production getProduction() {
+ return production;
+ }
+
+ public Production getPublicProduction() {
+ return publicProduction;
+ }
+
+ public TradingRules getTradingRules() {
+ return tradingRules;
+ }
+
+ public Science getScience() {
+ return science;
+ }
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int amount) {
+ this.gold = amount;
+ }
+
+ public void addGold(int amount) {
+ this.gold += amount;
+ }
+
+ public void removeGold(int amount) {
+ if (gold < amount) {
+ throw new InsufficientFundsException(gold, amount);
+ }
+ this.gold -= amount;
+ }
+
+ public Military getMilitary() {
+ return military;
+ }
+
+ public void addSpecial(SpecialAbility specialAbility) {
+ specialAbilities.add(specialAbility);
+ }
+
+ public boolean hasSpecial(SpecialAbility specialAbility) {
+ return specialAbilities.contains(specialAbility);
+ }
+
+ public boolean canPlayFreeCard(int age) {
+ return hasSpecial(SpecialAbility.ONE_FREE_PER_AGE) && !consumedFreeCards.getOrDefault(age, false);
+ }
+
+ public void consumeFreeCard(int age) {
+ consumedFreeCards.put(age, true);
+ }
+
+ public void setCopiedGuild(Card copiedGuild) {
+ if (copiedGuild.getColor() != Color.PURPLE) {
+ throw new IllegalArgumentException("The given card '" + copiedGuild + "' is not a Guild card");
+ }
+ this.copiedGuild = copiedGuild;
+ }
+
+ public Card getCopiedGuild() {
+ return copiedGuild;
+ }
+
+ public PlayerScore computePoints(Table table) {
+ PlayerScore score = new PlayerScore(gold);
+ score.put(ScoreCategory.CIVIL, computePointsForCards(table, Color.BLUE));
+ score.put(ScoreCategory.MILITARY, military.getTotalPoints());
+ score.put(ScoreCategory.SCIENCE, science.computePoints());
+ score.put(ScoreCategory.TRADE, computePointsForCards(table, Color.YELLOW));
+ score.put(ScoreCategory.GUILD, computePointsForCards(table, Color.PURPLE));
+ score.put(ScoreCategory.WONDER, wonder.computePoints(table, playerIndex));
+ score.put(ScoreCategory.GOLD, computeGoldPoints());
+ return score;
+ }
+
+ private int computePointsForCards(Table table, Color color) {
+ return playedCards.stream()
+ .filter(c -> c.getColor() == color)
+ .flatMap(c -> c.getEffects().stream())
+ .mapToInt(e -> e.computePoints(table, playerIndex))
+ .sum();
+ }
+
+ private int computeGoldPoints() {
+ return gold / 3 * pointsPer3Gold;
+ }
+
+ static class InsufficientFundsException extends RuntimeException {
+ InsufficientFundsException(int current, int required) {
+ super(String.format("Current balance is %d gold, but %d are required", current, required));
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java
new file mode 100644
index 00000000..e50f4ea0
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java
@@ -0,0 +1,28 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.cards.Color;
+
+public enum BoardElementType {
+ CARD {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getNbCardsOfColor(colors);
+ }
+ },
+ BUILT_WONDER_STAGES {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getWonder().getNbBuiltStages();
+ }
+ },
+ DEFEAT_TOKEN {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getMilitary().getNbDefeatTokens();
+ }
+ };
+
+ public abstract int getElementCount(Board board, List<Color> colors);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Military.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Military.java
new file mode 100644
index 00000000..e5cc7033
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Military.java
@@ -0,0 +1,56 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.Map;
+
+public class Military {
+
+ private final int lostPointsPerDefeat;
+
+ private final Map<Integer, Integer> wonPointsPerVictoryPerAge;
+
+ private int nbShields = 0;
+
+ private int totalPoints = 0;
+
+ private int nbDefeatTokens = 0;
+
+ Military(int lostPointsPerDefeat, Map<Integer, Integer> wonPointsPerVictoryPerAge) {
+ this.lostPointsPerDefeat = lostPointsPerDefeat;
+ this.wonPointsPerVictoryPerAge = wonPointsPerVictoryPerAge;
+ }
+
+ public int getNbShields() {
+ return nbShields;
+ }
+
+ public void addShields(int nbShields) {
+ this.nbShields += nbShields;
+ }
+
+ public int getTotalPoints() {
+ return totalPoints;
+ }
+
+ public int getNbDefeatTokens() {
+ return nbDefeatTokens;
+ }
+
+ public void victory(int age) {
+ Integer wonPoints = wonPointsPerVictoryPerAge.get(age);
+ if (wonPoints == null) {
+ throw new UnknownAgeException(age);
+ }
+ totalPoints += wonPoints;
+ }
+
+ public void defeat() {
+ totalPoints -= lostPointsPerDefeat;
+ nbDefeatTokens++;
+ }
+
+ static final class UnknownAgeException extends IllegalArgumentException {
+ UnknownAgeException(int unknownAge) {
+ super(String.valueOf(unknownAge));
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java
new file mode 100644
index 00000000..16b2f3a9
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java
@@ -0,0 +1,28 @@
+package org.luxons.sevenwonders.game.boards;
+
+public enum RelativeBoardPosition {
+ LEFT {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return wrapIndex(playerIndex - 1, nbPlayers);
+ }
+ },
+ SELF {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return playerIndex;
+ }
+ },
+ RIGHT {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return wrapIndex(playerIndex + 1, nbPlayers);
+ }
+ };
+
+ public abstract int getIndexFrom(int playerIndex, int nbPlayers);
+
+ int wrapIndex(int index, int nbPlayers) {
+ return Math.floorMod(index, nbPlayers);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Science.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Science.java
new file mode 100644
index 00000000..34928bcc
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/Science.java
@@ -0,0 +1,65 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class Science {
+
+ private Map<ScienceType, Integer> quantities = new EnumMap<>(ScienceType.class);
+
+ private int jokers;
+
+ public void add(ScienceType type, int quantity) {
+ quantities.merge(type, quantity, (x, y) -> x + y);
+ }
+
+ public void addJoker(int quantity) {
+ jokers += quantity;
+ }
+
+ public int getJokers() {
+ return jokers;
+ }
+
+ public void addAll(Science science) {
+ science.quantities.forEach(this::add);
+ jokers += science.jokers;
+ }
+
+ public int getQuantity(ScienceType type) {
+ return quantities.getOrDefault(type, 0);
+ }
+
+ public int size() {
+ return quantities.values().stream().mapToInt(q -> q).sum() + jokers;
+ }
+
+ public int computePoints() {
+ ScienceType[] types = ScienceType.values();
+ Integer[] values = new Integer[types.length];
+ for (int i = 0; i < types.length; i++) {
+ values[i] = quantities.getOrDefault(types[i], 0);
+ }
+ return computePoints(values, jokers);
+ }
+
+ private static int computePoints(Integer[] values, int jokers) {
+ if (jokers == 0) {
+ return computePointsNoJoker(values);
+ }
+ int maxPoints = 0;
+ for (int i = 0; i < values.length; i++) {
+ values[i]++;
+ maxPoints = Math.max(maxPoints, computePoints(values, jokers - 1));
+ values[i]--;
+ }
+ return maxPoints;
+ }
+
+ private static int computePointsNoJoker(Integer[] values) {
+ int independentSquaresSum = Arrays.stream(values).mapToInt(i -> i * i).sum();
+ int nbGroupsOfAll = Arrays.stream(values).mapToInt(i -> i).min().orElse(0);
+ return independentSquaresSum + nbGroupsOfAll * 7;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java
new file mode 100644
index 00000000..f1b14c6d
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java
@@ -0,0 +1,7 @@
+package org.luxons.sevenwonders.game.boards;
+
+public enum ScienceType {
+ COMPASS,
+ WHEEL,
+ TABLET
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Card.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Card.java
new file mode 100644
index 00000000..084d19a5
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Card.java
@@ -0,0 +1,128 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public class Card {
+
+ private final String name;
+
+ private final Color color;
+
+ private final Requirements requirements;
+
+ private final List<Effect> effects;
+
+ private final String chainParent;
+
+ private final List<String> chainChildren;
+
+ private final String image;
+
+ private CardBack back;
+
+ public Card(String name, Color color, Requirements requirements, List<Effect> effects, String chainParent,
+ List<String> chainChildren, String image) {
+ this.name = name;
+ this.color = color;
+ this.requirements = requirements;
+ this.chainParent = chainParent;
+ this.effects = effects;
+ this.chainChildren = chainChildren;
+ this.image = image;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public String getChainParent() {
+ return chainParent;
+ }
+
+ public Requirements getRequirements() {
+ return requirements;
+ }
+
+ public List<Effect> getEffects() {
+ return effects;
+ }
+
+ public List<String> getChainChildren() {
+ return chainChildren;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public CardBack getBack() {
+ return back;
+ }
+
+ public void setBack(CardBack back) {
+ this.back = back;
+ }
+
+ private boolean isAllowedOnBoard(Board board) {
+ return !board.isPlayed(name); // cannot play twice the same card
+ }
+
+ public boolean isChainableOn(Board board) {
+ return isAllowedOnBoard(board) && board.isPlayed(chainParent);
+ }
+
+ public boolean isFreeFor(Board board) {
+ if (!isAllowedOnBoard(board)) {
+ return false;
+ }
+ return isChainableOn(board) || (requirements.areMetWithoutNeighboursBy(board) && requirements.getGold() == 0);
+ }
+
+ public boolean isPlayable(Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ if (!isAllowedOnBoard(board)) {
+ return false;
+ }
+ return isChainableOn(board) || requirements.couldBeMetBy(table, playerIndex);
+ }
+
+ public void applyTo(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ Board playerBoard = table.getBoard(playerIndex);
+ if (!isChainableOn(playerBoard)) {
+ requirements.pay(table, playerIndex, boughtResources);
+ }
+ effects.forEach(e -> e.apply(table, playerIndex));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Card card = (Card) o;
+ return Objects.equals(name, card.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public String toString() {
+ return "Card{" + name + '}';
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java
new file mode 100644
index 00000000..f925b6c4
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.cards;
+
+public class CardBack {
+
+ private final String image;
+
+ public CardBack(String image) {
+ this.image = image;
+ }
+
+ public String getImage() {
+ return image;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Color.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Color.java
new file mode 100644
index 00000000..80d06c55
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Color.java
@@ -0,0 +1,11 @@
+package org.luxons.sevenwonders.game.cards;
+
+public enum Color {
+ BROWN,
+ GREY,
+ YELLOW,
+ BLUE,
+ GREEN,
+ RED,
+ PURPLE
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java
new file mode 100644
index 00000000..aa2b00bf
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java
@@ -0,0 +1,65 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Decks {
+
+ private Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+
+ public Decks(Map<Integer, List<Card>> cardsPerAge) {
+ this.cardsPerAge = cardsPerAge;
+ }
+
+ public Card getCard(String cardName) throws CardNotFoundException {
+ return cardsPerAge.values()
+ .stream()
+ .flatMap(List::stream)
+ .filter(c -> c.getName().equals(cardName))
+ .findAny()
+ .orElseThrow(() -> new CardNotFoundException(cardName));
+ }
+
+ public Hands deal(int age, int nbPlayers) {
+ List<Card> deck = getDeck(age);
+ validateNbCards(deck, nbPlayers);
+ return deal(deck, nbPlayers);
+ }
+
+ private List<Card> getDeck(int age) {
+ List<Card> deck = cardsPerAge.get(age);
+ if (deck == null) {
+ throw new IllegalArgumentException("No deck found for age " + age);
+ }
+ return deck;
+ }
+
+ private void validateNbCards(List<Card> deck, int nbPlayers) {
+ if (nbPlayers == 0) {
+ throw new IllegalArgumentException("Cannot deal cards between 0 players");
+ }
+ if (deck.size() % nbPlayers != 0) {
+ throw new IllegalArgumentException(
+ String.format("Cannot deal %d cards evenly between %d players", deck.size(), nbPlayers));
+ }
+ }
+
+ private Hands deal(List<Card> deck, int nbPlayers) {
+ Map<Integer, List<Card>> hands = new HashMap<>(nbPlayers);
+ for (int i = 0; i < nbPlayers; i++) {
+ hands.put(i, new ArrayList<>());
+ }
+ for (int i = 0; i < deck.size(); i++) {
+ hands.get(i % nbPlayers).add(deck.get(i));
+ }
+ return new Hands(hands, nbPlayers);
+ }
+
+ class CardNotFoundException extends RuntimeException {
+ CardNotFoundException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java
new file mode 100644
index 00000000..f3902fb5
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java
@@ -0,0 +1,21 @@
+package org.luxons.sevenwonders.game.cards;
+
+public enum HandRotationDirection {
+ LEFT(-1),
+ RIGHT(1);
+
+ private final int indexOffset;
+
+ HandRotationDirection(int i) {
+ this.indexOffset = i;
+ }
+
+ public int getIndexOffset() {
+ return indexOffset;
+ }
+
+ public static HandRotationDirection forAge(int age) {
+ // clockwise (pass to the left) at age 1, and alternating
+ return age % 2 == 0 ? HandRotationDirection.RIGHT : HandRotationDirection.LEFT;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java
new file mode 100644
index 00000000..4a8bc143
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java
@@ -0,0 +1,64 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.api.HandCard;
+import org.luxons.sevenwonders.game.api.Table;
+
+public class Hands {
+
+ private final int nbPlayers;
+
+ private Map<Integer, List<Card>> hands;
+
+ Hands(Map<Integer, List<Card>> hands, int nbPlayers) {
+ this.hands = hands;
+ this.nbPlayers = nbPlayers;
+ }
+
+ public List<Card> get(int playerIndex) {
+ if (!hands.containsKey(playerIndex)) {
+ throw new PlayerIndexOutOfBoundsException(playerIndex);
+ }
+ return hands.get(playerIndex);
+ }
+
+ public List<HandCard> createHand(Table table, int playerIndex) {
+ return hands.get(playerIndex)
+ .stream()
+ .map(c -> new HandCard(c, table, playerIndex))
+ .collect(Collectors.toList());
+ }
+
+ public Hands rotate(HandRotationDirection direction) {
+ Map<Integer, List<Card>> newHands = new HashMap<>(hands.size());
+ for (int i = 0; i < nbPlayers; i++) {
+ int newIndex = Math.floorMod(i + direction.getIndexOffset(), nbPlayers);
+ newHands.put(newIndex, hands.get(i));
+ }
+ return new Hands(newHands, nbPlayers);
+ }
+
+ public boolean isEmpty() {
+ return hands.values().stream().allMatch(List::isEmpty);
+ }
+
+ public boolean maxOneCardRemains() {
+ return hands.values().stream().mapToInt(List::size).max().orElse(0) <= 1;
+ }
+
+ public List<Card> gatherAndClear() {
+ List<Card> remainingCards = hands.values().stream().flatMap(List::stream).collect(Collectors.toList());
+ hands.clear();
+ return remainingCards;
+ }
+
+ class PlayerIndexOutOfBoundsException extends ArrayIndexOutOfBoundsException {
+ PlayerIndexOutOfBoundsException(int index) {
+ super(index);
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java
new file mode 100644
index 00000000..93683ff8
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java
@@ -0,0 +1,131 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.resources.BestPriceCalculator;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+public class Requirements {
+
+ private int gold;
+
+ private Resources resources = new Resources();
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int gold) {
+ this.gold = gold;
+ }
+
+ public Resources getResources() {
+ return resources;
+ }
+
+ public void setResources(Resources resources) {
+ this.resources = resources;
+ }
+
+ /**
+ * Returns whether the given board meets these requirements on its own.
+ *
+ * @param board
+ * the board to check
+ *
+ * @return true if the given board meets these requirements without any transaction with its neighbours
+ */
+ boolean areMetWithoutNeighboursBy(Board board) {
+ return hasRequiredGold(board) && producesRequiredResources(board);
+ }
+
+ /**
+ * 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
+ */
+ public boolean areMetWithHelpBy(Board board, List<BoughtResources> boughtResources) {
+ if (!hasRequiredGold(board, boughtResources)) {
+ return false;
+ }
+ if (producesRequiredResources(board)) {
+ return true;
+ }
+ return producesRequiredResourcesWithHelp(board, boughtResources);
+ }
+
+ /**
+ * Returns whether the given player's board could meet these requirements, on its own or by buying resources to
+ * neighbours.
+ *
+ * @param table
+ * the current game table
+ * @param playerIndex
+ * the index of the player to check
+ *
+ * @return true if the given player's board could meet these requirements
+ */
+ boolean couldBeMetBy(Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ if (!hasRequiredGold(board)) {
+ return false;
+ }
+ if (producesRequiredResources(board)) {
+ return true;
+ }
+ return BestPriceCalculator.bestPrice(resources, table, playerIndex) <= board.getGold() - gold;
+ }
+
+ private boolean hasRequiredGold(Board board) {
+ return board.getGold() >= gold;
+ }
+
+ private boolean hasRequiredGold(Board board, List<BoughtResources> boughtResources) {
+ int resourcesPrice = board.getTradingRules().computeCost(boughtResources);
+ return board.getGold() >= gold + resourcesPrice;
+ }
+
+ private boolean producesRequiredResources(Board board) {
+ return board.getProduction().contains(resources);
+ }
+
+ private boolean producesRequiredResourcesWithHelp(Board board, List<BoughtResources> boughtResources) {
+ Resources totalBoughtResources = getTotalResources(boughtResources);
+ Resources remainingResources = this.resources.minus(totalBoughtResources);
+ return board.getProduction().contains(remainingResources);
+ }
+
+ private static Resources getTotalResources(List<BoughtResources> boughtResources) {
+ return boughtResources.stream().map(BoughtResources::getResources).reduce(new Resources(), (r1, r2) -> {
+ r1.addAll(r2);
+ return r1;
+ });
+ }
+
+ void pay(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ table.getBoard(playerIndex).removeGold(gold);
+ payBoughtResources(table, playerIndex, boughtResources);
+ }
+
+ private void payBoughtResources(Table table, int playerIndex, List<BoughtResources> boughtResourcesList) {
+ boughtResourcesList.forEach(res -> payBoughtResources(table, playerIndex, res));
+ }
+
+ private void payBoughtResources(Table table, int playerIndex, BoughtResources boughtResources) {
+ Board board = table.getBoard(playerIndex);
+ int price = board.getTradingRules().computeCost(boughtResources);
+ board.removeGold(price);
+ RelativeBoardPosition providerPosition = boughtResources.getProvider().getBoardPosition();
+ Board providerBoard = table.getBoard(playerIndex, providerPosition);
+ providerBoard.addGold(price);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java
new file mode 100644
index 00000000..7604ca6a
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java
@@ -0,0 +1,66 @@
+package org.luxons.sevenwonders.game.data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Decks;
+import org.luxons.sevenwonders.game.data.definitions.DecksDefinition;
+import org.luxons.sevenwonders.game.data.definitions.WonderDefinition;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+public class GameDefinition {
+
+ /**
+ * This value is heavily dependent on the JSON data. Any change must be carefully thought through.
+ */
+ private static final int MIN_PLAYERS = 3;
+
+ /**
+ * This value is heavily dependent on the JSON data. Any change must be carefully thought through.
+ */
+ private static final int MAX_PLAYERS = 7;
+
+ private WonderDefinition[] wonders;
+
+ private DecksDefinition decksDefinition;
+
+ GameDefinition(WonderDefinition[] wonders, DecksDefinition decksDefinition) {
+ this.wonders = wonders;
+ this.decksDefinition = decksDefinition;
+ }
+
+ public int getMinPlayers() {
+ return MIN_PLAYERS;
+ }
+
+ public int getMaxPlayers() {
+ return MAX_PLAYERS;
+ }
+
+ public Game initGame(long id, CustomizableSettings customSettings, int nbPlayers) {
+ Settings settings = new Settings(nbPlayers, customSettings);
+ List<Board> boards = assignBoards(settings, nbPlayers);
+ Decks decks = decksDefinition.create(settings);
+ return new Game(id, settings, nbPlayers, boards, decks);
+ }
+
+ private List<Board> assignBoards(Settings settings, int nbPlayers) {
+ List<WonderDefinition> randomizedWonders = Arrays.asList(wonders);
+ Collections.shuffle(randomizedWonders, settings.getRandom());
+
+ List<Board> boards = new ArrayList<>(nbPlayers);
+ for (int i = 0; i < nbPlayers; i++) {
+ WonderDefinition def = randomizedWonders.get(i);
+ Wonder w = def.create(settings);
+ Board b = new Board(w, i, settings);
+ boards.add(b);
+ }
+ return boards;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java
new file mode 100644
index 00000000..7a9c5af0
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java
@@ -0,0 +1,85 @@
+package org.luxons.sevenwonders.game.data;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.List;
+
+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;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+public class GameDefinitionLoader {
+
+ private static final String BASE_PACKAGE = GameDefinitionLoader.class.getPackage().getName();
+
+ private static final String BASE_PACKAGE_PATH = '/' + BASE_PACKAGE.replace('.', '/');
+
+ private static final String CARDS_FILE = "cards.json";
+
+ private static final String WONDERS_FILE = "wonders.json";
+
+ private final GameDefinition gameDefinition;
+
+ public GameDefinitionLoader() {
+ gameDefinition = load();
+ }
+
+ public GameDefinition getGameDefinition() {
+ return gameDefinition;
+ }
+
+ private static GameDefinition load() {
+ return new GameDefinition(loadWonders(), loadDecks());
+ }
+
+ private static WonderDefinition[] loadWonders() {
+ return readJsonFile(WONDERS_FILE, WonderDefinition[].class);
+ }
+
+ private static DecksDefinition loadDecks() {
+ return readJsonFile(CARDS_FILE, DecksDefinition.class);
+ }
+
+ private static <T> T readJsonFile(String filename, Class<T> clazz) {
+ InputStream in = GameDefinitionLoader.class.getResourceAsStream(BASE_PACKAGE_PATH + '/' + filename);
+ Reader reader = new BufferedReader(new InputStreamReader(in));
+ Gson gson = createGson();
+ return gson.fromJson(reader, clazz);
+ }
+
+ private static Gson createGson() {
+ Type resourceTypeList = new TypeToken<List<ResourceType>>() {}.getType();
+ return new GsonBuilder().disableHtmlEscaping()
+ .registerTypeAdapter(Resources.class, new ResourcesSerializer())
+ .registerTypeAdapter(ResourceType.class, new ResourceTypeSerializer())
+ .registerTypeAdapter(resourceTypeList, new ResourceTypesSerializer())
+ .registerTypeAdapter(Production.class, new ProductionSerializer())
+ .registerTypeAdapter(ProductionIncrease.class, new ProductionIncreaseSerializer())
+ .registerTypeAdapter(MilitaryReinforcements.class, new NumericEffectSerializer())
+ .registerTypeAdapter(RawPointsIncrease.class, new NumericEffectSerializer())
+ .registerTypeAdapter(GoldIncrease.class, new NumericEffectSerializer())
+ .registerTypeAdapter(ScienceProgress.class, new ScienceProgressSerializer())
+ .create();
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java
new file mode 100644
index 00000000..621bed2c
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.List;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.cards.Requirements;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class CardDefinition implements Definition<Card> {
+
+ private String name;
+
+ private Color color;
+
+ private Requirements requirements;
+
+ private EffectsDefinition effect;
+
+ private String chainParent;
+
+ private List<String> chainChildren;
+
+ private Map<Integer, Integer> countPerNbPlayer;
+
+ private String image;
+
+ @Override
+ public Card create(Settings settings) {
+ return new Card(name, color, requirements, effect.create(settings), chainParent, chainChildren, image);
+ }
+
+ Map<Integer, Integer> getCountPerNbPlayer() {
+ return countPerNbPlayer;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java
new file mode 100644
index 00000000..6f97e55f
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java
@@ -0,0 +1,76 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.cards.Decks;
+
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection") // the fields are injected by Gson
+public class DecksDefinition implements Definition<Decks> {
+
+ private List<CardDefinition> age1;
+
+ private List<CardDefinition> age2;
+
+ private List<CardDefinition> age3;
+
+ private String age1Back;
+
+ private String age2Back;
+
+ private String age3Back;
+
+ private List<CardDefinition> guildCards;
+
+ @Override
+ public Decks create(Settings settings) {
+ Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+ cardsPerAge.put(1, prepareStandardDeck(age1, settings, age1Back));
+ cardsPerAge.put(2, prepareStandardDeck(age2, settings, age2Back));
+ cardsPerAge.put(3, prepareAge3Deck(settings));
+ return new Decks(cardsPerAge);
+ }
+
+ private static List<Card> prepareStandardDeck(List<CardDefinition> defs, Settings settings, String backImage) {
+ CardBack back = new CardBack(backImage);
+ List<Card> cards = createDeck(defs, settings, back);
+ Collections.shuffle(cards, settings.getRandom());
+ return cards;
+ }
+
+ private List<Card> prepareAge3Deck(Settings settings) {
+ CardBack back = new CardBack(age3Back);
+ List<Card> age3deck = createDeck(age3, settings, back);
+ age3deck.addAll(createGuildCards(settings, back));
+ Collections.shuffle(age3deck, settings.getRandom());
+ return age3deck;
+ }
+
+ private static List<Card> createDeck(List<CardDefinition> defs, Settings settings, CardBack back) {
+ List<Card> cards = new ArrayList<>();
+ for (CardDefinition def : defs) {
+ for (int i = 0; i < def.getCountPerNbPlayer().get(settings.getNbPlayers()); i++) {
+ Card card = def.create(settings);
+ card.setBack(back);
+ cards.add(card);
+ }
+ }
+ return cards;
+ }
+
+ private List<Card> createGuildCards(Settings settings, CardBack back) {
+ List<Card> guild = guildCards.stream()
+ .map((def) -> def.create(settings))
+ .peek(c -> c.setBack(back))
+ .collect(Collectors.toList());
+ Collections.shuffle(guild, settings.getRandom());
+ return guild.subList(0, settings.getNbPlayers() + 2);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java
new file mode 100644
index 00000000..6c6b4b19
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import org.luxons.sevenwonders.game.Settings;
+
+/**
+ * Represents a deserialized JSON definition of some data about the game. It is settings-agnostic. An instance of
+ * in-game data can be generated from this, given specific game settings.
+ *
+ * @param <T>
+ * the type of in-game object that can be generated from this definition
+ */
+public interface Definition<T> {
+
+ /**
+ * Creates a T object from the given settings. This method mustn't mutate this Definition as it may be called
+ * multiple times with different settings.
+ *
+ * @param settings
+ * the game settings to use to generate a game-specific object from this definition
+ *
+ * @return the new game-specific object created from this definition
+ */
+ T create(Settings settings);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java
new file mode 100644
index 00000000..e35463d4
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java
@@ -0,0 +1,66 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+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;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class EffectsDefinition implements Definition<List<Effect>> {
+
+ private GoldIncrease gold;
+
+ private MilitaryReinforcements military;
+
+ private ScienceProgress science;
+
+ private Discount discount;
+
+ private BonusPerBoardElement perBoardElement;
+
+ private ProductionIncrease production;
+
+ private RawPointsIncrease points;
+
+ private SpecialAbility action;
+
+ @Override
+ public List<Effect> create(Settings settings) {
+ List<Effect> effects = new ArrayList<>();
+ if (gold != null) {
+ effects.add(gold);
+ }
+ if (military != null) {
+ effects.add(military);
+ }
+ if (science != null) {
+ effects.add(science);
+ }
+ if (discount != null) {
+ effects.add(discount);
+ }
+ if (perBoardElement != null) {
+ effects.add(perBoardElement);
+ }
+ if (production != null) {
+ effects.add(production);
+ }
+ if (points != null) {
+ effects.add(points);
+ }
+ if (action != null) {
+ effects.add(new SpecialAbilityActivation(action));
+ }
+ return effects;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java
new file mode 100644
index 00000000..a972a517
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java
@@ -0,0 +1,27 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection") // the fields are injected by Gson
+public class WonderDefinition implements Definition<Wonder> {
+
+ private String name;
+
+ private Map<WonderSide, WonderSideDefinition> sides;
+
+ @Override
+ public Wonder create(Settings settings) {
+ Wonder wonder = new Wonder();
+ wonder.setName(name);
+
+ WonderSideDefinition wonderSideDef = sides.get(settings.pickWonderSide());
+ wonder.setInitialResource(wonderSideDef.getInitialResource());
+ wonder.setStages(wonderSideDef.createStages(settings));
+ wonder.setImage(wonderSideDef.getImage());
+ return wonder;
+ }
+
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java
new file mode 100644
index 00000000..34091350
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java
@@ -0,0 +1,6 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+public enum WonderSide {
+ A,
+ B
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java
new file mode 100644
index 00000000..c84bba4e
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java
@@ -0,0 +1,31 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.wonders.WonderStage;
+
+// the fields are injected by Gson
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection")
+class WonderSideDefinition {
+
+ private ResourceType initialResource;
+
+ private List<WonderStageDefinition> stages;
+
+ private String image;
+
+ ResourceType getInitialResource() {
+ return initialResource;
+ }
+
+ List<WonderStage> createStages(Settings settings) {
+ return stages.stream().map(def -> def.create(settings)).collect(Collectors.toList());
+ }
+
+ String getImage() {
+ return image;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java
new file mode 100644
index 00000000..08aaad14
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java
@@ -0,0 +1,36 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.Random;
+
+public enum WonderSidePickMethod {
+ ALL_A {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return WonderSide.A;
+ }
+ },
+ ALL_B {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return WonderSide.B;
+ }
+ },
+ EACH_RANDOM {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return random.nextBoolean() ? WonderSide.A : WonderSide.B;
+ }
+ },
+ SAME_RANDOM_FOR_ALL {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ if (lastPickedSide == null) {
+ return random.nextBoolean() ? WonderSide.A : WonderSide.B;
+ } else {
+ return lastPickedSide;
+ }
+ }
+ };
+
+ public abstract WonderSide pickSide(Random random, WonderSide lastPickedSide);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java
new file mode 100644
index 00000000..887b414a
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java
@@ -0,0 +1,21 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Requirements;
+import org.luxons.sevenwonders.game.wonders.WonderStage;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class WonderStageDefinition implements Definition<WonderStage> {
+
+ private Requirements requirements;
+
+ private EffectsDefinition effects;
+
+ @Override
+ public WonderStage create(Settings settings) {
+ WonderStage stage = new WonderStage();
+ stage.setRequirements(requirements);
+ stage.setEffects(effects.create(settings));
+ return stage;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java
new file mode 100644
index 00000000..c0a75d80
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java
@@ -0,0 +1,48 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+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 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;
+
+public class NumericEffectSerializer implements JsonSerializer<Effect>, JsonDeserializer<Effect> {
+
+ @Override
+ public JsonElement serialize(Effect effect, Type typeOfSrc, JsonSerializationContext context) {
+ int value;
+ if (MilitaryReinforcements.class.equals(typeOfSrc)) {
+ value = ((MilitaryReinforcements) effect).getCount();
+ } else if (GoldIncrease.class.equals(typeOfSrc)) {
+ value = ((GoldIncrease) effect).getAmount();
+ } else if (RawPointsIncrease.class.equals(typeOfSrc)) {
+ value = ((RawPointsIncrease) effect).getPoints();
+ } else {
+ throw new IllegalArgumentException("Unknown numeric effect " + typeOfSrc.getTypeName());
+ }
+ return new JsonPrimitive(value);
+ }
+
+ @Override
+ public Effect deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ int value = json.getAsInt();
+ if (MilitaryReinforcements.class.equals(typeOfT)) {
+ return new MilitaryReinforcements(value);
+ } else if (GoldIncrease.class.equals(typeOfT)) {
+ return new GoldIncrease(value);
+ } else if (RawPointsIncrease.class.equals(typeOfT)) {
+ return new RawPointsIncrease(value);
+ }
+ throw new IllegalArgumentException("Unknown numeric effet " + typeOfT.getTypeName());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java
new file mode 100644
index 00000000..c3eb1386
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java
@@ -0,0 +1,55 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.resources.Production;
+
+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;
+
+public class ProductionIncreaseSerializer implements JsonSerializer<ProductionIncrease>,
+ JsonDeserializer<ProductionIncrease> {
+
+ @Override
+ public JsonElement serialize(ProductionIncrease productionIncrease, Type typeOfSrc,
+ JsonSerializationContext context) {
+ Production production = productionIncrease.getProduction();
+ JsonElement json = context.serialize(production);
+ if (!json.isJsonNull() && !productionIncrease.isSellable()) {
+ return new JsonPrimitive(wrapInBrackets(json.getAsString()));
+ }
+ return json;
+ }
+
+ private String wrapInBrackets(String str) {
+ return '(' + str + ')';
+ }
+
+ @Override
+ public ProductionIncrease deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ ProductionIncrease productionIncrease = new ProductionIncrease();
+
+ String resourcesStr = json.getAsString();
+ boolean isSellable = !resourcesStr.startsWith("(");
+ if (!isSellable) {
+ resourcesStr = unwrapBrackets(resourcesStr);
+ json = new JsonPrimitive(resourcesStr);
+ }
+ productionIncrease.setSellable(isSellable);
+
+ Production production = context.deserialize(json, Production.class);
+ productionIncrease.setProduction(production);
+ return productionIncrease;
+ }
+
+ private static String unwrapBrackets(String str) {
+ return str.substring(1, str.length() - 1);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.java
new file mode 100644
index 00000000..178134bb
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionSerializer.java
@@ -0,0 +1,78 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+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;
+
+public class ProductionSerializer implements JsonSerializer<Production>, JsonDeserializer<Production> {
+
+ @Override
+ public JsonElement serialize(Production production, Type typeOfSrc, JsonSerializationContext context) {
+ Resources fixedResources = production.getFixedResources();
+ Set<Set<ResourceType>> choices = production.getAlternativeResources();
+ if (fixedResources.isEmpty()) {
+ return serializeAsChoice(choices, context);
+ } else if (choices.isEmpty()) {
+ return serializeAsResources(fixedResources, context);
+ } else {
+ throw new IllegalArgumentException("Cannot serialize a production with mixed fixed resources and choices");
+ }
+ }
+
+ private static JsonElement serializeAsChoice(Set<Set<ResourceType>> choices, JsonSerializationContext context) {
+ if (choices.isEmpty()) {
+ return JsonNull.INSTANCE;
+ }
+ if (choices.size() > 1) {
+ throw new IllegalArgumentException("Cannot serialize a production with more than one choice");
+ }
+ String str = choices.stream()
+ .flatMap(Set::stream)
+ .map(ResourceType::getSymbol)
+ .map(Object::toString)
+ .collect(Collectors.joining("/"));
+ return context.serialize(str);
+ }
+
+ private static JsonElement serializeAsResources(Resources fixedResources, JsonSerializationContext context) {
+ return context.serialize(fixedResources);
+ }
+
+ @Override
+ public Production deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ String resourcesStr = json.getAsString();
+ Production production = new Production();
+ if (resourcesStr.contains("/")) {
+ production.addChoice(createChoice(resourcesStr));
+ } else {
+ Resources fixedResources = context.deserialize(json, Resources.class);
+ production.addAll(fixedResources);
+ }
+ return production;
+ }
+
+ private ResourceType[] createChoice(String choiceStr) {
+ String[] symbols = choiceStr.split("/");
+ ResourceType[] choice = new ResourceType[symbols.length];
+ for (int i = 0; i < symbols.length; i++) {
+ if (symbols[i].length() != 1) {
+ throw new IllegalArgumentException("Choice elements must be resource types, got " + symbols[i]);
+ }
+ choice[i] = ResourceType.fromSymbol(symbols[i].charAt(0));
+ }
+ return choice;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java
new file mode 100644
index 00000000..d2a49180
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java
@@ -0,0 +1,31 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+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;
+
+public class ResourceTypeSerializer implements JsonSerializer<ResourceType>, JsonDeserializer<ResourceType> {
+
+ @Override
+ public JsonElement serialize(ResourceType type, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(type.getSymbol());
+ }
+
+ @Override
+ public ResourceType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ String str = json.getAsString();
+ if (str.isEmpty()) {
+ throw new IllegalArgumentException("Empty string is not a valid resource type");
+ }
+ return ResourceType.fromSymbol(str.charAt(0));
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java
new file mode 100644
index 00000000..89d3e723
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java
@@ -0,0 +1,37 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+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;
+
+public class ResourceTypesSerializer implements JsonSerializer<List<ResourceType>>,
+ JsonDeserializer<List<ResourceType>> {
+
+ @Override
+ public JsonElement serialize(List<ResourceType> resources, Type typeOfSrc, JsonSerializationContext context) {
+ String s = resources.stream().map(ResourceType::getSymbol).map(Object::toString).collect(Collectors.joining());
+ return new JsonPrimitive(s);
+ }
+
+ @Override
+ public List<ResourceType> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ String s = json.getAsString();
+ List<ResourceType> resources = new ArrayList<>();
+ for (char c : s.toCharArray()) {
+ resources.add(ResourceType.fromSymbol(c));
+ }
+ return resources;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java
new file mode 100644
index 00000000..9c27b2a1
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+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;
+
+public class ResourcesSerializer implements JsonSerializer<Resources>, JsonDeserializer<Resources> {
+
+ @Override
+ public JsonElement serialize(Resources resources, Type typeOfSrc, JsonSerializationContext context) {
+ String s = resources.asList()
+ .stream()
+ .map(ResourceType::getSymbol)
+ .map(Object::toString)
+ .collect(Collectors.joining());
+ return s.isEmpty() ? JsonNull.INSTANCE : new JsonPrimitive(s);
+ }
+
+ @Override
+ public Resources deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ String s = json.getAsString();
+ Resources resources = new Resources();
+ for (char c : s.toCharArray()) {
+ resources.add(ResourceType.fromSymbol(c), 1);
+ }
+ return resources;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java
new file mode 100644
index 00000000..cecad405
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java
@@ -0,0 +1,64 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import org.luxons.sevenwonders.game.boards.Science;
+import org.luxons.sevenwonders.game.boards.ScienceType;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+
+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;
+
+public class ScienceProgressSerializer implements JsonSerializer<ScienceProgress>, JsonDeserializer<ScienceProgress> {
+
+ @Override
+ public JsonElement serialize(ScienceProgress scienceProgress, Type typeOfSrc, JsonSerializationContext context) {
+ Science science = scienceProgress.getScience();
+
+ if (science.size() > 1) {
+ throw new UnsupportedOperationException("Cannot serialize science containing more than one element");
+ }
+
+ for (ScienceType type : ScienceType.values()) {
+ int quantity = science.getQuantity(type);
+ if (quantity == 1) {
+ return context.serialize(type);
+ }
+ }
+
+ if (science.getJokers() == 1) {
+ return new JsonPrimitive("any");
+ }
+
+ return JsonNull.INSTANCE;
+ }
+
+ @Override
+ public ScienceProgress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ String s = json.getAsString();
+ ScienceProgress scienceProgress = new ScienceProgress();
+ Science science = new Science();
+ if ("any".equals(s)) {
+ science.addJoker(1);
+ } else {
+ science.add(deserializeScienceType(json, context), 1);
+ }
+ scienceProgress.setScience(science);
+ return scienceProgress;
+ }
+
+ private ScienceType deserializeScienceType(JsonElement json, JsonDeserializationContext context) {
+ ScienceType type = context.deserialize(json, ScienceType.class);
+ if (type == null) {
+ throw new IllegalArgumentException("Invalid science type " + json.getAsString());
+ }
+ return type;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java
new file mode 100644
index 00000000..e9f9fe5f
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java
@@ -0,0 +1,86 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.BoardElementType;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.Color;
+
+public class BonusPerBoardElement implements Effect {
+
+ private List<RelativeBoardPosition> boards;
+
+ private int gold;
+
+ private int points;
+
+ private BoardElementType type;
+
+ // only relevant if type=CARD
+ private List<Color> colors;
+
+ public List<RelativeBoardPosition> getBoards() {
+ return boards;
+ }
+
+ public void setBoards(List<RelativeBoardPosition> boards) {
+ this.boards = boards;
+ }
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int gold) {
+ this.gold = gold;
+ }
+
+ public int getPoints() {
+ return points;
+ }
+
+ public void setPoints(int points) {
+ this.points = points;
+ }
+
+ public BoardElementType getType() {
+ return type;
+ }
+
+ public void setType(BoardElementType type) {
+ this.type = type;
+ }
+
+ public List<Color> getColors() {
+ return colors;
+ }
+
+ public void setColors(List<Color> colors) {
+ this.colors = colors;
+ }
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ int goldGain = gold * computeNbOfMatchingElementsIn(table, playerIndex);
+ Board board = table.getBoard(playerIndex);
+ board.addGold(goldGain);
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return points * computeNbOfMatchingElementsIn(table, playerIndex);
+ }
+
+ private int computeNbOfMatchingElementsIn(Table table, int playerIndex) {
+ return boards.stream()
+ .map(pos -> table.getBoard(playerIndex, pos))
+ .mapToInt(this::computeNbOfMatchingElementsIn)
+ .sum();
+ }
+
+ private int computeNbOfMatchingElementsIn(Board board) {
+ return type.getElementCount(board, colors);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java
new file mode 100644
index 00000000..976ebf35
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java
@@ -0,0 +1,44 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.Provider;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.TradingRules;
+
+public class Discount extends InstantOwnBoardEffect {
+
+ private final List<ResourceType> resourceTypes = new ArrayList<>();
+
+ private final List<Provider> providers = new ArrayList<>();
+
+ private int discountedPrice = 1;
+
+ public List<ResourceType> getResourceTypes() {
+ return resourceTypes;
+ }
+
+ public List<Provider> getProviders() {
+ return providers;
+ }
+
+ public int getDiscountedPrice() {
+ return discountedPrice;
+ }
+
+ public void setDiscountedPrice(int discountedPrice) {
+ this.discountedPrice = discountedPrice;
+ }
+
+ @Override
+ public void apply(Board board) {
+ TradingRules rules = board.getTradingRules();
+ for (ResourceType type : resourceTypes) {
+ for (Provider provider : providers) {
+ rules.setCost(type, provider, discountedPrice);
+ }
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java
new file mode 100644
index 00000000..692eaea0
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+/**
+ * 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.
+ */
+public interface Effect {
+
+ void apply(Table table, int playerIndex);
+
+ int computePoints(Table table, int playerIndex);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java
new file mode 100644
index 00000000..1bae16a6
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java
@@ -0,0 +1,11 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public abstract class EndGameEffect implements Effect {
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ // EndGameEffects don't do anything when applied to the board, they simply give more points in the end
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java
new file mode 100644
index 00000000..4c1215d4
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+
+public class GoldIncrease extends InstantOwnBoardEffect {
+
+ private final int amount;
+
+ public GoldIncrease(int amount) {
+ this.amount = amount;
+ }
+
+ public int getAmount() {
+ return amount;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.addGold(amount);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ GoldIncrease that = (GoldIncrease) o;
+ return amount == that.amount;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(amount);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java
new file mode 100644
index 00000000..8f4340cf
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+
+public abstract class InstantOwnBoardEffect implements Effect {
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ apply(table.getBoard(playerIndex));
+ }
+
+ protected abstract void apply(Board board);
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ // InstantEffects are only important when applied to the board, they don't give extra points in the end
+ return 0;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java
new file mode 100644
index 00000000..7da112f5
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+
+public class MilitaryReinforcements extends InstantOwnBoardEffect {
+
+ private final int count;
+
+ public MilitaryReinforcements(int count) {
+ this.count = count;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getMilitary().addShields(count);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MilitaryReinforcements that = (MilitaryReinforcements) o;
+ return count == that.count;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(count);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java
new file mode 100644
index 00000000..514c65db
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java
@@ -0,0 +1,54 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.Production;
+
+public class ProductionIncrease extends InstantOwnBoardEffect {
+
+ private Production production = new Production();
+
+ private boolean sellable = true;
+
+ public Production getProduction() {
+ return production;
+ }
+
+ public void setProduction(Production production) {
+ this.production = production;
+ }
+
+ public boolean isSellable() {
+ return sellable;
+ }
+
+ public void setSellable(boolean sellable) {
+ this.sellable = sellable;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getProduction().addAll(production);
+ if (sellable) {
+ board.getPublicProduction().addAll(production);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProductionIncrease that = (ProductionIncrease) o;
+ return Objects.equals(production, that.production);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(production);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java
new file mode 100644
index 00000000..9a5d66ed
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public class RawPointsIncrease extends EndGameEffect {
+
+ private final int points;
+
+ public RawPointsIncrease(int points) {
+ this.points = points;
+ }
+
+ public int getPoints() {
+ return points;
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return points;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RawPointsIncrease that = (RawPointsIncrease) o;
+ return points == that.points;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(points);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java
new file mode 100644
index 00000000..4e6764ee
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java
@@ -0,0 +1,22 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.Science;
+
+public class ScienceProgress extends InstantOwnBoardEffect {
+
+ private Science science;
+
+ public Science getScience() {
+ return science;
+ }
+
+ public void setScience(Science science) {
+ this.science = science;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getScience().addAll(science);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java
new file mode 100644
index 00000000..cdf67f20
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java
@@ -0,0 +1,47 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public enum 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
+ public int computePoints(Table table, int playerIndex) {
+ Card copiedGuild = table.getBoard(playerIndex).getCopiedGuild();
+ if (copiedGuild == null) {
+ throw new IllegalStateException("The copied Guild has not been chosen, cannot compute points");
+ }
+ return copiedGuild.getEffects().stream().mapToInt(e -> e.computePoints(table, playerIndex)).sum();
+ }
+ };
+
+ protected void apply(Board board) {
+ board.addSpecial(this);
+ }
+
+ public int computePoints(Table table, int playerIndex) {
+ return 0;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java
new file mode 100644
index 00000000..a5953c2f
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java
@@ -0,0 +1,26 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public class SpecialAbilityActivation implements Effect {
+
+ private final SpecialAbility specialAbility;
+
+ public SpecialAbilityActivation(SpecialAbility specialAbility) {
+ this.specialAbility = specialAbility;
+ }
+
+ public SpecialAbility getSpecialAbility() {
+ return specialAbility;
+ }
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ specialAbility.apply(table.getBoard(playerIndex));
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return specialAbility.computePoints(table, playerIndex);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java
new file mode 100644
index 00000000..f1cb50b3
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class BuildWonderMove extends CardFromHandMove {
+
+ BuildWonderMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void validate(Table table, List<Card> playerHand) throws InvalidMoveException {
+ super.validate(table, playerHand);
+ Board board = table.getBoard(getPlayerIndex());
+ if (!board.getWonder().isNextStageBuildable(table, getPlayerIndex(), getBoughtResources())) {
+ throw new InvalidMoveException(
+ String.format("Player %d cannot upgrade his wonder with the given resources", getPlayerIndex()));
+ }
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.getWonder().buildLevel(getCard().getBack());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ int playerIndex = getPlayerIndex();
+ Board board = table.getBoard(playerIndex);
+ board.getWonder().activateLastBuiltStage(table, playerIndex, getBoughtResources());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java
new file mode 100644
index 00000000..1794966d
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java
@@ -0,0 +1,23 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public abstract class CardFromHandMove extends Move {
+
+ CardFromHandMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void validate(Table table, List<Card> playerHand) throws InvalidMoveException {
+ if (!playerHand.contains(getCard())) {
+ throw new InvalidMoveException(
+ String.format("Player %d does not have the card '%s' in his hand", getPlayerIndex(),
+ getCard().getName()));
+ }
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java
new file mode 100644
index 00000000..a93670c5
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java
@@ -0,0 +1,56 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+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;
+
+public class CopyGuildMove extends Move {
+
+ CopyGuildMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void validate(Table table, List<Card> playerHand) throws InvalidMoveException {
+ Board board = table.getBoard(getPlayerIndex());
+ if (!board.hasSpecial(SpecialAbility.COPY_GUILD)) {
+ throw new InvalidMoveException(
+ String.format("Player %d does not have the ability to copy guild cards", getPlayerIndex()));
+ }
+ if (getCard().getColor() != Color.PURPLE) {
+ throw new InvalidMoveException(
+ String.format("Player %d cannot copy card %s because it is not a guild card", getPlayerIndex(),
+ getCard().getName()));
+ }
+ boolean leftNeighbourHasIt = neighbourHasTheCard(table, RelativeBoardPosition.LEFT);
+ boolean rightNeighbourHasIt = neighbourHasTheCard(table, RelativeBoardPosition.RIGHT);
+ if (!leftNeighbourHasIt && !rightNeighbourHasIt) {
+ throw new InvalidMoveException(
+ String.format("Player %d cannot copy card %s because none of his neighbour has it",
+ getPlayerIndex(), getCard().getName()));
+ }
+ }
+
+ private boolean neighbourHasTheCard(Table table, RelativeBoardPosition position) {
+ Board neighbourBoard = table.getBoard(getPlayerIndex(), position);
+ return neighbourBoard.getPlayedCards().contains(getCard());
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ // nothing special to do here
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.setCopiedGuild(getCard());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java
new file mode 100644
index 00000000..076a593c
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java
@@ -0,0 +1,27 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class DiscardMove extends CardFromHandMove {
+
+ DiscardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ discardedCards.add(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addGold(settings.getDiscardedCardGold());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/InvalidMoveException.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/InvalidMoveException.java
new file mode 100644
index 00000000..58190274
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/InvalidMoveException.java
@@ -0,0 +1,8 @@
+package org.luxons.sevenwonders.game.moves;
+
+public class InvalidMoveException extends IllegalArgumentException {
+
+ public InvalidMoveException(String message) {
+ super(message);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/Move.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/Move.java
new file mode 100644
index 00000000..40c61eea
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/Move.java
@@ -0,0 +1,50 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public abstract class Move {
+
+ private int playerIndex;
+
+ private Card card;
+
+ private MoveType type;
+
+ private List<BoughtResources> boughtResources = new ArrayList<>();
+
+ Move(int playerIndex, Card card, PlayerMove move) {
+ this.playerIndex = playerIndex;
+ this.card = card;
+ this.type = move.getType();
+ this.boughtResources = move.getBoughtResources();
+ }
+
+ public int getPlayerIndex() {
+ return playerIndex;
+ }
+
+ public Card getCard() {
+ return card;
+ }
+
+ public MoveType getType() {
+ return type;
+ }
+
+ public List<BoughtResources> getBoughtResources() {
+ return boughtResources;
+ }
+
+ public abstract void validate(Table table, List<Card> playerHand) throws InvalidMoveException;
+
+ public abstract void place(Table table, List<Card> discardedCards, Settings settings);
+
+ public abstract void activate(Table table, List<Card> discardedCards, Settings settings);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java
new file mode 100644
index 00000000..bf64344d
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.moves;
+
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public enum MoveType {
+ PLAY {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new PlayCardMove(playerIndex, card, move);
+ }
+ },
+ PLAY_FREE {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new PlayFreeCardMove(playerIndex, card, move);
+ }
+ },
+ UPGRADE_WONDER {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new BuildWonderMove(playerIndex, card, move);
+ }
+ },
+ DISCARD {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new DiscardMove(playerIndex, card, move);
+ }
+ },
+ COPY_GUILD {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new CopyGuildMove(playerIndex, card, move);
+ }
+ };
+
+ public abstract Move resolve(int playerIndex, Card card, PlayerMove move);
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java
new file mode 100644
index 00000000..82052981
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class PlayCardMove extends CardFromHandMove {
+
+ PlayCardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void validate(Table table, List<Card> playerHand) throws InvalidMoveException {
+ super.validate(table, playerHand);
+ Board board = table.getBoard(getPlayerIndex());
+ if (!getCard().isChainableOn(board) && !getCard().getRequirements()
+ .areMetWithHelpBy(board, getBoughtResources())) {
+ throw new InvalidMoveException(
+ String.format("Player %d cannot play the card %s with the given resources", getPlayerIndex(),
+ getCard().getName()));
+ }
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addCard(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ getCard().applyTo(table, getPlayerIndex(), getBoughtResources());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java
new file mode 100644
index 00000000..4e8eefa5
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class PlayFreeCardMove extends CardFromHandMove {
+
+ PlayFreeCardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void validate(Table table, List<Card> playerHand) throws InvalidMoveException {
+ super.validate(table, playerHand);
+ Board board = table.getBoard(getPlayerIndex());
+ if (!board.canPlayFreeCard(table.getCurrentAge())) {
+ throw new InvalidMoveException(
+ String.format("Player %d cannot play the card %s for free", getPlayerIndex(), getCard().getName()));
+ }
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addCard(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ // only apply effects, without paying the cost
+ getCard().getEffects().forEach(e -> e.apply(table, getPlayerIndex()));
+ Board board = table.getBoard(getPlayerIndex());
+ board.consumeFreeCard(table.getCurrentAge());
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BestPriceCalculator.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BestPriceCalculator.java
new file mode 100644
index 00000000..8fed41d1
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BestPriceCalculator.java
@@ -0,0 +1,102 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+
+public class BestPriceCalculator {
+
+ private final Resources resources;
+
+ private final List<ResourcePool> pools;
+
+ private BestPriceCalculator(Resources resources, Table table, int playerIndex) {
+ this.resources = resources;
+ this.pools = createResourcePools(table, playerIndex);
+ }
+
+ private static List<ResourcePool> createResourcePools(Table table, int playerIndex) {
+ Provider[] providers = Provider.values();
+ List<ResourcePool> pools = new ArrayList<>(providers.length + 1);
+
+ Board board = table.getBoard(playerIndex);
+ TradingRules rules = board.getTradingRules();
+ pools.add(new ResourcePool(board.getProduction(), null, rules));
+
+ for (Provider provider : providers) {
+ Board providerBoard = table.getBoard(playerIndex, provider.getBoardPosition());
+ ResourcePool pool = new ResourcePool(providerBoard.getPublicProduction(), provider, rules);
+ pools.add(pool);
+ }
+ return pools;
+ }
+
+ public static int bestPrice(Resources resources, Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ Resources leftToPay = resources.minus(board.getProduction().getFixedResources());
+ return new BestPriceCalculator(leftToPay, table, playerIndex).bestPrice();
+ }
+
+ private int bestPrice() {
+ if (resources.isEmpty()) {
+ return 0;
+ }
+ int currentMinPrice = Integer.MAX_VALUE;
+ for (ResourceType type : ResourceType.values()) {
+ if (resources.getQuantity(type) > 0) {
+ int minPriceUsingOwnResource = bestPriceWithout(type);
+ currentMinPrice = Math.min(currentMinPrice, minPriceUsingOwnResource);
+ }
+ }
+ return currentMinPrice;
+ }
+
+ private int bestPriceWithout(ResourceType type) {
+ resources.remove(type, 1);
+ int currentMinPrice = Integer.MAX_VALUE;
+ for (ResourcePool pool : pools) {
+ int resCostInPool = pool.getCost(type);
+ for (Set<ResourceType> choice : pool.getChoices()) {
+ if (choice.contains(type)) {
+ Set<ResourceType> temp = EnumSet.copyOf(choice);
+ choice.clear();
+ currentMinPrice = Math.min(currentMinPrice, bestPrice() + resCostInPool);
+ choice.addAll(temp);
+ }
+ }
+ }
+ resources.add(type, 1);
+ return currentMinPrice;
+ }
+
+ private static class ResourcePool {
+
+ private final Set<Set<ResourceType>> choices;
+
+ private final Provider provider;
+
+ private final TradingRules rules;
+
+ private ResourcePool(Production production, Provider provider, TradingRules rules) {
+ this.choices = production.asChoices();
+ this.provider = provider;
+ this.rules = rules;
+ }
+
+ Set<Set<ResourceType>> getChoices() {
+ return choices;
+ }
+
+ int getCost(ResourceType type) {
+ if (provider == null) {
+ return 0;
+ }
+ return rules.getCost(type, provider);
+ }
+ }
+}
+
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java
new file mode 100644
index 00000000..ec261c8c
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.resources;
+
+public class BoughtResources {
+
+ private Provider provider;
+
+ private Resources resources;
+
+ public Provider getProvider() {
+ return provider;
+ }
+
+ public void setProvider(Provider provider) {
+ this.provider = provider;
+ }
+
+ public Resources getResources() {
+ return resources;
+ }
+
+ public void setResources(Resources resources) {
+ this.resources = resources;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Production.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Production.java
new file mode 100644
index 00000000..7fa83e51
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Production.java
@@ -0,0 +1,106 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class Production {
+
+ private final Resources fixedResources = new Resources();
+
+ private final Set<Set<ResourceType>> alternativeResources = new HashSet<>();
+
+ public void addFixedResource(ResourceType type, int quantity) {
+ fixedResources.add(type, quantity);
+ }
+
+ public void addChoice(ResourceType... options) {
+ EnumSet<ResourceType> optionSet = EnumSet.copyOf(Arrays.asList(options));
+ alternativeResources.add(optionSet);
+ }
+
+ public void addAll(Resources resources) {
+ fixedResources.addAll(resources);
+ }
+
+ public void addAll(Production production) {
+ fixedResources.addAll(production.getFixedResources());
+ alternativeResources.addAll(production.getAlternativeResources());
+ }
+
+ public Resources getFixedResources() {
+ return fixedResources;
+ }
+
+ public Set<Set<ResourceType>> getAlternativeResources() {
+ return alternativeResources;
+ }
+
+ Set<Set<ResourceType>> asChoices() {
+ Set<Set<ResourceType>> result = new HashSet<>(fixedResources.size() + alternativeResources.size());
+ fixedResources.asList().stream().map(EnumSet::of).forEach(result::add);
+ result.addAll(alternativeResources);
+ return result;
+ }
+
+ public boolean contains(Resources resources) {
+ if (fixedResources.contains(resources)) {
+ return true;
+ }
+ Resources remaining = resources.minus(fixedResources);
+ return containedInAlternatives(remaining);
+ }
+
+ private boolean containedInAlternatives(Resources resources) {
+ return containedInAlternatives(resources, alternativeResources);
+ }
+
+ private static boolean containedInAlternatives(Resources resources, Set<Set<ResourceType>> alternatives) {
+ if (resources.isEmpty()) {
+ return true;
+ }
+ for (ResourceType type : ResourceType.values()) {
+ if (resources.getQuantity(type) <= 0) {
+ continue;
+ }
+ Set<ResourceType> candidate = findFirstAlternativeContaining(alternatives, type);
+ if (candidate == null) {
+ return false; // no alternative produces the resource of this entry
+ }
+ resources.remove(type, 1);
+ alternatives.remove(candidate);
+ boolean remainingAreContainedToo = containedInAlternatives(resources, alternatives);
+ resources.add(type, 1);
+ alternatives.add(candidate);
+ if (remainingAreContainedToo) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Set<ResourceType> findFirstAlternativeContaining(Set<Set<ResourceType>> alternatives,
+ ResourceType type) {
+ return alternatives.stream().filter(a -> a.contains(type)).findAny().orElse(null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Production that = (Production) o;
+ return Objects.equals(fixedResources, that.fixedResources) && Objects.equals(alternativeResources,
+ that.alternativeResources);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fixedResources, alternativeResources);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java
new file mode 100644
index 00000000..9c4aa3f9
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java
@@ -0,0 +1,18 @@
+package org.luxons.sevenwonders.game.resources;
+
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+
+public enum Provider {
+ LEFT_PLAYER(RelativeBoardPosition.LEFT),
+ RIGHT_PLAYER(RelativeBoardPosition.RIGHT);
+
+ private final RelativeBoardPosition boardPosition;
+
+ Provider(RelativeBoardPosition boardPosition) {
+ this.boardPosition = boardPosition;
+ }
+
+ public RelativeBoardPosition getBoardPosition() {
+ return boardPosition;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java
new file mode 100644
index 00000000..baad8451
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum ResourceType {
+ WOOD('W'),
+ STONE('S'),
+ ORE('O'),
+ CLAY('C'),
+ GLASS('G'),
+ PAPYRUS('P'),
+ LOOM('L');
+
+ private static final Map<Character, ResourceType> typesPerSymbol = new HashMap<>(7);
+
+ static {
+ for (ResourceType type : values()) {
+ typesPerSymbol.put(type.symbol, type);
+ }
+ }
+
+ private final Character symbol;
+
+ ResourceType(Character symbol) {
+ this.symbol = symbol;
+ }
+
+ public static ResourceType fromSymbol(Character symbol) {
+ ResourceType type = typesPerSymbol.get(symbol);
+ if (type == null) {
+ throw new IllegalArgumentException(String.format("Unknown resource type symbol '%s'", symbol));
+ }
+ return type;
+ }
+
+ public Character getSymbol() {
+ return symbol;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java
new file mode 100644
index 00000000..04278fea
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java
@@ -0,0 +1,91 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Resources {
+
+ private final Map<ResourceType, Integer> quantities = new EnumMap<>(ResourceType.class);
+
+ public void add(ResourceType type, int quantity) {
+ quantities.merge(type, quantity, (x, y) -> x + y);
+ }
+
+ public void remove(ResourceType type, int quantity) {
+ if (getQuantity(type) < quantity) {
+ throw new NoSuchElementException(String.format("Can't remove %d resources of type %s", quantity, type));
+ }
+ quantities.computeIfPresent(type, (t, oldQty) -> oldQty - quantity);
+ }
+
+ public void addAll(Resources resources) {
+ resources.quantities.forEach(this::add);
+ }
+
+ public int getQuantity(ResourceType type) {
+ return quantities.getOrDefault(type, 0);
+ }
+
+ public List<ResourceType> asList() {
+ return quantities.entrySet()
+ .stream()
+ .flatMap(e -> Stream.generate(e::getKey).limit(e.getValue()))
+ .collect(Collectors.toList());
+ }
+
+ public boolean contains(Resources resources) {
+ return resources.quantities.entrySet().stream().allMatch(this::hasAtLeast);
+ }
+
+ private boolean hasAtLeast(Entry<ResourceType, Integer> quantity) {
+ return quantity.getValue() <= getQuantity(quantity.getKey());
+ }
+
+ /**
+ * Creates new {@link Resources} object containing these resources minus the given resources.
+ *
+ * @param resources
+ * the resources to subtract from these resources
+ *
+ * @return a new {@link Resources} object containing these resources minus the given resources.
+ */
+ public Resources minus(Resources resources) {
+ Resources diff = new Resources();
+ quantities.forEach((type, count) -> {
+ int remainder = count - resources.getQuantity(type);
+ diff.quantities.put(type, Math.max(0, remainder));
+ });
+ return diff;
+ }
+
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public int size() {
+ return quantities.values().stream().reduce(0, Integer::sum);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Resources resources = (Resources) o;
+ return Objects.equals(quantities, resources.quantities);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(quantities);
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java
new file mode 100644
index 00000000..8cd1d9bc
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java
@@ -0,0 +1,43 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+public class TradingRules {
+
+ private final Map<ResourceType, Map<Provider, Integer>> costs = new EnumMap<>(ResourceType.class);
+
+ private final int defaultCost;
+
+ public TradingRules(int defaultCost) {
+ this.defaultCost = defaultCost;
+ }
+
+ public Map<ResourceType, Map<Provider, Integer>> getCosts() {
+ return costs;
+ }
+
+ int getCost(ResourceType type, Provider provider) {
+ return costs.computeIfAbsent(type, t -> new EnumMap<>(Provider.class)).getOrDefault(provider, defaultCost);
+ }
+
+ public void setCost(ResourceType type, Provider provider, int cost) {
+ costs.computeIfAbsent(type, t -> new EnumMap<>(Provider.class)).put(provider, cost);
+ }
+
+ public int computeCost(List<BoughtResources> boughtResources) {
+ return boughtResources.stream().mapToInt(this::computeCost).sum();
+ }
+
+ public int computeCost(BoughtResources boughtResources) {
+ Resources resources = boughtResources.getResources();
+ Provider provider = boughtResources.getProvider();
+ int total = 0;
+ for (ResourceType type : ResourceType.values()) {
+ int count = resources.getQuantity(type);
+ total += getCost(type, provider) * count;
+ }
+ return total;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java
new file mode 100644
index 00000000..f4a0d832
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.scoring;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PlayerScore {
+
+ private final int boardGold;
+
+ private final Map<ScoreCategory, Integer> scoresByCategory = new HashMap<>();
+
+ private int totalPoints = 0;
+
+ public PlayerScore(int boardGold) {
+ this.boardGold = boardGold;
+ }
+
+ public Integer put(ScoreCategory category, Integer points) {
+ totalPoints += points;
+ return scoresByCategory.put(category, points);
+ }
+
+ public int getPoints(ScoreCategory category) {
+ return scoresByCategory.get(category);
+ }
+
+ public Map<ScoreCategory, Integer> getPointsPerCategory() {
+ return scoresByCategory;
+ }
+
+ public int getTotalPoints() {
+ return totalPoints;
+ }
+
+ public int getBoardGold() {
+ return boardGold;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java
new file mode 100644
index 00000000..e7cdaedd
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.scoring;
+
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+public class ScoreBoard {
+
+ private static final Comparator<PlayerScore> comparator = Comparator.comparing(PlayerScore::getTotalPoints)
+ .thenComparing(PlayerScore::getBoardGold);
+
+ private PriorityQueue<PlayerScore> scores;
+
+ public ScoreBoard() {
+ scores = new PriorityQueue<>(comparator);
+ }
+
+ public void add(PlayerScore score) {
+ scores.add(score);
+ }
+
+ public PriorityQueue<PlayerScore> getScores() {
+ return scores;
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java
new file mode 100644
index 00000000..a6a9537d
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java
@@ -0,0 +1,11 @@
+package org.luxons.sevenwonders.game.scoring;
+
+public enum ScoreCategory {
+ CIVIL,
+ SCIENCE,
+ MILITARY,
+ TRADE,
+ GUILD,
+ WONDER,
+ GOLD
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java
new file mode 100644
index 00000000..73fff305
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java
@@ -0,0 +1,102 @@
+package org.luxons.sevenwonders.game.wonders;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+public class Wonder {
+
+ private String name;
+
+ private ResourceType initialResource;
+
+ private List<WonderStage> stages;
+
+ private String image;
+
+ public Wonder() {
+ }
+
+ public Wonder(String name, ResourceType initialResource, WonderStage... stages) {
+ this.name = name;
+ this.initialResource = initialResource;
+ this.stages = Arrays.asList(stages);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public ResourceType getInitialResource() {
+ return initialResource;
+ }
+
+ public void setInitialResource(ResourceType initialResource) {
+ this.initialResource = initialResource;
+ }
+
+ public List<WonderStage> getStages() {
+ return stages;
+ }
+
+ public void setStages(List<WonderStage> stages) {
+ this.stages = stages;
+ }
+
+ public int getNbBuiltStages() {
+ return (int) stages.stream().filter(WonderStage::isBuilt).count();
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public void setImage(String image) {
+ this.image = image;
+ }
+
+ public boolean isNextStageBuildable(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ int nextLevel = getNbBuiltStages();
+ if (nextLevel == stages.size()) {
+ return false;
+ }
+ return getNextStage().isBuildable(table, playerIndex, boughtResources);
+ }
+
+ public void buildLevel(CardBack cardBack) {
+ getNextStage().build(cardBack);
+ }
+
+ private WonderStage getNextStage() {
+ int nextLevel = getNbBuiltStages();
+ if (nextLevel == stages.size()) {
+ throw new IllegalStateException("This wonder has already reached its maximum level");
+ }
+ return stages.get(nextLevel);
+ }
+
+ public void activateLastBuiltStage(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ getLastBuiltStage().activate(table, playerIndex, boughtResources);
+ }
+
+ private WonderStage getLastBuiltStage() {
+ int lastLevel = getNbBuiltStages() - 1;
+ return stages.get(lastLevel);
+ }
+
+ public int computePoints(Table table, int playerIndex) {
+ return stages.stream()
+ .filter(WonderStage::isBuilt)
+ .flatMap(c -> c.getEffects().stream())
+ .mapToInt(e -> e.computePoints(table, playerIndex))
+ .sum();
+ }
+}
diff --git a/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java b/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java
new file mode 100644
index 00000000..5f6765ee
--- /dev/null
+++ b/game-engine/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java
@@ -0,0 +1,56 @@
+package org.luxons.sevenwonders.game.wonders;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+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.BoughtResources;
+
+public class WonderStage {
+
+ private Requirements requirements;
+
+ private List<Effect> effects;
+
+ private CardBack cardBack;
+
+ public Requirements getRequirements() {
+ return requirements;
+ }
+
+ public void setRequirements(Requirements requirements) {
+ this.requirements = requirements;
+ }
+
+ public List<Effect> getEffects() {
+ return effects;
+ }
+
+ public void setEffects(List<Effect> effects) {
+ this.effects = effects;
+ }
+
+ public CardBack getCardBack() {
+ return cardBack;
+ }
+
+ public boolean isBuilt() {
+ return cardBack != null;
+ }
+
+ public boolean isBuildable(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ Board board = table.getBoard(playerIndex);
+ return requirements.areMetWithHelpBy(board, boughtResources);
+ }
+
+ void build(CardBack cardBack) {
+ this.cardBack = cardBack;
+ }
+
+ void activate(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ effects.forEach(e -> e.apply(table, playerIndex));
+ }
+}
diff --git a/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json b/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
new file mode 100644
index 00000000..83777d9e
--- /dev/null
+++ b/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
@@ -0,0 +1,1719 @@
+{
+ "age1Back": "age1.png",
+ "age2Back": "age2.png",
+ "age3Back": "age3.png",
+ "age1": [
+ {
+ "name": "Clay Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "O/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "claypit.png"
+ },
+ {
+ "name": "Clay Pool",
+ "color": "BROWN",
+ "effect": {
+ "production": "C"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "claypool.png"
+ },
+ {
+ "name": "Excavation",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "excavation.png"
+ },
+ {
+ "name": "Forest Cave",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "forestcave.png"
+ },
+ {
+ "name": "Lumber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lumberyard.png"
+ },
+ {
+ "name": "Mine",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "mine.png"
+ },
+ {
+ "name": "Ore Vein",
+ "color": "BROWN",
+ "effect": {
+ "production": "O"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "orevein.png"
+ },
+ {
+ "name": "Stone Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "S"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stonepit.png"
+ },
+ {
+ "name": "Timber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/S"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "timberyard.png"
+ },
+ {
+ "name": "Tree Farm",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "treefarm.png"
+ },
+ {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ },
+ {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ },
+ {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ },
+ {
+ "name": "East Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "easttradingpost.png"
+ },
+ {
+ "name": "Marketplace",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "LGP",
+ "providers": [
+ "LEFT_PLAYER",
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Caravansery"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "marketplace.png"
+ },
+ {
+ "name": "Tavern",
+ "color": "YELLOW",
+ "effect": {
+ "gold": 5
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "tavern.png"
+ },
+ {
+ "name": "West Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "LEFT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "westtradingpost.png"
+ },
+ {
+ "name": "Altar",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Temple"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "altar.png"
+ },
+ {
+ "name": "Baths",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "S"
+ },
+ "chainChildren": [
+ "Aquaduct"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "baths.png"
+ },
+ {
+ "name": "Pawnshop",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "pawnshop.png"
+ },
+ {
+ "name": "Theater",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Statue"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "theater.png"
+ },
+ {
+ "name": "Apothecary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "L"
+ },
+ "chainChildren": [
+ "Stables",
+ "Dispensary"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "apothecary.png"
+ },
+ {
+ "name": "Scriptorium",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "P"
+ },
+ "chainChildren": [
+ "Courthouse",
+ "Library"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "scriptorium.png"
+ },
+ {
+ "name": "Workshop",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "G"
+ },
+ "chainChildren": [
+ "Archery Range",
+ "Laboratory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "workshop.png"
+ },
+ {
+ "name": "Barracks",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "O"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "barracks.png"
+ },
+ {
+ "name": "Guard Tower",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "C"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "guardtower.png"
+ },
+ {
+ "name": "Stockade",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "W"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "stockade.png"
+ }
+ ],
+ "age2": [
+ {
+ "name": "Brickyard",
+ "color": "BROWN",
+ "effect": {
+ "production": "CC"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "brickyard.png"
+ },
+ {
+ "name": "Foundry",
+ "color": "BROWN",
+ "effect": {
+ "production": "OO"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "foundry.png"
+ },
+ {
+ "name": "Quarry",
+ "color": "BROWN",
+ "effect": {
+ "production": "SS"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "quarry.png"
+ },
+ {
+ "name": "Sawmill",
+ "color": "BROWN",
+ "effect": {
+ "production": "WW"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "sawmill.png"
+ },
+ {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ },
+ {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ },
+ {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ },
+ {
+ "name": "Bazar",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF",
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "bazar.png"
+ },
+ {
+ "name": "Caravansery",
+ "color": "YELLOW",
+ "effect": {
+ "production": "(W/S/O/C)"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "chainParent": "Marketplace",
+ "chainChildren": [
+ "Lighthouse"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "caravansery.png"
+ },
+ {
+ "name": "Forum",
+ "color": "YELLOW",
+ "effect": {
+ "production": "(G/P/L)"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "chainParent": "East Trading Post",
+ "chainChildren": [
+ "Haven"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "forum.png"
+ },
+ {
+ "name": "Vineyard",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF",
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "vineyard.png"
+ },
+ {
+ "name": "Aqueduct",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "chainParent": "Baths",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "aqueduct.png"
+ },
+ {
+ "name": "Courthouse",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCL"
+ },
+ "chainParent": "Scriptorium",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "courthouse.png"
+ },
+ {
+ "name": "Statue",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOO"
+ },
+ "chainParent": "Theater",
+ "chainChildren": [
+ "Gardens"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "statue.png"
+ },
+ {
+ "name": "Temple",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCG"
+ },
+ "chainParent": "Altar",
+ "chainChildren": [
+ "Pantheon"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "temple.png"
+ },
+ {
+ "name": "Dispensary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OOG"
+ },
+ "chainParent": "Apothecary",
+ "chainChildren": [
+ "Arena",
+ "Lodge"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "dispensary.png"
+ },
+ {
+ "name": "Laboratory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCP"
+ },
+ "chainParent": "Workshop",
+ "chainChildren": [
+ "Siege Workshop",
+ "Observatory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "laboratory.png"
+ },
+ {
+ "name": "Library",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSL"
+ },
+ "chainParent": "Scriptorium",
+ "chainChildren": [
+ "Senate",
+ "University"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "library.png"
+ },
+ {
+ "name": "School",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WP"
+ },
+ "chainChildren": [
+ "Academy",
+ "Study"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "school.png"
+ },
+ {
+ "name": "Archery Range",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWO"
+ },
+ "chainParent": "Workshop",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "archeryrange.png"
+ },
+ {
+ "name": "Stables",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOC"
+ },
+ "chainParent": "Apothecary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stables.png"
+ },
+ {
+ "name": "Training Ground",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOO"
+ },
+ "chainChildren": [
+ "Circus"
+ ],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "trainingground.png"
+ },
+ {
+ "name": "Walls",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "chainChildren": [
+ "Fortifications"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "walls.png"
+ }
+ ],
+ "age3": [
+ {
+ "name": "Arena",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 3,
+ "points": 1,
+ "type": "WONDER_LEVEL"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSO"
+ },
+ "chainParent": "Dispensary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arena.png"
+ },
+ {
+ "name": "Chamber of Commerce",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 2,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCP"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "chamberofcommerce.png"
+ },
+ {
+ "name": "Haven",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOL"
+ },
+ "chainParent": "Forum",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "haven.png"
+ },
+ {
+ "name": "Lighthouse",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SG"
+ },
+ "chainParent": "Caravansery",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lighthouse.png"
+ },
+ {
+ "name": "Gardens",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCC"
+ },
+ "chainParent": "Statue",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "gardens.png"
+ },
+ {
+ "name": "Palace",
+ "color": "BLUE",
+ "effect": {
+ "points": 8
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WSOCGPL"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "palace.png"
+ },
+ {
+ "name": "Pantheon",
+ "color": "BLUE",
+ "effect": {
+ "points": 7
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OCCGPL"
+ },
+ "chainParent": "Temple",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "pantheon.png"
+ },
+ {
+ "name": "Senate",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWSO"
+ },
+ "chainParent": "Library",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "senate.png"
+ },
+ {
+ "name": "Town Hall",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSOG"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "townhall.png"
+ },
+ {
+ "name": "Academy",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSG"
+ },
+ "chainParent": "School",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "academy.png"
+ },
+ {
+ "name": "Lodge",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCPL"
+ },
+ "chainParent": "Dispensary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lodge.png"
+ },
+ {
+ "name": "Observatory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OOGL"
+ },
+ "chainParent": "Laboratory",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "observatory.png"
+ },
+ {
+ "name": "Study",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WPL"
+ },
+ "chainParent": "School",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "study.png"
+ },
+ {
+ "name": "University",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWGP"
+ },
+ "chainParent": "Library",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "university.png"
+ },
+ {
+ "name": "Arsenal",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWOL"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arsenal.png"
+ },
+ {
+ "name": "Circus",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSO"
+ },
+ "chainParent": "Training Ground",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "circus.png"
+ },
+ {
+ "name": "Fortifications",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SOOO"
+ },
+ "chainParent": "Walls",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "fortifications.png"
+ },
+ {
+ "name": "Siege Workshop",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCCC"
+ },
+ "chainParent": "Laboratory",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "siegeworkshop.png"
+ }
+ ],
+ "guildCards": [
+ {
+ "name": "Builders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "SELF",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "WONDER_LEVEL"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSCCG"
+ },
+ "chainChildren": [],
+ "image": "buildersguild.png"
+ },
+ {
+ "name": "Craftsmens Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSOO"
+ },
+ "chainChildren": [],
+ "image": "craftsmensguild.png"
+ },
+ {
+ "name": "Magistrates Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BLUE"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWWSL"
+ },
+ "chainChildren": [],
+ "image": "magistratesguild.png"
+ },
+ {
+ "name": "Philosophers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREEN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCPL"
+ },
+ "chainChildren": [],
+ "image": "philosophersguild.png"
+ },
+ {
+ "name": "Scientists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "science": "any"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWOOP"
+ },
+ "chainChildren": [],
+ "image": "scientistsguild.png"
+ },
+ {
+ "name": "Shipowners Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN",
+ "GREY",
+ "PURPLE"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWWGP"
+ },
+ "chainChildren": [],
+ "image": "shipownersguild.png"
+ },
+ {
+ "name": "Spies Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "RED"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCG"
+ },
+ "chainChildren": [],
+ "image": "spiesguild.png"
+ },
+ {
+ "name": "Strategists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "DEFEAT_TOKEN"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SOOL"
+ },
+ "chainChildren": [],
+ "image": "strategistsguild.png"
+ },
+ {
+ "name": "Traders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "YELLOW"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "chainChildren": [],
+ "image": "tradersguild.png"
+ },
+ {
+ "name": "Workers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WSOOC"
+ },
+ "chainChildren": [],
+ "image": "workersguild.png"
+ }
+ ]
+} \ No newline at end of file
diff --git a/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json b/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
new file mode 100644
index 00000000..5beceadd
--- /dev/null
+++ b/game-engine/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
@@ -0,0 +1,515 @@
+[
+ {
+ "name": "alexandria",
+ "sides": {
+ "A": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "production": "(W/S/O/C)"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GG"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaA.png"
+ },
+ "B": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "production": "(W/S/O/C)"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "production": "(G/P/L)"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaB.png"
+ }
+ }
+ },
+ {
+ "name": "babylon",
+ "sides": {
+ "A": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWW"
+ },
+ "effects": {
+ "science": "any"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCC"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "babylonA.png"
+ },
+ "B": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CL"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWG"
+ },
+ "effects": {
+ "action": "PLAY_LAST_CARD"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCP"
+ },
+ "effects": {
+ "science": "any"
+ }
+ }
+ ],
+ "image": "babylonB.png"
+ }
+ }
+ },
+ {
+ "name": "ephesos",
+ "sides": {
+ "A": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 9
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "PP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "ephesosA.png"
+ },
+ "B": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 2
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 5
+ }
+ }
+ ],
+ "image": "ephesosB.png"
+ }
+ }
+ },
+ {
+ "name": "gizah",
+ "sides": {
+ "A": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWW"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahA.png"
+ },
+ "B": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSSP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahB.png"
+ }
+ }
+ },
+ {
+ "name": "halikarnassus",
+ "sides": {
+ "A": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOO"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "LL"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "halikarnassusA.png"
+ },
+ "B": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 2,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 1,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ }
+ ],
+ "image": "halikarnassusB.png"
+ }
+ }
+ },
+ {
+ "name": "olympia",
+ "sides": {
+ "A": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "action": "ONE_FREE"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "olympiaA.png"
+ },
+ "B": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "discount": {
+ "resourceTypes": "WSOC",
+ "providers": [
+ "LEFT_PLAYER",
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOL"
+ },
+ "effects": {
+ "action": "COPY_GUILD"
+ }
+ }
+ ],
+ "image": "olympiaB.png"
+ }
+ }
+ },
+ {
+ "name": "rhodos",
+ "sides": {
+ "A": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "military": 2
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOOO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "rhodosA.png"
+ },
+ "B": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "gold": 3,
+ "military": 1,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOOO"
+ },
+ "effects": {
+ "gold": 4,
+ "military": 1,
+ "points": 4
+ }
+ }
+ ],
+ "image": "rhodosB.png"
+ }
+ }
+ }
+] \ No newline at end of file
bgstack15