commit f9d40590f83fad95ae409d983e42a45be014d880 Author: lorenzo Date: Mon Apr 13 09:46:34 2026 +0200 Initial commit of Mesos Java project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02ac4c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +.idea/ +*.iml +logs/ diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..02703c6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + org.example + Mesos2 + 1.0-SNAPSHOT + MesosLL07 + + + UTF-8 + 5.12.1 + 21.0.6 + 2.23.1 + + + + + + org.controlsfx + controlsfx + 11.2.1 + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-swing + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + + + org.apache.pdfbox + pdfbox + 2.0.29 + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + 25 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + default-cli + + org.example.mesosll07/org.example.mesosll07.HelloApplication + app + app + app + true + true + true + + + + + + + \ No newline at end of file diff --git a/src/main/java/Server/Automaton/ActionResult.java b/src/main/java/Server/Automaton/ActionResult.java new file mode 100644 index 0000000..4c7717a --- /dev/null +++ b/src/main/java/Server/Automaton/ActionResult.java @@ -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; } +} \ No newline at end of file diff --git a/src/main/java/Server/Automaton/Automaton.java b/src/main/java/Server/Automaton/Automaton.java new file mode 100644 index 0000000..b41996f --- /dev/null +++ b/src/main/java/Server/Automaton/Automaton.java @@ -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; + } + + +} diff --git a/src/main/java/Server/Automaton/Game.java b/src/main/java/Server/Automaton/Game.java new file mode 100644 index 0000000..bdee028 --- /dev/null +++ b/src/main/java/Server/Automaton/Game.java @@ -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 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 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 extraDrawQueue; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + public Game(List 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 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 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 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 others = new ArrayList<>(); + List 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 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 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 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 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); + } + + +} diff --git a/src/main/java/Server/Automaton/GameState.java b/src/main/java/Server/Automaton/GameState.java new file mode 100644 index 0000000..7420de9 --- /dev/null +++ b/src/main/java/Server/Automaton/GameState.java @@ -0,0 +1,68 @@ +package Server.Automaton; + +public enum GameState implements Comparable { + 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"; + } + } +} diff --git a/src/main/java/Server/BuildingManager.java b/src/main/java/Server/BuildingManager.java new file mode 100644 index 0000000..40857c4 --- /dev/null +++ b/src/main/java/Server/BuildingManager.java @@ -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 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 buildingCards) { + int numCard = 0; + for (BuildingCard buildingCard : buildingCards) { + + List playerCharacters = player.getPlayerTribe().getCharacters(); + + for (CharacterCard card : playerCharacters) { + if (card.getCharacterType() == buildingCard.getEffectTarget()) { + numCard++; + } + } + } + return numCard; + } + + public static List playerHasVariousBuildings(Player player, Trigger trigger) { + + List 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 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 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 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 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 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); + } +} diff --git a/src/main/java/Server/BuildingRules.java b/src/main/java/Server/BuildingRules.java new file mode 100644 index 0000000..5c2aaca --- /dev/null +++ b/src/main/java/Server/BuildingRules.java @@ -0,0 +1,48 @@ +package Server; + +import java.util.*; + +public class BuildingRules { + private static final Map> RULES = new HashMap<>(); + static { + // 2 players + Map p2 = new EnumMap<>(Era.class); + p2.put(Era.I, 1); + p2.put(Era.II, 2); + p2.put(Era.III, 3); + + // 3 players + Map p3 = new EnumMap<>(Era.class); + p3.put(Era.I, 2); + p3.put(Era.II, 2); + p3.put(Era.III, 4); + + // 4 players + Map p4 = new EnumMap<>(Era.class); + p4.put(Era.I, 2); + p4.put(Era.II, 3); + p4.put(Era.III, 4); + + // 5 players + Map 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 eraMap = RULES.get(players); + if (eraMap!=null){ + Integer value = eraMap.get(era); + if (value!=null) return value; + } + + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/Server/Cards/BuildingCard.java b/src/main/java/Server/Cards/BuildingCard.java new file mode 100644 index 0000000..78442d5 --- /dev/null +++ b/src/main/java/Server/Cards/BuildingCard.java @@ -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); + } +} diff --git a/src/main/java/Server/Cards/Card.java b/src/main/java/Server/Cards/Card.java new file mode 100644 index 0000000..45bb193 --- /dev/null +++ b/src/main/java/Server/Cards/Card.java @@ -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 + + '}'; + } +} diff --git a/src/main/java/Server/Cards/CardDeck.java b/src/main/java/Server/Cards/CardDeck.java new file mode 100644 index 0000000..e891bdd --- /dev/null +++ b/src/main/java/Server/Cards/CardDeck.java @@ -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 tribeDeck; + private Map> buildingDeck; + private static final Logger logger = LogManager.getLogger(CardDeck.class); + + public List getTribeDeck() { + return tribeDeck; + } + + public List getBuildingDeck(Era era) { + return buildingDeck.get(era); + } + + public void setForNPlayer(String path, int n) throws LoadingCardsException { + + List tribe = new ArrayList<>(); + List building = new ArrayList<>(); + + try { + List 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 drawTribe(int n) { + List 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 drawBuilding(int n, Era era) { + List 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 shuffle(List cards) { + List shuffled = new ArrayList<>(); + + for(Era e: Era.values()) { + List 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; + } +} diff --git a/src/main/java/Server/Cards/CharacterCard.java b/src/main/java/Server/Cards/CharacterCard.java new file mode 100644 index 0000000..dd377d6 --- /dev/null +++ b/src/main/java/Server/Cards/CharacterCard.java @@ -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 + + '}'; + } +} diff --git a/src/main/java/Server/Cards/CharacterType.java b/src/main/java/Server/Cards/CharacterType.java new file mode 100644 index 0000000..6c6b938 --- /dev/null +++ b/src/main/java/Server/Cards/CharacterType.java @@ -0,0 +1,10 @@ +package Server.Cards; + +public enum CharacterType { + INVENTOR, + HUNTER, + GATHERER, + SHAMAN, + ARTIST, + BUILDER +} diff --git a/src/main/java/Server/Cards/Event.java b/src/main/java/Server/Cards/Event.java new file mode 100644 index 0000000..3f46f28 --- /dev/null +++ b/src/main/java/Server/Cards/Event.java @@ -0,0 +1,8 @@ +package Server.Cards; + +public enum Event { + SUSTAINMENT, + HUNT, + SHAMANIC_RITUAL, + CAVE_PAINTINGS +} diff --git a/src/main/java/Server/Cards/EventCard.java b/src/main/java/Server/Cards/EventCard.java new file mode 100644 index 0000000..c9de2e8 --- /dev/null +++ b/src/main/java/Server/Cards/EventCard.java @@ -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 + + '}'; + } +} diff --git a/src/main/java/Server/Cards/Trigger.java b/src/main/java/Server/Cards/Trigger.java new file mode 100644 index 0000000..9c583c9 --- /dev/null +++ b/src/main/java/Server/Cards/Trigger.java @@ -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 +} diff --git a/src/main/java/Server/DeckGridAppFX.java b/src/main/java/Server/DeckGridAppFX.java new file mode 100644 index 0000000..dd8aae1 --- /dev/null +++ b/src/main/java/Server/DeckGridAppFX.java @@ -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 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 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 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> 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> 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); + } +} \ No newline at end of file diff --git a/src/main/java/Server/Era.java b/src/main/java/Server/Era.java new file mode 100644 index 0000000..d797dd4 --- /dev/null +++ b/src/main/java/Server/Era.java @@ -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; + } +} diff --git a/src/main/java/Server/EventsSolver.java b/src/main/java/Server/EventsSolver.java new file mode 100644 index 0000000..23bb2ae --- /dev/null +++ b/src/main/java/Server/EventsSolver.java @@ -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 events, List 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 sustainment(EventCard event, List players){ + + if(event.getEvent() != Event.SUSTAINMENT){throw new EventsManagerException("Not a sustainment card");} + + List 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 hunt(EventCard event, List players){ + if(event.getEvent() != Event.HUNT){throw new EventsManagerException("Not a hunt card");} + + List 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 shamanicRitual(EventCard event, List players){ + if(event.getEvent() != Event.SHAMANIC_RITUAL){throw new EventsManagerException("Not a shamanic ritual card");} + List 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 cavePaintings(EventCard event, List players){ + + if(event.getEvent() != Event.CAVE_PAINTINGS){throw new EventsManagerException("Not a cave painting card");} + List 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; + } +} diff --git a/src/main/java/Server/GameBoard.java b/src/main/java/Server/GameBoard.java new file mode 100644 index 0000000..4684da7 --- /dev/null +++ b/src/main/java/Server/GameBoard.java @@ -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 topRow; + private final List bottomRow; + private final List offeringTiles; + private final TurnTile turnTile; + private final BuildingManager buildingManager; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + /** + * Creates the board for a new game. + * Call {@link #setupInitialRows(int)} and {@link #initOfferingTiles(int)} + * separately after construction (mirrors the rulebook setup steps). + */ + public GameBoard(Era startingEra, CardDeck cardDeck, int numPlayers) { + this.era = startingEra; + this.cardDeck = cardDeck; + this.topRow = new ArrayList<>(); + this.bottomRow = new ArrayList<>(); + this.offeringTiles = new ArrayList<>(); + this.turnTile = new TurnTile(numPlayers); + this.buildingManager = new BuildingManager(); + } + + // ------------------------------------------------------------------------- + // Setup + // ------------------------------------------------------------------------- + + /** + * Creates and places the correct OfferingTiles for the given player count. + * + * Tile layout (from the rulebook): + * orderId 0 → tile A: 3 food (5-player only) + * orderId 1 → tile B: 1 DOWN (all) + * orderId 2 → tile C: 1 UP (all) + * orderId 3 → tile D: 2 DOWN (3+ players) + * orderId 4 → tile E: 1 DOWN 1 UP (all) + * orderId 5 → tile F: 2 UP (all) + * orderId 6 → tile G: 1 DOWN 2 UP (4+ players) + * + * With n players, exactly n tiles are used, starting from tile A if + * 5 players, otherwise starting from tile B. + */ + public void initOfferingTiles(int numPlayers) { + offeringTiles.clear(); + switch (numPlayers) { + case 2: + offeringTiles.add(new OfferingTile(1)); + offeringTiles.add(new OfferingTile(2)); + offeringTiles.add(new OfferingTile(4)); + offeringTiles.add(new OfferingTile(5)); + break; + case 3: + offeringTiles.add(new OfferingTile(1)); + offeringTiles.add(new OfferingTile(2)); + offeringTiles.add(new OfferingTile(3)); + offeringTiles.add(new OfferingTile(4)); + offeringTiles.add(new OfferingTile(5)); + break; + case 4: + offeringTiles.add(new OfferingTile(1)); + offeringTiles.add(new OfferingTile(2)); + offeringTiles.add(new OfferingTile(3)); + offeringTiles.add(new OfferingTile(4)); + offeringTiles.add(new OfferingTile(5)); + offeringTiles.add(new OfferingTile(6)); + break; + case 5: + offeringTiles.add(new OfferingTile(0)); + offeringTiles.add(new OfferingTile(1)); + offeringTiles.add(new OfferingTile(2)); + offeringTiles.add(new OfferingTile(3)); + offeringTiles.add(new OfferingTile(4)); + offeringTiles.add(new OfferingTile(5)); + offeringTiles.add(new OfferingTile(6)); + break; + + } + + } + + // Find the oddertingTile selected by Player + public OfferingTile getOfferingTile(Player p){ + OfferingTile offering = offeringTiles.stream().filter(o -> p.equals(o.getOccupant())).findFirst().orElse(null); + return offering; + } + /** + * Draws the initial two rows of cards according to rulebook setup steps 4-5: + * + * Bottom row: draw cards one at a time until (numPlayers + 1) Character cards + * have been placed. Any Event card drawn is placed in the top row instead. + * Top row: fill to (numPlayers + 4) with additional draws (accounting for + * any events already placed there from the bottom-row draw). + */ + public void setupInitialRows(int numPlayers) { + topRow.clear(); + bottomRow.clear(); + + // Draw bottom row — events are bumped to top row + while (bottomRow.size() < numPlayers + 1) { + Card card = cardDeck.drawTribeOne(); + if (card instanceof EventCard) { + topRow.add(card); + logger.info("Setup: Event card " + card.getCardId() + " moved to top row."); + } else { + bottomRow.add(card); + } + } + + // Fill top row up to numPlayers + 4 + int needed = (numPlayers + 4) - topRow.size(); + if (needed > 0) { + topRow.addAll(cardDeck.drawTribe(needed)); + } + + // Add all Building Era.I on top row 2P 1 3-4-5 2 + topRow.add(cardDeck.drawBuildingOne(Era.I)); + if (numPlayers > 2){ + topRow.add(cardDeck.drawBuildingOne(Era.I)); + } + } + + // ------------------------------------------------------------------------- + // End-of-round row management (rulebook "Fine del Round" steps 2-4) + // ------------------------------------------------------------------------- + + /** + * Performs the three row-management steps at the end of every round: + * + * Step 2 — Discard all Character and Event cards from the bottom row. + * Building cards stay. + * Step 3 — Move all Character and Event cards from the top row down to + * the bottom row. Building cards stay in the top row. + * Step 4 — Draw (numPlayers + 4) new cards into the top row. + * + * @return true if the newly drawn cards contain a card from the next Era, + * signalling that {@link #triggerEraChange()} should be called. + */ + public boolean advanceRows(int numPlayers) { + + Era eraBeforeDraw = this.era; + + // Step 2: discard non-building cards from the bottom row + bottomRow.removeIf( + c -> !(c instanceof BuildingCard) + ); + + // Step 3: move non-building cards from top row down to bottom row + Iterator it = topRow.iterator(); + while (it.hasNext()) { + Card c = it.next(); + if (!(c instanceof BuildingCard)) { + logger.debug("move card from top to bottom " + c); + bottomRow.add(c); + it.remove(); + } + } + + // Step 4: draw fresh cards into the top row + List newCards = cardDeck.drawTribe(numPlayers + 4); + topRow.addAll(newCards); + logger.info("NEW CARDS ON TOP {} " , newCards); + return isNewEraRevealed(eraBeforeDraw, newCards); + } + + /** + * Returns true if any of the newly drawn cards belongs to an era + * that is strictly later than the current one. + */ + private boolean isNewEraRevealed(Era currentEra, List newCards) { + for (Card c : newCards) { + Era cardEra = eraOf(c); + if (cardEra != null && cardEra.ordinal() > currentEra.ordinal()) { + logger.info("FOUND NEW ERA {} " , cardEra); + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------- + // Era change (rulebook "Inizio della Nuova Era") + // ------------------------------------------------------------------------- + + /** + * Advances the era and performs the three building-row transitions: + * + * Step 1 — (Era III only) Discard all Building cards from the bottom row. + * Step 2 — Move all Building cards from the top row to the bottom row + * (to the right of the Tribe cards). [Era II and III] + * Step 3 — Place the new era's Building cards face-up in the top row. + * [Era II and III] + * + * @return the new Era after the transition + */ + public Era triggerEraChange(int numPlayers) { + era = era.next(); // Era.I → II → III → FINAL + logger.info("ERA CHANGED → {} ", era); + + // Step 1 (Era III only): remove old era buildings from the bottom row + if (era == Era.III) { + bottomRow.removeIf(c -> c instanceof BuildingCard); + } + + // Step 2: move buildings from top row to bottom row + List descending = new ArrayList<>(); + Iterator it = topRow.iterator(); + while (it.hasNext()) { + Card c = it.next(); + if (c instanceof BuildingCard) { + descending.add((BuildingCard) c); + it.remove(); + } + } + bottomRow.addAll(descending); + logger.info("event=MOVED_BUILING era={} count={} cards={}", era, descending.size(), descending); + + // Step 3: place the new era's building cards in the top row + int n = BuildingRules.getBuildingCards(numPlayers, era); + List newEraBuildings = cardDeck.drawBuilding(n , era); + topRow.addAll(newEraBuildings); + + logger.info("event=ADD_BUILDINGS era={} count={} cards={}", era, n, newEraBuildings); + return era; + } + + // ------------------------------------------------------------------------- + // Offering tiles + // ------------------------------------------------------------------------- + + /** + * Attempts to place a player's totem on an offering tile. + * + * @return true if successful; false if the tile was already occupied + */ + public boolean placeTotem(Player player, OfferingTile tile, TurnTile turntile) { + if (!tile.isEmpty()) return false; + tile.setOccupant(player); + turntile.leaveTurnTile(player); + return true; + } + + /** + * Removes all totems from offering tiles. Call at the start of each new round. + */ + public void clearOfferingTiles() { + for (OfferingTile tile : offeringTiles) { + tile.removeOccupant(); + } + } + + // ------------------------------------------------------------------------- + // Card removal (called by Game when a player takes a card) + // ------------------------------------------------------------------------- + + /** + * Removes a card from the top row by ID and returns it. + * + * @return the card, or null if no card with that ID exists in the top row + */ + public Card takeFromTopRow(int cardId) { + return takeFromRow(topRow, cardId); + } + + /** + * Removes a card from the bottom row by ID and returns it. + * + * @return the card, or null if no card with that ID exists in the bottom row + */ + public Card takeFromBottomRow(int cardId) { + return takeFromRow(bottomRow, cardId); + } + + private Card takeFromRow(List row, int cardId) { + Iterator it = row.iterator(); + while (it.hasNext()) { + Card c = it.next(); + if (c.getCardId() == cardId) { + it.remove(); + return c; + } + } + return null; + } + + // ------------------------------------------------------------------------- + // Event visibility + // ------------------------------------------------------------------------- + + /** + * Returns all EventCards currently in the bottom row, sorted by era + * (ascending) so they are resolved in the correct order. + * Sustainment is NOT sorted last here — that is Game's responsibility. + */ + public List getVisibleEvents() { + List events = new ArrayList<>(); + for (Card c : bottomRow) { + if (c instanceof EventCard) { + events.add((EventCard) c); + } + } + events.sort((a, b) -> a.getEra().ordinal() - b.getEra().ordinal()); + return events; + } + + /** + * Returns EventCards from BOTH rows. + * Used during the final round when all visible events must be resolved. + */ + public List getAllVisibleEvents() { + List events = new ArrayList<>(); + for (Card c : bottomRow) { + if (c instanceof EventCard) events.add((EventCard) c); + } + for (Card c : topRow) { + if (c instanceof EventCard) events.add((EventCard) c); + } + events.sort((a, b) -> a.getEra().ordinal() - b.getEra().ordinal()); + return events; + } + + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Extracts the Era from a card, returning null for building cards. */ + private Era eraOf(Card c) { + if (c instanceof CharacterCard) return ((CharacterCard) c).getEra(); + if (c instanceof EventCard) return ((EventCard) c).getEra(); + return null; + } + + // ------------------------------------------------------------------------- + // Getters + // ------------------------------------------------------------------------- + + public Era getEra() { return era; } + public List getTopRow() { return topRow; } + public List getBottomRow() { return bottomRow; } + public List getOfferingTiles() { return offeringTiles; } + public TurnTile getTurnTile() { return turnTile; } + public BuildingManager getBuildingManager() { return buildingManager; } + public CardDeck getCardDeck() { return cardDeck; } + + + + @Override + public String toString() { + return "GameBoard{" + + "era=" + era + + ",\n offeringTiles=" + offeringTiles + + ",\n turnTile=" + turnTile + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/Server/OfferingTile.java b/src/main/java/Server/OfferingTile.java new file mode 100644 index 0000000..1c9b3dd --- /dev/null +++ b/src/main/java/Server/OfferingTile.java @@ -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 (A–G) 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 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 buildActions(int orderId) { + List 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 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() + "]"); + } +} diff --git a/src/main/java/Server/Player.java b/src/main/java/Server/Player.java new file mode 100644 index 0000000..23e7ea5 --- /dev/null +++ b/src/main/java/Server/Player.java @@ -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 + + ')'; + } +} diff --git a/src/main/java/Server/Symbol.java b/src/main/java/Server/Symbol.java new file mode 100644 index 0000000..e0a67a5 --- /dev/null +++ b/src/main/java/Server/Symbol.java @@ -0,0 +1,7 @@ +package Server; + +public enum Symbol { + UP, + DOWN, + FOOD +} diff --git a/src/main/java/Server/TotemColor.java b/src/main/java/Server/TotemColor.java new file mode 100644 index 0000000..5ad405b --- /dev/null +++ b/src/main/java/Server/TotemColor.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/Server/Tribe.java b/src/main/java/Server/Tribe.java new file mode 100644 index 0000000..e38fec3 --- /dev/null +++ b/src/main/java/Server/Tribe.java @@ -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 characters; + private List buildings; + + // Constructor + public Tribe() { + this.characters = new ArrayList<>(); + this.buildings = new ArrayList<>(); + } + + public List getCharacters() { + return characters; + } + public List 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 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; + } +} diff --git a/src/main/java/Server/TurnTile.java b/src/main/java/Server/TurnTile.java new file mode 100644 index 0000000..f050f3c --- /dev/null +++ b/src/main/java/Server/TurnTile.java @@ -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 players) { + + List 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 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 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/mesosll07/DeckGridApp2.java b/src/main/java/org/example/mesosll07/DeckGridApp2.java new file mode 100644 index 0000000..88d1db1 --- /dev/null +++ b/src/main/java/org/example/mesosll07/DeckGridApp2.java @@ -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 hand = new ArrayList<>(); + + public Player(String name, int food, int money) { + this.name = name; + this.food = food; + this.money = money; + } + public List 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 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 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 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 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> 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/mesosll07/DeckGridAppFX.java b/src/main/java/org/example/mesosll07/DeckGridAppFX.java new file mode 100644 index 0000000..60a2d52 --- /dev/null +++ b/src/main/java/org/example/mesosll07/DeckGridAppFX.java @@ -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 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 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 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 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 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> 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/mesosll07/DeckViewerFX.java b/src/main/java/org/example/mesosll07/DeckViewerFX.java new file mode 100644 index 0000000..d1daf76 --- /dev/null +++ b/src/main/java/org/example/mesosll07/DeckViewerFX.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/mesosll07/HelloApplication.java b/src/main/java/org/example/mesosll07/HelloApplication.java new file mode 100644 index 0000000..eb30004 --- /dev/null +++ b/src/main/java/org/example/mesosll07/HelloApplication.java @@ -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(); + } +} diff --git a/src/main/java/org/example/mesosll07/HelloController.java b/src/main/java/org/example/mesosll07/HelloController.java new file mode 100644 index 0000000..089e087 --- /dev/null +++ b/src/main/java/org/example/mesosll07/HelloController.java @@ -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!"); + } +} diff --git a/src/main/java/org/example/mesosll07/Launcher.java b/src/main/java/org/example/mesosll07/Launcher.java new file mode 100644 index 0000000..70be22e --- /dev/null +++ b/src/main/java/org/example/mesosll07/Launcher.java @@ -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); + } +} diff --git a/src/main/resources/files/Cards_total_back_PROMO.pdf b/src/main/resources/files/Cards_total_back_PROMO.pdf new file mode 100644 index 0000000..c7c5ab0 Binary files /dev/null and b/src/main/resources/files/Cards_total_back_PROMO.pdf differ diff --git a/src/main/resources/files/Cards_total_front_PROMO.pdf b/src/main/resources/files/Cards_total_front_PROMO.pdf new file mode 100644 index 0000000..63d46f4 Binary files /dev/null and b/src/main/resources/files/Cards_total_front_PROMO.pdf differ diff --git a/src/main/resources/files/Start_2P.png b/src/main/resources/files/Start_2P.png new file mode 100644 index 0000000..19b0765 Binary files /dev/null and b/src/main/resources/files/Start_2P.png differ diff --git a/src/main/resources/files/Start_3P.png b/src/main/resources/files/Start_3P.png new file mode 100644 index 0000000..5c34aa9 Binary files /dev/null and b/src/main/resources/files/Start_3P.png differ diff --git a/src/main/resources/files/Start_4P.png b/src/main/resources/files/Start_4P.png new file mode 100644 index 0000000..a83070a Binary files /dev/null and b/src/main/resources/files/Start_4P.png differ diff --git a/src/main/resources/files/Start_5P.png b/src/main/resources/files/Start_5P.png new file mode 100644 index 0000000..f7e72c2 Binary files /dev/null and b/src/main/resources/files/Start_5P.png differ diff --git a/src/main/resources/files/cards.csv b/src/main/resources/files/cards.csv new file mode 100644 index 0000000..e67206d --- /dev/null +++ b/src/main/resources/files/cards.csv @@ -0,0 +1,117 @@ +C;1;0;I;HUNTER;1;0 +C;2;0;I;HUNTER;1;0 +C;3;0;I;HUNTER;0;0 +C;4;3;I;HUNTER;0;0 +C;5;3;I;HUNTER;0;0 +C;6;0;I;BUILDER;1;3 +C;7;0;I;BUILDER;2;0 +C;8;5;I;BUILDER;2;1 +C;9;0;I;BUILDER;1;2 +C;10;0;I;GATHERER;3;0 +C;11;0;I;GATHERER;3;0 +C;12;3;I;GATHERER;3;0 +C;13;5;I;GATHERER;3;0 +C;14;0;I;ARTIST;0;0 +C;15;0;I;ARTIST;0;0 +C;16;0;I;ARTIST;0;0 +C;17;3;I;ARTIST;0;0 +C;18;4;I;ARTIST;0;0 +C;19;0;I;INVENTOR;8;0 +C;20;0;I;INVENTOR;0;0 +C;21;0;I;INVENTOR;1;0 +C;22;0;I;INVENTOR;9;0 +C;23;4;I;INVENTOR;5;0 +C;24;4;I;INVENTOR;7;0 +C;25;4;I;INVENTOR;4;0 +C;26;5;I;SHAMAN;2;0 +C;27;0;I;SHAMAN;2;0 +C;28;0;I;SHAMAN;1;0 +C;29;4;I;SHAMAN;1;0 +C;30;0;II;HUNTER;0;0 +C;31;0;II;HUNTER;0;0 +C;32;3;II;HUNTER;1;0 +C;33;0;II;HUNTER;1;0 +C;34;4;II;HUNTER;1;0 +C;35;5;II;HUNTER;0;0 +C;36;0;II;BUILDER;1;4 +C;37;0;II;BUILDER;2;1 +C;38;3;II;BUILDER;1;2 +C;39;0;II;BUILDER;2;3 +C;40;0;II;GATHERER;3;0 +C;41;3;II;GATHERER;3;0 +C;42;4;II;GATHERER;3;0 +C;43;5;II;GATHERER;3;0 +C;44;3;II;ARTIST;0;0 +C;45;0;II;ARTIST;0;0 +C;46;0;II;ARTIST;0;0 +C;47;0;II;ARTIST;0;0 +C;48;0;II;INVENTOR;0;0 +C;49;4;II;INVENTOR;0;0 +C;50;0;II;INVENTOR;0;0 +C;51;0;II;INVENTOR;0;0 +C;52;0;II;INVENTOR;0;0 +C;53;0;II;INVENTOR;0;0 +C;54;0;II;SHAMAN;2;0 +C;55;0;II;SHAMAN;2;0 +C;56;5;II;SHAMAN;1;0 +C;57;5;II;SHAMAN;2;0 +C;58;5;III;HUNTER;1;0 +C;59;0;III;HUNTER;0;0 +C;60;0;III;HUNTER;0;0 +C;61;0;III;HUNTER;1;0 +C;62;0;III;BUILDER;1;5 +C;63;0;III;BUILDER;2;3 +C;64;5;III;BUILDER;1;4 +C;65;0;III;BUILDER;2;2 +C;66;5;III;GATHERER;3;0 +C;67;4;III;GATHERER;3;0 +C;68;0;III;GATHERER;3;0 +C;69;5;III;ARTIST;0;0 +C;70;0;III;ARTIST;0;0 +C;71;0;III;ARTIST;0;0 +C;72;0;III;ARTIST;0;0 +C;73;4;III;INVENTOR;0;0 +C;74;3;III;INVENTOR;0;0 +C;75;3;III;INVENTOR;0;0 +C;76;0;III;INVENTOR;0;0 +C;77;0;III;INVENTOR;0;0 +C;78;0;III;INVENTOR;0;0 +C;79;0;III;INVENTOR;0;0 +C;80;3;III;SHAMAN;2;0 +C;81;0;III;SHAMAN;3;0 +C;82;0;III;SHAMAN;2;0 +C;83;0;III;SHAMAN;3;0 +C;84;4;III;SHAMAN;2;0 +E;85;0;I;HUNT;1;1 +E;86;0;I;SUSTAINMENT;1;1 +E;87;0;I;SHAMANIC_RITUAL;5;3 +E;88;0;I;CAVE_PAINTINGS;2;1 +E;89;0;II;HUNT;1;2 +E;90;0;II;SUSTAINMENT;1;2 +E;91;0;II;SHAMANIC_RITUAL;10;5 +E;92;0;II;CAVE_PAINTINGS;2;2 +E;93;0;III;HUNT;1;3 +E;94;0;III;CAVE_PAINTINGS;2;3 +E;95;0;FINAL;SUSTAINMENT;1;3 +E;96;0;FINAL;SHAMANIC_RITUAL;15;7 +B;97;0;I;4;3;FOOD_FOR_SIX +B;98;0;I;4;4;SUSTAIN_DISCOUNT +B;99;0;I;5;3;SUSTAIN_DISCOUNT +B;100;0;I;5;2;SHAMAN_NO_LOSS +B;101;0;I;3;3;BONUS_FOOD_ENDTURN +B;102;0;I;3;4;FOOD_PER_INVENTORS +B;103;0;II;7;0;SHAMAN_DOUBLE_POINTS +B;104;0;II;6;4;SHAMAN_BONUS +B;105;0;II;7;4;SUSTAIN_DISCOUNT +B;106;0;II;7;2;HUNT_BONUS +B;107;0;II;6;4;ENDGAME_BUILDER_BONUS +B;108;0;II;5;6;PAINTING_FOOD_BONUS +B;109;0;II;5;6;ENDGAME_FOR_SIX +B;110;0;III;8;8;ENDGAME_BONUS_CHARACTER +B;111;0;III;7;6;ENDGAME_BONUS_CHARACTER +B;112;0;III;7;4;ENDGAME_BONUS_CHARACTER +B;113;0;III;6;3;ENDGAME_BONUS_CHARACTER +B;114;0;III;7;4;ENDGAME_BONUS_CHARACTER +B;115;0;III;6;6;ENDGAME_BONUS_CHARACTER +B;116;0;III;9;3;EXTRA_DRAW +B;117;0;III;10;0;ENDGAME_BONUS_POINTS \ No newline at end of file diff --git a/src/main/resources/files/offeringA.png b/src/main/resources/files/offeringA.png new file mode 100644 index 0000000..cb65204 Binary files /dev/null and b/src/main/resources/files/offeringA.png differ diff --git a/src/main/resources/files/offeringB.png b/src/main/resources/files/offeringB.png new file mode 100644 index 0000000..5b7accc Binary files /dev/null and b/src/main/resources/files/offeringB.png differ diff --git a/src/main/resources/files/offeringC.png b/src/main/resources/files/offeringC.png new file mode 100644 index 0000000..c1d0826 Binary files /dev/null and b/src/main/resources/files/offeringC.png differ diff --git a/src/main/resources/files/offeringD.png b/src/main/resources/files/offeringD.png new file mode 100644 index 0000000..65b3f6d Binary files /dev/null and b/src/main/resources/files/offeringD.png differ diff --git a/src/main/resources/files/offeringE.png b/src/main/resources/files/offeringE.png new file mode 100644 index 0000000..03dfb12 Binary files /dev/null and b/src/main/resources/files/offeringE.png differ diff --git a/src/main/resources/files/offeringF.png b/src/main/resources/files/offeringF.png new file mode 100644 index 0000000..76ff8d4 Binary files /dev/null and b/src/main/resources/files/offeringF.png differ diff --git a/src/main/resources/files/offeringG.png b/src/main/resources/files/offeringG.png new file mode 100644 index 0000000..225e5b9 Binary files /dev/null and b/src/main/resources/files/offeringG.png differ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..95a6c2b --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/org/example/mesosll07/hello-view.fxml b/src/main/resources/org/example/mesosll07/hello-view.fxml new file mode 100644 index 0000000..bce0e78 --- /dev/null +++ b/src/main/resources/org/example/mesosll07/hello-view.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + +