795 lines
31 KiB
Java
795 lines
31 KiB
Java
package server.automaton;
|
|
|
|
import server.*;
|
|
import server.cards.*;
|
|
import server.*;
|
|
import server.cards.*;
|
|
import server.utils.LoadingCardsException;
|
|
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
/**
|
|
* Central game controller for MESOS.
|
|
*
|
|
* Owns the full game lifecycle:
|
|
*
|
|
* SETUP → newGame()
|
|
* TOTEM_PLACEMENT → placeTotem() — one call per player, in TurnTile order
|
|
* ACTION_RESOLUTION → resolveCardAction() — one call per pending action
|
|
* EXTRA_DRAW → extraDrawAction() / skipExtraDraw() [only if a player owns the building]
|
|
* EVENT_RESOLUTION → handled automatically at end of round
|
|
* GAME_OVER → endGame()
|
|
*
|
|
* All public methods that advance state return a boolean:
|
|
* true = action accepted, state may have changed
|
|
* false = action rejected, state unchanged (caller should log / notify client)
|
|
*
|
|
* NOTE: GameState must have EXTRA_DRAW added to the enum for this class to compile:
|
|
* UNKNOWN, SETUP, TOTEM_PLACEMENT, ACTION_RESOLUTION, EXTRA_DRAW, EVENT_RESOLUTION, GAME_OVER
|
|
*
|
|
* NOTE: Tribe.endPoints() already computes end-game building bonuses internally via
|
|
* buildingAbilitiesEndPoints(). Do NOT also call BuildingManager.endgameForSix() etc.
|
|
* in endGame() — that would count those bonuses twice.
|
|
*/
|
|
public class Game {
|
|
private static final Logger logger = LogManager.getLogger(Game.class);
|
|
// -------------------------------------------------------------------------
|
|
// Constants
|
|
// -------------------------------------------------------------------------
|
|
|
|
private static final int TOTAL_ROUNDS = 10;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Core state
|
|
// -------------------------------------------------------------------------
|
|
|
|
private final List<Player> players;
|
|
private GameBoard gameBoard;
|
|
private int round;
|
|
private GameState state;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase tracking
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** How many players have placed their totem this round. */
|
|
private int totemPlacedCount;
|
|
|
|
/** Index of the offering tile (central row) currently being resolved (left to right). */
|
|
private int currentOfferingTileIndex;
|
|
|
|
/** Actions the current player still has to resolve. */
|
|
private List<Symbol> pendingActions;
|
|
|
|
/**
|
|
* Queue of players who own the EXTRA_DRAW building and still need to
|
|
* make their extra-draw decision this round. Ordered by turn order.
|
|
*/
|
|
//TODO to check it should be a single player
|
|
private final List<Player> extraDrawQueue;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Constructor
|
|
// -------------------------------------------------------------------------
|
|
|
|
public Game(List<Player> players) {
|
|
this.players = new ArrayList<>(players);
|
|
this.round = 0;
|
|
this.state = GameState.SETUP;
|
|
this.pendingActions = new ArrayList<>();
|
|
this.extraDrawQueue = new ArrayList<>();
|
|
}
|
|
|
|
// =========================================================================
|
|
// SETUP
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Initialises a new game: loads the deck, builds the board, deals initial
|
|
* food, and randomises the first turn order.
|
|
*
|
|
* Initial food (rulebook setup step 10):
|
|
* Slot 1 -> 2 food, slots 2-3 -> 3 food, slots 4-5 -> 4 food.
|
|
*/
|
|
public void newGame(InputStream csvStream) {
|
|
try {
|
|
logger.info("STARTING NEW GAME");
|
|
|
|
CardDeck deck = new CardDeck();
|
|
deck.setForNPlayer(csvStream, players.size());
|
|
|
|
gameBoard = new GameBoard(Era.I, deck, players.size());
|
|
gameBoard.initOfferingTiles(players.size());
|
|
gameBoard.setupInitialRows(players.size());
|
|
|
|
gameBoard.getTurnTile().setInitialOrder(players, true);
|
|
dealInitialFood();
|
|
|
|
round = 1;
|
|
totemPlacedCount = 0;
|
|
state = GameState.TOTEM_PLACEMENT;
|
|
|
|
logger.info("Game started! Round 1.");
|
|
logTurnOrder();
|
|
|
|
} catch (LoadingCardsException e) {
|
|
System.err.println("Fatal: could not load cards — " + e.getMessage());
|
|
state = GameState.GAME_OVER;
|
|
}
|
|
}
|
|
|
|
private void dealInitialFood() {
|
|
Player[] order = gameBoard.getTurnTile().getPositions();
|
|
for (int i = 0; i < order.length; i++) {
|
|
int food = (i == 0) ? 2 : (i <= 2) ? 3 : 4;
|
|
order[i].addFood(food);
|
|
logger.info(" " + order[i].getNickname() + " starts with " + food + " food.");
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// PHASE 1 — TOTEM PLACEMENT
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Places the player's totem on an offering tile.
|
|
*
|
|
* Players must place in the order defined by the TurnTile (top to bottom).
|
|
* If the wrong player calls this, it is rejected.
|
|
*
|
|
* @param player the player attempting to place
|
|
* @param tileIndex 0-based index into the offering tile list
|
|
* @return true if placement was accepted
|
|
*/
|
|
public boolean placeTotem(Player player, int tileIndex) {
|
|
if (state != GameState.TOTEM_PLACEMENT) {
|
|
logger.info("Error: not the totem-placement phase.");
|
|
return false;
|
|
}
|
|
|
|
// Enforce turn order
|
|
Player expected = gameBoard.getTurnTile().getPositions()[totemPlacedCount];
|
|
if (expected != player) {
|
|
logger.info("Error: it is " + expected.getNickname()
|
|
+ "'s turn to place, not " + player.getNickname() + ".");
|
|
return false;
|
|
}
|
|
|
|
List<OfferingTile> tiles = gameBoard.getOfferingTiles();
|
|
if (tileIndex < 0 || tileIndex >= tiles.size()) {
|
|
logger.info("Error: tile index " + tileIndex + " out of range.");
|
|
return false;
|
|
}
|
|
|
|
OfferingTile chosen = tiles.get(tileIndex);
|
|
if (!gameBoard.placeTotem(player, chosen, gameBoard.getTurnTile())) {
|
|
logger.info("Tile " + chosen.getLetter() + " is already occupied.");
|
|
return false;
|
|
}
|
|
|
|
totemPlacedCount++;
|
|
logger.info("player {} TOTEM PLACED ON TILE {}", player.getNickname() , chosen.getLetter());
|
|
|
|
// + " (" + totemPlacedCount + "/" + players.size() + ").");
|
|
|
|
if (totemPlacedCount == players.size()) {
|
|
logger.info("START ACTION RESOLUTION");
|
|
startActionResolution();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// PHASE 2 — ACTION RESOLUTION
|
|
// =========================================================================
|
|
|
|
private void startActionResolution() {
|
|
state = GameState.ACTION_RESOLUTION;
|
|
currentOfferingTileIndex = 0;
|
|
logger.info("All totems placed — resolving actions left to right.");
|
|
loadNextPlayerActions();
|
|
}
|
|
|
|
/**
|
|
* Scans offering tiles left to right for the next occupied one, loads that
|
|
* player's actions, and either resolves them automatically (food tile) or
|
|
* waits for player input (card tiles).
|
|
*
|
|
* Calls onAllActionsResolved() when every tile has been processed.
|
|
*/
|
|
private void loadNextPlayerActions() {
|
|
List<OfferingTile> tiles = gameBoard.getOfferingTiles();
|
|
|
|
while (currentOfferingTileIndex < tiles.size()) {
|
|
OfferingTile tile = tiles.get(currentOfferingTileIndex);
|
|
|
|
if (tile.isEmpty()) {
|
|
currentOfferingTileIndex++;
|
|
continue;
|
|
}
|
|
|
|
Player player = tile.getOccupant();
|
|
pendingActions = new ArrayList<>(tile.getActions());
|
|
logger.info(" PLAYER {} PENDING_ACTIONS {} ", player.getNickname() , pendingActions );
|
|
// --- Food tile: resolve entirely without player input ---
|
|
if (pendingActions.get(0) == Symbol.FOOD) {
|
|
|
|
int food = pendingActions.size();
|
|
player.addFood(food);
|
|
BuildingManager.bonusEndTurn(player, food); // BONUS_FOOD_ENDTURN building effect
|
|
logger.info(player.getNickname() + " receives " + food
|
|
+ " food from tile " + tile.getLetter() + ".");
|
|
pendingActions.clear();
|
|
finishPlayerTurn(player);
|
|
currentOfferingTileIndex++;
|
|
continue;
|
|
}
|
|
|
|
// --- Card tile: strip impossible actions first ---
|
|
filterImpossibleActions(player);
|
|
|
|
if (pendingActions.isEmpty()) {
|
|
// All actions were impossible (both rows empty)
|
|
finishPlayerTurn(player);
|
|
currentOfferingTileIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Wait for the player to call resolveCardAction()
|
|
logger.info("player {} pending ACTIONS {}" , player.getNickname() , pendingActions);
|
|
// logger.info("It is " + player.getNickname() + "'s turn — actions: " + pendingActions);
|
|
return;
|
|
}
|
|
|
|
// Every tile has been processed
|
|
onAllActionsResolved();
|
|
}
|
|
|
|
/**
|
|
* Strips actions that cannot be executed because the required row is empty.
|
|
* Pure filter — no side effects beyond mutating pendingActions.
|
|
*/
|
|
private void filterImpossibleActions(Player player) {
|
|
|
|
Card ctop = gameBoard.getTopRow().stream().filter(s -> s instanceof CharacterCard || s instanceof BuildingCard).findFirst().orElse(null);
|
|
Card cdown = gameBoard.getBottomRow().stream().filter(s -> s instanceof CharacterCard || s instanceof BuildingCard).findFirst().orElse(null);
|
|
|
|
boolean topEmpty = ctop == null;
|
|
boolean bottomEmpty = cdown == null;
|
|
|
|
if (topEmpty) {
|
|
long removed = pendingActions.stream().filter(s -> s == Symbol.UP).count();
|
|
pendingActions.removeIf(s -> s == Symbol.UP);
|
|
if (removed > 0)
|
|
logger.info("{} LOSES ACTION UP" ,player.getNickname(), removed);
|
|
|
|
}
|
|
if (bottomEmpty) {
|
|
long removed = pendingActions.stream().filter(s -> s == Symbol.DOWN).count();
|
|
pendingActions.removeIf(s -> s == Symbol.DOWN);
|
|
if (removed > 0)
|
|
logger.info("{} LOSES ACTION DOWN" ,player.getNickname(), removed);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves one card-pick action for the current player.
|
|
* Works for both CharacterCards and BuildingCards.
|
|
*
|
|
* For BuildingCards: builder discount is applied and the player must have
|
|
* enough food. If not, the card is put back and the call is rejected — the
|
|
* player must choose a different card.
|
|
*
|
|
* @param player the acting player (must match the current tile occupant)
|
|
* @param fromTop true = pick from the top row; false = from the bottom row
|
|
* @param cardId the ID of the card to take
|
|
* @return true if the action was accepted and consumed
|
|
*/
|
|
public ActionResult resolveCardAction(Player player, boolean fromTop, int cardId) {
|
|
if (state != GameState.ACTION_RESOLUTION) {
|
|
logger.info("Error: not in ACTION_RESOLUTION phase.");
|
|
return ActionResult.failure("Error: not in ACTION_RESOLUTION phase.");
|
|
|
|
}
|
|
|
|
OfferingTile currentTile = gameBoard.getOfferingTiles().get(currentOfferingTileIndex);
|
|
if (currentTile.getOccupant() != player) {
|
|
logger.info("Error: it is not " + player.getNickname() + "'s turn to act.");
|
|
return ActionResult.failure("Error: it is not " + player.getNickname() + "'s turn to act.");
|
|
}
|
|
|
|
Symbol required = fromTop ? Symbol.UP : Symbol.DOWN;
|
|
if (!pendingActions.contains(required)) {
|
|
logger.info("Error: no " + required + " action available for "
|
|
+ player.getNickname() + ".");
|
|
return ActionResult.failure("Error: no " + required + " action available for "
|
|
+ player.getNickname() + ".");
|
|
}
|
|
|
|
// Find the card in the correct row
|
|
Card card = fromTop
|
|
? gameBoard.takeFromTopRow(cardId)
|
|
: gameBoard.takeFromBottomRow(cardId);
|
|
|
|
if (card == null) {
|
|
logger.info("Error: card " + cardId + " not found in the "
|
|
+ (fromTop ? "top" : "bottom") + " row.");
|
|
return ActionResult.failure("Error: card " + cardId + " not found in the "
|
|
+ (fromTop ? "top" : "bottom") + " row.");
|
|
}
|
|
|
|
// Event cards can never be taken by players
|
|
if (card instanceof EventCard) {
|
|
putCardBack(card, fromTop);
|
|
logger.info("Error: Event cards cannot be taken.");
|
|
return ActionResult.failure("Error: Event cards cannot be taken.");
|
|
}
|
|
|
|
// Resolve the card
|
|
if (card instanceof BuildingCard) {
|
|
ActionResult result =resolveBuildingCard(player, (BuildingCard) card);
|
|
if (!result.isSuccess()) {
|
|
putCardBack(card, fromTop);
|
|
return result;
|
|
}
|
|
} else {
|
|
resolveCharacterCard(player, (CharacterCard) card);
|
|
}
|
|
|
|
// Consume the action
|
|
pendingActions.remove(required);
|
|
logger.info("player {} TOOK CARD {} --> PEDNING ACTIONS {}",player.getNickname(), cardId, pendingActions);
|
|
|
|
|
|
// A row may have become empty after this draw — re-check impossible actions
|
|
if (!pendingActions.isEmpty()) {
|
|
filterImpossibleActions(player);
|
|
}
|
|
|
|
// If no more actions remain, finish this player's turn
|
|
if (pendingActions.isEmpty()) {
|
|
finishPlayerTurn(player);
|
|
currentOfferingTileIndex++;
|
|
loadNextPlayerActions();
|
|
}
|
|
|
|
return ActionResult.ok();
|
|
}
|
|
|
|
/**
|
|
* Handles building card acquisition.
|
|
* Applies builder discount, checks affordability, deducts food, and
|
|
* registers the building with BuildingManager.
|
|
*
|
|
* @return true if the acquisition succeeded
|
|
*/
|
|
private ActionResult resolveBuildingCard(Player player, BuildingCard building) {
|
|
int discount = player.getPlayerTribe().buildersDiscount();
|
|
int actualCost = Math.max(0, building.getCost() - discount);
|
|
|
|
if (player.getFoodTokens() < actualCost) {
|
|
logger.info(player.getNickname() + " cannot afford building "
|
|
+ building.getCardId() + " (cost after discount: " + actualCost
|
|
+ ", has: " + player.getFoodTokens() + " food).");
|
|
return ActionResult.failure(player.getNickname() + " cannot afford building "
|
|
+ building.getCardId() + " (cost after discount: " + actualCost
|
|
+ ", has: " + player.getFoodTokens() + " food).");
|
|
}
|
|
|
|
player.removeFood(actualCost);
|
|
player.getPlayerTribe().addBuilding(building);
|
|
gameBoard.getBuildingManager().addActiveBuilding(building, player);
|
|
|
|
logger.info(player.getNickname() + " bought building "
|
|
+ building.getCardId() + " for " + actualCost + " food.");
|
|
return ActionResult.ok();
|
|
}
|
|
|
|
/**
|
|
* Handles character card acquisition and triggers all immediate effects:
|
|
*
|
|
* Hunter with leg icon (iconValue > 0):
|
|
* Take 1 food per Hunter currently in tribe (including this one).
|
|
*
|
|
* FOOD_FOR_SIX building:
|
|
* Take 6 food if this draw completes a full set of all 6 character types.
|
|
*
|
|
* FOOD_PER_INVENTORS building:
|
|
* Take 3 food if this Inventor forms a matching pair.
|
|
*
|
|
* The card is added to the tribe BEFORE effects fire so all counts include it.
|
|
*/
|
|
private void resolveCharacterCard(Player player, CharacterCard character) {
|
|
player.getPlayerTribe().addCharacter(character);
|
|
|
|
// Hunter with leg icon: immediate food reward
|
|
if (character.getCharacterType() == CharacterType.HUNTER
|
|
&& character.getIconValue() > 0) {
|
|
int food = player.getPlayerTribe().huntersNumber() * character.getIconValue();
|
|
player.addFood(food);
|
|
logger.info(player.getNickname() + "'s hunter (leg icon) grants +"
|
|
+ food + " food.");
|
|
}
|
|
|
|
// Building-triggered effects on character draw
|
|
gameBoard.getBuildingManager().foodForSix(player, character);
|
|
if (character.getCharacterType() == CharacterType.INVENTOR) {
|
|
gameBoard.getBuildingManager().foodPerInventors(player, character);
|
|
}
|
|
|
|
logger.info(player.getNickname() + " drew "
|
|
+ character.getCharacterType() + " (id " + character.getCardId() + ").");
|
|
}
|
|
|
|
/**
|
|
* Marks the current player as done with their tile actions.
|
|
* Calls TurnTile.returnTotem(), which places them in the next available
|
|
* slot and immediately applies the position food reward or penalty.
|
|
*/
|
|
private void finishPlayerTurn(Player player) {
|
|
int slot = gameBoard.getTurnTile().returnTotem(player);
|
|
OfferingTile off = gameBoard.getOfferingTile(player);
|
|
logger.info("player {} leaving offering {} " ,player.getNickname(), off);
|
|
if(off!=null) off.setOccupant(null);
|
|
logger.info("player {} left offering {} slot {} " ,player.getNickname(), off, slot);
|
|
|
|
}
|
|
|
|
// =========================================================================
|
|
// PHASE 2b — EXTRA DRAW (EXTRA_DRAW building effect)
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Called when every offering tile has been resolved.
|
|
* Checks if any player owns the EXTRA_DRAW building and, if so, enters
|
|
* the EXTRA_DRAW phase. Otherwise proceeds directly to end of round.
|
|
*
|
|
* Rulebook appendix: "After resolving all actions (once all totems are back
|
|
* on the TurnTile) and before the end-of-round phase, you may take 1
|
|
* Character or Building card (paying its cost) from the top row."
|
|
*/
|
|
private void onAllActionsResolved() {
|
|
logger.info("All actions resolved.");
|
|
|
|
extraDrawQueue.clear();
|
|
// Iterate in turn order so extra draws happen in a consistent sequence
|
|
for (Player p : gameBoard.getTurnTile().getPositions()) {
|
|
if (p != null && gameBoard.getBuildingManager().extraDraw(p)) {
|
|
extraDrawQueue.add(p);
|
|
}
|
|
}
|
|
|
|
if (!extraDrawQueue.isEmpty()) {
|
|
state = GameState.EXTRA_DRAW;
|
|
System.out.println("Extra draw phase: "
|
|
+ extraDrawQueue.get(0).getNickname() + " may draw first.");
|
|
} else {
|
|
endRound();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The player uses their EXTRA_DRAW building to take one card from the top row.
|
|
* Building cards may also be taken, paying the cost after builder discounts.
|
|
*
|
|
* @param player the player taking the extra draw
|
|
* @param cardId the ID of the card they want from the top row
|
|
* @return true if the action was accepted
|
|
*/
|
|
public ActionResult extraDrawAction(Player player, int cardId) {
|
|
if (state != GameState.EXTRA_DRAW) {
|
|
System.out.println("Error: not in EXTRA_DRAW phase.");
|
|
return ActionResult.failure("Error: not in EXTRA_DRAW phase.");
|
|
}
|
|
if (extraDrawQueue.isEmpty() || extraDrawQueue.get(0) != player) {
|
|
System.out.println("Error: it is not " + player.getNickname()
|
|
+ "'s extra draw turn.");
|
|
return ActionResult.failure("Error: it is not " + player.getNickname()
|
|
+ "'s extra draw turn.");
|
|
}
|
|
|
|
Card card = gameBoard.takeFromTopRow(cardId);
|
|
if (card == null) {
|
|
System.out.println("Error: card " + cardId + " not found in top row.");
|
|
return ActionResult.failure("Error: card " + cardId + " not found in top row.");
|
|
}
|
|
if (card instanceof EventCard) {
|
|
gameBoard.getTopRow().add(card);
|
|
System.out.println("Error: cannot take Event cards.");
|
|
return ActionResult.failure("Error: cannot take Event cards.");
|
|
}
|
|
|
|
if (card instanceof BuildingCard) {
|
|
ActionResult result = resolveBuildingCard(player, (BuildingCard) card);
|
|
if (!result.isSuccess()) {
|
|
gameBoard.getTopRow().add(card);
|
|
return result;
|
|
}
|
|
} else {
|
|
resolveCharacterCard(player, (CharacterCard) card);
|
|
}
|
|
|
|
advanceExtraDrawQueue();
|
|
return ActionResult.ok();
|
|
}
|
|
|
|
/**
|
|
* The player declines to use their EXTRA_DRAW building this round.
|
|
*
|
|
* @return true if accepted
|
|
*/
|
|
public boolean skipExtraDraw(Player player) {
|
|
if (state != GameState.EXTRA_DRAW) return false;
|
|
if (extraDrawQueue.isEmpty() || extraDrawQueue.get(0) != player) return false;
|
|
|
|
System.out.println(player.getNickname() + " skips extra draw.");
|
|
advanceExtraDrawQueue();
|
|
return true;
|
|
}
|
|
|
|
private void advanceExtraDrawQueue() {
|
|
extraDrawQueue.remove(0);
|
|
if (extraDrawQueue.isEmpty()) {
|
|
endRound();
|
|
} else {
|
|
System.out.println("Next extra draw: " + extraDrawQueue.get(0).getNickname());
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// END OF ROUND
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Runs all end-of-round steps in rulebook order:
|
|
*
|
|
* Step 1 — Resolve Event cards.
|
|
* Normal rounds: only bottom row events.
|
|
* Round 10: events from BOTH rows; then GAME_OVER.
|
|
* Step 2 — Advance rows: discard bottom non-buildings, shift top
|
|
* non-buildings down, draw (numPlayers + 4) new cards.
|
|
* Step 3 — If a new Era card was revealed in step 2, trigger
|
|
* the Era transition (building row swap).
|
|
* Step 4 — Start the next round.
|
|
*/
|
|
private void endRound() {
|
|
state = GameState.EVENT_RESOLUTION;
|
|
//notify( GameState.EVENT_RESOLUTION);
|
|
|
|
logger.info("--- End of round " + round + " ---");
|
|
|
|
boolean isFinalRound = (round == TOTAL_ROUNDS);
|
|
|
|
// Step 1: resolve events
|
|
resolveVisibleEvents(isFinalRound);
|
|
|
|
if (isFinalRound) {
|
|
state = GameState.GAME_OVER;
|
|
notify( GameState.GAME_OVER, endGame());
|
|
logger.info("Round 10 complete — game over!");
|
|
return;
|
|
}
|
|
|
|
// Step 2: advance rows; returns true if a new Era card was revealed
|
|
boolean eraChangeTriggered = gameBoard.advanceRows(players.size());
|
|
|
|
// Step 3: handle Era transition if needed
|
|
if (eraChangeTriggered) {
|
|
gameBoard.triggerEraChange(players.size());
|
|
}
|
|
|
|
// Step 4: start next round
|
|
startNewRound();
|
|
}
|
|
|
|
/**
|
|
* Resolves all visible Event cards in the correct order.
|
|
*
|
|
* Rules:
|
|
* - Multiple events of different types: any order, Sustainment always last.
|
|
* - Two events of the same type in the same round: resolve in Era order.
|
|
* - Round 10: events from both rows must be resolved.
|
|
*
|
|
* @param bothRows true means events from both rows are included (round 10)
|
|
*/
|
|
private void resolveVisibleEvents(boolean bothRows) {
|
|
List<EventCard> events = bothRows
|
|
? gameBoard.getAllVisibleEvents() // already sorted by Era ordinal
|
|
: gameBoard.getVisibleEvents(); // already sorted by Era ordinal
|
|
|
|
if (events.isEmpty()) {
|
|
logger.info("No events to resolve this round.");
|
|
return;
|
|
}
|
|
|
|
// Sustainment must always be resolved last
|
|
List<EventCard> others = new ArrayList<>();
|
|
List<EventCard> sustainments = new ArrayList<>();
|
|
for (EventCard e : events) {
|
|
if (e.getEvent() == Event.SUSTAINMENT) sustainments.add(e);
|
|
else others.add(e);
|
|
}
|
|
|
|
for (EventCard e : others) resolveOneEvent(e);
|
|
for (EventCard e : sustainments) resolveOneEvent(e);
|
|
}
|
|
|
|
private void resolveOneEvent(EventCard event) {
|
|
logger.info(">>"+gameBoard.getTurnTile());
|
|
notify( GameState.EVENT_RESOLUTION, event.toString());
|
|
logger.info("SOLVE EVENT: " + event.getEvent() +" - " +event.getCardId() +" (Era " + event.getEra() + ")");
|
|
EventsSolver.solveEvents(Collections.singletonList(event), players);
|
|
logger.info("<<"+gameBoard.getTurnTile());
|
|
}
|
|
|
|
private void startNewRound() {
|
|
|
|
round++;
|
|
|
|
notify( GameState.NEXT_ROUND, "Round="+ round);
|
|
logger.info("startNewRound: {}", round );
|
|
totemPlacedCount = 0;
|
|
currentOfferingTileIndex = 0;
|
|
pendingActions.clear();
|
|
|
|
gameBoard.clearOfferingTiles();
|
|
gameBoard.getTurnTile().resetTrack();
|
|
|
|
// notify( GameState.TOTEM_PLACEMENT);
|
|
state = GameState.TOTEM_PLACEMENT;
|
|
logger.info("=== NEW Round " + round + " ===");
|
|
logTurnOrder();
|
|
}
|
|
|
|
// =========================================================================
|
|
// END GAME
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Computes the final leaderboard and returns it as a formatted string.
|
|
*
|
|
* Final score = prestige points accumulated during play
|
|
* + Tribe.endPoints() which covers: builder PP, inventor PP,
|
|
* artist PP, building base PP, and building end-game abilities.
|
|
*
|
|
* IMPORTANT: do NOT call BuildingManager.endgameForSix() /
|
|
* endgameBonusCharacter() / endgameBonusPoints() here.
|
|
* Tribe.endPoints() already computes those bonuses. Calling both would
|
|
* count them twice.
|
|
*
|
|
* Tie-breaking rule: most food wins. Further ties share the victory.
|
|
*/
|
|
public String endGame() {
|
|
state = GameState.GAME_OVER;
|
|
|
|
List<Player> ranked = new ArrayList<>(players);
|
|
ranked.sort((a, b) -> {
|
|
int diff = totalScore(b) - totalScore(a);
|
|
if (diff != 0) return diff;
|
|
return b.getFoodTokens() - a.getFoodTokens();
|
|
});
|
|
|
|
StringBuilder sb = new StringBuilder("=== FINAL LEADERBOARD ===\n");
|
|
for (int i = 0; i < ranked.size(); i++) {
|
|
Player p = ranked.get(i);
|
|
sb.append(i + 1).append(". ")
|
|
.append(p.getNickname())
|
|
.append(" — ").append(totalScore(p)).append(" PP")
|
|
.append(" (food: ").append(p.getFoodTokens()).append(")\n");
|
|
}
|
|
|
|
Player winner = ranked.get(0);
|
|
sb.append("\nWINNER: ")
|
|
.append(winner.getNickname().toUpperCase())
|
|
.append(" with ").append(totalScore(winner)).append(" PP!");
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
/** Prestige points accumulated during play + final scoring from the Tribe engine. */
|
|
private int totalScore(Player p) {
|
|
return p.getPrestigePoints() + p.getPlayerTribe().endPoints();
|
|
}
|
|
|
|
// =========================================================================
|
|
// HELPERS
|
|
// =========================================================================
|
|
|
|
private void putCardBack(Card card, boolean wasTopRow) {
|
|
if (wasTopRow) gameBoard.getTopRow().add(card);
|
|
else gameBoard.getBottomRow().add(card);
|
|
}
|
|
|
|
private void logTurnOrder() {
|
|
Player[] order = gameBoard.getTurnTile().getPositions();
|
|
StringBuilder sb = new StringBuilder("Turn order: ");
|
|
for (int i = 0; i < order.length; i++) {
|
|
if (order[i] != null)
|
|
sb.append(i + 1).append(". ").append(order[i].getNickname()).append(" ");
|
|
}
|
|
logger.info("logTurnOrder: " + sb);
|
|
}
|
|
|
|
// =========================================================================
|
|
// GETTERS
|
|
// =========================================================================
|
|
|
|
public List<Player> getPlayers() { return Collections.unmodifiableList(players); }
|
|
public GameBoard getGameBoard() { return gameBoard; }
|
|
public int getRound() { return round; }
|
|
public GameState getState() { return state; }
|
|
public void setState(GameState s) { this.state = s; }
|
|
public List<Symbol> getPendingActions() { return Collections.unmodifiableList(pendingActions); }
|
|
|
|
/**
|
|
* Returns the player currently expected to act:
|
|
* TOTEM_PLACEMENT — the next player who should place their totem
|
|
* ACTION_RESOLUTION — the player currently resolving tile actions
|
|
* EXTRA_DRAW — the next player to make their extra-draw decision
|
|
*/
|
|
public Player getCurrentPlayer() {
|
|
switch (state) {
|
|
case TOTEM_PLACEMENT:
|
|
if (totemPlacedCount < players.size())
|
|
return gameBoard.getTurnTile().getPositions()[totemPlacedCount];
|
|
return null;
|
|
case ACTION_RESOLUTION:
|
|
List<OfferingTile> tiles = gameBoard.getOfferingTiles();
|
|
if (currentOfferingTileIndex < tiles.size())
|
|
return tiles.get(currentOfferingTileIndex).getOccupant();
|
|
return null;
|
|
case EXTRA_DRAW:
|
|
return extraDrawQueue.isEmpty() ? null : extraDrawQueue.get(0);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Game{" +
|
|
" state=" + state +
|
|
",\n round=" + round +
|
|
",\n totemPlacedCount=" + totemPlacedCount +
|
|
",\n currentOfferingTileIndex=" + currentOfferingTileIndex +
|
|
",\n pendingActions=" + pendingActions +
|
|
",\n extraDrawQueue=" + extraDrawQueue +
|
|
" \n GameBoard=" + gameBoard +
|
|
'}';
|
|
}
|
|
|
|
|
|
public record GameEventNotification(GameState event, String message) {}
|
|
|
|
public interface GameEventListener {
|
|
void onEvent(GameEventNotification notification);
|
|
}
|
|
|
|
// Campo privato
|
|
private GameEventListener eventListener;
|
|
|
|
// Setter per il controller FX
|
|
public void setEventListener(GameEventListener listener) {
|
|
this.eventListener = listener;
|
|
}
|
|
|
|
// Metodi privati di notifica
|
|
private void notify(GameState event, String message) {
|
|
if (eventListener != null)
|
|
eventListener.onEvent(new GameEventNotification(event, message));
|
|
}
|
|
|
|
private void notify(GameState event) {
|
|
notify(event, null);
|
|
}
|
|
|
|
|
|
}
|