402 lines
15 KiB
Java
402 lines
15 KiB
Java
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<Card> topRow;
|
|
private final List<Card> bottomRow;
|
|
private final List<OfferingTile> 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<Card> 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<Card> 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<Card> 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<BuildingCard> descending = new ArrayList<>();
|
|
Iterator<Card> 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<Card> 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<Card> row, int cardId) {
|
|
Iterator<Card> 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<EventCard> getVisibleEvents() {
|
|
List<EventCard> 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<EventCard> getAllVisibleEvents() {
|
|
List<EventCard> 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<OfferingTile> 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<Card> getTopRow() { return topRow; }
|
|
public List<Card> getBottomRow() { return bottomRow; }
|
|
public List<OfferingTile> 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 +
|
|
'}';
|
|
}
|
|
} |