diff options
Diffstat (limited to 'game-engine/src/main')
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 |