diff --git a/src/main/scala/scatan/controllers/game/GameController.scala b/src/main/scala/scatan/controllers/game/GameController.scala index ecb4af23..f7910de7 100644 --- a/src/main/scala/scatan/controllers/game/GameController.scala +++ b/src/main/scala/scatan/controllers/game/GameController.scala @@ -4,24 +4,113 @@ import scatan.lib.mvc.{BaseController, Controller} import scatan.model.ApplicationState import scatan.model.components.* import scatan.model.game.ScatanModelOps.{onError, updateGame} -import scatan.model.game.config.ScatanPhases.{Game, Setup} import scatan.model.game.config.ScatanPlayer import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} import scatan.views.game.GameView -import scatan.views.game.components.CardContextMap.CardType +/** The controller for the game. + */ trait GameController extends Controller[ApplicationState]: - def onRoadSpot(spot: RoadSpot): Unit - def onStructureSpot(spot: StructureSpot): Unit - def onTradeWithBank(offer: ResourceType, request: ResourceType): Unit - def onTradeWithPlayer(receiver: ScatanPlayer, offer: Map[ResourceType, Int], request: Map[ResourceType, Int]): Unit + def state: ApplicationState + + /** Goes to the next turn. */ def nextTurn(): Unit + + /** Rolls the dice. */ def rollDice(): Unit - def clickCard(card: CardType): Unit + + /** Assigns a road to the current player. + * @param spot + * The spot to assign the road to. + */ + def assignRoad(spot: RoadSpot): Unit + + /** Assigns a settlement to the current player. + * @param spot + * The spot to assign the settlement to. + */ + def assignSettlement(spot: StructureSpot): Unit + + /** Builds a road for the current player. + * @param spot + * The spot to build the road on. + */ + def buildRoad(spot: RoadSpot): Unit + + /** Builds a settlement for the current player. + * @param spot + * The spot to build the settlement on. + */ + def buildSettlement(spot: StructureSpot): Unit + + /** Builds a city for the current player. + * @param spot + * The spot to build the city on. + */ + def buildCity(spot: StructureSpot): Unit + + /** Places the robber on a hexagon. + * @param hexagon + * The hexagon to place the robber on. + */ def placeRobber(hexagon: Hexagon): Unit + + /** Steals a card from a player. + * @param player + * The player to steal a card from. + */ def stealCard(player: ScatanPlayer): Unit + + /** Buys a development card for the current player. + */ def buyDevelopmentCard(): Unit + /** Plays a knight development card for the current player. + * @param robberPosition + * The hexagon to place the robber on. + */ + def playKnightDevelopment(robberPosition: Hexagon): Unit + + /** Plays a year of plenty development card for the current player. + * @param resource1 + * The first resource to get. + * @param resource2 + * The second resource to get. + */ + def playYearOfPlentyDevelopment(resource1: ResourceType, resource2: ResourceType): Unit + + /** Plays a monopoly development card for the current player. + * @param resource + * The resource to get. + */ + def playMonopolyDevelopment(resource: ResourceType): Unit + + /** Plays a road building development card for the current player. + * @param spot1 + * The first spot to build a road on. + * @param spot2 + * The second spot to build a road on. + */ + def playRoadBuildingDevelopment(spot1: RoadSpot, spot2: RoadSpot): Unit + + /** Trades with the bank. + * @param offer + * The resource to offer. + * @param request + * The resource to request. + */ + def tradeWithBank(offer: ResourceType, request: ResourceType): Unit + + /** Trades with a player. + * @param receiver + * The player to trade with. + * @param offer + * The cards to offer. + * @param request + * The cards to request. + */ + def tradeWithPlayer(receiver: ScatanPlayer, offer: Seq[ResourceCard], request: Seq[ResourceCard]): Unit + object GameController: def apply(requirements: Controller.Requirements[GameView, ApplicationState]): GameController = GameControllerImpl(requirements) @@ -30,71 +119,48 @@ private class GameControllerImpl(requirements: Controller.Requirements[GameView, extends BaseController(requirements) with GameController: + override def state: ApplicationState = this.model.state + + override def assignRoad(spot: RoadSpot): Unit = + this.model + .updateGame(_.assignRoad(spot)) + .onError(view.displayMessage("Cannot assign road here")) + + override def assignSettlement(spot: StructureSpot): Unit = + this.model + .updateGame(_.assignSettlement(spot)) + .onError(view.displayMessage("Cannot assign settlement here")) + + override def buildRoad(spot: RoadSpot): Unit = + this.model + .updateGame(_.buildRoad(spot)) + .onError(view.displayMessage("Cannot build road here")) + + override def buildSettlement(spot: StructureSpot): Unit = + this.model + .updateGame(_.buildSettlement(spot)) + .onError(view.displayMessage("Cannot build settlement here")) + + override def buildCity(spot: StructureSpot): Unit = + this.model + .updateGame(_.buildCity(spot)) + .onError(view.displayMessage("Cannot build city here")) + override def placeRobber(hexagon: Hexagon): Unit = this.model .updateGame(_.placeRobber(hexagon)) .onError(view.displayMessage("Cannot place robber")) - override def clickCard(card: CardType): Unit = ??? - - override def nextTurn(): Unit = this.model.updateGame(_.nextTurn) + override def nextTurn(): Unit = + this.model + .updateGame(_.nextTurn) + .onError(view.displayMessage("Cannot go to next turn")) override def rollDice(): Unit = - this.model.updateGame(_.rollDice(diceResult => this.view.displayMessage(s"Roll dice result: $diceResult"))); - - override def onRoadSpot(spot: RoadSpot): Unit = - val phase = this.model.state.game.map(_.gameStatus.phase).get - phase match - case Setup => - this.model - .updateGame(_.assignRoad(spot)) - .onError(view.displayMessage("Cannot assign road here")) - case Game => - this.model - .updateGame(_.buildRoad(spot)) - .onError(view.displayMessage("Cannot build road here")) - - override def onStructureSpot(spot: StructureSpot): Unit = - val phase = this.model.state.game.map(_.gameStatus.phase).get - phase match - case Setup => - this.model - .updateGame(_.assignSettlement(spot)) - .onError(view.displayMessage("Cannot assign settlement here")) - case Game => - val alreadyContainsSettlement = this.model.state.game - .map(_.state) - .map(_.assignedBuildings) - .flatMap(_.get(spot)) - .exists(_.buildingType == BuildingType.Settlement) - if alreadyContainsSettlement then - this.model - .updateGame(_.buildCity(spot)) - .onError(view.displayMessage("Cannot build city here")) - else - this.model - .updateGame(_.buildSettlement(spot)) - .onError(view.displayMessage("Cannot build settlement here")) - - override def onTradeWithBank(offer: ResourceType, request: ResourceType): Unit = this.model - .updateGame(_.tradeWithBank(offer, request)) - .onError( - view.displayMessage("Cannot trade this cards with bank") - ) - - override def onTradeWithPlayer( - receiver: ScatanPlayer, - offer: Map[ResourceType, Int], - request: Map[ResourceType, Int] - ): Unit = - val offerCards = offer.flatMap((resourceType, amount) => ResourceCard(resourceType) ** amount).toSeq - val requestCards = request.flatMap((resourceType, amount) => ResourceCard(resourceType) ** amount).toSeq - this.model - .updateGame(_.tradeWithPlayer(receiver, offerCards, requestCards)) - .onError( - view.displayMessage("Cannot trade this cards with player") - ) + .updateGame(_.rollDice(diceResult => this.view.displayMessage(s"Roll dice result: $diceResult"))) + .onError(view.displayMessage("Cannot roll dice")) + override def stealCard(player: ScatanPlayer): Unit = this.model .updateGame(_.stealCard(player)) @@ -104,3 +170,33 @@ private class GameControllerImpl(requirements: Controller.Requirements[GameView, this.model .updateGame(_.buyDevelopmentCard) .onError(view.displayMessage("Cannot buy development card")) + + override def playKnightDevelopment(robberPosition: Hexagon): Unit = + this.model + .updateGame(_.playKnightDevelopment(robberPosition)) + .onError(view.displayMessage("Cannot play knight development card")) + + override def playYearOfPlentyDevelopment(resource1: ResourceType, resource2: ResourceType): Unit = + this.model + .updateGame(_.playYearOfPlentyDevelopment(resource1, resource2)) + .onError(view.displayMessage("Cannot play year of plenty development card")) + + override def playMonopolyDevelopment(resource: ResourceType): Unit = + this.model + .updateGame(_.playMonopolyDevelopment(resource)) + .onError(view.displayMessage("Cannot play monopoly development card")) + + override def playRoadBuildingDevelopment(spot1: RoadSpot, spot2: RoadSpot): Unit = + this.model + .updateGame(_.playRoadBuildingDevelopment(spot1, spot2)) + .onError(view.displayMessage("Cannot play road building development card")) + + override def tradeWithBank(offer: ResourceType, request: ResourceType): Unit = + this.model + .updateGame(_.tradeWithBank(offer, request)) + .onError(view.displayMessage("Cannot trade with bank")) + + override def tradeWithPlayer(receiver: ScatanPlayer, offer: Seq[ResourceCard], request: Seq[ResourceCard]): Unit = + this.model + .updateGame(_.tradeWithPlayer(receiver, offer, request)) + .onError(view.displayMessage("Cannot trade with player")) diff --git a/src/main/scala/scatan/controllers/game/SetUpController.scala b/src/main/scala/scatan/controllers/game/SetUpController.scala index a0fa4cdb..a0528ad4 100644 --- a/src/main/scala/scatan/controllers/game/SetUpController.scala +++ b/src/main/scala/scatan/controllers/game/SetUpController.scala @@ -1,26 +1,24 @@ package scatan.controllers.game -import scatan.lib.mvc.{BaseController, Controller} +import scatan.lib.mvc.{Controller, EmptyController} import scatan.model.ApplicationState +import scatan.model.map.GameMap import scatan.views.game.SetUpView -import scatan.model.GameMap -/** This is the controller for the setup page. +/** The controller for the game setup screen. */ trait SetUpController extends Controller[ApplicationState]: + + /** Starts the game with the given usernames. + * @param gameMap, + * the game map to use. + * @param usernames, + * the usernames of the players. + */ def startGame(gameMap: GameMap, usernames: String*): Unit object SetUpController: def apply(requirements: Controller.Requirements[SetUpView, ApplicationState]): SetUpController = - SetUpControllerImpl(requirements) - -/** This is the implementation of the controller for the setup page. - * @param requirements, - * the requirements for the controller. - */ -private class SetUpControllerImpl(requirements: Controller.Requirements[SetUpView, ApplicationState]) - extends BaseController(requirements) - with SetUpController: - - override def startGame(gameMap: GameMap, usernames: String*): Unit = - this.model.update(_.createGame(gameMap, usernames*)) + new EmptyController(requirements) with SetUpController: + override def startGame(gameMap: GameMap, usernames: String*): Unit = + this.model.update(_.createGame(gameMap, usernames*)) diff --git a/src/main/scala/scatan/controllers/home/AboutController.scala b/src/main/scala/scatan/controllers/home/AboutController.scala index c5a60463..c36dfbab 100644 --- a/src/main/scala/scatan/controllers/home/AboutController.scala +++ b/src/main/scala/scatan/controllers/home/AboutController.scala @@ -1,16 +1,13 @@ package scatan.controllers.home -import scatan.Pages -import scatan.lib.mvc.{BaseController, Controller} +import scatan.lib.mvc.{Controller, EmptyController} import scatan.model.ApplicationState import scatan.views.home.AboutView +/** The about page controller. + */ trait AboutController extends Controller[ApplicationState] object AboutController: def apply(requirements: Controller.Requirements[AboutView, ApplicationState]): AboutController = - AboutControllerImpl(requirements) - -private class AboutControllerImpl(requirements: Controller.Requirements[AboutView, ApplicationState]) - extends BaseController(requirements) - with AboutController + new EmptyController(requirements) with AboutController diff --git a/src/main/scala/scatan/controllers/home/HomeController.scala b/src/main/scala/scatan/controllers/home/HomeController.scala index fe14c3d8..e38cdc57 100644 --- a/src/main/scala/scatan/controllers/home/HomeController.scala +++ b/src/main/scala/scatan/controllers/home/HomeController.scala @@ -1,22 +1,13 @@ package scatan.controllers.home -import scatan.Pages -import scatan.lib.mvc.{BaseController, Controller} +import scatan.lib.mvc.{Controller, EmptyController} import scatan.model.ApplicationState import scatan.views.home.HomeView -/** This is the controller for the home page. +/** The home page controller. */ trait HomeController extends Controller[ApplicationState] object HomeController: def apply(requirements: Controller.Requirements[HomeView, ApplicationState]): HomeController = - HomeControllerImpl(requirements) - -/** This is the implementation of the controller for the home page. - * @param requirements, - * the requirements for the controller. - */ -class HomeControllerImpl(requirements: Controller.Requirements[HomeView, ApplicationState]) - extends BaseController(requirements) - with HomeController + new EmptyController(requirements) with HomeController diff --git a/src/main/scala/scatan/lib/game/Game.scala b/src/main/scala/scatan/lib/game/Game.scala index ba0266ab..d80b4a32 100644 --- a/src/main/scala/scatan/lib/game/Game.scala +++ b/src/main/scala/scatan/lib/game/Game.scala @@ -1,6 +1,6 @@ package scatan.lib.game -import scatan.model.GameMap +import scatan.model.map.GameMap /** A game status is a pair of phase and step. * @param phase diff --git a/src/main/scala/scatan/lib/game/Rules.scala b/src/main/scala/scatan/lib/game/Rules.scala index 3b8c7f68..e818f4e9 100644 --- a/src/main/scala/scatan/lib/game/Rules.scala +++ b/src/main/scala/scatan/lib/game/Rules.scala @@ -1,6 +1,6 @@ package scatan.lib.game -import scatan.model.GameMap +import scatan.model.map.GameMap /** Rules of a game. * @param startingStateFactory @@ -70,23 +70,3 @@ final case class Rules[State, P, S, A, Player]( (status, action) -> step } } - -object Rules: - def empty[State, P, S, A, Player]: Rules[State, P, S, A, Player] = - fromStateFactory((_, _) => null.asInstanceOf[State]) - - def fromStateFactory[State, P, S, A, Player]( - initialStateFactory: (GameMap, Seq[Player]) => State - ): Rules[State, P, S, A, Player] = - Rules[State, P, S, A, Player]( - startingStateFactory = initialStateFactory, - startingPhase = null.asInstanceOf[P], - actions = Map.empty, - allowedPlayersSizes = Set.empty, - startingSteps = Map.empty, - phaseTurnIteratorFactories = Map.empty, - nextPhase = Map.empty, - endingSteps = Map.empty, - winnerFunction = (_: State) => None, - initialAction = Map.empty - ) diff --git a/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala index d47e7abe..b168f672 100644 --- a/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala +++ b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala @@ -1,6 +1,6 @@ package scatan.lib.game.dsl -import scatan.model.GameMap +import scatan.model.map.GameMap private object GameDSLDomain: diff --git a/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala b/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala index 662fe640..0f210d09 100644 --- a/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala +++ b/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala @@ -2,7 +2,7 @@ package scatan.lib.game.dsl.ops import scatan.lib.game.dsl.GameDSLDomain.* import scatan.lib.game.dsl.PropertiesDSL.* -import scatan.model.GameMap +import scatan.model.map.GameMap object GameCtxOps: diff --git a/src/main/scala/scatan/lib/game/ops/GamePlayOps.scala b/src/main/scala/scatan/lib/game/ops/GamePlayOps.scala index 032cbf4f..96f409ba 100644 --- a/src/main/scala/scatan/lib/game/ops/GamePlayOps.scala +++ b/src/main/scala/scatan/lib/game/ops/GamePlayOps.scala @@ -58,5 +58,7 @@ object GamePlayOps: else newGame extension (bool: Boolean) - def option: Option[Unit] = + /** Convert a boolean to an option, returning Some(()) if true, None if false + */ + private def option: Option[Unit] = if bool then Some(()) else None diff --git a/src/main/scala/scatan/lib/game/ops/RulesOps.scala b/src/main/scala/scatan/lib/game/ops/RulesOps.scala index 50b1716f..7c830791 100644 --- a/src/main/scala/scatan/lib/game/ops/RulesOps.scala +++ b/src/main/scala/scatan/lib/game/ops/RulesOps.scala @@ -1,7 +1,7 @@ package scatan.lib.game.ops import scatan.lib.game.{GameStatus, Rules} -import scatan.model.GameMap +import scatan.model.map.GameMap /** Operations on [[Rules]] related to their construction. */ diff --git a/src/main/scala/scatan/lib/mvc/Controller.scala b/src/main/scala/scatan/lib/mvc/Controller.scala index 233bdff3..4becb310 100644 --- a/src/main/scala/scatan/lib/mvc/Controller.scala +++ b/src/main/scala/scatan/lib/mvc/Controller.scala @@ -16,6 +16,14 @@ object Controller: trait Provider[C <: Controller[?]]: def controller: C +/** A wrapper for a model that updates the view when the model is updated. + * @param view + * The view to update + * @param model + * The model to wrap + * @tparam S + * The state type + */ class ReactiveModelWrapper[S <: Model.State](view: => View[S], model: Model[S]) extends Model[S]: private val internalModel = model override def state: S = internalModel.state @@ -23,6 +31,14 @@ class ReactiveModelWrapper[S <: Model.State](view: => View[S], model: Model[S]) internalModel.update(f) view.updateState(this.state) +/** A base controller that wraps a model and a view. + * @param requirements + * The requirements for the controller + * @tparam V + * The view type + * @tparam S + * The state type + */ abstract class BaseController[V <: View[S], S <: Model.State](requirements: Controller.Requirements[V, S]) extends Controller[S] with Controller.Dependencies(requirements): @@ -31,5 +47,11 @@ abstract class BaseController[V <: View[S], S <: Model.State](requirements: Cont override protected val model: Model[S] = new ReactiveModelWrapper(requirements.view, requirements.model) -class EmptyController[State <: Model.State](requirements: Controller.Requirements[View[State], State]) +/** A controller that does nothing. + * @param requirements + * The requirements for the controller + * @tparam State + * The state type + */ +class EmptyController[State <: Model.State, V <: View[State]](requirements: Controller.Requirements[V, State]) extends BaseController(requirements) diff --git a/src/main/scala/scatan/lib/mvc/Model.scala b/src/main/scala/scatan/lib/mvc/Model.scala index f47438da..446ae942 100644 --- a/src/main/scala/scatan/lib/mvc/Model.scala +++ b/src/main/scala/scatan/lib/mvc/Model.scala @@ -1,14 +1,18 @@ package scatan.lib.mvc +/** A model is a container for a state object. It provides a way to update the state object. + * @tparam S + * The type of the state object. + */ trait Model[S <: Model.State]: def state: S def update(f: S => S): Unit object Model: trait State - def apply[S <: State](__state: S): Model[S] = + def apply[S <: State](initialState: S): Model[S] = new Model[S]: - private var _state = __state + private var _state = initialState override def state: S = _state override def update(f: S => S): Unit = _state = f(_state) diff --git a/src/main/scala/scatan/lib/mvc/NavigableApplicationManager.scala b/src/main/scala/scatan/lib/mvc/NavigableApplicationManager.scala index c90764d9..3d8ae46d 100644 --- a/src/main/scala/scatan/lib/mvc/NavigableApplicationManager.scala +++ b/src/main/scala/scatan/lib/mvc/NavigableApplicationManager.scala @@ -2,6 +2,9 @@ package scatan.lib.mvc import scatan.lib.mvc.application.NavigableApplication +/** A singleton object that manages the currently running application. It is used to start the application and to + * navigate to a new route. It is also used to navigate back to the previous route. + */ object NavigableApplicationManager: private var _application: Option[NavigableApplication[?, ?]] = None diff --git a/src/main/scala/scatan/lib/mvc/ScalaJSView.scala b/src/main/scala/scatan/lib/mvc/ScalaJSView.scala index 4bbda361..467a44bf 100644 --- a/src/main/scala/scatan/lib/mvc/ScalaJSView.scala +++ b/src/main/scala/scatan/lib/mvc/ScalaJSView.scala @@ -3,16 +3,25 @@ package scatan.lib.mvc import com.raquo.laminar.api.L.* import org.scalajs.dom +/** A view that uses Laminar to render itself. + * @tparam State + * The type of the state of the view. + */ trait ScalaJSView[State <: Model.State]( - val container: String, - val initialState: State + val container: String, // The id of the container element + val initialState: State // The initial state of the view ) extends View[State]: private val _reactiveState = Var[State](initialState) - val reactiveState: Signal[State] = _reactiveState.signal override def updateState(state: State): Unit = _reactiveState.writer.onNext(state) + /** A signal that emits the current state of the application. + */ + def reactiveState: Signal[State] = _reactiveState.signal + + /** The element that is rendered by this view. + */ def element: Element override def show(): Unit = @@ -31,6 +40,16 @@ trait ScalaJSView[State <: Model.State]( object ScalaJSView: type Factory[C <: Controller[?], V <: View[?]] = (String, View.Requirements[C]) => V +/** A base class for views that use Laminar to render themselves. + * @param container + * The id of the container element + * @param requirements + * The requirements of the view. + * @tparam State + * The type of the state of the view. + * @tparam C + * The type of the controller of the view. + */ abstract class BaseScalaJSView[State <: Model.State, C <: Controller[State]]( container: String, requirements: View.Requirements[C] diff --git a/src/main/scala/scatan/lib/mvc/View.scala b/src/main/scala/scatan/lib/mvc/View.scala index ecef1edf..49adcb84 100644 --- a/src/main/scala/scatan/lib/mvc/View.scala +++ b/src/main/scala/scatan/lib/mvc/View.scala @@ -1,9 +1,29 @@ package scatan.lib.mvc +/** The View component. + * @tparam State + * The type of the state of the model. + */ trait View[State <: Model.State]: + + /** Displays the view. + */ def show(): Unit + + /** Hides the view. + */ def hide(): Unit + + /** Displays a message. + * @param message + * The message to display. + */ def displayMessage(message: String): Unit + + /** Updates the state of the view. + * @param state + * The new state of the view. + */ private[mvc] def updateState(state: State): Unit = () /** The View object. @@ -19,10 +39,21 @@ object View: trait Provider[V <: View[?]]: def view: V +/** A mixin trait for views that can navigate to other views. + */ trait NavigatorView extends View[?]: def navigateTo[Route](route: Route): Unit = NavigableApplicationManager.navigateTo(route) def navigateBack(): Unit = NavigableApplicationManager.navigateBack() +/** A base class for views. + * + * @param requirements + * The requirements of the view. + * @tparam State + * The type of the state of the model. + * @tparam C + * The type of the controller. + */ abstract class BaseView[State <: Model.State, C <: Controller[State]](requirements: View.Requirements[C]) extends View[State] with NavigatorView diff --git a/src/main/scala/scatan/lib/mvc/application/Navigable.scala b/src/main/scala/scatan/lib/mvc/application/Navigable.scala index d8621bd5..6ec78df6 100644 --- a/src/main/scala/scatan/lib/mvc/application/Navigable.scala +++ b/src/main/scala/scatan/lib/mvc/application/Navigable.scala @@ -3,6 +3,10 @@ package scatan.lib.mvc.application import scatan.lib.mvc.Model import scatan.lib.mvc.page.{ApplicationPage, PageFactory} +/** A mixin for an application that can navigate between pages. + * @tparam Route + * The type of the route. + */ trait Navigable[Route] extends Application[?, Route]: private var pagesHistory: Seq[Route] = Seq.empty def show(route: Route): Unit = @@ -14,6 +18,12 @@ trait Navigable[Route] extends Application[?, Route]: pagesHistory = pagesHistory.dropRight(1) pagesHistory.lastOption.foreach(pages(_).view.show()) +/** A Navigable application. + * @tparam S + * The state type of the model. + * @tparam Route + * The type of the route. + */ trait NavigableApplication[S <: Model.State, Route] extends Application[S, Route] with Navigable[Route] object NavigableApplication: diff --git a/src/main/scala/scatan/lib/mvc/page/ApplicationPage.scala b/src/main/scala/scatan/lib/mvc/page/ApplicationPage.scala index ac5cd8a6..0b5a0a27 100644 --- a/src/main/scala/scatan/lib/mvc/page/ApplicationPage.scala +++ b/src/main/scala/scatan/lib/mvc/page/ApplicationPage.scala @@ -20,14 +20,14 @@ trait ApplicationPage[S <: Model.State, C <: Controller[?], V <: View[?]]( val pageFactory: PageFactory[C, V, S] ) extends View.Requirements[C] with Controller.Requirements[V, S]: - lazy val _view: V = pageFactory.viewFactory(this) - lazy val _controller: C = pageFactory.controllerFactory(this) - override def controller: C = _controller - override def view: V = _view + override lazy val view: V = pageFactory.viewFactory(this) + override lazy val controller: C = pageFactory.controllerFactory(this) object ApplicationPage: + type Factory[S <: Model.State, C <: Controller[S], V <: View[S]] = Model[S] => ApplicationPage[S, C, V] + def apply[S <: Model.State, C <: Controller[?], V <: View[?]]( model: Model[S], pageFactory: PageFactory[C, V, S] diff --git a/src/main/scala/scatan/lib/mvc/page/PageFactory.scala b/src/main/scala/scatan/lib/mvc/page/PageFactory.scala index da80667c..61dbe22b 100644 --- a/src/main/scala/scatan/lib/mvc/page/PageFactory.scala +++ b/src/main/scala/scatan/lib/mvc/page/PageFactory.scala @@ -2,9 +2,17 @@ package scatan.lib.mvc.page import scatan.lib.mvc.{Controller, Model, ScalaJSView, View} +/** A factory for creating pages. + * @tparam C + * The controller type. + * @tparam V + * The view type. + * @tparam S + * The state type. + */ trait PageFactory[C <: Controller[?], V <: View[?], S <: Model.State]: - val viewFactory: View.Factory[C, V] - val controllerFactory: Controller.Factory[V, C, S] + def viewFactory: View.Factory[C, V] + def controllerFactory: Controller.Factory[V, C, S] object PageFactory: def apply[C <: Controller[S], V <: View[S], S <: Model.State]( diff --git a/src/main/scala/scatan/model/ApplicationState.scala b/src/main/scala/scatan/model/ApplicationState.scala index 6af88524..b7747bce 100644 --- a/src/main/scala/scatan/model/ApplicationState.scala +++ b/src/main/scala/scatan/model/ApplicationState.scala @@ -3,6 +3,7 @@ package scatan.model import scatan.lib.mvc.Model import scatan.model.game.ScatanGame import scatan.model.game.config.ScatanPlayer +import scatan.model.map.GameMap final case class ApplicationState(game: Option[ScatanGame]) extends Model.State: def createGame(gameMap: GameMap, usernames: String*): ApplicationState = diff --git a/src/main/scala/scatan/model/components/Award.scala b/src/main/scala/scatan/model/components/Award.scala index 9a5bf069..f9e35dce 100644 --- a/src/main/scala/scatan/model/components/Award.scala +++ b/src/main/scala/scatan/model/components/Award.scala @@ -2,12 +2,18 @@ package scatan.model.components import scatan.model.game.config.ScatanPlayer +/** Type of possible awards. + */ enum AwardType: case LongestRoad case LargestArmy +/** An award + */ final case class Award(awardType: AwardType) +/** The assigned awards to the current holder player and the number of points. + */ type Awards = Map[Award, Option[(ScatanPlayer, Int)]] object Awards: diff --git a/src/main/scala/scatan/model/components/Building.scala b/src/main/scala/scatan/model/components/Building.scala index 483bbdfc..cbc232ef 100644 --- a/src/main/scala/scatan/model/components/Building.scala +++ b/src/main/scala/scatan/model/components/Building.scala @@ -4,7 +4,7 @@ import scatan.model.components.* import scatan.model.components.BuildingType.* import scatan.model.components.ResourceType.* import scatan.model.game.config.ScatanPlayer -import scatan.model.map.{RoadSpot, Spot, StructureSpot} +import scatan.model.map.Spot import scala.collection.immutable.ListMap @@ -14,6 +14,8 @@ type Cost = Map[ResourceType, Int] object Cost: def apply(resourceCosts: ResourceCost*): Cost = resourceCosts.toMap +/** A building type and its cost. + */ enum BuildingType(val cost: Cost): case Settlement extends BuildingType( diff --git a/src/main/scala/scatan/model/components/DevelopmentCard.scala b/src/main/scala/scatan/model/components/DevelopmentCard.scala index ae9fb4d0..6a40daa8 100644 --- a/src/main/scala/scatan/model/components/DevelopmentCard.scala +++ b/src/main/scala/scatan/model/components/DevelopmentCard.scala @@ -2,6 +2,8 @@ package scatan.model.components import scatan.model.game.config.ScatanPlayer +/** Type of possible development cards. + */ enum DevelopmentType: case Knight case RoadBuilding @@ -9,11 +11,16 @@ enum DevelopmentType: case Monopoly case VictoryPoint +/** A development card. + */ final case class DevelopmentCard( developmentType: DevelopmentType, - drewAt: Option[Int] = None + drewAt: Option[Int] = None, + played: Boolean = false ) +/** The development cards hold by the players. + */ type DevelopmentCards = Map[ScatanPlayer, Seq[DevelopmentCard]] object DevelopmentCards: diff --git a/src/main/scala/scatan/model/components/ResourceCard.scala b/src/main/scala/scatan/model/components/ResourceCard.scala index 0beac3eb..3cb542d8 100644 --- a/src/main/scala/scatan/model/components/ResourceCard.scala +++ b/src/main/scala/scatan/model/components/ResourceCard.scala @@ -2,6 +2,8 @@ package scatan.model.components import scatan.model.game.config.ScatanPlayer +/** Type of possible resources. + */ enum ResourceType: case Wood case Brick @@ -9,10 +11,14 @@ enum ResourceType: case Wheat case Rock +/** A resource card. + */ final case class ResourceCard(resourceType: ResourceType) extension (card: ResourceCard) def **(amount: Int): Seq[ResourceCard] = Seq.fill(amount)(card) +/** The resource cards hold by the players. + */ type ResourceCards = Map[ScatanPlayer, Seq[ResourceCard]] object ResourceCards: diff --git a/src/main/scala/scatan/model/components/Terrain.scala b/src/main/scala/scatan/model/components/Terrain.scala index 499b43a5..8ecc854c 100644 --- a/src/main/scala/scatan/model/components/Terrain.scala +++ b/src/main/scala/scatan/model/components/Terrain.scala @@ -17,4 +17,6 @@ object Listable: enum UnproductiveTerrain: case Desert, Sea +/** All possible kind of terrains. + */ type Terrain = ResourceType | UnproductiveTerrain diff --git a/src/main/scala/scatan/model/game/DevelopmentCardsDeck.scala b/src/main/scala/scatan/model/game/DevelopmentCardsDeck.scala index a0342284..9669e415 100644 --- a/src/main/scala/scatan/model/game/DevelopmentCardsDeck.scala +++ b/src/main/scala/scatan/model/game/DevelopmentCardsDeck.scala @@ -3,13 +3,16 @@ package scatan.model.game import scatan.model.components.DevelopmentType.* import scatan.model.components.{DevelopmentCard, DevelopmentType} +import scala.annotation.targetName import scala.util.Random extension (int: Int) + @targetName("timesDevelopmentType") def *(developmentType: DevelopmentType): DevelopmentCardsDeck = Seq.fill(int)(DevelopmentCard(developmentType)) type DevelopmentCardsDeck = Seq[DevelopmentCard] + object DevelopmentCardsDeck: def defaultOrdered: DevelopmentCardsDeck = @@ -19,5 +22,6 @@ object DevelopmentCardsDeck: 2 * YearOfPlenty ++ 2 * Monopoly - def shuffled: DevelopmentCardsDeck = - Random.shuffle(defaultOrdered) + def shuffled(seed: Int = 1): DevelopmentCardsDeck = + val random = Random(seed) + defaultOrdered.sortBy(_ => random.nextDouble()) diff --git a/src/main/scala/scatan/model/game/ScatanDSL.scala b/src/main/scala/scatan/model/game/ScatanDSL.scala index faa811ca..ac22513e 100644 --- a/src/main/scala/scatan/model/game/ScatanDSL.scala +++ b/src/main/scala/scatan/model/game/ScatanDSL.scala @@ -1,16 +1,18 @@ package scatan.model.game import scatan.lib.game.Rules +import scatan.lib.game.dsl.GameDSL.* import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} -import scatan.model.game.ops.ResourceCardOps.assignResourcesAfterInitialPlacement -import scatan.model.game.ops.ScoreOps.winner +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.ResourceCardOps.assignResourcesAfterInitialPlacement +import scatan.model.game.state.ops.ScoreOps.winner import scala.language.postfixOps +/** Scatan game rules + */ object ScatanDSL: - import scatan.lib.game.dsl.GameDSL.* - private val game = Game[ScatanState, ScatanPhases, ScatanSteps, ScatanActions, ScatanPlayer] { Players { @@ -73,5 +75,4 @@ object ScatanDSL: } - def rules: Rules[ScatanState, ScatanPhases, ScatanSteps, ScatanActions, ScatanPlayer] = - game.rules + def rules: Rules[ScatanState, ScatanPhases, ScatanSteps, ScatanActions, ScatanPlayer] = game.rules diff --git a/src/main/scala/scatan/model/game/ScatanEffects.scala b/src/main/scala/scatan/model/game/ScatanEffects.scala index 9c9b9761..cdacacda 100644 --- a/src/main/scala/scatan/model/game/ScatanEffects.scala +++ b/src/main/scala/scatan/model/game/ScatanEffects.scala @@ -4,34 +4,84 @@ import scatan.lib.game.ops.Effect import scatan.model.components.{BuildingType, ResourceCard, ResourceType} import scatan.model.game.config.ScatanActions.* import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.BuildingOps.{assignBuilding, build} -import scatan.model.game.ops.DevelopmentCardOps.buyDevelopmentCard -import scatan.model.game.ops.ResourceCardOps.{assignResourcesFromNumber, stoleResourceCard} -import scatan.model.game.ops.RobberOps.moveRobber -import scatan.model.game.ops.TradeOps.{tradeWithBank, tradeWithPlayer} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.BuildingOps.{assignBuilding, build} +import scatan.model.game.state.ops.DevelopmentCardOps.* +import scatan.model.game.state.ops.ResourceCardOps.* +import scatan.model.game.state.ops.RobberOps.moveRobber +import scatan.model.game.state.ops.TradeOps.{tradeWithBank, tradeWithPlayer} import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} object ScatanEffects: + /** An effect that does nothing + * @tparam A + * The type of the action + * @return + * An effect that does nothing + */ def EmptyEffect[A]: Effect[A, ScatanState] = (state: ScatanState) => Some(state) + /** Changes the current player to the next player. It's an empty effect because the player is changed in the game + * engine. + */ def NextTurnEffect(): Effect[NextTurn.type, ScatanState] = EmptyEffect + /** Assigns a building to a spot + * @param player + * The player who is building + * @param spot + * The spot to build on + * @return + * An effect that assigns a building to a spot + */ def AssignSettlementEffect(player: ScatanPlayer, spot: StructureSpot): Effect[AssignSettlement.type, ScatanState] = (state: ScatanState) => state.assignBuilding(spot, BuildingType.Settlement, player) + /** Assigns a road to a spot + * @param player + * The player who is building + * @param spot + * The spot to build on + * @return + * An effect that assigns a road to a spot + */ def AssignRoadEffect(player: ScatanPlayer, spot: RoadSpot): Effect[AssignRoad.type, ScatanState] = (state: ScatanState) => state.assignBuilding(spot, BuildingType.Road, player) + /** Roll the dice and assign resources + * @param result + * The result of the dice roll + * @return + * An effect that rolls the dice and assigns resources + */ def RollEffect(result: Int): Effect[RollDice.type, ScatanState] = (state: ScatanState) => require(result != 7, "Use RollSevenEffect for rolling a 7") state.assignResourcesFromNumber(result) + /** Roll a 7 + * @return + * An effect that rolls a 7 + */ def RollSevenEffect(): Effect[RollSeven.type, ScatanState] = EmptyEffect + /** Move the robber + * @param hex + * The hexagon to move the robber to + * @return + * An effect that moves the robber + */ def PlaceRobberEffect(hex: Hexagon): Effect[PlaceRobber.type, ScatanState] = (state: ScatanState) => state.moveRobber(hex) + /** Steal a card from a player + * @param currentPlayer + * The player who is stealing + * @param victim + * The player who is being stolen from + * @return + * An effect that steals a card from a player + */ def StealCardEffect(currentPlayer: ScatanPlayer, victim: ScatanPlayer): Effect[StealCard.type, ScatanState] = (state: ScatanState) => state.stoleResourceCard(currentPlayer, victim) @@ -39,20 +89,138 @@ object ScatanEffects: * Building Ops */ + /** Build a road + * @param spot + * The spot to build on + * @param player + * The player who is building + * @return + * An effect that builds a road + */ def BuildRoadEffect(spot: RoadSpot, player: ScatanPlayer): Effect[BuildRoad.type, ScatanState] = (state: ScatanState) => state.build(spot, BuildingType.Road, player) + /** Build a settlement + * @param spot + * The spot to build on + * @param player + * The player who is building + * @return + * An effect that builds a settlement + */ def BuildSettlementEffect(spot: StructureSpot, player: ScatanPlayer): Effect[BuildSettlement.type, ScatanState] = (state: ScatanState) => state.build(spot, BuildingType.Settlement, player) + /** Build a city + * @param spot + * The spot to build on + * @param player + * The player who is building + * @return + * An effect that builds a city + */ def BuildCityEffect(spot: StructureSpot, player: ScatanPlayer): Effect[BuildCity.type, ScatanState] = (state: ScatanState) => state.build(spot, BuildingType.City, player) + /** Buy a development card + * @param player + * The player who is buying + * @param turnNumber + * The turn number + * @return + * An effect that buys a development card + */ def BuyDevelopmentCardEffect(player: ScatanPlayer, turnNumber: Int): Effect[BuyDevelopmentCard.type, ScatanState] = (state: ScatanState) => state.buyDevelopmentCard(player, turnNumber) - def PlayDevelopmentCardEffect(): Effect[PlayDevelopmentCard.type, ScatanState] = EmptyEffect + /* + * Development Card Ops + */ + /** Play a knight development card + * @param player + * The player who is playing the card + * @param turnNumber + * The turn number + * @param robberPosition + * The position to move the robber to + * @return + * An effect that plays a knight development card + */ + def PlayKnightDevelopmentCardEffect( + player: ScatanPlayer, + turnNumber: Int, + robberPosition: Hexagon + ): Effect[PlayDevelopmentCard.type, ScatanState] = + (state: ScatanState) => state.playKnightDevelopment(player, robberPosition, turnNumber) + + /** Play a monopoly development card + * @param player + * The player who is playing the card + * @param turnNumber + * The turn number + * @param resourceType + * The resource type to monopolize + * @return + * An effect that plays a monopoly development card + */ + def PlayMonopolyDevelopmentCardEffect( + player: ScatanPlayer, + turnNumber: Int, + resourceType: ResourceType + ): Effect[PlayDevelopmentCard.type, ScatanState] = + (state: ScatanState) => state.playMonopolyDevelopment(player, resourceType, turnNumber) + + /** Play a year of plenty development card + * @param player + * The player who is playing the card + * @param turnNumber + * The turn number + * @param firstResourceType + * The first resource type to get + * @param secondResourceType + * The second resource type to get + * @return + * An effect that plays a year of plenty development card + */ + def PlayYearOfPlentyDevelopmentCardEffect( + player: ScatanPlayer, + turnNumber: Int, + firstResourceType: ResourceType, + secondResourceType: ResourceType + ): Effect[PlayDevelopmentCard.type, ScatanState] = + (state: ScatanState) => state.playYearOfPlentyDevelopment(player, firstResourceType, secondResourceType, turnNumber) + + /** Play a road building development card + * @param player + * The player who is playing the card + * @param turnNumber + * The turn number + * @param spot1 + * The first road spot to build on + * @param spot2 + * The second road spot to build on + * @return + * An effect that plays a road building development card + */ + def PlayRoadBuildingDevelopmentCardEffect( + player: ScatanPlayer, + turnNumber: Int, + spot1: RoadSpot, + spot2: RoadSpot + ): Effect[PlayDevelopmentCard.type, ScatanState] = + (state: ScatanState) => state.playRoadBuildingDevelopment(player, spot1, spot2, turnNumber) + + /** Play a victory point development card + * @param player + * The player who is playing the card + * @param offer + * The resource type to offer + * @param request + * The resource type to request + * @return + * An effect that plays a victory point development card + */ def TradeWithBankEffect( player: ScatanPlayer, offer: ResourceType, @@ -64,6 +232,17 @@ object ScatanEffects: request ) + /** Trade with a player + * @param sender + * The player who is sending the trade + * @param receiver + * The player who is receiving the trade + * @param senderCards + * The cards the sender is offering + * @param receiverCards + * The cards the receiver is offering + * @return + */ def TradeWithPlayerEffect( sender: ScatanPlayer, receiver: ScatanPlayer, diff --git a/src/main/scala/scatan/model/game/ScatanGame.scala b/src/main/scala/scatan/model/game/ScatanGame.scala index 1aa5080d..daee9f64 100644 --- a/src/main/scala/scatan/model/game/ScatanGame.scala +++ b/src/main/scala/scatan/model/game/ScatanGame.scala @@ -2,20 +2,18 @@ package scatan.model.game import scatan.lib.game.ops.Effect import scatan.lib.game.ops.GamePlayOps.{allowedActions, play} -import scatan.lib.game.ops.GameTurnOps.nextTurn import scatan.lib.game.ops.GameWinOps.{isOver, winner} import scatan.lib.game.{Game, GameStatus, Turn} -import scatan.model.components.{ResourceCard, ResourceType} +import scatan.model.components.ResourceType.{Rock, Sheep, Wheat} +import scatan.model.components.{DevelopmentType, ResourceCard, ResourceType} import scatan.model.game.ScatanEffects.* import scatan.model.game.config.ScatanActions.* import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} -import scatan.model.game.ops.RobberOps.playersOnRobber -import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.RobberOps.playersOnRobber +import scatan.model.map.{GameMap, Hexagon, RoadSpot, StructureSpot} import scala.util.Random -import scatan.model.GameMap -import scatan.model.components.ResourceCard -import scatan.model.game.ops.RobberOps.playersOnRobber /** The status of a game of Scatan. It contains all the data without any possible action. * @param game @@ -38,6 +36,31 @@ private trait ScatanGameActions extends ScatanGameStatus: private def play(action: ScatanActions)(using effect: Effect[action.type, ScatanState]): Option[ScatanGame] = game.play(action).map(ScatanGame.apply) + /** Checks if the player can build a road. + * @return + * true if the player can build a road, false otherwise + */ + def canBuyDevelopment: Boolean = + val required = Seq(Rock, Wheat, Sheep) + game.allowedActions.contains(BuyDevelopmentCard) && required.forall(resource => + game.state.resourceCards(game.turn.player).exists(_.resourceType == resource) + ) + + /** Checks if the player can play the given development. + * @param developmentType + * the development type + * @return + * true if the player can build a road, false otherwise + */ + def canPlayDevelopment(developmentType: DevelopmentType): Boolean = + game.allowedActions.contains(PlayDevelopmentCard) && game.state + .developmentCards(game.turn.player) + .exists(card => card.developmentType == developmentType && !card.played && card.drewAt.get < game.turn.number) + + /** Changes the turn to the next player. + * @return + * the new game, if the action is allowed + */ def nextTurn: Option[ScatanGame] = play(ScatanActions.NextTurn)(using NextTurnEffect()) @@ -45,12 +68,30 @@ private trait ScatanGameActions extends ScatanGameStatus: * Assign Ops */ + /** Assigns a settlement to the given spot. + * @param spot + * the spot + * @return + * the new game, if the action is allowed + */ def assignSettlement(spot: StructureSpot): Option[ScatanGame] = play(AssignSettlement)(using AssignSettlementEffect(game.turn.player, spot)) + /** Assigns a city to the given spot. + * @param road + * the spot + * @return + * the new game, if the action is allowed + */ def assignRoad(road: RoadSpot): Option[ScatanGame] = play(AssignRoad)(using AssignRoadEffect(game.turn.player, road)) + /** Rolls the dice and applies the effects. + * @param callback + * the callback to be called with the roll + * @return + * the new game, if the action is allowed + */ def rollDice(callback: Int => Unit = x => ()): Option[ScatanGame] = val roll = Random.nextInt(6) + 1 + Random.nextInt(6) + 1 callback(roll) @@ -60,6 +101,12 @@ private trait ScatanGameActions extends ScatanGameStatus: case _ => play(RollDice)(using RollEffect(roll)) + /** Places the robber on the given hex. + * @param hex + * the hex + * @return + * the new game, if the action is allowed + */ def placeRobber(hex: Hexagon): Option[ScatanGame] = def isPossibleToStealCard(game: ScatanGame): Boolean = game.playersOnRobber.filter(_ != game.turn.player).exists(game.state.resourceCards(_).sizeIs > 0) @@ -68,9 +115,19 @@ private trait ScatanGameActions extends ScatanGameStatus: game.skipStealCard case game => game + /** Skip the steal card action, by playing an empty effect. + * @return + * the new game, if the action is allowed + */ private[ScatanGameActions] def skipStealCard: Option[ScatanGame] = play(ScatanActions.StealCard)(using EmptyEffect) + /** Steals a card from the given player. + * @param player + * the player + * @return + * the new game, if the action is allowed + */ def stealCard(player: ScatanPlayer): Option[ScatanGame] = play(StealCard)(using StealCardEffect(this.game.turn.player, player)) @@ -78,23 +135,112 @@ private trait ScatanGameActions extends ScatanGameStatus: * Build Ops */ + /** Builds a road on the given spot. + * @param road + * the spot + * @return + * the new game, if the action is allowed + */ def buildRoad(road: RoadSpot): Option[ScatanGame] = play(ScatanActions.BuildRoad)(using BuildRoadEffect(road, game.turn.player)) + /** Builds a settlement on the given spot. + * @param spot + * the spot + * @return + * the new game, if the action is allowed + */ def buildSettlement(spot: StructureSpot): Option[ScatanGame] = play(ScatanActions.BuildSettlement)(using BuildSettlementEffect(spot, game.turn.player)) + /** Builds a city on the given spot. + * @param spot + * the spot + * @return + * the new game, if the action is allowed + */ def buildCity(spot: StructureSpot): Option[ScatanGame] = play(ScatanActions.BuildCity)(using BuildCityEffect(spot, game.turn.player)) + /** Buy a development card. + * @return + * the new game, if the action is allowed + */ def buyDevelopmentCard: Option[ScatanGame] = play(ScatanActions.BuyDevelopmentCard)(using BuyDevelopmentCardEffect(game.turn.player, game.turn.number)) - def playDevelopmentCard: Option[ScatanGame] = ??? + /** Plays a knight development card. + * @param robberPosition + * the position of the robber + * @return + * the new game, if the action is allowed + */ + def playKnightDevelopment(robberPosition: Hexagon): Option[ScatanGame] = + play(ScatanActions.PlayDevelopmentCard)(using + PlayKnightDevelopmentCardEffect(game.turn.player, game.turn.number, robberPosition) + ) + + /** Plays a monopoly development card. + * @param resourceType + * the resource type + * @return + * the new game, if the action is allowed + */ + def playMonopolyDevelopment(resourceType: ResourceType): Option[ScatanGame] = + play(ScatanActions.PlayDevelopmentCard)(using + PlayMonopolyDevelopmentCardEffect(game.turn.player, game.turn.number, resourceType) + ) + + /** Plays a year of plenty development card. + * @param firstResourceType + * the first resource type + * @param secondResourceType + * the second resource type + * @return + * the new game, if the action is allowed + */ + def playYearOfPlentyDevelopment( + firstResourceType: ResourceType, + secondResourceType: ResourceType + ): Option[ScatanGame] = + play(ScatanActions.PlayDevelopmentCard)(using + PlayYearOfPlentyDevelopmentCardEffect(game.turn.player, game.turn.number, firstResourceType, secondResourceType) + ) + + /** Plays a road building development card. + * @param firstRoad + * the first road + * @param secondRoad + * the second road + * @return + * the new game, if the action is allowed + */ + def playRoadBuildingDevelopment(firstRoad: RoadSpot, secondRoad: RoadSpot): Option[ScatanGame] = + play(ScatanActions.PlayDevelopmentCard)(using + PlayRoadBuildingDevelopmentCardEffect(game.turn.player, game.turn.number, firstRoad, secondRoad) + ) + /** Trades with the bank. + * @param offer + * the offer + * @param request + * the request + * @return + * the new game, if the action is allowed + */ def tradeWithBank(offer: ResourceType, request: ResourceType): Option[ScatanGame] = play(ScatanActions.TradeWithBank)(using TradeWithBankEffect(game.turn.player, offer, request)) + /** Trades with the player. + * @param receiver + * the receiver + * @param senderTradeCards + * the cards the sender gives + * @param receiverTradeCards + * the cards the receiver gives + * @return + * the new game, if the action is allowed + */ def tradeWithPlayer( receiver: ScatanPlayer, senderTradeCards: Seq[ResourceCard], diff --git a/src/main/scala/scatan/model/game/ScatanModelOps.scala b/src/main/scala/scatan/model/game/ScatanModelOps.scala index 91e1bc26..c43fcf16 100644 --- a/src/main/scala/scatan/model/game/ScatanModelOps.scala +++ b/src/main/scala/scatan/model/game/ScatanModelOps.scala @@ -6,6 +6,12 @@ import scatan.model.ApplicationState object ScatanModelOps: extension (model: Model[ApplicationState]) + /** Update the game state if there is a game in progress. + * @param update + * A function that takes the current game state and returns the next game state. + * @return + * The next game state if there is a game in progress, otherwise None. + */ def updateGame(update: ScatanGame => Option[ScatanGame]): Option[ScatanGame] = for game <- model.state.game @@ -15,6 +21,10 @@ object ScatanModelOps: nextGame extension (game: Option[ScatanGame]) + /** Run the callback if there is no game. + * @param callback + * The callback to run. + */ def onError(callback: => Unit): Unit = game match case None => callback case Some(_) => () diff --git a/src/main/scala/scatan/model/game/config/ScatanActions.scala b/src/main/scala/scatan/model/game/config/ScatanActions.scala index 1c0274b5..1e17e1b6 100644 --- a/src/main/scala/scatan/model/game/config/ScatanActions.scala +++ b/src/main/scala/scatan/model/game/config/ScatanActions.scala @@ -1,19 +1,58 @@ package scatan.model.game.config enum ScatanActions: + /** Change the current turn to the next player. + */ case NextTurn - // Setup + + /** Assign a settlement to a player. This is used during the initial placement phase. + */ case AssignSettlement + + /** Assign a road to a player. This is used during the initial placement phase. + */ case AssignRoad - // Game + + /** Roll the dice and distribute resources. This is used during the main game. + */ case RollDice + + /** Roll a seven. This is used during the main game to switch to the robber placement phase. + */ case RollSeven + + /** Place the robber. + */ case PlaceRobber + + /** Steal a card from a player. + */ case StealCard + + /** Build a road. + */ case BuildRoad + + /** Build a settlement. + */ case BuildSettlement + + /** Build a city. + */ case BuildCity + + /** Buy a development card. + */ case BuyDevelopmentCard + + /** Play a development card. + */ case PlayDevelopmentCard + + /** Trade with the bank. + */ case TradeWithBank + + /** Trade with a player. + */ case TradeWithPlayer diff --git a/src/main/scala/scatan/model/game/config/ScatanPhases.scala b/src/main/scala/scatan/model/game/config/ScatanPhases.scala index db3bed7f..9b45037b 100644 --- a/src/main/scala/scatan/model/game/config/ScatanPhases.scala +++ b/src/main/scala/scatan/model/game/config/ScatanPhases.scala @@ -1,5 +1,10 @@ package scatan.model.game.config enum ScatanPhases: + /** The initial phase of the game, where players are placing their initial settlements and roads. + */ case Setup + + /** The main phase of the game. + */ case Game diff --git a/src/main/scala/scatan/model/game/config/ScatanPlayer.scala b/src/main/scala/scatan/model/game/config/ScatanPlayer.scala index 1787349c..c5781010 100644 --- a/src/main/scala/scatan/model/game/config/ScatanPlayer.scala +++ b/src/main/scala/scatan/model/game/config/ScatanPlayer.scala @@ -1,5 +1,7 @@ package scatan.model.game.config +/** A player in a game of Scatan. + */ trait ScatanPlayer: def name: String diff --git a/src/main/scala/scatan/model/game/config/ScatanSteps.scala b/src/main/scala/scatan/model/game/config/ScatanSteps.scala index 21984551..429cd1b5 100644 --- a/src/main/scala/scatan/model/game/config/ScatanSteps.scala +++ b/src/main/scala/scatan/model/game/config/ScatanSteps.scala @@ -1,12 +1,30 @@ package scatan.model.game.config enum ScatanSteps: + /** The game is in the process of changing turns. This is a special state that is used to trigger turn change event. + */ case ChangingTurn + /** The game is in the initial setup phase. Players are placing their initial road. + */ case SetupRoad + + /** The game is in the initial setup phase. Players are placing their initial settlement. + */ case SetupSettlement + /** The game is in the game phase. This is when players start their turns. + */ case Starting + + /** The game is in the game phase. This is when players have to place the robber. + */ case PlaceRobber + + /** The game is in the game phase. This is when players have to steal a card. + */ case StealCard + + /** The game is in the game phase. This is when players are rolled the dice and are free to trade and build. + */ case Playing diff --git a/src/main/scala/scatan/model/game/ops/DevelopmentCardOps.scala b/src/main/scala/scatan/model/game/ops/DevelopmentCardOps.scala deleted file mode 100644 index 09bd7704..00000000 --- a/src/main/scala/scatan/model/game/ops/DevelopmentCardOps.scala +++ /dev/null @@ -1,85 +0,0 @@ -package scatan.model.game.ops - -import scatan.model.components.{DevelopmentCard, ResourceType} -import scatan.model.game.ScatanState -import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.AwardOps.awards - -object DevelopmentCardOps: - - extension (state: ScatanState) - /** Returns a new ScatanState with the given development card assigned to the given player. The development card is - * added to the player's list of development cards. The assigned awards are updated. - * - * @param player - * The player to assign the development card to. - * @param developmentCard - * The development card to assign to the player. - * @return - * Some(ScatanState) if the player added the development card, None otherwise - */ - def assignDevelopmentCard(player: ScatanPlayer, developmentCard: DevelopmentCard): Option[ScatanState] = - Some( - state.copy( - developmentCards = state.developmentCards.updated(player, state.developmentCards(player) :+ developmentCard), - assignedAwards = state.awards - ) - ) - - /** Buys a development card for a given player and returns a new ScatanState with the updated resource cards and - * development cards. The player must have the required resources to buy the development card. - * - * @param player, - * the player who is buying the development card - * @param turnNumber, - * the turn number when the development card was bought - * @return - * Some(ScatanState) if the player bought the development card, None otherwise - */ - def buyDevelopmentCard(player: ScatanPlayer, turnNumber: Int): Option[ScatanState] = - val requiredResources = Seq( - ResourceType.Wheat, - ResourceType.Sheep, - ResourceType.Rock - ) - val playerResources = state.resourceCards(player) - val hasRequiredResources = requiredResources.forall(playerResources.map(_.resourceType).contains) - if !hasRequiredResources then None - else - val card = state.developmentCardsDeck.headOption - val cardWithTurnNumber = card.map(_.copy(drewAt = Some(turnNumber))) - cardWithTurnNumber match - case Some(developmentCard) => - val updatedResources = requiredResources.foldLeft(playerResources)((resources, resource) => - resources.filterNot(_.resourceType == resource) - ) - Some( - state.copy( - resourceCards = state.resourceCards.updated(player, updatedResources), - developmentCards = - state.developmentCards.updated(player, state.developmentCards(player) :+ developmentCard), - developmentCardsDeck = state.developmentCardsDeck.tail - ) - ) - case None => None - - /** Consumes a development card for a given player and returns a new ScatanState with the updated development cards - * and assigned awards. - * @param player - * the player who is consuming the development card - * @param developmentCard - * the development card to be consumed - * @return - * Some(ScatanState) if the player has the development card, None otherwise - */ - def consumeDevelopmentCard(player: ScatanPlayer, developmentCard: DevelopmentCard): Option[ScatanState] = - if !state.developmentCards(player).contains(developmentCard) then None - else - val remainingCards = - state.developmentCards(player).filter(_.developmentType == developmentCard.developmentType).drop(1) - Some( - state.copy( - developmentCards = state.developmentCards.updated(player, remainingCards), - assignedAwards = state.awards - ) - ) diff --git a/src/main/scala/scatan/model/game/ScatanState.scala b/src/main/scala/scatan/model/game/state/ScatanState.scala similarity index 89% rename from src/main/scala/scatan/model/game/ScatanState.scala rename to src/main/scala/scatan/model/game/state/ScatanState.scala index abee127d..de3d6705 100644 --- a/src/main/scala/scatan/model/game/ScatanState.scala +++ b/src/main/scala/scatan/model/game/state/ScatanState.scala @@ -1,15 +1,11 @@ -package scatan.model.game +package scatan.model.game.state -import scatan.model.GameMap import scatan.model.components.* -import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap -import scatan.model.components.DevelopmentType.Knight import scatan.model.components.UnproductiveTerrain.Desert +import scatan.model.game.DevelopmentCardsDeck import scatan.model.game.config.ScatanPlayer import scatan.model.map.* -import scala.collection.mutable.ListMap - /** Represents the state of a Scatan game. * * @param players @@ -43,6 +39,7 @@ object ScatanState: /** Creates a new ScatanState with the specified players. * * @param players + * The players in the game. * @return * a new ScatanState with the specified players */ diff --git a/src/main/scala/scatan/model/game/ops/AwardOps.scala b/src/main/scala/scatan/model/game/state/ops/AwardOps.scala similarity index 75% rename from src/main/scala/scatan/model/game/ops/AwardOps.scala rename to src/main/scala/scatan/model/game/state/ops/AwardOps.scala index 1b84a7cc..574be0a1 100644 --- a/src/main/scala/scatan/model/game/ops/AwardOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/AwardOps.scala @@ -1,12 +1,17 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap -import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState +/** Operations on [[ScatanState]] related to the awards. + */ object AwardOps: + private val minimumRoadLengthForAward = 5 + private val minimumKnightsForAward = 3 + extension (state: ScatanState) /** Returns a map of the current awards and their respective players. The awards are Longest Road and Largest Army. * Longest Road is awarded to the player with the longest continuous road of at least 5 segments. Largest Army is @@ -32,8 +37,10 @@ object AwardOps: else playerWithLargestArmy ) Map( - Award(AwardType.LongestRoad) -> (if longestRoad._2 >= 5 then Some((longestRoad._1, longestRoad._2)) + Award(AwardType.LongestRoad) -> (if longestRoad._2 >= minimumRoadLengthForAward then + Some((longestRoad._1, longestRoad._2)) else precedentLongestRoad), - Award(AwardType.LargestArmy) -> (if largestArmy._2 >= 3 then Some((largestArmy._1, largestArmy._2)) + Award(AwardType.LargestArmy) -> (if largestArmy._2 >= minimumKnightsForAward then + Some((largestArmy._1, largestArmy._2)) else precedentLargestArmy) ) diff --git a/src/main/scala/scatan/model/game/ops/BuildingOps.scala b/src/main/scala/scatan/model/game/state/ops/BuildingOps.scala similarity index 78% rename from src/main/scala/scatan/model/game/ops/BuildingOps.scala rename to src/main/scala/scatan/model/game/state/ops/BuildingOps.scala index 0cd9aa6b..cf850b15 100644 --- a/src/main/scala/scatan/model/game/ops/BuildingOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/BuildingOps.scala @@ -1,13 +1,16 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.{AssignedBuildings, AssignmentInfo, BuildingType, Cost} -import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.AwardOps.* -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptyStructureSpot} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.AwardOps.* +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptyStructureSpots} import scatan.model.map.{RoadSpot, Spot, StructureSpot} +/** Operations on [[ScatanState]] related to buildings actions. + */ object BuildingOps: + extension (state: ScatanState) /** Verifies if a player has enough resources to pay a certain cost. @@ -20,8 +23,8 @@ object BuildingOps: * true if the player has enough resources to pay the cost, false otherwise */ private def verifyResourceCost(player: ScatanPlayer, cost: Cost): Boolean = - cost.foldLeft(true)((result, resourceCost) => - result && state.resourceCards(player).count(_.resourceType == resourceCost._1) >= resourceCost._2 + cost.forall(resourceCost => + state.resourceCards(player).count(_.resourceType == resourceCost._1) >= resourceCost._2 ) /** Builds a building of a certain type on a certain spot for a certain player. If the player has not enough @@ -29,8 +32,11 @@ object BuildingOps: * the cost of the building, the building is built and the resources are consumed. * * @param position + * the position of the building * @param buildingType + * the type of the building * @param player + * the player that builds the building * @return * Some(ScatanState) if the building is built, None otherwise */ @@ -56,8 +62,11 @@ object BuildingOps: * must contain a settlement of the same player. * * @param spot + * the spot to assign the building to * @param buildingType + * the type of the building * @param player + * the player that assigns the building * @return * Some(ScatanState) if the building is assigned, None otherwise */ @@ -102,18 +111,34 @@ object BuildingOps: else None case _ => None + /** The default rules for building a settlement. + * @param spot + * the spot where to build + * @param player + * the player that want to build + * @return + * true if the player can build a settlement on the spot, false otherwise + */ private def defaultRulesForSettlementBuilding(spot: StructureSpot, player: ScatanPlayer): Boolean = - state.emptyStructureSpot.contains(spot) + state.emptyStructureSpots.contains(spot) && state.gameMap.neighboursOf(spot).flatMap(state.assignedBuildings.get).isEmpty + /** The default rules for building a road. + * @param spot + * the spot where to build + * @param player + * the player that want to build + * @return + * true if the player can build a road on the spot, false otherwise + */ private def defaultRulesForRoadBuilding(spot: RoadSpot, player: ScatanPlayer): Boolean = val structureSpot1 = spot._1 val structureSpot2 = spot._2 - state.emptyRoadSpot.contains(spot) + state.emptyRoadSpots.contains(spot) && ( state.assignedBuildings .filter(s => s._1 == structureSpot1 || s._1 == structureSpot2) - .map(_._2) + .values .exists(p => p.player == player) || state.gameMap .edgesOfNodesConnectedBy(spot) diff --git a/src/main/scala/scatan/model/game/state/ops/DevelopmentCardOps.scala b/src/main/scala/scatan/model/game/state/ops/DevelopmentCardOps.scala new file mode 100644 index 00000000..6e7bcff2 --- /dev/null +++ b/src/main/scala/scatan/model/game/state/ops/DevelopmentCardOps.scala @@ -0,0 +1,186 @@ +package scatan.model.game.state.ops + +import scatan.model.components.BuildingType.Road +import scatan.model.components.DevelopmentType.* +import scatan.model.components.{DevelopmentCard, DevelopmentType, ResourceCard, ResourceType} +import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.AwardOps.awards +import scatan.model.game.state.ops.BuildingOps.build +import scatan.model.game.state.ops.ResourceCardOps.{assignResourceCard, removeResourceCard} +import scatan.model.game.state.ops.RobberOps.moveRobber +import scatan.model.map.{Hexagon, RoadSpot} + +/** Operations on [[ScatanState]] related to development cards actions. + */ +object DevelopmentCardOps: + + extension (state: ScatanState) + /** Returns a new ScatanState with the given development card assigned to the given player. The development card is + * added to the player's list of development cards. The assigned awards are updated. + * + * @param player + * The player to assign the development card to. + * @param developmentCard + * The development card to assign to the player. + * @return + * Some(ScatanState) if the player added the development card, None otherwise + */ + def assignDevelopmentCard(player: ScatanPlayer, developmentCard: DevelopmentCard): Option[ScatanState] = + Some( + state.copy( + developmentCards = state.developmentCards.updated(player, state.developmentCards(player) :+ developmentCard), + assignedAwards = state.awards + ) + ) + + /** Buys a development card for a given player and returns a new ScatanState with the updated resource cards and + * development cards. The player must have the required resources to buy the development card. + * + * @param player, + * the player who is buying the development card + * @param turnNumber, + * the turn number when the development card was bought + * @return + * Some(ScatanState) if the player bought the development card, None otherwise + */ + def buyDevelopmentCard(player: ScatanPlayer, turnNumber: Int): Option[ScatanState] = + val requiredResources = Seq( + ResourceType.Wheat, + ResourceType.Sheep, + ResourceType.Rock + ) + val playerResources = state.resourceCards(player) + val hasRequiredResources = requiredResources.forall(playerResources.map(_.resourceType).contains) + if !hasRequiredResources then None + else + val card = state.developmentCardsDeck.headOption + val cardWithTurnNumber = card.map(_.copy(drewAt = Some(turnNumber))) + cardWithTurnNumber match + case Some(developmentCard) => + val updatedResources = requiredResources.foldLeft(playerResources)((resources, resource) => + resources.filterNot(_.resourceType == resource) + ) + Some( + state.copy( + resourceCards = state.resourceCards.updated(player, updatedResources), + developmentCards = + state.developmentCards.updated(player, state.developmentCards(player) :+ developmentCard), + developmentCardsDeck = state.developmentCardsDeck.tail + ) + ) + case None => None + + /** Consumes a development card for a given player and returns a new ScatanState with the updated development cards + * and assigned awards. + * @param player + * the player who is consuming the development card + * @param developmentCard + * the development card to be consumed + * @return + * Some(ScatanState) if the player has the development card, None otherwise + */ + def consumeDevelopmentCard(player: ScatanPlayer, developmentCard: DevelopmentCard): Option[ScatanState] = + if !state.developmentCards(player).contains(developmentCard) then None + else + val remainingCards = + state.developmentCards(player).filter(_.developmentType == developmentCard.developmentType).drop(1) + Some( + state.copy( + developmentCards = state.developmentCards.updated(player, remainingCards), + assignedAwards = state.awards + ) + ) + + /** Consumes a development card for a given player and returns a new ScatanState with the updated development cards + * and assigned awards. + * + * @param player + * the player who is consuming the development card + * @param developmentCard + * the development card to be consumed + * @return + * Some(ScatanState) if the player has the development card, None otherwise + */ + def removeDevelopmentCard(player: ScatanPlayer, developmentCard: DevelopmentCard): Option[ScatanState] = + if !state.developmentCards(player).contains(developmentCard) then None + else + val remainingCards = state + .developmentCards(player) + .foldLeft((Seq.empty[DevelopmentCard], false)) { + case ((cards, false), card) if card == developmentCard => (cards, true) + case ((cards, removed), card) => (cards :+ card, removed) + } + ._1 + Some( + state.copy( + developmentCards = state.developmentCards.updated(player, remainingCards), + assignedAwards = state.awards + ) + ) + + private def playDevelopment( + player: ScatanPlayer, + developmentType: DevelopmentType, + turnNumber: Int + )(effect: ScatanState => Option[ScatanState]): Option[ScatanState] = + val stateWithCardConsumed = for + developmentCards <- state.developmentCards.get(player) + card <- developmentCards.find(card => + card.developmentType == developmentType && !card.played && card.drewAt.isDefined && card.drewAt.get < turnNumber + ) + stateWithCardConsumed <- state.removeDevelopmentCard(player, card) + newState <- + if card.developmentType == Knight then + stateWithCardConsumed.assignDevelopmentCard(player, card.copy(played = true)) + else Some(stateWithCardConsumed) + yield newState + stateWithCardConsumed.flatMap(effect) match + case None => Some(state) + case other => other + + def playKnightDevelopment(player: ScatanPlayer, robberPosition: Hexagon, turnNumber: Int): Option[ScatanState] = + playDevelopment(player, DevelopmentType.Knight, turnNumber)(_.moveRobber(robberPosition)) + + def playRoadBuildingDevelopment( + player: ScatanPlayer, + firstRoad: RoadSpot, + secondRoad: RoadSpot, + turnNumber: Int + ): Option[ScatanState] = + playDevelopment(player, DevelopmentType.RoadBuilding, turnNumber) { + _.build(firstRoad, Road, player).flatMap(_.build(secondRoad, Road, player)) + } + + def playMonopolyDevelopment( + player: ScatanPlayer, + resourceType: ResourceType, + turnNumber: Int + ): Option[ScatanState] = + playDevelopment(player, DevelopmentType.Monopoly, turnNumber) { newState => + val otherPlayers = newState.players.filterNot(_ == player) + otherPlayers.foldLeft(Option(newState))((optState, otherPlayer) => + optState.flatMap(state => + val resourceCards = state.resourceCards(otherPlayer) + val resourceCardsToSteal = resourceCards.filter(_.resourceType == resourceType) + resourceCardsToSteal.foldLeft(Option(state))((optState, resourceCard) => + optState.flatMap(state => + state + .removeResourceCard(otherPlayer, resourceCard) + .flatMap(_.assignResourceCard(player, resourceCard)) + ) + ) + ) + ) + } + + def playYearOfPlentyDevelopment( + player: ScatanPlayer, + firstResource: ResourceType, + secondResource: ResourceType, + turnNumber: Int + ): Option[ScatanState] = + playDevelopment(player, DevelopmentType.YearOfPlenty, turnNumber) { + _.assignResourceCard(player, ResourceCard(firstResource)) + .flatMap(_.assignResourceCard(player, ResourceCard(secondResource))) + } diff --git a/src/main/scala/scatan/model/game/ops/EmptySpotOps.scala b/src/main/scala/scatan/model/game/state/ops/EmptySpotOps.scala similarity index 83% rename from src/main/scala/scatan/model/game/ops/EmptySpotOps.scala rename to src/main/scala/scatan/model/game/state/ops/EmptySpotOps.scala index 3250b835..7854f119 100644 --- a/src/main/scala/scatan/model/game/ops/EmptySpotOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/EmptySpotOps.scala @@ -1,9 +1,11 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops -import scatan.model.game.ScatanState +import scatan.model.game.state.ScatanState import scatan.model.map.HexagonInMap.layer import scatan.model.map.{RoadSpot, Spot, StructureSpot} +/** Operations on [[ScatanState]] related to empty spots. + */ object EmptySpotOps: extension (state: ScatanState) @@ -22,7 +24,7 @@ object EmptySpotOps: * @return * the empty structure spots of the game map */ - def emptyStructureSpot: Seq[StructureSpot] = + def emptyStructureSpots: Seq[StructureSpot] = state.gameMap.nodes .filter(!state.assignedBuildings.isDefinedAt(_)) .filter(_.toSet.exists(_.layer <= state.gameMap.withTerrainLayers)) @@ -32,7 +34,7 @@ object EmptySpotOps: * @return * the empty road spots of the game map */ - def emptyRoadSpot: Seq[RoadSpot] = + def emptyRoadSpots: Seq[RoadSpot] = state.gameMap.edges .filter(!state.assignedBuildings.isDefinedAt(_)) .filter(_.toSet.forall(_.toSet.exists(_.layer <= state.gameMap.withTerrainLayers))) diff --git a/src/main/scala/scatan/model/game/ops/ResourceCardOps.scala b/src/main/scala/scatan/model/game/state/ops/ResourceCardOps.scala similarity index 92% rename from src/main/scala/scatan/model/game/ops/ResourceCardOps.scala rename to src/main/scala/scatan/model/game/state/ops/ResourceCardOps.scala index 668e58f5..35518f6b 100644 --- a/src/main/scala/scatan/model/game/ops/ResourceCardOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/ResourceCardOps.scala @@ -1,12 +1,19 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops -import scatan.model.components.{AssignedBuildings, BuildingType, ResourceCard, ResourceType} -import scatan.model.game.ScatanState +import scatan.model.components.* +import scatan.model.components.BuildingType.Road +import scatan.model.components.DevelopmentType.Knight import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.AwardOps.* +import scatan.model.game.state.ops.BuildingOps.build +import scatan.model.game.state.ops.RobberOps.moveRobber import scatan.model.map.{Hexagon, RoadSpot, StructureSpot, TileContent} import scala.util.Random +/** Operations on [[ScatanState]] related to resource cards. + */ object ResourceCardOps: extension (state: ScatanState) @@ -55,7 +62,9 @@ object ResourceCardOps: /** Removes a resource card from a player. * * @param player + * the player to remove the resource card from * @param resourceCard + * the resource card to remove * @return * Some(ScatanState) if the resource card was removed, None otherwise */ diff --git a/src/main/scala/scatan/model/game/ops/RobberOps.scala b/src/main/scala/scatan/model/game/state/ops/RobberOps.scala similarity index 89% rename from src/main/scala/scatan/model/game/ops/RobberOps.scala rename to src/main/scala/scatan/model/game/state/ops/RobberOps.scala index 4b56919a..7708baf8 100644 --- a/src/main/scala/scatan/model/game/ops/RobberOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/RobberOps.scala @@ -1,11 +1,13 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.AssignmentInfo -import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState import scatan.model.map.HexagonInMap.layer import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} +/** Operations on [[ScatanState]] related to robber's actions. + */ object RobberOps: extension (state: ScatanState) /** Returns a new ScatanState with the robber moved to the specified hexagon. The robber can only be moved to a diff --git a/src/main/scala/scatan/model/game/ops/ScoreOps.scala b/src/main/scala/scatan/model/game/state/ops/ScoreOps.scala similarity index 94% rename from src/main/scala/scatan/model/game/ops/ScoreOps.scala rename to src/main/scala/scatan/model/game/state/ops/ScoreOps.scala index ad67b96a..da70427f 100644 --- a/src/main/scala/scatan/model/game/ops/ScoreOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/ScoreOps.scala @@ -1,11 +1,13 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap -import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.AwardOps.* +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.AwardOps.* +/** Operations on [[ScatanState]] related to the scores. + */ object ScoreOps: extension (state: ScatanState) diff --git a/src/main/scala/scatan/model/game/ops/TradeOps.scala b/src/main/scala/scatan/model/game/state/ops/TradeOps.scala similarity index 70% rename from src/main/scala/scatan/model/game/ops/TradeOps.scala rename to src/main/scala/scatan/model/game/state/ops/TradeOps.scala index d7b279fa..d5d22129 100644 --- a/src/main/scala/scatan/model/game/ops/TradeOps.scala +++ b/src/main/scala/scatan/model/game/state/ops/TradeOps.scala @@ -1,14 +1,30 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.{ResourceCard, ResourceType} -import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.ResourceCardOps.{assignResourceCard, removeResourceCard} -import scatan.views.game.components.ContextMap.resources +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.ResourceCardOps.{assignResourceCard, removeResourceCard} +/** Operations on [[ScatanState]] related to trades. + */ object TradeOps: val tradeWithBankRequiredCards = 4 extension (state: ScatanState) + + /** Trade with a player. The sender must have the senderCards and the receiver must have the receiverCards The + * sender will give the senderCards to the receiver and vice versa + * + * @param sender + * the player that will trade with the receiver + * @param receiver + * the player that will trade with the sender + * @param senderCards + * the cards that the sender will give to the receiver + * @param receiverCards + * the cards that the receiver will give to the sender + * @return + * Some(state) if the trade is allowed, None otherwise + */ def tradeWithPlayer( sender: ScatanPlayer, receiver: ScatanPlayer, diff --git a/src/main/scala/scatan/model/GameMap.scala b/src/main/scala/scatan/model/map/GameMap.scala similarity index 58% rename from src/main/scala/scatan/model/GameMap.scala rename to src/main/scala/scatan/model/map/GameMap.scala index 5d246ce8..b527798f 100644 --- a/src/main/scala/scatan/model/GameMap.scala +++ b/src/main/scala/scatan/model/map/GameMap.scala @@ -1,8 +1,8 @@ -package scatan.model +package scatan.model.map +import scatan.model.components.Terrain import scatan.model.map.* import scatan.model.map.HexagonInMap.* -import scatan.model.components.Terrain /** Hexagonal tiled game map of Scatan. * @@ -11,18 +11,21 @@ import scatan.model.components.Terrain * * @param withSeaLayers * number of concentric circles of hexagons the terrain ones. + * + * @param tileContentStrategy + * strategy to generate the content of the tiles */ final case class GameMap( withTerrainLayers: Int = 2, withSeaLayers: Int = 1, - tileContentsStrategy: TileContentStrategy = TileContentStrategyFactory.fixedForLayer2 + tileContentStrategy: TileContentStrategy = TileContentStrategyFactory.fixedForLayer2 ) extends HexagonalTiledMap(withTerrainLayers + withSeaLayers) with MapWithTileContent: val totalLayers = withTerrainLayers + withSeaLayers val tileWithTerrain = tiles.toSeq.filter(_.layer <= withTerrainLayers) - override val toContent: Map[Hexagon, TileContent] = tileContentsStrategy(tileWithTerrain) + override val toContent: Map[Hexagon, TileContent] = tileContentStrategy(tileWithTerrain) override def equals(x: Any): Boolean = x match @@ -32,14 +35,26 @@ final case class GameMap( (this.toContent.toSet & that.toContent.toSet).sizeIs == this.toContent.size case _ => false +/** A factory to create game maps. + */ object GameMapFactory: + /** @return + * a fixed game map for layer 2 + */ def defaultMap: GameMap = - GameMap(tileContentsStrategy = TileContentStrategyFactory.fixedForLayer2) + GameMap(tileContentStrategy = TileContentStrategyFactory.fixedForLayer2) + /** @return + * a random game map for layer 2 + */ def randomMap: GameMap = - GameMap(tileContentsStrategy = TileContentStrategyFactory.randomForLayer2) + GameMap(tileContentStrategy = TileContentStrategyFactory.randomForLayer2) + + private val strategies: Iterator[TileContentStrategy] = TileContentStrategyFactory.permutationForLayer2.toIterator - val strategies: Iterator[TileContentStrategy] = TileContentStrategyFactory.permutationForLayer2.toIterator + /** @return + * the next permutation of the game map for layer 2 + */ def nextPermutation: GameMap = - GameMap(tileContentsStrategy = strategies.next()) + GameMap(tileContentStrategy = strategies.next()) diff --git a/src/main/scala/scatan/model/map/TileContent.scala b/src/main/scala/scatan/model/map/TileContent.scala index 93de0d7e..9f9646bd 100644 --- a/src/main/scala/scatan/model/map/TileContent.scala +++ b/src/main/scala/scatan/model/map/TileContent.scala @@ -15,16 +15,22 @@ trait MapWithTileContent: */ def toContent: Map[Hexagon, TileContent] +/** A configuration to generate the content of the tiles. + */ trait TileContentConfig: def numbers: Seq[Int] def terrains: Seq[Terrain] +/** A strategy to generate the content of the tiles. + */ type TileContentStrategy = Seq[Hexagon] => Map[Hexagon, TileContent] -/** A factory to create terrains. +/** A factory to create strategies to generate the content of the tiles. */ object TileContentStrategyFactory: + /** Configuration for the layer 2 of the map. + */ object ConfigForLayer2 extends TileContentConfig: val terrains: List[Terrain] = List( 1 * Desert, @@ -52,13 +58,24 @@ object TileContentStrategyFactory: .from(tiles.zip(tileContents)) .withDefaultValue(TileContent(Sea, None)) + /** A strategy that generates a fixed content for the layer 2 of the map. + * + * @return + * the strategy. + */ def fixedForLayer2: TileContentStrategy = import ConfigForLayer2.* given TileContentConfig = ConfigForLayer2 fromConfig + /** A strategy that generates a random content for the layer 2 of the map. + * + * @return + * the strategy. + */ def randomForLayer2: TileContentStrategy = import ConfigForLayer2.* + import scala.util.Random.shuffle given TileContentConfig with val terrains = shuffle(ConfigForLayer2.terrains) @@ -79,6 +96,11 @@ object TileContentStrategyFactory: pr <- permutations(r) yield e :: pr + /** A strategy that generates all the possible permutations of the content for the layer 2 of the map. + * + * @return + * the LazyList of strategies. + */ def permutationForLayer2: LazyList[TileContentStrategy] = import ConfigForLayer2.* for diff --git a/src/main/scala/scatan/model/map/UndirectedGraph.scala b/src/main/scala/scatan/model/map/UndirectedGraph.scala index 1d3dfcb1..2e1ffd3f 100644 --- a/src/main/scala/scatan/model/map/UndirectedGraph.scala +++ b/src/main/scala/scatan/model/map/UndirectedGraph.scala @@ -16,11 +16,35 @@ trait UndirectedGraph[Node, Edge <: UnorderedPair[Node]]: */ def edges: Set[Edge] +/** A mixin that add ops over a graph. + */ trait UndirectedGraphOps[Node, Edge <: UnorderedPair[Node]] extends UndirectedGraph[Node, Edge]: + /** Returns the set of edges that are connected to the given node. + * + * @param node + * the node to get the edges + * @return + * the edges that are connected to the given node + */ def edgesOf(node: Node): Set[Edge] = edges.filter(_.contains(node)) + /** Returns the set of nodes that are neighbours of the given node. + * + * @param node + * the node to get the neighbours + * @return + * the nodes that are neighbours of the given node + */ def neighboursOf(node: Node): Set[Node] = edgesOf(node).flatMap(_.toSet).filterNot(_ == node) + /** Returns the set of edges that are connected to the nodes those are connected by the given edge. + * + * @param edge + * the edge to start from + * + * @return + * the set of edges + */ def edgesOfNodesConnectedBy(edge: Edge): Set[Edge] = (edgesOf(edge._1) | edgesOf(edge._2)).filterNot(_ == edge) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 3f495e14..a57d6d48 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -1,15 +1,13 @@ package scatan.views.game import com.raquo.laminar.api.L.* -import org.scalajs.dom import scatan.controllers.game.GameController import scatan.lib.mvc.{BaseScalaJSView, View} import scatan.model.ApplicationState import scatan.model.game.config.ScatanPhases import scatan.views.game.components.* -import scatan.views.game.components.RightTabComponent.areTradeEnabled import scatan.views.utils.TypeUtils -import scatan.views.utils.TypeUtils.{Displayable, DisplayableSource} +import scatan.views.viewmodel.ScatanViewModel trait GameView extends View[ApplicationState] @@ -21,11 +19,12 @@ private class ScalaJsGameView(container: String, requirements: View.Requirements extends BaseScalaJSView[ApplicationState, GameController](container, requirements) with GameView: - given Signal[ApplicationState] = this.reactiveState - given GameController = this.controller + given ScatanViewModel = ScatanViewModel(this.reactiveState) + given GameViewClickHandler = GameViewClickHandler(this, controller) override def element: Element = div( + DevelopmentCardPopups.All, EndgameComponent.endgamePopup, div( className := LeftTabComponent.leftTabCssClass, diff --git a/src/main/scala/scatan/views/game/SetUpView.scala b/src/main/scala/scatan/views/game/SetUpView.scala index 7dd1f440..49b188d3 100644 --- a/src/main/scala/scatan/views/game/SetUpView.scala +++ b/src/main/scala/scatan/views/game/SetUpView.scala @@ -5,25 +5,17 @@ import scatan.Pages import scatan.controllers.game.SetUpController import scatan.lib.mvc.{BaseScalaJSView, View} import scatan.model.ApplicationState -import scatan.views.game.components.map.MapComponent -import scatan.model.GameMap -import MapSelectionMode.* +import scatan.model.map.{GameMap, GameMapFactory} +import scatan.views.game.MapSelectionMode.* import scatan.views.game.components.LeftTabComponent.buttonsComponent -import scatan.model.GameMapFactory +import scatan.views.game.components.MapComponent enum MapSelectionMode: case Default, Random, WithIterator /** This is the view for the setup page. */ -trait SetUpView extends View[ApplicationState]: - /** This method is called when the user clicks the start button. - */ - def switchToGame(): Unit - - /** This method is called when the user clicks the back button. - */ - def switchToHome(): Unit +trait SetUpView extends View[ApplicationState] object SetUpView: def apply(container: String, requirements: View.Requirements[SetUpController]): SetUpView = @@ -40,13 +32,13 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement extends BaseScalaJSView[ApplicationState, SetUpController](container, requirements) with SetUpView: - val numberOfUsers: Var[Int] = Var(3) - val reactiveNumberOfUsers: Signal[Int] = numberOfUsers.signal + private val numberOfUsers: Var[Int] = Var(3) + private val reactiveNumberOfUsers: Signal[Int] = numberOfUsers.signal val mapSelectionMode: Var[MapSelectionMode] = Var(MapSelectionMode.Default) val reactiveGameMap: Var[GameMap] = Var(GameMapFactory.defaultMap) - def changeMap: Unit = + private def changeMap: Unit = mapSelectionMode.now() match case Default => reactiveGameMap.set(GameMapFactory.defaultMap) @@ -58,7 +50,7 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement private def validateNames(usernames: String*) = usernames.forall(_.matches(".*\\S.*")) - override def switchToGame(): Unit = + private def switchToGame(): Unit = val usernames = for i <- 1 to numberOfUsers.now() yield document @@ -71,7 +63,7 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement this.controller.startGame(reactiveGameMap.now(), usernames*) this.navigateTo(Pages.Game) - override def switchToHome(): Unit = + private def switchToHome(): Unit = this.navigateTo(Pages.Home) override def element: Element = diff --git a/src/main/scala/scatan/views/game/components/CardsComponent.scala b/src/main/scala/scatan/views/game/components/CardsComponent.scala index 981180db..2fabdc0b 100644 --- a/src/main/scala/scatan/views/game/components/CardsComponent.scala +++ b/src/main/scala/scatan/views/game/components/CardsComponent.scala @@ -7,16 +7,11 @@ import scatan.model.components.DevelopmentType.* import scatan.model.components.ResourceType.* import scatan.model.game.* import scatan.model.game.config.* -import scatan.views.game.components.CardContextMap.{CardType, cardImages, countCardOf} +import scatan.views.game.components.CardContextMap.{CardType, cardImages} import scatan.views.utils.TypeUtils.* +import scatan.views.viewmodel.ops.ViewModelPlayersOps.cardCountOfCurrentPlayer object CardContextMap: - extension (state: ScatanState) - def countCardOf(player: ScatanPlayer)(cardType: CardType): Int = cardType match - case resourceType: ResourceType => - state.resourceCards(player).count(_.resourceType == resourceType) - case developmentType: DevelopmentType => - state.developmentCards(player).count(_.developmentType == developmentType) type CardType = ResourceType | DevelopmentType @@ -34,6 +29,11 @@ object CardContextMap: ) object CardsComponent: + + /** Display the cards of the current player. + * @return + * the component + */ def cardsComponent: DisplayableSource[Element] = div( cls := "game-view-card-container", @@ -41,27 +41,33 @@ object CardsComponent: cardCountComponent(cardImages.collect { case (k: DevelopmentType, v) => (k, v) }) ) + /** Display the given cards with the given images paths. + * @param cards + * the cards to display with their images paths + * @return + * the component + */ private def cardCountComponent(cards: Map[CardType, String]): DisplayableSource[Element] = div( cls := "game-view-child-container", for (cardType, path) <- cards.toList yield div( cls := "game-view-card-item", - onClick --> (_ => gameController.clickCard(cardType)), + onClick --> { _ => clickHandler.onCardClick(cardType) }, div( cls := "game-view-card-count", - child.text <-- reactiveState.map(state => - (for - game <- state.game - currentPlayer = game.turn.player - resourceCount = game.state.countCardOf(currentPlayer)(cardType) - yield resourceCount).getOrElse(0) - ) + child.text <-- gameViewModel.cardCountOfCurrentPlayer(cardType).map(_.toString) ), cardImageBy(path) ) ) + /** Display the card image with the given path. + * @param path + * the path of the image + * @return + * the component + */ private def cardImageBy(path: String): Element = img( cls := "game-view-card", diff --git a/src/main/scala/scatan/views/game/components/DevelopmentCardPopups.scala b/src/main/scala/scatan/views/game/components/DevelopmentCardPopups.scala new file mode 100644 index 00000000..7ab8d3ad --- /dev/null +++ b/src/main/scala/scatan/views/game/components/DevelopmentCardPopups.scala @@ -0,0 +1,67 @@ +package scatan.views.game.components + +import com.raquo.laminar.api.L.* +import scatan.model.ApplicationState +import scatan.model.components.ResourceType +import scatan.views.utils.TypeUtils.{DisplayableSource, applicationViewModel, reactiveState} + +class Popup[P](title: String, options: Signal[ApplicationState] => Signal[Seq[P]]): + var onSelect: Option[P => Unit] = None + val toBeShown: Var[Boolean] = Var(false) + def show(onSelect: P => Unit): Unit = + this.onSelect = Some(onSelect) + toBeShown.writer.onNext(true) + def element: DisplayableSource[Element] = + div( + display <-- this.toBeShown.signal.map(if _ then "block" else "none"), + position.fixed, + top("50%"), + left("50%"), + transform := "translate(-50%, -50%)", + padding := "20px", + backgroundColor := "white", + boxShadow := "0 2px 10px rgba(0, 0, 0, 0.1)", + borderRadius := "5px", + zIndex := 1000, + div( + title, + textAlign.center, + fontSize := "20px", + margin := "10px" + ), + children <-- options(applicationViewModel.state).map(_.map { option => + button( + option.toString, + onClick.mapTo(option) --> { _ => + this.onSelect.foreach(_(option)); toBeShown.writer.onNext(false) + }, + margin := "10px" + ) + }) + ) + +object DevelopmentCardPopups: + + def All: Seq[DisplayableSource[Element]] = Seq( + FirstYearOfPlentyCardPopup, + SecondYearOfPlentyCardPopup, + MonopolyCardPopup + ).map(_.element) + + val FirstYearOfPlentyCardPopup = + new Popup( + title = "Select the first resource", + options = _ => Var(Seq(ResourceType.values.toSeq*)).signal + ) + + val SecondYearOfPlentyCardPopup = + new Popup( + title = "Select the second resource", + options = _ => Var(Seq(ResourceType.values.toSeq*)).signal + ) + + val MonopolyCardPopup = + new Popup( + title = "Select the resource to monopolize", + options = _ => Var(Seq(ResourceType.values.toSeq*)).signal + ) diff --git a/src/main/scala/scatan/views/game/components/EndgameComponent.scala b/src/main/scala/scatan/views/game/components/EndgameComponent.scala index 873f0a26..17ad1842 100644 --- a/src/main/scala/scatan/views/game/components/EndgameComponent.scala +++ b/src/main/scala/scatan/views/game/components/EndgameComponent.scala @@ -2,14 +2,18 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* import org.scalajs.dom -import scatan.views.utils.TypeUtils.{DisplayableSource, reactiveState} +import scatan.views.utils.TypeUtils.{DisplayableSource, gameViewModel, reactiveState} +import scatan.views.viewmodel.ops.ViewModelWinOps.{isEnded, winner, winnerName} object EndgameComponent: + + /** A popup that appears when the game is over + * @return + * the component + */ def endgamePopup: DisplayableSource[Element] = - val winnerSignal: Signal[Option[String]] = reactiveState.map(_.game.flatMap(_.winner.map(_.name))) div( - display <-- winnerSignal - .map(_.isDefined) + display <-- gameViewModel.isEnded .map(if _ then "block" else "none"), // Show or hide based on the presence of a winner position.fixed, top("50%"), @@ -20,7 +24,7 @@ object EndgameComponent: boxShadow := "0 2px 10px rgba(0, 0, 0, 0.1)", borderRadius := "5px", zIndex := 1000, - child.text <-- winnerSignal.map(_.getOrElse("No winner")), + child.text <-- gameViewModel.winnerName, br(), button( "Restart", diff --git a/src/main/scala/scatan/views/game/components/GameMapComponent.scala b/src/main/scala/scatan/views/game/components/GameMapComponent.scala index 794c3d33..9abeb164 100644 --- a/src/main/scala/scatan/views/game/components/GameMapComponent.scala +++ b/src/main/scala/scatan/views/game/components/GameMapComponent.scala @@ -1,172 +1,118 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* -import scatan.controllers.game.GameController -import scatan.model.components.ResourceType.* -import scatan.model.components.UnproductiveTerrain.* -import scatan.model.components.{AssignedBuildings, AssignmentInfo, BuildingType, Terrain} -import scatan.model.game.ScatanState +import scatan.model.components.{AssignmentInfo, BuildingType, Terrain} import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState import scatan.model.map.* -import scatan.model.{ApplicationState, GameMap} -import scatan.views.game.components.ContextMap.{toImgId, viewBuildingType, viewPlayer} +import scatan.views.game.components.MapComponent.{MapElement, radius, given} import scatan.views.utils.Coordinates import scatan.views.utils.Coordinates.* import scatan.views.utils.TypeUtils.* -object ContextMap: - - private var viewPlayers: Map[ScatanPlayer, String] = Map.empty - private val buildings: Map[BuildingType, String] = Map( - BuildingType.Settlement -> "S", - BuildingType.City -> "C" - ) - - val resources: Map[Terrain, String] = Map( - Wood -> "res/img/hexagonal/wood.jpg", - Sheep -> "res/img/hexagonal/sheep.jpg", - Wheat -> "res/img/hexagonal/wheat.jpg", - Rock -> "res/img/hexagonal/ore.jpg", - Brick -> "res/img/hexagonal/clay.jpg", - Desert -> "res/img/hexagonal/desert.jpg", - Sea -> "res/img/hexagonal/water.jpg" - ) - - private def updateAndGetPlayer(player: ScatanPlayer): String = - viewPlayers.get(player) match - case Some(viewPlayer) => viewPlayer - case None => - val viewPlayer = s"player-${viewPlayers.size + 1}" - viewPlayers = viewPlayers + (player -> viewPlayer) - viewPlayer - - extension (info: AssignmentInfo) - def viewPlayer: String = updateAndGetPlayer(info.player) - def viewBuildingType: String = buildings(info.buildingType) - - extension (terrain: Terrain) def toImgId: String = s"img-${terrain.toString.toLowerCase}" - /** A component to display the game map. */ object GameMapComponent: - private given hexSize: Int = 100 - private val radius = hexSize / 4 - private val svgCornersPoints: String = - (for - i <- 0 to 5 - angleDeg = 60 * i + 30 - angleRad = Math.PI / 180 * angleDeg - x = hexSize * math.cos(angleRad) - y = hexSize * math.sin(angleRad) - yield s"$x,$y").mkString(" ") - private val layersToCanvasSize: Int => Int = x => (2 * x * hexSize) + 50 + /** An anti corruption layer that maps the model concept to the view ones. + */ + private object ModelContextMapping: + private var viewPlayers: Map[ScatanPlayer, String] = Map.empty + private val buildings: Map[BuildingType, String] = Map( + BuildingType.Settlement -> "S", + BuildingType.City -> "C" + ) + private def updateAndGetPlayer(player: ScatanPlayer): String = + viewPlayers.get(player) match + case Some(viewPlayer) => viewPlayer + case None => + val viewPlayer = s"player-${viewPlayers.size + 1}" + viewPlayers = viewPlayers + (player -> viewPlayer) + viewPlayer + + extension (info: AssignmentInfo) + def viewPlayer: String = updateAndGetPlayer(info.player) + def viewBuildingType: String = buildings(info.buildingType) + + import ModelContextMapping.* + + /** Display the game map. + * @return + * the component. + */ def mapComponent: DisplayableSource[Element] = div( className := "game-view-game-tab", - child <-- reactiveState - .map(state => - (for - game <- state.game - state = game.state - yield - given ScatanState = state - getHexagonalMap - ).getOrElse(div("No game")) - ) + child <-- gameViewModel.state.map(_.state).map(gameHexagonalMap(using clickHandler)(using _)) ) - private def gameMap(using ScatanState): GameMap = state.gameMap - private def contentOf(hex: Hexagon)(using ScatanState): TileContent = state.gameMap.toContent(hex) + private def gameMap(using ScatanState): GameMap = scatanState.gameMap private def robberPlacement(using ScatanState): Hexagon = summon[ScatanState].robberPlacement private def assignmentInfoOf(spot: Spot)(using ScatanState): Option[AssignmentInfo] = summon[ScatanState].assignedBuildings.get(spot) - private def getHexagonalMap: InputSourceWithState[Element] = - val canvasSize = layersToCanvasSize(gameMap.totalLayers) - svg.svg( - svgImages, - svg.viewBox := s"-${canvasSize} -${canvasSize} ${2 * canvasSize} ${2 * canvasSize}", + private def gameHexagonalMap: InputSourceWithState[Element] = + given GameMap = gameMap + MapComponent.mapContainer( for hex <- gameMap.tiles.toList - yield svgHexagonWithNumber(hex), + yield svgHexagonWithCrossedNumber(hex), for road <- gameMap.edges.toList yield svgRoad(road), for spot <- gameMap.nodes.toList yield svgSpot(spot) ) - /** A svg hexagon. + /** A svg hexagon with a number inside. * * @param hex, * the hexagon * @return * the svg hexagon. */ - private def svgHexagonWithNumber(hex: Hexagon): InputSourceWithState[Element] = - val Coordinates(x, y) = hex.center - svg.g( - svg.transform := s"translate($x, $y)", - svg.polygon( - svg.points := svgCornersPoints, - svg.cls := "hexagon", - svg.fill := s"url(#${contentOf(hex).terrain.toImgId})" - ), - contentOf(hex).terrain match - case Sea => "" - case _ => circularNumberWithRobber(hex) - ) + private def svgHexagonWithCrossedNumber(hex: Hexagon): InputSourceWithState[MapElement] = + MapComponent.svgHexagon(hex, circularNumberWithRobber(hex)) - /** A svg circular number - * @param number, - * the number to display + /** A svg circular number with a robber cross. + * @param hex + * the hexagon * @return + * the component */ - private def circularNumberWithRobber(hex: Hexagon): InputSourceWithState[Element] = - svg.g( - svg.circle( - svg.cx := "0", - svg.cy := "0", - svg.r := s"$radius", - svg.className := "hexagon-center-circle" - ), - svg.text( - svg.x := "0", - svg.y := "0", - svg.fontSize := s"$radius", - svg.className := "hexagon-center-number", - contentOf(hex).number.map(_.toString).getOrElse("") - ), - onClick --> (_ => gameController.placeRobber(hex)), + private def circularNumberWithRobber(hex: Hexagon): InputSourceWithState[MapElement] = + MapComponent.circularNumber( + hex, + onClick --> (_ => clickHandler.onHexagonClick(hex)), if robberPlacement == hex then robberCross else "" ) + /** @return + * the Element representing the robber's cross on the game map. + */ private def robberCross: Element = svg.g( svg.className := "robber", svg.line( - svg.x1 := s"-${radius}", - svg.y1 := s"-${radius}", + svg.x1 := s"-$radius", + svg.y1 := s"-$radius", svg.x2 := s"$radius", svg.y2 := s"$radius" ), svg.line( - svg.x1 := s"-${radius}", - svg.y1 := s"${radius}", + svg.x1 := s"-$radius", + svg.y1 := s"$radius", svg.x2 := s"$radius", svg.y2 := s"-$radius" ) ) - /** Generate the road graphic - * @param spot1, - * the first spot - * @param spot2, - * the second spot + /** Create a svg road. + * @param road + * the road * @return - * the road graphic + * the svg road */ private def svgRoad(road: RoadSpot): InputSourceWithState[Element] = val Coordinates(x1, y1) = road._1.coordinates.get @@ -174,10 +120,10 @@ object GameMapComponent: val player = assignmentInfoOf(road).map(_.viewPlayer) svg.g( svg.line( - svg.x1 := s"${x1}", - svg.y1 := s"${y1}", - svg.x2 := s"${x2}", - svg.y2 := s"${y2}", + svg.x1 := s"$x1", + svg.y1 := s"$y1", + svg.x2 := s"$x2", + svg.y2 := s"$y2", svg.className := s"road ${player.getOrElse("")}" ), player match @@ -188,17 +134,15 @@ object GameMapComponent: svg.cy := s"${y1 + (y2 - y1) / 2}", svg.className := "road-center", svg.r := s"$radius", - onClick --> (_ => gameController.onRoadSpot(road)) + onClick --> (_ => clickHandler.onRoadClick(road)) ) ) - /** Generate the spot graphic - * @param x, - * the x coordinate of the spot - * @param y, - * the y coordinate of the spot + /** Create a svg spot for a structure. + * @param structure + * the spot * @return - * the spot graphic + * the svg spot */ private def svgSpot(structure: StructureSpot): InputSourceWithState[Element] = val Coordinates(x, y) = structure.coordinates.get @@ -206,36 +150,17 @@ object GameMapComponent: val structureType = assignmentInfoOf(structure).map(_.viewBuildingType) svg.g( svg.circle( - svg.cx := s"${x}", - svg.cy := s"${y}", + svg.cx := s"$x", + svg.cy := s"$y", svg.r := s"$radius", svg.className := s"${player.getOrElse("spot")}", - onClick --> (_ => gameController.onStructureSpot(structure)) + onClick --> (_ => clickHandler.onStructureClick(structure)) ), svg.text( - svg.x := s"${x}", - svg.y := s"${y}", + svg.x := s"$x", + svg.y := s"$y", svg.className := "spot-text", svg.fontSize := s"$radius", s"${structureType.getOrElse("")}" ) ) - - private val svgImages: Element = - svg.svg( - svg.defs( - for (terrain, path) <- ContextMap.resources.toList - yield svg.pattern( - svg.idAttr := terrain.toImgId, - svg.width := "100%", - svg.height := "100%", - svg.patternContentUnits := "objectBoundingBox", - svg.image( - svg.href := path, - svg.width := "1", - svg.height := "1", - svg.preserveAspectRatio := "none" - ) - ) - ) - ) diff --git a/src/main/scala/scatan/views/game/components/GameViewClickHandler.scala b/src/main/scala/scatan/views/game/components/GameViewClickHandler.scala new file mode 100644 index 00000000..ae354b43 --- /dev/null +++ b/src/main/scala/scatan/views/game/components/GameViewClickHandler.scala @@ -0,0 +1,171 @@ +package scatan.views.game.components + +import com.raquo.airstream.core.Signal +import scatan.controllers.game.GameController +import scatan.model.ApplicationState +import scatan.model.components.* +import scatan.model.components.DevelopmentType.{Knight, Monopoly, RoadBuilding, YearOfPlenty} +import scatan.model.game.ScatanGame +import scatan.model.game.config.ScatanPhases.{Game, Setup} +import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState +import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} +import scatan.views.game.GameView +import scatan.views.game.components.CardContextMap.CardType + +trait GameViewClickHandler: + /** Handles a click on a road spot. + * @param roadSpot + * the road spot that was clicked + */ + def onRoadClick(roadSpot: RoadSpot): Unit + + /** Handles a click on a structure spot. + * @param roadSpot + * the structure spot that was clicked + */ + def onStructureClick(structureSpot: StructureSpot): Unit + + /** Handles a click on an hexagon. + * @param roadSpot + * the hexagon that was clicked + */ + def onHexagonClick(hexagon: Hexagon): Unit + + /** Handles a click on the roll dice button. + */ + def onRollDiceClick(): Unit + + /** Handles a click on the buy development card button. + */ + def onBuyDevelopmentCardClick(): Unit + + /** Handles a click on the end turn button. + */ + def onEndTurnClick(): Unit + + /** Handles a click on the steal card button. + * @param player + * the player to steal a card from + */ + def onStealCardClick(player: ScatanPlayer): Unit + + /** Handles a click on a card. + * @param cardType + * the card that was clicked + */ + def onCardClick(cardType: CardType): Unit + + /** Handles a click on the trade with bank button. + * @param offer + * the resource type to offer + * @param request + * the resource type to request + */ + def onTradeWithBank(offer: ResourceType, request: ResourceType): Unit + + /** Handles a click on the trade with player button. + * @param receiver + * the player to trade with + * @param offer + * the resource type to offer + * @param request + * the resource type to request + */ + def onTradeWithPlayer( + receiver: ScatanPlayer, + offer: Map[ResourceType, Int], + request: Map[ResourceType, Int] + ): Unit + +object GameViewClickHandler: + def apply(view: GameView, gameController: GameController): GameViewClickHandler = + new GameViewClickHandler: + + def state: ApplicationState = gameController.state + def game: ScatanGame = state.game.get + + var playingKnight = false + var playingRoadBuilding = false + var roadBuildingRoads: Seq[RoadSpot] = Seq.empty + + override def onRoadClick(roadSpot: RoadSpot): Unit = + if playingRoadBuilding then + roadBuildingRoads = roadBuildingRoads :+ roadSpot + if roadBuildingRoads.sizeIs == 2 then + playingRoadBuilding = false + gameController.playRoadBuildingDevelopment(roadBuildingRoads.head, roadBuildingRoads.last) + roadBuildingRoads = Seq.empty + else view.displayMessage("Select another road") + else + game.gameStatus.phase match + case Setup => + gameController.assignRoad(roadSpot) + case Game => + gameController.buildRoad(roadSpot) + + override def onStructureClick(structureSpot: StructureSpot): Unit = + game.gameStatus.phase match + case Setup => + gameController.assignSettlement(structureSpot) + case Game => + val alreadyContainsSettlement = + game.state.assignedBuildings + .get(structureSpot) + .exists(_.buildingType == BuildingType.Settlement) + if alreadyContainsSettlement then gameController.buildCity(structureSpot) + else gameController.buildSettlement(structureSpot) + + override def onHexagonClick(hexagon: Hexagon): Unit = + if playingKnight then + playingKnight = false + gameController.playKnightDevelopment(hexagon) + else gameController.placeRobber(hexagon) + + override def onRollDiceClick(): Unit = + gameController.rollDice() + + override def onBuyDevelopmentCardClick(): Unit = + gameController.buyDevelopmentCard() + + override def onEndTurnClick(): Unit = + gameController.nextTurn() + + override def onStealCardClick(player: ScatanPlayer): Unit = + gameController.stealCard(player) + + override def onCardClick(cardType: CardType): Unit = + cardType match + case development: DevelopmentType => + if state.game.exists(_.canPlayDevelopment(development)) then + development match + case Knight => + playingKnight = true + view.displayMessage("Select a hexagon to place the robber") + case YearOfPlenty => + DevelopmentCardPopups.FirstYearOfPlentyCardPopup.show { first => + DevelopmentCardPopups.SecondYearOfPlentyCardPopup.show { second => + gameController.playYearOfPlentyDevelopment(first, second) + } + } + case Monopoly => + DevelopmentCardPopups.MonopolyCardPopup.show { resource => + gameController.playMonopolyDevelopment(resource) + } + case RoadBuilding => + view.displayMessage("Select two roads to build") + playingRoadBuilding = true + case _ => () + case _ => () + + override def onTradeWithBank(offer: ResourceType, request: ResourceType): Unit = + gameController.tradeWithBank(offer, request) + + override def onTradeWithPlayer( + receiver: ScatanPlayer, + offer: Map[ResourceType, Int], + request: Map[ResourceType, Int] + ): Unit = + val offerCards = offer.flatMap((resourceType, amount) => ResourceCard(resourceType) ** amount).toSeq + val requestCards = request.flatMap((resourceType, amount) => ResourceCard(resourceType) ** amount).toSeq + gameController.tradeWithPlayer(receiver, offerCards, requestCards) diff --git a/src/main/scala/scatan/views/game/components/LeftTabComponent.scala b/src/main/scala/scatan/views/game/components/LeftTabComponent.scala index 4e4c5a7d..572d133f 100644 --- a/src/main/scala/scatan/views/game/components/LeftTabComponent.scala +++ b/src/main/scala/scatan/views/game/components/LeftTabComponent.scala @@ -1,63 +1,64 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* -import scatan.controllers.game.GameController -import scatan.lib.mvc.ScalaJSView -import scatan.model.ApplicationState -import scatan.model.components.{Award, AwardType} -import scatan.model.game.ScatanState +import scatan.model.components.Award import scatan.model.game.config.ScatanActions -import scatan.model.game.ops.AwardOps.awards -import scatan.model.game.ops.ScoreOps.scores -import scatan.views.game.GameView -import scatan.views.utils.TypeUtils.{Displayable, DisplayableSource, gameController, reactiveState} +import scatan.views.utils.TypeUtils.* +import scatan.views.viewmodel.ops.ViewModelActionsOps.* +import scatan.views.viewmodel.ops.ViewModelCurrentStatusOps.* +import scatan.views.viewmodel.ops.ViewModelPlayersOps.* object LeftTabComponent: - extension (action: ScatanActions) def toViewAction: String = action.toString - def leftTabCssClass: String = "game-view-left-tab" + /** Displays the current player, their score, and the current phase and step of the game. + * @return + * the component + */ def currentPlayerComponent: Displayable[Element] = div( h2( className := "game-view-player", - child.text <-- reactiveState - .map("Current Player: " + _.game.map(_.turn.player.name).getOrElse("No player")) + child.text <-- gameViewModel.currentPlayer.map("Current Player: " + _.name) ), h2( className := "game-view-player-score", - child.text <-- reactiveState - .map("Score: " + _.game.map(game => game.state.scores(game.turn.player)).getOrElse("No score")) + child.text <-- gameViewModel.currentPlayerScore.map("Score: " + _) ), h2( className := "game-view-phase", - child.text <-- reactiveState - .map("Phase: " + _.game.map(_.gameStatus.phase.toString).getOrElse("No phase")) + child.text <-- gameViewModel.currentPhase.map("Phase: " + _) ), h2( className := "game-view-step", - child.text <-- reactiveState - .map("Step: " + _.game.map(_.gameStatus.step.toString).getOrElse("No step")) + child.text <-- gameViewModel.currentStep.map("Step: " + _) ) ) + extension (scatanAction: ScatanActions) + private def toDisplayable: String = + scatanAction.toString.split("(?=[A-Z])").map(_.capitalize).mkString(" ") + + /** Displays all the actions the current player can take. + * @return + * the component + */ def possibleMovesComponent: Displayable[Element] = div( className := "game-view-moves", "Possible moves:", ul( - children <-- reactiveState - .map(state => - for move <- state.game.map(_.allowedActions.toSeq).getOrElse(Seq.empty) - yield li(cls := "game-view-move", move.toViewAction) - ) + children <-- gameViewModel.allowedActions.split(_.ordinal) { (_, action, _) => + li(cls := "game-view-move", action.toDisplayable) + } ) ) - def isActionDisabled(action: ScatanActions): Displayable[Signal[Boolean]] = - reactiveState.map(_.game.exists(!_.allowedActions.contains(action))) - + /** Displays the buttons to perform actions. + * @return + * the component + */ def buttonsComponent: DisplayableSource[Element] = div( div( @@ -65,63 +66,54 @@ object LeftTabComponent: button( className := "game-view-button roll-dice-button", "Roll dice", - onClick --> { _ => gameController.rollDice() }, - disabled <-- isActionDisabled(ScatanActions.RollDice) + disabled <-- gameViewModel.isActionDisabled(ScatanActions.RollDice), + onClick --> { _ => clickHandler.onRollDiceClick() } ), button( className := "game-view-button buy-development-card-button", "Buy Dev. Card", - onClick --> { _ => gameController.buyDevelopmentCard() }, - disabled <-- isActionDisabled(ScatanActions.BuyDevelopmentCard) + onClick --> { _ => clickHandler.onBuyDevelopmentCardClick() }, + disabled <-- gameViewModel.canBuyDevelopment ), button( className := "game-view-button end-turn-button", "End Turn", - onClick --> { _ => gameController.nextTurn() }, - disabled <-- isActionDisabled(ScatanActions.NextTurn) + disabled <-- gameViewModel.isActionDisabled(ScatanActions.NextTurn), + onClick --> { _ => clickHandler.onEndTurnClick() } ) ), div( - StealCardPopup.userSelectionPopup(), className := "game-view-buttons", + StealCardPopup.userSelectionPopup(), button( className := "game-view-button steal-card-button", "Steal Card", onClick --> { _ => StealCardPopup.show() }, - disabled <-- isActionDisabled(ScatanActions.StealCard) + disabled <-- gameViewModel.isActionDisabled(ScatanActions.StealCard) ) ) ) + /** Displays the awards that have been given out so far, if any. + * @return + * the component + */ def awardsComponent: DisplayableSource[Element] = div( className := "awards", h2("Current awards"), - child <-- reactiveState - .map(state => - (for - game <- state.game - gameState = game.state - yield getCurrentAwards(gameState)) - .getOrElse(div("No game")) - ) - ) - private def getCurrentAwards(state: ScatanState): Element = - div( - ul( - li( - "Longest road: ", - state - .awards(Award(AwardType.LongestRoad)) - .map(award => s"${award._1.name} (${award._2} roads)") - .getOrElse("Nobody yet") - ), - li( - "Largest army: ", - state - .awards(Award(AwardType.LargestArmy)) - .map(award => s"${award._1.name} (${award._2} cards)") - .getOrElse("Nobody yet") + div( + ul( + children <-- gameViewModel.currentAwards.map(_.toSeq).split(_._1) { (award, opt, _) => + li( + award.toDisplayable, + opt._2.map((player, score) => s" ($player: $score)").getOrElse("Nobody Yet") + ) + } ) ) ) + + extension (award: Award) + private def toDisplayable: String = + award.awardType.toString.split("(?=[A-Z])").map(_.capitalize).mkString(" ") diff --git a/src/main/scala/scatan/views/game/components/MapComponent.scala b/src/main/scala/scatan/views/game/components/MapComponent.scala new file mode 100644 index 00000000..bbed0f3d --- /dev/null +++ b/src/main/scala/scatan/views/game/components/MapComponent.scala @@ -0,0 +1,149 @@ +package scatan.views.game.components + +import com.raquo.laminar.api.L.* +import com.raquo.laminar.nodes.ReactiveSvgElement +import org.scalajs.dom +import org.scalajs.dom.SVGGElement +import scatan.model.components.ResourceType.* +import scatan.model.components.{Terrain, UnproductiveTerrain} +import scatan.model.components.UnproductiveTerrain.* +import scatan.model.map.{GameMap, Hexagon, TileContent} +import scatan.views.utils.Coordinates +import scatan.views.utils.Coordinates.center + +/** A component to display the map of hexagons. + */ +object MapComponent: + + /** An anti corruption layer that maps the game map concept to the view ones. + */ + private object MapContextMapping: + /** Resources images. + */ + val resources: Map[Terrain, String] = Map( + Wood -> "res/img/hexagonal/wood.jpg", + Sheep -> "res/img/hexagonal/sheep.jpg", + Wheat -> "res/img/hexagonal/wheat.jpg", + Rock -> "res/img/hexagonal/ore.jpg", + Brick -> "res/img/hexagonal/clay.jpg", + Desert -> "res/img/hexagonal/desert.jpg", + Sea -> "res/img/hexagonal/water.jpg" + ) + + extension (terrain: Terrain) def toImgId: String = s"img-${terrain.toString.toLowerCase}" + + import MapContextMapping.* + + given hexSize: Int = 100 + val radius = hexSize / 4 + val svgCornersPoints: String = + (for + i <- 0 to 5 + angleDeg = 60 * i + 30 + angleRad = Math.PI / 180 * angleDeg + x = hexSize * math.cos(angleRad) + y = hexSize * math.sin(angleRad) + yield s"$x,$y").mkString(" ") + val layersToCanvasSize: Int => Int = x => (2 * x * hexSize) + 50 + + type LaminarElement = Modifier[ReactiveSvgElement[dom.svg.Element]] + + type MapElement = GameMap ?=> Element + private def gameMap(using GameMap): GameMap = summon[GameMap] + private def contentOf(hex: Hexagon)(using GameMap): TileContent = gameMap.toContent(hex) + + /** A map compoment composed by hexagons. + * @return + * the component. + */ + def map: MapElement = + mapContainer( + for hex <- gameMap.tiles.toList + yield svgHexagonWithNumber(hex) + ) + + /** Wrap the elements inside the container for map. + * @param elements + * to put inside the container. + * @return + * elements wrapped. + */ + private[components] def mapContainer( + elements: LaminarElement* + ): MapElement = + val canvasSize = layersToCanvasSize(gameMap.totalLayers) + svg.svg( + svgImages, + svg.viewBox := s"-${canvasSize} -${canvasSize} ${2 * canvasSize} ${2 * canvasSize}", + elements + ) + + private def svgHexagonWithNumber(hex: Hexagon): MapElement = + svgHexagon(hex, circularNumber(hex)) + + /** A svg representation of hexagon with terrain. + * @param hex, + * the hexagon + * @return + * the svg hexagon. + */ + private[components] def svgHexagon(hex: Hexagon, elements: LaminarElement*): MapElement = + val Coordinates(x, y) = hex.center + svg.g( + svg.transform := s"translate($x, $y)", + svg.polygon( + svg.points := svgCornersPoints, + svg.cls := "hexagon", + svg.fill := s"url(#${contentOf(hex).terrain.toImgId})" + ), + elements + ) + + /** A svg representation of a circular number. + * @param hex, + * the hexagon + * @return + * the svg circular number. + */ + private[components] def circularNumber( + hex: Hexagon, + elements: LaminarElement* + ): MapElement = + contentOf(hex).terrain match + case UnproductiveTerrain.Sea => svg.g() + case _ => + svg.g( + svg.circle( + svg.cx := "0", + svg.cy := "0", + svg.r := s"$radius", + svg.className := "hexagon-center-circle" + ), + svg.text( + svg.x := "0", + svg.y := "0", + svg.fontSize := s"$radius", + svg.className := "hexagon-center-number", + contentOf(hex).number.map(_.toString).getOrElse("") + ), + elements + ) + + private val svgImages: Element = + svg.svg( + svg.defs( + for (terrain, path) <- resources.toList + yield svg.pattern( + svg.idAttr := terrain.toImgId, + svg.width := "100%", + svg.height := "100%", + svg.patternContentUnits := "objectBoundingBox", + svg.image( + svg.href := path, + svg.width := "1", + svg.height := "1", + svg.preserveAspectRatio := "none" + ) + ) + ) + ) diff --git a/src/main/scala/scatan/views/game/components/RightTabComponent.scala b/src/main/scala/scatan/views/game/components/RightTabComponent.scala index 0945d762..fd6d9753 100644 --- a/src/main/scala/scatan/views/game/components/RightTabComponent.scala +++ b/src/main/scala/scatan/views/game/components/RightTabComponent.scala @@ -1,16 +1,16 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* -import scatan.controllers.game.GameController -import scatan.model.ApplicationState import scatan.model.components.ResourceType import scatan.model.game.ScatanGame import scatan.model.game.config.ScatanActions +import scatan.model.game.config.ScatanActions.TradeWithBank import scatan.views.utils.TypeUtils.* +import scatan.views.viewmodel.ops.ViewModelActionsOps.isActionEnabled object RightTabComponent: - private def resourceTypefromName(name: String): ResourceType = + private def resourceTypeFromName(name: String): ResourceType = name match case "Wood" => ResourceType.Wood case "Brick" => ResourceType.Brick @@ -31,12 +31,9 @@ object RightTabComponent: h2("Trade:"), tradePlayerComponent, tradeBankComponent, - visibility <-- areTradeEnabled.map(if _ then "visible" else "hidden") + visibility <-- gameViewModel.isActionEnabled(TradeWithBank).map(if _ then "visible" else "hidden") ) - private def areTradeEnabled: Displayable[Signal[Boolean]] = - reactiveState.map(_.game.map(_.allowedActions.contains(ScatanActions.TradeWithBank)).getOrElse(false)) - private def tradePlayerComponent: DisplayableSource[Element] = div( h3("Players:"), @@ -47,11 +44,7 @@ object RightTabComponent: ), div( className := "game-view-players", - child <-- reactiveState - .map(state => - (for game <- state.game - yield getPlayersList(game)).getOrElse(div("No game")) - ) + child <-- gameViewModel.state.map(getPlayersList) ) ) @@ -66,7 +59,7 @@ object RightTabComponent: button( className := "trade-bank-button", onClick --> (_ => - gameController.onTradeWithBank( + clickHandler.onTradeWithBank( bankTradeOffer.now(), bankTradeRequest.now() ) @@ -80,8 +73,8 @@ object RightTabComponent: div( className := "game-view-resource-type-choice", select( - onChange.mapToValue.map(resourceTypefromName(_)) --> changing, className := "game-view-resource-type-choice-select", + onChange.mapToValue.map(resourceTypeFromName) --> changing, // for each type of resource add an option for resource <- ResourceType.values yield option(resource.toString, value := resource.toString) @@ -97,7 +90,7 @@ object RightTabComponent: button( className := "trade-player-button", onClick --> (_ => - gameController.onTradeWithPlayer( + clickHandler.onTradeWithPlayer( player, playerTradeOffer.now(), playerTradeRequest.now() diff --git a/src/main/scala/scatan/views/game/components/StealCardPopup.scala b/src/main/scala/scatan/views/game/components/StealCardPopup.scala index b31da33c..f51af3f5 100644 --- a/src/main/scala/scatan/views/game/components/StealCardPopup.scala +++ b/src/main/scala/scatan/views/game/components/StealCardPopup.scala @@ -3,7 +3,8 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* import com.raquo.laminar.nodes.ReactiveHtmlElement import scatan.model.game.config.ScatanPlayer -import scatan.views.utils.TypeUtils.{DisplayableSource, gameController, reactiveState} +import scatan.views.utils.TypeUtils.{DisplayableSource, clickHandler, gameViewModel} +import scatan.views.viewmodel.ops.ViewModelPlayersOps.playersOnRobberExceptCurrent object StealCardPopup: @@ -11,13 +12,12 @@ object StealCardPopup: def show(): Unit = toBeShown.writer.onNext(true) + /** Displays a popup for selecting the user to steal a card from. + * @return + * the element. + */ def userSelectionPopup(): DisplayableSource[Element] = - val options: Signal[Seq[ScatanPlayer]] = reactiveState.map(_.game match - case Some(game) => - game.playersOnRobber - .filter(_ != game.turn.player) - case None => Nil - ) + val options: Signal[Seq[ScatanPlayer]] = gameViewModel.playersOnRobberExceptCurrent div( display <-- toBeShown.signal.map(if _ then "block" else "none"), cls := "popup", @@ -26,7 +26,7 @@ object StealCardPopup: player.name, onClick --> { _ => // Close the popup, you can implement your own logic here - gameController.stealCard(player) + clickHandler.onStealCardClick(player) toBeShown.writer.onNext(false) } ) diff --git a/src/main/scala/scatan/views/game/components/map/MapComponent.scala b/src/main/scala/scatan/views/game/components/map/MapComponent.scala deleted file mode 100644 index eacfc5f7..00000000 --- a/src/main/scala/scatan/views/game/components/map/MapComponent.scala +++ /dev/null @@ -1,87 +0,0 @@ -package scatan.views.game.components.map - -import scatan.model.GameMap -import com.raquo.laminar.api.L.* -import scatan.model.map.Hexagon -import scatan.views.utils.Coordinates.center -import scatan.views.utils.Coordinates -import scatan.views.game.components.ContextMap -import scatan.views.game.components.ContextMap.toImgId -import scatan.model.map.TileContent -import scatan.model.components.UnproductiveTerrain - -object MapComponent: - - private given hexSize: Int = 100 - private val radius = hexSize / 4 - private val svgCornersPoints: String = - (for - i <- 0 to 5 - angleDeg = 60 * i + 30 - angleRad = Math.PI / 180 * angleDeg - x = hexSize * math.cos(angleRad) - y = hexSize * math.sin(angleRad) - yield s"$x,$y").mkString(" ") - private val layersToCanvasSize: Int => Int = x => (2 * x * hexSize) + 50 - - def map: GameMap ?=> Element = - val gameMap = summon[GameMap] - val canvasSize = layersToCanvasSize(gameMap.totalLayers) - svg.svg( - svgImages, - svg.viewBox := s"-${canvasSize} -${canvasSize} ${2 * canvasSize} ${2 * canvasSize}", - for - hex <- gameMap.tiles.toList - content = gameMap.toContent(hex) - yield svgHexagon(hex, content) - ) - - private def svgHexagon(hex: Hexagon, content: TileContent): Element = - val Coordinates(x, y) = hex.center - svg.g( - svg.transform := s"translate($x, $y)", - svg.polygon( - svg.points := svgCornersPoints, - svg.cls := "hexagon", - svg.fill := s"url(#${content.terrain.toImgId})" - ), - content.terrain match - case UnproductiveTerrain.Sea => "" - case _ => circularNumber(hex, content) - ) - - def circularNumber(hex: Hexagon, content: TileContent): Element = - svg.g( - svg.circle( - svg.cx := "0", - svg.cy := "0", - svg.r := s"$radius", - svg.className := "hexagon-center-circle" - ), - svg.text( - svg.x := "0", - svg.y := "0", - svg.fontSize := s"$radius", - svg.className := "hexagon-center-number", - content.number.map(_.toString).getOrElse("") - ) - ) - - private val svgImages: Element = - svg.svg( - svg.defs( - for (terrain, path) <- ContextMap.resources.toList - yield svg.pattern( - svg.idAttr := terrain.toImgId, - svg.width := "100%", - svg.height := "100%", - svg.patternContentUnits := "objectBoundingBox", - svg.image( - svg.href := path, - svg.width := "1", - svg.height := "1", - svg.preserveAspectRatio := "none" - ) - ) - ) - ) diff --git a/src/main/scala/scatan/views/home/AboutView.scala b/src/main/scala/scatan/views/home/AboutView.scala index bb26c89b..b0f58cfe 100644 --- a/src/main/scala/scatan/views/home/AboutView.scala +++ b/src/main/scala/scatan/views/home/AboutView.scala @@ -1,7 +1,6 @@ package scatan.views.home import com.raquo.laminar.api.L.* -import scatan.Pages import scatan.controllers.home.AboutController import scatan.lib.mvc.{BaseScalaJSView, View} import scatan.model.ApplicationState diff --git a/src/main/scala/scatan/views/utils/Coordinate.scala b/src/main/scala/scatan/views/utils/Coordinate.scala index 8365348a..e92a5885 100644 --- a/src/main/scala/scatan/views/utils/Coordinate.scala +++ b/src/main/scala/scatan/views/utils/Coordinate.scala @@ -3,6 +3,7 @@ package scatan.views.utils import scatan.model.map.{Hexagon, StructureSpot} /** @param value + * the value of the double to wrap. */ final case class DoubleWithPrecision(value: Double): override def equals(x: Any): Boolean = diff --git a/src/main/scala/scatan/views/utils/TypeUtils.scala b/src/main/scala/scatan/views/utils/TypeUtils.scala index 070bf3ee..e1760153 100644 --- a/src/main/scala/scatan/views/utils/TypeUtils.scala +++ b/src/main/scala/scatan/views/utils/TypeUtils.scala @@ -1,21 +1,45 @@ package scatan.views.utils import com.raquo.airstream.core.Signal -import scatan.controllers.game.GameController import scatan.model.ApplicationState -import scatan.model.game.ScatanState +import scatan.model.game.state.ScatanState +import scatan.views.game.components.GameViewClickHandler +import scatan.views.viewmodel.{GameViewModel, ScatanViewModel} +/** Utils of types that can be useful in views. + */ object TypeUtils: - type Displayable[T] = Signal[ApplicationState] ?=> T - type InputSource[T] = GameController ?=> T + /** A type that can be displayed in the view. + */ + type Displayable[T] = ScatanViewModel ?=> T + + /** A type can be clicked. + */ + type InputSource[T] = GameViewClickHandler ?=> T + + /** A type that can be displayed in the view and clicked. + */ type DisplayableSource[T] = Displayable[InputSource[T]] - type StateKnoledge[T] = ScatanState ?=> T - type InputSourceWithState[T] = InputSource[StateKnoledge[T]] + + /** A type bring the knowledge of the game state. + */ + type GameStateKnowledge[T] = ScatanState ?=> T + + /** A type can be clicked enriched by the knowledge of the game state. + */ + type InputSourceWithState[T] = InputSource[GameStateKnowledge[T]] private[views] def reactiveState(using Signal[ApplicationState]): Signal[ApplicationState] = summon[Signal[ApplicationState]] - private[views] def gameController(using GameController): GameController = - summon[GameController] - private[views] def state(using ScatanState): ScatanState = + private[views] def clickHandler(using GameViewClickHandler): GameViewClickHandler = + summon[GameViewClickHandler] + private[views] def scatanState(using ScatanState): ScatanState = summon[ScatanState] + + private[views] def applicationViewModel(using ScatanViewModel): ScatanViewModel = + summon[ScatanViewModel] + + private[views] def gameViewModel(using scatanViewModel: ScatanViewModel): GameViewModel = + val reactiveGame = scatanViewModel.state.map(_.game) + GameViewModel(reactiveGame.map(_.get)) diff --git a/src/main/scala/scatan/views/viewmodel/ViewModel.scala b/src/main/scala/scatan/views/viewmodel/ViewModel.scala new file mode 100644 index 00000000..0d498153 --- /dev/null +++ b/src/main/scala/scatan/views/viewmodel/ViewModel.scala @@ -0,0 +1,12 @@ +package scatan.views.viewmodel + +import com.raquo.airstream.core.Signal +import scatan.model.ApplicationState +import scatan.model.game.ScatanGame + +trait ViewModel[S]: + def state: Signal[S] + +class ScatanViewModel(override val state: Signal[ApplicationState]) extends ViewModel[ApplicationState] + +class GameViewModel(override val state: Signal[ScatanGame]) extends ViewModel[ScatanGame] diff --git a/src/main/scala/scatan/views/viewmodel/ops/ViewModelActionsOps.scala b/src/main/scala/scatan/views/viewmodel/ops/ViewModelActionsOps.scala new file mode 100644 index 00000000..787ee085 --- /dev/null +++ b/src/main/scala/scatan/views/viewmodel/ops/ViewModelActionsOps.scala @@ -0,0 +1,21 @@ +package scatan.views.viewmodel.ops + +import com.raquo.airstream.core.Signal +import scatan.model.game.config.ScatanActions +import scatan.views.viewmodel.GameViewModel + +object ViewModelActionsOps: + + extension (gameViewModel: GameViewModel) + + def allowedActions: Signal[Seq[ScatanActions]] = + gameViewModel.state.map(_.allowedActions.toSeq) + + def isActionEnabled(action: ScatanActions): Signal[Boolean] = + allowedActions.map(_.contains(action)) + + def isActionDisabled(action: ScatanActions): Signal[Boolean] = + allowedActions.map(!_.contains(action)) + + def canBuyDevelopment: Signal[Boolean] = + gameViewModel.state.map(_.canBuyDevelopment) diff --git a/src/main/scala/scatan/views/viewmodel/ops/ViewModelCurrentStatusOps.scala b/src/main/scala/scatan/views/viewmodel/ops/ViewModelCurrentStatusOps.scala new file mode 100644 index 00000000..5f04bf56 --- /dev/null +++ b/src/main/scala/scatan/views/viewmodel/ops/ViewModelCurrentStatusOps.scala @@ -0,0 +1,22 @@ +package scatan.views.viewmodel.ops + +import com.raquo.airstream.core.Signal +import scatan.model.components.Awards +import scatan.model.game.config.{ScatanPhases, ScatanSteps} +import scatan.model.game.state.ScatanState +import scatan.views.viewmodel.GameViewModel + +object ViewModelCurrentStatusOps: + extension (gameViewModel: GameViewModel) + + def currentPhase: Signal[ScatanPhases] = + gameViewModel.state.map(_.gameStatus.phase) + + def currentStep: Signal[ScatanSteps] = + gameViewModel.state.map(_.gameStatus.step) + + def currentAwards: Signal[Awards] = + gameViewModel.state.map(_.state.assignedAwards) + + def currentState: Signal[ScatanState] = + gameViewModel.state.map(_.state) diff --git a/src/main/scala/scatan/views/viewmodel/ops/ViewModelPlayersOps.scala b/src/main/scala/scatan/views/viewmodel/ops/ViewModelPlayersOps.scala new file mode 100644 index 00000000..de9f03ae --- /dev/null +++ b/src/main/scala/scatan/views/viewmodel/ops/ViewModelPlayersOps.scala @@ -0,0 +1,35 @@ +package scatan.views.viewmodel.ops + +import com.raquo.airstream.core.Signal +import scatan.model.components.{DevelopmentType, ResourceType} +import scatan.model.game.config.ScatanPlayer +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.ScoreOps.scores +import scatan.views.game.components.CardContextMap.CardType +import scatan.views.viewmodel.GameViewModel + +object ViewModelPlayersOps: + + extension (gameViewModel: GameViewModel) + + def currentPlayer: Signal[ScatanPlayer] = + gameViewModel.state.map(_.turn.player) + + def currentPlayerScore: Signal[Int] = + gameViewModel.state.map(game => game.state.scores(game.turn.player)) + + def playersOnRobberExceptCurrent: Signal[Seq[ScatanPlayer]] = + gameViewModel.state.map(game => game.playersOnRobber.filter(_ != game.turn.player)) + + def cardCountOfCurrentPlayer(cardType: CardType): Signal[Int] = + for + game <- gameViewModel.state + player = game.turn.player + yield game.state.countCardOf(player)(cardType) + + extension (state: ScatanState) + private def countCardOf(player: ScatanPlayer)(cardType: CardType): Int = cardType match + case resourceType: ResourceType => + state.resourceCards(player).count(_.resourceType == resourceType) + case developmentType: DevelopmentType => + state.developmentCards(player).count(_.developmentType == developmentType) diff --git a/src/main/scala/scatan/views/viewmodel/ops/ViewModelWinOps.scala b/src/main/scala/scatan/views/viewmodel/ops/ViewModelWinOps.scala new file mode 100644 index 00000000..cf17b077 --- /dev/null +++ b/src/main/scala/scatan/views/viewmodel/ops/ViewModelWinOps.scala @@ -0,0 +1,18 @@ +package scatan.views.viewmodel.ops + +import com.raquo.airstream.core.Signal +import scatan.model.game.config.ScatanPlayer +import scatan.views.viewmodel.GameViewModel + +object ViewModelWinOps: + + extension (gameViewModel: GameViewModel) + + def winner: Signal[Option[ScatanPlayer]] = + gameViewModel.state.map(_.winner) + + def winnerName: Signal[String] = + winner.map(_.map(_.name).getOrElse("No one")) + + def isEnded: Signal[Boolean] = + winner.map(_.isDefined) diff --git a/src/test/scala/scatan/lib/game/GameTest.scala b/src/test/scala/scatan/lib/game/GameTest.scala index 3c266f1e..b5a66dff 100644 --- a/src/test/scala/scatan/lib/game/GameTest.scala +++ b/src/test/scala/scatan/lib/game/GameTest.scala @@ -1,7 +1,7 @@ package scatan.lib.game import scatan.BaseTest -import scatan.model.GameMap +import scatan.model.map.GameMap class GameTest extends BaseTest: diff --git a/src/test/scala/scatan/lib/game/RulesTest.scala b/src/test/scala/scatan/lib/game/RulesTest.scala index e708c09a..ea612e75 100644 --- a/src/test/scala/scatan/lib/game/RulesTest.scala +++ b/src/test/scala/scatan/lib/game/RulesTest.scala @@ -1,7 +1,7 @@ package scatan.lib.game import scatan.BaseTest -import scatan.model.GameMap +import scatan.model.map.GameMap class RulesTest extends BaseTest: @@ -14,12 +14,8 @@ class RulesTest extends BaseTest: Rules } - it should "have a empty ruleset" in { - Rules.empty - } - - it should "be validateble" in { - Rules.empty.valid shouldBe false + it should "be validatable" in { + emptyGameRules.valid shouldBe true } it should "have an initial state factory" in { diff --git a/src/test/scala/scatan/lib/game/TurnTest.scala b/src/test/scala/scatan/lib/game/TurnTest.scala index d202d294..61284997 100644 --- a/src/test/scala/scatan/lib/game/TurnTest.scala +++ b/src/test/scala/scatan/lib/game/TurnTest.scala @@ -1,7 +1,6 @@ package scatan.lib.game import scatan.BaseTest -import scatan.lib.game.ops.TurnOps.next import scatan.model.game.config.ScatanPlayer class TurnTest extends BaseTest: diff --git a/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala index 2dadcd76..27ecf343 100644 --- a/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala @@ -4,7 +4,7 @@ import scatan.BaseTest import scatan.lib.game.EmptyDomain.Actions.{NotPlayableAction, StartGame} import scatan.lib.game.ops.GamePlayOps.{canPlay, play} import scatan.lib.game.{EmptyDomain, Game} -import scatan.model.GameMap +import scatan.model.map.GameMap class GamePlayOpsTest extends BaseTest: diff --git a/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala index 39398a86..748e4069 100644 --- a/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala @@ -2,12 +2,9 @@ package scatan.lib.game.ops import scatan.BaseTest import scatan.lib.game.EmptyDomain.Actions.NextTurn -import scatan.lib.game.EmptyDomain.{EmptyDomainRules, MyPhases, Player} import scatan.lib.game.ops.GamePlayOps.play -import scatan.lib.game.ops.GameTurnOps.nextTurn -import scatan.lib.game.ops.RulesOps.{withNextPhase, withStartingStep} import scatan.lib.game.{EmptyDomain, Game, GameStatus} -import scatan.model.GameMap +import scatan.model.map.GameMap class GameTurnOpsTest extends BaseTest: diff --git a/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala index 42215b42..2c0b4fd0 100644 --- a/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala @@ -2,9 +2,8 @@ package scatan.lib.game.ops import scatan.BaseTest import scatan.lib.game.ops.GameWinOps.* -import scatan.lib.game.ops.RulesOps.withWinnerFunction import scatan.lib.game.{EmptyDomain, Game} -import scatan.model.GameMap +import scatan.model.map.GameMap class GameWinOpsTest extends BaseTest: @@ -15,13 +14,9 @@ class GameWinOpsTest extends BaseTest: "A Game" should "expose a isOver method" in { val game = Game(GameMap(), players) game.isOver shouldBe false - val endedGame = Game(GameMap(), players)(using EmptyDomain.rules.withWinnerFunction(_ => Some(players.head))) - endedGame.isOver shouldBe true } it should "expose a winner method" in { val game = Game(GameMap(), players) game.winner shouldBe None - val endedGame = Game(GameMap(), players)(using EmptyDomain.rules.withWinnerFunction(_ => Some(players.head))) - endedGame.winner shouldBe Some(players.head) } diff --git a/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala b/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala index 12f6ad56..d11e4804 100644 --- a/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala @@ -3,7 +3,7 @@ package scatan.lib.game.ops import scatan.BaseTest import scatan.lib.game.ops.RulesOps.* import scatan.lib.game.{EmptyDomain, GameStatus} -import scatan.model.GameMap +import scatan.model.map.GameMap class RulesOpsTest extends BaseTest: diff --git a/src/test/scala/scatan/model/ApplicationStateTest.scala b/src/test/scala/scatan/model/ApplicationStateTest.scala index 8c8e73c1..972f4f27 100644 --- a/src/test/scala/scatan/model/ApplicationStateTest.scala +++ b/src/test/scala/scatan/model/ApplicationStateTest.scala @@ -2,6 +2,7 @@ package scatan.model import scatan.BaseTest import scatan.lib.game.Game +import scatan.model.map.GameMap class ApplicationStateTest extends BaseTest: diff --git a/src/test/scala/scatan/model/GameTest.scala b/src/test/scala/scatan/model/GameTest.scala index 654c3a8a..10238668 100644 --- a/src/test/scala/scatan/model/GameTest.scala +++ b/src/test/scala/scatan/model/GameTest.scala @@ -5,11 +5,14 @@ import scatan.lib.game.ops.GamePlayOps.play import scatan.lib.game.ops.GameTurnOps.nextTurn import scatan.lib.game.ops.GameWinOps.{isOver, winner} import scatan.lib.game.{Game, GameStatus, Rules} +import scatan.model.game.ScatanDSL import scatan.model.game.ScatanEffects.{AssignRoadEffect, AssignSettlementEffect, NextTurnEffect} import scatan.model.game.config.ScatanActions.* import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptyStructureSpot} -import scatan.model.game.{ScatanDSL, ScatanState} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptyStructureSpots} + +import scatan.model.map.GameMap class GameTest extends BaseTest: @@ -75,11 +78,11 @@ class GameTest extends BaseTest: def nextTurn(game: ScatanGame): Option[ScatanGame] = for - structureSpot <- game.state.emptyStructureSpot.headOption + structureSpot <- game.state.emptyStructureSpots.headOption gameAfterBuildSettlement <- game.play(AssignSettlement)(using AssignSettlementEffect(game.turn.player, structureSpot) ) - roadSpot <- gameAfterBuildSettlement.state.emptyRoadSpot.headOption + roadSpot <- gameAfterBuildSettlement.state.emptyRoadSpots.headOption gameAfterBuildRoad <- gameAfterBuildSettlement.play(AssignRoad)(using AssignRoadEffect(gameAfterBuildSettlement.turn.player, roadSpot) ) diff --git a/src/test/scala/scatan/model/ScatanEffectsTest.scala b/src/test/scala/scatan/model/ScatanEffectsTest.scala index 53cf60ab..d2ea41ea 100644 --- a/src/test/scala/scatan/model/ScatanEffectsTest.scala +++ b/src/test/scala/scatan/model/ScatanEffectsTest.scala @@ -3,9 +3,10 @@ package scatan.model import scatan.BaseTest import scatan.model.components.{ResourceCard, ResourceType} import scatan.model.game.ScatanEffects.{NextTurnEffect, PlaceRobberEffect, RollEffect, TradeWithPlayerEffect} +import scatan.model.game.ScatanGame import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.ResourceCardOps.assignResourceCard -import scatan.model.game.{ScatanGame, ScatanState} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.ResourceCardOps.assignResourceCard class ScatanEffectsTest extends BaseTest: diff --git a/src/test/scala/scatan/model/game/BaseScatanStateTest.scala b/src/test/scala/scatan/model/game/BaseScatanStateTest.scala index ab9faace..bc54034c 100644 --- a/src/test/scala/scatan/model/game/BaseScatanStateTest.scala +++ b/src/test/scala/scatan/model/game/BaseScatanStateTest.scala @@ -3,8 +3,9 @@ package scatan.model.game import scatan.BaseTest import scatan.model.components.BuildingType import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptySpots} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.BuildingOps.assignBuilding +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptySpots} import scatan.model.map.{RoadSpot, Spot, StructureSpot} abstract class BaseScatanStateTest extends BaseTest: @@ -16,7 +17,7 @@ abstract class BaseScatanStateTest extends BaseTest: protected def noConstrainToBuildSettlment: ScatanState => ((StructureSpot, ScatanPlayer) => Boolean) = _ => (_, _) => true protected def spotShouldBeEmptyToBuildRoad: ScatanState => ((RoadSpot, ScatanPlayer) => Boolean) = s => - (r, _) => s.emptyRoadSpot.contains(r) + (r, _) => s.emptyRoadSpots.contains(r) // Avoid heavy check on spot type extension (state: ScatanState) diff --git a/src/test/scala/scatan/model/game/ScatanRulesTest.scala b/src/test/scala/scatan/model/game/ScatanRulesTest.scala index 27b04a0b..2e61c819 100644 --- a/src/test/scala/scatan/model/game/ScatanRulesTest.scala +++ b/src/test/scala/scatan/model/game/ScatanRulesTest.scala @@ -4,11 +4,12 @@ import org.scalatest.matchers.should.Matchers.shouldBe import scatan.BaseTest import scatan.lib.game.GameStatus import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} -import scatan.model.GameMap +import scatan.model.game.state.ScatanState +import scatan.model.map.GameMap class ScatanRulesTest extends BaseTest: - val rules = ScatanDSL.rules + private val rules = ScatanDSL.rules "The rules" should "be valid" in { rules.valid should be(true) @@ -20,7 +21,6 @@ class ScatanRulesTest extends BaseTest: it should "start with a Scatan State" in { val players = Seq(ScatanPlayer("a"), ScatanPlayer("b"), ScatanPlayer("c")) - val initialState = rules.startingStateFactory(GameMap(), players) initialState should be(ScatanState(players)) } diff --git a/src/test/scala/scatan/model/game/ScatanStateTest.scala b/src/test/scala/scatan/model/game/ScatanStateTest.scala index e6995fdf..98f2c83f 100644 --- a/src/test/scala/scatan/model/game/ScatanStateTest.scala +++ b/src/test/scala/scatan/model/game/ScatanStateTest.scala @@ -1,8 +1,8 @@ package scatan.model.game -import scatan.model.GameMap import scatan.model.components.{AssignmentInfo, Awards, DevelopmentCards, ResourceCards} -import scatan.model.map.{Hexagon, Spot} +import scatan.model.game.state.ScatanState +import scatan.model.map.{GameMap, Spot} import scala.language.postfixOps diff --git a/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/AwardOpsTest.scala similarity index 93% rename from src/test/scala/scatan/model/game/ops/AwardOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/AwardOpsTest.scala index e89455e9..bc089e0b 100644 --- a/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/AwardOpsTest.scala @@ -1,11 +1,12 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* -import scatan.model.game.ops.AwardOps.* -import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.DevelopmentCardOps.assignDevelopmentCard -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptyStructureSpot} -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.AwardOps.* +import scatan.model.game.state.ops.BuildingOps.assignBuilding +import scatan.model.game.state.ops.DevelopmentCardOps.assignDevelopmentCard +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptyStructureSpots} class AwardOpsTest extends BaseScatanStateTest: @@ -18,7 +19,7 @@ class AwardOpsTest extends BaseScatanStateTest: it should "assign a LongestRoad award if there are conditions" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val it = state.emptyRoadSpot.iterator + val it = state.emptyRoadSpots.iterator val stateWithAward = for stateWithOneRoad <- state.assignRoadWithoutRule(it.next, player1) stateWithTwoRoad <- stateWithOneRoad.assignRoadWithoutRule(it.next, player1) @@ -49,7 +50,7 @@ class AwardOpsTest extends BaseScatanStateTest: val state = ScatanState(threePlayers) val player1 = threePlayers.head val player2 = threePlayers.tail.head - val it = state.emptyRoadSpot.iterator + val it = state.emptyRoadSpots.iterator val firstStateWithAward = for stateWithOneRoad <- state.assignRoadWithoutRule(it.next, player1) stateWithTwoRoad <- stateWithOneRoad.assignRoadWithoutRule(it.next, player1) @@ -77,7 +78,7 @@ class AwardOpsTest extends BaseScatanStateTest: val state = ScatanState(threePlayers) val player1 = threePlayers.head val player2 = threePlayers.tail.head - val it = state.emptyRoadSpot.iterator + val it = state.emptyRoadSpots.iterator val firstStateWithAward = for stateWithOneRoad <- state.assignRoadWithoutRule(it.next, player1) stateWithTwoRoad <- stateWithOneRoad.assignRoadWithoutRule(it.next, player1) diff --git a/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/BuildingOpsTest.scala similarity index 94% rename from src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/BuildingOpsTest.scala index 8eb7c9b5..934170f9 100644 --- a/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/BuildingOpsTest.scala @@ -1,23 +1,24 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* +import scatan.model.game.BaseScatanStateTest import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ops.BuildingOps.{assignBuilding, build} -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptyStructureSpot} -import scatan.model.game.ops.ResourceCardOps.assignResourceCard -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.BuildingOps.{assignBuilding, build} +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptyStructureSpots} +import scatan.model.game.state.ops.ResourceCardOps.assignResourceCard import scatan.model.map.{RoadSpot, StructureSpot} class BuildingOpsTest extends BaseScatanStateTest: private def spotToBuildStructure(state: ScatanState): StructureSpot = - state.emptyStructureSpot.head + state.emptyStructureSpots.head private def spotToBuildRoad(state: ScatanState): RoadSpot = - state.emptyRoadSpot.head + state.emptyRoadSpots.head private def roadNearSpot(state: ScatanState, spot: StructureSpot): RoadSpot = - state.emptyRoadSpot.filter(_.contains(spot)).head + state.emptyRoadSpots.filter(_.contains(spot)).head "A State with buildings Ops" should "have empty buildings when state start" in { val state = ScatanState(threePlayers) @@ -182,7 +183,7 @@ class BuildingOpsTest extends BaseScatanStateTest: it should "not allow to assign a building if another is near" in { val state = ScatanState(threePlayers) val spot = spotToBuildStructure(state) - val anotherSpot = (state.gameMap.neighboursOf(spot) & state.emptyStructureSpot.toSet).head + val anotherSpot = (state.gameMap.neighboursOf(spot) & state.emptyStructureSpots.toSet).head val stateAssigned = state .assignBuilding(anotherSpot, BuildingType.Settlement, threePlayers.head) diff --git a/src/test/scala/scatan/model/game/ops/DevelopmentCardOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/DevelopmentCardOpsTest.scala similarity index 89% rename from src/test/scala/scatan/model/game/ops/DevelopmentCardOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/DevelopmentCardOpsTest.scala index 6c863b64..356fc0f3 100644 --- a/src/test/scala/scatan/model/game/ops/DevelopmentCardOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/DevelopmentCardOpsTest.scala @@ -1,10 +1,11 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.lib.game.Game import scatan.model.components.{DevelopmentCard, DevelopmentType, ResourceCard, ResourceType} -import scatan.model.game.ops.DevelopmentCardOps.{assignDevelopmentCard, buyDevelopmentCard, consumeDevelopmentCard} -import scatan.model.game.ops.ResourceCardOps.assignResourceCard -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.DevelopmentCardOps.* +import scatan.model.game.state.ops.ResourceCardOps.* class DevelopmentCardOpsTest extends BaseScatanStateTest: @@ -65,7 +66,7 @@ class DevelopmentCardOpsTest extends BaseScatanStateTest: case Some(state) => state.developmentCards(player1) should be(Seq(DevelopmentCard(DevelopmentType.Knight))) state.developmentCards(player2) should be(Seq.empty[DevelopmentCard]) - val stateWithDevCardConsumed = state.consumeDevelopmentCard(player1, DevelopmentCard(DevelopmentType.Knight)) + val stateWithDevCardConsumed = state.removeDevelopmentCard(player1, DevelopmentCard(DevelopmentType.Knight)) stateWithDevCardConsumed match case Some(state) => state.developmentCards(player1) should be(Seq.empty[DevelopmentCard]) @@ -82,7 +83,7 @@ class DevelopmentCardOpsTest extends BaseScatanStateTest: stateWithDevCardAssigned match case Some(state) => state.developmentCards(player1) should be(Seq(DevelopmentCard(DevelopmentType.Knight))) - val stateWithDevCardConsumed = state.consumeDevelopmentCard(player1, DevelopmentCard(DevelopmentType.Knight)) + val stateWithDevCardConsumed = state.removeDevelopmentCard(player1, DevelopmentCard(DevelopmentType.Knight)) stateWithDevCardConsumed match case Some(state) => state.developmentCards(player1) should be(Seq.empty[DevelopmentCard]) diff --git a/src/test/scala/scatan/model/game/ops/ResourceCardOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/ResourceCardOpsTest.scala similarity index 91% rename from src/test/scala/scatan/model/game/ops/ResourceCardOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/ResourceCardOpsTest.scala index 306dc120..2b1e83ad 100644 --- a/src/test/scala/scatan/model/game/ops/ResourceCardOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/ResourceCardOpsTest.scala @@ -1,10 +1,11 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.{BuildingType, ResourceCard, ResourceCards, ResourceType} -import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.EmptySpotOps.emptyStructureSpot -import scatan.model.game.ops.ResourceCardOps.* -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.BuildingOps.assignBuilding +import scatan.model.game.state.ops.EmptySpotOps.emptyStructureSpots +import scatan.model.game.state.ops.ResourceCardOps.* import scatan.model.map.HexagonInMap.layer import scatan.model.map.{RoadSpot, Spot, StructureSpot} import scatan.utils.UnorderedTriple @@ -76,7 +77,7 @@ class ResourceCardOpsTest extends BaseScatanStateTest: it should "assign a resource card to the player who has a settlement on a spot having that resource terrain" in { val state = ScatanState(threePlayers) val hexagonWithSheep = state.gameMap.toContent.filter(_._2.terrain == ResourceType.Sheep).head._1 - val spotWhereToBuild = state.emptyStructureSpot.filter(_.contains(hexagonWithSheep)).head + val spotWhereToBuild = state.emptyStructureSpots.filter(_.contains(hexagonWithSheep)).head val stateWithResources = for stateWithSettlement <- state.assignBuilding(spotWhereToBuild, BuildingType.Settlement, state.players.head) stateAfterRollDice <- stateWithSettlement.tryEveryRollDices() @@ -92,7 +93,7 @@ class ResourceCardOpsTest extends BaseScatanStateTest: it should "assign two resource cards to the player who has a city on a spot having that resource terrain" in { val state = ScatanState(threePlayers) val hexagonWithSheep = state.gameMap.toContent.filter(_._2.terrain == ResourceType.Sheep).head._1 - val spotWhereToBuild = state.emptyStructureSpot.filter(_.contains(hexagonWithSheep)).head + val spotWhereToBuild = state.emptyStructureSpots.filter(_.contains(hexagonWithSheep)).head val stateWithResources = for stateWithSettlement <- state.assignBuilding(spotWhereToBuild, BuildingType.Settlement, state.players.head) stateWithCity <- stateWithSettlement.assignBuilding(spotWhereToBuild, BuildingType.City, state.players.head) @@ -109,7 +110,7 @@ class ResourceCardOpsTest extends BaseScatanStateTest: it should "assign only the resource card corresponding to the last building placed after initial phase" in { val state = ScatanState(threePlayers) val hexagonWithSheep = state.gameMap.toContent.filter(_._2.terrain == ResourceType.Brick).head._1 - val spotWhereToBuild = state.emptyStructureSpot.filter(_.contains(hexagonWithSheep)).iterator + val spotWhereToBuild = state.emptyStructureSpots.filter(_.contains(hexagonWithSheep)).iterator // simulate initial placement val stateWithBuildings = state .assignSettlmentWithoutRule(spotWhereToBuild.next(), state.players.head) diff --git a/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/RobberOpsTest.scala similarity index 86% rename from src/test/scala/scatan/model/game/ops/RobberOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/RobberOpsTest.scala index 6d347ec1..8d23b9ff 100644 --- a/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/RobberOpsTest.scala @@ -1,8 +1,9 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.UnproductiveTerrain.Desert -import scatan.model.game.ops.RobberOps.moveRobber -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.RobberOps.moveRobber import scatan.model.map.Hexagon class RobberOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/ScoreOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/ScoreOpsTest.scala similarity index 85% rename from src/test/scala/scatan/model/game/ops/ScoreOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/ScoreOpsTest.scala index 09230e59..dc1d6531 100644 --- a/src/test/scala/scatan/model/game/ops/ScoreOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/ScoreOpsTest.scala @@ -1,11 +1,13 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* -import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.DevelopmentCardOps.assignDevelopmentCard -import scatan.model.game.ops.EmptySpotOps.{emptyRoadSpot, emptyStructureSpot} -import scatan.model.game.ops.ScoreOps.* -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.BuildingOps.assignBuilding +import scatan.model.game.state.ops.DevelopmentCardOps.assignDevelopmentCard +import scatan.model.game.state.ops.EmptySpotOps.{emptyRoadSpots, emptyStructureSpots} +import scatan.model.game.state.ops.ResourceCardOps.* +import scatan.model.game.state.ops.ScoreOps.* class ScoreOpsTest extends BaseScatanStateTest: @@ -17,7 +19,7 @@ class ScoreOpsTest extends BaseScatanStateTest: it should "increment score to one if assign a settlement" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val it = state.emptyStructureSpot.iterator + val it = state.emptyStructureSpots.iterator val stateWithSettlement = state.assignBuilding(it.next(), BuildingType.Settlement, player1) stateWithSettlement match case Some(state) => @@ -28,7 +30,7 @@ class ScoreOpsTest extends BaseScatanStateTest: it should "increment score to two if assign a city" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val it = state.emptyStructureSpot.iterator + val it = state.emptyStructureSpots.iterator val spotToBuild = it.next() val stateWithSettlement = state.assignBuilding(spotToBuild, BuildingType.Settlement, player1) stateWithSettlement match @@ -44,7 +46,7 @@ class ScoreOpsTest extends BaseScatanStateTest: it should "not increment score if assign a road" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val it = state.emptyRoadSpot.iterator + val it = state.emptyRoadSpots.iterator val stateWithRoad = state.assignRoadWithoutRule(it.next(), player1) stateWithRoad match case Some(state) => @@ -65,10 +67,10 @@ class ScoreOpsTest extends BaseScatanStateTest: it should "increment score if assign an award a building and a victory point" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val roadSpotIterator = state.emptyRoadSpot.iterator + val roadSpotIterator = state.emptyRoadSpots.iterator val stateWithSettlementAndAward = for - stateWithSettlement <- state.assignBuilding(state.emptyStructureSpot.head, BuildingType.Settlement, player1) + stateWithSettlement <- state.assignBuilding(state.emptyStructureSpots.head, BuildingType.Settlement, player1) oneRoadState <- stateWithSettlement.assignRoadWithoutRule(roadSpotIterator.next, player1) twoRoadState <- oneRoadState.assignRoadWithoutRule(roadSpotIterator.next, player1) threeRoadState <- twoRoadState.assignRoadWithoutRule(roadSpotIterator.next, player1) @@ -88,7 +90,7 @@ class ScoreOpsTest extends BaseScatanStateTest: it should "recognize if there is a winner" in { val state = ScatanState(threePlayers) val player1 = threePlayers.head - val it = state.emptyStructureSpot.iterator + val it = state.emptyStructureSpots.iterator val stateWithAWinner = for oneSettlementState <- state.assignSettlmentWithoutRule(it.next, player1) twoSettlementState <- oneSettlementState.assignSettlmentWithoutRule(it.next, player1) diff --git a/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala b/src/test/scala/scatan/model/game/state/ops/TradeOpsTest.scala similarity index 96% rename from src/test/scala/scatan/model/game/ops/TradeOpsTest.scala rename to src/test/scala/scatan/model/game/state/ops/TradeOpsTest.scala index c4e0b013..7473e675 100644 --- a/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala +++ b/src/test/scala/scatan/model/game/state/ops/TradeOpsTest.scala @@ -1,9 +1,10 @@ -package scatan.model.game.ops +package scatan.model.game.state.ops import scatan.model.components.* -import scatan.model.game.ops.ResourceCardOps.assignResourceCard -import scatan.model.game.ops.TradeOps.{tradeWithBank, tradeWithPlayer} -import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.BaseScatanStateTest +import scatan.model.game.state.ScatanState +import scatan.model.game.state.ops.ResourceCardOps.assignResourceCard +import scatan.model.game.state.ops.TradeOps.{tradeWithBank, tradeWithPlayer} class TradeOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/map/GameMapFactoryTest.scala b/src/test/scala/scatan/model/map/GameMapFactoryTest.scala index 28845fef..4c682514 100644 --- a/src/test/scala/scatan/model/map/GameMapFactoryTest.scala +++ b/src/test/scala/scatan/model/map/GameMapFactoryTest.scala @@ -2,8 +2,6 @@ package scatan.model.map import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scatan.BaseTest -import scatan.model.GameMap -import scatan.model.GameMapFactory class GameMapFactoryTest extends BaseTest with ScalaCheckPropertyChecks: diff --git a/src/test/scala/scatan/model/map/GameMapTest.scala b/src/test/scala/scatan/model/map/GameMapTest.scala index 1d0b25ad..42d7700e 100644 --- a/src/test/scala/scatan/model/map/GameMapTest.scala +++ b/src/test/scala/scatan/model/map/GameMapTest.scala @@ -2,7 +2,6 @@ package scatan.model.map import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scatan.BaseTest -import scatan.model.GameMap import scatan.model.components.UnproductiveTerrain.* import scatan.model.map.HexagonInMap.layer diff --git a/src/test/scala/scatan/model/map/GameMapWithGraphOps.scala b/src/test/scala/scatan/model/map/GameMapWithGraphOps.scala index 8cca28ea..070be03e 100644 --- a/src/test/scala/scatan/model/map/GameMapWithGraphOps.scala +++ b/src/test/scala/scatan/model/map/GameMapWithGraphOps.scala @@ -2,7 +2,6 @@ package scatan.model.map import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scatan.BaseTest -import scatan.model.GameMap import scatan.utils.{UnorderedPair, UnorderedTriple} class GameMapWithGraphOpsTest extends BaseTest with ScalaCheckPropertyChecks: