Initial commit of Mesos Java project

This commit is contained in:
2026-04-13 09:46:34 +02:00
commit f9d40590f8
76 changed files with 6785 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package Server.Automaton;
public class ActionResult {
private final boolean success;
private final String errorMessage; // null se success
private ActionResult(boolean success, String errorMessage) {
this.success = success;
this.errorMessage = errorMessage;
}
public static ActionResult ok() {
return new ActionResult(true, null);
}
public static ActionResult failure(String message) {
return new ActionResult(false, message);
}
public boolean isSuccess() { return success; }
public String getErrorMessage(){ return errorMessage; }
}

View File

@@ -0,0 +1,53 @@
package Server.Automaton;
public class Automaton {
private GameState state = GameState.SETUP;
public GameState getState(){
return state;
}
private Boolean canEvolve(int val){
int currOrd = state.ordinal();
int setupOrd = GameState.SETUP.ordinal();
if (currOrd == setupOrd && val > 5) {
System.out.println("Invalid number of player");
return false;
}
return true;
}
public Boolean evolve(int i) {
if (!canEvolve(i))
return false;
int currOrd = state.ordinal();
int lastOrd = GameState.GAME_OVER.ordinal();
if (currOrd < lastOrd) {
state = state.next(i);
return true;
}
return false;
}
public Boolean evolveTo(GameState toState, int i){
if (!canEvolve(i))
return false;
int toOrd = toState.ordinal();
int currOrd = state.ordinal();
if (toOrd>currOrd) {
state = toState;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,789 @@
package Server.Automaton;
import Server.*;
import Server.Cards.*;
import Server.Utils.LoadingCardsException;
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(String cardsFilePath) {
try {
logger.info("STARTING NEW GAME : ", cardsFilePath);
CardDeck deck = new CardDeck();
deck.setForNPlayer(cardsFilePath, players.size());
gameBoard = new GameBoard(Era.I, deck, players.size());
gameBoard.initOfferingTiles(players.size());
gameBoard.setupInitialRows(players.size());
gameBoard.getTurnTile().setInitialOrder(players);
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) {
notify( GameState.EVENT_RESOLUTION, event.toString());
logger.info("EVENT Resolving: " + event.getEvent() + " (Era " + event.getEra() + ")");
EventsSolver.solveEvents(Collections.singletonList(event), players);
}
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);
}
}

View File

@@ -0,0 +1,68 @@
package Server.Automaton;
public enum GameState implements Comparable<GameState> {
UNKNOWN,
SETUP,
NEXT_ROUND,
TOTEM_PLACEMENT,
ACTION_RESOLUTION,
END_ACTION,
EXTRA_DRAW,
EVENT_RESOLUTION,
GAME_OVER;
GameState next(int turn){
switch (this) {
case SETUP:
return SETUP;
case NEXT_ROUND:
return NEXT_ROUND;
case TOTEM_PLACEMENT:
return ACTION_RESOLUTION;
case ACTION_RESOLUTION:
return END_ACTION;
case EXTRA_DRAW:
return EXTRA_DRAW;
case END_ACTION:
if(turn != 10) return GAME_OVER;
return TOTEM_PLACEMENT;
default: return UNKNOWN;
}
}
static GameState fromString(String s){
return switch (s.toUpperCase().charAt(0)) {
case 'S' -> SETUP;
case 'T' -> TOTEM_PLACEMENT;
case 'A' -> ACTION_RESOLUTION;
case 'E' -> END_ACTION;
case 'X' -> EXTRA_DRAW;
case 'G' -> GAME_OVER;
case 'N' -> NEXT_ROUND;
default -> UNKNOWN;
};
}
@Override
public String toString() {
switch (this) {
case SETUP:
return "SETUP";
case TOTEM_PLACEMENT:
return "TOTEM_PLACEMENT";
case ACTION_RESOLUTION:
return "ACTION_RESOLUTION";
case END_ACTION:
return "END_ACTION";
case EVENT_RESOLUTION:
return "EVENT_RESOLUTION";
case NEXT_ROUND:
return "NEXT_ROUND";
case EXTRA_DRAW:
return "EXTRA_DRAW";
case GAME_OVER:
return "GAME_OVER";
default: return "UNKNOWN";
}
}
}

View File

@@ -0,0 +1,214 @@
package Server;
import Server.Cards.BuildingCard;
import Server.Cards.CharacterCard;
import Server.Cards.CharacterType;
import Server.Cards.Trigger;
import Server.Utils.EventsManagerException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BuildingManager {
private static List<BuildingCard> activeBuildings;
public BuildingManager() {
activeBuildings = new ArrayList<>();
}
public void addActiveBuilding (BuildingCard buildingCard, Player player) {
buildingCard.setOwner(player);
activeBuildings.add(buildingCard);
}
public static BuildingCard playerHasBuilding(Player player, Trigger trigger) {
for (BuildingCard buildingCard : activeBuildings) {
if(buildingCard.getAbilityTrigger() == trigger && buildingCard.getOwner() == player) {
return buildingCard;
}
}
return null;
}
public static int numberOfTargets(Player player, List<BuildingCard> buildingCards) {
int numCard = 0;
for (BuildingCard buildingCard : buildingCards) {
List<CharacterCard> playerCharacters = player.getPlayerTribe().getCharacters();
for (CharacterCard card : playerCharacters) {
if (card.getCharacterType() == buildingCard.getEffectTarget()) {
numCard++;
}
}
}
return numCard;
}
public static List<BuildingCard> playerHasVariousBuildings(Player player, Trigger trigger) {
List<BuildingCard> buildingCards = new ArrayList<>();
for (BuildingCard buildingCard : activeBuildings) {
if(buildingCard.getAbilityTrigger() == trigger && buildingCard.getOwner() == player) {
buildingCards.add(buildingCard);
}
}
return buildingCards;
}
//When a player draw a characterCard, if said card complete a set of 6
//different character type, give the player 6 food
public static void foodForSix(Player player, CharacterCard characterCard) {
if(playerHasBuilding(player, Trigger.FOOD_FOR_SIX) == null){
return;
}
List<CharacterCard> playerTribe = player.getPlayerTribe().getCharacters();
int [] preDraw = new int[CharacterType.values().length];
int [] postDraw = new int[CharacterType.values().length];
for(CharacterCard card: playerTribe){
preDraw[card.getCharacterType().ordinal()]++;
postDraw[card.getCharacterType().ordinal()]++;
}
postDraw[characterCard.getCharacterType().ordinal()]++;
int minPre = Arrays.stream(preDraw).min().getAsInt();
int minPost = Arrays.stream(postDraw).min().getAsInt();
if(minPre < minPost){
player.addFood(6);
}
}
//the method return the discount received for the player based on the cards he owns
//the card providing the discount are indicated on the BuildingCard
public static int sustainDiscount(Player player) {
List<BuildingCard> buildingCards = playerHasVariousBuildings(player, Trigger.SUSTAIN_DISCOUNT);
if(buildingCards.isEmpty()){
return 0;
}
return numberOfTargets(player, buildingCards);
}
//if the player is going to lose during the shamanicRitual, the loss are nullif
public static boolean shamanNoLoss(Player player) {
return playerHasBuilding(player, Trigger.SHAMAN_NO_LOSS) != null;
}
//check if the player has a SHAMAN_DOUBLE_POINTS card
public static boolean shamanDoublePoints(Player player) {
return playerHasBuilding(player, Trigger.SHAMAN_DOUBLE_POINTS) != null;
}
//if the player has a BONUS_END_TURN buildingCard the player gain additional food
//after placing his totem on a tile whit food
public static int bonusEndTurn(Player player, int positionReward) {
if (playerHasBuilding(player, Trigger.BONUS_FOOD_ENDTURN) == null) return -1;
if (positionReward >= 1){
player.addFood(1);
return 1;
}
return 0;
}
//if the player has a BuildingCard whit FOOD_PER_INVENTORS, and he
//draws a copy of an inventor that is already in his tribe he gains 3 food
public static void foodPerInventors(Player player, CharacterCard characterCard) {
if(playerHasBuilding(player, Trigger.FOOD_PER_INVENTORS) == null){
return;
}
if(characterCard.getCharacterType()!= CharacterType.INVENTOR){
throw new EventsManagerException("Invalid character type");
}
List<CharacterCard> playerTribe = player.getPlayerTribe().getCharacters();
int numberOfInventors = 0;
for(CharacterCard card: playerTribe){
if(card.getIconValue() == characterCard.getIconValue() && card.getCharacterType() == CharacterType.INVENTOR){
numberOfInventors++;
}
}
if(numberOfInventors%2 == 1)
player.addFood(3);
}
//if the player has a SHAMAN_BONUS building the function return 3, else return 0
public static int shamanBonus(Player player) {
if (BuildingManager.playerHasBuilding(player, Trigger.SHAMAN_BONUS) == null) return 0;
return 3;
}
//return if HUNT_BONUS is active for the player
public static boolean hunterBonus(Player player) {
return playerHasBuilding(player, Trigger.HUNT_BONUS) != null;
}
//return if ENDGAME_BUILDER_BONUS is active for the player
public static boolean endgameBuilderBonus(Player player) {
return playerHasBuilding(player, Trigger.ENDGAME_BUILDER_BONUS) != null;
}
//return the amount of food gained if the player has a BuildingCard with
//PAINTING_FOOD_BONUS, a unit for each ARTIST in his tribe
public static void paintingFoodBonus(Player player) {
if(playerHasBuilding(player, Trigger.PAINTING_FOOD_BONUS) == null) return;
player.addFood(player.getPlayerTribe().artistsNumber());
}
//return the amount of buildingPoint if the player has a BuildingCard with
//ENDGAME_FOR_SIX, a unit for each set of 6 character of different Type
public static void endgameForSix(Player player) {
if(playerHasBuilding(player, Trigger.ENDGAME_FOR_SIX) == null) return;
List<CharacterCard> playerTribe = player.getPlayerTribe().getCharacters();
int [] typeDuplicates = new int[CharacterType.values().length];
for(CharacterCard card: playerTribe){
typeDuplicates[card.getCharacterType().ordinal()]++;
}
int minCharacterType = Arrays.stream(typeDuplicates).min().getAsInt();
player.addPrestigePoints(minCharacterType*6);
}
//add the bonus prestigePoints due to ENDGAME_BONUS_CHARACTER, the amount is based
//on the CharacterType indicated on the BuildingCards, 3 for each character of the same type
//A plater can own different BuildingCards with the same effect
public static void endgameBonusCharacter(Player player) {
List<BuildingCard> buildingCards = playerHasVariousBuildings(player, Trigger.ENDGAME_BONUS_CHARACTER);
if(buildingCards.isEmpty()){
return;
}
player.addPrestigePoints(numberOfTargets(player, buildingCards)* 3);
}
//return if EXTRA_DRAW is active for the player
public static boolean extraDraw(Player player) {
return playerHasBuilding(player, Trigger.EXTRA_DRAW) != null;
}
//return if the player has a BuildingCard with
//ENDGAME_BONUS_POINTS, then add 25 points if he has
public static void endgameBonusPoints(Player player) {
if(playerHasBuilding(player, Trigger.ENDGAME_BONUS_POINTS) == null) return;
player.addPrestigePoints(25);
}
}

View File

@@ -0,0 +1,48 @@
package Server;
import java.util.*;
public class BuildingRules {
private static final Map<Integer, Map<Era, Integer>> RULES = new HashMap<>();
static {
// 2 players
Map<Era, Integer> p2 = new EnumMap<>(Era.class);
p2.put(Era.I, 1);
p2.put(Era.II, 2);
p2.put(Era.III, 3);
// 3 players
Map<Era, Integer> p3 = new EnumMap<>(Era.class);
p3.put(Era.I, 2);
p3.put(Era.II, 2);
p3.put(Era.III, 4);
// 4 players
Map<Era, Integer> p4 = new EnumMap<>(Era.class);
p4.put(Era.I, 2);
p4.put(Era.II, 3);
p4.put(Era.III, 4);
// 5 players
Map<Era, Integer> p5 = new EnumMap<>(Era.class);
p4.put(Era.I, 2);
p4.put(Era.II, 3);
p4.put(Era.III, 5);
// insert into main map
RULES.put(2, p2);
RULES.put(3, p3);
RULES.put(4, p4);
RULES.put(5, p5);
}
public static int getBuildingCards(int players, Era era) {
Map<Era, Integer> eraMap = RULES.get(players);
if (eraMap!=null){
Integer value = eraMap.get(era);
if (value!=null) return value;
}
return 0;
}
}

View File

@@ -0,0 +1,84 @@
package Server.Cards;
import Server.Era;
import Server.Player;
public class BuildingCard extends Card{
private final int cost;
private final int endPP;
private final Trigger abilityTrigger;
private final CharacterType effectTarget;
private Player owner;
public BuildingCard(int cardId, int forMinPlayer, Era era, int cost, int endPP, Trigger abilityTrigger,CharacterType characterType, Player owner) {
super(cardId, forMinPlayer, era);
this.cost = cost;
this.endPP = endPP;
this.abilityTrigger = abilityTrigger;
this.effectTarget = characterType;
this.owner = owner;
}
public BuildingCard(int cardId, int forMinPlayer, Era era, int cost, int endPP, Trigger abilityTrigger, CharacterType characterType) {
super(cardId, forMinPlayer, era);
this.cost = cost;
this.endPP = endPP;
this.abilityTrigger = abilityTrigger;
this.effectTarget = characterType;
this.owner = null;
}
public BuildingCard(int cardId, int forMinPlayer, Era era, int cost, int endPP, Trigger abilityTrigger) {
super(cardId, forMinPlayer, era);
this.cost = cost;
this.endPP = endPP;
this.abilityTrigger = abilityTrigger;
this.effectTarget = null;
this.owner = null;
}
public void setOwner(Player owner) {
this.owner = owner;
}
public int getCost() {
return cost;
}
public int getEndPP() {
return endPP;
}
public Trigger getAbilityTrigger() {
return abilityTrigger;
}
public Player getOwner() {
return owner;
}
public CharacterType getEffectTarget() {
return effectTarget;
}
public static BuildingCard parsRow(String row) {
String cleanRow = row.trim();
String[] values = cleanRow.split(";");
int cardId = Integer.parseInt(values[1]);
int forMinPlayer = Integer.parseInt(values[2]);
Era era = Era.valueOf(values[3]);
int cost = Integer.parseInt(values[4]);
int endPP = Integer.parseInt(values[5]);
Trigger abilityTrigger = Trigger.valueOf(values[6]);
/* MANCA LA COLONNA 8 nel CSV
if(abilityTrigger == Trigger.SUSTAIN_DISCOUNT || abilityTrigger == Trigger.ENDGAME_BONUS_CHARACTER){
CharacterType effectTarget = CharacterType.valueOf(values[7]);
return new BuildingCard(cardId, forMinPlayer, era, cost, endPP, abilityTrigger, effectTarget);
}
*/
return new BuildingCard(cardId, forMinPlayer, era, cost, endPP, abilityTrigger);
}
}

View File

@@ -0,0 +1,35 @@
package Server.Cards;
import Server.Era;
public abstract class Card {
private final int cardId;
private final int forMinPlayer;
private final Era era;
public Card(int cardId, int forMinPlayer, Era era) {
this.cardId = cardId;
this.forMinPlayer = forMinPlayer;
this.era = era;
}
public int getCardId() {
return cardId;
}
public Era getEra() {
return era;
}
public int getForMinPlayer() {
return forMinPlayer;
}
@Override
public String toString() {
return "Card{" +
"cardId=" + cardId +
", era=" + era +
'}';
}
}

View File

@@ -0,0 +1,131 @@
package Server.Cards;
import Server.Automaton.Game;
import Server.Era;
import Server.Utils.LoadingCardsException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
public class CardDeck {
private List<Card> tribeDeck;
private Map<Era, List<Card>> buildingDeck;
private static final Logger logger = LogManager.getLogger(CardDeck.class);
public List<Card> getTribeDeck() {
return tribeDeck;
}
public List<Card> getBuildingDeck(Era era) {
return buildingDeck.get(era);
}
public void setForNPlayer(String path, int n) throws LoadingCardsException {
List<Card> tribe = new ArrayList<>();
List<Card> building = new ArrayList<>();
try {
List<String> rows = Files.readAllLines(Path.of(path));
int p=0;
for (String row : rows) {
String cleanRow = row.trim();
String[] fields = cleanRow.split(";");
logger.info((p++) + " ROW " +row);
switch (fields[0]) {
case "C":
tribe.add(CharacterCard.parsRow(row));
break;
case "E":
tribe.add(EventCard.parsRow(row));
break;
case "B":
building.add(BuildingCard.parsRow(row));
break;
default:
throw new LoadingCardsException("Content not supported");
}
}
} catch (IOException e) {
logger.error(e);
throw new LoadingCardsException("file not found");
}
building = CardDeck.shuffle(building);
this.tribeDeck = new ArrayList<>();
for(Card card : tribe) {
if (card.getForMinPlayer() <= n)
this.tribeDeck.add(card);
}
this.tribeDeck = CardDeck.shuffle(this.tribeDeck);
// groups the building cards by era
this.buildingDeck = building.stream().collect(Collectors.groupingBy(Card::getEra));
}
public List<Card> drawTribe(int n) {
List<Card> cards = new ArrayList<>();
for (int i = 0; i < n; i++) {
if(tribeDeck.isEmpty()) break;
cards.add(tribeDeck.getFirst());
tribeDeck.remove(tribeDeck.getFirst());
}
return cards;
}
public List<Card> drawBuilding(int n, Era era) {
List<Card> cards = new ArrayList<>();
for (int i = 0; i < n; i++) {
if(buildingDeck.isEmpty()) break;
Card card = buildingDeck.get(era).getFirst();
cards.add(card);
buildingDeck.remove(card);
}
return cards;
}
public static List<Card> shuffle(List<Card> cards) {
List<Card> shuffled = new ArrayList<>();
for(Era e: Era.values()) {
List<Card> cardsToShuffle = new ArrayList<>();
for(Card card : cards) {
if(card.getEra() == e){
cardsToShuffle.add(card);
}
}
Collections.shuffle(cardsToShuffle);
shuffled.addAll(cardsToShuffle);
}
return shuffled;
}
public Card getFirstCardForCover() {
if(tribeDeck.isEmpty()) return null;
Card card = tribeDeck.getFirst();
return card;
}
public Card drawTribeOne() {
if(tribeDeck.isEmpty()) return null;
Card card = tribeDeck.getFirst();
tribeDeck.remove(card);
return card;
}
public Card drawBuildingOne(Era era) {
if(buildingDeck.isEmpty()) return null;
Card card = buildingDeck.get(era).getFirst();
buildingDeck.get(era).remove(card);
return card;
}
}

View File

@@ -0,0 +1,58 @@
package Server.Cards;
import Server.Era;
import Server.Utils.LoadingCardsException;
public class CharacterCard extends Card{
private final CharacterType characterType;
private final int iconValue;
private final int prestigePoints;
public CharacterCard(int cardId, int forMinPlayer, Era era, CharacterType characterType, int iconValue, int prestigePoints) {
super(cardId, forMinPlayer, era);
this.characterType = characterType;
this.iconValue = iconValue;
this.prestigePoints = prestigePoints;
}
public CharacterType getCharacterType() {
return characterType;
}
public int getIconValue() {
return iconValue;
}
public int getPrestigePoints() {
return prestigePoints;
}
public static CharacterCard parsRow(String row){
String cleanRow = row.trim();
String[] values = cleanRow.split(";");
if(!values[0].equals("C")){
throw new LoadingCardsException("Not a character card");
}
int cardId = Integer.parseInt(values[1]);
int forMinPlayer = Integer.parseInt(values[2]);
Era era = Era.valueOf(values[3]);
CharacterType characterType = CharacterType.valueOf(values[4]);
int iconValue = Integer.parseInt(values[5]);
int prestigePoints = Integer.parseInt(values[6]);
return new CharacterCard(cardId, forMinPlayer, era, characterType, iconValue, prestigePoints);
}
@Override
public String toString() {
return "CharacterCard{" +
"characterType=" + characterType +
", value=" + iconValue +
", points=" + prestigePoints +
'}';
}
}

View File

@@ -0,0 +1,10 @@
package Server.Cards;
public enum CharacterType {
INVENTOR,
HUNTER,
GATHERER,
SHAMAN,
ARTIST,
BUILDER
}

View File

@@ -0,0 +1,8 @@
package Server.Cards;
public enum Event {
SUSTAINMENT,
HUNT,
SHAMANIC_RITUAL,
CAVE_PAINTINGS
}

View File

@@ -0,0 +1,55 @@
package Server.Cards;
import Server.Era;
import Server.Utils.LoadingCardsException;
public class EventCard extends Card {
private final Event event;
private final int firstValue;
private final int secondValue;
public EventCard(int cardId, int forMinPlayer, Era era, Event event, int firstValue, int secondValue) {
super(cardId, forMinPlayer, era);
this.event = event;
this.firstValue = firstValue;
this.secondValue = secondValue;
}
public Event getEvent() {
return event;
}
public int getFirstValue() {
return firstValue;
}
public int getSecondValue() {
return secondValue;
}
public static EventCard parsRow(String row){
String cleanRow = row.trim();
String[] values = cleanRow.split(";");
if (!values[0].equals("E")) {
throw new LoadingCardsException("Not an EventCard");
}
int cardId = Integer.parseInt(values[1]);
int forMinPlayer = Integer.parseInt(values[2]);
Era era = Era.valueOf(values[3]);
Event event = Event.valueOf(values[4]);
int firstValue = Integer.parseInt(values[5]);
int secondValue = Integer.parseInt(values[6]);
return new EventCard(cardId, forMinPlayer, era, event, firstValue, secondValue);
}
@Override
public String toString() {
return "EventCard{" +
"event=" + event +
", firstValue=" + firstValue +
", secondValue=" + secondValue +
'}';
}
}

View File

@@ -0,0 +1,18 @@
package Server.Cards;
public enum Trigger {
FOOD_FOR_SIX,
SUSTAIN_DISCOUNT,
SHAMAN_NO_LOSS,
BONUS_FOOD_ENDTURN,
FOOD_PER_INVENTORS,
SHAMAN_BONUS,
SHAMAN_DOUBLE_POINTS,
HUNT_BONUS,
ENDGAME_BUILDER_BONUS,
PAINTING_FOOD_BONUS,
ENDGAME_FOR_SIX,
ENDGAME_BONUS_CHARACTER,
EXTRA_DRAW,
ENDGAME_BONUS_POINTS
}

View File

@@ -0,0 +1,684 @@
package Server;
import Server.Automaton.ActionResult;
import Server.Automaton.Game;
import Server.Cards.*;
import Server.Utils.GameException;
import javafx.animation.*;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
public class DeckGridAppFX extends Application {
private static final Logger logger = LogManager.getLogger(DeckGridAppFX.class);
private static final int IMG_HEIGHT=200;
private PDDocument documentFront;
private PDDocument documentBack;
private PDFRenderer pdfRendererFront;
private PDFRenderer pdfRendererBack;
private int totalCards = 0;
// Contenitori Layout
private HBox topMenu;
private HBox topRow;
private HBox centerRow;
private HBox bottomRow;
private HBox playersArea; // NUOVO: Area per i giocatori
private Button btnRefresh;
private Button btnTop;
private Button btnBottom;
private Button btnChooseOffering;
private Button btnMsg;
// --------------------------------
@Override
public void start(Stage primaryStage) {
List<Player> players = new ArrayList<>();
//players.add(new Player("Yellow", TotemColor.YELLOW));
players.add(new Player("Blue", TotemColor.BLUE));
//players.add(new Player("Purple", TotemColor.PURPLE));
players.add(new Player("Red", TotemColor.RED));
//players.add(new Player("Green", TotemColor.GREEN));
Game game = new Game(players);
String fileCards="/home/lorenzo/dev/Mesos2/src/main/resources/files/cards.csv";
String fileCardsImgFront="/home/lorenzo/dev/Mesos2/src/main/resources/files/Cards_total_front_PROMO.pdf";
String fileCardsImgCover="/home/lorenzo/dev/Mesos2/src/main/resources/files/Cards_total_back_PROMO.pdf";
try {
File fileFront = new File(fileCardsImgFront);
File fileBack = new File(fileCardsImgCover);
documentFront = PDDocument.load(fileFront);
pdfRendererFront = new PDFRenderer(documentFront);
documentBack = PDDocument.load(fileBack);
pdfRendererBack = new PDFRenderer(documentBack);
} catch ( IOException e) {
throw new RuntimeException(e);
}
game.newGame(fileCards);
game.setEventListener(notification -> {
Platform.runLater(() -> {
drawGameState(game);
switch (notification.event()) {
case TOTEM_PLACEMENT-> showPopup("TOTEM PLACEMENT\n" + (notification.message()!=null?notification.message():""));
case SETUP -> showPopup("SETUP \n" + (notification.message()!=null?notification.message():""));
case ACTION_RESOLUTION -> showPopup("ACTION_RESOLUTION \n" + (notification.message()!=null?notification.message():"" ));
case END_ACTION -> showPopup("END_ACTION \n" + (notification.message()!=null?notification.message():"" ));
case EVENT_RESOLUTION -> showPopup("EVENT_RESOLUTION \n" + (notification.message()!=null?notification.message():"" ));
case EXTRA_DRAW -> showPopup("EXTRA_DRAW \n" + (notification.message()!=null?notification.message():"" ));
case GAME_OVER -> showPopup("GAME OVER");
}
});
});
primaryStage.setTitle("Tavolo da Gioco - Board & Players");
btnTop = new Button("Pick Top");
btnBottom = new Button("Pick Bottom");
btnMsg = new Button("Game State");
btnRefresh= new Button("Refresh");
btnChooseOffering= new Button("Choose Offering");
//btnNext.setDisable(true);
topMenu = new HBox(15, btnRefresh, btnMsg);
topMenu.setAlignment(Pos.CENTER);
topMenu.setPadding(new Insets(10));
Label stateLabel = new Label("Game State = " + game.getState() );
stateLabel.setFont(Font.font("System", FontWeight.BOLD, 16));
stateLabel.setTextFill(Color.DARKBLUE);
//showPopup(topMenu, "STARTING GAME");
btnMsg.setOnAction(e -> {
new Alert(Alert.AlertType.INFORMATION,
game.toString()
).showAndWait();
});
btnRefresh.setOnAction(e -> {
drawGameState(game);
});
topRow = createRowContainer();
centerRow = createRowContainer();
bottomRow = createRowContainer();
// NUOVO: Inizializza l'area giocatori
playersArea = new HBox(30);
playersArea.setAlignment(Pos.CENTER);
playersArea.setPadding(new Insets(20));
VBox tableArea = new VBox(20,
new Label(" "), topRow,
new Label(" "), centerRow,
new Label(" "), bottomRow,
new Label("--- ASSET PLAYERS ---"), playersArea
);
tableArea.setAlignment(Pos.CENTER);
tableArea.setPadding(new Insets(20));
ScrollPane scrollPane = new ScrollPane(tableArea);
scrollPane.setFitToWidth(true);
BorderPane root = new BorderPane();
root.setTop(topMenu);
root.setCenter(scrollPane);
Scene scene = new Scene(root, 1300, 900);
primaryStage.setScene(scene);
primaryStage.show();
drawGameState(game);
}
private HBox createRowContainer() {
HBox row = new HBox(10);
row.setAlignment(Pos.CENTER);
return row;
}
private void drawGameState(Game game) {
if (documentFront == null) return;
topMenu.getChildren().clear();
topRow.getChildren().clear();
centerRow.getChildren().clear();
bottomRow.getChildren().clear();
playersArea.getChildren().clear();
drawTopMenu(topMenu, game);
drawTopRow(topRow, game);
drawTurnTileAndOffering(centerRow, game.getPlayers().size(), game);
drawBottomRow(bottomRow, game );
List<Player> players =game.getPlayers();
renderPlayers(players, game);
logger.info(game.getCurrentPlayer());
}
private void pickTopCardAction(Player player, GameBoard board, int cardid){
Card card = board.getTopRow().stream()
.filter(CharacterCard.class::isInstance)
.map(CharacterCard.class::cast)
.filter(c -> c.getCardId() == cardid)
.findFirst()
.orElse(null);
if (card != null) {
board.getTopRow().remove(card);
CharacterCard charCard = (CharacterCard)card;
player.addCharacterToTribe((CharacterCard) card);
}
}
private void pickBottomCardAction(Player player, GameBoard board, int cardid){
Card card = board.getBottomRow().stream()
.filter(CharacterCard.class::isInstance)
.map(CharacterCard.class::cast)
.filter(c -> c.getCardId() == cardid)
.findFirst()
.orElse(null);
if (card != null) {
board.getBottomRow().remove(card);
player.addCharacterToTribe((CharacterCard) card);
}
}
private void drawBottomRow(HBox row, Game game) {
for (Card c : game.getGameBoard().getBottomRow()) {
ImageView cardImage = createCardImageFromPdf(c.getCardId(), IMG_HEIGHT, true);
if (cardImage != null) {
// 1. Create a Label for your debugging text
Label debugLabel = new Label("ID: " + c.getCardId());
// 2. Style the label so it's readable over any card background
// (White text with a semi-transparent black background)
debugLabel.setStyle("-fx-background-color: rgba(0, 0, 0, 0.7); " +
"-fx-text-fill: white; " +
"-fx-padding: 3px; " +
"-fx-font-weight: bold;");
// 3. Create a StackPane and add both the image and the text
StackPane cardContainer = new StackPane();
cardContainer.getChildren().addAll(cardImage, debugLabel);
// 4. Align the text wherever you want (e.g., Top-Left corner of the card)
StackPane.setAlignment(debugLabel, Pos.TOP_LEFT);
// You can also use Pos.CENTER, Pos.BOTTOM_RIGHT, etc.
cardImage.setOnMouseClicked(event -> {
logger.info("Bottom Card clicked");
Player p = game.getCurrentPlayer();
//pickBottomCardAction(p, game.getGameBoard(), c.getCardId());
ActionResult result = game.resolveCardAction(p, false, c.getCardId());
if (!result.isSuccess()) {
new Alert(Alert.AlertType.ERROR,
result.getErrorMessage()
).showAndWait();
}
drawGameState(game);
});
// 5. Add the StackPane (which now holds both) to the row
row.getChildren().add(cardContainer);
}
}
}
private void drawTopMenu(HBox row, Game game) {
//topMenu = new HBox(15, btnChooseOffering, btnAction, btnTop, btnBottom, btnMsg);
//topMenu.setAlignment(Pos.CENTER);
//topMenu.setPadding(new Insets(10));
Label stateLabel = new Label("Game State = " + game.getState() );
stateLabel.setFont(Font.font("System", FontWeight.BOLD, 16));
stateLabel.setTextFill(Color.DARKBLUE);
row.getChildren().add(stateLabel);
row.getChildren().add(btnRefresh);
row.getChildren().add(btnMsg);
}
private void drawTopRow(HBox row, Game game) {
for (Card c : game.getGameBoard().getTopRow()) {
ImageView cardImage = createCardImageFromPdf(c.getCardId(), IMG_HEIGHT, true);
if (cardImage != null) {
// 1. Create a Label for your debugging text
Label debugLabel = new Label("ID: " + c.getCardId());
// 2. Style the label so it's readable over any card background
// (White text with a semi-transparent black background)
debugLabel.setStyle("-fx-background-color: rgba(0, 0, 0, 0.7); " +
"-fx-text-fill: white; " +
"-fx-padding: 3px; " +
"-fx-font-weight: bold;");
// 3. Create a StackPane and add both the image and the text
StackPane cardContainer = new StackPane();
cardContainer.getChildren().addAll(cardImage, debugLabel);
// 4. Align the text wherever you want (e.g., Top-Left corner of the card)
StackPane.setAlignment(debugLabel, Pos.TOP_LEFT);
// You can also use Pos.CENTER, Pos.BOTTOM_RIGHT, etc.
cardImage.setOnMouseClicked(event -> {
logger.info("Card clicked");
Player p = game.getCurrentPlayer();
//pickTopCardAction(p, game.getGameBoard(), c.getCardId());
//game.resolveCardAction(p, true, c.getCardId());
ActionResult result = game.resolveCardAction(p, true, c.getCardId());
if (!result.isSuccess()) {
new Alert(Alert.AlertType.ERROR,
result.getErrorMessage()
).showAndWait();
}
drawGameState(game);
});
// 5. Add the StackPane (which now holds both) to the row
row.getChildren().add(cardContainer);
}
}
}
private StackPane drawTurnTileCardImageWithPlayers(String imagePath, int height, Player[] players) {
try {
Image fxImage = new Image("file:" + imagePath);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
StackPane stack = new StackPane(imageView);
VBox playerBox = new VBox(5);
playerBox.setPadding(new Insets(5));
playerBox.setAlignment(Pos.CENTER_RIGHT);
// 🔥 DEBUG background (to SEE the box)
playerBox.setStyle("-fx-background-color: rgba(255,0,0,0.3);");
for (Player p : players) {
if (p==null) continue;
Rectangle rect = new Rectangle(15,15);
rect.setFill(p.getTotemColor().getFxColor());
rect.setStroke(Color.WHITE);
rect.setStrokeWidth(1);
playerBox.getChildren().add(rect);
}
playerBox.setPadding(new Insets(22));
// 🔥 IMPORTANT: bring to front
playerBox.toFront();
// position it clearly
StackPane.setAlignment(playerBox, Pos.CENTER);
stack.getChildren().add(playerBox);
return stack;
} catch (Exception e) {
logger.error("Errore caricamento immagine {}", imagePath, e);
return null;
}
}
private void drawTurnTileAndOffering(HBox row, int n, Game game) {
// Clear row if needed (optional but recommended)
row.getChildren().clear();
// --- LEFT BLOCK (Turn + initial card) ---
VBox turnBox = new VBox(5);
turnBox.setAlignment(Pos.CENTER);
// Turn label
int round = game.getRound(); // adjust if different
Label turnLabel = new Label("Round = " + round);
turnLabel.setFont(Font.font("System", FontWeight.BOLD, 16));
turnLabel.setTextFill(Color.DARKBLUE);
Card first = game.getGameBoard().getCardDeck().getFirstCardForCover();
if (first!=null){
ImageView imgCover = createCardImageFromPdf(first.getCardId(), 120, false);
turnBox.getChildren().addAll(turnLabel, imgCover);
} else turnBox.getChildren().addAll(turnLabel);
// Add FIRST
row.getChildren().add(turnBox);
// --- TURN TILE ---
StackPane pane = drawTurnTileCardImageWithPlayers(
"/home/lorenzo/dev/Mesos2/src/main/resources/files/Start_" + n + "P.png",
IMG_HEIGHT,
game.getGameBoard().getTurnTile().getPositions()
);
row.getChildren().add(pane);
// --- OFFERINGS ---
for (OfferingTile offering : game.getGameBoard().getOfferingTiles()) {
Player occupant = offering.getOccupant();
StackPane offPane =drawOfferingCardImageWithTotem(
"/home/lorenzo/dev/Mesos2/src/main/resources/files/offering" + offering.getLetter() + ".png",
IMG_HEIGHT,
occupant, game, offering);
if (offPane != null) {
row.getChildren().add(offPane);
}
}
}
// --- NUOVO: RENDERIZZAZIONE GIOCATORI E RAGGRUPPAMENTO (JAVA 8) ---
private void renderPlayers(List<Player> players, Game game) {
playersArea.getChildren().clear();
for (Player player : players) {
boolean active = player.equals(game.getCurrentPlayer());
VBox playerBox = new VBox(10);
playerBox.setPadding(new Insets(15));
playerBox.setStyle("-fx-border-color: #555; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-color: #f9f9f9; -fx-background-radius: 10;");
if (active) {
// Strong highlight (gold border + soft background)
playerBox.setStyle(
"-fx-border-color: gold;" +
"-fx-border-width: 3;" +
"-fx-border-radius: 10;" +
"-fx-background-color: linear-gradient(to bottom, #fffbe6, #f9f9f9);" +
"-fx-background-radius: 10;"
);
// Slight scale-up
playerBox.setScaleX(1.05);
playerBox.setScaleY(1.05);
// Glow effect
DropShadow glow = new DropShadow();
glow.setColor(Color.GOLD);
glow.setRadius(20);
playerBox.setEffect(glow);
// Smooth pulse animation (NOT blinking)
Timeline pulse = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(glow.radiusProperty(), 10)
),
new KeyFrame(Duration.seconds(1.2),
new KeyValue(glow.radiusProperty(), 25)
)
);
pulse.setAutoReverse(true);
pulse.setCycleCount(Animation.INDEFINITE);
pulse.play();
}
HBox nameRow = new HBox(8);
nameRow.setAlignment(Pos.CENTER_LEFT);
Rectangle rect = new Rectangle(12, 12);
TotemColor color = player.getTotemColor();
rect.setFill(color.getFxColor());
rect.setStroke(Color.BLACK);
rect.setStrokeWidth(1);
Label nameLbl = new Label(player.getNickname());
nameLbl.setFont(Font.font("System", FontWeight.BOLD, 16));
if (active) {
Label turnLbl = new Label(" ▶ Your Turn");
turnLbl.setTextFill(Color.GOLDENROD);
turnLbl.setFont(Font.font("System", FontWeight.BOLD, 14));
nameRow.getChildren().addAll(rect, nameLbl, turnLbl);
} else {
nameRow.getChildren().addAll(rect, nameLbl);
}
// Risorse Cibo e Soldi
Label statsLbl = new Label("🍖 Food: " + player.getFoodTokens() + " | 💰 Points: " + player.getPrestigePoints());
statsLbl.setTextFill(Color.DARKRED);
statsLbl.setFont(Font.font("System", FontWeight.BOLD, 14));
playerBox.getChildren().addAll(nameRow, statsLbl);
Map<CharacterType, List<CharacterCard>> groupedCards = player.getPlayerTribe().getCharacters().stream()
.collect(Collectors.groupingBy(CharacterCard::getCharacterType));
// Itera sui gruppi creati e genera la grafica
groupedCards.forEach((type, cardsOfType) -> {
Label typeLbl = new Label("Tipo: " + type.name() + " (" + cardsOfType.size() + ")");
typeLbl.setFont(Font.font("System", FontWeight.NORMAL, 12));
HBox cardImagesRow = new HBox(5);
for (Card c : cardsOfType) {
// Carte più piccole (90px) per l'area giocatore
ImageView img = createCardImageFromPdf(c.getCardId(), 90, true);
if (img != null) cardImagesRow.getChildren().add(img);
}
playerBox.getChildren().addAll(typeLbl, cardImagesRow);
});
Map<Era, List<BuildingCard>> groupedBuildCards = player.getPlayerTribe().getBuildingCard().stream()
.collect(Collectors.groupingBy(BuildingCard::getEra));
groupedBuildCards.forEach((type, cardsOfType) -> {
Label typeLbl = new Label("Build Tipo: " + type.name() + " (" + cardsOfType.size() + ")");
typeLbl.setFont(Font.font("System", FontWeight.NORMAL, 12));
HBox cardImagesRow = new HBox(5);
for (Card c : cardsOfType) {
// Carte più piccole (90px) per l'area giocatore
ImageView img = createCardImageFromPdf(c.getCardId(), 90, true);
if (img != null) cardImagesRow.getChildren().add(img);
}
playerBox.getChildren().addAll(typeLbl, cardImagesRow);
//playerBox.getChildren().addAll(nameRow, statsLbl);
});
playersArea.getChildren().add(playerBox);
}
}
private ImageView createCardImageFromPdf(int pageIndex, int height, boolean front) {
try {
BufferedImage bim =null;
if (front) bim = pdfRendererFront.renderImageWithDPI(pageIndex-1, 100);
else bim = pdfRendererBack.renderImageWithDPI(pageIndex-1, 100);
Image fxImage = SwingFXUtils.toFXImage(bim, null);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
return imageView;
} catch (IOException e) {
logger.error("Errore rendering pagina {}", pageIndex, e);
return null;
}
}
private StackPane drawOfferingCardImageWithTotem(String imagePath, int height, Player occupant, Game game, OfferingTile offering) {
try {
Image fxImage = new Image("file:" + imagePath);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
imageView.setOnMouseClicked(event -> {
int idx = game.getGameBoard().getOfferingTiles().indexOf(offering);
game.placeTotem(game.getCurrentPlayer(), idx);
logger.info(" PLAYER {} choose {} ", game.getCurrentPlayer().getNickname() , offering );
drawGameState(game);
});
StackPane stack = new StackPane(imageView);
if(occupant!=null){
Rectangle rect = new Rectangle(25, 25);
rect.setFill(occupant.getTotemColor().getFxColor());
rect.setStroke(Color.WHITE);
rect.setStrokeWidth(1);
StackPane.setAlignment(rect, Pos.TOP_CENTER);
rect.setTranslateY(20);
StackPane.setMargin(rect, new Insets(5));
stack.getChildren().add(rect);
}
return stack;
} catch (Exception e) {
logger.error("Errore caricamento immagine {}", imagePath, e);
return null;
}
}
private ImageView createCardImage(String imagePath, int height) {
try {
Image fxImage = new Image("file:" + imagePath);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
return imageView;
} catch (Exception e) {
logger.error("Errore caricamento immagine {}", imagePath, e);
return null;
}
}
private void showPopup(String message) {
Platform.runLater(() -> {
Label label = new Label("" + message); // icon improves readability
label.setWrapText(true);
label.setMaxWidth(400);
label.setStyle(
"-fx-background-color: rgba(20,20,20,0.9);" +
"-fx-text-fill: white;" +
"-fx-font-size: 20px;" +
"-fx-font-weight: bold;" +
"-fx-padding: 15 25 15 25;" +
"-fx-background-radius: 12;" +
"-fx-border-radius: 12;" +
"-fx-border-color: #00c3ff;"
);
label.setOpacity(0); // start invisible
topMenu.getChildren().add(label);
// Fade IN
FadeTransition fadeIn = new FadeTransition(Duration.millis(250), label);
fadeIn.setFromValue(0);
fadeIn.setToValue(1);
// Stay visible
PauseTransition pause = new PauseTransition(Duration.seconds(4));
// Fade OUT
FadeTransition fadeOut = new FadeTransition(Duration.millis(400), label);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished(f -> topMenu.getChildren().remove(label));
fadeIn.play();
fadeIn.setOnFinished(e -> pause.play());
pause.setOnFinished(e -> fadeOut.play());
});
}
private void showPopupNo(String message) {
Platform.runLater(() -> {
Label label = new Label(message);
label.setStyle(
"-fx-background-color: rgba(0,0,0,0.75);" +
"-fx-text-fill: white;" +
"-fx-font-size: 28px;" +
"-fx-font-weight: bold;" +
"-fx-padding: 20 40 20 40;" +
"-fx-background-radius: 12;"
);
label.setMouseTransparent(true);
topMenu.getChildren().add(label);
PauseTransition pause = new PauseTransition(Duration.seconds(5));
pause.setOnFinished(e -> {
FadeTransition fade = new FadeTransition(Duration.millis(400), label);
fade.setFromValue(1.0);
fade.setToValue(0.0);
fade.setOnFinished(f -> topMenu.getChildren().remove(label));
fade.play();
});
pause.play();
});
}
@Override
public void stop() throws Exception {
if (documentFront != null) documentFront.close();
super.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,19 @@
package Server;
public enum Era {
I,
II,
III,
FINAL;
public Era next() {
Era[] eras = values();
// Calcola l'indice del prossimo elemento
int nextOrdinal = this.ordinal() + 1;
if (nextOrdinal < eras.length) {
return eras[nextOrdinal];
}
return this;
}
}

View File

@@ -0,0 +1,136 @@
package Server;
import Server.Cards.Event;
import Server.Cards.EventCard;
import Server.Cards.Trigger;
import Server.Utils.EventsManagerException;
import java.util.ArrayList;
import java.util.List;
public class EventsSolver {
public static boolean solveEvents(List<EventCard> events, List<Player> players){
for(EventCard event: events){
switch (event.getEvent()){
case Event.SUSTAINMENT:
EventsSolver.sustainment(event, players);
break;
case Event.HUNT:
EventsSolver.hunt(event, players);
break;
case Event.SHAMANIC_RITUAL:
EventsSolver.shamanicRitual(event, players);
break;
case Event.CAVE_PAINTINGS:
EventsSolver.cavePaintings(event, players);
break;
default:
throw new EventsManagerException("Unknown event type");
}
}
return true;
}
public static List<Integer> sustainment(EventCard event, List<Player> players){
if(event.getEvent() != Event.SUSTAINMENT){throw new EventsManagerException("Not a sustainment card");}
List<Integer> result = new ArrayList<>();
for(Player p: players){
int subpoints = p.getPlayerTribe().getCharacters().size();
int discount = p.getPlayerTribe().gathererDiscount() + BuildingManager.sustainDiscount(p);
if(subpoints <= discount){subpoints = 0;}else{
subpoints -= discount;
}
if(p.getFoodTokens() >= subpoints){
p.removeFood(subpoints);
}else{
subpoints -= p.getFoodTokens();
p.removeFood(p.getFoodTokens());
p.removePrestigePoints(subpoints*event.getFirstValue());
}
result.add(p.getFoodTokens());
result.add(p.getPrestigePoints());
}
return result;
}
public static List<Integer> hunt(EventCard event, List<Player> players){
if(event.getEvent() != Event.HUNT){throw new EventsManagerException("Not a hunt card");}
List<Integer> result = new ArrayList<>();
for(Player p: players){
p.addFood(p.getPlayerTribe().huntersNumber());
p.addPrestigePoints(p.getPlayerTribe().huntersNumber()*event.getFirstValue());
if(BuildingManager.hunterBonus(p)){
p.addFood(p.getPlayerTribe().huntersNumber());
p.addPrestigePoints(p.getPlayerTribe().huntersNumber());
}
result.add(p.getFoodTokens());
result.add(p.getPrestigePoints());
}
return result;
}
public static List<Integer> shamanicRitual(EventCard event, List<Player> players){
if(event.getEvent() != Event.SHAMANIC_RITUAL){throw new EventsManagerException("Not a shamanic ritual card");}
List<Integer> result = new ArrayList<>();
int maxSymbols = 0;
int minSymbols = 999;
for(Player p: players){
int symbols = p.getPlayerTribe().shamansIcons() + BuildingManager.shamanBonus(p);
if (symbols > maxSymbols){maxSymbols = p.getPlayerTribe().shamansIcons();}
if (symbols < minSymbols){minSymbols = p.getPlayerTribe().shamansIcons();}
}
for(Player p: players){
if(p.getPlayerTribe().shamansIcons() == maxSymbols){
p.addPrestigePoints(event.getFirstValue());
//activating building shamanDoublePoints card effect
if(BuildingManager.shamanDoublePoints(p))
p.addPrestigePoints(event.getFirstValue());
}
if(p.getPlayerTribe().shamansIcons() == minSymbols){
//activating building shamanNoLoss card effect
if(!BuildingManager.shamanNoLoss(p))
p.removePrestigePoints(event.getSecondValue());
}
result.add(p.getPrestigePoints());
}
return result;
}
public static List<Integer> cavePaintings(EventCard event, List<Player> players){
if(event.getEvent() != Event.CAVE_PAINTINGS){throw new EventsManagerException("Not a cave painting card");}
List<Integer> result = new ArrayList<>();
for(Player p: players){
BuildingManager.paintingFoodBonus(p);
if(p.getPlayerTribe().artistsNumber() >= event.getFirstValue()){
p.addPrestigePoints(p.getPlayerTribe().artistsNumber()*event.getSecondValue());
}else{
p.removePrestigePoints(event.getSecondValue());
}
result.add(p.getPrestigePoints());
}
return result;
}
}

View File

@@ -0,0 +1,395 @@
package Server;
import Server.Automaton.Game;
import Server.Cards.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* 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;
}
// -------------------------------------------------------------------------
// 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 +
'}';
}
}

View File

@@ -0,0 +1,148 @@
package Server;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
/**
* Represents a single tile on the offering track.
*
* Each tile has a fixed letter (AG) and a fixed list of actions the
* occupying player must resolve. The tile is either empty or occupied
* by exactly one player's totem at any given time.
*
* Tile layout (from the rulebook):
*
* Letter │ orderId │ Actions │ Player count
* ───────┼─────────┼──────────────────────┼─────────────
* A │ 0 │ FOOD FOOD FOOD │ 5 only
* B │ 1 │ BOTTOM │ all
* C │ 2 │ UP │ all
* D │ 3 │ BOTTOM BOTTOM │ 3+
* E │ 4 │ BOTTOM UP │ all
* F │ 5 │ UP UP │ all
* G │ 6 │ BOTTOM UP UP │ 4+
*
* Which tiles are included in a game is decided by GameBoard.initOfferingTiles(),
* not by this class.
*/
public class OfferingTile {
private static final char FIRST_LETTER = 'A';
private final int orderId;
private final char letter;
private final List<Symbol> actions; // unmodifiable after construction
private Player occupant;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
public OfferingTile(int orderId) {
this.orderId = orderId;
this.letter = (char) (FIRST_LETTER + orderId);
this.occupant = null;
this.actions = Collections.unmodifiableList(buildActions(orderId));
}
/**
* Returns the fixed action list for each tile orderId.
* Extracted into a private method to keep the constructor clean.
*/
private static List<Symbol> buildActions(int orderId) {
List<Symbol> list = new ArrayList<>();
switch (orderId) {
case 0: // A — food tile (5p only)
list.add(Symbol.FOOD);
list.add(Symbol.FOOD);
list.add(Symbol.FOOD);
break;
case 1: // B
list.add(Symbol.DOWN);
break;
case 2: // C
list.add(Symbol.UP);
break;
case 3: // D
list.add(Symbol.DOWN);
list.add(Symbol.DOWN);
break;
case 4: // E
list.add(Symbol.DOWN);
list.add(Symbol.UP);
break;
case 5: // F
list.add(Symbol.UP);
list.add(Symbol.UP);
break;
case 6: // G
list.add(Symbol.DOWN);
list.add(Symbol.UP);
list.add(Symbol.UP);
break;
default:
throw new IllegalArgumentException("Invalid OfferingTile orderId: " + orderId);
}
return list;
}
// -------------------------------------------------------------------------
// Occupant management
// -------------------------------------------------------------------------
/** Returns true if no totem is currently placed on this tile. */
public boolean isEmpty() {
return occupant == null;
}
/**
* Places a player's totem on this tile.
* Callers should check {@link #isEmpty()} before calling this.
*/
public void setOccupant(Player player) {
this.occupant = player;
}
/**
* Removes the totem from this tile and returns the player who was on it,
* or null if the tile was already empty.
*/
public Player removeOccupant() {
Player previous = this.occupant;
this.occupant = null;
return previous;
}
// -------------------------------------------------------------------------
// Getters
// -------------------------------------------------------------------------
/** The player currently occupying this tile, or null if empty. */
public Player getOccupant() {
return occupant;
}
/**
* The actions this tile grants to its occupant (e.g. [BOTTOM, UP]).
* The returned list is unmodifiable.
*/
public List<Symbol> getActions() {
return actions;
}
/** 0-based position index; determines the tile's letter and action set. */
public int getOrderId() {
return orderId;
}
/** The tile's letter label ('A''G'), useful for display and logging. */
public char getLetter() {
return letter;
}
@Override
public String toString() {
return "Tile " + letter + " " + actions + (isEmpty() ? " [empty]" : " [" + occupant.getNickname() + "]");
}
}

View File

@@ -0,0 +1,96 @@
package Server;
import Server.Cards.CharacterCard;
import java.util.Objects;
public class Player {
//Attributes
private String nickname;
private TotemColor totemColor;
private int foodTokens;
private int prestigePoints;
private Tribe playerTribe;
//Constructor
public Player(String nickname, TotemColor totemColor) {
this.nickname = nickname;
this.totemColor = totemColor;
this.foodTokens = 0;
this.prestigePoints = 0;
this.playerTribe = new Tribe();
}
//Methods
public void addFood(int food){
this.foodTokens += food;
}
public boolean removeFood(int food){
if (this.foodTokens >= food) {
this.foodTokens -= food;
return true; // Pagamento andato a buon fine
}
return false; // Il giocatore non ha abbastanza cibo, transazione negata, la quantità di cibo rimane quella di prima.
}
public void addPrestigePoints(int prestige){
this.prestigePoints += prestige;
}
public int removePrestigePoints(int prestige){
this.prestigePoints -= prestige;
return prestigePoints;
}
public void addCharacterToTribe(CharacterCard card) {
int foodGained = this.playerTribe.addCharacter(card);
this.addFood(foodGained);
}
//Getters
public String getNickname() {
return nickname;
}
public int getFoodTokens() {
return foodTokens;
}
public int getPrestigePoints() {
return prestigePoints;
}
public TotemColor getTotemColor() {
return totemColor;
}
public Tribe getPlayerTribe() {
return playerTribe;
}
// two Player objects to be considered equal only by nickname
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Player player = (Player) o;
return Objects.equals(nickname, player.nickname);
}
@Override
public int hashCode() {
return Objects.hash(nickname);
}
@Override
public String toString() {
return "Player(" +
"name='" + nickname + '\'' +
", food=" + foodTokens +
", pp=" + prestigePoints +
')';
}
}

View File

@@ -0,0 +1,7 @@
package Server;
public enum Symbol {
UP,
DOWN,
FOOD
}

View File

@@ -0,0 +1,21 @@
package Server;
import javafx.scene.paint.Color;
public enum TotemColor {
RED(Color.RED),
BLUE(Color.BLUE),
YELLOW(Color.YELLOW),
GREEN(Color.GREEN),
PURPLE(Color.PURPLE);
private final Color fxColor;
TotemColor(Color fxColor) {
this.fxColor = fxColor;
}
public Color getFxColor() {
return fxColor;
}
}

View File

@@ -0,0 +1,239 @@
package Server;
import Server.Cards.BuildingCard;
import Server.Cards.CharacterCard;
import Server.Cards.CharacterType;
import Server.Cards.Trigger;
import java.util.ArrayList;
import java.util.List;
public class Tribe {
// Attributes
private List<CharacterCard> characters;
private List<BuildingCard> buildings;
// Constructor
public Tribe() {
this.characters = new ArrayList<>();
this.buildings = new ArrayList<>();
}
public List<CharacterCard> getCharacters() {
return characters;
}
public List<BuildingCard> getBuildingCard() {
return buildings;
}
// METODI
public int addCharacter(CharacterCard card) {
this.characters.add(card);
if (card.getCharacterType().equals(CharacterType.HUNTER)) {
return hunterGetFood(card);
}
return 0;
}
// Metodo per aggiungere un nuovo building alla tribù
public void addBuilding(BuildingCard card) {
this.buildings.add(card);
}
// Metodo per ottenere lo sconto in cibo in base a quanti gatherer abbiamo nella tribù
public int gathererDiscount() {
int discount = 0;
for (CharacterCard c : characters) {
if (c.getCharacterType() == CharacterType.GATHERER) {
discount += 3; // i gatherers prendono sempre 3 cibi
}
}
return discount;
}
// Metodo per ottenere lo sconto totale sugli edifici grazie ai builder nella tribù
public int buildersDiscount() {
int discount = 0;
for (CharacterCard c : characters) {
if (c.getCharacterType() == CharacterType.BUILDER) {
discount += c.getIconValue(); // con getIconValue intendo lo sconto del costruttore
}
}
return discount;
}
// Metodo che conta quante stelle degli sciamani abbiamo in totale nella tribù
public int shamansIcons() {
int totalIcons = 0;
for (CharacterCard c : characters) {
if (c.getCharacterType() == CharacterType.SHAMAN) {
totalIcons += c.getIconValue();
}
}
return totalIcons;
}
// Metodo che restituisce il numero di artisti nella tribù
public int artistsNumber() {
return countCharactersByType(CharacterType.ARTIST);
}
// Metodo che restituisce il numero di cacciatori nella tribù
public int huntersNumber() {
return countCharactersByType(CharacterType.HUNTER);
}
// Metodo universale per contare le carte di un certo tipo all'interno della tribù
public int countCharactersByType(CharacterType typeToCount) {
int count = 0;
for (CharacterCard c : characters) {
if (c.getCharacterType() == typeToCount) {
count++;
}
}
return count;
}
// Metodo che ritorna il numero totale di cibi ottenuti dopo aver pescato il cacciatore col cosciotto
public int hunterGetFood(CharacterCard hunter) {
return huntersNumber() * hunter.getIconValue(); // getIconValue = 1 se il cacciatore ha il cosciotto
}
// Metodo che restituisce i punti finali degli inventori
public int inventorsEndPoints() {
int inventorCount = 0;
List<Integer> uniqueInventions = new ArrayList<>();
for (CharacterCard c : characters) {
if (c.getCharacterType() == CharacterType.INVENTOR) {
inventorCount++;
int inventionId = c.getIconValue(); // Usiamo l'iconValue del file cards.csv
// Se non abbiamo ancora contato questa invenzione, la aggiungiamo
if (!uniqueInventions.contains(inventionId)) {
uniqueInventions.add(inventionId);
}
}
}
return inventorCount * uniqueInventions.size();
}
// Metodo che restituisce i punti finali degli artisti
public int artistsEndPoints() {
return (artistsNumber() / 2) * 10; // in java 1/2 fa 0
}
// Metodo che restituisce i punti finali dei costruttori
public int buildersEndPoints() {
int points = 0;
for (CharacterCard c : characters) {
if (c.getCharacterType() == CharacterType.BUILDER) {
points += c.getPrestigePoints();
}
}
return points;
}
// Metodo che restituisce i punti finali guadagnati grazie agli EFFETTI delle carte building
private int buildingAbilitiesEndPoints() {
int bonusPoints = 0;
for (BuildingCard b : buildings) {
Trigger trigger = b.getAbilityTrigger(); // leggiamo il trigger della carta edificio
// Se la carta per qualche motivo non ha trigger, passiamo alla prossima
if (trigger == null) {
continue;
}
switch (trigger) {
case ENDGAME_BUILDER_BONUS: // id carta: 107
bonusPoints += (buildersEndPoints() * 2);
break;
case ENDGAME_FOR_SIX: // id carta: 109
// Prepariamo i contatori per tutti e 6 i tipi di characters
int inv = 0, hun = 0, gat = 0, sha = 0, art = 0, bui = 0;
// contiamo le carte nella tribù
for (CharacterCard c : characters) {
switch (c.getCharacterType()) {
case INVENTOR: inv++; break;
case HUNTER: hun++; break;
case GATHERER: gat++; break;
case SHAMAN: sha++; break;
case ARTIST: art++; break;
case BUILDER: bui++; break;
}
}
// troviamo il numero di set completi
int min1 = Math.min(inv, hun);
int min2 = Math.min(gat, sha);
int min3 = Math.min(art, bui);
int completeSets = Math.min(Math.min(min1, min2), min3);
// aggiungiamo 6 punti prestigio per ogni set completo
bonusPoints += (completeSets * 6);
break;
case ENDGAME_BONUS_CHARACTER:
int id = b.getCardId(); // uso l'id della carta per capire che edificio è
// il numero id corrisponde al numero di pagina nel pdf
if (id == 110) { // edificio dei cacciatori
bonusPoints += countCharactersByType(CharacterType.HUNTER) * 3;
}
else if (id == 111) { // edificio dei gatherer
bonusPoints += countCharactersByType(CharacterType.GATHERER) * 4;
}
else if (id == 112) { // edificio degli sciamani
bonusPoints += countCharactersByType(CharacterType.SHAMAN) * 4;
}
else if (id == 113) { // edificio dei costruttori
bonusPoints += countCharactersByType(CharacterType.BUILDER) * 4;
}
else if (id == 114) { // edificio degli artisti
bonusPoints += countCharactersByType(CharacterType.ARTIST) * 4;
}
else if (id == 115) { // edificio degli inventori
bonusPoints += countCharactersByType(CharacterType.INVENTOR) * 2;
}
break;
case ENDGAME_BONUS_POINTS: // id carta: 117
bonusPoints += 25; // dà 25 punti bonus
break;
default:
break;
}
}
return bonusPoints;
}
// Metodo che calcola i punti finali totali
public int endPoints() {
int total = 0;
// sommiamo i punti calcolati dai vari personaggi
total += inventorsEndPoints();
total += artistsEndPoints();
total += buildersEndPoints();
// sommiamo i punti prestigio BASE di tutti gli edifici (EndPP)
for (BuildingCard b : buildings) {
total += b.getEndPP();
}
// sommiamo gli effetti degli edifici sul punteggio finale, NON quelli durante la partita
total += buildingAbilitiesEndPoints();
return total;
}
}

View File

@@ -0,0 +1,176 @@
package Server;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Represents the Turn Order tile.
*
* Lifecycle per round:
* 1. At round start, read positions top-to-bottom via nextToPlace() to know
* which player places their totem on the offering track first.
* 2. After a player resolves all their offering actions, they call returnTotem().
* returnTotem() places them in the next free slot (top-to-bottom) and
* immediately gives them the position reward/penalty.
* 3. The order in which players called returnTotem() becomes the new turn
* order for the NEXT round.
* 4. resetTrack() is called at the start of each new round to restart the
* placement cursor and the return-slot counter, WITHOUT clearing positions
* (they already hold the new order from step 2-3).
*/
public class TurnTile {
private static final Logger logger = LogManager.getLogger(TurnTile.class);
private final Player[] positions; // current turn order; updated each round via returnTotem()
private int nextFreeSlot; // next available slot for a returning totem
private int placementCursor; // cursor used during the totem-placement phase
public TurnTile(int numP) {
this.positions = new Player[numP];
this.nextFreeSlot = 0;
this.placementCursor = 0;
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
/**
* Randomises the initial turn order. Call once during game setup.
*/
public void setInitialOrder(List<Player> players) {
List<Player> shuffled = new ArrayList<>(players);
Collections.shuffle(shuffled);
for (int i = 0; i < shuffled.size(); i++) {
positions[i] = shuffled.get(i);
}
logger.info("setInitialOrder " + this);
}
// -------------------------------------------------------------------------
// Totem-placement phase (phase 1 of a round)
// -------------------------------------------------------------------------
/**
* Returns the next player who must place their totem on the offering track
* (following the current turn order, top to bottom).
* Returns null when all players have already been served this round.
*/
public Player nextToPlace() {
if (placementCursor < positions.length) {
return positions[placementCursor++];
}
return null;
}
/**
* Returns the player who is currently expected to place their totem
* (i.e. the last player returned by nextToPlace), or null if none yet.
*/
public Player getLastPlacedPlayer() {
if (placementCursor > 0) {
return positions[placementCursor - 1];
}
return null;
}
// -------------------------------------------------------------------------
// Action-resolution phase (phase 2 of a round)
// -------------------------------------------------------------------------
/**
* Called when a player has finished resolving all their offering actions.
* Places the player's totem in the next free slot (determining next-round
* order) and applies the food reward or penalty for that slot.
*
* Reward rules (from the rulebook):
* - Slot 0 (first to return): +1 food (2p), +2 food (3-4p), +3 food (5p)
* - Slot 1 (second to return, only 4-5 player games): +1 food
* - Last slot: pay 1 food; if unable, lose 2 PP instead
*
* @return the slot index the player was placed in
*/
public int returnTotem(Player player) {
logger.info("returnTotem " + player);
int slot = nextFreeSlot;
// --- Position food reward ---
int positionFood = 0;
if (slot == 0) {
positionFood = (positions.length == 2) ? 1
: (positions.length == 5) ? 3
: 2; // 3 or 4 players
} else if (slot == 1 && positions.length >= 4) {
positionFood = 1;
}
player.addFood(positionFood);
// --- Activate BONUS_FOOD_ENDTURN building if the player has it ---
// (The building gives +1 extra food whenever the player lands on a food slot)
BuildingManager.bonusEndTurn(player, positionFood);
// --- Last-slot penalty ---
if (slot == positions.length - 1) {
if (!player.removeFood(1)) {
player.removePrestigePoints(2);
}
}
positions[slot] = player;
nextFreeSlot++;
logger.info(player.getNickname() + " returned totem to slot " + slot
+ (positionFood > 0 ? " and received " + positionFood + " food" : ""));
return slot;
}
public void leaveTurnTile(Player player) {
for(int i=0;i<positions.length;i++)
if (positions[i] !=null && positions[i].equals(player)) positions[i]=null;
}
// -------------------------------------------------------------------------
// Round reset
// -------------------------------------------------------------------------
/**
* Resets the cursors for a new round.
* Does NOT clear positions: those already contain the new turn order
* established during the previous round's returnTotem() calls.
*/
public void resetTrack() {
nextFreeSlot = 0;
placementCursor = 0;
}
// -------------------------------------------------------------------------
// Getters
// -------------------------------------------------------------------------
/**
* Returns the full positions array (turn order).
* Index 0 = first to act, last index = last to act.
*/
public Player[] getPositions() {
return positions;
}
/** How many players have already returned their totem this round. */
public int getReturnedCount() {
return nextFreeSlot;
}
@Override
public String toString() {
return "TurnTile{" +
"positions=" + Arrays.toString(positions) +
'}';
}
}

View File

@@ -0,0 +1,89 @@
package Server;
import java.util.List;
public class TurnTileOld {
private final Player[] positions;
private int place;
private int currentplayer;
public TurnTileOld(int numP) {
this.positions = new Player[numP];
this.place = 0;
this.currentplayer = 0;
}
public Player[] startOrder (List<Player> players){
for (Player p: players){
positions[place]=p;
place++;
}
//Collections.shuffle(Arrays.asList(positions));
return positions;
}
public Player giveReward (Player player){
if (player == positions[positions.length-1]){
if (!player.removeFood(1)){
player.removePrestigePoints(2);
}
}
else if (player == positions[0]){
if (positions.length==2){
player.addFood(1);
}
else if (positions.length == 5){
player.addFood(3);
}
else{
player.addFood(2);
}
}
else if (player == positions[1] && positions.length >= 4){
player.addFood(1);
}
return player;
}
public Player nextPlayer() {
currentplayer++;
if (currentplayer== positions.length){
currentplayer=0;
}
return positions[currentplayer];
}
public Player addPlayer(Player player){
positions[currentplayer]=player;
return positions[currentplayer];
}
}

View File

@@ -0,0 +1,7 @@
package Server.Utils;
public class EventsManagerException extends RuntimeException {
public EventsManagerException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package Server.Utils;
public class GameException extends RuntimeException {
public GameException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package Server.Utils;
public class LoadingCardsException extends RuntimeException {
public LoadingCardsException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,18 @@
module org.example.mesosll07 {
requires javafx.controls;
requires javafx.fxml;
requires org.controlsfx.controls;
requires javafx.swing;
requires org.apache.pdfbox;
requires org.apache.logging.log4j;
opens org.example.mesosll07 to javafx.fxml;
exports org.example.mesosll07;
exports Server;
opens Server to javafx.fxml;
exports Server.Cards;
opens Server.Cards to javafx.fxml;
exports Server.Automaton;
opens Server.Automaton to javafx.fxml;
}

View File

@@ -0,0 +1,208 @@
package org.example.mesosll07;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DeckGridApp extends Application {
// Inizializza il Logger
private static final Logger logger = LogManager.getLogger(DeckGridApp.class);
private PDDocument document;
private PDFRenderer pdfRenderer;
private int totalCards = 0;
// Contenitori per le tre righe
private HBox topRow;
private HBox centerRow;
private HBox bottomRow;
private Button btnShuffle;
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Tavolo da Gioco - Carte Casuali");
logger.info("Avvio dell'applicazione JavaFX");
// Pulsanti
Button btnLoad = new Button("Carica PDF Mazzo");
btnShuffle = new Button("Rimescola Carte");
btnShuffle.setDisable(true); // Disabilitato finché non si carica un PDF
btnLoad.setOnAction(e -> loadPdf(primaryStage));
btnShuffle.setOnAction(e -> distributeRandomCards());
HBox topMenu = new HBox(15, btnLoad, btnShuffle);
topMenu.setAlignment(Pos.CENTER);
topMenu.setPadding(new Insets(10));
// Setup delle tre righe (spazio tra le carte impostato a 10px)
topRow = createRowContainer();
centerRow = createRowContainer();
bottomRow = createRowContainer();
// Contenitore verticale per le righe
VBox tableArea = new VBox(30,
new Label("TOP (8 Carte):"), topRow,
new Label("CENTER (6 Carte):"), centerRow,
new Label("BOTTOM (7 Carte):"), bottomRow
);
tableArea.setAlignment(Pos.CENTER);
tableArea.setPadding(new Insets(20));
// Mettiamo il tavolo in uno ScrollPane nel caso le carte siano troppo grandi per lo schermo
ScrollPane scrollPane = new ScrollPane(tableArea);
scrollPane.setFitToWidth(true);
BorderPane root = new BorderPane();
root.setTop(topMenu);
root.setCenter(scrollPane);
// Finestra un po' più grande per contenere tutte queste carte
Scene scene = new Scene(root, 1200, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
private HBox createRowContainer() {
HBox row = new HBox(15); // 15px di spazio tra una carta e l'altra
row.setAlignment(Pos.CENTER);
return row;
}
private void loadPdf(Stage stage) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Seleziona il PDF del mazzo di carte");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("File PDF", "*.pdf"));
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
try {
if (document != null) {
document.close();
}
logger.info("Caricamento file PDF: {}", file.getAbsolutePath());
document = PDDocument.load(file);
pdfRenderer = new PDFRenderer(document);
totalCards = document.getNumberOfPages();
logger.info("PDF caricato. Pagine totali (carte): {}", totalCards);
btnShuffle.setDisable(false);
// Distribuisci le carte per la prima volta
distributeRandomCards();
} catch (IOException e) {
logger.error("Errore nel caricamento del PDF", e);
}
}
}
private void distributeRandomCards() {
if (document == null || totalCards == 0) return;
logger.info("Inizio rimescolamento e distribuzione carte...");
// Pulisce il tavolo dalle carte precedenti
topRow.getChildren().clear();
centerRow.getChildren().clear();
bottomRow.getChildren().clear();
// 1. Crea una lista con tutti gli indici (da 0 al numero totale di pagine)
List<Integer> deckIndices = new ArrayList<>();
for (int i = 0; i < totalCards; i++) {
deckIndices.add(i);
}
// 2. Mescola la lista (Simula la mescolata del mazzo)
Collections.shuffle(deckIndices);
// 3. Distribuisci nelle righe (evitando IndexOutOfBounds se il PDF ha meno di 21 pagine)
// Riga Top: 8 carte
populateRow(topRow, deckIndices, 0, 8);
// Riga Center: 6 carte
populateRow(centerRow, deckIndices, 8, 6);
// Riga Bottom: 7 carte
populateRow(bottomRow, deckIndices, 14, 7);
logger.info("Distribuzione completata.");
}
private void populateRow(HBox row, List<Integer> deckIndices, int startIndex, int numberOfCards) {
for (int i = 0; i < numberOfCards; i++) {
int actualIndex = startIndex + i;
// Se abbiamo esaurito le carte nel PDF, interrompiamo
if (actualIndex >= deckIndices.size()) {
logger.warn("Il PDF non ha abbastanza carte per riempire questa riga.");
break;
}
int pageNumber = deckIndices.get(actualIndex);
ImageView cardImage = createCardImageView(pageNumber);
if (cardImage != null) {
row.getChildren().add(cardImage);
}
}
}
private ImageView createCardImageView(int pageIndex) {
try {
// Renderizza la pagina. Usiamo 100 DPI invece di 150 per risparmiare RAM,
// dato che ora stiamo generando 21 immagini contemporaneamente.
BufferedImage bim = pdfRenderer.renderImageWithDPI(pageIndex, 100);
Image fxImage = SwingFXUtils.toFXImage(bim, null);
ImageView imageView = new ImageView(fxImage);
// Impostiamo l'altezza fissa per le carte, in modo che stiano sullo schermo
imageView.setFitHeight(180);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.5), 5, 0, 0, 0);");
return imageView;
} catch (IOException e) {
logger.error("Errore durante il rendering della pagina {}", pageIndex, e);
return null;
}
}
@Override
public void stop() throws Exception {
if (document != null) {
document.close();
logger.info("Documento PDF chiuso correttamente.");
}
logger.info("Applicazione terminata.");
super.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,263 @@
package org.example.mesosll07;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
public class DeckGridApp2 extends Application {
private static final Logger logger = LogManager.getLogger(DeckGridApp2.class);
private PDDocument document;
private PDFRenderer pdfRenderer;
private int totalCards = 0;
// Contenitori Layout
private HBox topRow;
private HBox centerRow;
private HBox bottomRow;
private HBox playersArea; // NUOVO: Area per i giocatori
private Button btnShuffle;
// --- LOGICA DI GIOCO SIMULATA ---
public enum CardType {
CACCIA, UTENSILI, RITUALI, COSTRUZIONI
}
public static class Card {
int pdfPageIndex;
CardType type;
public Card(int pdfPageIndex, CardType type) {
this.pdfPageIndex = pdfPageIndex;
this.type = type;
}
public CardType getType() { return type; }
}
public static class Player {
String name;
int food;
int money;
List<Card> hand = new ArrayList<>();
public Player(String name, int food, int money) {
this.name = name;
this.food = food;
this.money = money;
}
public List<Card> getHand() { return hand; }
}
// --------------------------------
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Tavolo da Gioco - Board & Players");
Button btnLoad = new Button("Carica PDF Mazzo");
btnShuffle = new Button("Rimescola & Distribuisci");
btnShuffle.setDisable(true);
btnLoad.setOnAction(e -> loadPdf(primaryStage));
btnShuffle.setOnAction(e -> distributeAll());
HBox topMenu = new HBox(15, btnLoad, btnShuffle);
topMenu.setAlignment(Pos.CENTER);
topMenu.setPadding(new Insets(10));
topRow = createRowContainer();
centerRow = createRowContainer();
bottomRow = createRowContainer();
// NUOVO: Inizializza l'area giocatori
playersArea = new HBox(30);
playersArea.setAlignment(Pos.CENTER);
playersArea.setPadding(new Insets(20));
VBox tableArea = new VBox(20,
new Label("TOP (8 Carte):"), topRow,
new Label("CENTER (6 Carte):"), centerRow,
new Label("BOTTOM (7 Carte):"), bottomRow,
new Label("--- SITUAZIONE GIOCATORI ---"), playersArea
);
tableArea.setAlignment(Pos.CENTER);
tableArea.setPadding(new Insets(20));
ScrollPane scrollPane = new ScrollPane(tableArea);
scrollPane.setFitToWidth(true);
BorderPane root = new BorderPane();
root.setTop(topMenu);
root.setCenter(scrollPane);
Scene scene = new Scene(root, 1300, 900);
primaryStage.setScene(scene);
primaryStage.show();
}
private HBox createRowContainer() {
HBox row = new HBox(10);
row.setAlignment(Pos.CENTER);
return row;
}
private void loadPdf(Stage stage) {
FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("File PDF", "*.pdf"));
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
try {
if (document != null) document.close();
document = PDDocument.load(file);
pdfRenderer = new PDFRenderer(document);
totalCards = document.getNumberOfPages();
btnShuffle.setDisable(false);
distributeAll();
} catch (IOException e) {
logger.error("Errore PDF", e);
}
}
}
private void distributeAll() {
if (document == null || totalCards == 0) return;
topRow.getChildren().clear();
centerRow.getChildren().clear();
bottomRow.getChildren().clear();
playersArea.getChildren().clear();
List<Integer> deck = new ArrayList<>();
for (int i = 0; i < totalCards; i++) deck.add(i);
Collections.shuffle(deck);
// Distribuisce sul tavolo le prime 21 carte
int index = 0;
index = populateTable(topRow, deck, index, 8);
index = populateTable(centerRow, deck, index, 6);
index = populateTable(bottomRow, deck, index, 7);
// NUOVO: Simula 2 giocatori con le carte rimanenti
Random rand = new Random();
List<Player> players = Arrays.asList(
new Player("Giocatore 1 (Tribù Rossa)", rand.nextInt(15), rand.nextInt(50)),
new Player("Giocatore 2 (Tribù Blu)", rand.nextInt(15), rand.nextInt(50))
);
// Assegna 5 carte a caso a ogni giocatore e simula il loro Tipo
CardType[] types = CardType.values();
for (Player p : players) {
for (int i = 0; i < 5; i++) {
if (index < deck.size()) {
CardType randomType = types[rand.nextInt(types.length)];
p.getHand().add(new Card(deck.get(index), randomType));
index++;
}
}
}
// Renderizza i giocatori nella UI
renderPlayers(players);
}
private int populateTable(HBox row, List<Integer> deck, int startIndex, int amount) {
for (int i = 0; i < amount; i++) {
if (startIndex >= deck.size()) break;
ImageView card = createCardImage(deck.get(startIndex), 150); // Altezza 150px
if (card != null) row.getChildren().add(card);
startIndex++;
}
return startIndex;
}
// --- NUOVO: RENDERIZZAZIONE GIOCATORI E RAGGRUPPAMENTO (JAVA 8) ---
private void renderPlayers(List<Player> players) {
for (Player player : players) {
VBox playerBox = new VBox(10);
playerBox.setPadding(new Insets(15));
playerBox.setStyle("-fx-border-color: #555; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-color: #f9f9f9; -fx-background-radius: 10;");
// Intestazione Giocatore
Label nameLbl = new Label(player.name);
nameLbl.setFont(Font.font("System", FontWeight.BOLD, 16));
// Risorse Cibo e Soldi
Label statsLbl = new Label("🍖 Cibo: " + player.food + " | 💰 Money: " + player.money);
statsLbl.setTextFill(Color.DARKRED);
statsLbl.setFont(Font.font("System", FontWeight.BOLD, 14));
playerBox.getChildren().addAll(nameLbl, statsLbl);
// JAVA 8 MAGIA: Raggruppa le carte del giocatore per Tipo!
Map<CardType, List<Card>> groupedCards = player.getHand().stream()
.collect(Collectors.groupingBy(Card::getType));
// Itera sui gruppi creati e genera la grafica
groupedCards.forEach((type, cardsOfType) -> {
Label typeLbl = new Label("Tipo: " + type.name() + " (" + cardsOfType.size() + ")");
typeLbl.setFont(Font.font("System", FontWeight.NORMAL, 12));
HBox cardImagesRow = new HBox(5);
for (Card c : cardsOfType) {
// Carte più piccole (90px) per l'area giocatore
ImageView img = createCardImage(c.pdfPageIndex, 90);
if (img != null) cardImagesRow.getChildren().add(img);
}
playerBox.getChildren().addAll(typeLbl, cardImagesRow);
});
playersArea.getChildren().add(playerBox);
}
}
private ImageView createCardImage(int pageIndex, int height) {
try {
BufferedImage bim = pdfRenderer.renderImageWithDPI(pageIndex, 100);
Image fxImage = SwingFXUtils.toFXImage(bim, null);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
return imageView;
} catch (IOException e) {
logger.error("Errore rendering pagina {}", pageIndex, e);
return null;
}
}
@Override
public void stop() throws Exception {
if (document != null) document.close();
super.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,271 @@
package org.example.mesosll07;
import Server.Automaton.Game;
import Server.Era;
import Server.Player;
import Server.TotemColor;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import Server.Cards.*;
import java.nio.file.Paths;
public class DeckGridAppFX extends Application {
private static final Logger logger = LogManager.getLogger(DeckGridAppFX.class);
private PDDocument documentFront;
private PDDocument documentBack;
private PDFRenderer pdfRendererFront;
private PDFRenderer pdfRendererBack;
private int totalCards = 0;
// Contenitori Layout
private HBox topRow;
private HBox centerRow;
private HBox bottomRow;
private HBox playersArea; // NUOVO: Area per i giocatori
private Button btnShuffle;
// --------------------------------
@Override
public void start(Stage primaryStage) {
List<Player> players = new ArrayList<>();
players.add(new Player("Yoshi", TotemColor.YELLOW));
players.add(new Player("Pippo", TotemColor.PURPLE));
players.add(new Player("John", TotemColor.RED));
players.add(new Player("Baggio", TotemColor.BLUE));
players.add(new Player("Gino", TotemColor.GREEN));
Game game = new Game(players);
String fileCards="/home/lorenzo/dev/Mesos2/src/main/resources/files/cards.csv";
String fileCardsImgFront="/home/lorenzo/dev/Mesos2/src/main/resources/files/Cards_total_front_PROMO.pdf";
String fileCardsImgCover="/home/lorenzo/dev/Mesos2/src/main/resources/files/Cards_total_back_PROMO.pdf";
try {
File fileFront = new File(fileCardsImgFront);
File fileBack = new File(fileCardsImgCover);
documentFront = PDDocument.load(fileFront);
pdfRendererFront = new PDFRenderer(documentFront);
documentBack = PDDocument.load(fileBack);
pdfRendererBack = new PDFRenderer(documentBack);
} catch ( IOException e) {
throw new RuntimeException(e);
}
game.newGame(fileCards);
primaryStage.setTitle("Tavolo da Gioco - Board & Players");
btnShuffle = new Button("Rimescola & Distribuisci");
btnShuffle.setDisable(true);
btnShuffle.setOnAction(e -> distributeAll(game));
HBox topMenu = new HBox(15, btnShuffle);
topMenu.setAlignment(Pos.CENTER);
topMenu.setPadding(new Insets(10));
topRow = createRowContainer();
centerRow = createRowContainer();
bottomRow = createRowContainer();
// NUOVO: Inizializza l'area giocatori
playersArea = new HBox(30);
playersArea.setAlignment(Pos.CENTER);
playersArea.setPadding(new Insets(20));
distributeAll(game);
VBox tableArea = new VBox(20,
new Label("TOP"), topRow,
new Label("CENTER "), centerRow,
new Label("DOWN"), bottomRow,
new Label("--- SITUAZIONE GIOCATORI ---"), playersArea
);
tableArea.setAlignment(Pos.CENTER);
tableArea.setPadding(new Insets(20));
ScrollPane scrollPane = new ScrollPane(tableArea);
scrollPane.setFitToWidth(true);
BorderPane root = new BorderPane();
root.setTop(topMenu);
root.setCenter(scrollPane);
Scene scene = new Scene(root, 1300, 900);
primaryStage.setScene(scene);
primaryStage.show();
}
private HBox createRowContainer() {
HBox row = new HBox(10);
row.setAlignment(Pos.CENTER);
return row;
}
private void distributeAll(Game game) {
if (documentFront == null) return;
topRow.getChildren().clear();
centerRow.getChildren().clear();
bottomRow.getChildren().clear();
playersArea.getChildren().clear();
List<Integer> deck = new ArrayList<>();
for (int i = 0; i < 118; i++) deck.add(i);
Collections.shuffle(deck);
// Distribuisce sul tavolo le prime 21 carte
int index = 0;
index = populateTable(topRow, deck, index, 8);
index = populateTable(centerRow, deck, index, 6);
index = populateTable(bottomRow, deck, index, 7);
// NUOVO: Simula 2 giocatori con le carte rimanenti
Random rand = new Random();
List<Player> players =game.getPlayers();
// Assegna 5 carte a caso a ogni giocatore e simula il loro Tipo
for (Player p : players) {
for (int j=0;j<4;j++){
int idx = rand.nextInt(game.getGameBoard().getCardDeck().getTribeDeck().size());
Card c = game.getGameBoard().getCardDeck().drawTribeOne();
p.getPlayerTribe().addCharacter((CharacterCard) c);
}
for (int j=0;j<2;j++){
int idx = rand.nextInt(game.getGameBoard().getCardDeck().getBuildingDeck(Era.I).size());
Card c = game.getGameBoard().getCardDeck().drawBuildingOne(Era.I);
p.getPlayerTribe().addBuilding((BuildingCard) c);
}
}
// Renderizza i giocatori nella UI
renderPlayers(players);
}
private int populateTable(HBox row, List<Integer> deck, int startIndex, int amount) {
for (int i = 0; i < amount; i++) {
if (startIndex >= deck.size()) break;
ImageView card = createCardImage(deck.get(startIndex), 150, true); // Altezza 150px
if (card != null) row.getChildren().add(card);
startIndex++;
}
return startIndex;
}
// --- NUOVO: RENDERIZZAZIONE GIOCATORI E RAGGRUPPAMENTO (JAVA 8) ---
private void renderPlayers(List<Player> players) {
for (Player player : players) {
VBox playerBox = new VBox(10);
playerBox.setPadding(new Insets(15));
playerBox.setStyle("-fx-border-color: #555; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-color: #f9f9f9; -fx-background-radius: 10;");
// Intestazione Giocatore
Label nameLbl = new Label(player.getNickname());
nameLbl.setFont(Font.font("System", FontWeight.BOLD, 16));
// Risorse Cibo e Soldi
Label statsLbl = new Label("🍖 Cibo: " + player.getFoodTokens() + " | 💰 Money: " + player.getPrestigePoints());
statsLbl.setTextFill(Color.DARKRED);
statsLbl.setFont(Font.font("System", FontWeight.BOLD, 14));
playerBox.getChildren().addAll(nameLbl, statsLbl);
// JAVA 8 MAGIA: Raggruppa le carte del giocatore per Tipo!
Map<CharacterType, List<CharacterCard>> groupedCards = player.getPlayerTribe().getCharacters().stream()
.collect(Collectors.groupingBy(CharacterCard::getCharacterType));
// Itera sui gruppi creati e genera la grafica
groupedCards.forEach((type, cardsOfType) -> {
Label typeLbl = new Label("Tipo: " + type.name() + " (" + cardsOfType.size() + ")");
typeLbl.setFont(Font.font("System", FontWeight.NORMAL, 12));
HBox cardImagesRow = new HBox(5);
for (Card c : cardsOfType) {
// Carte più piccole (90px) per l'area giocatore
ImageView img = createCardImage(c.getCardId(), 90, true);
if (img != null) cardImagesRow.getChildren().add(img);
}
playerBox.getChildren().addAll(typeLbl, cardImagesRow);
});
playersArea.getChildren().add(playerBox);
}
}
private ImageView createCardImage(int pageIndex, int height, boolean front) {
try {
BufferedImage bim =null;
if (front) bim = pdfRendererFront.renderImageWithDPI(pageIndex, 100);
else bim = pdfRendererBack.renderImageWithDPI(pageIndex, 100);
Image fxImage = SwingFXUtils.toFXImage(bim, null);
ImageView imageView = new ImageView(fxImage);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 4, 0, 0, 0);");
return imageView;
} catch (IOException e) {
logger.error("Errore rendering pagina {}", pageIndex, e);
return null;
}
}
@Override
public void stop() throws Exception {
if (documentFront != null) documentFront.close();
super.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,159 @@
package org.example.mesosll07;
import Server.Automaton.Game;
import Server.Player;
import Server.TotemColor;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DeckViewerFX extends Application {
private PDDocument document;
private PDFRenderer pdfRenderer;
private int currentIndex = 0;
private int totalCards = 0;
private ImageView cardImageView;
private Label pageLabel;
private Button btnPrev;
private Button btnNext;
@Override
public void start(Stage primaryStage) {
List<Player> players = new ArrayList<>();
players.add(new Player("Yoshi", TotemColor.YELLOW));
players.add(new Player("Pippo", TotemColor.PURPLE));
players.add(new Player("John", TotemColor.RED));
players.add(new Player("Baggio", TotemColor.BLUE));
players.add(new Player("Gino", TotemColor.GREEN));
Game game = new Game(players);
game.newGame("/home/lorenzo/dev/Mesos2/src/main/resources/files/cards.csv");
primaryStage.setTitle("Visualizzatore Mazzo di Carte (PDF)");
// Componente per mostrare la carta
cardImageView = new ImageView();
cardImageView.setFitHeight(500); // Altezza fissa, larghezza in proporzione
cardImageView.setPreserveRatio(true);
cardImageView.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.5), 10, 0, 0, 0);");
// Controlli UI
btnPrev = new Button("⬅ Carta Precedente");
btnNext = new Button("Carta Successiva ➡");
pageLabel = new Label("Nessun mazzo caricato");
Button btnLoad = new Button("Carica PDF Mazzo");
// Azioni dei pulsanti
btnLoad.setOnAction(e -> loadPdf(primaryStage));
btnPrev.setOnAction(e -> showCard(currentIndex - 1));
btnNext.setOnAction(e -> showCard(currentIndex + 1));
updateButtons();
// Layout
HBox controls = new HBox(15, btnPrev, pageLabel, btnNext);
controls.setAlignment(Pos.CENTER);
VBox topBox = new VBox(10, btnLoad);
topBox.setAlignment(Pos.CENTER);
topBox.setPadding(new Insets(10));
BorderPane root = new BorderPane();
root.setTop(topBox);
root.setCenter(cardImageView);
root.setBottom(controls);
BorderPane.setMargin(cardImageView, new Insets(20));
BorderPane.setMargin(controls, new Insets(20));
Scene scene = new Scene(root, 600, 700);
primaryStage.setScene(scene);
primaryStage.show();
}
private void loadPdf(Stage stage) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Seleziona il PDF del mazzo di carte");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("File PDF", "*.pdf"));
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
try {
// Chiude il documento precedente se esiste
if (document != null) {
document.close();
}
// Carica il nuovo PDF
document = PDDocument.load(file);
pdfRenderer = new PDFRenderer(document);
totalCards = document.getNumberOfPages();
// Mostra la prima carta
showCard(0);
} catch (IOException e) {
e.printStackTrace();
pageLabel.setText("Errore nel caricamento del file!");
}
}
}
private void showCard(int index) {
if (document == null || index < 0 || index >= totalCards) return;
try {
// Renderizza la pagina del PDF in un'immagine BufferedImage (DPI 150 per buona qualità)
BufferedImage bim = pdfRenderer.renderImageWithDPI(index, 150);
// Converte l'immagine di AWT (Swing) in un'immagine JavaFX
Image fxImage = SwingFXUtils.toFXImage(bim, null);
cardImageView.setImage(fxImage);
currentIndex = index;
pageLabel.setText("Carta " + (currentIndex + 1) + " di " + totalCards);
updateButtons();
} catch (IOException e) {
e.printStackTrace();
}
}
private void updateButtons() {
btnPrev.setDisable(document == null || currentIndex == 0);
btnNext.setDisable(document == null || currentIndex == totalCards - 1);
}
@Override
public void stop() throws Exception {
// Importante: chiudere il documento per liberare la memoria quando l'app si chiude
if (document != null) {
document.close();
}
super.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,19 @@
package org.example.mesosll07;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
}

View File

@@ -0,0 +1,14 @@
package org.example.mesosll07;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class HelloController {
@FXML
private Label welcomeText;
@FXML
protected void onHelloButtonClick() {
welcomeText.setText("Welcome to JavaFX Application!");
}
}

View File

@@ -0,0 +1,9 @@
package org.example.mesosll07;
import javafx.application.Application;
public class Launcher {
public static void main(String[] args) {
Application.launch(HelloApplication.class, args);
}
}