diff --git a/.travis.yml b/.travis.yml index a96a774d..1cdf7ef5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,6 @@ jobs: - git config --local user.email "daniele.tentoni.1996@gmail.com" - export TRAVIS_TAG=${TRAVIS_TAG:-$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)} - git tag $TRAVIS_TAG - before_deploy: - sbt clean packArchive deploy: provider: releases @@ -77,7 +76,9 @@ jobs: token: $GH_TOKEN file_glob: true file: + - target/client-*.tar.gz - target/client-*.zip + - target/server-*.tar.gz - target/server-*.zip on: branch: master @@ -85,7 +86,7 @@ jobs: stages: - name: Run sbt with OpenJdk11 - if: type IN (pull_request, push) && !(branch = master) + if: type IN (pull_request, push) - name: Generate coverage and scaladoc if: type = push && branch IN (master, dev) - name: Deploy to Github Releases diff --git a/README.md b/README.md index d8ce7d54..de224289 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,46 @@ -# Scalavelli project - -### Build status -| Branch | Status | -| --- | --- | -| Master | [![Build Status](https://travis-ci.com/Part-Time-Team/scalavelli.svg?branch=master)](https://travis-ci.com/Part-Time-Team/scalavelli) | -| Dev | [![Build Status](https://travis-ci.com/Part-Time-Team/scalavelli.svg?branch=dev)](https://travis-ci.com/Part-Time-Team/scalavelli) | - -### Coverage status -| Branch | Status | -| --- | --- | -| Master | [![codecov](https://codecov.io/gh/Part-Time-Team/scalavelli/branch/master/graph/badge.svg)](https://codecov.io/gh/Part-Time-Team/scalavelli) | -| Dev | [![codecov](https://codecov.io/gh/Part-Time-Team/scalavelli/branch/dev/graph/badge.svg)](https://codecov.io/gh/Part-Time-Team/scalavelli) | +# Scalavelli project Project for PPS. - Use openjdk11 and scala 2.12.8. -### How to execute +### Build status +| Branch | Build | Coverage | +| --- | --- | --- | +| Master | [![Build Status](https://travis-ci.com/Part-Time-Team/scalavelli.svg?branch=master)](https://travis-ci.com/Part-Time-Team/scalavelli) | [![codecov](https://codecov.io/gh/Part-Time-Team/scalavelli/branch/master/graph/badge.svg)](https://codecov.io/gh/Part-Time-Team/scalavelli) | +| Dev | [![Build Status](https://travis-ci.com/Part-Time-Team/scalavelli.svg?branch=dev)](https://travis-ci.com/Part-Time-Team/scalavelli) | [![codecov](https://codecov.io/gh/Part-Time-Team/scalavelli/branch/dev/graph/badge.svg)](https://codecov.io/gh/Part-Time-Team/scalavelli) | + +### How to compile Move to main dir of project and execute following commands to compile executable packages ```shell script -sbt pack +sbt clean pack ``` -This command produce executable scripts in client and server directories. You can execute them with those commands: +You can also modify configuration files to execute the server on a different machine. You have to change the Constant as: -| Project | Command | -| --- | --- | -| Server | `./server/target/pack/bin/scalavelli-server` | -| Client | `./client/target/pack/bin/applauncher` | +```scala +// Run on a LAN machine. +final val SERVER_ADDRESS = "192.168.43.21" +final val SERVER_PORT = 8081 +``` + +```scala +// Run on the local machine. +final val SERVER_ADDRESS = "localhost" +final val SERVER_PORT = 5150 +``` Look to our travis.yml file for packaging scripts. +### How to execute + +This command produces executable scripts in client and server directories. You can execute them with those commands: + +| Project | Linux | Windows | +| --- | --- | --- | +| Server | `./server/target/pack/bin/scalavelli-server` | `.\server\target\pack\bin\scalavelli-server` | +| Client | `./client/target/pack/bin/applauncher` | `.\client\target\pack\bin\applauncher` | + Be aware to execute first the server and after that how many clients you want. -You can also modify configuration files to execute server on a different machine. -We don't provide guide to deploy on remote machine or containers. \ No newline at end of file + +We don't provide the guide to deploy on a remote machine or containers. diff --git a/build.sbt b/build.sbt index e1858325..b0f6dc3b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ name := "scalavelli" -version in ThisBuild := "0.1.2" +version in ThisBuild := "0.1.3" scalaVersion in ThisBuild := "2.12.8" organization in ThisBuild := "it.parttimeteam" @@ -132,4 +132,4 @@ lazy val client = Project( /* * END PROJECT DEFINITIONS. - */ \ No newline at end of file + */ diff --git a/client/src/main/resources/application.conf b/client/src/main/resources/application.conf index 4981f483..3677d2ec 100644 --- a/client/src/main/resources/application.conf +++ b/client/src/main/resources/application.conf @@ -1,6 +1,6 @@ akka { - loglevel = "DEBUG" + loglevel = "INFO" actor { provider = remote diff --git a/client/src/main/resources/images/game_title.png b/client/src/main/resources/images/game_title.png index 2e7b2c4d..07ed0d67 100644 Binary files a/client/src/main/resources/images/game_title.png and b/client/src/main/resources/images/game_title.png differ diff --git a/client/src/main/scala/it/parttimeteam/controller/MainControllerImpl.scala b/client/src/main/scala/it/parttimeteam/controller/MainControllerImpl.scala index 5f6077c7..5036742e 100644 --- a/client/src/main/scala/it/parttimeteam/controller/MainControllerImpl.scala +++ b/client/src/main/scala/it/parttimeteam/controller/MainControllerImpl.scala @@ -1,7 +1,7 @@ package it.parttimeteam.controller import it.parttimeteam.controller.game.{GameController, GameControllerImpl} -import it.parttimeteam.controller.startup.{StartUpController, StartUpControllerImpl} +import it.parttimeteam.controller.startup.{StartupController, StartupControllerImpl} import it.parttimeteam.model.startup.GameMatchInformations import it.parttimeteam.view.{View, ViewImpl} import scalafx.application.JFXApp @@ -9,19 +9,19 @@ import scalafx.application.JFXApp class MainControllerImpl(app: JFXApp) extends MainController { val view: View = new ViewImpl(app) - val startUpController: StartUpController = new StartUpControllerImpl + val startUpController: StartupController = new StartupControllerImpl val gameController: GameController = new GameControllerImpl(() => playAgain()) override def start(): Unit = { startUpController.start(app, startGame) } - def startGame(gameInfo: GameMatchInformations): Unit = { + private def startGame(gameInfo: GameMatchInformations): Unit = { startUpController.end() gameController.start(app, gameInfo) } - def playAgain(): Unit = { + private def playAgain(): Unit = { startUpController.start(app, startGame) gameController.end() } diff --git a/client/src/main/scala/it/parttimeteam/controller/ViewMessage.scala b/client/src/main/scala/it/parttimeteam/controller/ViewMessage.scala new file mode 100644 index 00000000..820be845 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/controller/ViewMessage.scala @@ -0,0 +1,11 @@ +package it.parttimeteam.controller + +sealed class ViewMessage + +object ViewMessage { + + case class ActualPlayerTurn(playerUsername: String) extends ViewMessage + + case object YourTurn extends ViewMessage + +} \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/controller/game/GameControllerImpl.scala b/client/src/main/scala/it/parttimeteam/controller/game/GameControllerImpl.scala index 0cb3cf02..3f2e6b5c 100644 --- a/client/src/main/scala/it/parttimeteam/controller/game/GameControllerImpl.scala +++ b/client/src/main/scala/it/parttimeteam/controller/game/GameControllerImpl.scala @@ -3,11 +3,13 @@ package it.parttimeteam.controller.game import java.util.concurrent.TimeUnit import it.parttimeteam.Constants +import it.parttimeteam.controller.ViewMessage import it.parttimeteam.controller.game.TurnTimer.TurnTimerImpl -import it.parttimeteam.core.{GameError, GameInterfaceImpl} +import it.parttimeteam.core.GameInterfaceImpl import it.parttimeteam.core.cards.Card import it.parttimeteam.core.collections.{Board, CardCombination, Hand} import it.parttimeteam.gamestate.{Opponent, PlayerGameState} +import it.parttimeteam.model.ErrorEvent import it.parttimeteam.model.game._ import it.parttimeteam.model.startup.GameMatchInformations import it.parttimeteam.view.game._ @@ -15,7 +17,7 @@ import scalafx.application.{JFXApp, Platform} class GameControllerImpl(playAgain: () => Unit) extends GameController { - private var gameStage: MachiavelliGameStage = _ + private var gameStage: GameStage = _ private var gameService: GameService = _ private var currentState: ClientGameState = _ @@ -23,81 +25,20 @@ class GameControllerImpl(playAgain: () => Unit) extends GameController { override def onStart(): Unit = { val time = millisToMinutesAndSeconds(Constants.Client.TURN_TIMER_DURATION * 1000) - println(s"TIMER -> Timer started: ${time._1}:${time._2}") gameStage.showTimer(time._1, time._2) } override def onEnd(): Unit = { - println(s"TIMER -> Timer ended") gameStage.notifyTimerEnded() gameService.endTurnDrawingACard() } override def onTick(millis: Long): Unit = { val time = millisToMinutesAndSeconds(millis) - - println(s"TIMER -> Timer tick: ${time._1}:${time._2}") - gameStage.updateTimer(time._1, time._2) } }) - override def start(app: JFXApp, gameInfo: GameMatchInformations): Unit = { - Platform.runLater({ - gameStage = MachiavelliGameStage(this) - gameStage.initMatch() - app.stage = gameStage - }) - - this.gameService = new GameServiceImpl(gameInfo, notifyEvent, new GameInterfaceImpl()) - this.gameService.playerReady() - } - - def notifyEvent(serverGameEvent: GameEvent): Unit = serverGameEvent match { - - case StateUpdatedEvent(state: ClientGameState) => { - currentState = state - Platform.runLater({ - gameStage.matchReady() - gameStage.updateState(state) - }) - } - - case OpponentInTurnEvent(actualPlayerName) => { - gameStage.setMessage(s"It's $actualPlayerName turn") - } - - case InTurnEvent => { - gameStage.setInTurn() - } - - case InfoEvent(message: String) => { - gameStage.notifyInfo(message) - } - - case GameErrorEvent(reason: GameError) => { - gameStage.notifyError(reason) - } - - case GameWonEvent => { - gameStage.notifyGameEnd(GameWon) - } - - case GameLostEvent(winnerName: String) => { - gameStage.notifyGameEnd(GameLost(winnerName)) - } - - case GameEndedWithErrorEvent(reason: String) => { - gameStage.notifyGameEnd(GameEndWithError(reason)) - } - - case TurnEndedEvent => { - gameStage.setTurnEnded() - } - - case _ => - } - override def onViewEvent(viewEvent: ViewGameEvent): Unit = viewEvent match { case LeaveGameEvent => gameService.leaveGame() @@ -133,6 +74,50 @@ class GameControllerImpl(playAgain: () => Unit) extends GameController { case TurnStartedEvent => turnTimer.start() } + override def start(app: JFXApp, gameInfo: GameMatchInformations): Unit = { + Platform.runLater({ + gameStage = GameStage(this) + gameStage.initMatch() + app.stage = gameStage + }) + + this.gameService = new GameServiceImpl(gameInfo, notifyEvent, new GameInterfaceImpl()) + this.gameService.playerReady() + } + + override def end(): Unit = { + + } + + private def notifyEvent(serverGameEvent: GameEvent): Unit = serverGameEvent match { + + case StateUpdatedEvent(state: ClientGameState) => { + currentState = state + Platform.runLater({ + gameStage.matchReady() + gameStage.updateState(state) + }) + } + + case OpponentInTurnEvent(actualPlayerName) => gameStage.setMessage(ViewMessage.ActualPlayerTurn(actualPlayerName)) + + case InTurnEvent => gameStage.setInTurn() + + case InfoEvent(message: String) => gameStage.notifyInfo(message) + + case GameErrorEvent(error: ErrorEvent) => gameStage.notifyError(error) + + case GameWonEvent => gameStage.notifyGameEnd(GameWon) + + case GameLostEvent(winnerName: String) => gameStage.notifyGameEnd(GameLost(winnerName)) + + case GameEndedBecausePlayerLeft => gameStage.notifyGameEnd(GameEndPlayerLeft) + + case TurnEndedEvent => gameStage.setTurnEnded() + + case _ => + } + private def millisToMinutesAndSeconds(millis: Long): (Long, Long) = { val minutes: Long = TimeUnit.MILLISECONDS.toMinutes(millis) val seconds: Long = TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(minutes) @@ -172,9 +157,4 @@ class GameControllerImpl(playAgain: () => Unit) extends GameController { ClientGameState(PlayerGameState(board, hand, players), true, true, true, true) } - - - override def end(): Unit = { - - } } diff --git a/client/src/main/scala/it/parttimeteam/controller/startup/GameStartUpListener.scala b/client/src/main/scala/it/parttimeteam/controller/startup/GameStartUpListener.scala deleted file mode 100644 index 13e3ff27..00000000 --- a/client/src/main/scala/it/parttimeteam/controller/startup/GameStartUpListener.scala +++ /dev/null @@ -1,8 +0,0 @@ -package it.parttimeteam.controller.startup - -import it.parttimeteam.view.startup.StartUpViewEvent - -trait GameStartUpListener { - - def onViewEvent(viewEvent: StartUpViewEvent): Unit -} diff --git a/client/src/main/scala/it/parttimeteam/controller/startup/StartUpControllerImpl.scala b/client/src/main/scala/it/parttimeteam/controller/startup/StartUpControllerImpl.scala deleted file mode 100644 index 064f868e..00000000 --- a/client/src/main/scala/it/parttimeteam/controller/startup/StartUpControllerImpl.scala +++ /dev/null @@ -1,70 +0,0 @@ -package it.parttimeteam.controller.startup - -import it.parttimeteam.Constants -import it.parttimeteam.model._ -import it.parttimeteam.model.startup.{GameMatchInformations, GameStartUpEvent, GameStartedEvent, LobbyJoinErrorEvent, LobbyJoinedEvent, PrivateLobbyCreatedEvent, StartupService, StartupServiceImpl} -import it.parttimeteam.view.startup._ -import scalafx.application.{JFXApp, Platform} - -class StartUpControllerImpl extends StartUpController { - - private var startUpStage: MachiavelliStartUpStage = _ - private val startUpService: StartupService = new StartupServiceImpl(notifyEvent) - private var startGameFunction: GameMatchInformations => Unit = _ - - override def start(app: JFXApp, startGame: GameMatchInformations => Unit): Unit = { - Platform.runLater({ - startUpStage = MachiavelliStartUpStage(this) - app.stage = startUpStage - }) - startGameFunction = startGame - this.startUpService.connect(Constants.Remote.SERVER_ADDRESS, Constants.Remote.SERVER_PORT) - } - - override def onViewEvent(viewEvent: StartUpViewEvent): Unit = viewEvent match { - case PublicGameSubmitViewEvent(username, playersNumber) => { - System.out.println(s"PublicGameSubmitViewEvent $username - $playersNumber") - this.startUpService.joinPublicLobby(username, playersNumber) - } - case PrivateGameSubmitViewEvent(username, code) => { - System.out.println(s"PrivateGameSubmitViewEvent $username - $code") - this.startUpService.joinPrivateLobby(username, code) - } - case CreatePrivateGameSubmitViewEvent(username, playersNumber) => { - System.out.println(s"CreatePrivateGameSubmitViewEvent $username - $playersNumber") - this.startUpService.createPrivateLobby(username, playersNumber) - } - case LeaveLobbyViewEvent => { - this.startUpService.leaveLobby() - } - case _ => - } - - def notifyEvent(gameStartUpEvent: GameStartUpEvent): Unit = gameStartUpEvent match { - - case LobbyJoinedEvent => { - startUpStage.notifyLobbyJoined() - } - - case PrivateLobbyCreatedEvent(privateCode: String) => { - startUpStage.notifyPrivateCode(privateCode) - startUpStage.notifyLobbyJoined() - } - - - case LobbyJoinErrorEvent(result: String) => { - startUpStage.notifyError(result) - } - - case GameStartedEvent(gameInfo: GameMatchInformations) => { - startGameFunction(gameInfo) - } - - case _ => - - } - - override def end(): Unit = { - - } -} diff --git a/client/src/main/scala/it/parttimeteam/controller/startup/StartUpController.scala b/client/src/main/scala/it/parttimeteam/controller/startup/StartupController.scala similarity index 81% rename from client/src/main/scala/it/parttimeteam/controller/startup/StartUpController.scala rename to client/src/main/scala/it/parttimeteam/controller/startup/StartupController.scala index 867c4a55..112acf37 100644 --- a/client/src/main/scala/it/parttimeteam/controller/startup/StartUpController.scala +++ b/client/src/main/scala/it/parttimeteam/controller/startup/StartupController.scala @@ -7,7 +7,7 @@ import scalafx.application.JFXApp /** * Controller responsible of the game initialization */ -trait StartUpController extends BaseController with GameStartUpListener { +trait StartupController extends BaseController with StartupListener { def start(app: JFXApp, startGame: GameMatchInformations => Unit): Unit } diff --git a/client/src/main/scala/it/parttimeteam/controller/startup/StartupControllerImpl.scala b/client/src/main/scala/it/parttimeteam/controller/startup/StartupControllerImpl.scala new file mode 100644 index 00000000..4b2a9200 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/controller/startup/StartupControllerImpl.scala @@ -0,0 +1,73 @@ +package it.parttimeteam.controller.startup + +import it.parttimeteam.Constants +import it.parttimeteam.model.ErrorEvent +import it.parttimeteam.model.startup._ +import it.parttimeteam.view.startup._ +import scalafx.application.{JFXApp, Platform} + +class StartupControllerImpl extends StartupController { + + private var startUpStage: StartupStage = _ + private val startUpService: StartupService = new StartupServiceImpl(notifyEvent) + private var startGameFunction: GameMatchInformations => Unit = _ + + override def start(app: JFXApp, startGame: GameMatchInformations => Unit): Unit = { + Platform.runLater({ + startUpStage = StartupStage(this) + app.stage = startUpStage + }) + startGameFunction = startGame + connect() + } + + override def onViewEvent(viewEvent: StartupViewEvent): Unit = viewEvent match { + + case PublicGameSubmitViewEvent(username, playersNumber) => this.startUpService.joinPublicLobby(username, playersNumber) + + case PrivateGameSubmitViewEvent(username, code) => this.startUpService.joinPrivateLobby(username, code) + + case CreatePrivateGameSubmitViewEvent(username, playersNumber) => this.startUpService.createPrivateLobby(username, playersNumber) + + case LeaveLobbyViewEvent => this.startUpService.leaveLobby() + + case RetryServerConnection => connect() + + case _ => + } + + private def notifyEvent(gameStartupEvent: GameStartupEvent): Unit = gameStartupEvent match { + + case LobbyJoinedEvent => { + Platform.runLater({ + startUpStage.notifyLobbyJoined() + }) + } + + case PrivateLobbyCreatedEvent(privateCode: String) => { + Platform.runLater({ + startUpStage.notifyPrivateCode(privateCode) + startUpStage.notifyLobbyJoined() + }) + } + + case LobbyJoinErrorEvent(error: ErrorEvent) => { + Platform.runLater({ + startUpStage.notifyError(error) + }) + } + + case GameStartedEvent(gameInfo: GameMatchInformations) => { + startGameFunction(gameInfo) + } + + case _ => + + } + + private def connect(): Unit = this.startUpService.connect(Constants.Remote.SERVER_ADDRESS, Constants.Remote.SERVER_PORT) + + override def end(): Unit = { + + } +} diff --git a/client/src/main/scala/it/parttimeteam/controller/startup/StartupListener.scala b/client/src/main/scala/it/parttimeteam/controller/startup/StartupListener.scala new file mode 100644 index 00000000..45eee969 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/controller/startup/StartupListener.scala @@ -0,0 +1,8 @@ +package it.parttimeteam.controller.startup + +import it.parttimeteam.view.startup.StartupViewEvent + +trait StartupListener { + + def onViewEvent(viewEvent: StartupViewEvent): Unit +} diff --git a/client/src/main/scala/it/parttimeteam/model/ErrorEvent.scala b/client/src/main/scala/it/parttimeteam/model/ErrorEvent.scala index bc129fbb..5020c203 100644 --- a/client/src/main/scala/it/parttimeteam/model/ErrorEvent.scala +++ b/client/src/main/scala/it/parttimeteam/model/ErrorEvent.scala @@ -6,8 +6,6 @@ sealed class ErrorEvent object ErrorEvent { - // TODO inserire i vari errori generabili dal model verso i controller - case class GenericError(reason: String) extends ErrorEvent case object ServerNotFound extends ErrorEvent @@ -20,13 +18,15 @@ object ErrorEvent { case object NoCardInBoard extends ErrorEvent + case object LobbyCodeNotValid extends ErrorEvent + /** * Function * * @param error error GameError * @return ErrorEvent */ - def mapError(error : GameError) : ErrorEvent = error match { + def mapError(error: GameError): ErrorEvent = error match { case GameError.CombinationNotValid => this.CombinationNotValid case GameError.HandNotContainCard => this.HandNotContainCard case GameError.NoCardInBoard => this.NoCardInBoard diff --git a/client/src/main/scala/it/parttimeteam/model/game/GameEvent.scala b/client/src/main/scala/it/parttimeteam/model/game/GameEvent.scala index f9b9b983..47d2f409 100644 --- a/client/src/main/scala/it/parttimeteam/model/game/GameEvent.scala +++ b/client/src/main/scala/it/parttimeteam/model/game/GameEvent.scala @@ -18,7 +18,7 @@ case object GameWonEvent extends GameEvent case class GameLostEvent(winnerName: String) extends GameEvent -case class GameEndedWithErrorEvent(reason: String) extends GameEvent +case object GameEndedBecausePlayerLeft extends GameEvent case object TurnEndedEvent extends GameEvent diff --git a/client/src/main/scala/it/parttimeteam/model/game/GameServiceImpl.scala b/client/src/main/scala/it/parttimeteam/model/game/GameServiceImpl.scala index 26667ce4..2d383c7c 100644 --- a/client/src/main/scala/it/parttimeteam/model/game/GameServiceImpl.scala +++ b/client/src/main/scala/it/parttimeteam/model/game/GameServiceImpl.scala @@ -5,6 +5,7 @@ import it.parttimeteam.core.cards.Card import it.parttimeteam.gamestate.PlayerGameState import it.parttimeteam.messages.GameMessage.{LeaveGame, PlayerActionMade, Ready} import it.parttimeteam.model.ErrorEvent +import it.parttimeteam.model.ErrorEvent.NoValidTurnPlay import it.parttimeteam.model.game.RemoteGameActor.MatchServerResponseListener import it.parttimeteam.model.startup.GameMatchInformations import it.parttimeteam.{ActorSystemManager, DrawCard, PlayedMove} @@ -13,11 +14,11 @@ import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ /** - * - * @param gameInformation information user to participate to a game match - * @param notifyEvent function used to notify the external component about game updates - * @param gameInterface the game core api - */ + * + * @param gameInformation information user to participate to a game match + * @param notifyEvent function used to notify the external component about game updates + * @param gameInterface the game core api + */ class GameServiceImpl(private val gameInformation: GameMatchInformations, private val notifyEvent: GameEvent => Unit, private val gameInterface: GameInterface) extends GameService { @@ -58,18 +59,13 @@ class GameServiceImpl(private val gameInformation: GameMatchInformations, override def opponentInTurn(opponentName: String): Unit = notifyEvent(OpponentInTurnEvent(opponentName)) - override def turnEndedWithCartDrawn(card: Card): Unit = { - turnHistory = turnHistory.clear() - notifyEvent(StateUpdatedEvent(generateClientGameState(storeOpt.get.onCardDrawn(card), turnHistory))) - notifyEvent(TurnEndedEvent) - } - override def gameWon(): Unit = notifyEvent(GameWonEvent) override def gameLost(winnerName: String): Unit = notifyEvent(GameLostEvent(winnerName)) - override def gameEndedWithErrorEvent(reason: String): Unit = notifyEvent(GameEndedWithErrorEvent(reason)) + override def gameEndedBecausePlayerLeft(): Unit = notifyEvent(GameEndedBecausePlayerLeft) + override def invalidPlayerAction(): Unit = notifyEvent(GameErrorEvent(NoValidTurnPlay)) } private val gameClientActorRef = ActorSystemManager.actorSystem.actorOf(RemoteGameActor.props(this.matchServerResponseListener)) @@ -136,15 +132,15 @@ class GameServiceImpl(private val gameInformation: GameMatchInformations, } override def undoTurn(): Unit = { - this.updateStateThroughHistory(turnHistory.previous) + this.updateStateThroughHistory(() => turnHistory.previous()) } override def redoTurn(): Unit = { - this.updateStateThroughHistory(turnHistory.next) + this.updateStateThroughHistory(() => turnHistory.next()) } override def resetTurnState(): Unit = { - this.updateStateThroughHistory(turnHistory.reset) + this.updateStateThroughHistory(() => turnHistory.reset()) } override def pickCardCombination(combinationId: String): Unit = { @@ -201,10 +197,10 @@ class GameServiceImpl(private val gameInformation: GameMatchInformations, ) /** - * Execute the history method, updates the history and the resulting state - * - * @param historyUpdate History method to execute. - */ + * Execute the history method, updates the history and the resulting state + * + * @param historyUpdate History method to execute. + */ private def updateStateThroughHistory(historyUpdate: () => (Option[PlayerGameState], History[PlayerGameState])): Unit = { val (optState, updatedHistory) = historyUpdate() this.turnHistory = updatedHistory @@ -215,11 +211,11 @@ class GameServiceImpl(private val gameInformation: GameMatchInformations, } /** - * Return if Turn is valid or not. - * - * @param currentState Current Game State. - * @return True if the Turn isn't valid, false anywhere. - */ + * Return if Turn is valid or not. + * + * @param currentState Current Game State. + * @return True if the Turn isn't valid, false anywhere. + */ private def isTurnValid(currentState: PlayerGameState) = { val startState = turnHistory.headOption startState.isDefined && gameInterface.validateTurn( diff --git a/client/src/main/scala/it/parttimeteam/model/game/GameUpdate.scala b/client/src/main/scala/it/parttimeteam/model/game/GameUpdate.scala deleted file mode 100644 index 7e283d9c..00000000 --- a/client/src/main/scala/it/parttimeteam/model/game/GameUpdate.scala +++ /dev/null @@ -1,3 +0,0 @@ -package it.parttimeteam.model.game - -sealed class GameUpdate diff --git a/client/src/main/scala/it/parttimeteam/model/game/RemoteGameActor.scala b/client/src/main/scala/it/parttimeteam/model/game/RemoteGameActor.scala index e4a92d8b..2142988e 100644 --- a/client/src/main/scala/it/parttimeteam/model/game/RemoteGameActor.scala +++ b/client/src/main/scala/it/parttimeteam/model/game/RemoteGameActor.scala @@ -1,8 +1,8 @@ package it.parttimeteam.model.game import akka.actor.{Actor, ActorLogging, Props} -import it.parttimeteam.core.cards.Card import it.parttimeteam.gamestate.PlayerGameState +import it.parttimeteam.messages.GameMessage.MatchError.PlayerActionNotValid import it.parttimeteam.messages.GameMessage._ import it.parttimeteam.model.game.RemoteGameActor.MatchServerResponseListener @@ -19,14 +19,14 @@ object RemoteGameActor { def opponentInTurn(opponentName: String) - def turnEndedWithCartDrawn(card: Card) - - def gameEndedWithErrorEvent(reason: String) + def gameEndedBecausePlayerLeft() def gameWon() def gameLost(winnerName: String) + def invalidPlayerAction() + } } @@ -47,17 +47,18 @@ class RemoteGameActor(private val listener: MatchServerResponseListener) extends case OpponentInTurn(name) => this.listener.opponentInTurn(name) - case CardDrawn(card) => this.listener.turnEndedWithCartDrawn(card) - case TurnEnded => this.listener.turnEnded() case Won => this.listener.gameWon() case Lost(winnerName) => this.listener.gameLost(winnerName) - case GameEndedForPlayerLeft => this.listener.gameEndedWithErrorEvent("a player left the game") + case GameEndedForPlayerLeft => this.listener.gameEndedBecausePlayerLeft() - case MatchErrorOccurred(_) => + case MatchErrorOccurred(error) => error match { + case PlayerActionNotValid => this.listener.invalidPlayerAction() + case _ => + } } } diff --git a/client/src/main/scala/it/parttimeteam/model/startup/GameStartUpEvent.scala b/client/src/main/scala/it/parttimeteam/model/startup/GameStartUpEvent.scala deleted file mode 100644 index 0053ba59..00000000 --- a/client/src/main/scala/it/parttimeteam/model/startup/GameStartUpEvent.scala +++ /dev/null @@ -1,13 +0,0 @@ -package it.parttimeteam.model.startup - -sealed class GameStartUpEvent - -case object LobbyJoinedEvent extends GameStartUpEvent - -case class PrivateLobbyCreatedEvent(privateCode: String) extends GameStartUpEvent - -case class GameStartedEvent(gameInfo: GameMatchInformations) extends GameStartUpEvent - -case class LobbyJoinErrorEvent(result: String) extends GameStartUpEvent - - diff --git a/client/src/main/scala/it/parttimeteam/model/startup/GameStartupEvent.scala b/client/src/main/scala/it/parttimeteam/model/startup/GameStartupEvent.scala new file mode 100644 index 00000000..011656ed --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/model/startup/GameStartupEvent.scala @@ -0,0 +1,15 @@ +package it.parttimeteam.model.startup + +import it.parttimeteam.model.ErrorEvent + +sealed class GameStartupEvent + +case object LobbyJoinedEvent extends GameStartupEvent + +case class PrivateLobbyCreatedEvent(privateCode: String) extends GameStartupEvent + +case class GameStartedEvent(gameInfo: GameMatchInformations) extends GameStartupEvent + +case class LobbyJoinErrorEvent(error: ErrorEvent) extends GameStartupEvent + + diff --git a/client/src/main/scala/it/parttimeteam/model/startup/StartUpActor.scala b/client/src/main/scala/it/parttimeteam/model/startup/StartUpActor.scala index b5eb90bc..44092556 100644 --- a/client/src/main/scala/it/parttimeteam/model/startup/StartUpActor.scala +++ b/client/src/main/scala/it/parttimeteam/model/startup/StartUpActor.scala @@ -1,10 +1,10 @@ package it.parttimeteam.model.startup import akka.actor.{Actor, ActorLogging, Props} -import it.parttimeteam.messages.LobbyMessages.{Connected, MatchFound, PrivateLobbyCreated, UserAddedToLobby} +import it.parttimeteam.messages.LobbyMessages._ -object StartUpActor { - def props(serverResponsesListener: StartupServerResponsesListener): Props = Props(new StartUpActor(serverResponsesListener)) +object StartupActor { + def props(serverResponsesListener: StartupServerResponsesListener): Props = Props(new StartupActor(serverResponsesListener)) } /** @@ -12,13 +12,17 @@ object StartUpActor { * * @param serverResponsesListener function user to notify back about the received event */ -class StartUpActor(private val serverResponsesListener: StartupServerResponsesListener) extends Actor with ActorLogging { +class StartupActor(private val serverResponsesListener: StartupServerResponsesListener) extends Actor with ActorLogging { override def receive: Receive = { case Connected(id) => this.serverResponsesListener.connected(id, sender()) case UserAddedToLobby() => this.serverResponsesListener.addedToLobby() case PrivateLobbyCreated(lobbyCode) => this.serverResponsesListener.privateLobbyCreated(lobbyCode) case MatchFound(gameRoom) => this.serverResponsesListener.matchFound(gameRoom) + case LobbyErrorOccurred(error) => error match { + case LobbyError.PrivateLobbyIdNotValid => this.serverResponsesListener.privateLobbyCodeNotValid() + case _ => + } case m: String => log.debug(m) } diff --git a/client/src/main/scala/it/parttimeteam/model/startup/StartupServerResponsesListener.scala b/client/src/main/scala/it/parttimeteam/model/startup/StartupServerResponsesListener.scala index bd135b58..4656602d 100644 --- a/client/src/main/scala/it/parttimeteam/model/startup/StartupServerResponsesListener.scala +++ b/client/src/main/scala/it/parttimeteam/model/startup/StartupServerResponsesListener.scala @@ -34,6 +34,6 @@ trait StartupServerResponsesListener { */ def matchFound(matchRef: ActorRef) - // TODO error + def privateLobbyCodeNotValid() } diff --git a/client/src/main/scala/it/parttimeteam/model/startup/StartupServiceImpl.scala b/client/src/main/scala/it/parttimeteam/model/startup/StartupServiceImpl.scala index 84a4eca0..9b3debf1 100644 --- a/client/src/main/scala/it/parttimeteam/model/startup/StartupServiceImpl.scala +++ b/client/src/main/scala/it/parttimeteam/model/startup/StartupServiceImpl.scala @@ -2,6 +2,7 @@ package it.parttimeteam.model.startup import akka.actor.ActorRef import it.parttimeteam.messages.LobbyMessages.{JoinPublicLobby, _} +import it.parttimeteam.model.ErrorEvent import it.parttimeteam.{ActorSystemManager, Constants} import scala.concurrent.ExecutionContext.Implicits.global @@ -9,9 +10,9 @@ import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.{Failure, Success} -class StartupServiceImpl(private val notifyEvent: GameStartUpEvent => Unit) extends StartupService { +class StartupServiceImpl(private val notifyEvent: GameStartupEvent => Unit) extends StartupService { - private lazy val startupActorRef = ActorSystemManager.actorSystem.actorOf(StartUpActor.props(serverResponseListener), "client-lobby") + private lazy val startupActorRef = ActorSystemManager.actorSystem.actorOf(StartupActor.props(serverResponseListener), "client-lobby") private var serverLobbyRef: Option[ActorRef] = None private var clientGeneratedId: String = _ @@ -30,6 +31,9 @@ class StartupServiceImpl(private val notifyEvent: GameStartUpEvent => Unit) exte override def matchFound(matchRef: ActorRef): Unit = notifyEvent(GameStartedEvent(GameMatchInformations(clientGeneratedId, matchRef))) + override def privateLobbyCodeNotValid(): Unit = + notifyEvent(LobbyJoinErrorEvent(ErrorEvent.LobbyCodeNotValid)) + } override def connect(address: String, port: Int): Unit = { @@ -38,8 +42,7 @@ class StartupServiceImpl(private val notifyEvent: GameStartUpEvent => Unit) exte ref ! Connect(startupActorRef) } case Failure(t) => { - // TODO MATTEOC notify - this.notifyEvent(LobbyJoinErrorEvent("Server not found error")) + this.notifyEvent(LobbyJoinErrorEvent(ErrorEvent.ServerNotFound)) } } @@ -67,13 +70,6 @@ class StartupServiceImpl(private val notifyEvent: GameStartUpEvent => Unit) exte _ ! LeaveLobby(clientGeneratedId) } - // case JoinPublicLobby => notifyEvent(LobbyJoinedEvent("")) - // case PrivateLobbyCreatedEvent(generatedUserId: String, lobbyCode: String) => notifyEvent(PrivateLobbyCreatedEvent(generatedUserId, lobbyCode)) - // case MatchFound(gameRoom: ActorRef) => notifyEvent(GameStartedEvent(gameRoom)) - // case LobbyJoinError(reason: String) => notifyEvent(LobbyJoinErrorEvent(reason)) - // case Stop() => context.stop(self) - // case _ => - // endregion private def generateServerActorPath(address: String, port: Int): String = @@ -89,9 +85,7 @@ class StartupServiceImpl(private val notifyEvent: GameStartUpEvent => Unit) exte private def withServerLobbyRef(f: (ActorRef) => Unit): Unit = { this.serverLobbyRef match { case Some(ref) => f(ref) - case None => this.notifyEvent(LobbyJoinErrorEvent("Server not found error")) + case None => this.notifyEvent(LobbyJoinErrorEvent(ErrorEvent.ServerNotFound)) } } - - } diff --git a/client/src/main/scala/it/parttimeteam/view/BaseStage.scala b/client/src/main/scala/it/parttimeteam/view/BaseStage.scala index 673dd951..aa32497f 100644 --- a/client/src/main/scala/it/parttimeteam/view/BaseStage.scala +++ b/client/src/main/scala/it/parttimeteam/view/BaseStage.scala @@ -6,6 +6,6 @@ import scalafx.application.JFXApp class BaseStage extends JFXApp.PrimaryStage { title = Constants.Client.GAME_NAME resizable = true - width = ViewConfig.screenWidth - height = ViewConfig.screenHeight + width = ViewConfig.SCREEN_WIDTH + height = ViewConfig.SCREEN_HEIGHT } diff --git a/client/src/main/scala/it/parttimeteam/view/ViewConfig.scala b/client/src/main/scala/it/parttimeteam/view/ViewConfig.scala index 0f922f45..857881fc 100644 --- a/client/src/main/scala/it/parttimeteam/view/ViewConfig.scala +++ b/client/src/main/scala/it/parttimeteam/view/ViewConfig.scala @@ -4,28 +4,46 @@ package it.parttimeteam.view * View default values */ object ViewConfig { + + // Panes + final val DEFAULT_SPACING: Double = 5d + // Fonts - val formLabelFontSize: Double = 20d - val baseFontSize: Double = 20d - val titleFontSize: Double = 100d + final val FORM_LABEL_FONT_SIZE: Double = 20d + final val BASE_FONT_SIZE: Double = 20d + final val TITLE_FONT_SIZE: Double = 100d + final val CODE_FONT_SIZE: Double = 100d // Screen - val screenWidth: Double = 800d - val screenHeight: Double = 600d - val screenPadding: Double = 20d + final val SCREEN_WIDTH: Double = 800d + final val SCREEN_HEIGHT: Double = 600d + final val SCREEN_PADDING: Double = 20d // Forms - val formWidth: Double = 400d - val formSpacing: Double = 10d + final val FORM_WIDTH: Double = 400d + final val FORM_SPACING: Double = 10d // Cards - val CARD_Y_TRANSLATION: Double = 20d - val HAND_CARD_HEIGHT: Double = 100d - val HAND_PADDING: Double = 30d - val HAND_CARD_PADDING: Double = 5d + final val CARD_Y_TRANSLATION: Double = 20d + final val HAND_CARD_HEIGHT: Double = 100d + final val HAND_PADDING: Double = 30d + final val HAND_CARD_PADDING: Double = 5d + + // Other Players + final val OTHER_PLAYERS_TILE_GAP: Double = 10d + final val OTHER_PLAYERS_COLUMNS: Int = 2 + final val OTHER_PLAYERS_WIDTH: Double = 200 + + // History + final val RESET_BTN_MIN_WIDTH: Double = 60d + final val HISTORY_BTN_MIN_WIDTH: Double = 30d + final val BTN_ICON_HEIGHT: Double = 15d - // TilePane - val TILE_GAP: Double = 10d + // Board + final val BOARD_TILE_GAP: Double = 20d + // InitMatchDialog + final val INIT_MATCH_DIALOG_MIN_WIDTH: Double = 200d + final val INIT_MATCH_DIALOG_MIN_HEIGHT: Double = 100d } diff --git a/client/src/main/scala/it/parttimeteam/view/ViewImpl.scala b/client/src/main/scala/it/parttimeteam/view/ViewImpl.scala index a08858b3..951fdfe5 100644 --- a/client/src/main/scala/it/parttimeteam/view/ViewImpl.scala +++ b/client/src/main/scala/it/parttimeteam/view/ViewImpl.scala @@ -1,6 +1,6 @@ package it.parttimeteam.view -import it.parttimeteam.view.startup.MachiavelliStartUpStage +import it.parttimeteam.view.startup.StartupStage import scalafx.application.JFXApp import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} @@ -11,5 +11,5 @@ class ViewImpl(private val app: JFXApp) extends View { implicit val executionContextExecutor: ExecutionContextExecutor = ExecutionContext.global /** Setting the primary stage */ - app.stage = MachiavelliStartUpStage() + app.stage = StartupStage() } \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/game/GameEndType.scala b/client/src/main/scala/it/parttimeteam/view/game/GameEndType.scala index 7acce506..21652c59 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/GameEndType.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/GameEndType.scala @@ -18,8 +18,6 @@ case object GameWon extends GameEndType case class GameLost(winnerUsername: String) extends GameEndType /** - * The game ended with an error. - * - * @param reason the reason why the game ended + * The game ended. Another player left the game. */ -case class GameEndWithError(reason: String) extends GameEndType +case object GameEndPlayerLeft extends GameEndType diff --git a/client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStage.scala b/client/src/main/scala/it/parttimeteam/view/game/GameStage.scala similarity index 85% rename from client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStage.scala rename to client/src/main/scala/it/parttimeteam/view/game/GameStage.scala index 60abcadc..b0d21db0 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStage.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/GameStage.scala @@ -1,15 +1,15 @@ package it.parttimeteam.view.game +import it.parttimeteam.controller.ViewMessage import it.parttimeteam.controller.game.GameListener import it.parttimeteam.model.ErrorEvent import it.parttimeteam.model.game.ClientGameState import it.parttimeteam.view._ -import it.parttimeteam.view.game.listeners.GameStageListener /** * Main stage for the game view, interacts with GameScene */ -trait MachiavelliGameStage extends BaseStage with GameStageListener { +trait GameStage extends BaseStage { /** * Set the current user turn * @@ -75,7 +75,7 @@ trait MachiavelliGameStage extends BaseStage with GameStageListener { * * @param message the message to be displayed */ - def setMessage(message: String): Unit + def setMessage(message: ViewMessage): Unit /** * Called when the match is ready. @@ -103,9 +103,9 @@ trait MachiavelliGameStage extends BaseStage with GameStageListener { } /** - * Companion object for MachiavelliGameStage + * Companion object for ScalavelliGameStage */ -object MachiavelliGameStage { +object GameStage { - def apply(listener: GameListener): MachiavelliGameStage = new MachiavelliGameStageImpl(listener) + def apply(listener: GameListener): GameStage = new GameStageImpl(listener) } \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/game/GameStageImpl.scala b/client/src/main/scala/it/parttimeteam/view/game/GameStageImpl.scala new file mode 100644 index 00000000..30e474f6 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/game/GameStageImpl.scala @@ -0,0 +1,214 @@ +package it.parttimeteam.view.game + +import it.parttimeteam.controller.ViewMessage +import it.parttimeteam.controller.game.GameListener +import it.parttimeteam.core.cards.Card +import it.parttimeteam.model.ErrorEvent +import it.parttimeteam.model.game.ClientGameState +import it.parttimeteam.view.game.listeners.GameStageListener +import it.parttimeteam.view.game.scenes.{GameScene, GameSceneImpl} +import it.parttimeteam.view.utils.{ScalavelliAlert, StringParser, Strings} +import scalafx.application.Platform +import scalafx.scene.control.Alert.AlertType +import scalafx.scene.control.{Alert, ButtonType} + +/** + * GameStage implementation + * + * @param gameListener listener for view actions + */ +class GameStageImpl(gameListener: GameListener) extends GameStage { + val stage: GameStage = this + + val listener: GameStageListener = new GameStageListener() { + /** @inheritdoc */ + override def pickCombination(combinationId: String): Unit = gameListener.onViewEvent(PickCardCombinationEvent(combinationId)) + + /** @inheritdoc */ + override def endTurn(): Unit = gameListener.onViewEvent(EndTurnEvent) + + /** @inheritdoc */ + override def nextState(): Unit = gameListener.onViewEvent(RedoEvent) + + /** @inheritdoc */ + override def previousState(): Unit = gameListener.onViewEvent(UndoEvent) + + /** @inheritdoc */ + override def makeCombination(cards: Seq[Card]): Unit = gameListener.onViewEvent(MakeCombinationEvent(cards)) + + /** @inheritdoc */ + override def pickCards(cards: Seq[Card]): Unit = gameListener.onViewEvent(PickCardsEvent(cards)) + + /** @inheritdoc */ + override def sortHandBySuit(): Unit = gameListener.onViewEvent(SortHandBySuitEvent) + + /** @inheritdoc */ + override def sortHandByRank(): Unit = gameListener.onViewEvent(SortHandByRankEvent) + + /** @inheritdoc */ + override def updateCardCombination(combinationId: String, cards: Seq[Card]): Unit = gameListener.onViewEvent(UpdateCardCombinationEvent(combinationId, cards)) + + /** @inheritdoc */ + override def leaveGame(): Unit = { + val alert: Alert = ScalavelliAlert(Strings.LEAVE_GAME_DIALOG_TITLE, Strings.LEAVE_GAME_DIALOG_MESSAGE, AlertType.Confirmation, stage) + + alert.showAndWait match { + case Some(b) => + if (b == ButtonType.OK) { + gameListener.onViewEvent(LeaveGameEvent) + System.exit(0) + } + + case None => + } + } + + /** @inheritdoc */ + override def resetHistory(): Unit = gameListener.onViewEvent(ResetEvent) + + } + + val gameScene: GameScene = new GameSceneImpl(stage, listener) + + scene = gameScene + + onCloseRequest = _ => System.exit(0) + + /** @inheritdoc*/ + override def setMessage(message: ViewMessage): Unit = { + Platform.runLater({ + gameScene.setMessage(StringParser.parseMessage(message)) + }) + } + + /** @inheritdoc*/ + override def updateState(viewGameState: ClientGameState): Unit = { + Platform.runLater({ + gameScene.setState(viewGameState) + }) + } + + /** @inheritdoc*/ + override def initMatch(): Unit = { + Platform.runLater({ + gameScene.disableActions() + gameScene.showInitMatch() + }) + } + + /** @inheritdoc*/ + override def matchReady(): Unit = { + Platform.runLater({ + gameScene.hideInitMatch() + }) + } + + /** @inheritdoc*/ + override def notifyGameEnd(gameEndType: GameEndType): Unit = { + Platform.runLater({ + val message: String = gameEndType match { + case GameWon => Strings.GAME_WON_ALERT_MESSAGE + + case GameLost(winnerUsername: String) => Strings.GAME_LOST_ALERT_MESSAGE(winnerUsername) + + case GameEndPlayerLeft => Strings.GAME_END_PLAYER_LEFT_MESSAGE + + case _ => "" + } + + val alert = ScalavelliAlert(Strings.GAME_ENDED_ALERT_TITLE, message, AlertType.Information, this) + alert.showAndWait match { + case Some(b) => + if (b == ButtonType.OK) { + gameListener.onViewEvent(PlayAgainEvent) + } else { + System.exit(0) + } + + case None => + } + }) + } + + /** @inheritdoc*/ + override def enableActions(): Unit = { + Platform.runLater({ + gameScene.enableActions() + }) + } + + /** @inheritdoc*/ + override def disableActions(): Unit = { + Platform.runLater({ + gameScene.disableActions() + }) + } + + /** @inheritdoc*/ + override def notifyError(error: ErrorEvent): Unit = { + Platform.runLater({ + val alert: Alert = ScalavelliAlert(Strings.ERROR_DIALOG_TITLE, error, AlertType.Error, this) + alert.showAndWait() + }) + } + + /** @inheritdoc*/ + override def notifyInfo(message: String): Unit = { + Platform.runLater({ + val alert: Alert = ScalavelliAlert("", message, AlertType.Information, this) + alert.showAndWait() + }) + } + + /** @inheritdoc*/ + override def setInTurn(): Unit = { + gameScene.setInTurn(true) + Platform.runLater({ + val alert = ScalavelliAlert("", Strings.YOUR_TURN_ALERT_MESSAGE, AlertType.Information, this) + alert.showAndWait match { + case Some(b) => + if (b == ButtonType.OK) { + gameListener.onViewEvent(TurnStartedEvent) + } else { + System.exit(0) + } + + case None => + } + + setMessage(ViewMessage.YourTurn) + }) + } + + /** @inheritdoc*/ + override def setTurnEnded(): Unit = { + gameScene.setInTurn(false) + Platform.runLater({ + gameScene.setMessage("") + gameScene.hideTimer() + }) + } + + /** @inheritdoc*/ + override def showTimer(minutes: Long, seconds: Long): Unit = { + Platform.runLater({ + gameScene.showTimer(minutes, seconds) + }) + } + + /** @inheritdoc*/ + override def updateTimer(minutes: Long, seconds: Long): Unit = { + Platform.runLater({ + gameScene.updateTimer(minutes, seconds) + }) + } + + /** @inheritdoc*/ + override def notifyTimerEnded(): Unit = { + Platform.runLater({ + gameScene.notifyTimerEnd() + notifyInfo(Strings.TIMER_END_INFO_MESSAGE) + }) + } + +} \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStageImpl.scala b/client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStageImpl.scala deleted file mode 100644 index 1f145641..00000000 --- a/client/src/main/scala/it/parttimeteam/view/game/MachiavelliGameStageImpl.scala +++ /dev/null @@ -1,214 +0,0 @@ -package it.parttimeteam.view.game - -import it.parttimeteam.controller.game.GameListener -import it.parttimeteam.core.cards.Card -import it.parttimeteam.model.ErrorEvent -import it.parttimeteam.model.game.ClientGameState -import it.parttimeteam.view.game.scenes.{GameScene, GameSceneImpl} -import it.parttimeteam.view.utils.MachiavelliAlert -import scalafx.application.Platform -import scalafx.scene.control.Alert.AlertType -import scalafx.scene.control.{Alert, ButtonType} - -/** - * MachiavelliGameStage implementation - * - * @param gameListener - */ -class MachiavelliGameStageImpl(gameListener: GameListener) extends MachiavelliGameStage { - val stage: MachiavelliGameStage = this - - val gameScene: GameScene = new GameSceneImpl(stage) - - scene = gameScene - - onCloseRequest = _ => System.exit(0) - - - - /** @inheritdoc */ - override def setMessage(message: String): Unit = { - Platform.runLater({ - gameScene.setMessage(message) - }) - } - - /** @inheritdoc */ - override def updateState(viewGameState: ClientGameState): Unit = { - Platform.runLater({ - gameScene.setState(viewGameState) - }) - } - - /** @inheritdoc */ - override def initMatch(): Unit = { - Platform.runLater({ - gameScene.disableActions() - gameScene.showInitMatch() - }) - } - - /** @inheritdoc */ - override def matchReady(): Unit = { - Platform.runLater({ - gameScene.hideInitMatch() - }) - } - - /** @inheritdoc */ - override def notifyGameEnd(gameEndType: GameEndType): Unit = { - Platform.runLater({ - val message: String = gameEndType match { - case GameWon => "Congratulations! You won the game! Do you want to play again?" - - case GameLost(winnerUsername: String) => s"This game has a winner. And the winner is.. $winnerUsername! Do you want to play again?" - - case GameEndWithError(reason: String) => s"The game ended. $reason. Do you want to play again?" - - case _ => "" - } - - val alert = MachiavelliAlert("Game ended", message, AlertType.Information) - alert.showAndWait match { - case Some(b) => - if (b == ButtonType.OK) { - gameListener.onViewEvent(PlayAgainEvent) - } else { - System.exit(0) - } - - case None => - } - }) - } - - /** @inheritdoc */ - override def enableActions(): Unit = { - Platform.runLater({ - gameScene.enableActions() - }) - } - - /** @inheritdoc */ - override def disableActions(): Unit = { - Platform.runLater({ - gameScene.disableActions() - }) - } - - /** @inheritdoc */ - override def notifyError(result: ErrorEvent): Unit = { - // TODO create string error - Platform.runLater({ - val alert: Alert = MachiavelliAlert("Error", result toString, AlertType.Error) - alert.showAndWait() - }) - } - - /** @inheritdoc */ - override def notifyInfo(message: String): Unit = { - Platform.runLater({ - val alert: Alert = MachiavelliAlert("", message, AlertType.Information) - alert.showAndWait() - }) - } - - /** @inheritdoc */ - override def setInTurn(): Unit = { - gameScene.setInTurn(true) - Platform.runLater({ - val alert = MachiavelliAlert("", "It's your turn", AlertType.Information) - alert.showAndWait match { - case Some(b) => - if (b == ButtonType.OK) { - gameListener.onViewEvent(TurnStartedEvent) - } else { - System.exit(0) - } - - case None => - } - - setMessage("Your turn") - }) - } - - /** @inheritdoc */ - override def setTurnEnded(): Unit = { - gameScene.setInTurn(false) - Platform.runLater({ - setMessage("") - gameScene.hideTimer() - }) - } - - // view actions - /** @inheritdoc*/ - override def pickCombination(combinationId: String): Unit = gameListener.onViewEvent(PickCardCombinationEvent(combinationId)) - - /** @inheritdoc*/ - override def endTurn(): Unit = gameListener.onViewEvent(EndTurnEvent) - - /** @inheritdoc*/ - override def nextState(): Unit = gameListener.onViewEvent(RedoEvent) - - /** @inheritdoc*/ - override def previousState(): Unit = gameListener.onViewEvent(UndoEvent) - - /** @inheritdoc*/ - override def makeCombination(cards: Seq[Card]): Unit = gameListener.onViewEvent(MakeCombinationEvent(cards)) - - /** @inheritdoc*/ - override def pickCards(cards: Seq[Card]): Unit = gameListener.onViewEvent(PickCardsEvent(cards)) - - /** @inheritdoc*/ - override def sortHandBySuit(): Unit = gameListener.onViewEvent(SortHandBySuitEvent) - - /** @inheritdoc*/ - override def sortHandByRank(): Unit = gameListener.onViewEvent(SortHandByRankEvent) - - /** @inheritdoc*/ - override def updateCardCombination(combinationId: String, cards: Seq[Card]): Unit = gameListener.onViewEvent(UpdateCardCombinationEvent(combinationId, cards)) - - /** @inheritdoc*/ - override def leaveGame(): Unit = { - val alert: Alert = MachiavelliAlert("Leave the game", "Are you sure you want to leave the game? The action cannot be undone.", AlertType.Confirmation) - - alert.showAndWait match { - case Some(b) => - if (b == ButtonType.OK) { - gameListener.onViewEvent(LeaveGameEvent) - System.exit(0) - } - - case None => - } - } - - /** @inheritdoc*/ - override def resetHistory(): Unit = gameListener.onViewEvent(ResetEvent) - - /** @inheritdoc */ - override def showTimer(minutes: Long, seconds: Long): Unit = { - Platform.runLater({ - gameScene.showTimer(minutes, seconds) - }) - } - - /** @inheritdoc */ - override def updateTimer(minutes: Long, seconds: Long): Unit = { - Platform.runLater({ - gameScene.updateTimer(minutes, seconds) - }) - } - - /** @inheritdoc */ - override def notifyTimerEnded(): Unit = { - Platform.runLater({ - gameScene.notifyTimerEnd() - notifyInfo("You haven't passed your turn. We will pass and draw a card for you.") - }) - } - - -} \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/game/SelectionManager.scala b/client/src/main/scala/it/parttimeteam/view/game/SelectionManager.scala index fcc643ca..584d46d3 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/SelectionManager.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/SelectionManager.scala @@ -43,18 +43,15 @@ object SelectionManager { class SelectionManagerImpl[T <: SelectableItem](allowOnlyOne: Boolean, private var selectedItems: Seq[T] = List.empty) extends SelectionManager[T] { /** @inheritdoc */ override def onItemSelected(item: T): Unit = { - if (allowOnlyOne) { - this.clearSelection() - this.addItem(item) + if (selectedItems.contains(item)) { + this.removeItem(item) } else { - if (selectedItems.contains(item)) { - this.removeItem(item) - } else { - this.addItem(item) + if (allowOnlyOne){ + this.clearSelection() } - } - println(s"Selected cards: ${getSelectedItems.toString}") + this.addItem(item) + } } /** @inheritdoc */ diff --git a/client/src/main/scala/it/parttimeteam/view/game/listeners/GameStageListener.scala b/client/src/main/scala/it/parttimeteam/view/game/listeners/GameStageListener.scala index 0d810737..5bb3ce3d 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/listeners/GameStageListener.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/listeners/GameStageListener.scala @@ -3,7 +3,7 @@ package it.parttimeteam.view.game.listeners import it.parttimeteam.core.cards.Card /** - * Actions which can be made by MachiavelliGameStage + * Actions which can be made by ScalavelliGameStage */ trait GameStageListener { /** diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/GameScene.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/GameScene.scala index f7567cf0..7ec6d54a 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/GameScene.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/GameScene.scala @@ -5,8 +5,8 @@ import it.parttimeteam.view.game.scenes.model.{GameCard, GameCardCombination} import scalafx.scene.Scene /** - * Main scene for MachiavelliGameStage. - * Allows MachiavelliGameStage to interact with each view element. + * Main scene for ScalavelliGameStage. + * Allows ScalavelliGameStage to interact with each view element. */ trait GameScene extends Scene { @@ -35,10 +35,11 @@ trait GameScene extends Scene { /** * Show timer's up message + * * @return */ - def notifyTimerEnd(): Unit - + def notifyTimerEnd(): Unit + /** * Enable all the view actions during the player turn. */ diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/GameSceneImpl.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/GameSceneImpl.scala index 292d21a8..af6e0268 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/GameSceneImpl.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/GameSceneImpl.scala @@ -2,20 +2,22 @@ package it.parttimeteam.view.game.scenes import it.parttimeteam.core.cards.Card import it.parttimeteam.model.game.ClientGameState +import it.parttimeteam.view.game.listeners.GameStageListener import it.parttimeteam.view.game.scenes.GameScene.BoardListener import it.parttimeteam.view.game.scenes.model.{GameCard, GameCardCombination} import it.parttimeteam.view.game.scenes.panes.ActionBar.ActionBarImpl import it.parttimeteam.view.game.scenes.panes.BoardPane.BoardPaneImpl -import it.parttimeteam.view.game.scenes.panes.GameInfoBar.GameInfoBarImpl import it.parttimeteam.view.game.scenes.panes.HandBar.HandBarImpl import it.parttimeteam.view.game.scenes.panes.InitMatchDialog.InitMatchDialogImpl +import it.parttimeteam.view.game.scenes.panes.SidePane.SidePaneImpl import it.parttimeteam.view.game.scenes.panes._ -import it.parttimeteam.view.game.{MachiavelliGameStage, SelectionManager} +import it.parttimeteam.view.game.{GameStage, SelectionManager} +import it.parttimeteam.view.utils.Strings import scalafx.application.Platform import scalafx.scene.layout.{BorderPane, VBox} /** @inheritdoc */ -class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { +class GameSceneImpl(val parentStage: GameStage, listener: GameStageListener) extends GameScene { var inTurn = false val handSelectionManager: SelectionManager[GameCard] = SelectionManager(allowOnlyOne = false) @@ -25,19 +27,19 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { val gameInfoBarListener: GameInfoBarListener = new GameInfoBarListener { /** @inheritdoc*/ - override def endTurn(): Unit = parentStage.endTurn() + override def endTurn(): Unit = listener.endTurn() /** @inheritdoc*/ - override def leaveGame(): Unit = parentStage.leaveGame() + override def leaveGame(): Unit = listener.leaveGame() /** @inheritdoc*/ - override def nextState(): Unit = parentStage.nextState() + override def nextState(): Unit = listener.nextState() /** @inheritdoc*/ - override def previousState(): Unit = parentStage.previousState() + override def previousState(): Unit = listener.previousState() /** @inheritdoc*/ - override def resetState(): Unit = parentStage.resetHistory() + override def resetState(): Unit = listener.resetHistory() } val boardListener: BoardListener = new BoardListener { @@ -55,7 +57,7 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { /** @inheritdoc*/ override def onPickCombinationClick(cardCombination: GameCardCombination): Unit = { - parentStage.pickCombination(cardCombination.getCombination.id) + listener.pickCombination(cardCombination.getCombination.id) } } @@ -63,34 +65,34 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { /** @inheritdoc*/ override def pickCombination(combinationId: String): Unit = { - parentStage.pickCombination(combinationId) + listener.pickCombination(combinationId) } /** @inheritdoc */ override def makeCombination(): Unit = { val cards: Seq[Card] = handSelectionManager.getSelectedItems.map(p => p.getCard) - parentStage.makeCombination(cards) + listener.makeCombination(cards) } /** @inheritdoc */ override def pickCards(): Unit = { val cards: Seq[Card] = boardSelectionManager.getSelectedItems.map(p => p.getCard) - parentStage.pickCards(cards) + listener.pickCards(cards) } /** @inheritdoc */ override def sortHandBySuit(): Unit = { - parentStage.sortHandBySuit() + listener.sortHandBySuit() } /** @inheritdoc */ override def sortHandByRank(): Unit = { - parentStage.sortHandByRank() + listener.sortHandByRank() } /** @inheritdoc*/ - override def clearHandSelection(): Unit = { - handSelectionManager.clearSelection() + override def clearSelection(): Unit = { + clearAllSelections() updateActionBarButtons() } @@ -98,11 +100,11 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { override def updateCombination(): Unit = { val combination = combinationSelectionManager.getSelectedItems.head val selectedCards = handSelectionManager.getSelectedItems - parentStage.updateCardCombination(combination.getCombination.id, selectedCards.map(c => c.getCard)) + listener.updateCardCombination(combination.getCombination.id, selectedCards.map(c => c.getCard)) } } - val rightBar: GameInfoBar = new GameInfoBarImpl(gameInfoBarListener) + val rightBar: SidePane = new SidePaneImpl(gameInfoBarListener) val bottom = new VBox() @@ -130,9 +132,6 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { /** @inheritdoc */ override def setState(clientGameState: ClientGameState): Unit = { - this.handSelectionManager.clearSelection() - this.boardSelectionManager.clearSelection() - this.combinationSelectionManager.clearSelection() Platform.runLater({ centerPane.setBoard(clientGameState.playerGameState.board) handBar.setHand(clientGameState.playerGameState.hand) @@ -141,7 +140,9 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { rightBar.setRedoEnabled(inTurn && clientGameState.canRedo) rightBar.setResetEnabled(inTurn && clientGameState.canReset) rightBar.setNextEnabled(inTurn) - rightBar.setNextText(if (clientGameState.canDrawCard) "Pass & Draw" else "Pass") + rightBar.setNextText(if (clientGameState.canDrawCard) Strings.PASS_AND_DRAW_BTN else Strings.PASS_BTN) + + clearAllSelections() }) } @@ -173,14 +174,6 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { rightBar.setNextEnabled(inTurn) } - /** @inheritdoc */ - private def updateActionBarButtons(): Unit = { - actionBar.enableMakeCombination(!handSelectionManager.isSelectionEmpty && inTurn) - actionBar.enableClearHandSelection(!handSelectionManager.isSelectionEmpty) - actionBar.enableUpdateCardCombination(combinationSelectionManager.getSelectedItems.size == 1 && handSelectionManager.getSelectedItems.size == 1 && inTurn) - actionBar.enablePickCards(!boardSelectionManager.isSelectionEmpty && inTurn) - } - /** @inheritdoc*/ override def disableActions(): Unit = { rightBar.disableActions() @@ -208,4 +201,18 @@ class GameSceneImpl(val parentStage: MachiavelliGameStage) extends GameScene { /** @inheritdoc*/ override def hideTimer(): Unit = rightBar.hideTimer() + + private def updateActionBarButtons(): Unit = { + actionBar.enableMakeCombination(!handSelectionManager.isSelectionEmpty && inTurn) + actionBar.enableClearHandSelection(!handSelectionManager.isSelectionEmpty || !boardSelectionManager.isSelectionEmpty || !combinationSelectionManager.isSelectionEmpty) + actionBar.enableUpdateCardCombination(combinationSelectionManager.getSelectedItems.size == 1 && handSelectionManager.getSelectedItems.size == 1 && inTurn) + actionBar.enablePickCards(!boardSelectionManager.isSelectionEmpty && inTurn) + } + + private def clearAllSelections(): Unit = { + this.handSelectionManager.clearSelection() + this.boardSelectionManager.clearSelection() + this.combinationSelectionManager.clearSelection() + updateActionBarButtons() + } } diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCard.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCard.scala index 1e7baac5..2818d8b8 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCard.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCard.scala @@ -4,7 +4,7 @@ import it.parttimeteam.core.cards.Card import it.parttimeteam.view.ViewConfig import it.parttimeteam.view.game.scenes.GameScene.CardListener import it.parttimeteam.view.game.scenes.panes.ActionGamePane -import it.parttimeteam.view.utils.CardUtils +import it.parttimeteam.view.utils.{CardUtils, ImagePaths} import scalafx.geometry.{Insets, Pos} import scalafx.scene.image.{Image, ImageView} import scalafx.scene.layout.StackPane @@ -61,7 +61,7 @@ object GameCard { * @inheritdoc */ override def setAsBoardCard(): Unit = { - val prohibitionIcon: ImageView = new ImageView(new Image("images/prohibitionSign.png", 15, 15, false, false)) + val prohibitionIcon: ImageView = new ImageView(new Image(ImagePaths.PROHIBITION_SIGN, 15, 15, false, false)) prohibitionIcon.margin = Insets(5d) this.getStyleClass.remove("baseCard") diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCardCombination.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCardCombination.scala index ccfe2def..2efe1a53 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCardCombination.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/model/GameCardCombination.scala @@ -4,6 +4,7 @@ import it.parttimeteam.core.cards.Card import it.parttimeteam.core.collections.CardCombination import it.parttimeteam.view.game.scenes.GameScene.BoardListener import it.parttimeteam.view.game.scenes.panes.ActionGamePane +import it.parttimeteam.view.utils.ImagePaths import scalafx.geometry.Insets import scalafx.scene.control.Button import scalafx.scene.image.{Image, ImageView} @@ -35,7 +36,7 @@ object GameCardCombination { combinationCards.spacing = 10d val pickBtn = new Button() - val img = new ImageView(new Image("images/pick.png")) + val img = new ImageView(new Image(ImagePaths.PICK_ICON)) img.fitHeight = 15d img.preserveRatio = true diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/ActionBar.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/ActionBar.scala index bfc9c161..5a322767 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/ActionBar.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/ActionBar.scala @@ -1,7 +1,7 @@ package it.parttimeteam.view.game.scenes.panes import it.parttimeteam.view.ViewConfig -import it.parttimeteam.view.utils.MachiavelliButton +import it.parttimeteam.view.utils.{ScalavelliButton, Strings} import scalafx.geometry.Insets import scalafx.scene.layout.HBox @@ -43,7 +43,7 @@ trait ActionBarListener { /** * Clear selected cards */ - def clearHandSelection(): Unit + def clearSelection(): Unit /** * The player pick a combination from the game board. @@ -81,15 +81,15 @@ trait ActionBarListener { object ActionBar { class ActionBarImpl(listener: ActionBarListener) extends ActionBar { - this.spacing = ViewConfig.formSpacing + this.spacing = ViewConfig.FORM_SPACING this.padding = Insets(5) - val combinationBtn = MachiavelliButton("Make Combination", () => listener.makeCombination()) - val clearHandSelectionBtn = MachiavelliButton("Clear Selection", () => listener.clearHandSelection()) - val pickCardsBtn = MachiavelliButton("Pick Cards", () => listener.pickCards()) - val updateCombinationBtn = MachiavelliButton("Update Combination", () => listener.updateCombination()) - val sortBySuitBtn = MachiavelliButton("Sort Suit", () => listener.sortHandBySuit()) - val sortByRankBtn = MachiavelliButton("Sort Rank", () => listener.sortHandByRank()) + val combinationBtn = ScalavelliButton(Strings.MAKE_COMBINATION_BTN, () => listener.makeCombination()) + val clearHandSelectionBtn = ScalavelliButton(Strings.CLEAR_SELECTION_BTN, () => listener.clearSelection()) + val pickCardsBtn = ScalavelliButton(Strings.PICK_CARDS_BTN, () => listener.pickCards()) + val updateCombinationBtn = ScalavelliButton(Strings.UPDATE_COMBINATION_BTN, () => listener.updateCombination()) + val sortBySuitBtn = ScalavelliButton(Strings.SORT_SUIT_BTN, () => listener.sortHandBySuit()) + val sortByRankBtn = ScalavelliButton(Strings.SORT_RANK_BTN, () => listener.sortHandByRank()) combinationBtn.setDisable(true) clearHandSelectionBtn.setDisable(true) @@ -119,7 +119,9 @@ object ActionBar { } /** @inheritdoc */ - override def enableActions(): Unit = {} + override def enableActions(): Unit = { + + } } } \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/BoardPane.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/BoardPane.scala index 241a17fd..5e361c97 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/BoardPane.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/BoardPane.scala @@ -29,10 +29,10 @@ object BoardPane { var tilePane: TilePane = new TilePane() tilePane.prefColumns = 2 tilePane.prefWidth <= this.getWidth - tilePane.vgap = 20d - tilePane.hgap = 20d + tilePane.vgap = ViewConfig.BOARD_TILE_GAP + tilePane.hgap = ViewConfig.BOARD_TILE_GAP - tilePane.padding = Insets(ViewConfig.CARD_Y_TRANSLATION, ViewConfig.screenPadding, ViewConfig.screenPadding, ViewConfig.screenPadding) + tilePane.padding = Insets(ViewConfig.CARD_Y_TRANSLATION, ViewConfig.SCREEN_PADDING, ViewConfig.SCREEN_PADDING, ViewConfig.SCREEN_PADDING) content = tilePane this.setFitToWidth(true) diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HandBar.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HandBar.scala index cc411373..771743d4 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HandBar.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HandBar.scala @@ -29,7 +29,7 @@ object HandBar { this.getStyleClass.add("transparent") val handCardsContainer = new HBox() - handCardsContainer.spacing = 5d + handCardsContainer.spacing = ViewConfig.DEFAULT_SPACING handCardsContainer.padding = Insets(ViewConfig.CARD_Y_TRANSLATION + ViewConfig.HAND_CARD_PADDING, ViewConfig.HAND_CARD_PADDING, ViewConfig.HAND_CARD_PADDING, ViewConfig.HAND_CARD_PADDING) this.setMinHeight(ViewConfig.HAND_CARD_HEIGHT + ViewConfig.CARD_Y_TRANSLATION + ViewConfig.HAND_PADDING) diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HistoryNavigationPane.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HistoryNavigationPane.scala index f21d845a..9818d080 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HistoryNavigationPane.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/HistoryNavigationPane.scala @@ -1,36 +1,68 @@ package it.parttimeteam.view.game.scenes.panes -import it.parttimeteam.view.utils.MachiavelliButton +import it.parttimeteam.view.ViewConfig +import it.parttimeteam.view.utils.{ImagePaths, ScalavelliButton, Strings} import scalafx.geometry.Pos import scalafx.scene.layout.HBox +/** + * Pane which allows user to move between history states + */ trait HistoryNavigationPane extends HBox with ActionGamePane { + /** + * Sets the redo button enabled or not + * + * @param enabled if the button should be enabled + */ def setRedoEnabled(enabled: Boolean) + + /** + * Sets the undo button enabled or not + * + * @param enabled if the button should be enabled + */ def setUndoEnabled(enabled: Boolean) + /** + * Sets the reset button enabled or not + * + * @param enabled if the button should be enabled + */ def setResetEnabled(enabled: Boolean) } +/** + * Listener for HistoryNavigationPane callback + */ trait HistoryNavigationListener { + /** + * Action performed when undo button has been clicked + */ def onUndoClick(): Unit + /** + * Action performed when redo button has been clicked + */ def onRedoClick(): Unit + /** + * Action performed when reset button has been clicked + */ def onResetClick(): Unit } object HistoryNavigationPane { class HistoryNavigationPaneImpl(listener: HistoryNavigationListener) extends HistoryNavigationPane { - val undoBtn = MachiavelliButton("", () => listener.onUndoClick(), "images/undo.png", 15d, 30d) - val redoBtn = MachiavelliButton("", () => listener.onRedoClick(), "images/redo.png", 15d, 30d) - val resetBtn = MachiavelliButton("Reset", () => listener.onResetClick(), 60d) + val undoBtn = ScalavelliButton("", () => listener.onUndoClick(), ImagePaths.UNDO_BTN, ViewConfig.BTN_ICON_HEIGHT, ViewConfig.HISTORY_BTN_MIN_WIDTH) + val redoBtn = ScalavelliButton("", () => listener.onRedoClick(), ImagePaths.REDO_BTN, ViewConfig.BTN_ICON_HEIGHT, ViewConfig.HISTORY_BTN_MIN_WIDTH) + val resetBtn = ScalavelliButton(Strings.RESET_BTN, () => listener.onResetClick(), ViewConfig.RESET_BTN_MIN_WIDTH) this.alignment = Pos.Center - this.spacing = 5d + this.spacing = ViewConfig.DEFAULT_SPACING this.children.addAll(undoBtn, redoBtn, resetBtn) /** @inheritdoc */ diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/InitMatchDialog.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/InitMatchDialog.scala index eb5dda8f..de6addd3 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/InitMatchDialog.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/InitMatchDialog.scala @@ -1,5 +1,7 @@ package it.parttimeteam.view.game.scenes.panes +import it.parttimeteam.view.ViewConfig +import it.parttimeteam.view.utils.Strings import scalafx.geometry.Pos import scalafx.scene.Scene import scalafx.scene.control.{Label, ProgressBar} @@ -29,27 +31,27 @@ object InitMatchDialog { this.initStyle(StageStyle.Decorated) this.setResizable(false) this.initModality(Modality.WindowModal) - this.setTitle("Game loading") - this.setMinWidth(200) - this.setMinHeight(100) + this.setTitle(Strings.GAME_LOADING_TITLE) + this.setMinWidth(ViewConfig.INIT_MATCH_DIALOG_MIN_WIDTH) + this.setMinHeight(ViewConfig.INIT_MATCH_DIALOG_MIN_HEIGHT) - val label = new Label("Preparing your cards...") + val label = new Label(Strings.GAME_LOADING_MESSAGE) val vb = new VBox() - vb.setSpacing(5) + vb.setSpacing(ViewConfig.DEFAULT_SPACING) vb.setAlignment(Pos.Center) vb.getChildren.addAll(label, progressBar) val dialogScene = new Scene(vb) this.setScene(dialogScene) this.initOwner(parentStage) - /** @inheritdoc */ + /** @inheritdoc*/ override def showDialog(): Unit = { this.showAndWait() this.setAlwaysOnTop(true) } - /** @inheritdoc */ + /** @inheritdoc*/ override def hideDialog(): Unit = { this.close() } diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/OtherPlayersPane.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/OtherPlayersPane.scala index 44ce3b1e..ce5a89ea 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/OtherPlayersPane.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/OtherPlayersPane.scala @@ -2,41 +2,49 @@ package it.parttimeteam.view.game.scenes.panes import it.parttimeteam.gamestate.Opponent import it.parttimeteam.view.ViewConfig -import it.parttimeteam.view.utils.MachiavelliLabel +import it.parttimeteam.view.utils.{ImagePaths, ScalavelliLabel, Strings} import scalafx.scene.image.{Image, ImageView} import scalafx.scene.layout.{HBox, TilePane, VBox} +/** + * Pane where other players info are displayed + */ trait OtherPlayersPane extends VBox { + /** + * Sets the other players info inside the pane + * + * @param otherPlayers other players info + */ def setOtherPlayers(otherPlayers: Seq[Opponent]): Unit } object OtherPlayersPane { class OtherPlayersPaneImpl extends OtherPlayersPane { - var label = MachiavelliLabel("Other players:") + var label = ScalavelliLabel(Strings.OTHER_PLAYERS) label.getStyleClass.add("boldText") var pane: TilePane = new TilePane() - pane.setPrefColumns(2) + pane.setPrefColumns(ViewConfig.OTHER_PLAYERS_COLUMNS) - this.prefWidth = 200d + this.prefWidth = ViewConfig.OTHER_PLAYERS_WIDTH this.children.addAll(label, pane) - /** @inheritdoc*/ + /** @inheritdoc */ override def setOtherPlayers(otherPlayers: Seq[Opponent]): Unit = { pane.children.clear() - pane.hgap = ViewConfig.TILE_GAP - pane.vgap = ViewConfig.TILE_GAP + pane.hgap = ViewConfig.OTHER_PLAYERS_TILE_GAP + pane.vgap = ViewConfig.OTHER_PLAYERS_TILE_GAP for (player: Opponent <- otherPlayers) { - val nameLabel = MachiavelliLabel(player.name) - val cardsNumberLabel = MachiavelliLabel(player.cardsNumber.toString) + val nameLabel = ScalavelliLabel(player.name) + val cardsNumberLabel = ScalavelliLabel(player.cardsNumber.toString) val playerInfoContainer: VBox = new VBox() val cardInfoContainer: HBox = new HBox() - val cardImage: ImageView = new ImageView(new Image("images/cards/backBlue.png")) + val cardImage: ImageView = new ImageView(new Image(ImagePaths.BLUE_BACK_CARD)) cardImage.fitWidth = 20d cardImage.preserveRatio = true diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/GameInfoBar.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/SidePane.scala similarity index 90% rename from client/src/main/scala/it/parttimeteam/view/game/scenes/panes/GameInfoBar.scala rename to client/src/main/scala/it/parttimeteam/view/game/scenes/panes/SidePane.scala index 3a9547c4..1f825d7e 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/GameInfoBar.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/SidePane.scala @@ -1,10 +1,11 @@ package it.parttimeteam.view.game.scenes.panes import it.parttimeteam.gamestate.Opponent +import it.parttimeteam.view.ViewConfig import it.parttimeteam.view.game.scenes.panes.HistoryNavigationPane.HistoryNavigationPaneImpl import it.parttimeteam.view.game.scenes.panes.OtherPlayersPane.OtherPlayersPaneImpl import it.parttimeteam.view.game.scenes.panes.TimerPane.TimerPaneImpl -import it.parttimeteam.view.utils.{MachiavelliButton, MachiavelliLabel} +import it.parttimeteam.view.utils.{ScalavelliButton, ScalavelliLabel, Strings} import scalafx.geometry.Insets import scalafx.scene.layout.{BorderPane, VBox} @@ -12,7 +13,7 @@ import scalafx.scene.layout.{BorderPane, VBox} * Pane which contains turn and other players information. * Allows to navigate turn history and pass the turn to an other player. */ -trait GameInfoBar extends BorderPane with ActionGamePane { +trait SidePane extends BorderPane with ActionGamePane { /** * Hide the timer countdown. @@ -122,9 +123,9 @@ trait GameInfoBarListener { } -object GameInfoBar { +object SidePane { - class GameInfoBarImpl(val listener: GameInfoBarListener) extends GameInfoBar { + class SidePaneImpl(val listener: GameInfoBarListener) extends SidePane { padding = Insets(10d) this.getStyleClass.add("woodBack") @@ -141,7 +142,7 @@ object GameInfoBar { }) val btnContainer = new VBox() - btnContainer.spacing = 5d + btnContainer.spacing = ViewConfig.DEFAULT_SPACING historyNavigationPane.prefWidth <== btnContainer.width @@ -150,13 +151,13 @@ object GameInfoBar { val stateContainer = new VBox() - val messageLabel = MachiavelliLabel() + val messageLabel = ScalavelliLabel() timerPane.hide() stateContainer.children.addAll(messageLabel, timerPane) - val nextBtn = MachiavelliButton("Pass", () => listener.endTurn()) - val leaveBtn = MachiavelliButton("Leave Game", () => listener.leaveGame()) + val nextBtn = ScalavelliButton(Strings.PASS_BTN, () => listener.endTurn()) + val leaveBtn = ScalavelliButton(Strings.LEAVE_GAME_BTN, () => listener.leaveGame()) nextBtn.prefWidth <== rightBottom.width leaveBtn.prefWidth <== rightBottom.width @@ -211,7 +212,7 @@ object GameInfoBar { override def updateTimer(minutes: Long, seconds: Long): Unit = timerPane.set(minutes, seconds) /** @inheritdoc*/ - override def notifyTimerEnd(): Unit = timerPane.displayMessage("Your time is up!") + override def notifyTimerEnd(): Unit = timerPane.displayMessage(Strings.TIME_IS_UP) /** @inheritdoc*/ override def hideTimer(): Unit = timerPane.hide() diff --git a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/TimerPane.scala b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/TimerPane.scala index e621e3b1..58141bef 100644 --- a/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/TimerPane.scala +++ b/client/src/main/scala/it/parttimeteam/view/game/scenes/panes/TimerPane.scala @@ -1,39 +1,67 @@ package it.parttimeteam.view.game.scenes.panes -import it.parttimeteam.view.utils.MachiavelliLabel +import it.parttimeteam.view.utils.{ScalavelliLabel, Strings} import scalafx.scene.layout.VBox +/** + * Pane which show the countdown during user turn + */ trait TimerPane extends VBox { + /** + * Display a message instead of countdown time + * + * @param message the message to be displayed + */ def displayMessage(message: String): Unit + /** + * Sets the actual countdown time + * + * @param minutes remaining minutes + * @param seconds remaining seconds + */ def set(minutes: Long, seconds: Long): Unit + /** + * + * Show the countdown with the starting time + * + * @param minutes starting minutes + * @param seconds starting seconds + */ def show(startingMinutes: Long, startingSeconds: Long): Unit + /** + * Hide the countdown + */ def hide(): Unit } object TimerPane { class TimerPaneImpl extends TimerPane { - val timerLabel = MachiavelliLabel("Timer") + val timerLabel = ScalavelliLabel(Strings.TIMER) timerLabel.getStyleClass.add("boldText") - val countdownLabel = MachiavelliLabel() + val countdownLabel = ScalavelliLabel() this.children.addAll(timerLabel, countdownLabel) + /** @inheritdoc*/ override def hide(): Unit = { visible = false } + /** @inheritdoc*/ override def displayMessage(message: String): Unit = { setText(message) } + /** @inheritdoc*/ override def set(minutes: Long, seconds: Long): Unit = setText(timeToLabel(minutes, seconds)) + /** @inheritdoc*/ override def show(startingMinutes: Long, startingSeconds: Long): Unit = { visible = true setText(timeToLabel(startingMinutes, startingSeconds)) diff --git a/client/src/main/scala/it/parttimeteam/view/startup/MachiavelliStartUpStage.scala b/client/src/main/scala/it/parttimeteam/view/startup/MachiavelliStartUpStage.scala deleted file mode 100644 index 4be16ee7..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/MachiavelliStartUpStage.scala +++ /dev/null @@ -1,100 +0,0 @@ -package it.parttimeteam.view.startup - -import it.parttimeteam.controller.startup.GameStartUpListener -import it.parttimeteam.view.startup.listeners._ -import it.parttimeteam.view.startup.scenes._ -import it.parttimeteam.view.utils.MachiavelliAlert -import it.parttimeteam.view.{BaseStage, ViewConfig} -import scalafx.application.Platform -import scalafx.scene.control.Alert -import scalafx.scene.control.Alert.AlertType - - -/** - * Stage for startup scenes. - */ -trait MachiavelliStartUpStage extends BaseStage with PrimaryStageListener { - def notifyPrivateCode(privateCode: String): Unit - - def notifyLobbyJoined(): Unit - - def notifyError(result: String): Unit -} - -/** - * Stage for startup scenes. - * - * @param gameStartUpListener enables to call actions exposed by controller - */ -class MachiavelliStartUpStageImpl(gameStartUpListener: GameStartUpListener) extends MachiavelliStartUpStage { - private val mainScene = new SelectScene(this, new SelectSceneListener { - - override def onSelectedPublicGame(): Unit = setCurrentScene(publicGameScene) - - override def onSelectedPrivateGame(): Unit = setCurrentScene(privateGameScene) - - override def onSelectedCreatePrivateGame(): Unit = setCurrentScene(createPrivateGame) - }) - - val stage: MachiavelliStartUpStage = this - val publicGameScene: PublicGameStartUpScene = new PublicGameStartUpScene(this, this) - val privateGameScene: PrivateGameStartUpScene = new PrivateGameStartUpScene(this, this) - val createPrivateGame: CreatePrivateGameStartUpSceneImpl = new CreatePrivateGameStartUpSceneImpl(this, this) - - var currentInnerScene: BaseStartUpFormScene = _ - - scene = mainScene - - onCloseRequest = _ => { - System.exit(0) - } - - def setCurrentScene(newScene: BaseStartUpFormScene): Unit = { - scene = newScene - currentInnerScene = newScene - } - - // View actions - override def onBackPressed(): Unit = { - // TODO: Luca - Call leave lobby only when joined - gameStartUpListener.onViewEvent(LeaveLobbyViewEvent) - - currentInnerScene.resetScreen() - scene = mainScene - } - - override def onSubmit(viewEvent: StartUpViewEvent): Unit = { - gameStartUpListener.onViewEvent(viewEvent) - } - - // Controller actions - override def notifyPrivateCode(privateCode: String): Unit = { - Platform.runLater(createPrivateGame.showCode(privateCode)) - notifyLobbyJoined() - } - - override def notifyLobbyJoined(): Unit = { - Platform.runLater(currentInnerScene.showMessage("Waiting for other players")) - } - - override def notifyError(result: String): Unit = { - Platform.runLater { - val alert: Alert = MachiavelliAlert("Error", result, AlertType.Error) - alert.showAndWait() - } - } -} - -/** - * Companion object for MachiavelliStartupPrimaryStage - */ -object MachiavelliStartUpStage { - val windowWidth: Double = ViewConfig.screenWidth - val windowHeight: Double = ViewConfig.screenHeight - - def apply(listener: GameStartUpListener): MachiavelliStartUpStage = new MachiavelliStartUpStageImpl(listener) - - def apply(): MachiavelliStartUpStage = new MachiavelliStartUpStageImpl(null) -} - -trait PrimaryStageListener extends StartUpSceneListener diff --git a/client/src/main/scala/it/parttimeteam/view/startup/StartUpViewEvent.scala b/client/src/main/scala/it/parttimeteam/view/startup/StartUpViewEvent.scala deleted file mode 100644 index 0df6982d..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/StartUpViewEvent.scala +++ /dev/null @@ -1,15 +0,0 @@ -package it.parttimeteam.view.startup - -sealed class StartUpViewEvent - -// StartUp ViewEvent -case class PublicGameSubmitViewEvent(username: String, playersNumber: Int) extends StartUpViewEvent - -case class PrivateGameSubmitViewEvent(username: String, privateCode: String) extends StartUpViewEvent - -case class CreatePrivateGameSubmitViewEvent(username: String, playersNumber: Int) extends StartUpViewEvent - -case object LeaveLobbyViewEvent extends StartUpViewEvent - - - diff --git a/client/src/main/scala/it/parttimeteam/view/startup/StartupStage.scala b/client/src/main/scala/it/parttimeteam/view/startup/StartupStage.scala new file mode 100644 index 00000000..be759079 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/StartupStage.scala @@ -0,0 +1,131 @@ +package it.parttimeteam.view.startup + +import it.parttimeteam.controller.startup.StartupListener +import it.parttimeteam.model.ErrorEvent +import it.parttimeteam.model.ErrorEvent.ServerNotFound +import it.parttimeteam.view.startup.listeners._ +import it.parttimeteam.view.startup.scenes._ +import it.parttimeteam.view.utils.{ScalavelliAlert, Strings} +import it.parttimeteam.view.{BaseStage, ViewConfig} +import scalafx.application.Platform +import scalafx.scene.control.Alert.AlertType +import scalafx.scene.control.ButtonBar.ButtonData +import scalafx.scene.control.{Alert, ButtonType} + +/** + * Stage for startup scenes. + */ +trait StartupStage extends BaseStage { + /** + * Display the private code generated + * + * @param privateCode the code to be displayed + */ + def notifyPrivateCode(privateCode: String): Unit + + /** + * Notify the user the joined lobby event + */ + def notifyLobbyJoined(): Unit + + /** + * Display an error alert + * + * @param error the error event + */ + def notifyError(error: ErrorEvent): Unit +} + +/** + * Companion object for ScalavelliStartupPrimaryStage + */ +object StartupStage { + val windowWidth: Double = ViewConfig.SCREEN_WIDTH + val windowHeight: Double = ViewConfig.SCREEN_HEIGHT + + def apply(listener: StartupListener): StartupStage = new StartupStageImpl(listener) + + def apply(): StartupStage = new StartupStageImpl(null) + + /** @inheritdoc */ + class StartupStageImpl(gameStartupListener: StartupListener) extends StartupStage { + private val mainScene: SelectScene = new SelectScene(this, new SelectSceneListener { + + override def onSelectedPublicGame(): Unit = setCurrentScene(publicGameScene) + + override def onSelectedPrivateGame(): Unit = setCurrentScene(privateGameScene) + + override def onSelectedCreatePrivateGame(): Unit = setCurrentScene(createPrivateGame) + }) + + val stage: StartupStage = this + + val listener: StartupSceneListener = new StartupSceneListener { + + override def onBackPressed(): Unit = { + gameStartupListener.onViewEvent(LeaveLobbyViewEvent) + + currentInnerScene.resetScreen() + scene = mainScene + } + + override def onSubmit(viewEvent: StartupViewEvent): Unit = { + gameStartupListener.onViewEvent(viewEvent) + } + } + + scene = mainScene + + val publicGameScene: PublicGameScene = new PublicGameScene(this, listener) + val privateGameScene: PrivateGameScene = new PrivateGameScene(this, listener) + val createPrivateGame: CreatePrivateGameStartupSceneImpl = new CreatePrivateGameStartupSceneImpl(this, listener) + + var currentInnerScene: StartupFormScene = _ + + onCloseRequest = _ => { + System.exit(0) + } + + // Controller actions + override def notifyPrivateCode(privateCode: String): Unit = { + Platform.runLater(createPrivateGame.showCode(privateCode)) + notifyLobbyJoined() + } + + override def notifyLobbyJoined(): Unit = { + Platform.runLater(currentInnerScene.showMessage(Strings.WAITING_FOR_PLAYERS)) + } + + override def notifyError(error: ErrorEvent): Unit = { + Platform.runLater { + var alert: Alert = ScalavelliAlert(Strings.ERROR_DIALOG_TITLE, error, AlertType.Error, stage) + + if (error == ServerNotFound) { + val buttonTypeRetry = new ButtonType(Strings.RETRY, ButtonData.Yes) + val buttonTypeCancel = new ButtonType(Strings.CLOSE, ButtonData.CancelClose) + + alert.getButtonTypes.setAll(buttonTypeRetry, buttonTypeCancel) + alert.showAndWait match { + case Some(b) => { + if (b == buttonTypeRetry) { + gameStartupListener.onViewEvent(RetryServerConnection) + } + } + + case None => + } + } else { + alert = ScalavelliAlert(Strings.ERROR_DIALOG_TITLE, error, AlertType.Error, stage) + alert.showAndWait() + } + } + } + + private def setCurrentScene(newScene: StartupFormScene): Unit = { + scene = newScene + currentInnerScene = newScene + } + + } + +} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/StartupViewEvent.scala b/client/src/main/scala/it/parttimeteam/view/startup/StartupViewEvent.scala new file mode 100644 index 00000000..fbfd8c56 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/StartupViewEvent.scala @@ -0,0 +1,17 @@ +package it.parttimeteam.view.startup + +sealed class StartupViewEvent + +// Startup ViewEvent +case class PublicGameSubmitViewEvent(username: String, playersNumber: Int) extends StartupViewEvent + +case class PrivateGameSubmitViewEvent(username: String, privateCode: String) extends StartupViewEvent + +case class CreatePrivateGameSubmitViewEvent(username: String, playersNumber: Int) extends StartupViewEvent + +case object LeaveLobbyViewEvent extends StartupViewEvent + +case object RetryServerConnection extends StartupViewEvent + + + diff --git a/client/src/main/scala/it/parttimeteam/view/startup/listeners/SelectSceneListener.scala b/client/src/main/scala/it/parttimeteam/view/startup/listeners/SelectSceneListener.scala index 25461b3c..5d70ae9a 100644 --- a/client/src/main/scala/it/parttimeteam/view/startup/listeners/SelectSceneListener.scala +++ b/client/src/main/scala/it/parttimeteam/view/startup/listeners/SelectSceneListener.scala @@ -1,10 +1,22 @@ package it.parttimeteam.view.startup.listeners +/** + * Action which the user can make into SelectScene + */ trait SelectSceneListener { + /** + * The user wants to join a public game + */ def onSelectedPublicGame(): Unit + /** + * The user wants to join a private game + */ def onSelectedPrivateGame(): Unit + /** + * The user wants to create a private game + */ def onSelectedCreatePrivateGame(): Unit } diff --git a/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartUpSceneListener.scala b/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartUpSceneListener.scala deleted file mode 100644 index 3bf52ab0..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartUpSceneListener.scala +++ /dev/null @@ -1,10 +0,0 @@ -package it.parttimeteam.view.startup.listeners - -import it.parttimeteam.view.startup.StartUpViewEvent - -trait StartUpSceneListener { - - def onBackPressed(): Unit - - def onSubmit(viewEvent: StartUpViewEvent): Unit -} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartupSceneListener.scala b/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartupSceneListener.scala new file mode 100644 index 00000000..3e310bc7 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/listeners/StartupSceneListener.scala @@ -0,0 +1,21 @@ +package it.parttimeteam.view.startup.listeners + +import it.parttimeteam.view.startup.StartupViewEvent + +/** + * Actions which user can make inside input scenes + */ +trait StartupSceneListener { + + /** + * The user press back button + */ + def onBackPressed(): Unit + + /** + * The user press submit button + * + * @param viewEvent the event to trigger + */ + def onSubmit(viewEvent: StartupViewEvent): Unit +} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpFormScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpFormScene.scala deleted file mode 100644 index 1168446a..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpFormScene.scala +++ /dev/null @@ -1,14 +0,0 @@ -package it.parttimeteam.view.startup.scenes - -import scalafx.stage.Stage - -abstract class BaseStartUpFormScene(val parentStage: Stage) extends BaseStartUpScene(parentStage) { - - def showMessage(message: String): Unit - - def hideMessage(): Unit - - def disableButtons(): Unit - - def resetScreen(): Unit -} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupFormScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupFormScene.scala new file mode 100644 index 00000000..d2dbd99a --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupFormScene.scala @@ -0,0 +1,33 @@ +package it.parttimeteam.view.startup.scenes + +/** + * Actions which each input scene must implement + */ +trait BaseStartupFormScene { + /** + * Display a message inside the scene + * + * @param message the message to be displayed + */ + def showMessage(message: String): Unit + + /** + * Hide the message inside the scene + */ + def hideMessage(): Unit + + /** + * Disable actions inside the view + */ + def disableActions(): Unit + + /** + * Enable actions inside the view + */ + def enableActions(): Unit + + /** + * Clear user input and elements visibility + */ + def resetScreen(): Unit +} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupScene.scala similarity index 63% rename from client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpScene.scala rename to client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupScene.scala index 5aec49d1..2b0139d4 100644 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartUpScene.scala +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/BaseStartupScene.scala @@ -1,25 +1,30 @@ package it.parttimeteam.view.startup.scenes import it.parttimeteam.view.ViewConfig +import it.parttimeteam.view.utils.ImagePaths import scalafx.geometry.Insets import scalafx.scene.Scene import scalafx.scene.image.{Image, ImageView} import scalafx.scene.layout.{BorderPane, StackPane} import scalafx.stage.Stage -abstract class BaseStartUpScene(parentStage: Stage) extends Scene() { +/** + * Extend by all the StartupScenes + * + * @param parentStage the parent stage + */ +abstract class BaseStartupScene(parentStage: Stage) extends Scene() { - val background: ImageView = new ImageView(new Image("/images/background.png")) { + val background: ImageView = new ImageView(new Image(ImagePaths.STARTUP_BACKGROUND)) { fitWidth <== parentStage.width fitHeight <== parentStage.height } - background.preserveRatio = true val mainContent: BorderPane = new BorderPane() mainContent.prefWidth <== parentStage.width mainContent.maxHeight <== parentStage.height - mainContent.setPadding(Insets(ViewConfig.screenPadding)) + mainContent.setPadding(Insets(ViewConfig.SCREEN_PADDING)) val rootContent = new StackPane() rootContent.getChildren.addAll(background, mainContent) diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/CreatePrivateGameScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/CreatePrivateGameScene.scala index 6323b584..6300485e 100644 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/CreatePrivateGameScene.scala +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/CreatePrivateGameScene.scala @@ -1,10 +1,12 @@ package it.parttimeteam.view.startup.scenes -import it.parttimeteam.GamePreferences +import it.parttimeteam.Constants import it.parttimeteam.view.ViewConfig import it.parttimeteam.view.startup.CreatePrivateGameSubmitViewEvent -import it.parttimeteam.view.startup.listeners.StartUpSceneListener -import it.parttimeteam.view.utils.{MachiavelliAlert, MachiavelliLabel, MachiavelliTextField} +import it.parttimeteam.view.startup.listeners.StartupSceneListener +import it.parttimeteam.view.startup.scenes.StartupSceneBottomBar.StartupSceneBottomBarImpl +import it.parttimeteam.view.startup.scenes.StartupSceneTopBar.StartupSceneTopBarImpl +import it.parttimeteam.view.utils.{ScalavelliAlert, ScalavelliLabel, ScalavelliTextField, Strings} import javafx.scene.text.Font import scalafx.geometry.Pos.Center import scalafx.scene.control.Alert.AlertType @@ -27,22 +29,22 @@ trait CreatePrivateGameScene { * * @param listener to interact with parent stage */ -class CreatePrivateGameStartUpSceneImpl(override val parentStage: Stage, val listener: StartUpSceneListener) extends BaseStartUpFormScene(parentStage) with CreatePrivateGameScene { - val topBar: StartUpSceneTopBar = new StartUpSceneTopBar(listener) - val bottomBar: StartUpSceneBottomBar = new StartUpSceneBottomBar(() => submit()) +class CreatePrivateGameStartupSceneImpl(val parentStage: Stage, val listener: StartupSceneListener) extends StartupFormScene(parentStage) with CreatePrivateGameScene { + val topBar: StartupSceneTopBar = new StartupSceneTopBarImpl(listener) + val bottomBar: StartupSceneBottomBar = new StartupSceneBottomBarImpl(() => submit()) - val usernameLabel: Label = MachiavelliLabel("Username", ViewConfig.formLabelFontSize) - val usernameField: TextField = MachiavelliTextField("Username") + val usernameLabel: Label = ScalavelliLabel(Strings.USERNAME, ViewConfig.FORM_LABEL_FONT_SIZE) + val usernameField: TextField = ScalavelliTextField(Strings.USERNAME) - val options: Range = GamePreferences.MIN_PLAYERS_NUM to GamePreferences.MAX_PLAYERS_NUM by 1 + val options: Range = Constants.Client.MIN_PLAYERS_NUM to Constants.Client.MAX_PLAYERS_NUM by 1 - val selectPlayersLabel: Label = MachiavelliLabel("Select players number", ViewConfig.formLabelFontSize) + val selectPlayersLabel: Label = ScalavelliLabel(Strings.SELECT_PLAYERS_NUM, ViewConfig.FORM_LABEL_FONT_SIZE) val comboBox = new ComboBox(options) - comboBox.setValue(GamePreferences.MIN_PLAYERS_NUM) + comboBox.setValue(Constants.Client.MIN_PLAYERS_NUM) val center: VBox = new VBox() - center.spacing = ViewConfig.formSpacing - center.maxWidth = ViewConfig.formWidth + center.spacing = ViewConfig.FORM_SPACING + center.maxWidth = ViewConfig.FORM_WIDTH usernameLabel.maxWidth <== center.width usernameField.maxWidth <== center.width @@ -56,9 +58,9 @@ class CreatePrivateGameStartUpSceneImpl(override val parentStage: Stage, val lis mainContent.bottom = bottomBar val codeContainer: VBox = new VBox() - val codeLabel: Label = MachiavelliLabel("Here is your code") - val codeValue: Label = MachiavelliLabel(ViewConfig.titleFontSize) - codeValue.setFont(new Font(100)) + val codeLabel: Label = ScalavelliLabel(Strings.HERE_IS_YOUR_CODE) + val codeValue: Label = ScalavelliLabel(ViewConfig.TITLE_FONT_SIZE) + codeValue.setFont(new Font(ViewConfig.CODE_FONT_SIZE)) codeContainer.getChildren.addAll(codeLabel, codeValue) codeContainer.setVisible(false) @@ -67,34 +69,27 @@ class CreatePrivateGameStartUpSceneImpl(override val parentStage: Stage, val lis center.getChildren.addAll(usernameLabel, usernameField, selectPlayersLabel, comboBox, codeContainer) - val alert: Alert = MachiavelliAlert("Input missing", "You must enter username and select players number.", AlertType.Warning) + val alert: Alert = ScalavelliAlert(Strings.INPUT_MISSING_DIALOG_TITLE, Strings.INPUT_MISSING_USER_NUM_DIALOG_MESSAGE, AlertType.Warning, parentStage) override def showMessage(message: String): Unit = bottomBar.showMessage(message) override def hideMessage(): Unit = bottomBar.hideMessage() - private def submit(): Unit = { - val username: String = usernameField.getText - val nPlayers: Int = comboBox.getValue - - if (!username.isEmpty && nPlayers >= GamePreferences.MIN_PLAYERS_NUM) { - listener.onSubmit(CreatePrivateGameSubmitViewEvent(username, nPlayers)) - bottomBar.showLoading() - disableButtons() - } else { - alert.showAndWait() - } - } - override def showCode(code: String): Unit = { codeContainer.setVisible(true) codeValue.setText(code) } - override def disableButtons(): Unit = { + override def disableActions(): Unit = { usernameField.setEditable(false) comboBox.setDisable(true) - bottomBar.disableButtons() + bottomBar.disableActions() + } + + override def enableActions(): Unit = { + usernameField.setEditable(true) + comboBox.setDisable(false) + bottomBar.enableActions() } override def resetScreen(): Unit = { @@ -103,6 +98,19 @@ class CreatePrivateGameStartUpSceneImpl(override val parentStage: Stage, val lis codeValue.text = "" codeContainer.setVisible(false) comboBox.setDisable(false) - bottomBar.reset() + bottomBar.resetScreen() + } + + private def submit(): Unit = { + val username: String = usernameField.getText + val nPlayers: Int = comboBox.getValue + + if (!username.isEmpty && nPlayers >= Constants.Client.MIN_PLAYERS_NUM) { + listener.onSubmit(CreatePrivateGameSubmitViewEvent(username, nPlayers)) + bottomBar.showLoading() + disableActions() + } else { + alert.showAndWait() + } } } diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameStartUpScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameScene.scala similarity index 52% rename from client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameStartUpScene.scala rename to client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameScene.scala index 413021ea..79f2e79e 100644 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameStartUpScene.scala +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/PrivateGameScene.scala @@ -2,8 +2,10 @@ package it.parttimeteam.view.startup.scenes import it.parttimeteam.view.ViewConfig import it.parttimeteam.view.startup.PrivateGameSubmitViewEvent -import it.parttimeteam.view.startup.listeners.StartUpSceneListener -import it.parttimeteam.view.utils.{MachiavelliAlert, MachiavelliLabel, MachiavelliTextField} +import it.parttimeteam.view.startup.listeners.StartupSceneListener +import it.parttimeteam.view.startup.scenes.StartupSceneBottomBar.StartupSceneBottomBarImpl +import it.parttimeteam.view.startup.scenes.StartupSceneTopBar.StartupSceneTopBarImpl +import it.parttimeteam.view.utils.{ScalavelliAlert, ScalavelliLabel, ScalavelliTextField, Strings} import scalafx.geometry.Pos.Center import scalafx.scene.control.Alert.AlertType import scalafx.scene.control._ @@ -15,19 +17,19 @@ import scalafx.stage.Stage * * @param listener to interact with parent stage */ -class PrivateGameStartUpScene(override val parentStage: Stage, val listener: StartUpSceneListener) extends BaseStartUpFormScene(parentStage) { - val topBar: StartUpSceneTopBar = new StartUpSceneTopBar(listener) - val bottomBar: StartUpSceneBottomBar = new StartUpSceneBottomBar(() => submit()) +class PrivateGameScene(val parentStage: Stage, val listener: StartupSceneListener) extends StartupFormScene(parentStage) { + val topBar: StartupSceneTopBar = new StartupSceneTopBarImpl(listener) + val bottomBar: StartupSceneBottomBar = new StartupSceneBottomBarImpl(() => submit()) - val usernameLabel: Label = MachiavelliLabel("Username", ViewConfig.formLabelFontSize) - val usernameField: TextField = MachiavelliTextField("Username") + val usernameLabel: Label = ScalavelliLabel(Strings.USERNAME, ViewConfig.FORM_LABEL_FONT_SIZE) + val usernameField: TextField = ScalavelliTextField(Strings.USERNAME) - val codeLabel: Label = MachiavelliLabel("Code", ViewConfig.formLabelFontSize) - val codeField: TextField = MachiavelliTextField("Code") + val codeLabel: Label = ScalavelliLabel("Code", ViewConfig.FORM_LABEL_FONT_SIZE) + val codeField: TextField = ScalavelliTextField("Code") val center: VBox = new VBox() - center.spacing = ViewConfig.formSpacing - center.maxWidth = ViewConfig.formWidth + center.spacing = ViewConfig.FORM_SPACING + center.maxWidth = ViewConfig.FORM_WIDTH usernameLabel.maxWidth <== center.width usernameField.maxWidth <== center.width @@ -45,7 +47,8 @@ class PrivateGameStartUpScene(override val parentStage: Stage, val listener: Sta bottomBar.hideLoading() bottomBar.hideMessage() - val alert: Alert = MachiavelliAlert("Input missing", "You must enter username and code.", AlertType.Warning) + val alert: Alert = ScalavelliAlert(Strings.INPUT_MISSING_DIALOG_TITLE, Strings.INPUT_MISSING_USER_CODE_DIALOG_MESSAGE, AlertType.Warning, parentStage) + override def showMessage(message: String): Unit = bottomBar.showMessage(message) @@ -58,23 +61,29 @@ class PrivateGameStartUpScene(override val parentStage: Stage, val listener: Sta if (!username.isEmpty && !code.isEmpty) { listener.onSubmit(PrivateGameSubmitViewEvent(username, code)) bottomBar.showLoading() - disableButtons() + disableActions() } else { alert.showAndWait() } } - override def disableButtons(): Unit = { - bottomBar.disableButtons() + override def disableActions(): Unit = { + bottomBar.disableActions() usernameField.setEditable(false) codeField.setEditable(false) } + override def enableActions(): Unit = { + bottomBar.enableActions() + usernameField.setEditable(true) + codeField.setEditable(true) + } + override def resetScreen(): Unit = { usernameField.text = "" codeField.text = "" usernameField.setEditable(true) codeField.setEditable(true) - bottomBar.reset() + bottomBar.resetScreen() } } diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameScene.scala new file mode 100644 index 00000000..77b7622b --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameScene.scala @@ -0,0 +1,91 @@ +package it.parttimeteam.view.startup.scenes + +import it.parttimeteam.Constants +import it.parttimeteam.view.ViewConfig +import it.parttimeteam.view.startup.PublicGameSubmitViewEvent +import it.parttimeteam.view.startup.listeners.StartupSceneListener +import it.parttimeteam.view.startup.scenes.StartupSceneBottomBar.StartupSceneBottomBarImpl +import it.parttimeteam.view.startup.scenes.StartupSceneTopBar.StartupSceneTopBarImpl +import it.parttimeteam.view.utils.{ScalavelliAlert, ScalavelliLabel, ScalavelliTextField, Strings} +import scalafx.geometry.Pos.Center +import scalafx.scene.control.Alert.AlertType +import scalafx.scene.control._ +import scalafx.scene.layout.VBox +import scalafx.stage.Stage + +/** + * Allow to participate to a game by selecting the number of players to play with. + * + * @param listener to interact with parent stage + */ +class PublicGameScene(val parentStage: Stage, val listener: StartupSceneListener) extends StartupFormScene(parentStage) { + val topBar: StartupSceneTopBar = new StartupSceneTopBarImpl(listener) + val bottomBar: StartupSceneBottomBar = new StartupSceneBottomBarImpl(() => submit()) + + val usernameLabel: Label = ScalavelliLabel(Strings.USERNAME, ViewConfig.FORM_LABEL_FONT_SIZE) + val usernameField: TextField = ScalavelliTextField(Strings.USERNAME) + + val options: Range = Constants.Client.MIN_PLAYERS_NUM to Constants.Client.MAX_PLAYERS_NUM by 1 + + val selectPlayersLabel: Label = ScalavelliLabel(Strings.SELECT_PLAYERS_NUM, ViewConfig.FORM_LABEL_FONT_SIZE) + val comboBox = new ComboBox(options) + comboBox.setValue(Constants.Client.MIN_PLAYERS_NUM) + + val center: VBox = new VBox() + center.spacing = ViewConfig.FORM_SPACING + center.maxWidth = ViewConfig.FORM_WIDTH + + selectPlayersLabel.maxWidth <== center.width + usernameLabel.maxWidth <== center.width + + comboBox.maxWidth <== center.width + + center.alignment = Center + + mainContent.center = center + mainContent.top = topBar + mainContent.bottom = bottomBar + + bottomBar.hideLoading() + bottomBar.hideMessage() + + center.getChildren.addAll(usernameLabel, usernameField, selectPlayersLabel, comboBox) + + val alert: Alert = ScalavelliAlert(Strings.INPUT_MISSING_DIALOG_TITLE, Strings.INPUT_MISSING_USER_NUM_DIALOG_MESSAGE, AlertType.Warning, parentStage) + + override def showMessage(message: String): Unit = bottomBar.showMessage(message) + + override def hideMessage(): Unit = bottomBar.hideMessage() + + private def submit(): Unit = { + val username: String = usernameField.getText + val nPlayers: Int = comboBox.getValue + + if (!username.isEmpty && nPlayers >= Constants.Client.MIN_PLAYERS_NUM) { + listener.onSubmit(PublicGameSubmitViewEvent(username, nPlayers)) + bottomBar.showLoading() + disableActions() + } else { + alert.showAndWait() + } + } + + override def disableActions(): Unit = { + bottomBar.disableActions() + usernameField.setEditable(false) + comboBox.setDisable(true) + } + + override def enableActions(): Unit = { + bottomBar.enableActions() + usernameField.setEditable(true) + comboBox.setDisable(false) + } + + override def resetScreen(): Unit = { + usernameField.setEditable(true) + comboBox.setDisable(false) + usernameField.text = "" + bottomBar.resetScreen() + } +} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameStartUpScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameStartUpScene.scala deleted file mode 100644 index 4db08639..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/PublicGameStartUpScene.scala +++ /dev/null @@ -1,83 +0,0 @@ -package it.parttimeteam.view.startup.scenes - -import it.parttimeteam.GamePreferences -import it.parttimeteam.view.ViewConfig -import it.parttimeteam.view.startup.PublicGameSubmitViewEvent -import it.parttimeteam.view.startup.listeners.StartUpSceneListener -import it.parttimeteam.view.utils.{MachiavelliAlert, MachiavelliLabel, MachiavelliTextField} -import scalafx.geometry.Pos.Center -import scalafx.scene.control.Alert.AlertType -import scalafx.scene.control._ -import scalafx.scene.layout.VBox -import scalafx.stage.Stage - -/** - * Allow to participate to a game by selecting the number of players to play with. - * - * @param listener to interact with parent stage - */ -class PublicGameStartUpScene(override val parentStage: Stage, val listener: StartUpSceneListener) extends BaseStartUpFormScene(parentStage) { - val topBar: StartUpSceneTopBar = new StartUpSceneTopBar(listener) - val bottomBar: StartUpSceneBottomBar = new StartUpSceneBottomBar(() => submit()) - - val usernameLabel: Label = MachiavelliLabel("Username", ViewConfig.formLabelFontSize) - val usernameField: TextField = MachiavelliTextField("Username") - - val options: Range = GamePreferences.MIN_PLAYERS_NUM to GamePreferences.MAX_PLAYERS_NUM by 1 - - val selectPlayersLabel: Label = MachiavelliLabel("Select players number", ViewConfig.formLabelFontSize) - val comboBox = new ComboBox(options) - comboBox.setValue(GamePreferences.MIN_PLAYERS_NUM) - - val center: VBox = new VBox() - center.spacing = ViewConfig.formSpacing - center.maxWidth = ViewConfig.formWidth - - selectPlayersLabel.maxWidth <== center.width - usernameLabel.maxWidth <== center.width - - comboBox.maxWidth <== center.width - - center.alignment = Center - - mainContent.center = center - mainContent.top = topBar - mainContent.bottom = bottomBar - - bottomBar.hideLoading() - bottomBar.hideMessage() - - center.getChildren.addAll(usernameLabel, usernameField, selectPlayersLabel, comboBox) - - val alert: Alert = MachiavelliAlert("Input missing", "You must enter username and select players number.", AlertType.Warning) - - override def showMessage(message: String): Unit = bottomBar.showMessage(message) - - override def hideMessage(): Unit = bottomBar.hideMessage() - - private def submit(): Unit = { - val username: String = usernameField.getText - val nPlayers: Int = comboBox.getValue - - if (!username.isEmpty && nPlayers >= GamePreferences.MIN_PLAYERS_NUM) { - listener.onSubmit(PublicGameSubmitViewEvent(username, nPlayers)) - bottomBar.showLoading() - disableButtons() - } else { - alert.showAndWait() - } - } - - override def disableButtons(): Unit = { - bottomBar.disableButtons() - usernameField.setEditable(false) - comboBox.setDisable(true) - } - - override def resetScreen(): Unit = { - usernameField.setEditable(true) - comboBox.setDisable(false) - usernameField.text = "" - bottomBar.reset() - } -} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/SelectScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/SelectScene.scala index cdce4cfd..98245cd4 100644 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/SelectScene.scala +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/SelectScene.scala @@ -2,7 +2,7 @@ package it.parttimeteam.view.startup.scenes import it.parttimeteam.view.ViewConfig import it.parttimeteam.view.startup.listeners.SelectSceneListener -import it.parttimeteam.view.utils.MachiavelliButton +import it.parttimeteam.view.utils.{ImagePaths, ScalavelliButton, Strings} import scalafx.geometry.Pos.Center import scalafx.scene.control.Button import scalafx.scene.image.{Image, ImageView} @@ -15,20 +15,20 @@ import scalafx.stage.Stage * @param parentStage the stage which contains the scene * @param listener the listener which allow to select the modality */ -class SelectScene(val parentStage: Stage, val listener: SelectSceneListener) extends BaseStartUpScene(parentStage) { - val title: ImageView = new ImageView(new Image("/images/game_title.png")) { +class SelectScene(val parentStage: Stage, val listener: SelectSceneListener) extends BaseStartupScene(parentStage) { + val title: ImageView = new ImageView(new Image(ImagePaths.GAME_TITLE)) { fitWidth <== parentStage.width / 3 preserveRatio = true } val center: VBox = new VBox() center.alignment = Center - center.spacing = ViewConfig.formSpacing - center.setMaxWidth(ViewConfig.formWidth) + center.spacing = ViewConfig.FORM_SPACING + center.setMaxWidth(ViewConfig.FORM_WIDTH) - val btnPublicGame: Button = MachiavelliButton("Start new game", () => listener.onSelectedPublicGame()) - val btnPrivateGame: Button = MachiavelliButton("Participate with a code", () => listener.onSelectedPrivateGame()) - val btnCreatePrivateGame: Button = MachiavelliButton("Create new code", () => listener.onSelectedCreatePrivateGame()) + val btnPublicGame: Button = ScalavelliButton(Strings.START_NEW_GAME, () => listener.onSelectedPublicGame()) + val btnPrivateGame: Button = ScalavelliButton(Strings.JOIN_WITH_CODE, () => listener.onSelectedPrivateGame()) + val btnCreatePrivateGame: Button = ScalavelliButton(Strings.CREATE_PRIVATE_CODE, () => listener.onSelectedCreatePrivateGame()) btnPublicGame.prefWidth <== center.width btnPrivateGame.prefWidth <== center.width diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneBottomBar.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneBottomBar.scala deleted file mode 100644 index 62e8da28..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneBottomBar.scala +++ /dev/null @@ -1,47 +0,0 @@ -package it.parttimeteam.view.startup.scenes - -import it.parttimeteam.view.utils.{MachiavelliButton, MachiavelliLabel} -import scalafx.application.Platform -import scalafx.geometry.Pos.BottomRight -import scalafx.scene.control.{Button, Label, ProgressIndicator} -import scalafx.scene.layout.HBox - -class StartUpSceneBottomBar(onSubmit: () => Unit) extends HBox { - alignment = BottomRight - - val btnSubmit: Button = MachiavelliButton("Send", () => onSubmit()) - val progress: ProgressIndicator = new ProgressIndicator() - val messageContainer: Label = MachiavelliLabel() - - progress.prefHeight <== height - - children.addAll(messageContainer, progress, btnSubmit) - - def disableButtons(): Unit = btnSubmit.setDisable(true) - - def enableButtons(): Unit = btnSubmit.setDisable(false) - - def showLoading(): Unit = progress.setVisible(true) - - def hideLoading(): Unit = progress.setVisible(false) - - def showMessage(message: String): Unit = { - Platform.runLater({ - messageContainer.setText(message) - messageContainer.setVisible(true) - }) - } - - def hideMessage(): Unit = { - Platform.runLater({ - messageContainer.setText("") - messageContainer.setVisible(false) - }) - } - - def reset(): Unit = { - enableButtons() - hideLoading() - hideMessage() - } -} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneTopBar.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneTopBar.scala deleted file mode 100644 index 34f87e6c..00000000 --- a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartUpSceneTopBar.scala +++ /dev/null @@ -1,12 +0,0 @@ -package it.parttimeteam.view.startup.scenes - -import it.parttimeteam.view.startup.listeners.StartUpSceneListener -import it.parttimeteam.view.utils.MachiavelliButton -import scalafx.scene.control.Button -import scalafx.scene.layout.HBox - -class StartUpSceneTopBar(listener: StartUpSceneListener) extends HBox { - val btnBack: Button = MachiavelliButton("<", () => listener.onBackPressed()) - - children.add(btnBack) -} diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupFormScene.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupFormScene.scala new file mode 100644 index 00000000..c8f204f2 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupFormScene.scala @@ -0,0 +1,13 @@ +package it.parttimeteam.view.startup.scenes + +import scalafx.stage.Stage + +/** + * Extend by all the Startup form scenes + * + * @param parentStage the parent stage + */ +abstract class StartupFormScene(parentStage: Stage) extends BaseStartupScene(parentStage) with BaseStartupFormScene { + +} + diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneBottomBar.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneBottomBar.scala new file mode 100644 index 00000000..e1616ddf --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneBottomBar.scala @@ -0,0 +1,67 @@ +package it.parttimeteam.view.startup.scenes + +import it.parttimeteam.view.utils.{ScalavelliButton, ScalavelliLabel, Strings} +import scalafx.application.Platform +import scalafx.geometry.Pos.BottomRight +import scalafx.scene.control.{Button, Label, ProgressIndicator} +import scalafx.scene.layout.HBox + +/** + * Bottom bar for each Startup Scene + */ +trait StartupSceneBottomBar extends HBox with BaseStartupFormScene { + /** + * Show progress + */ + def showLoading(): Unit + + /** + * Hide progress + */ + def hideLoading(): Unit +} + +object StartupSceneBottomBar { + + class StartupSceneBottomBarImpl(onSubmit: () => Unit) extends StartupSceneBottomBar { + alignment = BottomRight + + val btnSubmit: Button = ScalavelliButton(Strings.SEND, () => onSubmit()) + val progress: ProgressIndicator = new ProgressIndicator() + val messageContainer: Label = ScalavelliLabel() + + progress.prefHeight <== height + + children.addAll(messageContainer, progress, btnSubmit) + + override def enableActions(): Unit = btnSubmit.setDisable(false) + + override def showLoading(): Unit = progress.setVisible(true) + + override def hideLoading(): Unit = progress.setVisible(false) + + override def disableActions(): Unit = btnSubmit.setDisable(true) + + override def showMessage(message: String): Unit = { + Platform.runLater({ + messageContainer.setText(message) + messageContainer.setVisible(true) + }) + } + + override def hideMessage(): Unit = { + Platform.runLater({ + messageContainer.setText("") + messageContainer.setVisible(false) + }) + } + + override def resetScreen(): Unit = { + enableActions() + hideLoading() + hideMessage() + } + } + +} + diff --git a/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneTopBar.scala b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneTopBar.scala new file mode 100644 index 00000000..8652ea06 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/startup/scenes/StartupSceneTopBar.scala @@ -0,0 +1,24 @@ +package it.parttimeteam.view.startup.scenes + +import it.parttimeteam.view.startup.listeners.StartupSceneListener +import it.parttimeteam.view.utils.ScalavelliButton +import scalafx.scene.control.Button +import scalafx.scene.layout.HBox + +/** + * Bottom bar for each Startup Scene + */ +trait StartupSceneTopBar extends HBox { + +} + + +object StartupSceneTopBar { + + class StartupSceneTopBarImpl(listener: StartupSceneListener) extends StartupSceneTopBar { + val btnBack: Button = ScalavelliButton("<", () => listener.onBackPressed()) + + children.add(btnBack) + } + +} \ No newline at end of file diff --git a/client/src/main/scala/it/parttimeteam/view/utils/CardUtils.scala b/client/src/main/scala/it/parttimeteam/view/utils/CardUtils.scala index 4871dc7a..9f5c38d6 100644 --- a/client/src/main/scala/it/parttimeteam/view/utils/CardUtils.scala +++ b/client/src/main/scala/it/parttimeteam/view/utils/CardUtils.scala @@ -13,7 +13,7 @@ object CardUtils { * @return the card image path */ def getCardPath(card: Card): String = { - val path: String = "/images/cards/" + val path: String = ImagePaths.CARDS_BASE_PATH val suitPath = card.suit match { case Hearts() => "H" case Diamonds() => "D" diff --git a/client/src/main/scala/it/parttimeteam/view/utils/ImagePaths.scala b/client/src/main/scala/it/parttimeteam/view/utils/ImagePaths.scala new file mode 100644 index 00000000..4df11ec9 --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/utils/ImagePaths.scala @@ -0,0 +1,15 @@ +package it.parttimeteam.view.utils + +object ImagePaths { + final val GAME_TITLE = "/images/game_title.png" + + final val BLUE_BACK_CARD = "images/cards/backBlue.png" + final val STARTUP_BACKGROUND = "/images/background.png" + + final val UNDO_BTN = "images/undo.png" + final val REDO_BTN = "images/redo.png" + final val PICK_ICON = "images/pick.png" + final val PROHIBITION_SIGN = "images/prohibitionSign.png" + + final val CARDS_BASE_PATH = "/images/cards/" +} diff --git a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliAlert.scala b/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliAlert.scala deleted file mode 100644 index ad2ff5a0..00000000 --- a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliAlert.scala +++ /dev/null @@ -1,18 +0,0 @@ -package it.parttimeteam.view.utils - -import scalafx.scene.control.Alert -import scalafx.scene.control.Alert.AlertType - -/** - * Builder for a default Alert dialog - */ -object MachiavelliAlert { - - def apply(title: String, message: String, alertType: AlertType): Alert = { - val alert: Alert = new Alert(alertType) - alert.setTitle(title) - alert.setHeaderText(null) - alert.setContentText(message) - alert - } -} diff --git a/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliAlert.scala b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliAlert.scala new file mode 100644 index 00000000..675eaf9d --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliAlert.scala @@ -0,0 +1,31 @@ +package it.parttimeteam.view.utils + +import it.parttimeteam.model.ErrorEvent +import scalafx.scene.control.Alert +import scalafx.scene.control.Alert.AlertType +import scalafx.stage.Stage + +/** + * Builder for a default Alert dialog + */ +object ScalavelliAlert { + + def apply(title: String, message: String, alertType: AlertType, parent: Stage): Alert = { + val alert: Alert = new Alert(alertType) + alert.setTitle(title) + alert.setHeaderText(null) + alert.setContentText(message) + //alert.initOwner(parent) // this break dialogs content + alert + } + + def apply(title: String, error: ErrorEvent, alertType: AlertType, parent: Stage): Alert = { + val alert: Alert = new Alert(alertType) + alert.setTitle(title) + alert.setHeaderText(null) + alert.setContentText(StringParser.parseError(error)) + //alert.initOwner(parent) // this break dialogs content + alert + } + +} diff --git a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliButton.scala b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliButton.scala similarity index 97% rename from client/src/main/scala/it/parttimeteam/view/utils/MachiavelliButton.scala rename to client/src/main/scala/it/parttimeteam/view/utils/ScalavelliButton.scala index 9ee2cbea..e27bea9f 100644 --- a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliButton.scala +++ b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliButton.scala @@ -6,7 +6,7 @@ import scalafx.scene.image.{Image, ImageView} /** * Builder for a default Button */ -object MachiavelliButton { +object ScalavelliButton { def apply(text: String, onClick: () => Unit): Button = { val btn: Button = new Button(text) diff --git a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliLabel.scala b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliLabel.scala similarity index 87% rename from client/src/main/scala/it/parttimeteam/view/utils/MachiavelliLabel.scala rename to client/src/main/scala/it/parttimeteam/view/utils/ScalavelliLabel.scala index a18bfa2f..b0cb9809 100644 --- a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliLabel.scala +++ b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliLabel.scala @@ -7,7 +7,7 @@ import scalafx.scene.text.Font /** * Builder for a default Label */ -object MachiavelliLabel { +object ScalavelliLabel { def apply(text: String, fontSize:Double): Label = { val label:Label = Label(text) @@ -16,7 +16,7 @@ object MachiavelliLabel { } def apply(text: String): Label = { - val label = this (text, ViewConfig.baseFontSize) + val label = this (text, ViewConfig.BASE_FONT_SIZE) label } diff --git a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliTextField.scala b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliTextField.scala similarity index 90% rename from client/src/main/scala/it/parttimeteam/view/utils/MachiavelliTextField.scala rename to client/src/main/scala/it/parttimeteam/view/utils/ScalavelliTextField.scala index 4e595914..a5471cc8 100644 --- a/client/src/main/scala/it/parttimeteam/view/utils/MachiavelliTextField.scala +++ b/client/src/main/scala/it/parttimeteam/view/utils/ScalavelliTextField.scala @@ -5,7 +5,7 @@ import scalafx.scene.control.TextField /** * Builder for a default TextField */ -object MachiavelliTextField { +object ScalavelliTextField { def apply(promptText: String): TextField = { val textField: TextField = new TextField() diff --git a/client/src/main/scala/it/parttimeteam/view/utils/StringParser.scala b/client/src/main/scala/it/parttimeteam/view/utils/StringParser.scala new file mode 100644 index 00000000..7c84442f --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/utils/StringParser.scala @@ -0,0 +1,37 @@ +package it.parttimeteam.view.utils + +import it.parttimeteam.controller.ViewMessage +import it.parttimeteam.controller.ViewMessage._ +import it.parttimeteam.model.ErrorEvent +import it.parttimeteam.model.ErrorEvent._ + +object StringParser { + + def parseError(error: ErrorEvent): String = error match { + + case GenericError(reason: String) => reason + + case ServerNotFound => Strings.Error.SERVER_NOT_FOUND + + case NoValidTurnPlay => Strings.Error.NOT_VALID_PLAY + + case CombinationNotValid => Strings.Error.COMBINATION_NOT_VALID + + case HandNotContainCard => Strings.Error.CARD_NOT_CONTAINED_IN_HAND + + case NoCardInBoard => Strings.Error.NO_CARD_IN_BOARD + + case LobbyCodeNotValid => Strings.Error.LOBBY_CODE_NOT_VALID + + case _ => Strings.Error.UNEXPECTED_ERROR + } + + def parseMessage(message: ViewMessage): String = message match { + + case ActualPlayerTurn(playerName: String) => Strings.PLAYER_TURN_MESSAGE(playerName) + + case YourTurn => Strings.YOUR_TURN + + case _ => "" + } +} diff --git a/client/src/main/scala/it/parttimeteam/view/utils/Strings.scala b/client/src/main/scala/it/parttimeteam/view/utils/Strings.scala new file mode 100644 index 00000000..9e0acf1d --- /dev/null +++ b/client/src/main/scala/it/parttimeteam/view/utils/Strings.scala @@ -0,0 +1,57 @@ +package it.parttimeteam.view.utils + +object Strings { + + final val CLEAR_SELECTION_BTN = "Clear Selection" + final val CLOSE = "Close" + final val CREATE_PRIVATE_CODE = "Create a private code" + final val ERROR_DIALOG_TITLE = "Error" + final val GAME_ENDED_ALERT_TITLE = "Game ended" + final val GAME_END_PLAYER_LEFT_MESSAGE = s"The game ended. Another player left the game. Do you want to play again?" + final val GAME_LOADING_MESSAGE = "Preparing your cards..." + final val GAME_LOADING_TITLE = "Game loading" + final val GAME_WON_ALERT_MESSAGE = "Congratulations! You won the game! Do you want to play again?" + final val HERE_IS_YOUR_CODE = "Here is your code" + final val INPUT_MISSING_DIALOG_TITLE = "Input missing" + final val INPUT_MISSING_USER_CODE_DIALOG_MESSAGE = "You must enter both username and code." + final val INPUT_MISSING_USER_NUM_DIALOG_MESSAGE = "You must enter username and select players number." + final val JOIN_WITH_CODE = "Join with a code" + final val LEAVE_GAME_BTN = "Leave Game" + final val LEAVE_GAME_DIALOG_MESSAGE = "Are you sure you want to leave the game? The action cannot be undone." + final val LEAVE_GAME_DIALOG_TITLE = "Leave the game" + final val MAKE_COMBINATION_BTN = "Make Combination" + final val OTHER_PLAYERS = "Other players:" + final val PASS_AND_DRAW_BTN = "Pass & Draw" + final val PASS_BTN = "Pass" + final val PICK_CARDS_BTN = "Pick Cards" + final val RESET_BTN = "Reset" + final val RETRY = "Retry" + final val SELECT_PLAYERS_NUM = "Select players number" + final val SEND = "Send" + final val SORT_RANK_BTN = "Sort Rank" + final val SORT_SUIT_BTN = "Sort Suit" + final val START_NEW_GAME = "Start new game" + final val TIMER = "Timer" + final val TIMER_END_INFO_MESSAGE = "You haven't passed your turn. We will pass and draw a card for you." + final val TIME_IS_UP = "Your time is up!" + final val UPDATE_COMBINATION_BTN = "Update Combination" + final val USERNAME = "Username" + final val WAITING_FOR_PLAYERS = "Waiting for other players" + final val YOUR_TURN = "Your turn" + final val YOUR_TURN_ALERT_MESSAGE = "It's your turn" + + final def GAME_LOST_ALERT_MESSAGE(winnerUsername: String): String = s"This game has a winner. And the winner is.. $winnerUsername! Do you want to play again?" + + final def PLAYER_TURN_MESSAGE(playerName: String): String = s"It's $playerName turn" + + object Error { + final val CARD_NOT_CONTAINED_IN_HAND = "Hand doesn't contains a card you selected." + final val COMBINATION_NOT_VALID = "Invalid combination." + final val LOBBY_CODE_NOT_VALID = "The code is not valid." + final val NOT_VALID_PLAY = "Invalid play in this turn." + final val NO_CARD_IN_BOARD = "Board doesn't contains a card you selected." + final val SERVER_NOT_FOUND = "Server not found." + final val UNEXPECTED_ERROR = "An unexpected error occurred." + } + +} diff --git a/client/src/test/scala/it/parttimeteam/model/game/RemoteGameActorSpec.scala b/client/src/test/scala/it/parttimeteam/model/game/RemoteGameActorSpec.scala index c94e46ef..c2a9eea2 100644 --- a/client/src/test/scala/it/parttimeteam/model/game/RemoteGameActorSpec.scala +++ b/client/src/test/scala/it/parttimeteam/model/game/RemoteGameActorSpec.scala @@ -33,34 +33,28 @@ class RemoteGameActorSpec extends TestKit(ActorSystem("test", ConfigFactory.load (mockListener.gameStateUpdated _).verify(*).once() } - "notify the lister on turn started" in { + "notify the listener on turn started" in { actor ! PlayerTurn (mockListener.turnStarted _).verify().once() } - "notify the lister on opponent turn started" in { + "notify the listener on opponent turn started" in { val name = "sampleName" actor ! OpponentInTurn(name) (mockListener.opponentInTurn _).verify(name).once() } - "notify the lister on turn ended" in { + "notify the listener on turn ended" in { actor ! TurnEnded (mockListener.turnEnded _).verify().once() } - "notify the lister on turn ended with card drawn" in { - val sampleCard = Card.string2card("2CR") - actor ! CardDrawn(sampleCard) - (mockListener.turnEndedWithCartDrawn _).verify(sampleCard).once() - } - - "notify the lister on game finished with a win" in { + "notify the listener on game finished with a win" in { actor ! Won (mockListener.gameWon _).verify().once() } - "notify the lister on game finished with a lost " in { + "notify the listener on game finished with a lost " in { val winnerName = "winner" actor ! Lost(winnerName) (mockListener.gameLost _).verify(winnerName).once() diff --git a/commons/src/main/scala/it/parttimeteam/Constants.scala b/commons/src/main/scala/it/parttimeteam/Constants.scala index 755aeef0..e4c6ebe6 100644 --- a/commons/src/main/scala/it/parttimeteam/Constants.scala +++ b/commons/src/main/scala/it/parttimeteam/Constants.scala @@ -3,8 +3,10 @@ package it.parttimeteam object Constants { object Client { - final val GAME_NAME = "Machiavelli" + final val GAME_NAME = "Scalavelli" final val TURN_TIMER_DURATION = 120 + final val MIN_PLAYERS_NUM: Int = 2 + final val MAX_PLAYERS_NUM: Int = 6 } object Remote { diff --git a/commons/src/main/scala/it/parttimeteam/GamePreferences.scala b/commons/src/main/scala/it/parttimeteam/GamePreferences.scala deleted file mode 100644 index 49516ef0..00000000 --- a/commons/src/main/scala/it/parttimeteam/GamePreferences.scala +++ /dev/null @@ -1,6 +0,0 @@ -package it.parttimeteam - -object GamePreferences { - val MIN_PLAYERS_NUM: Int = 2 - val MAX_PLAYERS_NUM: Int = 6 -} diff --git a/commons/src/main/scala/it/parttimeteam/messages/GameMessage.scala b/commons/src/main/scala/it/parttimeteam/messages/GameMessage.scala index 3881e3e9..19822e21 100644 --- a/commons/src/main/scala/it/parttimeteam/messages/GameMessage.scala +++ b/commons/src/main/scala/it/parttimeteam/messages/GameMessage.scala @@ -56,13 +56,6 @@ object GameMessage { */ case class MatchErrorOccurred(errorType: MatchError) - /** - * Send the drawn card to the current player - * - * @param card drawn card - */ - case class CardDrawn(card: Card) // TODO MATTEOC remove - /** * Tells the current player his turn is finished */ @@ -92,8 +85,6 @@ object GameMessage { object MatchError { - case object InvalidPlays extends MatchError - case object PlayerActionNotValid extends MatchError } diff --git a/core/src/main/resources/rules.pl b/core/src/main/resources/rules.pl index 3f5c092f..89921a2a 100644 --- a/core/src/main/resources/rules.pl +++ b/core/src/main/resources/rules.pl @@ -34,24 +34,24 @@ lengthList([(_,_)|T],X) :- lengthList(T,N), X is N+1. % sameNumber(+List) -sameNumber([(N,_,_)]). +sameNumber([(_,_,_)]). sameNumber([(N1,_,_), (N2,_,_) | T]) :- integer(N1), integer(N2), N1 =:= N2, sameNumber([(N2,_,_)| T]). % sameSuit(+List) -sameSuit([(_,S,_)]). +sameSuit([(_,_,_)]). sameSuit([(_,S1,_), (_,S2,_) | T]) :- S1 == S2, sameSuit([(_,S2,_)| T]). % sameElementList(+ListSuit, +Suit) -sameElementList([], Suit). +sameElementList([], _). sameElementList([H|T], Suit) :- H \== Suit, sameElementList(T, Suit). % differentSuit(+List) -differentSuit([], Suit). +differentSuit([], _). differentSuit([(_, S1, _), (_, S2, _) |T]) :- S1 \== S2, append([], [S1, S2], ListSuit), differentSuit(T, ListSuit). @@ -61,13 +61,13 @@ differentSuit(T, NewListSuit). % endSequence(+Cards) -endSequence([(N,_,_)]). -endSequence([(N1,_,_), (N2,_,_) | T]) :- ( N1 =:= 13, N2 =:= 14 -> lengthList(T, S), S =:= 0 - ; endSequence([(N2,_,_) | T]) - ). +endSequence([(_,_,_)]). +endSequence([(13,_,_), (14,_,_) | T]):- lengthList(T, S), S =:= 0. +endSequence([(_,_,_), (N2,_,_) | T]) :- endSequence([(N2,_,_) | T]). + % orderByValue(+List) -checkOrderByValue([(N,_,_)]). +checkOrderByValue([(_,_,_)]). checkOrderByValue([(N1,_,_), (N2,_,_) | T]) :- integer(N1), integer(N2), X is N1 + 1, @@ -79,7 +79,6 @@ sameNumber(L), differentSuit(L). - % validationChain(+Cards) validationChain(L) :- lengthList(L, X), X >= 3, X =< 14, sameSuit(L), @@ -94,7 +93,9 @@ append(LOs,[(X,Sx,Cx)|BOs],Ys). partitionValue([],_,[],[]). -partitionValue([(X,Sx,Cx)|Xs],(Y,Sy,Cy),[(X,Sx,Cx)|Ls],Bs):- X { - val drawn = acc._1 draw() - (drawn._1, drawn._2 +: acc._2) + acc._1 draw() match { + case (deck, Some(card)) => (deck, card +: acc._2) + case (deck, None) => (deck, acc._2) + } } } diff --git a/core/src/main/scala/it/parttimeteam/core/prolog/PrologGame.scala b/core/src/main/scala/it/parttimeteam/core/prolog/PrologGame.scala index 66ce8e01..e026106c 100644 --- a/core/src/main/scala/it/parttimeteam/core/prolog/PrologGame.scala +++ b/core/src/main/scala/it/parttimeteam/core/prolog/PrologGame.scala @@ -75,7 +75,7 @@ class PrologGame() { */ def sortByRank(cards: Seq[Card]): Seq[Card] = { - val optionalAceCards: Seq[Card] = conversion optionalValueCards cards + val optionalAceCards: Seq[Card] = conversion optionalValueAce cards val prologResult: Seq[Term] = engine goal orderByValue + conversion.cardsConvertToString(optionalAceCards)(Some(X)) conversion sortedCard prologResult } diff --git a/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologConverter.scala b/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologConverter.scala index 4adc5424..1bee171b 100644 --- a/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologConverter.scala +++ b/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologConverter.scala @@ -41,7 +41,7 @@ trait PrologConverter { * @param cards sequence of cards * @return new sequence of cards */ - def optionalValueCards(cards: Seq[Card]): Seq[Card] + def optionalValueAce(cards: Seq[Card]): Seq[Card] /** * Convert tuple sequence in a prolog list diff --git a/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologGameConverter.scala b/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologGameConverter.scala index 3df42074..23c683ea 100644 --- a/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologGameConverter.scala +++ b/core/src/main/scala/it/parttimeteam/core/prolog/converter/PrologGameConverter.scala @@ -48,7 +48,7 @@ class PrologGameConverter extends PrologConverter { /** * @inheritdoc */ - override def optionalValueCards(cards: Seq[Card]): Seq[Card] = { + override def optionalValueAce(cards: Seq[Card]): Seq[Card] = { // List where overflowaces are converted into ace val convertList: Seq[Card] = cards.map(card => if (card.rank == OverflowAce()) card.copy(rank = Ace()) else card) diff --git a/core/src/test/scala/it/parttimeteam/core/GameInterfaceSuite.scala b/core/src/test/scala/it/parttimeteam/core/GameInterfaceSuite.scala index 1e6e5495..a6568e87 100644 --- a/core/src/test/scala/it/parttimeteam/core/GameInterfaceSuite.scala +++ b/core/src/test/scala/it/parttimeteam/core/GameInterfaceSuite.scala @@ -5,8 +5,6 @@ import it.parttimeteam.core.collections.{Board, CardCombination, Deck, Hand} import org.scalamock.matchers.Matchers import org.scalamock.scalatest.MockFactory import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.must.Matchers.be -import org.scalatest.matchers.should.Matchers.an class GameInterfaceSuite extends AnyFunSpec with MockFactory with Matchers { describe("A game manager") { @@ -39,14 +37,19 @@ class GameInterfaceSuite extends AnyFunSpec with MockFactory with Matchers { it("Draw cards from a non empty deck") { val drawnDeck = Deck.cards2deck(deck.cards.tail) - assert(gameInterface.draw(deck) equals(drawnDeck, deck.cards.head)) + assert(gameInterface.draw(deck) equals(drawnDeck, deck.cards.headOption)) } - it("Throw an exception when drawing from an empty deck") { + it("Doesn't draw cards from an empty deck") { + val emptyDeck = Deck.empty + assert(gameInterface.draw(emptyDeck) equals(emptyDeck, None)) + } + + /*it("Throw an exception when drawing from an empty deck") { val emptyDeck = Deck.empty an[UnsupportedOperationException] should be thrownBy (gameInterface draw emptyDeck) - } + }*/ } describe("Validate a move") { @@ -115,7 +118,7 @@ class GameInterfaceSuite extends AnyFunSpec with MockFactory with Matchers { it("Play a combination with a royal flush") { val handSeq = Seq(ACE_CLUBS_RED, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, ACE_CLUBS_BLUE, QUEEN_CLUBS, KING_CLUBS) - val cards = Seq(ACE_CLUBS_RED, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, ACE_CLUBS_BLUE, QUEEN_CLUBS, KING_CLUBS) + val cards = Seq(ACE_CLUBS_RED, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, ACE_CLUBS_BLUE, QUEEN_CLUBS, KING_CLUBS) val result = gameInterface.playCombination(Hand(handSeq), state.board, cards) assert(result.isRight) } @@ -164,6 +167,14 @@ class GameInterfaceSuite extends AnyFunSpec with MockFactory with Matchers { val res = gameInterface.putCardsInCombination(hand, board.right.get, "#1", Seq(THREE_CLUBS, FOUR_CLUBS)) assertResult(Board(Seq(comb)))(res.right.get._2) } + + it("Update a jack, queen, king combination") { + val hand = Hand(Seq(ACE_CLUBS_BLUE, FIVE_DIAMONDS, EIGHT_CLUBS)) + val comb = CardCombination("#1", Seq(JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS)) + + val res = gameInterface.putCardsInCombination(hand, Board(Seq(comb)), "#1", Seq(ACE_CLUBS_BLUE)) + assertResult(Board(Seq(CardCombination("#1", Seq(JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE)))))(res.right.get._2) + } } } } \ No newline at end of file diff --git a/core/src/test/scala/it/parttimeteam/core/collections/DeckSpec.scala b/core/src/test/scala/it/parttimeteam/core/collections/DeckSpec.scala index 5eb9f4ce..11de9255 100644 --- a/core/src/test/scala/it/parttimeteam/core/collections/DeckSpec.scala +++ b/core/src/test/scala/it/parttimeteam/core/collections/DeckSpec.scala @@ -23,7 +23,7 @@ class DeckSpec extends AnyFunSpec { assert(before equals 104) val cardDrawn = deck.draw() assert(cardDrawn._1.remaining equals 103) - assert(!(cardDrawn._2.name equals "")) + assert(cardDrawn._2.isDefined) } it("Must be empty if all cards are drawn") { diff --git a/core/src/test/scala/it/parttimeteam/core/prolog/converter/PrologGameConverterSuite.scala b/core/src/test/scala/it/parttimeteam/core/prolog/converter/PrologGameConverterSuite.scala index 2b3d1cbe..bd13df15 100644 --- a/core/src/test/scala/it/parttimeteam/core/prolog/converter/PrologGameConverterSuite.scala +++ b/core/src/test/scala/it/parttimeteam/core/prolog/converter/PrologGameConverterSuite.scala @@ -38,11 +38,11 @@ class PrologGameConverterSuite extends AnyFunSuite { test("It replaces the Ace value card in the Overflow Ace card in specific cases") { - assertResult(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE.copy(rank = "14")))(prologConverter.optionalValueCards(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE))) + assertResult(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE.copy(rank = "14")))(prologConverter.optionalValueAce(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE))) - assertResult(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE.copy(rank = "14"), ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))(prologConverter.optionalValueCards(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE, ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))) + assertResult(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE.copy(rank = "14"), ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))(prologConverter.optionalValueAce(Seq(QUEEN_CLUBS, KING_CLUBS, ACE_CLUBS_BLUE, ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))) - assertResult(Seq(ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))(prologConverter.optionalValueCards(Seq(ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))) + assertResult(Seq(ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))(prologConverter.optionalValueAce(Seq(ACE_CLUBS_BLUE, TWO_CLUBS, THREE_CLUBS))) } test("Convert a tuple sequence into string") { diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf index 2bcfafd2..3031700d 100644 --- a/server/src/main/resources/application.conf +++ b/server/src/main/resources/application.conf @@ -29,12 +29,6 @@ akka { log-sent-messages = on log-received-messages = on log-remote-lifecycle-events = off - -// watch-failure-detector { -// # How often keep-alive heartbeat messages should be sent to each connection. -// heartbeat-interval = 2 s -// } - } } diff --git a/server/src/main/scala/it/parttimeteam/lobby/LobbyManagerActor.scala b/server/src/main/scala/it/parttimeteam/lobby/LobbyManagerActor.scala index 6680c395..6740715c 100644 --- a/server/src/main/scala/it/parttimeteam/lobby/LobbyManagerActor.scala +++ b/server/src/main/scala/it/parttimeteam/lobby/LobbyManagerActor.scala @@ -1,8 +1,8 @@ package it.parttimeteam.lobby import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} -import it.parttimeteam.`match`.GameMatchManagerActor -import it.parttimeteam.`match`.GameMatchManagerActor.GamePlayers +import it.parttimeteam.`match`.{GameHelper, GameMatchActor} +import it.parttimeteam.`match`.GameMatchActor.GamePlayers import it.parttimeteam.common.{GamePlayer, IdGenerator} import it.parttimeteam.core.GameInterfaceImpl import it.parttimeteam.messages.LobbyMessages.LobbyError.PrivateLobbyIdNotValid @@ -84,7 +84,7 @@ class LobbyManagerActor extends Actor with IdGenerator with ActorLogging { } private def generateAndStartGameActor(lobbyType: LobbyType)(players: Seq[GamePlayer]): Unit = { - val gameActor = context.actorOf(GameMatchManagerActor.props(lobbyType.numberOfPlayers, new GameInterfaceImpl())) + val gameActor = context.actorOf(GameMatchActor.props(lobbyType.numberOfPlayers, new GameHelper(new GameInterfaceImpl()))) players.foreach(p => { context.unwatch(p.actorRef) // remove player form lobby diff --git a/server/src/main/scala/it/parttimeteam/lobby/PrivateLobbyService.scala b/server/src/main/scala/it/parttimeteam/lobby/PrivateLobbyService.scala index 1b2fc721..5457e2fc 100644 --- a/server/src/main/scala/it/parttimeteam/lobby/PrivateLobbyService.scala +++ b/server/src/main/scala/it/parttimeteam/lobby/PrivateLobbyService.scala @@ -10,6 +10,11 @@ object PrivateLobbyService { */ trait PrivateLobbyService { + /** + * + * @param numberOfPlayers number of player + * @return the + */ def generateNewPrivateLobby(numberOfPlayers: Int): PrivateLobby def retrieveExistingLobby(lobbyId: String): Option[PrivateLobby] diff --git a/server/src/main/scala/it/parttimeteam/match/GameMatchManager.scala b/server/src/main/scala/it/parttimeteam/match/GameHelper.scala similarity index 80% rename from server/src/main/scala/it/parttimeteam/match/GameMatchManager.scala rename to server/src/main/scala/it/parttimeteam/match/GameHelper.scala index 12835c91..9e51f786 100644 --- a/server/src/main/scala/it/parttimeteam/match/GameMatchManager.scala +++ b/server/src/main/scala/it/parttimeteam/match/GameHelper.scala @@ -1,13 +1,14 @@ package it.parttimeteam.`match` -import it.parttimeteam.{DrawCard, PlayedMove, PlayerAction} -import it.parttimeteam.`match`.GameMatchManagerActor.{CardDrawnInfo, StateResult} +import it.parttimeteam.`match`.GameMatchActor.{CardDrawnInfo, StateResult} import it.parttimeteam.common.GamePlayer import it.parttimeteam.core.collections.{Board, Hand} import it.parttimeteam.core.player.Player.{PlayerId, PlayerName} import it.parttimeteam.core.{GameInterface, GameState} +import it.parttimeteam.messages.GameMessage.MatchError +import it.parttimeteam.{DrawCard, PlayedMove, PlayerAction} -class GameMatchManager(private val gameApi: GameInterface) { +class GameHelper(private val gameApi: GameInterface) { def retrieveInitialState(players: Seq[(PlayerId, PlayerName)]): GameState = @@ -22,13 +23,10 @@ class GameMatchManager(private val gameApi: GameInterface) { * @param playerAction action made my the player * @return a state result or a string representing an error */ - def determineNextState(currentState: GameState, playerInTurn: GamePlayer, playerAction: PlayerAction): Either[String, StateResult] = { + def determineNextState(currentState: GameState, playerInTurn: GamePlayer, playerAction: PlayerAction): Either[MatchError, StateResult] = { playerAction match { case DrawCard => nextStateOnCardDrawn(currentState, playerInTurn) - case PlayedMove(updatedHand, updatedBoard) => nextStateOnPlayerMove(currentState, playerInTurn, updatedHand, updatedBoard) - - case _ => Left("Non supported action") } } @@ -46,7 +44,7 @@ class GameMatchManager(private val gameApi: GameInterface) { )) } else { - Left("Non valid plays") + Left(MatchError.PlayerActionNotValid) } } @@ -57,12 +55,12 @@ class GameMatchManager(private val gameApi: GameInterface) { val updatedState = currentState .getPlayer(playerInTurn.id) .map(p => currentState.updatePlayer(p.copy( - hand = p.hand.copy(playerCards = cardDrawn +: p.hand.playerCards)))) + hand = p.hand.copy(playerCards = cardDrawn.map(_ +: p.hand.playerCards).getOrElse(p.hand.playerCards))))) .get.copy(deck = updateDeck) Right(StateResult( updatedState = updatedState, - additionalInformation = Some(CardDrawnInfo(cardDrawn)) - )) + additionalInformation = cardDrawn.map(CardDrawnInfo)) + ) } } diff --git a/server/src/main/scala/it/parttimeteam/match/GameMatchManagerActor.scala b/server/src/main/scala/it/parttimeteam/match/GameMatchActor.scala similarity index 88% rename from server/src/main/scala/it/parttimeteam/match/GameMatchManagerActor.scala rename to server/src/main/scala/it/parttimeteam/match/GameMatchActor.scala index c1b392ac..12fe5df9 100644 --- a/server/src/main/scala/it/parttimeteam/match/GameMatchManagerActor.scala +++ b/server/src/main/scala/it/parttimeteam/match/GameMatchActor.scala @@ -1,11 +1,11 @@ package it.parttimeteam.`match` import akka.actor.{Actor, ActorLogging, PoisonPill, Props, Stash, Terminated} -import it.parttimeteam.`match`.GameMatchManagerActor.{CardDrawnInfo, GamePlayers, StateResult} +import it.parttimeteam.`match`.GameMatchActor.{GamePlayers, StateResult} import it.parttimeteam.common.GamePlayer +import it.parttimeteam.core.GameState import it.parttimeteam.core.cards.Card import it.parttimeteam.core.player.Player.PlayerId -import it.parttimeteam.core.{GameInterface, GameState} import it.parttimeteam.gamestate.{Opponent, PlayerGameState} import it.parttimeteam.messages.GameMessage._ import it.parttimeteam.messages.LobbyMessages.MatchFound @@ -13,8 +13,8 @@ import it.parttimeteam.messages.LobbyMessages.MatchFound import scala.concurrent.duration.DurationInt -object GameMatchManagerActor { - def props(numberOfPlayers: Int, gameApi: GameInterface): Props = Props(new GameMatchManagerActor(numberOfPlayers, gameApi: GameInterface)) +object GameMatchActor { + def props(numberOfPlayers: Int, gameHelper: GameHelper): Props = Props(new GameMatchActor(numberOfPlayers, gameHelper)) case class StateResult(updatedState: GameState, additionalInformation: Option[AdditionalInfo]) @@ -35,10 +35,8 @@ object GameMatchManagerActor { /** * Responsible for a game match * - * @param numberOfPlayers number of players - * @param gameApi */ -class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInterface) +class GameMatchActor(numberOfPlayers: Int, private val gameHelper: GameHelper) extends Actor with ActorLogging with Stash { override def receive: Receive = idle @@ -48,9 +46,6 @@ class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInter private var players: Seq[GamePlayer] = _ private var turnManager: TurnManager[GamePlayer] = _ - // TODO spostare in costruzione a posto di gameApi - private val gameMatchManager = new GameMatchManager(gameApi) - private def idle: Receive = { case GamePlayers(players) => { log.info(s"initial players $players") @@ -150,7 +145,7 @@ class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInter this.turnManager = TurnManager[GamePlayer](players) log.info("initializing game..") - val gameState = this.gameMatchManager.retrieveInitialState(players.map(p => (p.id, p.username))) + val gameState = this.gameHelper.retrieveInitialState(players.map(p => (p.id, p.username))) this.broadcastGameStateToPlayers(gameState) val currentPlayer = this.turnManager.getInTurn currentPlayer.actorRef ! PlayerTurn @@ -158,24 +153,28 @@ class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInter context.become(inTurn(gameState, currentPlayer) orElse terminationAfterGameStarted()) } + /** + * Server behaviour during a player turn + * + * @param gameState current game state + * @param playerInTurn current player in turn + */ private def inTurn(gameState: GameState, playerInTurn: GamePlayer): Receive = { case PlayerActionMade(playerId, action) if playerId == playerInTurn.id => { log.info(s"received action $action from ${playerInTurn.username}") - this.gameMatchManager.determineNextState(gameState, playerInTurn, action) match { + this.gameHelper.determineNextState(gameState, playerInTurn, action) match { case Right(stateResult) => this.handleStateResult(stateResult, playerInTurn) - case Left(errorMessage) => + case Left(error) => log.error("Error resolving player action") - playerInTurn.actorRef ! MatchError.PlayerActionNotValid + playerInTurn.actorRef ! MatchErrorOccurred(error) } - } } private def handleStateResult(stateResult: StateResult, playerInTurn: GamePlayer): Unit = { - // notify the state this.broadcastGameStateToPlayers(stateResult.updatedState) @@ -224,7 +223,6 @@ class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInter */ private def broadcastGameStateToPlayers(gameState: GameState) { - println(gameState.toString) this.players.foreach(player => { player.actorRef ! GameStateUpdated(PlayerGameState( gameState.board, @@ -233,7 +231,6 @@ class GameMatchManagerActor(numberOfPlayers: Int, private val gameApi: GameInter )) }) - //this.broadcastMessageToPlayers(PlayerGameState(Board(), Hand(), Seq.empty)) } diff --git a/server/src/test/scala/it/parttimeteam/match/GameStateMatchActorSpec.scala b/server/src/test/scala/it/parttimeteam/match/GameStateMatchActorSpec.scala index 2e373ea5..18819009 100644 --- a/server/src/test/scala/it/parttimeteam/match/GameStateMatchActorSpec.scala +++ b/server/src/test/scala/it/parttimeteam/match/GameStateMatchActorSpec.scala @@ -3,7 +3,7 @@ package it.parttimeteam.`match` import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestActorRef, TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import it.parttimeteam.`match`.GameMatchManagerActor.GamePlayers +import it.parttimeteam.`match`.GameMatchActor.GamePlayers import it.parttimeteam.core.cards.Card import it.parttimeteam.core.collections.{Board, Deck, Hand} import it.parttimeteam.core.player.Player @@ -30,7 +30,8 @@ class GameStateMatchActorSpec extends TestKit(ActorSystem("test", ConfigFactory. "a game actor" should { "accept players and notify game started with initial state" in { - val gameActor = TestActorRef[GameMatchManagerActor](GameMatchManagerActor.props(NUMBER_OF_PLAYERS, new FakeGameInterface())) + val gameActor = TestActorRef[GameMatchActor](GameMatchActor.props(NUMBER_OF_PLAYERS, new GameHelper(new FakeGameInterface()) + )) val player1 = TestProbe() val player2 = TestProbe() @@ -48,7 +49,7 @@ class GameStateMatchActorSpec extends TestKit(ActorSystem("test", ConfigFactory. player1.expectMsgType[GameStateUpdated] player2.expectMsgType[OpponentInTurn] player1.expectMsg(TurnEnded) - player2.expectMsg(GameStateUpdated(PlayerGameState(Board(List()),Hand(List(),List()),List(Opponent("player1",1)))) + player2.expectMsg(GameStateUpdated(PlayerGameState(Board(List()), Hand(List(), List()), List(Opponent("player1", 1)))) ) } @@ -70,7 +71,7 @@ class GameStateMatchActorSpec extends TestKit(ActorSystem("test", ConfigFactory. Board.empty, players.map(pair => Player(pair._2, pair._1, Hand(List(), List())))) - override def draw(deck: Deck): (Deck, Card) = (deck, FakeGameInterface.cardToDraw) + override def draw(deck: Deck): (Deck, Option[Card]) = (deck, Some(FakeGameInterface.cardToDraw)) override def validateMove(board: Board, hand: Hand): Boolean = ??? diff --git a/server/src/test/scala/it/parttimeteam/match/TurnManagerSpec.scala b/server/src/test/scala/it/parttimeteam/match/TurnManagerSpec.scala index 64bf244a..2e80e078 100644 --- a/server/src/test/scala/it/parttimeteam/match/TurnManagerSpec.scala +++ b/server/src/test/scala/it/parttimeteam/match/TurnManagerSpec.scala @@ -1,6 +1,5 @@ package it.parttimeteam.`match` -import it.parttimeteam.`match`.TurnManager import org.scalatest.flatspec.AnyFlatSpecLike class TurnManagerSpec extends AnyFlatSpecLike { diff --git a/source/doc/design-architetturale.tex b/source/doc/design-architetturale.tex new file mode 100644 index 00000000..00bfd159 --- /dev/null +++ b/source/doc/design-architetturale.tex @@ -0,0 +1,42 @@ +Per gestire l'ambito multiplayer è stata realizzata un’architettura \textit{Client}-\textit{Server}. +Ogni giocatore è rappresentato dal \textit{Client} mente un unico \textit{Server} si occupa di gestire le varie connessioni con i \textit{Client} e lo stato delle partite. +\begin{center} + \includegraphics[scale=0.6]{architettura} +\end{center} +\subsection[Architettura]{Architettura} +\subsubsection{Core} +E’ stato poi realizzato, in maniera indipendente dalle altre componenti, un modulo di gioco che si occupa della sola gestione del gioco stesso, ovvero Machiavelli. +Esso definisce unicamente le entità base e le regole che lo caratterizzano.\newline \newline +Esso può essere considerato come se fosse un libreria esterna che non mantiene alcun stato relativo al gioco in corso, ma espone solamente delle API che possono essere utilizzate da chiunque. \newline \newline +Nel nostro caso è utilizzato sia dal \textit{Server} ma anche dal \textit{Client}: dal primo per gestire lo stato globale della partita ed aggiornarla in risposta alle mosse dei vari giocatori. Dal secondo per gestire internamente la fase del turno, validando le mosse effettuate, in cui l’utente può effettuare una quantità di mosse a piacimento interfacciandosi col core, per poi inviare al \textit{Server} il riepilogo delle azioni effettuate a fine turno. +Questo componente è stato realizzato integrando i linguaggi \textit{Scala-Prolog}. +\subsubsection{Server} +Il \textit{Server} è la componente principale del sistema, si occupa di gestire le connessioni con i \textit{Client} durante la fase di matchmaking, di creare le varie partite di gioco e di mantenere lo stato. +E’ diviso internamente in più componenti ed è costituito da 2 componenti principali: +\begin{itemize} + \item \textit{LobbyManagerActor}: si occupa della gestione della lobby. + È a lui che i Client chiedendo di giocare. + Una volta raggiunte le condizioni necessarie per avviare una partita, genera un \textit{GameMatchManager} che si occuperà da quel momento in poi della gestione del gioco. + \item \textit{GameMatchManagerActor}: si occupa di gestire una partita in corso. + In fase di runtime ne saranno presenti molteplici attivi contemporaneamente, uno per ogni partita. + Mantiene lo stato della partita corrente e gestisce i turni di gioco, ricevendo le azioni da ogni utente e comunicando a tutti gli altri ogni aggiornamento avvenuto. +\end{itemize} +\subsubsection{Client - MVC} +É utilizzato dall’utente per poter cercare una partita secondo le proprie preferenze e per giocare la stessa. +Internamente realizzato seguendo il pattern MVC, è poi ulteriormente diviso in due macro parti che rispecchiano all’incirca quello già visto sul \textit{Server}: +\begin{itemize} + \item Una di gestione della fase iniziale di gioco, ovvero la lobby, utilizzata per poter ricercare la partita desiderata; + \item Una di gestione del gioco stesso, utilizzata nel momento in cui la partita è in esecuzione, con cui l’utente può effettuare mosse durante il proprio turno, comunicarle al server e ricevere gli aggiornamenti conseguenti alle azioni effettuate dagli altri giocatori. +\end{itemize} +\subsubsection{Comunicazione client-server} +Tra il \textit{Client} e il \textit{Server} si è scelto di utilizzare un modello di comunicazione a scambio di messaggi, realizzata grazie all’utilizzo del framework \textit{Akka}. +Questa scelta ha permesso di gestire ad alto livello le comunicazioni tra le componenti senza preoccuparsi dei dettagli implementativi della comunicazione stessa. +\subsubsection[Flussi di interazione]{Flusso di interazione client server} +Il grafico seguente riassume gli aspetti visti in precedenza nei paragrafi relativi alla parte \textit{Client} e \textit{Server}, mostrando come la parte del flusso di gioco del turno del giocatore venga gestito completamente in locale, mentre il \textit{Server} viene contattato solo al termine per validare le mosse effettuate dal giocatore e inviare lo stato aggiornato a tutti gli altri. +\begin{center} + \includegraphics[scale=0.5]{flusso-interazione-client-server} +\end{center} +Questa scelta è stata fatta per garantire ottime prestazioni durante il gioco. +La fase del turno è infatti quella caratterizzata dalla frequenza più elevata di interazioni utente (riposizionamento carte, ordinamento della carte in mano, annullamento delle mosse precedenti ecc..), tutte operazioni non di rilevanza per gli altri giocatori connessi, a cui interessa più che altro lo stato finale del turno dell’avversario. +Il sistema è stato comunque modellato in maniera tale da prevedere modifiche future anche sotto questo aspetto, in maniera da spostare tutto lato server o lato client a seconda delle esigenza (ad esempio in una modalità singolo giocatore vs AI). +\newpage \ No newline at end of file diff --git a/source/doc/design-di-dettaglio.tex b/source/doc/design-di-dettaglio.tex new file mode 100644 index 00000000..c1294c13 --- /dev/null +++ b/source/doc/design-di-dettaglio.tex @@ -0,0 +1,371 @@ +\subsection{Organizzazione del codice} +La suddivisione del codice rispecchia fortemente il design architetturale generale descritto in precedenza. +\begin{center} + \includegraphics[scale=0.5]{moduli} +\end{center} +Sono stati realizzati 4 moduli: +\begin{itemize} + \item Core: comprende tutte le entità necessarie alla realizzazione del gioco e le regole per poter portare avanti una partita, è l’unico tra i moduli totalmente indipendente dagli altri; + \item Common: codice comune alle parti di client e server, per la maggior parte definisce i messaggi utilizzati per lo scambio di informazioni; + \item Client: contenente tutto ciò che concerne il client, interfaccia utente, parte di comunicazione con il server e di aggiornamento dello stato della partita; + \item Server: contenente tutta la parte di gestione delle lobby e delle partite in corso. +\end{itemize} +\subsection{Core} +Il core modella al suo interno tutte le entità del gioco Machiavelli reale, come le carte, il mazzo, il tavolo da gioco e la \textit{GameInterface}, cioè un insieme di funzioni che gli altri moduli del progetto devono usare per poter interagire con le entità. Tali funzioni infatti modellano tutte le azioni che un giocatore può svolgere nel gioco reale. +\subsubsection{Entità} +\begin{center} + \includegraphics[width=\textwidth]{classi-Page-1} +\end{center} +Le entità di gioco sono: +\begin{itemize} + \item la card, corredata da tre case class: un rank (valore nominale), un seme e un colore. + All’interno del Rank si sono definiti tutti i possibili valori che una carta può assumere (da 1 a 13). Inoltre è stato modellato anche il caso del rank asso come 14-esimo valore, chiamato \textit{overflowAce}, per poterne effettuare la validazione qualora si trovasse dopo il Re (13-esimo valore) in una combinazione. + All’interno della case class Suit sono stati definiti i quattro semi disponibili (Cuori, Picche, Quadri e Fiori) mentre nel Color abbiamo modellato il colore rosso e blu. Le cards e le relative case class sono contenute nel package \textit{core.cards}; + \item il Player composto da un username (necessario per essere memorizzato nel server), un id e una mano di gioco. Tale entità è contenuta nel package \textit{core.player}. +\end{itemize} +All’interno del package \textit{core.collections} abbiamo modellato le entità: +\begin{itemize} + \item Hand, cha rappresenta la mano di gioco di un giocatore. Questa entità contiene le funzioni per poter aggiungere (o rimuovere) carte dal tavolo alla mano (o dalla mano al tavolo) e funzioni per ordinare le carte. + \item CardCombination, ottenuta da una sequenza di Cards. Tale entità dispone di un id in modo tale da poterla identificare univocamente sul tavolo, da tutte le altre combinazioni. A livello di gioco, una combinazione è rappresentata da un tris, un poker o una scala ordinata. All’interno di questa case class sono stati definite le funzioni: + \begin{itemize} + \item \textit{isValid}: per poter validare una combinazione; + \item \textit{pickCards}: per prendere una combinazione; + \item \textit{putCards}: per mettere una combinazione nel tavolo. + \end{itemize} + \item Deck, costituito da una sequenza di Cards, rappresenta il mazzo. All’interno sono stati definite le funzioni: + \begin{itemize} + \item \textit{sorted}: per generare il mazzo attraverso prolog; + \item \textit{shuffled}: permette di mischiare il mazzo; + \item \textit{draw}: restituisce la prima carta partendo dalla cima del mazzo. A livello di gioco rappresenta una pescata; + \item \textit{remaining}: ritorna il numero di carte presenti ancora nel mazzo. + \end{itemize} + \item Board, è costituita da una sequenza di CardCombination valide. A livello di gioco rappresenta il tavolo nella quale sono contenute le combinazioni. Le funzioni definite sono: + \begin{itemize} + \item \textit{putCombination}: permette di aggiungere una combinazione valida alla Board; + \item \textit{pickCards}: permette di prendere delle Cards dalla Board; + \item \textit{putCards}: permette di aggiungere una Card alla Board; + \item \textit{cardsInBoard}: verifica se è presente almeno una combinazione nella Board. + \end{itemize} +\end{itemize} +\newpage +\subsubsection{Prolog} +Per la gestione delle regole di validazione, si è deciso di utilizzare questo linguaggio poichè è possibile esprimerle in maniera totalmente dichiarativa ed efficiente. La libreria utilizzata è TuProlog. +\newline +\newline Tale package è composto dalle classi: \textit{PrologGame}, \textit{PrologGameEngine} e \textit{PrologGameConverter}. La figura mostra le dipendenze tra di esse.\newline +\begin{center} + \includegraphics[scale=0.6]{prolog} +\end{center} +Di seguito vengono descritte le classi: +\begin{itemize} + \item \textit{PrologGame}: espone tutte le funzionalità implementate attraverso tale linguaggio. Ogni qualvolta che si deve eseguire una funzionalità in Prolog, è necessario richiamare una funzione di questa classe corrispondente all’azione di Prolog. + In particolare permette di creare le carte corredate da un valore, un seme e un colore per formare il deck di gioco, di eseguire la validazione di una combinazione di carte, che sia essa una scala, un tris o un poker e ne esegue l’ordinamento per seme e per valore. + + \item \textit{PrologGameEngine}: esegue effettivamente le azioni di Prolog. + Si è deciso di realizzare un piccolo DSL che permettesse di facilitare l’utilizzo della libreria TuProlog e di aumentarne l’espressività del codice. Dopo aver caricato la specifica teoria, il \textit{PrologGameEngine} esegue le funzioni in grado di: + \begin{itemize} + \item risolvere un singolo obiettivo o più obiettivi; + \item verificare se un obiettivo ha successo; + \item se vi sono altre soluzioni dopo averne trovata almeno una; + \item estrarre i valori dalle variabili, dopo l’esecuzione di un predicato, tramite la funzione \textit{bindingVars}. + \end{itemize} + + \item \textit{PrologGameConverter}: questa classe espone funzioni in grado di formulare obiettivi nel giusto ‘formato’ in Prolog e di convertire il risultato ottenuto dal \textit{PrologGameEngine} nel tipo corretto, a seconda dell’utilizzo. In particolare, grazie all’utilizzo dell’oggetto \textit{PrologUtils}, espone funzioni in grado di ‘pulire’ (da caratteri non conformi) il risultato del Prolog dopo averlo convertito in stringa. Questo è stato reso necessario poiché, quando si dava in input un obiettivo che conteneva una lista di tuple (ogni carta è una tupla che contiene nell’ordine valore, seme e colore), il risultato risultava essere ‘sporco’ da caratteri estranei rispetto al predicato dato in input. + La classe permette infine di gestire la validazione di specifici casi, ad esempio una combinazione che contiene uno o due assi. Essi, in una combinazione che forma una scala, possono essere posti uno prima del due e l’altro dopo del re, assumendo rispettivamente il valore di 1 o 14 a seconda della posizione inserita in una scala. La funzione \textit{OptionalValueAce} gestisce i casi appena descritti cambiando il valore dell’asso da 1 a 14. +\end{itemize} + +\paragraph{Predicati} +Per ottenere le funzionalità descritte è stato creato il file rules.pl nella cartella delle risorse del modulo \textit{Core}. +\newline \newline +I predicati che permettono di creare le entità sono: +\begin{itemize} + \item \textit{color}: specifica il tipo di colore di una carta; + \item \textit{suit}: specifica il tipo di seme di una carta; + \item \textit{card}: definisce l’entità carta composta da un valore, seme e colore. +\end{itemize} + +I predicati utilizzati per la validazione delle combinazioni sono: +\begin{itemize} + \item \textit{lengthList}: permette di determinare la lunghezza di una lista; + \item \textit{sameNumber}: verifica se all’interno della lista le carte hanno tutti lo stesso valore; + \item \textit{sameSuit}: verifica se all’interno della lista le carte hanno tutti lo stesso seme; + \item \textit{differentSuit}: verifica se all’interno di una lista è presente più volte lo stesso seme. + Tale predicato ne utilizza internamente un altro chiamato \textit{sameElementList} che mantiene memorizzato il seme della prima carta per poter confrontarlo con tutte le altre carte della lista; + \item \textit{endSequence}: verifica la giusta terminazione di una scala che termina con un asso posto dopo il re. + Questo significa che non è possibile continuare la scala aggiungendo un due e via di seguito; + \item \textit{checkOrderByValue}: in caso di una scala verifica se le carte sono in ordine di valore; + \item \textit{validationQuarter}: verifica se una combinazione è un tris o un poker. + La funzionalità di questo predicato è ottenuta grazie alla composizione dei predicati precedentemente descritti, posti nel seguente ordine: \textit{lengthList}, \textit{sameNumber} e \textit{differentSuit}; + \item \textit{validationChain}: verifica se una combinazione è una scala. + La funzionalità di questo predicato è ottenuta grazie alla composizione dei predicati precedentemente descritti, posti nel seguente ordine: \textit{lengthList}, \textit{sameSuit}, \textit{endSequence} e \textit{checkOrderByValue}. +\end{itemize} + +I predicati utilizzati per l’ordinamento delle carte sono: +\begin{itemize} + \item \textit{priority}: definisce la priorità dei semi. + L’ordine utilizzato è: cuori, quadri, fiori e picche; + \item \textit{quickSortSuit}: esegue l’ordinamento per seme. + Internamente utilizza il predicato \textit{compareCard} per ordinare per valore tutte le carte dello stesso seme in base alla priorità; + \item \textit{quickSortValue}: esegue l’ordinamento per valore. +\end{itemize} +Gli algoritmi di ordinamento quicksort per valore (o seme) a sua volta implementano il predicato \textit{partitionValue} (o \textit{partitionSuit}) che specifica la condizione necessario per ordinare. +\newline \newline +Descrivere le funzionalità più complesse come composizione di singoli predicati specifici ha permesso una maggiore leggibilità e manutenibilità del codice, in modo da poterli riutilizzare più volte all’interno della teoria. + +\subsubsection{Game Interface} +\begin{center} + \includegraphics[width=\textwidth]{classi-Page-2} +\end{center} +Consiste in un insieme di funzioni utilizzabili dall’esterno del core per effettuare le normali operazioni che un giocatore reale farebbe durante una partita. +Queste operazioni comprendono: +\begin{itemize} + \item Creare uno stato iniziale di una partita dato il numero e il nome dei players da aggiungerci; + \item Pescare una carta da un mazzo; + \item Validare una combinazione di carte; + \item Validare una Hand e una Board, cioè una mossa effettuata da un giocatore. Validare una Hand significa controllare che non vi siano carte prese dal tavolo e non più riposte; + \item Validare un turno intero, quindi confrontare una Hand e una Board attuale con la Hand e la Board di inizio turno; + \item Prendere delle carte da una CardCombination in una Board; + \item Giocare delle carte da una Hand in una Board in una CardCombination esistente; + \item Giocare una nuova CardCombination da una Hand su una Board. +\end{itemize} +Il risultato dei vari metodi consiste in un oggetto complesso, un Either, che può contenere un valore piuttosto che un altro a seconda del risultato di un’altra funzione. Esso viene usato per poter gestire in modo consistente gli errori che eventualmente possono essere lanciati all’interno delle relative funzioni. Questo permette di evitare di lanciare delle eccezioni che non sono corrette in un approccio funzionale. + +\subsection{Client} +\begin{center} + \includegraphics[width=\textwidth]{architetturaClient} +\end{center} + +\subsubsection{MVC} +Per la parte client dell’applicazione si è scelto di utilizzare una architettura basata sul pattern MVC\@. +Questa ci ha consentito di tenere, anche all’interno del client, un buon livello di separazione tra le singole classi. +\newline +All’avvio dell’applicazione \textit{AppLauncher} avvia \textit{MainController}, che istanzia una istanza di View che fa da contenitore per entrambe le due parti di gioco (quella di avvio e quella di partita). +\textit{MainController} avvierà prima \textit{StartupController} per la parte di iscrizione alle lobby, e una volta ricevuto da quest’ultimo l'evento di creazione della partita, avvia il \textit{GameController}. +Ciascun controller setta nella view il proprio Stage e istanzia la classe Service, che nel MVC gioca il ruolo di model. +\begin{center} + \includegraphics[scale=0.5]{messaggi_Startup_client} +\end{center} +Come si può notare dallo schema, il controller, per notificare eventi alla view, avendo creato il relativo \textit{Stage}, richiama i metodi che questo espone. +\newline +Allo stesso tempo, in creazione passa anche \textit{onViewEvent()}, una funzione che viene invocata dalla view ogni qualvolta debba notificare un evento al controller. +Questa funzione prende in input un oggetto di tipo case class/object che estende la sealed class \textit{ViewEvent}.\newline +Questa scelta di utilizzare da una parte l’interfaccia e dall’altra un metodo a callback è dovuta al fatto che logicamente, la view può notificare al controller qualsiasi tipo di evento, e sarà il controller a decidere quale di questi gestire; al contrario la view espone solamente le funzionalità che espone nella propria interfaccia. +\newline \newline +Simile è anche l’interazione tra \textit{Service} e \textit{Controller}. Il Controller avendo creato il Service ha il suo riferimento ed invoca i metodi che questo espone nella sua interfaccia. Allo stesso tempo passa in creazione il metodo \textit{notifyEvent()} che viene chiamato dal Service quando deve notificare un cambiamento di stato. + +\subsubsection{View} +In entrambi gli stage \textit{Stage} vengono caricate le varie scene, ciascuna delle quali ha un interfaccia di metodi che possono essere chiamati dallo Stage che la contiene. +\newline \newline +Per velocizzare la creazione degli elementi di view più utilizzati, sono state creati degli object che contengono i factory methods per creare i relativi elementi: +\begin{itemize} + \item \textit{ScalavelliButton}, factory per gli oggetti Button; + \item \textit{ScalavelliLabel}, factory per gli oggetti Label; + \item \textit{ScalavelliAlert}, factory per gli oggetti Alert; + \item \textit{ScalavelliTextField}, factory per gli oggetti Button. +\end{itemize} +L’object \textit{CardUtils} è invece una utility che permette di ottenere il percorso dell’immagine raffigurante la carta da gioco a partire dall’entità \textit{Card}. \newline \newline +Si è scelto di gestire la visualizzazione degli alert e degli errori direttamente da \textit{Stage} in quanto questa operazione deve essere indipendente dalla scena in cui ci si trova. +Tutti i metodi che agiscono sugli elementi già renderizzati dalla view e gli aggiornamenti di stato hanno l’esigenza di agire sullo stesso thread di ScalaFX. Per poter ottenere questo risultato sono stati eseguiti all’interno della chiamata a \textit{Platform.runlater()}. \newline \newline +Le informazioni e la configurazione comune sia allo StartupStage che al GameStage sono contenute all’interno della classe \textit{BaseStage}. + +\paragraph{Lobby} +\begin{center} + \includegraphics[scale=0.5]{home} +\end{center} +La parte di view si caratterizza di 4 scene che vengono caricate all’interno di \textit{StartupStage}. La scena principale permette di scegliere tra le 3 modalità di iscrizione a disposizione: +\begin{enumerate} + \item Iscrizione ad una lobby pubblica, in base al numero di players selezionati; + \item Iscrizione ad una lobby privata, inserendo un codice segreto: + \item Creazione di una lobby privata, selezionando il numero di membri necessari per questa lobby. +\end{enumerate} +Alla selezione di una di queste modalità, viene caricata una nuova scena, alla quale viene passato un listener di tipo \textit{StartUpSceneListener} che tramite callback permette di reagire alle azione di \textit{submit()} e della pressione del tasto back. +\newline \newline +Tutte le scene estendono la classe astratta \textit{BaseStartupScene} che contiene la configurazione comune a tutte le scene. Questa classe è a sua volta estesa da \textit{BaseStartupFormScene}, la quale aggiunge i metodi comuni a tutte le scene in cui l’utente si iscrive ad una lobby, come ad esempio quelli per mostrare il messaggio di caricamento, per disabilitare i pulsanti e per ripulire gli input digitati. +\newline \newline +Ciascuna scena che estende \textit{BaseStartupFormScene} si compone di 3 parti: +\begin{itemize} + \item \textit{StartupSceneTopBar}, che contiene il pulsante per tornare alla schermata precedente; + \item \textit{StartupSceneBottomBar}, che contiene il pulsante di invio e la rotella di caricamento mentre si è in attesa della creazione di una partita; + \item Un nodo centrale in cui è possibile l’inserimento dei dati (numero dei giocatori e username) o, nel caso della creazione di un codice segreto, la visualizzazione di quest’ultimo. +\end{itemize} + +\begin{center} + \includegraphics[scale=0.26]{create_join_lobby} +\end{center} +\newpage +\paragraph{Game} +\begin{center} + \includegraphics[scale=0.4]{game} +\end{center} +La view per la parte di Game si compone di una singola scena, GameScene, la quale si aggancia alla View attraverso GameStage.In questa schermata sono visualizzate tutte le informazioni di gioco e viene resa possibile l’interazione del giocatore con la partita. \newline +\begin{center} + \includegraphics[scale=0.5]{viewPanes} +\end{center} +La view si compone di diversi Panes che sono stati implementati in classi distinte. Ciascun Pane ha anche la sua interfaccia che descrive i metodi che espone il pannello. +Ciascun pannello prende in input anche un listener con le callback che vengono istanziate all’interno della GameScene, nella quale vengono creati tutti i Panes. +\newline \newline +Ciascun Pane è stato pensato in modo da racchiudere poche funzionalità simili tra loro, in modo da avere un codice più leggibile e riutilizzabile: +\begin{itemize} + \item \textit{BoardPane}: permette di visualizzare la board del gioco, permettendo all’utente di selezionare le singole carte, le combinazioni e di poter raccogliere le intere combinazioni dal tavolo; + \item \textit{ActionBar}: contiene tutti i pulsanti tramite i quali l’utente può fare una giocata o riordinare la sua mano; + \item \textit{SidePane} invece è il pannello laterale, e contiene a sua volta diversi pannelli: + \begin{itemize} + \item una label di informazione sul turno corrente; + \item \textit{TimerPane}, dove viene mostrato il tempo a disposizione durante il turno del giocatore; + \item \textit{OtherPlayersPane}, in cui vengono visualizzate il numero di carte che gli altri giocatori hanno in mano; + \item \textit{HistoryNavigationPane} tramite il quale è possibile navigare la history e resettare il proprio turno; + \item pulsanti per passare il turno e lasciare la partita. + \end{itemize} +\end{itemize} +Ciascun \textit{Pane}, sul quale è possibile abilitare o disabilitare le azioni quando non è il turno del giocatore, estende il trait \textit{ActionGamePane}. +\newline +\begin{center} + \includegraphics[scale=0.2]{gameViewArchitecture} +\end{center} +All’interno di \textit{GameScene}, attraverso più istanze della classe SelectionManager, si tiene traccia delle carte e delle combinazioni selezionate e deselezionate.\newline SelectionManager è generico in T, con T che estende SelectableItem, un trait che espone i metodi per settare un oggetto come selezionato o deselezionato.\newline Così facendo, SelectionManager avrà la sola responsabilità di tenere traccia degli elementi selezionati, mentre sarà l’elemento stesso ad implementare cosa avviene al momento della selezione, questo secondo il principio di singola responsabilità. \newline \newline +\textit{SelectionManager} prende in costruzione il parametro booleano \textit{allowOnlyOne} che indica se è possibile selezionare solamente un elemento per volta (deselezionando quindi quello precedente) o se al contrario, sia possibile selezionare più elementi. \newline Questa classe espone, oltre al metodo per selezionare o meno un elemento, i metodi per ottenere gli elementi selezionati e per pulire la selezione. \newline \newline +Le azioni che prevedono di avere accesso alle carte selezionate dalla mano o dalla board vengono gestite direttamente all’interno di GameScene, dove si ottengono le carte selezionate e vengono mandate al Controller. \newline \newline +GameStage espone tutti i metodi per mostrare messaggi di informazione, errore, terminazione della partita, aggiornamento del timer ma soprattutto, il metodo \textit{updateState()} che permette di aggiornare in continuazione lo stato della partita, sia durante il proprio turno, sia durante il turno degli altri giocatori. +\newline \newline +Questo metodo prende in input un oggetto di tipo \textit{ClientGameState} che oltre allo stato della board, della mano e degli altri giocatori, contiene anche le info sulla navigabilità della history (se è possibile fare undo/redo/reset) e sul fatto che sia già stata fatta o meno una azione dall’utente, così da poter aggiornare il testo del pulsante per il passaggio del turno.\newline Per togliere questa logica dalla parte di view, sarà comunque responsabilità del controller a controllare se l’utente potrà passare semplicemente o se invece quest’ultimo dovrà pescare una carta. +\subsubsection{Controller} +\paragraph{Lobby} +\textit{StarupController} avvia la view che permette all’utente di selezionare la modalità con cui partecipare ad una lobby e l’inserimento dei suoi dati. Una volta fatto questo, tramite l’interazione con \textit{StartupService} comunica al server la scelta dell’utente. \newline Da questo momento in poi l’utente resta in attesa di ricevere l’avvio del gioco da parte del Service, il quale comunica al Controller l’avvio del match.\newline +Per poter passare alla parte di Game avendo già il riferimento alla partita, a \textit{StartupController} è stato passato in costruzione la funzione \textit{startGame()} che viene invocata passando come parametro un oggetto di tipo \textit{GameMatchInformations} che contiene l’id dell’utente e il riferimento all’attore del match, il quale è stato costruito da \textit{StartupService}. +\paragraph{Game} +\begin{center} + \includegraphics[scale=0.5]{gameController} +\end{center} +\textit{GameController} viene inizializzato direttamente da \textit{MainController}, il quale gli passa in costruzione anche la funzione da invocare per ricominciare una nuova partita.\newline \textit{GameController} istanzia e comunica con \textit{GameService}, invocandone i metodi esposti dall’interfaccia. In creazione invece passa la funzione \textit{notifyEvent()} che mappa in entrata gli eventi di tipo \textit{GameEvent}.\newline +Alla ricezione di questi chiama i metodi esposti dall’interfaccia \textit{GameStage}, aggiornando la schermata mostrata al giocatore. \newline \newline +Anche in questo caso, quando l’utente compie un'azione, viene generato un evento di tipo \textit{ViewGameEvent} e viene chiamata la funzione \textit{onViewEvent()} alla quale viene passato l’evento come parametro. Questo viene gestito da \textit{GameController} che sceglie quali metodi di \textit{GameService} chiamare. +\newline \newline Oltre a fare da collante tra Service e View, a \textit{GameController} è lasciata la responsabilità della gestione del timer durante il turno del giocatore. \newline Questo timer, per scelta a livello di design è stato lasciato solamente lato client, ed è implementato sfruttando le librerie Timer e TimerTask di java.utils. \newline \newline Espone tramite interfaccia i metodi \textit{start()} e \textit{stop()}, con i quali si avviano i due task che vengono eseguiti per ciascun turno. \newline Il primo task, chiamato tickTask, ha un periodo di 1000ms e permette di notificare un evento ogni secondo. Il secondo invece, chiamato endTask ha il compito di far scattare un evento allo scadere del tempo definito all’interno del file Constants.\newline Quando un utente inizia il proprio turno, il timer viene avviato, mentre sia quando scade, sia quando un utente passa prima del termine del turno, viene stoppato, azione con la quale si fa la \textit{cancel()} di entrambi i task e la \textit{purge()} del timer. \newline \newline +Al timer vengono passati in costruzione la durata prevista e un listener che contiene le callback che vengono triggerate dai vari task. Tra queste c’è la \textit{onStart()}, che viene chiamata all’avvio del timer, la \textit{onEnd()}, che viene chiamata allo scadere di endTask, e la onTick() che viene invece chiamata a ogni passaggio di tickTask. \newline \newline Il controller ha anche il compito di decidere se al passaggio di turno, l’utente dovrà pescare o passare semplicemente. Per farlo si affida all’ultimo aggiornamento di stato ricevuto, facendone una copia di volta in volta, e controlla se in questo l’utente doveva pescare oppure no. +\subsubsection{Model} +La parte di model del client è rappresentata dai trait StartupService e GameService con tutte le strutture a loro collegate. Rappresentano il punto in cui risiede tutta la business logic del client. Sono entrambi delle interfacce che espongono tutte le funzionalità rese disponibili dall’applicazione, su cui in controller si appoggiano per eseguire tutte le operazioni risultanti dalle azioni utente effettuate attraverso l’interazione con la view. \newline Permettono quindi di astrarre ai componenti utilizzatori le modalità con cui le varie azioni vengono risolte, ad esempio decidere se risolverle in locale o in remote contattando il server. +È solamente in questa parte del codice client che sono presenti riferimenti al framework akka. A differenza del server, il client non è stato modellato totalmente ad attori: questi sono stati utilizzati solo per la parte di comunicazione con il server tramite scambio di messaggi. +\newpage +\paragraph{StartupService} +\begin{center} + \includegraphics[scale=0.5]{startup-service} +\end{center} +È il componente che si occupa della gestione della lobby. +La sua funzione primaria, essendo la lobby gestita totalmente lato server, è quella di comunicare con quest’ultimo. \newline La connessione al server viene fatta tramite tramite il metodo \textit{actorSelection} messo a disposizione dall’unica istanza dell’oggetto \textit{ActorSystem} presente nell’object \textit{ActorSystemManager}, in caso di connessione avvenuta, si ottiene il riferimento all’attore remoto \textit{serverLobbyRef} responsabile per la gestione delle lobby, che viene utilizzato per l’invio dei successivi messaggi di richiesta di aggiunta alla lobby. \newline +I messaggi di risposta del server vengono invece ricevuti tramite l’attore \textit{StartupActor}, creato all’inizializzazione dell’oggetto sempre tramite l’istanza di \textit{ActorSystem}, il cui riferimento viene passato al server al momento della connessione.\newline Questo attore ha l’unica funzione di ricevere i messaggi inviati dal server e di redirezionarli tramite il trait \textit{StartupServerResponsesListener} che richiede in costruzione.\newline +La decisione di comunicare con il server su due vie separate è stata fatta per evitare duplicazione di messaggi: far fare tutto a StartupActor avrebbe richiesto l’invio di messaggi a quest’ultimo che a sua volta avrebbe dovuto inviarli al server. +I risultati delle varie operazioni vengono poi notificati al controller attraverso la funzione \textit{notifyEvent} richiesta in costruzione, a cui viene passato un oggetto di tipo \textit{GameStartupEvent}.\newline Il risultato della fase di lobby è l’oggetto \textit{GameMatchInformations}, che racchiude tutte le informazioni necessarie a poter avviare la partita lato client, come l'id del giocatore e la reference all'attore remoto del server. +\newpage +\paragraph{GameService} +\begin{center} + \includegraphics[scale=0.5]{game-service} +\end{center} +È il componente che si occupa di gestire lato client tutta la fase di gioco. +Come il componente descritto in precedenza per la fase di lobby, anche questo consiste in un’interfaccia che espone tutti i metodi corrispondenti alle funzionalità rese disponibili dal client di gioco.\newline La sua implementazione \textit{GameServiceImpl} è poi caratterizzata da diversi elementi, ciascuno dei quali gestisce una componente specifica del gioco.\newline Il riferimento all’attore server remoto \textit{serverActorRef} utilizzato per inviare messaggi al server. \textit{ClientGameActorRef}, il riferimento all’attore locale, responsabile di ricevere i messaggi inviati dal server, con una struttura analoga a quella descritta precedentemente per la lobby. +\newline GameStateStore utilizzato per mantenere lo stato locale della partita ed aggiornarlo a seguito delle azioni compiute dall’utente o dagli aggiornamenti ricevuti dal server. History, una struttura dati immutabile utilizzata per salvare lo storico degli stati risultanti dalle mosse eseguite dall’utente durante il turno. In seguito ad ogni mossa effettuata dal giocatore durante il turno, la History viene aggiornata e utilizzata per ripristinare lo stato delle versioni precedenti a seguito delle azioni di undo/redo.\newline \textit{GameInterface}, l’interfaccia core utilizzata per eseguire in locale le azioni di gioco effettuate durante il turno. \newline In costruzione riceve poi una funzione che utilizza per notificare al chiamante (in questo caso il controller) gli aggiornamenti di stato e gli altri eventi di sistema. +\newline +\newline +Di seguito la sequenza di operazioni che avvengono durante il turno di un giocatore quando quest’ultimo esegue una mossa: +\begin{itemize} + \item Un chiamante invoca un metodo del \textit{GameService} corrispondente a un’azione di gioco; + \item \textit{GameService} risolve l’azione per mezzo di \textit{GameInterface}; + \item Sulla base dell’output della funzione invocata su \textit{GameInterface} viene aggiornato lo stato locale di gioco tramite un metodo di \textit{GameStateStore}; + \item Il nuovo stato di gioco viene salvato sulla \textit{History}; + \item Il nuovo stato di gioco viene notificato al componente sottostante per mezzo della funzione notifyEvent ottenuta in costruzione. +\end{itemize} +Nel caso di una mossa eseguita in remoto la chiamata al \textit{GameInterface} è sostituita con un messaggio inviato al \textit{Server}, mentre manca l’aggiornamento della \textit{History} che viene utilizzata solamente durante la gestione locale del turno. +\subsection{Server} +È la componente che permette lo svolgersi del gioco in modalità multiplayer.Ogni sessione utente è possibile distinguerla in 2 fasi principali: +\begin{itemize} + \item una fase di startup o lobby: in questa fase l’utente, una volta avviata l’applicazione, cerca di connettersi al server inviandogli i dati necessari per poter trovare una partita. + Il server contemporaneamente rimane in attesa di connessioni dei client. + Una volta connessi e ottenute le loro informazioni di gioco li inserisce all’interno della lobby, in attesa che si verifichino le condizioni necessarie affinché una partita possa essere generata. + Al raggiungimento di tali condizioni, viene creata una partita, inserendo i giocatori che vengono rimossi dalla lobby. + \item una fase di gioco: questa fase gestisce lo svolgersi della partita ed è ulteriormente suddivisa in 2 sotto fasi: + \begin{itemize} + \item Fase di inizializzazione, in cui i client ricevono un messaggio del fatto che la partita è stata trovata e ne danno conferma al server. + Il server aspetta i messaggi da parte di tutti i client e una volta ricevuti, viene generato lo stato iniziale della partita, comunicato ai giocatori.Ora il gioco può iniziare. + \item Fase di gioco, in cui il server si occupa di far proseguire il gioco, determinando il giocatore corrente, ricevendo le sue mosse, aggiornando lo stato in maniera ciclica, fino al verificarsi delle condizioni di terminazione del gioco. + Il client si occupa di intercettare tutti le azioni effettuate dall’utente durante la partita, di comunicarle al server al termine del turno e di ricevere le varie informazioni sullo stato di avanzamento della partita. + \end{itemize} +\end{itemize} + +\subsubsection{Lobby} +\begin{center} + \includegraphics[width=\textwidth]{server-classi-lobby} +\end{center} +LobbyManagerActor è l’attore che si occupa di tutta la parte di gestione della fase di startup del gioco, è stato implementato estendendo il trait Actor di akka, rendendo quindi possibile la ricezioni di messaggi provenienti dai giocatori client. +Parte della logica di questo componente è stata poi portata fuori in altre strutture dati cercando di seguire il principio di Separation of Concerns. +Lobby è la struttura base che rappresenta una lista di giocatori accomunati dalle stesse preferenze riguardanti il numero di giocatori necessari per poter iniziare una partita. +LobbyManager è invece il componente che si occupa di mantenere i riferimenti a tutte le lobby create fino a quel momento, espone i metodi per inserire, rimuovere o estrarre i giocatori da una specifica lobby. +Per fare ciò mantiene una Lobby in corrispondenza di ogni LobbyType, che rappresenta l’informazione sulle caratteristiche di una lobby. + +\paragraph{Modellazione delle lobby private} +La lobby privata presenta la caratteristica di una normale lobby di avere associato un valore corrispondente al numero di giocatori tale da poter essere estratti per formare un partita.Oltre a questo, ha un codice univoco per poterla identificare univocamente tra le tante. +PrivateLobbyService supporta la creazione di lobby private, generando un id univoco ogni volta che ne viene richiesta una nuova. +\newpage +\paragraph{Interazione con il client} +\begin{center} + \includegraphics[width=\textwidth]{server-diagramma-attivita-lobby} +\end{center} +La sequenza di operazioni necessarie per poter entrare in una lobby e partecipare ad una partita è la seguente: +\begin{itemize} + \item un client invia un messaggio Connect al server passandogli il riferimento all’attore client a cui quest’ultimo dovrà rispondere, a seguito del quale viene generato un id univoco che viene restituito al client; + \item il client dopo aver scelto le preferenze di gioco, richiede al server di essere aggiunto ad una lobby (privata o pubblica) o di crearne una sua privata. + Il server aggiunge il client alla lobby corrispondente rispondendo con un messaggio di avvenuta aggiunta. + \item dopo aver aggiunto un giocatore ad una lobby, il server controlla se si sono verificate le condizioni per l’avvio di una nuova partita sulla lobby corrente tramite il metodo di LobbyManager attemptExtractPlayerForMatch che tenta di estrarre la lista di giocatori. + In caso positivo i giocatori vengono rimossi dal sistema di lobby e viene creato un attore specifico per la partita GameMatchActor a cui vengono passati i loro riferimenti.Da quel momento in poi si occuperà di tutta la fase di gioco. +\end{itemize} + +\subsubsection{Game} +\begin{center} + \includegraphics[width=\textwidth]{server-game-classes} +\end{center} +\textit{GameMatchActor} è l’attore server che si occupa della gestione di una partita, creato dall’attore Lobby descritto in precedenza. +Fa uso di due interfacce principali: +\begin{itemize} + \item \textit{TurnManager} è la struttura che gestisce l'ordine del turno dei giocatori.Possiede la lista dei giocatori e la logica per determinare quello successivo a partire da quello corrente. + \item \textit{GameHelper}, la cui implementazione è l’unica classe server che ha riferimento al core, espone i metodi necessari a creare lo stato iniziale della partita e a determinare lo stato successivo della partita sulla base dell’azione compiuta dal giocatore a fine turno. +\end{itemize} +Mantiene inoltre lo stato globale della partita, ad ogni suo aggiornamento lo trasmette a tutti giocatori connessi. +Per questioni di sicurezza, ad ogni giocatore non viene inviato lo stato globale ma uno stato parziale ricavato da esso con il metodo broadcastGameStateToPlayers, contenente le sole informazioni di interesse al giocatore (ad esempio l’informazione sul deck rimane solo al server, come anche la composizione delle mani degli avversari, che il giocatore corrente ovviamente non deve conoscere). + +\paragraph{Comunicazione nella fase di inizializzazione della partita} +\begin{center} + \includegraphics[width=\textwidth]{server-sequence-diagram-inizializzazione-gioco} +\end{center} +Una volta creato dall’attore lobby, l’attore responsabile della gestione della partita: +\begin{itemize} + \item rimane in attesa di un messaggio di inizializzazione (GamePlayers) da parte di quest’ultimo, contenente le informazioni dei giocatori; + \item notifica i giocatori che la partita è stata trovata tramite il messaggio MatchFound; + \item rimane in attesa del loro messaggio di conferma Ready, contenente oltre all’id del giocatore il riferimento all’attore a cui inviare i successivi messaggi; + \item ricevuti tutti i messaggi di conferma la partita viene inizializzata, viene generato lo stato iniziale ed inviato ai vari giocatori.Inoltre viene inviata una notifica al giocatore del turno corrente. +\end{itemize} +\newpage +\paragraph{Comunicazione durante il turno di gioco} +\begin{center} + \includegraphics[width=\textwidth]{server-sequence-diagram-turno} +\end{center} +Una volta inizializzato il gioco e stabilito il giocatore corrente, il server rimane in attesa dell’azione di fino turno (le restanti azioni effettuate durante il turno sono gestite in locale come descritto in precedenza). +Dopodiché determina lo stato successivo sulla base dell’azione effettuata che può essere: +\begin{itemize} + \item DrawCard: utente non effettua alcuna azione e decide di passare pescando una carta; + \item PlayerMove(hand, board) : l’utente conclude il turno effettuando una o più mosse, con conseguente modifica della sua mano e del tavolo. +\end{itemize} +Determinato lo stato successivo, il server comunica la fine del turno facendo broadcast dello stato corrente a tutti i giocatori.Comunica la fine del turno al giocatore corrente e manda avanti la partita determinando il successivo. +\newline +Tutte queste operazioni verranno ripetute ciclicamente fino al verificarsi delle condizioni di vittoria, che comporta la notifica ai vari giocatori del termine della partita e la terminazione dell’attore server per la gestione della stessa. + +\subsubsection{Fault Tolerance} +In entrambe le componenti del server (lobby e gioco) si è cercato di rilevare e gestire al meglio situazioni di errore come la disconnessione improvvisa dei client. +Per rilevare queste situazioni è stato utilizzata la funzionalità di supervisione e monitoraggio messa a disposizione da akka.\newline +Conoscendo il riferimento ad un attore è possibile ricevere gli eventi di terminazione, ricevendoli tramite il messaggio Terminated.\newline +L’attore lobby sfrutta questa funzionalità per rimuovere gli utenti terminati dalle code. +L’attore responsabile delle partita invece rileva la terminazione di uno dei giocatori, termina la partita stessa notificandola agli altri. + +\subsection[Errori]{Gestione degli errori} +In tutte le componenti dell’applicazione si è cercato di evitare il più possibile la generazione di eccezioni qualora le funzioni avessero dovuto generare errori a causa dell’impossibilità di eseguire l’operazione richiesta. +Sono stati utilizzati dei meccanismi comuni sfruttando le funzionalità e le classi messe a disposizione dal linguaggio akka. +Option per evitare di avere valori null qualora la funzione non dovesse ritornare nulla. +Either per poter tornare un errore specifico nel caso in cui la funzione andasse in errore. +A questo scopo abbiamo creato delle classi di errore specifiche nei vari moduli. +Ad esempio nel metodo playCombination di GameInterface, in caso ci sia un errore nei parametri di input forniti, al posto di lanciare un’eccezione o di tornare un errore generico come Throwable, viene ritornato un oggetto della classe GameError, come CombinationNotValid. +\newpage \ No newline at end of file diff --git a/source/doc/images/SprintTrello.png b/source/doc/images/SprintTrello.png new file mode 100644 index 00000000..bf03f921 Binary files /dev/null and b/source/doc/images/SprintTrello.png differ diff --git a/source/doc/images/architettura.png b/source/doc/images/architettura.png new file mode 100644 index 00000000..78a14969 Binary files /dev/null and b/source/doc/images/architettura.png differ diff --git a/source/doc/images/architetturaClient.png b/source/doc/images/architetturaClient.png new file mode 100644 index 00000000..8fcd80d0 Binary files /dev/null and b/source/doc/images/architetturaClient.png differ diff --git a/source/doc/images/casiDUso_Game.png b/source/doc/images/casiDUso_Game.png new file mode 100644 index 00000000..527b9462 Binary files /dev/null and b/source/doc/images/casiDUso_Game.png differ diff --git a/source/doc/images/casiDUso_Lobby.png b/source/doc/images/casiDUso_Lobby.png new file mode 100644 index 00000000..853f0f0d Binary files /dev/null and b/source/doc/images/casiDUso_Lobby.png differ diff --git a/source/doc/images/classi-Page-1.png b/source/doc/images/classi-Page-1.png new file mode 100644 index 00000000..0bab273f Binary files /dev/null and b/source/doc/images/classi-Page-1.png differ diff --git a/source/doc/images/classi-Page-2.png b/source/doc/images/classi-Page-2.png new file mode 100644 index 00000000..49dc607e Binary files /dev/null and b/source/doc/images/classi-Page-2.png differ diff --git a/source/doc/images/create_join_lobby.png b/source/doc/images/create_join_lobby.png new file mode 100644 index 00000000..ca5a620c Binary files /dev/null and b/source/doc/images/create_join_lobby.png differ diff --git a/source/doc/images/etichetteTrello.png b/source/doc/images/etichetteTrello.png new file mode 100644 index 00000000..06012714 Binary files /dev/null and b/source/doc/images/etichetteTrello.png differ diff --git a/source/doc/images/flusso-interazione-client-server.png b/source/doc/images/flusso-interazione-client-server.png new file mode 100644 index 00000000..fd2c80fd Binary files /dev/null and b/source/doc/images/flusso-interazione-client-server.png differ diff --git a/source/doc/images/game-service.png b/source/doc/images/game-service.png new file mode 100644 index 00000000..d11fcd3d Binary files /dev/null and b/source/doc/images/game-service.png differ diff --git a/source/doc/images/game.png b/source/doc/images/game.png new file mode 100644 index 00000000..daaf3f58 Binary files /dev/null and b/source/doc/images/game.png differ diff --git a/source/doc/images/gameController.png b/source/doc/images/gameController.png new file mode 100644 index 00000000..2dededf5 Binary files /dev/null and b/source/doc/images/gameController.png differ diff --git a/source/doc/images/gameViewArchitecture.png b/source/doc/images/gameViewArchitecture.png new file mode 100644 index 00000000..4855ec01 Binary files /dev/null and b/source/doc/images/gameViewArchitecture.png differ diff --git a/source/doc/images/git-workflow-1-1.png b/source/doc/images/git-workflow-1-1.png new file mode 100644 index 00000000..a11f8623 Binary files /dev/null and b/source/doc/images/git-workflow-1-1.png differ diff --git a/source/doc/images/home.png b/source/doc/images/home.png new file mode 100644 index 00000000..5fc22f4f Binary files /dev/null and b/source/doc/images/home.png differ diff --git a/source/doc/images/messaggi_Startup_client.png b/source/doc/images/messaggi_Startup_client.png new file mode 100644 index 00000000..410d2ddc Binary files /dev/null and b/source/doc/images/messaggi_Startup_client.png differ diff --git a/source/doc/images/moduli.png b/source/doc/images/moduli.png new file mode 100644 index 00000000..093c3947 Binary files /dev/null and b/source/doc/images/moduli.png differ diff --git a/source/doc/images/prolog.png b/source/doc/images/prolog.png new file mode 100644 index 00000000..14116019 Binary files /dev/null and b/source/doc/images/prolog.png differ diff --git a/source/doc/images/selectScene.png b/source/doc/images/selectScene.png new file mode 100644 index 00000000..ffd62cb7 Binary files /dev/null and b/source/doc/images/selectScene.png differ diff --git a/source/doc/images/server-classi-lobby.png b/source/doc/images/server-classi-lobby.png new file mode 100644 index 00000000..1d5116ec Binary files /dev/null and b/source/doc/images/server-classi-lobby.png differ diff --git a/source/doc/images/server-diagramma-attivita-lobby.png b/source/doc/images/server-diagramma-attivita-lobby.png new file mode 100644 index 00000000..219a24f5 Binary files /dev/null and b/source/doc/images/server-diagramma-attivita-lobby.png differ diff --git a/source/doc/images/server-game-classes.png b/source/doc/images/server-game-classes.png new file mode 100644 index 00000000..0fc01cef Binary files /dev/null and b/source/doc/images/server-game-classes.png differ diff --git a/source/doc/images/server-recap.png b/source/doc/images/server-recap.png new file mode 100644 index 00000000..fd2c80fd Binary files /dev/null and b/source/doc/images/server-recap.png differ diff --git a/source/doc/images/server-sequence-diagram-inizializzazione-gioco.png b/source/doc/images/server-sequence-diagram-inizializzazione-gioco.png new file mode 100644 index 00000000..04bc5037 Binary files /dev/null and b/source/doc/images/server-sequence-diagram-inizializzazione-gioco.png differ diff --git a/source/doc/images/server-sequence-diagram-turno.png b/source/doc/images/server-sequence-diagram-turno.png new file mode 100644 index 00000000..755198ac Binary files /dev/null and b/source/doc/images/server-sequence-diagram-turno.png differ diff --git a/source/doc/images/startup-service.png b/source/doc/images/startup-service.png new file mode 100644 index 00000000..fb0c79d2 Binary files /dev/null and b/source/doc/images/startup-service.png differ diff --git a/source/doc/images/viewPanes.png b/source/doc/images/viewPanes.png new file mode 100644 index 00000000..6f630ad8 Binary files /dev/null and b/source/doc/images/viewPanes.png differ diff --git a/source/doc/implementazione.tex b/source/doc/implementazione.tex new file mode 100644 index 00000000..8b620ede --- /dev/null +++ b/source/doc/implementazione.tex @@ -0,0 +1,55 @@ +\subsection{Matteo} +Per il progetto ho deciso di assumere il ruolo di Product Owner, mi sono occupato della coordinare il lavoro dei miei colleghi e di curare quanto possibile la bacheca trello, organizzando con l’aiuto degli altri le attività da svolgere in ogni sprint e cercando di tenere sempre sotto controllo lo stato dei lavori al fine di rispettare le scadenze programmate in principio. +A livello implementativo mi sono invece occupato della realizzazione della parte server in tutte le sue componenti. +Ho deciso di utilizzare il modello ad attori e un modello di comunicazione interamente basato su scambio di messaggi utilizzando il framework Akka in quanto l’ho ritenuto il modello ideale per modellare una situazione come quella di un gioco multiplayer, in cui ogni utente è caratterizzato da una fase “attiva” di gioco in cui esegue delle azioni, ma anche una “passiva” (nel caso specifico di Machiavelli quella del turno dell’avversario) in cui rimane silente ricevendo solo gli aggiornamenti del server riguardanti le mosse degli altri giocatori. +Ho lavorato anche sui file di configurazione di server e client per poter eseguire e testare il gioco in modalità multiplayer su una rete privata, e quindi non per forza su un unico computer. +Questi file di configurazione sono stati anche utile per poter personalizzare e visualizzare i log di akka, utili in certe fase dello sviluppo per poter identificare alcuni problemi durante il gioco reale, non venuti a galla in precedenza con i vari unit test realizzati. +L’altra componente di cui mi sono occupato è quella lato client della parte di Service sia dello lobby che di gioco, e strutture annesse: +\begin{itemize} + \item Attori di lobby e di game per la comunicazione con il server. + \item History, per la gestione dello storico delle mosse effettuate durante il turno del giocatore e poterci navigare avanti e indietro. + \item GameStateStore, per la memorizzazione e l’aggiornamento dello stato locale della partita. +\end{itemize} +Ho poi contribuito alla realizzazione e modifica di alcuni elementi del core, aiutando anche a risolvere problemi sorti durante lo sviluppo. +In generale ho cercato sempre di curare la parte architetturale, facendo attenzione che le dipendenze tra classi e moduli dell’app venissero sempre rispettate. +Ho cercato anche di mantenermi il più possibile aderente allo stile di programmazione funzionale, preferendo sempre l’utilizzo di funzioni di libreria immutabili, promuovendo l’utilizzo di espressioni lambda e l’utilizzo di monadi come Either e Option. +Anche nelle classi realizzate ho cercato di seguire quando possibile il modello funzionale, prediligendo l’immutabilità, tranne in quei casi in cui a fronte di un’attenta analisi, ho optato per la creazione di strutture non totalmente funzionali con uso di side effect, come nei casi di classi caratterizzate dal fatto di mantenere uno stato locale aggiornato secondo delle funzioni esposte all’esterno, come il caso di LobbyManager lato server o GameStateStore sul client. + +\subsection{Luca} +Per questo progetto ho assunto il ruolo di Committente, ruolo per il quale ho in primis cercato le regole e analizzato assieme a tutti gli altri elementi del gruppo le funzionalità che la nostra applicazione avrebbe dovuto supportare. \newline \newline Il lavorare sulla parte client, in particolar modo sulla view, oltre al fatto di essere il committente del progetto, mi ha permesso di tenere sempre sotto osservazione l’effettivo rispetto dei requisiti che ci eravamo prefissati, magari richiedendo anche agli altri membri del gruppo il supporto di una funzionalità che era sfuggita durante l’implementazione degli altri moduli. +\subsubsection{Sviluppo Client - View e Controller} +Durante lo sviluppo, invece, ho lavorato sulla parte client dell’applicazione, e dopo aver deciso di adottare il pattern MVC, ho lavorato specialmente sulle parti di View e Controller.\newline \newline In un primo momento ho valutato attentamente l’utilizzo della libreria ScalaFX, per vedere se avesse potuto portare delle difficoltà durante lo sviluppo per via di possibili funzionalità mancanti, le quali però non sono state evidenziate. \newline \newline Nello sviluppo ho sempre cercato di separare logicamente le funzionalità, i componenti della view, le parti di gioco in modo da permettere di avere una struttura modulare e ottenere delle classi più leggibili. A tal proposito ho spesso utilizzato i companion object per contenere la parte di implementazione ed utilizzare trait e classi astratte per esporre le funzionalità messe a disposizione dai vari elementi. Trait e classi astratte sono state utili anche per descrivere funzionalità e scrivere pezzi di codice comuni a più elementi di view. \newline \newline Anche per la creazione di pulsanti, labels, alerts e textFields ho deciso di utilizzare degli oggetti che contengono diversi metodi apply() a seconda di come si vuole creare quell’ elemento, e durante la scrittura del codice, la cosa si è rivelata molto utile.\newline \newline Sia per la creazione di TurnTimer che di SelectionManager, ho cercato di rendere le classi il più riutilizzabili possibile, nel primo caso avvalendomi delle callback per reagire agli eventi generati dai TimerTask, nel secondo creando una classe generica, che permetta di selezionare oggetti che implementano il trait SelectableItem, rendendolo utilizzabile anche in altri contesti. +\newline \newline Infine, assieme a Lorenzo e Matteo abbiamo lavorato ad un refactoring della gestione degli errori che, avendo suddiviso il progetto in più moduli distinti, potevano provenire da diverse parti. I testi degli errori, di informazione, così come tutte le label, sono stati gestiti solamente lato view, così da poterli in futuro modificare, magari aggiungendo il supporto multilingua. +\subsubsection{Interazioni nel MVC} +Sempre assieme a Matteo, il quale ha lavorato sulla parte di Model che interagisce con il Controller, ho valutato attentamente in quali casi fosse opportuno esporre una interfaccia di metodi e in quali passare una funzione con un evento come parametro. La scelta è ricaduta sul primo approccio per la comunicazione Controller-View e Controller-Model, mentre invece si è scelto il secondo per quella View-Controller e Model-Controller, in quanto View e Model generano comunque eventi e deve essere il controller a decidere quali gestire ed in che modo. Così facendo, è stato possibile aggiungere funzionalità alla View e al Model semplicemente dichiarando il nuovo tipo di evento, che sarebbe poi stato gestito alla necessità.\newline \newline Durante lo sviluppo ho poi fatto lavorato su GameService e History per riuscire ad ottenere le informazioni sulla possibilità di navigare la History da parte dell’utente. A tal proposito, assieme a Matteo si è deciso di creare l’oggetto ClientGameState che racchiudesse tutte le informazioni necessarie all’aggiornamento dello stato. +\subsection{Lorenzo} +Durante lo sviluppo del progetto ho cercato di mantenermi il più possibile consono allo stile della programmazione funzionale, cercando di mantenere uno stato immutabile nei punti in cui fosse necessario. \newline \newline Mi sono occupato principalmente della parte core del progetto: lavorando sulle entità che lo compongono e su tutta la logica correlata alla gestione delle regole necessarie per poter implementare il gioco. Quest’ultima parte è stata realizzata integrando Prolog con Scala. +\subsubsection{Entità - Core} +Assieme a Daniele ho definito e implementato le entità base per l’interazione tra esse e altri componenti del progetto.\newline Negli Sprint 3 e 4, ho revisionato questa parte per ottimizzare il codice. +\subsubsection{Prolog - Core} +La creazione delle entità e gestione delle regole è stata realizzata tramite il linguaggio Prolog. +In particolare ho creato il file rules.pl in cui ho definito tutte le regole di validazione che il gioco ammette (tris, poker e scale), ho realizzato l’ordinamento delle carte nella mano di un giocatore per seme e per valore e ho creato le entità di base del gioco. +Ho implementato tutta la parte in scala di gestione al prolog utilizzando la libreria TuProlog. +In quest'ultimo caso è stato necessario definire la classe \textit{PrologGameEngine} in grado di risolvere i vari goals dati in input ed ottenere i rispettivi risultati. +Ho definito la classe \textit{PrologGameConverter} che converte i risultati ottenuti da prolog in un formato conforme alle entità del progetto. +Quest’ultima classe non è stata banale implementarla poiché ho riscontrato alcuni errori della libreria TuProlog. +Questo accadeva quando il predicato specificato per risolvere un goal conteneva una lista di tuple. +Per questo motivo ho creato un oggetto ad-hoc, \textit{PrologUtils}, in grado di poter definire delle funzioni che permettevano di ‘pulire’ il risultato ottenuto con specifici caratteri per poterlo convertire nel formato richiesto (ad esempio una carta). +Infine ho aiutato gli altri membri nella realizzazione di alcuni task. +\subsection{Daniele} +\subsubsection{Continuous Integration} +Mi sono occupato di configurare opportunamente l'ambiente di CI scelto in modo da poter verificare la correttezza di ogni singola build, compilando ad ogni push su ogni branch e pull request. +Inoltre, documentandomi online, ho trovato molto utile anche il sito \textit{Codecov.io}, che si occupa di mantenere delle statistiche sulla copertura dei test sul progetto. +Per il rilascio della Relazione e Scaladoc ad ogni rilascio sul branch di dev ho scelto \textit{Github Pages} in modo da renderlo disponibile per tutti. +Per il rilascio dei pacchetti eseguibili degli applicativi server e client ad ogni push etichettata sul master ho scelto sempre \textit{Github Releases}. +Questa parte ha richiesto molto lavoro ancora prima di iniziare a sviluppare, ma successivamente tutto il team ne ha tratto beneficio. +In corso d'opera, si e trattato solamente di adattare mano a mano la configurazione già esistente alle crescenti esigenze del progetto (sopratutto in quanto a riduzione dei tempi di compilazione). +\subsubsection{Build automation} +Mi sono occupato inoltre degli script necessari per eseguire agilmente una compilazione di tutto il progetto o di parte di esso in base alle esigenze. +Tutto il progetto è stato incapsulato in moduli logicamente separati, seppur talvolta con dipendenze gli uni dagli altri. +Questo ha permesso tante volte di ridurre tempi morti, compilando o testando solamente parti del progetto. +\subsubsection{Core} +Assieme a Lorenzo mi sono occupato dello sviluppo delle entità base del progetto e di tutti quegli elementi di gioco che riguardavano le regole del gioco e le interazioni tra di esse. +Ho speso tanto tempo nel ottimizzare il codice e di slegarmi dallo schema mentale della programmazione ad oggetti che ho utilizzato fino a prima dell'inizio del progetto. +Ancora adesso penso di non essere riuscito appieno nell'impresa, dato che comunque anche al lavoro sono costretto ad usare comunque l'altro approccio. +\newpage \ No newline at end of file diff --git a/source/doc/processo-di-sviluppo.tex b/source/doc/processo-di-sviluppo.tex new file mode 100644 index 00000000..e60992d4 --- /dev/null +++ b/source/doc/processo-di-sviluppo.tex @@ -0,0 +1,57 @@ +\subsection{Metodologia} +Sin da subito abbiamo deciso di adottare una metodologia di sviluppo \textit{Agile-Scrum}, seppur non scegliendo un vero e proprio Scrum Master. +Chi più e chi meno, a seconda dei vari sprint, ognuno ha avuto l'occasione e il modo di ricoprire tale ruolo. +In questo modo tutti hanno potuto dare il loro contributo per la riuscita del progetto, coordinando il team con l'aiuto degli strumenti a disposizione. +In accordo con tale metodologia, il lavoro è stato suddiviso in \textit{Sprint}, della durata media di una settimana e mezzo. +Allo scadere del tempo si sarebbe dovuti arrivare a sviluppare un numero minimo di funzionalità dell'applicativo. +I meeting sono stati frequenti all'inizio e ne sono stati svolti alcuni saltuariamente all'interno di ogni sprint. +Questo perché, alternandosi con un periodo di lavoro autonomo, abbiamo ritenuto necessario e producente confrontarsi anche durante gli sprint su scelte sintattiche e pattern di sviluppo tra tutti i membri del team, in modo da condividere conoscenze ed entusiasmo. +\subsection{Strumenti adottati} +\subsubsection{VCS} +Si è deciso di utilizzare \textit{Git} per effettuare il versioning del codice durante lo sviluppo attraverso la piattaforma \textit{GitHub}. +\begin{center} + \includegraphics[scale=0.4]{git-workflow-1-1} +\end{center} +L’utilizzo che ne abbiamo fatto è descritto nella figura: il branch di default è sempre il \textit{master}, al quale \textit{dev} è sempre allineato. +Nel caso in cui debbano essere prodotti degli hotfix, essi vengono svolti su un branch che parte dal \textit{master} e vi ritorna subito, senza passare da \textit{dev}. +Ogni volta che si implementava una nuova funzionalità o si risolveva una fix, veniva creato una branch a parte. +Successivamente, a lavoro ultimato, veniva creata una pull request per mergiare sul branch \textit{dev}. +Ogni pull request su \textit{dev} deve essere approvata almeno da un altro membro del gruppo. +Per mergiare invece un insieme di funzionalità sviluppate e testate dal branch \textit{dev} al \textit{master}, deve sempre essere aperta una pull request, che deve essere approvata da tutti i membri del gruppo. +Questo per essere certi che tutti i membri del gruppo siano consapevoli del lavoro svolto da tutti gli altri. + +\subsubsection{Build Automation} +Si è deciso di usare \textit{SBT}, dato che durante il corso lo si é sempre preferito rispetto al 'cugino' \textit{Gradle} per lo svolgimento degli elaborati, nonostante il fatto che alcuni componenti del gruppo lo utilizzino in ambito aziendale. + +\subsubsection{Continuous Integration} +Si è deciso di usare \textit{Travis CI}. +Consisteva nell'unica soluzione freeware che i componenti del gruppo abbiano mai usato, introdotta proprio in questo corso. +Tuttavia, è stato necessario approfondire l'argomento tramite studio autonomo, dato che ciò che si era appreso a lezione é stato ritenuto insufficiente per la buona riuscita del progetto per come è stato pensato. +Ad esso é stata adattata l'esecuzione dei test necessari per verificare la correttezza del lavoro svolto e poter poi effettuare dei rilasci senza regressioni. +Tramite lo stesso servizio é stata effettuata la \textit{Continuous Delivery}, rilasciando dei pacchetti compilati su \textit{Github Releases}. + +\subsubsection{Bacheca} +Per avere una visione sull’andamento generale del progetto e sulle specifiche feature da svolgere o completate, abbiamo utilizzato \textit{Trello}. +\newline +Si sono realizzate delle etichette personalizzate (vedi figura sotto) da associare alle feature da implementare per una migliore organizzazione per ogni membro del team. Inoltre si sono create delle colonne (vedi figura sotto) in cui raggruppare le feature a seconda del suo stato di sviluppo. +\begin{center} + \includegraphics[width=5cm]{etichetteTrello} +\end{center} +\begin{center} + \includegraphics[width=5cm]{SprintTrello} +\end{center} +Alcune di queste sono: +\begin{itemize} + \item \textit{To Do}: contiene le card associate alle features da sviluppare; + \item \textit{Done}: contiene le card associate alle features sviluppate; + \item \textit{Bug}: contiene le card associate alle features che presentano bug da ‘fixare’; + \item \textit{Sprint}: contiene le card che riassumono le features da svolgere per ogni sprint. +\end{itemize} +La bacheca è possibile consultarla direttamente cliccando il link: \href{https://trello.com/b/Nk4j3Kuf/pps}{Trello}. +\subsubsection{Test} +Durante il progetto si è data importanza anche allo sviluppo dei test. +In particolare ogni membro del team, ha testato tutte le features da lui sviluppate creando test ad-hoc utilizzando l'approccio più consono. +Ogni sviluppatore prima di poter eseguire una pull request della propria features, doveva controllare che i test eseguiti passassero non solo in locale ma anche sul server di CI. +Lo sviluppo dei test è stato di aiuto soprattutto negli Sprint 3 e 4 poiché ci hanno permesso di individuare e di risolvere i bug più velocemente. + +\newpage \ No newline at end of file diff --git a/source/doc/rel.tex b/source/doc/rel.tex index 1dbf1a43..11c82328 100644 --- a/source/doc/rel.tex +++ b/source/doc/rel.tex @@ -2,137 +2,70 @@ \usepackage[utf8]{inputenc} \usepackage{graphicx} +\usepackage{sidecap} +\usepackage[T1]{fontenc} +\usepackage{hyperref} +\hypersetup{ +colorlinks=true, +linkcolor=blue, +filecolor=magenta, +urlcolor=cyan, +} -% TODO: Resolve unsupported character. -\title{Scalavelli project} -\date{25 agosto 2020} -\author{Daniele Tentoni "Part Time Team"} - -\begin{document} - \pagenumbering{gobble} % no numbers. - \maketitle - \newpage - \pagenumbering{arabic} % start counting numbers. - - \begin{abstract} - Progetto scritto in Scala per giocare al celebre gioco Machivelli sul proprio computer e sfidare altri giocatori. - \end{abstract} - - \tableofcontents - - \newpage - - - \section{Processo di sviluppo} - - \subsection{Metodologia} - - Sin da subito abbiamo deciso di adottare una metodologia di sviluppo \textit{Agile-Scrum}, seppur non scegliendo un - vero e proprio Scrum Master. Chi più e chi meno, a seconda dei vari sprint, praticamente tutti hanno avuto - l'occasione, il modo e la motivazione di ricoprire tale ruolo, in modo tale da favorire il proseguimento del - progetto coordinando il team con l'aiuto degli strumenti a disposizione. % Chi è il nostro Scrum Master? - In accordo con tale metodologia, il lavoro è stato suddiviso in sprint al termine dei quali si sarebbero dovute - sviluppare un numero minimo di funzionalità dell'applicativo, della durata media di una settimana e mezzo. I meeting - sono stati frequenti all'inizio e ne sono stati svolti alcuni saltuariamente all'interno di ogni sprint. Questo - perché, alternandosi con un periodo di lavoro autonomo, abbiamo ritenuto necessario e producente confrontarsi anche - durante gli sprint su scelte sintattiche e pattern di sviluppo tra tutti i membri del team, in modo da condividere - conoscenze ed entusiasmo. - - \subsection{Strumenti adottati} - Come strategia di workflowing è stata scelta questa variante molto semplice, schematizzata di seguito: - % TODO: Produrre immagine che rappresenti il workflow. - Come strumenti di \textit{Build Automation} si è deciso di usare \textit{SBT}, dato che durante il corso lo si è - sempre preferito rispetto al cugino Gradle per lo svolgimento degli elaborati, nonostante il fatto che alcuni - componenti del gruppo lo uilizzino in ambito aziendale. - La comunicazione all'interno del gruppo è avvenuta su \textit{Telegram} per quanto concerne meri aspetti - organizzativi, mentre la condivisione di appunti e blocchi di codice durante le sessioni di sviluppo è stato - sfruttato \textit{Microsoft Teams}. - Per la \textit{Continuous Integration} è stato scelto \textit{Travis CI}. Consisteva nell'unica soluzione freeware - che i componenti del gruppo abbiano mai usato, introdotta proprio in questo corso. Tuttavia, è stato necessario - approfondire l'argomento tramite studio autonomo, dato che ciò che si era appreso a lezione è stato ritenuto - insufficente per la buona riuscita del progetto per come è stato pensato. Ad esso è stata affidata l'esecuzione dei - test necessari per verificare la correttezza del lavoro svolto e poter poi effettuare dei rilasci senza regressioni. - Tramite lo stesso servizio, è stata effettuata la \textit{Continuous Delivery}, rilasciando dei pacchetti compilati - su \textit{Github Releases}. - - \newpage - - - \section{Requisiti} +\urlstyle{same} - \subsection{Business} - - \subsection{Utente} - - \subsection{Funzionali} - - \subsection{Non Funzionali} - - \subsection{Implementativi} - - \newpage - - - \section{Design Architetturale} - - \subsection{Model} - - \subsection{View} - - \subsection{Controller} - - \newpage +% Path relative to the main .tex file. +\graphicspath{ {./images/} } +\title{Scalavelli project} - \section{Design di Dettaglio} +\date{14 Agosto - 15 Settembre 2020} - \newpage +\author{ +Cavalluzzo M. +\and +Giorgetti L. +\and +Pagnini L. +\and +Tentoni D. +} +\begin{document} + \pagenumbering{gobble} % no numbers. + \maketitle + \newpage + \pagenumbering{arabic} % start counting numbers. - \section{Implementazione} + \begin{center} + Progetto scritto in Scala per giocare al celebre gioco Machiavelli sul proprio computer e sfidare altri giocatori. + \end{center} - \subsection{Matteo} + \tableofcontents - \subsection{Luca} + \newpage - \subsection{Lorenzo} - \subsection{Daniele} + \section{Processo di sviluppo}\label{sec:processo-di-sviluppo} + \input{processo-di-sviluppo} - \subsubsection{Continuous Integration} - Io mi sono occupato di configurare opportunamente l'ambiente di CI scelto in modo da poter verificare la - correttezza di ogni singola build, compilando ad ogni push su ogni branch e pull request. Inoltre, viene - effettuato anche un upload sul sito Codecov.io (che si occupa di mantenere delle statistiche sulla copertura - dei test sul progetto), del rilascio della Doc e Scaladoc ad ogni rilascio sul branch di sviluppo sull'ambiente - di Github-Pages in modo da renderlo disponibile per tutti e di effettuare un rilascio dei pacchetti eseguibili - degli applicativi server e client ad ogni push sul master che sia stata etichettata da un tag. Questa parte - ha richiesto molto lavoro ancora prima di iniziare a sviluppare, ma successivamente tutto il team ne ha tratto - beneficio. Successivamente, in corso d'opera, si è trattato solamente di adattare mano a mano la configurazione - già esistente alle crescenti esigenze del progetto. In primo luogo, il bisogno di stringere i tempi di compilazione - sull'ambiente remoto, che già all'inizio del progetto, quando quindi era ancora relativamente piccolo, iniziavano - già a richiedere diversi minuti per ogni singolo passaggio, andandosi a sommare in lunghe compilazioni tra i - 10 e i 15 minuti. Successivamente ha richiesto di risolvere qualche problema con la compilazione e l'impacchettamento - degli eseguibili. - \subsubsection{Build automation} - Mi sono occupato inoltre degli script necessari per eseguire agilmente una compilazione di tutto il progetto - o di parte di esso in base alle esigenze. Ho configurato tutto il progetto in modo che fosse logicamente diviso - in moduli in modo da aumentare l'incapsulamente e l'esposizione ad altre porzioni di progetto di solo le parti - necessarie. Ho inoltre configurato tutti i pacchetti e le librerie aggiuntive da importare nel progetto. + \section{Requisiti}\label{sec:requisiti} + \input{requisiti} - \subsubsection{Model} - Assieme a Lorenzo mi sono occupato dello sviluppo delle entità base del progetto e di tutti quegli elementi - di gioco che riguardavano le regole del gioco e l'iterazione tra di esse. - \newpage + \section{Design Architetturale}\label{sec:design-architetturale} + \input{design-architetturale} + \section{Design di Dettaglio}\label{sec:design-di-dettaglio} + \input{design-di-dettaglio} - \section{Retrospettiva} - \subsection{Problemi riscontrati} + \section{Implementazione}\label{sec:implementazione} + \input{implementazione} - \subsection{Sviluppi futuri} - \newpage + \section{Retrospettiva}\label{sec:retrospettiva} + \input{retrospettiva} \end{document} diff --git a/source/doc/requisiti.tex b/source/doc/requisiti.tex new file mode 100644 index 00000000..632d6211 --- /dev/null +++ b/source/doc/requisiti.tex @@ -0,0 +1,107 @@ +\subsection{Business} +Il progetto Scalavelli vuole ricreare l’esperienza del celebre gioco di carte Machiavelli (Ramino Machiavellico) in modalità multiplayer, tra giocatori differenti collocati sulla stessa macchina o nella stessa LAN. Ogni giocatore deve potersi identificare con un proprio username e connettersi ad una lobby. +Essa deve poter contenere da 2 a 6 giocatori. +Raggiunto il numero necessario di essi si potrà partecipare ad una partita. +Vi è anche la possibilità di scegliere di giocare specificatamente con i propri amici inserendo il codice identificativo per una partita privata. + +\subsection{Utente} +L’utente medio utilizzatore potrà interagire solamente con il client (come descritto in seguito). +Le operazioni in fase di creazione della partita sono: +\begin{itemize} + \item Specificare il proprio username (per potersi registrare nel server) e il numero di giocatori di una partita pubblica; + \item Specificare il proprio username, il numero di giocatori e generare o specificare un codice per una partita privata; + \item Creare una lobby privata; + \item Connettersi ad una lobby e attendere il raggiungimento del numero di giocatori. +\end{itemize} +Dopo aver generato una partita ed esservi entrato, in una nuova schermata il giocatore potrà: +\begin{itemize} + \item Vedere sullo schermo le combinazioni che ci sono attualmente sul tavolo da gioco, le carte che ha in mano, il nome degli altri giocatori e il numero di carte nelle loro mani; + \item Solamente nel proprio turno, eseguire le seguenti azioni: + \begin{itemize} + \item Giocare una combinazione: si possono mettere sul tavolo da giocare una sequenza di carte che compongono una combinazione tra un tris, un poker oppure una scala. + Se la combinazione è valida, allora vengono tolte le carte che si vuole giocare dalla mano e vengono messe sul tavolo; + \item Aggiungere carte ad una combinazione sul tavolo già esistente: si possono scegliere delle carte dalla propria mano, che non necessariamente compongono una combinazione valida e mettere assieme alle carte che compongono un’altra combinazione, a patto che venga sempre rispettata la validità della combinazione; + \item Prendere delle carte dal tavolo: si possono scegliere delle carte che appartengono ad una combinazione presente sul tavolo da gioco e metterle nella propria mano. + \item Passare il turno al giocatore successivo: dopo che viene eseguita questa azione, il turno viene passato al giocatore successivo. + Il giocatore che ha eseguito questa azione non ne può effettuare nessun’altra fino a che non riprende il proprio turno dall’azione del giocatore precedente nell’ordine. + Nel caso in cui il giocatore di turno: + \begin{itemize} + \item non avesse eseguito nessuna mossa; + \item avesse in mano delle carte che ha preso dal tavolo da gioco senza averle rigiocate; + \item se non si trovasse con meno carte in mano rispetto a quando ha iniziato il turno; + \item se scade il tempo a disposizione; + \end{itemize} + allora è costretto a pescare una carta dal mazzo principale. + \end{itemize} +\end{itemize} + +\subsection{Funzionali} +\begin{center} + \includegraphics[scale=0.4]{casiDUso_Lobby} +\end{center} +\space +\hspace{4cm}Esempio di gestione lobby +\subsubsection{Connessione al server e creazione della lobby} +Il sistema dovrà: +\begin{itemize} + \item Permettere al client di connettersi a internet; + \item Una volta connessi al server permettersi di unirsi ad una lobby attraverso il proprio username; + \item Assicurarsi che il server mantenga sempre attiva la connessione con il client, per poter inviare e ricevere messaggi inerenti alla partita; + \item Assicurarsi che qualora venga raggiunto il numero di giocatori necessario, il server dovrà subito far partire la partita tra i giocatori; + \item Nel caso in cui un giocatore venga disconnesso dalla partita, sia una disconnessione volontaria o un problema dell’infrastruttura di rete, terminare la partita per tutti, venendo visto come un abbandono; + \item Al termine di una partita permetterne di iniziarne una nuova. +\end{itemize} +\begin{center} + \includegraphics[scale=0.4]{casiDUso_Game} +\end{center} +\hspace{4cm}Esempio di gestione partita +\subsubsection{Gioco} +All’inizio della partita vengono mischiati due mazzi da 52 carte ognuno e distribuite 13 carte ad ogni giocatore. +Nel proprio turno si ha un tempo massimo di 2 minuti per svolgere le proprie mosse. +Una volta scaduto, tutte le mosse effettuate vengono annullate, si pesca una carta e si passa il turno al giocatore successivo. +In ogni momento devono sempre essere visibili: +\begin{itemize} + \item le proprie carte in mano; + \item il tavolo da gioco con le varie combinazioni; + \item gli altri giocatori, con i loro nomi e il numero di carte nelle loro mani. +\end{itemize} + +\subsection{Non Funzionali} + +\subsubsection{Scalabilità} +Il server deve essere in grado di supportare un numero indefinito di utenti nell’insieme di tutte le partite. +Nel caso in cui non ci siano abbastanza giocatori in coda per iniziare una partita, allora questi devono essere lasciati in attesa dall’arrivo di altri giocatori che vogliano giocare anche loro. +Il sistema non consente l’accesso ad altri utenti per una partita specifica quando questa ha raggiunto il numero di partecipanti richiesto. + +\subsubsection{Modularità} +Il progetto è stato pensato in modo tale da dover effettuare meno modifiche possibili al client nel caso in cui debba essere cambiato il server e viceversa. +Nello specifico: se il server deve subire un aggiornamento, esso dovrebbe essere spento e poi riacceso. +Gli utenti finali non devono eseguire nessuna operazione sui loro client, o almeno l’aggiornamento deve essere mantenuto il più piccolo possibile. +Viceversa, nel caso di aggiornamento dei client, non dovrebbe essere necessario né riavviare il server, né aggiornare nessuna sua parte. +Tutto il software deve riuscire a funzionare anche cambiando l’implementazione interna del modulo del “core” (regole e logica di gioco) a patto di non impattare sull’interfaccia dello stesso. + +\subsubsection{Usabilità} +Il sistema deve fornire agli utenti un'interfaccia chiara, semplice, ben organizzata in modo da poter utilizzare al meglio tutte le sue funzionalità messe a disposizione e visualizzate. + +\subsubsection{Reattività} +Il sistema deve poter consentire di giocare una partita senza ritardi e/o blocchi temporali dati dall’esecuzione degli algoritmi di validazione e controllo e dai protocolli di comunicazione. + +\subsubsection{Sicurezza} +Il sistema deve aver sempre la possibilità di controllare lo stato attuale del gioco in modo da impedire che alcuni giocatori possano "iniettarne" uno non veritiero nella partita attuale. +Per questo ad ogni fine turno deve essere validato da parte del server in modo che un client malevolo non possa rovinare l’esperienza di gioco agli altri giocatori. + +\subsection{Implementativi} +Di seguito vengono descritti i vincoli che abbiamo cercato di rispettare per l’intero sviluppo del progetto: +\begin{itemize} + \item Mantenere un approccio il più funzionale possibile; + \item Mantenere, in tutti i casi in cui era possibile, una stato immutabile nei vari componenti del sistema. +\end{itemize} +Principali tecnologie e modelli utilizzati sono: +\begin{itemize} + \item Scala: Il sistema deve essere prevalentemente sviluppato in scala. + \item ScalaFX: Il sistema deve disporre di un’interfaccia grafica per poter interagire con il gioco e il server. + \item Prolog: Utilizzo del prolog per implementare l’ordinamento delle carte (per seme e valore), le entità del gioco e la validazione della correttezza delle combinazioni di carte. + \item Akka: Utilizzo di varie componenti per implementare un server e un client che comunichino, secondo il paradigma di programmazione ad attori, per mezzo di messaggi asincroni. + \item TDD (Test Driven Development): Ci siamo ispirati a questa tecnica di sviluppo per scrivere un codice più efficiente, efficace e il più possibile esente da problemi. +\end{itemize} +\newpage \ No newline at end of file diff --git a/source/doc/retrospettiva.tex b/source/doc/retrospettiva.tex new file mode 100644 index 00000000..68f54ede --- /dev/null +++ b/source/doc/retrospettiva.tex @@ -0,0 +1,75 @@ +\subsection{Organizzazione del processo di sviluppo} +Il processo di sviluppo è stato suddiviso in un primo sprint iniziale per confrontarsi sul design architetturale da adottare e discutere delle interazioni che i vari componenti dovevano avere. +In questo sprint abbiamo poi definito i quattro successivi di sviluppo del progetto. +Ogni sprint (eccetto quello iniziale) ha avuto mediamente una durata di circa 1,5/2 settimane. + +\subsubsection{Preparazione iniziale} +Durante questo fase ci siamo incontrati più volte per definire l’architettura e i requisiti base che il nostro progetto doveva soddisfare. +Inoltre abbiamo individuato i componenti di gioco e le loro interazioni. +In questo modo abbiamo avuto l'opportunità di confrontarci e discutere per una migliore organizzazione dei successivi sprint. +Abbiamo inoltre configurato l’ambiente di sviluppo e assegnatoci i rispettivi compiti in base all’interesse di ognuno. +Tutta questa fase è stata completamente da remoto con lunghe chiamate su Microsoft Teams, tranne per l’ultima riunione in cui siamo riusciti a vederci tutti insieme. + +\subsubsection{Sprint 1} +Questo sprint consisteva nel sviluppare le seguenti funzionalità: +\begin{itemize} + \item Visualizzazione di una view di base della lobby in cui l’utente può inserire un username e il numero di giocatori con cui vuole partecipare. + \item Per la parte server, creazione della lobby e gestione degli utenti in coda fino al raggiungimento necessario dei numeri di giocatori. + \item Per il core, modellazione delle entità di gioco sia in Scala che in Prolog: colore, seme, valore, carte e le regole di validazione. +\end{itemize} +In questa fase abbiamo notato che il carico di lavoro per il facimento del Core e del Prolog è stato maggiore rispetto agli altri, dato che su di essi si sarebbe basato molto del lavoro del successivo sprint, causando una maggiore pressione nei due membri del team a cui è stato assegnato questo compito. + +\subsubsection{Sprint 2} +Questo sprint consisteva nel sviluppare le seguenti funzionalità: +\begin{itemize} + \item Creazione della struttura del client, collegamento del client con server durante la fase di registrazione ad una lobby. + \item Inizio gioco: collegamento della lobby con lo stato iniziale della partita. + \item View di gioco con la visualizzazione del tavolo e delle carte in mano ad un giocatore. + \item Definizione dell’interfaccia core per la gestione del gioco. + \item Arricchimento la libreria prolog con l’aggiunta dell’ordinamento delle carte per seme e per valore che fino a quel momento erano state implementate in scala. +\end{itemize} + +\subsubsection{Sprint 3} +L’obiettivo di questo sprint era quello di collegare le singole parti sviluppate in modo da ottenere un collegamento tra client-core-server e view visualizzando le funzionalità delle partita. +Abbiamo terminato tutta l’implementazione del Core con qualche piccolo aggiustamento. +Da questo sprint in poi sono iniziati ad essere rilevati tanti problemi con la validazione delle combinazioni in Prolog, causati dalla molteplice valenza del valore Asso (come primo e ultimo valore di una scala e possibilità di essere presente in due copie). +\newline +Questo è stato lo sprint più importante di tutto il processo di sviluppo poiché sono stati completati molti task alla gestione del gioco. +Inoltre, abbiamo notato il problema inverso rispetto al primo sprint: il carico di lavoro sul Server e sul Client è stato maggiore che sul resto. +Tuttavia in questo caso è stato possibile per i due membri restanti aiutare gli altri nella realizzazione dei loro task. + +\subsubsection{Sprint 4} +Nell’ultimo sprint ci siamo occupati di risolvere alcuni problemi legati al server come la disconnessione improvvisa da parte di un utente o l’abbandono della partita. +Inoltre, lato client, abbiamo implementato la gestione del tempo che ogni giocatore ha a disposizione per completare il turno. +In questa fase tutti i membri del team hanno contribuito a risolvere i bug e rifattorizzare il codice presente. + +\subsection{Sviluppi futuri} +Di sicuro come prossimi sviluppi potrebbero essere affrontati tutti i punti opzionali che non siamo riusciti a svolgere entro il tempo del progetto, quali: +\begin{itemize} + \item Funzionalità che suggerisca all'utente una possibile mossa da fare nel proprio turno. + \item Semplice AI contro cui giocare in locale. + \item Possibilità di personalizzare alcune caratteristiche di questo gioco. +\end{itemize} +Al di fuori di essi, principalmente si è pensato a: +\begin{itemize} + \item modificare il server per poterlo installare su di un container da mettere su una macchina remoto raggiungibile da remoto in modo da poter estendere i possibili utenti utilizzatori del gioco, per creare partite più avvincenti ed un’esperienza d’uso più completa. + \item creare una base dati per la memorizzazione dei risultati delle partite e degli utenti in modo da generare periodicamente una classifica dei migliori giocatori. + \item migliorare la funzionalità di generazione di partite private per arrivare a creare dei veri e propri tornei. + Gli utenti, inseriti dentro a delle maxi lobby private, all’avvio del turno di un torneo, verrebbero accoppiati ad altri giocatori in base ai risultati delle partite precedenti. + Gli utenti tornerebbero nella stessa maxi lobby al termine del turno per poter di nuovo essere accoppiati. + Regole di gestione del torneo ulteriori, come ad esempio i gironi all’italiana o i punti della classifica potrebbero essere scelte direttamente dai gestori del torneo. +\end{itemize} + +\subsection{Commenti finali} +Ci siamo trovati bene a lavorare con la metodologia Agile imparata a lezione in questo corso. Avere degli obiettivi precisi da portare a termine in poco tempo proporzionati alle forze del team ha aiutato a ridurre drasticamente il classico stress dovuto all’approssimarsi delle scadenze. \newline \newline Anche l’uso di Trello ha contribuito, rendendo più veloce la comunicazione all’interno del team in termini di reporting delle issues e dei job per ogni membro.\newline \newline Il nome Part-Time-Team dato al gruppo non è stato a caso, il motivo per cui l’abbiamo scelto è che 3 componenti su 4 del gruppo sono lavoratori part-time che hanno anche richiesto l’estensione del piano di studi. Non sempre siamo riusciti a dedicare lo stesso lasso di tempo durante la giornata al progetto, quindi l’uso degli strumenti sopra citati è stato più essenziale che opzionale, permettendoci di lavorare in modo indipendente gli uni dagli altri. \newline \newline La tecnologia più ostica per tutto il gruppo è stato il Prolog, dato che ha richiesto di cambiare totalmente il modo di affrontare il problema. Alcune volte, si è pensato ad usare solamente il linguaggio Scala per lo svolgimento di tutte le funzionalità sviluppate invece di Prolog, credendo di risparmiare tempo prezioso. Nonostante le difficoltà, abbiamo comunque pensato che non fosse il caso di rinunciare a tutto il lavoro già svolto. +\paragraph{Daniele} +Penso che partecipare a questo progetto sia stata una bellissima esperienza, sia per l’opportunità di lavorare con tecnologie che non conoscevo, sia per l’aver collaborato con dei bravissimi compagni. Penso che se una persona non conoscesse l’esistenza di queste tecnologie (prima fra tutti la CI) non le andrebbe a cercare tanto, non si sveglierebbe alla mattina pensando di inventare un sistema simile, ma una volta venutone a conoscenza, non ne potrebbe più fare a meno, proprio come è successo a me. +\paragraph{Lorenzo} +Partecipare a questo progetto mi ha aiutato ad approcciarmi con tecnologie che fin'ora non conoscevo e soprattutto mi ha permesso di acquisire nuove abilità sull'approccio funzionale e sulla programmazione in generale. Non avendo fatto ancora un'esperienza lavorativa, partecipare a questo progetto mi ha fatto capire, seppur in minima parte, cosa significa lavorare in un team.\newline Penso di aver raggiunto l'obiettivo finale di questo corso, anche grazie all'aiuto dei membri del team. Inoltre l'esperienza mi ha permesso di conoscere nuovi amici. Li ringrazio della fiducia iniziale che mi hanno dato. +\paragraph{Matteo} +La realizzazione del progetto e in generale il corso di PPS è stata un'esperienza molto positiva, mi ha permesso di approfondire aspetti della programmazione funzionale e soprattuto del testing che ho poi avuto modo di applicare anche al lavoro. +Sicuramente il margine di miglioramento è ancora molto ampio visti i molteplici concetti toccati in un lasso di tempo così limitato, ma gli spunti sono tanti e piano piano cercherò di approfondirli al meglio uno ad uno, partendo dal libro "Clean Code" già pronto sulla mensola. +\paragraph{Luca} +La realizzazione di questo progetto è stata un’esperienza molto positiva. Certamente non sono mancate le difficoltà nel riuscire a incastrare gli impegni di tutti per poter incontrarsi di persona, essendo per la maggior parte lavoratori, ma nell’organizzazione degli sprint abbiamo tenuto conto sia di quello sia delle ferie estive che erano già state programmate. Avevamo lasciato anche un po’ di margine, che si è rivelato fondamentale per via di qualche ritardo nello sviluppo dovuto ad aspetti implementativi e di design che non avevamo considerato. +Le volte in cui siamo riusciti ad incontrarci con il gruppo (sia su Teams, ma soprattutto di persona) sono state fondamentali, sia per condividere le difficoltà che ognuno di noi aveva, sia per fare un po’ di pair-programming, che ci ha dato qualche sicurezza in più quando si usavano costrutti non molto utilizzati fino ad ora. +Esperienza che sicuramente mi ha fatto crescere dal punto di vista del lavoro in gruppo. diff --git a/source/index.html b/source/index.html index 68835939..69482eb2 100644 --- a/source/index.html +++ b/source/index.html @@ -56,9 +56,10 @@

The Scalavelli project!

-
Machiavelli -
-

Our personal implementation for this game.

+
Real Machiavelli
+

Learn basic rules at Wikipedia. This is our personal + implementation for this game.

Learn more on Github! @@ -155,7 +156,8 @@

- This is right! Actually, the project is in Alpha stage, so many features can not work as expected. See other similar issues on Github or open a new one. Developers certainly thank. + This is right! Actually, the project is in Alpha stage, so many features can not work as + expected. See other similar issues on Github or open a new one. Developers certainly thank.

@@ -165,14 +167,14 @@

- Third answer. + Because we had implemented it as that.
diff --git a/source/screen/create_lobby.png b/source/screen/create_lobby.png new file mode 100644 index 00000000..351741e1 Binary files /dev/null and b/source/screen/create_lobby.png differ diff --git a/source/screen/game.png b/source/screen/game.png new file mode 100644 index 00000000..daaf3f58 Binary files /dev/null and b/source/screen/game.png differ diff --git a/source/screen/home.png b/source/screen/home.png new file mode 100644 index 00000000..5fc22f4f Binary files /dev/null and b/source/screen/home.png differ diff --git a/source/screen/join_lobby.png b/source/screen/join_lobby.png new file mode 100644 index 00000000..824906f7 Binary files /dev/null and b/source/screen/join_lobby.png differ