package server; import server.cards.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import server.cards.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.stream.IntStream; /** * Represents the physical game board: the two card rows, the offering tiles, * the turn-order tile, and the card deck. * * Responsibilities: * - Owning and mutating the top and bottom card rows. * - Owning the OfferingTiles and the TurnTile. * - Performing end-of-round row transitions and era-change building swaps. * - Exposing visible EventCards to Game for resolution. * * NOT responsible for: * - Resolving events (that is EventsSolver's job, called by Game). * - Applying player rewards/penalties (that is Game's job). * - Holding a reference to the player list. */ public class GameBoard { private static final Logger logger = LogManager.getLogger(GameBoard.class); // ------------------------------------------------------------------------- // Fields // ------------------------------------------------------------------------- private Era era; private final CardDeck cardDeck; private final List topRow; private final List bottomRow; private final List offeringTiles; private final TurnTile turnTile; private final BuildingManager buildingManager; // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- /** * Creates the board for a new game. * Call {@link #setupInitialRows(int)} and {@link #initOfferingTiles(int)} * separately after construction (mirrors the rulebook setup steps). */ public GameBoard(Era startingEra, CardDeck cardDeck, int numPlayers) { this.era = startingEra; this.cardDeck = cardDeck; this.topRow = new ArrayList<>(); this.bottomRow = new ArrayList<>(); this.offeringTiles = new ArrayList<>(); this.turnTile = new TurnTile(numPlayers); this.buildingManager = new BuildingManager(); } // ------------------------------------------------------------------------- // Setup // ------------------------------------------------------------------------- /** * Creates and places the correct OfferingTiles for the given player count. * * Tile layout (from the rulebook): * orderId 0 → tile A: 3 food (5-player only) * orderId 1 → tile B: 1 DOWN (all) * orderId 2 → tile C: 1 UP (all) * orderId 3 → tile D: 2 DOWN (3+ players) * orderId 4 → tile E: 1 DOWN 1 UP (all) * orderId 5 → tile F: 2 UP (all) * orderId 6 → tile G: 1 DOWN 2 UP (4+ players) * * With n players, exactly n tiles are used, starting from tile A if * 5 players, otherwise starting from tile B. */ public void initOfferingTiles(int numPlayers) { offeringTiles.clear(); switch (numPlayers) { case 2: offeringTiles.add(new OfferingTile(1)); offeringTiles.add(new OfferingTile(2)); offeringTiles.add(new OfferingTile(4)); offeringTiles.add(new OfferingTile(5)); break; case 3: offeringTiles.add(new OfferingTile(1)); offeringTiles.add(new OfferingTile(2)); offeringTiles.add(new OfferingTile(3)); offeringTiles.add(new OfferingTile(4)); offeringTiles.add(new OfferingTile(5)); break; case 4: offeringTiles.add(new OfferingTile(1)); offeringTiles.add(new OfferingTile(2)); offeringTiles.add(new OfferingTile(3)); offeringTiles.add(new OfferingTile(4)); offeringTiles.add(new OfferingTile(5)); offeringTiles.add(new OfferingTile(6)); break; case 5: offeringTiles.add(new OfferingTile(0)); offeringTiles.add(new OfferingTile(1)); offeringTiles.add(new OfferingTile(2)); offeringTiles.add(new OfferingTile(3)); offeringTiles.add(new OfferingTile(4)); offeringTiles.add(new OfferingTile(5)); offeringTiles.add(new OfferingTile(6)); break; } } // Find the oddertingTile selected by Player public OfferingTile getOfferingTile(Player p){ OfferingTile offering = offeringTiles.stream().filter(o -> p.equals(o.getOccupant())).findFirst().orElse(null); return offering; } /** * Draws the initial two rows of cards according to rulebook setup steps 4-5: * * Bottom row: draw cards one at a time until (numPlayers + 1) Character cards * have been placed. Any Event card drawn is placed in the top row instead. * Top row: fill to (numPlayers + 4) with additional draws (accounting for * any events already placed there from the bottom-row draw). */ public void setupInitialRows(int numPlayers) { topRow.clear(); bottomRow.clear(); // Draw bottom row — events are bumped to top row while (bottomRow.size() < numPlayers + 1) { Card card = cardDeck.drawTribeOne(); if (card instanceof EventCard) { topRow.add(card); logger.info("Setup: Event card " + card.getCardId() + " moved to top row."); } else { bottomRow.add(card); } } // Fill top row up to numPlayers + 4 int needed = (numPlayers + 4) - topRow.size(); if (needed > 0) { topRow.addAll(cardDeck.drawTribe(needed)); } // Add all Building Era.I on top row 2P 1 3-4-5 2 topRow.add(cardDeck.drawBuildingOne(Era.I)); if (numPlayers > 2){ topRow.add(cardDeck.drawBuildingOne(Era.I)); } } // ------------------------------------------------------------------------- // End-of-round row management (rulebook "Fine del Round" steps 2-4) // ------------------------------------------------------------------------- /** * Performs the three row-management steps at the end of every round: * * Step 2 — Discard all Character and Event cards from the bottom row. * Building cards stay. * Step 3 — Move all Character and Event cards from the top row down to * the bottom row. Building cards stay in the top row. * Step 4 — Draw (numPlayers + 4) new cards into the top row. * * @return true if the newly drawn cards contain a card from the next Era, * signalling that {@link #triggerEraChange()} should be called. */ public boolean advanceRows(int numPlayers) { Era eraBeforeDraw = this.era; // Step 2: discard non-building cards from the bottom row bottomRow.removeIf( c -> !(c instanceof BuildingCard) ); // Step 3: move non-building cards from top row down to bottom row Iterator it = topRow.iterator(); while (it.hasNext()) { Card c = it.next(); if (!(c instanceof BuildingCard)) { logger.debug("move card from top to bottom " + c); bottomRow.add(c); it.remove(); } } // Step 4: draw fresh cards into the top row List newCards = cardDeck.drawTribe(numPlayers + 4); topRow.addAll(newCards); logger.info("New cards on top: {} " , newCards); return isNewEraRevealed(eraBeforeDraw, newCards); } /** * Returns true if any of the newly drawn cards belongs to an era * that is strictly later than the current one. */ private boolean isNewEraRevealed(Era currentEra, List newCards) { for (Card c : newCards) { Era cardEra = eraOf(c); if (cardEra != null && cardEra.ordinal() > currentEra.ordinal()) { logger.info("FOUND NEW ERA {} " , cardEra); return true; } } return false; } // ------------------------------------------------------------------------- // Era change (rulebook "Inizio della Nuova Era") // ------------------------------------------------------------------------- /** * Advances the era and performs the three building-row transitions: * * Step 1 — (Era III only) Discard all Building cards from the bottom row. * Step 2 — Move all Building cards from the top row to the bottom row * (to the right of the Tribe cards). [Era II and III] * Step 3 — Place the new era's Building cards face-up in the top row. * [Era II and III] * * @return the new Era after the transition */ public Era triggerEraChange(int numPlayers) { era = era.next(); // Era.I → II → III → FINAL logger.info("ERA CHANGED → {} ", era); // Step 1 (Era III only): remove old era buildings from the bottom row if (era == Era.III) { bottomRow.removeIf(c -> c instanceof BuildingCard); } // Step 2: move buildings from top row to bottom row List descending = new ArrayList<>(); Iterator it = topRow.iterator(); while (it.hasNext()) { Card c = it.next(); if (c instanceof BuildingCard) { descending.add((BuildingCard) c); it.remove(); } } bottomRow.addAll(descending); logger.info("event=MOVED_BUILING era={} count={} cards={}", era, descending.size(), descending); // Step 3: place the new era's building cards in the top row int n = BuildingRules.getBuildingCards(numPlayers, era); List newEraBuildings = cardDeck.drawBuilding(n , era); topRow.addAll(newEraBuildings); logger.info("event=ADD_BUILDINGS era={} count={} cards={}", era, n, newEraBuildings); return era; } // ------------------------------------------------------------------------- // Offering tiles // ------------------------------------------------------------------------- /** * Attempts to place a player's totem on an offering tile. * * @return true if successful; false if the tile was already occupied */ public boolean placeTotem(Player player, OfferingTile tile, TurnTile turntile) { if (!tile.isEmpty()) return false; tile.setOccupant(player); turntile.leaveTurnTile(player); return true; } /** * Removes all totems from offering tiles. Call at the start of each new round. */ public void clearOfferingTiles() { for (OfferingTile tile : offeringTiles) { tile.removeOccupant(); } } // ------------------------------------------------------------------------- // Card removal (called by Game when a player takes a card) // ------------------------------------------------------------------------- /** * Removes a card from the top row by ID and returns it. * * @return the card, or null if no card with that ID exists in the top row */ public Card takeFromTopRow(int cardId) { return takeFromRow(topRow, cardId); } /** * Removes a card from the bottom row by ID and returns it. * * @return the card, or null if no card with that ID exists in the bottom row */ public Card takeFromBottomRow(int cardId) { return takeFromRow(bottomRow, cardId); } private Card takeFromRow(List row, int cardId) { Iterator it = row.iterator(); while (it.hasNext()) { Card c = it.next(); if (c.getCardId() == cardId) { it.remove(); return c; } } return null; } // ------------------------------------------------------------------------- // Event visibility // ------------------------------------------------------------------------- /** * Returns all EventCards currently in the bottom row, sorted by era * (ascending) so they are resolved in the correct order. * Sustainment is NOT sorted last here — that is Game's responsibility. */ public List getVisibleEvents() { List events = new ArrayList<>(); for (Card c : bottomRow) { if (c instanceof EventCard) { events.add((EventCard) c); } } events.sort((a, b) -> a.getEra().ordinal() - b.getEra().ordinal()); return events; } /** * Returns EventCards from BOTH rows. * Used during the final round when all visible events must be resolved. */ public List getAllVisibleEvents() { List events = new ArrayList<>(); for (Card c : bottomRow) { if (c instanceof EventCard) events.add((EventCard) c); } for (Card c : topRow) { if (c instanceof EventCard) events.add((EventCard) c); } events.sort((a, b) -> a.getEra().ordinal() - b.getEra().ordinal()); return events; } public int getOfferingIdxFromLetter(char letter) { List tiles = getOfferingTiles(); return IntStream.range(0, tiles.size()) .filter(i -> tiles.get(i).getLetter() == letter) .findFirst() .orElse(-1); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /** Extracts the Era from a card, returning null for building cards. */ private Era eraOf(Card c) { if (c instanceof CharacterCard) return ((CharacterCard) c).getEra(); if (c instanceof EventCard) return ((EventCard) c).getEra(); return null; } // ------------------------------------------------------------------------- // Getters // ------------------------------------------------------------------------- public Era getEra() { return era; } public List getTopRow() { return topRow; } public List getBottomRow() { return bottomRow; } public List getOfferingTiles() { return offeringTiles; } public TurnTile getTurnTile() { return turnTile; } public BuildingManager getBuildingManager() { return buildingManager; } public CardDeck getCardDeck() { return cardDeck; } @Override public String toString() { return "GameBoard{" + "era=" + era + ",\n offeringTiles=" + offeringTiles + ",\n turnTile=" + turnTile + '}'; } }