diff --git a/src/main/scala/scatan/controllers/game/GameController.scala b/src/main/scala/scatan/controllers/game/GameController.scala index 4fc6b92a..ded067b8 100644 --- a/src/main/scala/scatan/controllers/game/GameController.scala +++ b/src/main/scala/scatan/controllers/game/GameController.scala @@ -17,6 +17,7 @@ trait GameController extends Controller[ApplicationState]: def rollDice(): Unit def clickCard(card: CardType): Unit def placeRobber(hexagon: Hexagon): Unit + def buyDevelopmentCard(): Unit object GameController: def apply(requirements: Controller.Requirements[GameView, ApplicationState]): GameController = @@ -71,3 +72,8 @@ private class GameControllerImpl(requirements: Controller.Requirements[GameView, this.model .updateGame(_.buildSettlement(spot)) .onError(view.displayMessage("Cannot build settlement here")) + + override def buyDevelopmentCard(): Unit = + this.model + .updateGame(_.buyDevelopmentCard) + .onError(view.displayMessage("Cannot buy development card")) diff --git a/src/main/scala/scatan/lib/game/Rules.scala b/src/main/scala/scatan/lib/game/Rules.scala index 2e16a682..f8accef6 100644 --- a/src/main/scala/scatan/lib/game/Rules.scala +++ b/src/main/scala/scatan/lib/game/Rules.scala @@ -29,16 +29,16 @@ package scatan.lib.game * player of the game */ final case class Rules[State, P, S, A, Player]( + allowedPlayersSizes: Set[Int], startingStateFactory: Seq[Player] => State, startingPhase: P, startingSteps: Map[P, S], - actions: Map[GameStatus[P, S], Map[A, S]], - allowedPlayersSizes: Set[Int], - phaseTurnIteratorFactories: Map[P, Seq[Player] => Iterator[Player]], - nextPhase: Map[P, P] = Map.empty[P, P], endingSteps: Map[P, S], winnerFunction: State => Option[Player], - initialAction: Map[P, State => State] + initialAction: Map[P, State => State], + phaseTurnIteratorFactories: Map[P, Seq[Player] => Iterator[Player]], + nextPhase: Map[P, P] = Map.empty[P, P], + actions: Map[GameStatus[P, S], Map[A, S]] ): def valid: Boolean = startingStateFactory != null && diff --git a/src/main/scala/scatan/lib/game/dsl/GameDSL.scala b/src/main/scala/scatan/lib/game/dsl/GameDSL.scala index 1eb68934..3ed64fa5 100644 --- a/src/main/scala/scatan/lib/game/dsl/GameDSL.scala +++ b/src/main/scala/scatan/lib/game/dsl/GameDSL.scala @@ -1,24 +1,87 @@ package scatan.lib.game.dsl -/** A type-safe DSL for defining games. - * @tparam Player - * The type of player in the game. - * @tparam State - * The type of the game state. - * @tparam PhaseType - * The type of the phase of the game. - * @tparam StepType - * The type of the step of the game. - * @tparam ActionType - * The type of the action of the game. - */ -trait GameDSL: - type Player - type State - type PhaseType - type StepType - type ActionType +import scatan.lib.game.{GameStatus, Rules} - private val typedDSL = new TypedGameDSL[State, PhaseType, StepType, ActionType, Player] {} +object GameDSL: + import GameDSLDomain.{*, given} + import PropertiesDSL.{*, given} - export typedDSL.{isOver as _, winner as _, *} + export ops.GameCtxOps.* + export ops.PlayersCtxOps.* + export ops.PhaseCtxOps.* + export ops.StepCtxOps.* + + /** DSL for defining a game + * @tparam State + * The type of the game state + * @tparam Phase + * The type of the phases + * @tparam Step + * The type of the steps + * @tparam Actions + * The type of the actions + * @tparam Player + * The type of the players + */ + def Game[State, Phase, Step, Actions, Player]: ObjectBuilder[GameCtx[State, Phase, Step, Actions, Player]] = + ObjectBuilder() + + extension [State, P, S, A, Player](game: GameCtx[State, P, S, A, Player]) + /** Create the rules for the game + */ + def rules: Rules[State, P, S, A, Player] = + val ruless: Seq[Rules[State, P, S, A, Player]] = (for + startingStateFactory <- game.stateFactory + startingPhase <- game.initialPhase + winner <- game.winner + playersCtx <- game.players + allowedSizes <- playersCtx.allowedSizes + yield + val startingSteps: Map[P, S] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + startingStep <- phaseCtx.initialStep + yield phase -> startingStep).toMap + val endingSteps: Map[P, S] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + endingStep <- phaseCtx.endingStep + yield phase -> endingStep).toMap + val initialActions: Map[P, State => State] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + initialAction <- phaseCtx.onEnter + yield phase -> initialAction).toMap + val phaseTurnPlayerIteratorFactories: Map[P, Seq[Player] => Iterator[Player]] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + phaseTurnPlayerIteratorFactory <- phaseCtx.playerIteratorFactory + yield phase -> phaseTurnPlayerIteratorFactory).toMap + val nextPhases: Map[P, P] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + nextPhase <- phaseCtx.nextPhase + yield phase -> nextPhase).toMap + val actions: Map[GameStatus[P, S], Map[A, S]] = (for + phaseCtx <- game.phases + phase <- phaseCtx.phase + stepCtx <- phaseCtx.steps + step <- stepCtx.step + yield GameStatus(phase, step) -> + (for when <- stepCtx.when + yield when).toMap).toMap + Rules( + allowedPlayersSizes = allowedSizes.toSet, + startingStateFactory = startingStateFactory, + startingPhase = startingPhase, + startingSteps = startingSteps, + endingSteps = endingSteps, + winnerFunction = winner, + initialAction = initialActions, + phaseTurnIteratorFactories = phaseTurnPlayerIteratorFactories, + nextPhase = nextPhases, + actions = actions + ) + ).toSeq + require(ruless.sizeIs == 1, "Invalid rules") + ruless.headOption.get diff --git a/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala new file mode 100644 index 00000000..50db1a68 --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala @@ -0,0 +1,54 @@ +package scatan.lib.game.dsl + +private object GameDSLDomain: + + import PropertiesDSL.* + export Factories.given + + /** The game context is used to define the game. + */ + case class GameCtx[State, P, S, A, Player]( + phases: SequenceProperty[PhaseCtx[State, P, S, A, Player]] = SequenceProperty[PhaseCtx[State, P, S, A, Player]](), + players: OptionalProperty[PlayersCtx] = OptionalProperty[PlayersCtx](), + winner: OptionalProperty[State => Option[Player]] = OptionalProperty[State => Option[Player]](), + initialPhase: OptionalProperty[P] = OptionalProperty[P](), + stateFactory: OptionalProperty[Seq[Player] => State] = OptionalProperty[Seq[Player] => State]() + ) + + /** The players context is used to define the players info of the game. + */ + case class PlayersCtx(allowedSizes: OptionalProperty[Seq[Int]] = OptionalProperty[Seq[Int]]()) + + /** The phase context is used to define a phase of the game. + */ + case class PhaseCtx[State, Phase, Step, Action, Player]( + phase: OptionalProperty[Phase] = OptionalProperty[Phase](), + initialStep: OptionalProperty[Step] = OptionalProperty[Step](), + endingStep: OptionalProperty[Step] = OptionalProperty[Step](), + nextPhase: OptionalProperty[Phase] = OptionalProperty[Phase](), + onEnter: OptionalProperty[State => State] = OptionalProperty[State => State](), + steps: SequenceProperty[StepCtx[Phase, Step, Action]] = SequenceProperty[StepCtx[Phase, Step, Action]](), + playerIteratorFactory: OptionalProperty[Seq[Player] => Iterator[Player]] = + OptionalProperty[Seq[Player] => Iterator[Player]]() + ) + + /** The step context is used to define a step of the game. + */ + case class StepCtx[P, S, A]( + step: OptionalProperty[S] = OptionalProperty[S](), + when: SequenceProperty[(A, S)] = SequenceProperty[(A, S)]() + ) + + private object Factories: + + given [State, P, S, A, Player]: Factory[GameCtx[State, P, S, A, Player]] with + override def apply(): GameCtx[State, P, S, A, Player] = new GameCtx[State, P, S, A, Player] + + given Factory[PlayersCtx] with + override def apply(): PlayersCtx = new PlayersCtx + + given [State, Phase, Step, Action, Player]: Factory[PhaseCtx[State, Phase, Step, Action, Player]] with + override def apply(): PhaseCtx[State, Phase, Step, Action, Player] = PhaseCtx() + + given [P, S, A]: Factory[StepCtx[P, S, A]] with + override def apply(): StepCtx[P, S, A] = new StepCtx[P, S, A] diff --git a/src/main/scala/scatan/lib/game/dsl/PhaseDSLOps.scala b/src/main/scala/scatan/lib/game/dsl/PhaseDSLOps.scala deleted file mode 100644 index c8f8dc9f..00000000 --- a/src/main/scala/scatan/lib/game/dsl/PhaseDSLOps.scala +++ /dev/null @@ -1,38 +0,0 @@ -package scatan.lib.game.dsl - -import scatan.lib.game.GameStatus -import scatan.lib.game.dsl.TurnDSLOps.TurnDSLContext -import scatan.lib.game.ops.RulesOps.{withActions, withOnEnter} - -/** Operations for defining phases and steps in a game. - */ -object PhaseDSLOps: - case class PhaseDSLContext[State, PhaseType, StepType, ActionType, Player](phase: PhaseType)(using - val dsl: TypedGameDSL[State, PhaseType, StepType, ActionType, Player] - ): - def addStepToPhase(phase: PhaseType, step: StepType, actions: Map[ActionType, StepType]): Unit = - val status = GameStatus(phase, step) - dsl.rules = dsl.rules.withActions(status, actions) - def addOnEnterToPhase(phase: PhaseType, onEnter: State => State): Unit = - dsl.rules = dsl.rules.withOnEnter(phase, onEnter) - - def OnEnter[State, PhaseType, StepType, ActionType, Player]( - onEnter: State => State - )(using - phaseDSLContext: PhaseDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - phaseDSLContext.addOnEnterToPhase(phaseDSLContext.phase, onEnter) - - def Turn[State, PhaseType, StepType, ActionType, Player]( - init: TurnDSLContext[State, PhaseType, StepType, ActionType, Player] ?=> Unit - )(using - phaseDSLContext: PhaseDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - given TurnDSLContext[State, PhaseType, StepType, ActionType, Player] = - TurnDSLContext[State, PhaseType, StepType, ActionType, Player](phaseDSLContext.phase)(using phaseDSLContext.dsl) - init - - def When[State, PhaseType, StepType, ActionType, Player](step: StepType)(init: (ActionType, StepType)*)(using - phaseDSLContext: PhaseDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - phaseDSLContext.addStepToPhase(phaseDSLContext.phase, step, init.toMap) diff --git a/src/main/scala/scatan/lib/game/dsl/PhasesDSLOps.scala b/src/main/scala/scatan/lib/game/dsl/PhasesDSLOps.scala deleted file mode 100644 index a7f41510..00000000 --- a/src/main/scala/scatan/lib/game/dsl/PhasesDSLOps.scala +++ /dev/null @@ -1,21 +0,0 @@ -package scatan.lib.game.dsl - -import scatan.lib.game.dsl.PhaseDSLOps.PhaseDSLContext - -/** Operations for defining phases in a game. - */ -object PhasesDSLOps: - case class PhasesDSLContext[State, PhaseType, StepType, ActionType, Player]()(using - val dsl: TypedGameDSL[State, PhaseType, StepType, ActionType, Player] - ) - - def On[State, PhaseType, StepType, ActionType, Player](using - phasesDSLContext: PhasesDSLContext[State, PhaseType, StepType, ActionType, Player] - )( - phase: PhaseType - )( - init: PhaseDSLContext[State, PhaseType, StepType, ActionType, Player] ?=> Unit - ): Unit = - given context: PhaseDSLContext[State, PhaseType, StepType, ActionType, Player] = - PhaseDSLContext[State, PhaseType, StepType, ActionType, Player](phase)(using phasesDSLContext.dsl) - init(using context) diff --git a/src/main/scala/scatan/lib/game/dsl/PlayersDSLOps.scala b/src/main/scala/scatan/lib/game/dsl/PlayersDSLOps.scala deleted file mode 100644 index ea3330a2..00000000 --- a/src/main/scala/scatan/lib/game/dsl/PlayersDSLOps.scala +++ /dev/null @@ -1,25 +0,0 @@ -package scatan.lib.game.dsl - -import scatan.lib.game.ops.RulesOps.withAllowedPlayersSizes - -/** Operations for the players DSL. - */ -object PlayersDSLOps: - case class PlayersDSLContext[State, PhaseType, StepType, ActionType, Player]()(using - val dsl: TypedGameDSL[State, PhaseType, StepType, ActionType, Player] - ) - - def canBe[State, PhaseType, StepType, ActionType, Player](sizes: Set[Int])(using - playersDSLContext: PlayersDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - playersDSLContext.dsl.rules = playersDSLContext.dsl.rules.withAllowedPlayersSizes(sizes) - - def canBe[State, PhaseType, StepType, ActionType, Player](sizes: Int*)(using - playersDSLContext: PlayersDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - canBe(sizes.toSet) - - def canBe[State, PhaseType, StepType, ActionType, Player](sizes: Range)(using - playersDSLContext: PlayersDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - canBe(sizes.toSet) diff --git a/src/main/scala/scatan/lib/game/dsl/PropertiesDSL.scala b/src/main/scala/scatan/lib/game/dsl/PropertiesDSL.scala new file mode 100644 index 00000000..6b8ecc16 --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/PropertiesDSL.scala @@ -0,0 +1,103 @@ +package scatan.lib.game.dsl + +import scala.annotation.targetName + +object PropertiesDSL: + + // Properties + + /** An updatable property. + * @tparam P + * the type of the property + */ + sealed trait UpdatableProperty[P]: + def apply(newValue: P): Unit + + /** An optional property, which can be updated with a new value. + * @param value + * the current value of the property + * @tparam P + * the type of the property + */ + final case class OptionalProperty[P](var value: Option[P] = None) extends UpdatableProperty[P]: + override def apply(newValue: P): Unit = value = Some(newValue) + + /** A sequence property, in which new values can be added. + * @param value + * the new value to add to the sequence + * @tparam P + * the type of the property + */ + final case class SequenceProperty[P](var value: Seq[P] = Seq.empty[P]) extends UpdatableProperty[P]: + override def apply(newValue: P): Unit = value = value :+ newValue + + given [P]: Conversion[OptionalProperty[P], Iterable[P]] with + def apply(optionalProperty: OptionalProperty[P]): Iterable[P] = + optionalProperty.value.toList + + given [P]: Conversion[SequenceProperty[P], Iterable[P]] with + def apply(sequenceProperty: SequenceProperty[P]): Iterable[P] = + sequenceProperty.value + + // Setter + + /** A property setter, which can be used to update a property with a new value. + * @param property + * the property to update + * @tparam P + * the type of the property + */ + class PropertySetter[P](property: UpdatableProperty[P]): + @targetName("set") + def :=(value: P): Unit = property(value) + + given [P]: Conversion[UpdatableProperty[P], PropertySetter[P]] = PropertySetter(_) + + // Updater + + /** An object builder, which can be used to update an object. Basically, it is a function which takes an implicit + * parameter of type `P` and returns `Unit`. + */ + private type Builder[P] = P ?=> Unit + + trait Factory[P]: + def apply(): P + + /** A property builder. It creates a new object and build it with the given builder. The property is then updated with + * the built value. + * @param property + * the property to update + * @param factory$P$0 + * the factory to create a new object + * @tparam P + * the type of the property + */ + class PropertyBuilder[P: Factory](property: UpdatableProperty[P]): + def apply(builder: Builder[P]): Unit = + val obj = summon[Factory[P]].apply() + builder(using obj) + property(obj) + + given [P: Factory]: Conversion[UpdatableProperty[P], PropertyBuilder[P]] = PropertyBuilder(_) + + // Builder + + /** A property creator. It creates a new object and build it with the given builder. + * @param factory$P$0 + * the factory to create a new object + * @tparam P + * the type of the property + */ + class ObjectBuilder[P: Factory]: + def apply(builder: Builder[P]): P = + val obj = summon[Factory[P]].apply() + builder(using obj) + obj + + /** A contexted function, which impose a given context to the function. + * @tparam Ctx + * the required context + * @tparam P + * the type of the function + */ + type Contexted[Ctx, P] = Ctx ?=> P diff --git a/src/main/scala/scatan/lib/game/dsl/TurnDSLOps.scala b/src/main/scala/scatan/lib/game/dsl/TurnDSLOps.scala deleted file mode 100644 index 9504da4c..00000000 --- a/src/main/scala/scatan/lib/game/dsl/TurnDSLOps.scala +++ /dev/null @@ -1,36 +0,0 @@ -package scatan.lib.game.dsl - -import scatan.lib.game.ops.RulesOps.* - -/** Operations for defining the rules of a turn-based game. - */ -object TurnDSLOps: - case class TurnDSLContext[State, PhaseType, StepType, ActionType, Player](phase: PhaseType)(using - val dsl: TypedGameDSL[State, PhaseType, StepType, ActionType, Player] - ) - - def StartIn[State, PhaseType, StepType, ActionType, Player](step: StepType)(using - turnDSLContext: TurnDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - turnDSLContext.dsl.rules = turnDSLContext.dsl.rules.withStartingStep(turnDSLContext.phase, step) - - def CanEndIn[State, PhaseType, StepType, ActionType, Player]( - step: StepType - )(using turnDSLContext: TurnDSLContext[State, PhaseType, StepType, ActionType, Player]): Unit = - turnDSLContext.dsl.rules = turnDSLContext.dsl.rules.withEndingStep(turnDSLContext.phase, step) - - def Iterate[State, PhaseType, StepType, ActionType, Player](factory: Seq[Player] => Iterator[Player])(using - turnDSLContext: TurnDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - turnDSLContext.dsl.rules = turnDSLContext.dsl.rules.withPhaseTurnIteratorFactory(turnDSLContext.phase, factory) - - def NextPhase[State, PhaseType, StepType, ActionType, Player](phase: PhaseType)(using - turnDSLContext: TurnDSLContext[State, PhaseType, StepType, ActionType, Player] - ): Unit = - turnDSLContext.dsl.rules = turnDSLContext.dsl.rules.withNextPhase(turnDSLContext.phase, phase) - - def once[Player] = (players: Seq[Player]) => players.iterator - def normal[Player] = (players: Seq[Player]) => Iterator.continually(players).flatten - def reverse[Player] = (players: Seq[Player]) => Iterator.continually(players.reverse).flatten - def random[Player] = (players: Seq[Player]) => Iterator.continually(scala.util.Random.shuffle(players)).flatten - def circularWithBack[Player] = (players: Seq[Player]) => (players ++ players.reverse).iterator diff --git a/src/main/scala/scatan/lib/game/dsl/TypedGameDSL.scala b/src/main/scala/scatan/lib/game/dsl/TypedGameDSL.scala deleted file mode 100644 index f794d06b..00000000 --- a/src/main/scala/scatan/lib/game/dsl/TypedGameDSL.scala +++ /dev/null @@ -1,40 +0,0 @@ -package scatan.lib.game.dsl - -import scatan.lib.game.Rules -import scatan.lib.game.dsl.PhasesDSLOps.PhasesDSLContext -import scatan.lib.game.dsl.PlayersDSLOps.PlayersDSLContext -import scatan.lib.game.ops.RulesOps.* - -/** A DSL for defining a game. - * @tparam State - * The type of the game state. - * @tparam PhaseType - * The type of the game phase. - * @tparam StepType - * The type of the game step. - * @tparam ActionType - * The type of the game action. - * @tparam Player - * The type of the player. - */ -trait TypedGameDSL[State, PhaseType, StepType, ActionType, Player]: - var rules: Rules[State, PhaseType, StepType, ActionType, Player] = Rules.empty - given TypedGameDSL[State, PhaseType, StepType, ActionType, Player] = this - - def Players(init: PlayersDSLContext[State, PhaseType, StepType, ActionType, Player] ?=> Unit): Unit = - given PlayersDSLContext[State, PhaseType, StepType, ActionType, Player] = PlayersDSLContext() - init - - def Phases(init: PhasesDSLContext[State, PhaseType, StepType, ActionType, Player] ?=> Unit): Unit = - given PhasesDSLContext[State, PhaseType, StepType, ActionType, Player] = - PhasesDSLContext[State, PhaseType, StepType, ActionType, Player]() - init - - def Winner(winner: State => Option[Player]): Unit = - rules = rules.withWinnerFunction(winner) - - def StartWithPhase(phase: PhaseType): Unit = - rules = rules.withStartingPhase(phase) - - def StartWithStateFactory(stateFactory: Seq[Player] => State): Unit = - rules = rules.withStartingStateFactory(stateFactory) diff --git a/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala b/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala new file mode 100644 index 00000000..a3a3b210 --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala @@ -0,0 +1,35 @@ +package scatan.lib.game.dsl.ops + +import scatan.lib.game.dsl.GameDSLDomain.* +import scatan.lib.game.dsl.PropertiesDSL.* + +object GameCtxOps: + + /** Define the winner function, which is a function that takes a game state and returns the winner of the game, if + * any. + */ + def WinnerFunction[State, Player] + : Contexted[GameCtx[State, ?, ?, ?, Player], PropertySetter[State => Option[Player]]] = + ctx ?=> ctx.winner + + /** Define a phase of the game. + */ + def Phase[State, Phase, Step, Action, Player]: Contexted[GameCtx[State, Phase, Step, Action, Player], PropertyBuilder[ + PhaseCtx[State, Phase, Step, Action, Player] + ]] = + ctx ?=> ctx.phases + + /** Define the Players info of the game. + */ + def Players: Contexted[GameCtx[?, ?, ?, ?, ?], PropertyBuilder[PlayersCtx]] = + ctx ?=> ctx.players + + /** Define the initial phase of the game. + */ + def InitialPhase[Phase]: Contexted[GameCtx[?, Phase, ?, ?, ?], PropertySetter[Phase]] = + ctx ?=> ctx.initialPhase + + /** Define the initial state factory of the game. + */ + def StateFactory[State, Player]: Contexted[GameCtx[State, ?, ?, ?, Player], PropertySetter[Seq[Player] => State]] = + ctx ?=> ctx.stateFactory diff --git a/src/main/scala/scatan/lib/game/dsl/ops/PhaseCtxOps.scala b/src/main/scala/scatan/lib/game/dsl/ops/PhaseCtxOps.scala new file mode 100644 index 00000000..ea9c91bb --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/ops/PhaseCtxOps.scala @@ -0,0 +1,58 @@ +package scatan.lib.game.dsl.ops + +import scatan.lib.game.dsl.GameDSLDomain.* +import scatan.lib.game.dsl.PropertiesDSL.* + +object PhaseCtxOps: + + /** Define the phase type for the phase + */ + def PhaseType[Phase]: Contexted[PhaseCtx[?, Phase, ?, ?, ?], PropertySetter[Phase]] = + ctx ?=> ctx.phase + + /** Define the initial step for the phase + */ + def InitialStep[Step]: Contexted[PhaseCtx[?, ?, Step, ?, ?], PropertySetter[Step]] = + ctx ?=> ctx.initialStep + + /** Define the ending step for the phase + */ + def EndingStep[Step]: Contexted[PhaseCtx[?, ?, Step, ?, ?], PropertySetter[Step]] = + ctx ?=> ctx.endingStep + + /** Define the next phase for the phase + */ + def NextPhase[Phase]: Contexted[PhaseCtx[?, Phase, ?, ?, ?], PropertySetter[Phase]] = + ctx ?=> ctx.nextPhase + + /** Define an action to be executed when the phase is entered The action is a function that takes the current state + * and returns the new state + */ + def OnEnter[State]: Contexted[PhaseCtx[State, ?, ?, ?, ?], PropertySetter[State => State]] = + ctx ?=> ctx.onEnter + + /** Define a step in the phase + */ + def Step[Phase, StepType, Action] + : Contexted[PhaseCtx[?, Phase, StepType, Action, ?], PropertyBuilder[StepCtx[Phase, StepType, Action]]] = + ctx ?=> ctx.steps + + /** Possible Iterators Factory for Players + */ + object Iterations: + /** Iterate over the sequence only once + */ + def Once[X]: Seq[X] => Iterator[X] = _.iterator + + /** Iterate over the sequence infinitely + */ + def Circular[X]: Seq[X] => Iterator[X] = Iterator.continually(_).flatten + + /** Iterate over the sequence once and then back + */ + def OnceAndBack[X]: Seq[X] => Iterator[X] = seq => (seq ++ seq.reverse).iterator + + /** Define how the players are iterated over in the phase + */ + def Iterate[Player]: Contexted[PhaseCtx[?, ?, ?, ?, Player], PropertySetter[Seq[Player] => Iterator[Player]]] = + ctx ?=> ctx.playerIteratorFactory diff --git a/src/main/scala/scatan/lib/game/dsl/ops/PlayersCtxOps.scala b/src/main/scala/scatan/lib/game/dsl/ops/PlayersCtxOps.scala new file mode 100644 index 00000000..3f323d0e --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/ops/PlayersCtxOps.scala @@ -0,0 +1,10 @@ +package scatan.lib.game.dsl.ops + +import scatan.lib.game.dsl.GameDSLDomain.* +import scatan.lib.game.dsl.PropertiesDSL.* + +object PlayersCtxOps: + /** Define the allowed numbers of players in the game. + */ + def CanBe: Contexted[PlayersCtx, PropertySetter[Seq[Int]]] = + ctx ?=> ctx.allowedSizes diff --git a/src/main/scala/scatan/lib/game/dsl/ops/StepCtxOps.scala b/src/main/scala/scatan/lib/game/dsl/ops/StepCtxOps.scala new file mode 100644 index 00000000..fde8a8a7 --- /dev/null +++ b/src/main/scala/scatan/lib/game/dsl/ops/StepCtxOps.scala @@ -0,0 +1,16 @@ +package scatan.lib.game.dsl.ops + +import scatan.lib.game.dsl.GameDSLDomain.* +import scatan.lib.game.dsl.PropertiesDSL.* + +object StepCtxOps: + + /** Define the type of the step + */ + def StepType[Step]: Contexted[StepCtx[?, Step, ?], PropertySetter[Step]] = + ctx ?=> ctx.step + + /** Define a new supported action for the step and the relative step to go to when the action is performed + */ + def when[Step, Action]: Contexted[StepCtx[?, Step, Action], PropertySetter[(Action, Step)]] = + ctx ?=> ctx.when diff --git a/src/main/scala/scatan/model/components/Building.scala b/src/main/scala/scatan/model/components/Building.scala index 59212ffa..483bbdfc 100644 --- a/src/main/scala/scatan/model/components/Building.scala +++ b/src/main/scala/scatan/model/components/Building.scala @@ -5,6 +5,7 @@ import scatan.model.components.BuildingType.* import scatan.model.components.ResourceType.* import scatan.model.game.config.ScatanPlayer import scatan.model.map.{RoadSpot, Spot, StructureSpot} + import scala.collection.immutable.ListMap type ResourceCost = (ResourceType, Int) diff --git a/src/main/scala/scatan/model/game/ScatanDSL.scala b/src/main/scala/scatan/model/game/ScatanDSL.scala index 0a0ccd9f..0f7798ea 100644 --- a/src/main/scala/scatan/model/game/ScatanDSL.scala +++ b/src/main/scala/scatan/model/game/ScatanDSL.scala @@ -1,91 +1,78 @@ package scatan.model.game -import scatan.lib.game.dsl.PhaseDSLOps.* -import scatan.lib.game.dsl.PhasesDSLOps.* -import scatan.lib.game.dsl.PlayersDSLOps.* -import scatan.lib.game.dsl.TurnDSLOps.* -import scatan.lib.game.dsl.{GameDSL, PhaseDSLOps, PhasesDSLOps} +import scatan.lib.game.Rules import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} import scatan.model.game.ops.CardOps.assignResourcesAfterInitialPlacement -import scatan.model.game.ops.ScoreOps.* +import scatan.model.game.ops.ScoreOps.winner import scala.language.postfixOps -object ScatanDSL extends GameDSL: - override type Player = ScatanPlayer - override type State = ScatanState - override type PhaseType = ScatanPhases - override type StepType = ScatanSteps - override type ActionType = ScatanActions +object ScatanDSL: - import scatan.model.game.config.ScatanSteps.* + import scatan.lib.game.dsl.GameDSL.* - Players { - canBe(3 to 4) - } + private val game = Game[ScatanState, ScatanPhases, ScatanSteps, ScatanActions, ScatanPlayer] { - StartWithStateFactory(ScatanState(_)) - StartWithPhase(ScatanPhases.Setup) + Players { + CanBe := (3 to 4) + } - Winner(_.winner) + WinnerFunction := winner + InitialPhase := ScatanPhases.Setup + StateFactory := ScatanState.apply - Phases { - On(ScatanPhases.Setup) { + Phase { + PhaseType := ScatanPhases.Setup + InitialStep := ScatanSteps.SetupSettlement + EndingStep := ScatanSteps.ChangingTurn + NextPhase := ScatanPhases.Game + Iterate := Iterations.OnceAndBack - Turn { - Iterate(circularWithBack) - StartIn(SetupSettlement) - CanEndIn(ChangingTurn) - NextPhase(ScatanPhases.Game) + Step { + StepType := ScatanSteps.SetupSettlement + when := ScatanActions.AssignSettlement -> ScatanSteps.SetupRoad + } + Step { + StepType := ScatanSteps.SetupRoad + when := ScatanActions.AssignRoad -> ScatanSteps.ChangingTurn } - - When(SetupSettlement)( - ScatanActions.AssignSettlement -> SetupRoad - ) - - When(SetupRoad)( - ScatanActions.AssignRoad -> ChangingTurn - ) - - When(ChangingTurn)() } - - On(ScatanPhases.Game) { - - OnEnter((state: ScatanState) => state.assignResourcesAfterInitialPlacement.get) - - Turn { - Iterate(normal) - StartIn(Starting) - CanEndIn(ChangingTurn) + Phase { + PhaseType := ScatanPhases.Game + InitialStep := ScatanSteps.Starting + EndingStep := ScatanSteps.ChangingTurn + OnEnter := { (state: ScatanState) => state.assignResourcesAfterInitialPlacement.get } + Iterate := Iterations.Circular + + Step { + StepType := ScatanSteps.Starting + when := ScatanActions.RollDice -> ScatanSteps.Playing + when := ScatanActions.RollSeven -> ScatanSteps.PlaceRobber + when := ScatanActions.PlayDevelopmentCard -> ScatanSteps.Starting + } + Step { + StepType := ScatanSteps.PlaceRobber + when := ScatanActions.PlaceRobber -> ScatanSteps.StealCard + } + Step { + StepType := ScatanSteps.StealCard + when := ScatanActions.StoleCard -> ScatanSteps.Playing + } + Step { + StepType := ScatanSteps.Playing + when := ScatanActions.BuildSettlement -> ScatanSteps.Playing + when := ScatanActions.BuildRoad -> ScatanSteps.Playing + when := ScatanActions.BuildCity -> ScatanSteps.Playing + when := ScatanActions.BuyDevelopmentCard -> ScatanSteps.Playing + when := ScatanActions.PlayDevelopmentCard -> ScatanSteps.Playing + when := ScatanActions.TradeWithBank -> ScatanSteps.Playing + when := ScatanActions.TradeWithPlayer -> ScatanSteps.Playing + when := ScatanActions.NextTurn -> ScatanSteps.ChangingTurn } - - When(Starting)( - ScatanActions.RollDice -> Playing, - ScatanActions.RollSeven -> PlaceRobber, - ScatanActions.PlayDevelopmentCard -> Starting - ) - - When(PlaceRobber)( - ScatanActions.PlaceRobber -> StealCard - ) - - When(StealCard)( - ScatanActions.StoleCard -> Playing - ) - - When(Playing)( - ScatanActions.BuildSettlement -> Playing, - ScatanActions.BuildRoad -> Playing, - ScatanActions.BuildCity -> Playing, - ScatanActions.BuyDevelopmentCard -> Playing, - ScatanActions.PlayDevelopmentCard -> Playing, - ScatanActions.TradeWithBank -> Playing, - ScatanActions.TradeWithPlayer -> Playing, - ScatanActions.NextTurn -> ChangingTurn - ) - - When(ChangingTurn)() } + } + + 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 3214b046..7cda9630 100644 --- a/src/main/scala/scatan/model/game/ScatanEffects.scala +++ b/src/main/scala/scatan/model/game/ScatanEffects.scala @@ -1,18 +1,19 @@ package scatan.model.game import scatan.lib.game.ops.Effect -import scatan.model.components.BuildingType +import scatan.model.components.{BuildingType, ResourceCard} 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.CardOps.buyDevelopmentCard -import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} -import scatan.model.components.ResourceCard -import scatan.model.game.ops.CardOps.removeResourceCard -import scatan.model.game.ops.CardOps.assignResourceCard -import scatan.model.game.ops.TradeOps.tradeWithPlayer -import scatan.model.game.ops.CardOps.assignResourcesFromNumber +import scatan.model.game.ops.CardOps.{ + assignResourceCard, + assignResourcesFromNumber, + buyDevelopmentCard, + removeResourceCard +} import scatan.model.game.ops.RobberOps.moveRobber +import scatan.model.game.ops.TradeOps.tradeWithPlayer +import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} object ScatanEffects: diff --git a/src/main/scala/scatan/model/game/ScatanGame.scala b/src/main/scala/scatan/model/game/ScatanGame.scala index df4a4c86..2e188149 100644 --- a/src/main/scala/scatan/model/game/ScatanGame.scala +++ b/src/main/scala/scatan/model/game/ScatanGame.scala @@ -5,13 +5,13 @@ 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 import scatan.model.game.ScatanEffects.* import scatan.model.game.config.ScatanActions.* import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, ScatanSteps} import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} import scala.util.Random -import scatan.model.components.ResourceCard /** The status of a game of Scatan. It contains all the data without any possible action. * @param game diff --git a/src/main/scala/scatan/model/game/ScatanState.scala b/src/main/scala/scatan/model/game/ScatanState.scala index a988e3a3..cb269eb0 100644 --- a/src/main/scala/scatan/model/game/ScatanState.scala +++ b/src/main/scala/scatan/model/game/ScatanState.scala @@ -6,7 +6,9 @@ import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap import scatan.model.components.DevelopmentType.Knight import scatan.model.game.config.ScatanPlayer import scatan.model.map.* + import scala.collection.mutable.ListMap +import scatan.model.components.UnproductiveTerrain.Desert /** Represents the state of a Scatan game. * @@ -50,7 +52,7 @@ object ScatanState: def apply(players: Seq[ScatanPlayer], developmentCardsDeck: DevelopmentCardsDeck): ScatanState = require(players.sizeIs >= 3 && players.sizeIs <= 4, "The number of players must be between 3 and 4") val gameMap = GameMap() - val desertHexagon = gameMap.tiles.find(gameMap.toContent(_).terrain == UnproductiveTerrain.Desert).get + val desertHexagon = gameMap.tiles.find(gameMap.toContent(_).terrain == Desert).get ScatanState( players, GameMap(), diff --git a/src/main/scala/scatan/model/game/ops/AwardOps.scala b/src/main/scala/scatan/model/game/ops/AwardOps.scala index aa42c1c0..eaade073 100644 --- a/src/main/scala/scatan/model/game/ops/AwardOps.scala +++ b/src/main/scala/scatan/model/game/ops/AwardOps.scala @@ -1,13 +1,9 @@ package scatan.model.game.ops +import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap +import scatan.model.components.* import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.components.Awards -import scatan.model.components.Award -import scatan.model.components.AwardType -import scatan.model.components.DevelopmentType -import scatan.model.components.AssignedBuildingsAdapter.asPlayerMap -import scatan.model.components.BuildingType object AwardOps: diff --git a/src/main/scala/scatan/model/game/ops/BuildingOps.scala b/src/main/scala/scatan/model/game/ops/BuildingOps.scala index d9b4ed77..cc1e35d7 100644 --- a/src/main/scala/scatan/model/game/ops/BuildingOps.scala +++ b/src/main/scala/scatan/model/game/ops/BuildingOps.scala @@ -3,8 +3,8 @@ package scatan.model.game.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.EmptySpotsOps.{emptyRoadSpot, emptyStructureSpot} import scatan.model.game.ops.AwardOps.* +import scatan.model.game.ops.EmptySpotsOps.{emptyRoadSpot, emptyStructureSpot} import scatan.model.map.{RoadSpot, Spot, StructureSpot} object BuildingOps: diff --git a/src/main/scala/scatan/model/game/ops/CardOps.scala b/src/main/scala/scatan/model/game/ops/CardOps.scala index d19b78b5..e3d76c31 100644 --- a/src/main/scala/scatan/model/game/ops/CardOps.scala +++ b/src/main/scala/scatan/model/game/ops/CardOps.scala @@ -1,11 +1,10 @@ package scatan.model.game.ops -import scatan.model.components.{BuildingType, DevelopmentCard, ResourceCard, ResourceType} +import scatan.model.components.* import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.map.{Hexagon, RoadSpot, StructureSpot, TileContent} import scatan.model.game.ops.AwardOps.* -import scatan.model.components.AssignedBuildings +import scatan.model.map.{Hexagon, RoadSpot, StructureSpot, TileContent} object CardOps: diff --git a/src/main/scala/scatan/model/game/ops/TradeOps.scala b/src/main/scala/scatan/model/game/ops/TradeOps.scala index 5b97a932..754a2438 100644 --- a/src/main/scala/scatan/model/game/ops/TradeOps.scala +++ b/src/main/scala/scatan/model/game/ops/TradeOps.scala @@ -1,10 +1,9 @@ package scatan.model.game.ops +import scatan.model.components.ResourceCard import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer -import scatan.model.components.ResourceCard -import scatan.model.game.ops.CardOps.removeResourceCard -import scatan.model.game.ops.CardOps.assignResourceCard +import scatan.model.game.ops.CardOps.{assignResourceCard, removeResourceCard} object TradeOps: extension (state: ScatanState) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 0544bb45..30bdb3a9 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -1,10 +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.views.game.components.{CardsComponent, GameMapComponent, LeftTabComponent} +import scatan.views.game.components.{CardsComponent, EndgameComponent, GameMapComponent, LeftTabComponent} +import scatan.views.utils.TypeUtils +import scatan.views.utils.TypeUtils.{Displayable, DisplayableSource} trait GameView extends View[ApplicationState] @@ -18,8 +21,10 @@ private class ScalaJsGameView(container: String, requirements: View.Requirements given Signal[ApplicationState] = this.reactiveState given GameController = this.controller + override def element: Element = div( + EndgameComponent.endgamePopup, div( className := LeftTabComponent.leftTabCssClass, LeftTabComponent.currentPlayerComponent, diff --git a/src/main/scala/scatan/views/game/components/CardsComponent.scala b/src/main/scala/scatan/views/game/components/CardsComponent.scala index 2be73c09..fd44c088 100644 --- a/src/main/scala/scatan/views/game/components/CardsComponent.scala +++ b/src/main/scala/scatan/views/game/components/CardsComponent.scala @@ -1,18 +1,15 @@ package scatan.views.game.components import com.raquo.laminar.api.L.* -import scatan.model.components.ResourceType -import scatan.model.components.ResourceType.* -import scatan.model.ApplicationState -import scatan.model.components.ResourceCard -import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ScatanState -import scatan.model.components.DevelopmentType -import scatan.model.components.DevelopmentType.* -import scatan.views.game.components.CardContextMap.cardImages -import scatan.views.game.components.CardContextMap.countCardOf -import scatan.views.game.components.CardContextMap.CardType import scatan.controllers.game.GameController +import scatan.model.game.* +import scatan.model.game.config.* +import scatan.model.components.* +import scatan.views.utils.TypeUtils.{Displayable, DisplayableSource} +import scatan.views.utils.TypeUtils.{gameController, reactiveState, state} +import ResourceType.* +import DevelopmentType.* +import scatan.views.game.components.CardContextMap.{CardType, cardImages, countCardOf} object CardContextMap: extension (state: ScatanState) @@ -38,16 +35,14 @@ object CardContextMap: ) object CardsComponent: - def cardsComponent(using reactiveState: Signal[ApplicationState])(using gameController: GameController): Element = + def cardsComponent: DisplayableSource[Element] = div( cls := "game-view-card-container", cardCountComponent(cardImages.collect { case (k: ResourceType, v) => (k, v) }), cardCountComponent(cardImages.collect { case (k: DevelopmentType, v) => (k, v) }) ) - private def cardCountComponent(using reactiveState: Signal[ApplicationState])(using gameController: GameController)( - cards: Map[CardType, String] - ): Element = + private def cardCountComponent(cards: Map[CardType, String]): DisplayableSource[Element] = div( cls := "game-view-child-container", for (cardType, path) <- cards.toList diff --git a/src/main/scala/scatan/views/game/components/EndgameComponent.scala b/src/main/scala/scatan/views/game/components/EndgameComponent.scala new file mode 100644 index 00000000..873f0a26 --- /dev/null +++ b/src/main/scala/scatan/views/game/components/EndgameComponent.scala @@ -0,0 +1,32 @@ +package scatan.views.game.components + +import com.raquo.laminar.api.L.* +import org.scalajs.dom +import scatan.views.utils.TypeUtils.{DisplayableSource, reactiveState} + +object EndgameComponent: + def endgamePopup: DisplayableSource[Element] = + val winnerSignal: Signal[Option[String]] = reactiveState.map(_.game.flatMap(_.winner.map(_.name))) + div( + display <-- winnerSignal + .map(_.isDefined) + .map(if _ then "block" else "none"), // Show or hide based on the presence of a winner + 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, + child.text <-- winnerSignal.map(_.getOrElse("No winner")), + br(), + button( + "Restart", + onClick --> { _ => + // Close the popup, you can implement your own logic here + dom.window.location.reload() + } + ) + ) diff --git a/src/main/scala/scatan/views/game/components/GameMapComponent.scala b/src/main/scala/scatan/views/game/components/GameMapComponent.scala index f1566810..107b0ec9 100644 --- a/src/main/scala/scatan/views/game/components/GameMapComponent.scala +++ b/src/main/scala/scatan/views/game/components/GameMapComponent.scala @@ -9,9 +9,13 @@ import scatan.model.game.ScatanState import scatan.model.game.config.ScatanPlayer import scatan.model.map.{Hexagon, StructureSpot, TileContent} import scatan.model.{ApplicationState, GameMap} -import scatan.views.Coordinates -import scatan.views.Coordinates.* +import scatan.model.components.AssignedBuildings +import scatan.views.utils.Coordinates +import scatan.views.utils.Coordinates.* import scatan.views.game.components.ContextMap.{viewBuildingType, viewPlayer, toImgId} +import scatan.model.map.{RoadSpot, Spot} +import scatan.views.utils.TypeUtils.{DisplayableSource, InputSourceWithState, InputSource, StateKnoledge} +import scatan.views.utils.TypeUtils.{gameController, state, reactiveState} object ContextMap: @@ -39,7 +43,6 @@ object ContextMap: viewPlayers = viewPlayers + (player -> viewPlayer) viewPlayer - extension (state: ScatanState) def gameMap: GameMap = state.gameMap extension (info: AssignmentInfo) def viewPlayer: String = updateAndGetPlayer(info.player) def viewBuildingType: String = buildings(info.buildingType) @@ -62,7 +65,7 @@ object GameMapComponent: yield s"$x,$y").mkString(" ") private val layersToCanvasSize: Int => Int = x => (2 * x * hexSize) + 50 - def mapComponent(using reactiveState: Signal[ApplicationState])(using gameController: GameController): Element = + def mapComponent: DisplayableSource[Element] = div( className := "game-view-game-tab", child <-- reactiveState @@ -70,34 +73,30 @@ object GameMapComponent: (for game <- state.game state = game.state - yield getHexagonalMap(state)).getOrElse(div("No game")) + yield + given ScatanState = state + getHexagonalMap + ).getOrElse(div("No game")) ) ) - private def getHexagonalMap(state: ScatanState)(using gameController: GameController): Element = - val gameMap = state.gameMap + private def gameMap(using ScatanState): GameMap = state.gameMap + private def contentOf(hex: Hexagon)(using ScatanState): TileContent = state.gameMap.toContent(hex) + 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}", - for - hex <- gameMap.tiles.toList - content = gameMap.toContent(hex) - hasRobber = state.robberPlacement == hex - yield svgHexagonWithNumber(hex, content, hasRobber, () => gameController.placeRobber(hex)), - for - road <- gameMap.edges.toList - spot1Coordinates <- road._1.coordinates - spot2Coordinates <- road._2.coordinates - player = state.assignedBuildings.get(road).map(_.viewPlayer) - yield svgRoad(spot1Coordinates, spot2Coordinates, player, () => gameController.onRoadSpot(road)), - for - spot <- gameMap.nodes.toList - coordinates <- spot.coordinates - assignmentInfo = state.assignedBuildings.get(spot) - player = assignmentInfo.map(_.viewPlayer) - buildingType = assignmentInfo.map(_.viewBuildingType) - yield svgSpot(coordinates, player, buildingType, () => gameController.onStructureSpot(spot)) + for hex <- gameMap.tiles.toList + yield svgHexagonWithNumber(hex), + for road <- gameMap.edges.toList + yield svgRoad(road), + for spot <- gameMap.nodes.toList + yield svgSpot(spot) ) /** A svg hexagon. @@ -107,23 +106,18 @@ object GameMapComponent: * @return * the svg hexagon. */ - private def svgHexagonWithNumber( - hex: Hexagon, - tileContent: TileContent, - hasRobber: Boolean, - onPlaceRobber: () => Unit - ): Element = + 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(#${tileContent.terrain.toImgId})" + svg.fill := s"url(#${contentOf(hex).terrain.toImgId})" ), - tileContent.terrain match + contentOf(hex).terrain match case Sea => "" - case _ => circularNumberWithRobber(tileContent.number, hasRobber, onPlaceRobber) + case _ => circularNumberWithRobber(hex) ) /** A svg circular number @@ -131,7 +125,7 @@ object GameMapComponent: * the number to display * @return */ - private def circularNumberWithRobber(number: Option[Int], hasRobber: Boolean, onPlaceRobber: () => Unit): Element = + private def circularNumberWithRobber(hex: Hexagon): InputSourceWithState[Element] = svg.g( svg.circle( svg.cx := "0", @@ -144,10 +138,12 @@ object GameMapComponent: svg.y := "0", svg.fontSize := s"$radius", svg.className := "hexagon-center-number", - number.map(_.toString).getOrElse("") + contentOf(hex).number.map(_.toString).getOrElse("") ), - onClick --> (_ => onPlaceRobber()), - if hasRobber then robberCross else "" + onClick --> (_ => gameController.placeRobber(hex)), + if robberPlacement == hex + then robberCross + else "" ) private def robberCross: Element = @@ -175,23 +171,19 @@ object GameMapComponent: * @return * the road graphic */ - private def svgRoad( - spot1: Coordinates, - spot2: Coordinates, - withPlayer: Option[String], - onRoadClick: () => Unit - ): Element = - val Coordinates(x1, y1) = spot1 - val Coordinates(x2, y2) = spot2 + private def svgRoad(road: RoadSpot): InputSourceWithState[Element] = + val Coordinates(x1, y1) = road._1.coordinates.get + val Coordinates(x2, y2) = road._2.coordinates.get + 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.className := s"road ${withPlayer.getOrElse("")}" + svg.className := s"road ${player.getOrElse("")}" ), - withPlayer match + player match case Some(_) => "" case _ => svg.circle( @@ -199,7 +191,7 @@ object GameMapComponent: svg.cy := s"${y1 + (y2 - y1) / 2}", svg.className := "road-center", svg.r := s"$radius", - onClick --> (_ => onRoadClick()) + onClick --> (_ => gameController.onRoadSpot(road)) ) ) @@ -211,27 +203,24 @@ object GameMapComponent: * @return * the spot graphic */ - private def svgSpot( - coordinate: Coordinates, - withPlayer: Option[String], - withType: Option[String], - onSpotClick: () => Unit - ): Element = - val Coordinates(x, y) = coordinate + private def svgSpot(structure: StructureSpot): InputSourceWithState[Element] = + val Coordinates(x, y) = structure.coordinates.get + val player = assignmentInfoOf(structure).map(_.viewPlayer) + val structureType = assignmentInfoOf(structure).map(_.viewBuildingType) svg.g( svg.circle( svg.cx := s"${x}", svg.cy := s"${y}", svg.r := s"$radius", - svg.className := s"${withPlayer.getOrElse("spot")}", - onClick --> (_ => onSpotClick()) + svg.className := s"${player.getOrElse("spot")}", + onClick --> (_ => gameController.onStructureSpot(structure)) ), svg.text( svg.x := s"${x}", svg.y := s"${y}", svg.className := "spot-text", svg.fontSize := s"$radius", - s"${withType.getOrElse("")}" + s"${structureType.getOrElse("")}" ) ) diff --git a/src/main/scala/scatan/views/game/components/LeftTabComponent.scala b/src/main/scala/scatan/views/game/components/LeftTabComponent.scala index 093ee6ac..a2ac8a7e 100644 --- a/src/main/scala/scatan/views/game/components/LeftTabComponent.scala +++ b/src/main/scala/scatan/views/game/components/LeftTabComponent.scala @@ -5,7 +5,10 @@ import scatan.controllers.game.GameController import scatan.lib.mvc.ScalaJSView import scatan.model.ApplicationState import scatan.model.game.config.ScatanActions +import scatan.model.game.ops.ScoreOps.scores import scatan.views.game.GameView +import scatan.views.utils.TypeUtils.{Displayable, DisplayableSource} +import scatan.views.utils.TypeUtils.{gameController, reactiveState} object LeftTabComponent: @@ -13,31 +16,36 @@ object LeftTabComponent: def leftTabCssClass: String = "game-view-left-tab" - def currentPlayerComponent(using view: Signal[ApplicationState]): Element = + def currentPlayerComponent: Displayable[Element] = div( h2( className := "game-view-player", - child.text <-- view + child.text <-- reactiveState .map("Current Player: " + _.game.map(_.turn.player.name).getOrElse("No player")) ), + h2( + className := "game-view-player-score", + child.text <-- reactiveState + .map("Score: " + _.game.map(game => game.state.scores(game.turn.player)).getOrElse("No score")) + ), h2( className := "game-view-phase", - child.text <-- view + child.text <-- reactiveState .map("Phase: " + _.game.map(_.gameStatus.phase.toString).getOrElse("No phase")) ), h2( className := "game-view-step", - child.text <-- view + child.text <-- reactiveState .map("Step: " + _.game.map(_.gameStatus.step.toString).getOrElse("No step")) ) ) - def possibleMovesComponent(using view: Signal[ApplicationState]): Element = + def possibleMovesComponent: Displayable[Element] = div( className := "game-view-moves", "Possible moves:", ul( - children <-- view + children <-- reactiveState .map(state => for move <- state.game.map(_.allowedActions.toSeq).getOrElse(Seq.empty) yield li(cls := "game-view-move", move.toViewAction) @@ -45,22 +53,28 @@ object LeftTabComponent: ) ) - def isActionDisabled(using view: Signal[ApplicationState])(action: ScatanActions): Signal[Boolean] = - view.map(_.game.exists(!_.allowedActions.contains(action))) + def isActionDisabled(action: ScatanActions): Displayable[Signal[Boolean]] = + reactiveState.map(_.game.exists(!_.allowedActions.contains(action))) - def buttonsComponent(using view: Signal[ApplicationState])(using controller: GameController): Element = + def buttonsComponent: DisplayableSource[Element] = div( className := "game-view-buttons", button( className := "game-view-button roll-dice-button", "Roll dice", - onClick --> { _ => controller.rollDice() }, + onClick --> { _ => gameController.rollDice() }, disabled <-- isActionDisabled(ScatanActions.RollDice) ), + button( + className := "game-view-button buy-development-card-button", + "Buy Dev. Card", + onClick --> { _ => gameController.buyDevelopmentCard() }, + disabled <-- isActionDisabled(ScatanActions.BuyDevelopmentCard) + ), button( className := "game-view-button end-turn-button", "End Turn", - onClick --> { _ => controller.nextTurn() }, + onClick --> { _ => gameController.nextTurn() }, disabled <-- isActionDisabled(ScatanActions.NextTurn) ) ) diff --git a/src/main/scala/scatan/views/Coordinate.scala b/src/main/scala/scatan/views/utils/Coordinate.scala similarity index 98% rename from src/main/scala/scatan/views/Coordinate.scala rename to src/main/scala/scatan/views/utils/Coordinate.scala index efe0c006..8365348a 100644 --- a/src/main/scala/scatan/views/Coordinate.scala +++ b/src/main/scala/scatan/views/utils/Coordinate.scala @@ -1,4 +1,4 @@ -package scatan.views +package scatan.views.utils import scatan.model.map.{Hexagon, StructureSpot} diff --git a/src/main/scala/scatan/views/utils/TypeUtils.scala b/src/main/scala/scatan/views/utils/TypeUtils.scala new file mode 100644 index 00000000..bf74ba1b --- /dev/null +++ b/src/main/scala/scatan/views/utils/TypeUtils.scala @@ -0,0 +1,21 @@ +package scatan.views.utils + +import scatan.model.ApplicationState +import scatan.controllers.game.GameController +import scatan.model.game.ScatanState +import com.raquo.airstream.core.Signal + +object TypeUtils: + + type Displayable[T] = Signal[ApplicationState] ?=> T + type InputSource[T] = GameController ?=> T + type DisplayableSource[T] = Displayable[InputSource[T]] + type StateKnoledge[T] = ScatanState ?=> T + type InputSourceWithState[T] = InputSource[StateKnoledge[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 = + summon[ScatanState] diff --git a/src/test/scala/scatan/lib/game/EmptyDomain.scala b/src/test/scala/scatan/lib/game/EmptyDomain.scala index 7b75561f..6469884e 100644 --- a/src/test/scala/scatan/lib/game/EmptyDomain.scala +++ b/src/test/scala/scatan/lib/game/EmptyDomain.scala @@ -1,13 +1,7 @@ package scatan.lib.game -import scatan.lib.game.EmptyDomain.MyPhases -import scatan.lib.game.EmptyDomain.MyPhases.* -import scatan.lib.game.EmptyDomain.Steps.Initial -import scatan.lib.game.dsl.PhaseDSLOps.{Turn, When} -import scatan.lib.game.dsl.PhasesDSLOps.On -import scatan.lib.game.dsl.PlayersDSLOps.canBe -import scatan.lib.game.dsl.TurnDSLOps.* -import scatan.lib.game.dsl.{GameDSL, PhaseDSLOps, TurnDSLOps} +import scatan.lib.game.dsl.GameDSL +import scatan.lib.game.dsl.GameDSL.rules import scatan.lib.game.ops.Effect object EmptyDomain: @@ -27,54 +21,41 @@ object EmptyDomain: def NextTurnEffect: Effect[Actions.NextTurn.type, State] = (state: State) => Some(state) - object EmptyGameDSL extends GameDSL: - override type State = EmptyDomain.State - override type PhaseType = EmptyDomain.MyPhases - override type StepType = EmptyDomain.Steps - override type ActionType = EmptyDomain.Actions - override type Player = EmptyDomain.Player - - import Steps.* - - import scala.language.postfixOps + def rules = game.rules + import GameDSL.* + private val game = Game[State, MyPhases, Steps, Actions, Player] { Players { - canBe(2 to 4) + CanBe := 2 to 4 } - - StartWithStateFactory((_) => State()) - StartWithPhase(Game) - Winner(_ => None) - - Phases { - On(Game) { - - Turn { - Iterate(once) - StartIn(Initial) - CanEndIn(ChangingTurn) - NextPhase(GameOver) - } - - When(Steps.Initial)( - Actions.StartGame -> Steps.Initial, - Actions.NextTurn -> Steps.ChangingTurn - ) - - When(Steps.ChangingTurn)() - + StateFactory := (_ => State()) + InitialPhase := MyPhases.Game + WinnerFunction := (_ => None) + + Phase { + PhaseType := MyPhases.Game + InitialStep := Steps.Initial + EndingStep := Steps.ChangingTurn + NextPhase := MyPhases.GameOver + Iterate := Iterations.Once + + Step { + StepType := Steps.Initial + when := Actions.StartGame -> Steps.Initial + when := Actions.NextTurn -> Steps.ChangingTurn } - On(GameOver) { - - Turn { - Iterate(once) - StartIn(Initial) - CanEndIn(ChangingTurn) - NextPhase(GameOver) - } - + Step { + StepType := Steps.ChangingTurn } + } - def rules = EmptyGameDSL.rules + Phase { + PhaseType := MyPhases.GameOver + InitialStep := Steps.Initial + EndingStep := Steps.ChangingTurn + NextPhase := MyPhases.GameOver + Iterate := Iterations.Once + } + } diff --git a/src/test/scala/scatan/model/ScatanEffectsTest.scala b/src/test/scala/scatan/model/ScatanEffectsTest.scala index c431e9f7..36ed67c4 100644 --- a/src/test/scala/scatan/model/ScatanEffectsTest.scala +++ b/src/test/scala/scatan/model/ScatanEffectsTest.scala @@ -1,13 +1,11 @@ package scatan.model import scatan.BaseTest -import scatan.model.game.config.ScatanPlayer -import scatan.model.game.ScatanState -import scatan.model.components.ResourceCard -import scatan.model.components.ResourceType +import scatan.model.components.{ResourceCard, ResourceType} import scatan.model.game.ScatanEffects.{NextTurnEffect, PlaceRobberEffect, RollEffect, TradeWithPlayerEffect} +import scatan.model.game.{ScatanGame, ScatanState} +import scatan.model.game.config.ScatanPlayer import scatan.model.game.ops.CardOps.assignResourceCard -import scatan.model.game.ScatanGame class ScatanEffectsTest extends BaseTest: diff --git a/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala b/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala index 9a0f5ea3..13a375bf 100644 --- a/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/AwardOpsTest.scala @@ -1,12 +1,11 @@ package scatan.model.game.ops import scatan.model.components.* +import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.ops.AwardOps.* import scatan.model.game.ops.BuildingOps.assignBuilding import scatan.model.game.ops.CardOps.assignDevelopmentCard import scatan.model.game.ops.EmptySpotsOps.{emptyRoadSpot, emptyStructureSpot} -import scatan.model.game.ops.AwardOps.* -import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ScatanState class AwardOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala b/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala index 3f1e804f..cc29ac7d 100644 --- a/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/BuildingOpsTest.scala @@ -1,14 +1,14 @@ package scatan.model.game.ops import scatan.model.components.* +import scatan.model.game.ScatanState +import scatan.model.game.{BaseScatanStateTest, ScatanState} import scatan.model.game.ops.BuildingOps.{assignBuilding, build} import scatan.model.game.ops.CardOps.assignResourceCard import scatan.model.game.ops.EmptySpotsOps.{emptyStructureSpot, emptyRoadSpot} import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ScatanState -import scatan.model.map.StructureSpot -import scatan.model.map.RoadSpot import scatan.model.game.config.ScatanPlayer +import scatan.model.map.{RoadSpot, StructureSpot} class BuildingOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/DevCardOpsTest.scala b/src/test/scala/scatan/model/game/ops/DevCardOpsTest.scala index 153ba171..cf2d8407 100644 --- a/src/test/scala/scatan/model/game/ops/DevCardOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/DevCardOpsTest.scala @@ -2,14 +2,13 @@ package scatan.model.game.ops import scatan.lib.game.Game import scatan.model.components.{DevelopmentCard, DevelopmentType, ResourceCard, ResourceType} -import scatan.model.game.ScatanState +import scatan.model.game.{BaseScatanStateTest, ScatanState} import scatan.model.game.ops.CardOps.{ assignDevelopmentCard, assignResourceCard, buyDevelopmentCard, consumeDevelopmentCard } -import scatan.model.game.BaseScatanStateTest class DevCardOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/ResCardOpsTest.scala b/src/test/scala/scatan/model/game/ops/ResCardOpsTest.scala index a3243e50..c012ad7b 100644 --- a/src/test/scala/scatan/model/game/ops/ResCardOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/ResCardOpsTest.scala @@ -1,17 +1,18 @@ package scatan.model.game.ops import scatan.model.components.{BuildingType, ResourceCard, ResourceCards, ResourceType} +import scatan.model.game.{BaseScatanStateTest, ScatanState} import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.CardOps.assignResourcesFromNumber +import scatan.model.game.ops.CardOps.{ + assignResourceCard, + assignResourcesAfterInitialPlacement, + assignResourcesFromNumber, + removeResourceCard +} import scatan.model.game.ops.EmptySpotsOps.emptyStructureSpot import scatan.model.map.HexagonInMap.layer import scatan.model.map.{RoadSpot, Spot, StructureSpot} import scatan.utils.UnorderedTriple -import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ScatanState -import scatan.model.game.ops.CardOps.assignResourceCard -import scatan.model.game.ops.CardOps.removeResourceCard -import scatan.model.game.ops.CardOps.assignResourcesAfterInitialPlacement class ResCardOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala b/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala index 9b05d434..9b53ac66 100644 --- a/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/RobberOpsTest.scala @@ -1,10 +1,9 @@ package scatan.model.game.ops -import scatan.model.map.Hexagon -import scatan.model.game.ops.RobberOps.moveRobber -import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ScatanState import scatan.model.components.UnproductiveTerrain.Desert +import scatan.model.game.{BaseScatanStateTest, ScatanState} +import scatan.model.game.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/ops/ScoreOpsTest.scala index d96705a8..d26ad2eb 100644 --- a/src/test/scala/scatan/model/game/ops/ScoreOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/ScoreOpsTest.scala @@ -1,12 +1,10 @@ package scatan.model.game.ops import scatan.model.components.{AssignedBuildings, BuildingType, Scores} -import scatan.model.game.ScatanState +import scatan.model.game.{BaseScatanStateTest, ScatanState} import scatan.model.game.ops.BuildingOps.assignBuilding -import scatan.model.game.ops.EmptySpotsOps.emptyStructureSpot +import scatan.model.game.ops.EmptySpotsOps.{emptyRoadSpot, emptyStructureSpot} import scatan.model.game.ops.ScoreOps.* -import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ops.EmptySpotsOps.emptyRoadSpot class ScoreOpsTest extends BaseScatanStateTest: diff --git a/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala b/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala index ee3e8789..bbe00ea0 100644 --- a/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala +++ b/src/test/scala/scatan/model/game/ops/TradeOpsTest.scala @@ -1,10 +1,8 @@ package scatan.model.game.ops -import scatan.model.game.BaseScatanStateTest -import scatan.model.game.ScatanState +import scatan.model.components.{ResourceCard, ResourceType} +import scatan.model.game.{BaseScatanStateTest, ScatanState} import scatan.model.game.ops.CardOps.assignResourceCard -import scatan.model.components.ResourceCard -import scatan.model.components.ResourceType import scatan.model.game.ops.TradeOps.tradeWithPlayer class TradeOpsTest extends BaseScatanStateTest: diff --git a/style.css b/style.css index 8d9db771..52893c1b 100644 --- a/style.css +++ b/style.css @@ -195,6 +195,11 @@ body { .game-view-player { font-size: 1.5em; } + +.game-view-player-score { + font-size: 1em; +} + .game-view-phase { font-size: 1em; } @@ -209,7 +214,7 @@ body { } .game-view-button { - width: 25%; + width: 33%; height: 3em; font-size: 1em; font-weight: bold; @@ -220,6 +225,10 @@ body { background-color: #b5ffa0; } +.buy-development-card-button { + background-color: #b5b5ff; +} + .end-turn-button { background-color: #fb9b9b; }