diff --git a/build.sbt b/build.sbt index 7c5754f9..5be83a0b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,15 +1,20 @@ ThisBuild / scalaVersion := "3.3.1" wartremoverWarnings ++= Warts.all -wartremoverWarnings -= Wart.Equals -wartremoverWarnings -= Wart.Overloading -wartremoverWarnings -= Wart.ImplicitParameter -wartremoverWarnings -= Wart.IterableOps -wartremoverWarnings -= Wart.DefaultArguments -wartremoverWarnings -= Wart.AsInstanceOf -wartremoverWarnings -= Wart.OptionPartial -wartremoverWarnings -= Wart.IsInstanceOf -wartremoverWarnings -= Wart.Var + +wartremoverWarnings --= Seq( + Wart.Equals, + Wart.Overloading, + Wart.ImplicitParameter, + Wart.IterableOps, + Wart.DefaultArguments, + Wart.AsInstanceOf, + Wart.OptionPartial, + Wart.IsInstanceOf, + Wart.Var, + Wart.SeqApply, + Wart.Recursion +) lazy val scatan = (project in file(".")) .enablePlugins(ScalaJSPlugin) diff --git a/src/main/scala/scatan/controllers/game/SetUpController.scala b/src/main/scala/scatan/controllers/game/SetUpController.scala index 8c5782b2..a0fa4cdb 100644 --- a/src/main/scala/scatan/controllers/game/SetUpController.scala +++ b/src/main/scala/scatan/controllers/game/SetUpController.scala @@ -3,11 +3,12 @@ package scatan.controllers.game import scatan.lib.mvc.{BaseController, Controller} import scatan.model.ApplicationState import scatan.views.game.SetUpView +import scatan.model.GameMap /** This is the controller for the setup page. */ trait SetUpController extends Controller[ApplicationState]: - def startGame(usernames: String*): Unit + def startGame(gameMap: GameMap, usernames: String*): Unit object SetUpController: def apply(requirements: Controller.Requirements[SetUpView, ApplicationState]): SetUpController = @@ -21,5 +22,5 @@ private class SetUpControllerImpl(requirements: Controller.Requirements[SetUpVie extends BaseController(requirements) with SetUpController: - override def startGame(usernames: String*): Unit = - this.model.update(_.createGame(usernames*)) + override def startGame(gameMap: GameMap, usernames: String*): Unit = + this.model.update(_.createGame(gameMap, usernames*)) diff --git a/src/main/scala/scatan/lib/game/Game.scala b/src/main/scala/scatan/lib/game/Game.scala index f7188365..ba0266ab 100644 --- a/src/main/scala/scatan/lib/game/Game.scala +++ b/src/main/scala/scatan/lib/game/Game.scala @@ -1,5 +1,7 @@ package scatan.lib.game +import scatan.model.GameMap + /** A game status is a pair of phase and step. * @param phase * the current phase @@ -48,6 +50,7 @@ final case class Game[State, PhaseType, StepType, ActionType, Player]( object Game: def apply[State, PhaseType, StepType, ActionType, Player]( + gameMap: GameMap, players: Seq[Player] )(using rules: Rules[State, PhaseType, StepType, ActionType, Player] @@ -56,7 +59,7 @@ object Game: val iterator = rules.phaseTurnIteratorFactories.get(rules.startingPhase).map(_(players)).getOrElse(players.iterator) Game( players = players, - state = rules.startingStateFactory(players), + state = rules.startingStateFactory(gameMap, players), gameStatus = GameStatus(rules.startingPhase, rules.startingSteps(rules.startingPhase)), turn = Turn[Player](1, iterator.next()), playersIterator = iterator, diff --git a/src/main/scala/scatan/lib/game/Rules.scala b/src/main/scala/scatan/lib/game/Rules.scala index f8accef6..3b8c7f68 100644 --- a/src/main/scala/scatan/lib/game/Rules.scala +++ b/src/main/scala/scatan/lib/game/Rules.scala @@ -1,5 +1,7 @@ package scatan.lib.game +import scatan.model.GameMap + /** Rules of a game. * @param startingStateFactory * initial state of the game @@ -30,7 +32,7 @@ package scatan.lib.game */ final case class Rules[State, P, S, A, Player]( allowedPlayersSizes: Set[Int], - startingStateFactory: Seq[Player] => State, + startingStateFactory: (GameMap, Seq[Player]) => State, startingPhase: P, startingSteps: Map[P, S], endingSteps: Map[P, S], @@ -71,10 +73,10 @@ final case class Rules[State, P, S, A, Player]( object Rules: def empty[State, P, S, A, Player]: Rules[State, P, S, A, Player] = - fromStateFactory(_ => null.asInstanceOf[State]) + fromStateFactory((_, _) => null.asInstanceOf[State]) def fromStateFactory[State, P, S, A, Player]( - initialStateFactory: Seq[Player] => State + initialStateFactory: (GameMap, Seq[Player]) => State ): Rules[State, P, S, A, Player] = Rules[State, P, S, A, Player]( startingStateFactory = initialStateFactory, diff --git a/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala index 50db1a68..d47e7abe 100644 --- a/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala +++ b/src/main/scala/scatan/lib/game/dsl/GameDSLDomain.scala @@ -1,5 +1,7 @@ package scatan.lib.game.dsl +import scatan.model.GameMap + private object GameDSLDomain: import PropertiesDSL.* @@ -12,7 +14,8 @@ private object GameDSLDomain: 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]() + stateFactory: OptionalProperty[(GameMap, Seq[Player]) => State] = + OptionalProperty[(GameMap, Seq[Player]) => State]() ) /** The players context is used to define the players info of the game. 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 a3a3b210..662fe640 100644 --- a/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala +++ b/src/main/scala/scatan/lib/game/dsl/ops/GameCtxOps.scala @@ -2,6 +2,7 @@ package scatan.lib.game.dsl.ops import scatan.lib.game.dsl.GameDSLDomain.* import scatan.lib.game.dsl.PropertiesDSL.* +import scatan.model.GameMap object GameCtxOps: @@ -31,5 +32,6 @@ object GameCtxOps: /** Define the initial state factory of the game. */ - def StateFactory[State, Player]: Contexted[GameCtx[State, ?, ?, ?, Player], PropertySetter[Seq[Player] => State]] = + def StateFactory[State, Player] + : Contexted[GameCtx[State, ?, ?, ?, Player], PropertySetter[(GameMap, Seq[Player]) => State]] = ctx ?=> ctx.stateFactory diff --git a/src/main/scala/scatan/lib/game/ops/RulesOps.scala b/src/main/scala/scatan/lib/game/ops/RulesOps.scala index ebfd7afd..50b1716f 100644 --- a/src/main/scala/scatan/lib/game/ops/RulesOps.scala +++ b/src/main/scala/scatan/lib/game/ops/RulesOps.scala @@ -1,6 +1,7 @@ package scatan.lib.game.ops import scatan.lib.game.{GameStatus, Rules} +import scatan.model.GameMap /** Operations on [[Rules]] related to their construction. */ @@ -16,7 +17,7 @@ object RulesOps: def withAllowedPlayersSizes(sizes: Set[Int]): Rules[State, P, S, A, Player] = rules.copy(allowedPlayersSizes = sizes) - def withStartingStateFactory(stateFactory: Seq[Player] => State): Rules[State, P, S, A, Player] = + def withStartingStateFactory(stateFactory: (GameMap, Seq[Player]) => State): Rules[State, P, S, A, Player] = rules.copy(startingStateFactory = stateFactory) /** Set the starting phase for this game. diff --git a/src/main/scala/scatan/model/ApplicationState.scala b/src/main/scala/scatan/model/ApplicationState.scala index c5687eb6..6af88524 100644 --- a/src/main/scala/scatan/model/ApplicationState.scala +++ b/src/main/scala/scatan/model/ApplicationState.scala @@ -5,9 +5,9 @@ import scatan.model.game.ScatanGame import scatan.model.game.config.ScatanPlayer final case class ApplicationState(game: Option[ScatanGame]) extends Model.State: - def createGame(usernames: String*): ApplicationState = + def createGame(gameMap: GameMap, usernames: String*): ApplicationState = val players = usernames.map(ScatanPlayer(_)) - ApplicationState(Option(ScatanGame(players))) + ApplicationState(Option(ScatanGame(gameMap, players))) object ApplicationState: def apply(): ApplicationState = ApplicationState(Option.empty) diff --git a/src/main/scala/scatan/model/GameMap.scala b/src/main/scala/scatan/model/GameMap.scala index f22ab6f0..5d246ce8 100644 --- a/src/main/scala/scatan/model/GameMap.scala +++ b/src/main/scala/scatan/model/GameMap.scala @@ -2,6 +2,7 @@ package scatan.model import scatan.model.map.* import scatan.model.map.HexagonInMap.* +import scatan.model.components.Terrain /** Hexagonal tiled game map of Scatan. * @@ -11,12 +12,34 @@ import scatan.model.map.HexagonInMap.* * @param withSeaLayers * number of concentric circles of hexagons the terrain ones. */ -final case class GameMap(withTerrainLayers: Int = 2, withSeaLayers: Int = 1) - extends HexagonalTiledMap(withTerrainLayers + withSeaLayers) +final case class GameMap( + withTerrainLayers: Int = 2, + withSeaLayers: Int = 1, + tileContentsStrategy: 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] = - TileContentFactory.fixedForLayer2(tileWithTerrain) + override val toContent: Map[Hexagon, TileContent] = tileContentsStrategy(tileWithTerrain) + + override def equals(x: Any): Boolean = + x match + case that: GameMap => + this.withTerrainLayers == that.withTerrainLayers && + this.withSeaLayers == that.withSeaLayers && + (this.toContent.toSet & that.toContent.toSet).sizeIs == this.toContent.size + case _ => false + +object GameMapFactory: + + def defaultMap: GameMap = + GameMap(tileContentsStrategy = TileContentStrategyFactory.fixedForLayer2) + + def randomMap: GameMap = + GameMap(tileContentsStrategy = TileContentStrategyFactory.randomForLayer2) + + val strategies: Iterator[TileContentStrategy] = TileContentStrategyFactory.permutationForLayer2.toIterator + def nextPermutation: GameMap = + GameMap(tileContentsStrategy = strategies.next()) diff --git a/src/main/scala/scatan/model/game/ScatanDSL.scala b/src/main/scala/scatan/model/game/ScatanDSL.scala index 78e3ff04..2f8c3369 100644 --- a/src/main/scala/scatan/model/game/ScatanDSL.scala +++ b/src/main/scala/scatan/model/game/ScatanDSL.scala @@ -19,7 +19,7 @@ object ScatanDSL: WinnerFunction := winner InitialPhase := ScatanPhases.Setup - StateFactory := ScatanState.apply + StateFactory := { (m, p) => ScatanState(m, p) } Phase { PhaseType := ScatanPhases.Setup diff --git a/src/main/scala/scatan/model/game/ScatanGame.scala b/src/main/scala/scatan/model/game/ScatanGame.scala index 763af25c..efbf7a5e 100644 --- a/src/main/scala/scatan/model/game/ScatanGame.scala +++ b/src/main/scala/scatan/model/game/ScatanGame.scala @@ -12,6 +12,7 @@ import scatan.model.game.config.{ScatanActions, ScatanPhases, ScatanPlayer, Scat import scatan.model.map.{Hexagon, RoadSpot, StructureSpot} import scala.util.Random +import scatan.model.GameMap import scatan.model.components.ResourceCard import scatan.model.game.ops.RobberOps.playersOnRobber @@ -109,5 +110,5 @@ class ScatanGame(game: Game[ScatanState, ScatanPhases, ScatanSteps, ScatanAction object ScatanGame: def apply(game: Game[ScatanState, ScatanPhases, ScatanSteps, ScatanActions, ScatanPlayer]): ScatanGame = new ScatanGame(game) - def apply(players: Seq[ScatanPlayer]): ScatanGame = - new ScatanGame(Game(players)(using ScatanDSL.rules)) + def apply(gameMap: GameMap, players: Seq[ScatanPlayer]): ScatanGame = + new ScatanGame(Game(gameMap, players)(using ScatanDSL.rules)) diff --git a/src/main/scala/scatan/model/game/ScatanState.scala b/src/main/scala/scatan/model/game/ScatanState.scala index a829bd45..abee127d 100644 --- a/src/main/scala/scatan/model/game/ScatanState.scala +++ b/src/main/scala/scatan/model/game/ScatanState.scala @@ -47,15 +47,18 @@ object ScatanState: * a new ScatanState with the specified players */ def apply(players: Seq[ScatanPlayer]): ScatanState = - ScatanState(players, DevelopmentCardsDeck.defaultOrdered) + ScatanState(GameMap(), players, DevelopmentCardsDeck.defaultOrdered) - def apply(players: Seq[ScatanPlayer], developmentCardsDeck: DevelopmentCardsDeck): ScatanState = + def apply( + gameMap: GameMap = GameMap(), + players: Seq[ScatanPlayer], + developmentCardsDeck: DevelopmentCardsDeck = DevelopmentCardsDeck.defaultOrdered + ): 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 == Desert).get ScatanState( players, - GameMap(), + gameMap, AssignedBuildings.empty, Awards.empty, ResourceCards.empty(players), diff --git a/src/main/scala/scatan/model/map/TileContent.scala b/src/main/scala/scatan/model/map/TileContent.scala index d43cf6a1..93de0d7e 100644 --- a/src/main/scala/scatan/model/map/TileContent.scala +++ b/src/main/scala/scatan/model/map/TileContent.scala @@ -15,28 +15,77 @@ trait MapWithTileContent: */ def toContent: Map[Hexagon, TileContent] +trait TileContentConfig: + def numbers: Seq[Int] + def terrains: Seq[Terrain] + +type TileContentStrategy = Seq[Hexagon] => Map[Hexagon, TileContent] + /** A factory to create terrains. */ -object TileContentFactory: +object TileContentStrategyFactory: - def fixedForLayer2(tiles: Seq[Hexagon]): Map[Hexagon, TileContent] = + object ConfigForLayer2 extends TileContentConfig: val terrains: List[Terrain] = List( + 1 * Desert, 4 * Wood, 4 * Sheep, 4 * Wheat, 3 * Rock, 3 * Brick ).flatten - val numbers = 2 :: 12 :: (for i <- (3 to 11).toList if i != 7 yield List(i, i)).flatten - val tileContents = - terrains.zip(numbers).map(p => TileContent(p._1, Some(p._2))) + private def fromConfig(using config: TileContentConfig): TileContentStrategy = + val iterator = config.numbers.iterator + val tileContents = config.terrains.map { t => + t match + case Desert => TileContent(t, None) + case _ => TileContent(t, iterator.nextOption()) + } + tiles => + Map + .from(tiles.zip(tileContents)) + .withDefaultValue(TileContent(Sea, None)) + + def fixedForLayer2: TileContentStrategy = + import ConfigForLayer2.* + given TileContentConfig = ConfigForLayer2 + fromConfig + + def randomForLayer2: TileContentStrategy = + import ConfigForLayer2.* + import scala.util.Random.shuffle + given TileContentConfig with + val terrains = shuffle(ConfigForLayer2.terrains) + val numbers = shuffle(ConfigForLayer2.numbers) + fromConfig + + private def removeAtPos[A](list: List[A], n: Int): List[A] = + val splitted = list.splitAt(n) + splitted._1 ::: splitted._2.tail + + private def permutations[A](list: List[A]): LazyList[List[A]] = list match + case Nil => LazyList(Nil) + case _ => + for + i <- list.indices.to(LazyList) + e = list(i) + r = removeAtPos(list, i) + pr <- permutations(r) + yield e :: pr - Map - .from(tiles.zip(TileContent(Desert, None) :: tileContents)) - .withDefaultValue(TileContent(Sea, None)) + def permutationForLayer2: LazyList[TileContentStrategy] = + import ConfigForLayer2.* + for + t <- permutations(terrains) + n <- permutations(numbers.toList) + yield + given TileContentConfig with + val terrains = t + val numbers = n + fromConfig diff --git a/src/main/scala/scatan/views/game/SetUpView.scala b/src/main/scala/scatan/views/game/SetUpView.scala index 3f34892d..7dd1f440 100644 --- a/src/main/scala/scatan/views/game/SetUpView.scala +++ b/src/main/scala/scatan/views/game/SetUpView.scala @@ -5,6 +5,14 @@ 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.views.game.components.LeftTabComponent.buttonsComponent +import scatan.model.GameMapFactory + +enum MapSelectionMode: + case Default, Random, WithIterator /** This is the view for the setup page. */ @@ -35,6 +43,18 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement val numberOfUsers: Var[Int] = Var(3) val reactiveNumberOfUsers: Signal[Int] = numberOfUsers.signal + val mapSelectionMode: Var[MapSelectionMode] = Var(MapSelectionMode.Default) + val reactiveGameMap: Var[GameMap] = Var(GameMapFactory.defaultMap) + + def changeMap: Unit = + mapSelectionMode.now() match + case Default => + reactiveGameMap.set(GameMapFactory.defaultMap) + case Random => + reactiveGameMap.set(GameMapFactory.randomMap) + case WithIterator => + reactiveGameMap.set(GameMapFactory.nextPermutation) + private def validateNames(usernames: String*) = usernames.forall(_.matches(".*\\S.*")) @@ -48,7 +68,7 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement .value if validateNames(usernames*) then println(usernames) - this.controller.startGame(usernames*) + this.controller.startGame(reactiveGameMap.now(), usernames*) this.navigateTo(Pages.Game) override def switchToHome(): Unit = @@ -104,5 +124,29 @@ private class ScalaJsSetUpView(container: String, requirements: View.Requirement cls := "setup-menu-button", onClick --> (_ => this.switchToHome()), "Back" + ), + div( + cls := "setup-menu-map-picker", + div( + cls := "setup-menu-map-picker-left-tab", + select( + cls := "setup-menu-map-picker-combobox", + onChange.mapToValue.map(v => MapSelectionMode.fromOrdinal(v.toInt)) --> mapSelectionMode, + for (mode <- MapSelectionMode.values) + yield option( + value := s"${mode.ordinal}", + mode.toString + ) + ), + button( + cls := "setup-menu-map-picker-button", + "Change Map", + onClick --> (_ => changeMap) + ) + ), + div( + cls := "setup-menu-map", + child <-- reactiveGameMap.signal.map(gameMap => MapComponent.map(using gameMap)) + ) ) ) diff --git a/src/main/scala/scatan/views/game/components/map/MapComponent.scala b/src/main/scala/scatan/views/game/components/map/MapComponent.scala new file mode 100644 index 00000000..eacfc5f7 --- /dev/null +++ b/src/main/scala/scatan/views/game/components/map/MapComponent.scala @@ -0,0 +1,87 @@ +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/test/scala/scatan/lib/game/EmptyDomain.scala b/src/test/scala/scatan/lib/game/EmptyDomain.scala index 6469884e..0907a52c 100644 --- a/src/test/scala/scatan/lib/game/EmptyDomain.scala +++ b/src/test/scala/scatan/lib/game/EmptyDomain.scala @@ -28,7 +28,7 @@ object EmptyDomain: Players { CanBe := 2 to 4 } - StateFactory := (_ => State()) + StateFactory := ((_, _) => State()) InitialPhase := MyPhases.Game WinnerFunction := (_ => None) diff --git a/src/test/scala/scatan/lib/game/GameTest.scala b/src/test/scala/scatan/lib/game/GameTest.scala index 82480f92..3c266f1e 100644 --- a/src/test/scala/scatan/lib/game/GameTest.scala +++ b/src/test/scala/scatan/lib/game/GameTest.scala @@ -1,38 +1,40 @@ package scatan.lib.game import scatan.BaseTest +import scatan.model.GameMap class GameTest extends BaseTest: import EmptyDomain.* val players = Seq(Player("A"), Player("B"), Player("C")) + val gameMap = GameMap() given EmptyDomainRules = EmptyDomain.rules "A Game" should "exists given the rules" in { - val game = Game(players) + val game = Game(gameMap, players) } it should "have a state" in { - val game = Game(players) + val game = Game(gameMap, players) game.state should be(State()) } it should "have a turn" in { - val game = Game(players) + val game = Game(gameMap, players) game.turn should be(Turn.initial(players.head)) } it should "have a game status" in { - val game = Game(players) + val game = Game(gameMap, players) game.gameStatus should be(GameStatus(MyPhases.Game, Steps.Initial)) } it should "have a players iterator" in { - val game = Game(players) + val game = Game(gameMap, players) game.playersIterator.toSeq should be(players.tail) } it should "contains the rules" in { - val game = Game(players) + val game = Game(gameMap, players) game.rules shouldBe a[EmptyDomainRules] } diff --git a/src/test/scala/scatan/lib/game/RulesTest.scala b/src/test/scala/scatan/lib/game/RulesTest.scala index c000b107..e708c09a 100644 --- a/src/test/scala/scatan/lib/game/RulesTest.scala +++ b/src/test/scala/scatan/lib/game/RulesTest.scala @@ -1,11 +1,13 @@ package scatan.lib.game import scatan.BaseTest +import scatan.model.GameMap class RulesTest extends BaseTest: import EmptyDomain.* val players = Seq(Player("Alice"), Player("Bob"), Player("Carol")) + val gameMap = GameMap() val emptyGameRules = EmptyDomain.rules "The Rules" should "exists" in { @@ -21,9 +23,9 @@ class RulesTest extends BaseTest: } it should "have an initial state factory" in { - emptyGameRules.startingStateFactory shouldBe a[Seq[Player] => State] - emptyGameRules.startingStateFactory(players) shouldBe a[State] - emptyGameRules.startingStateFactory(players) shouldBe State() + emptyGameRules.startingStateFactory shouldBe a[(GameMap, Seq[Player]) => State] + emptyGameRules.startingStateFactory(gameMap, players) shouldBe a[State] + emptyGameRules.startingStateFactory(gameMap, players) shouldBe State() } it should "have a initial phase" in { diff --git a/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala index 51d184f1..2dadcd76 100644 --- a/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GamePlayOpsTest.scala @@ -4,6 +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 class GamePlayOpsTest extends BaseTest: @@ -11,7 +12,7 @@ class GamePlayOpsTest extends BaseTest: given EmptyDomainRules = EmptyDomain.rules val players = Seq(Player("p1"), Player("p2"), Player("p3")) - val game = Game(players) + val game = Game(GameMap(), players) "A Game" should "allow to check if an action is playable" in { game.canPlay(StartGame) shouldBe true diff --git a/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala index f78b0ca4..39398a86 100644 --- a/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GameTurnOpsTest.scala @@ -7,6 +7,7 @@ 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 class GameTurnOpsTest extends BaseTest: @@ -15,14 +16,14 @@ class GameTurnOpsTest extends BaseTest: val players = Seq(Player("p1"), Player("p2"), Player("p3")) "A Game" should "allow to change turn" in { - val game = Game(players) + val game = Game(GameMap(), players) val newGame = game.play(NextTurn)(using NextTurnEffect) newGame should be(defined) newGame.get.turn.player should be(players(1)) } it should "also change the phase if the iterator is empty" in { - val game = Game(players) + val game = Game(GameMap(), players) var newGame = game newGame.gameStatus should be(GameStatus(MyPhases.Game, Steps.Initial)) newGame = newGame.play(NextTurn)(using NextTurnEffect).get diff --git a/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala b/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala index a5b4713c..42215b42 100644 --- a/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/GameWinOpsTest.scala @@ -4,6 +4,7 @@ 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 class GameWinOpsTest extends BaseTest: @@ -12,15 +13,15 @@ class GameWinOpsTest extends BaseTest: val players = Seq(Player("p1"), Player("p2"), Player("p3")) "A Game" should "expose a isOver method" in { - val game = Game(players) + val game = Game(GameMap(), players) game.isOver shouldBe false - val endedGame = Game(players)(using EmptyDomain.rules.withWinnerFunction(_ => Some(players.head))) + 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(players) + val game = Game(GameMap(), players) game.winner shouldBe None - val endedGame = Game(players)(using EmptyDomain.rules.withWinnerFunction(_ => Some(players.head))) + 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 3282816e..12f6ad56 100644 --- a/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala +++ b/src/test/scala/scatan/lib/game/ops/RulesOpsTest.scala @@ -3,6 +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 class RulesOpsTest extends BaseTest: @@ -14,9 +15,9 @@ class RulesOpsTest extends BaseTest: } it should "allow to specify the starting state factory" in { - val newRules = rules.withStartingStateFactory((_) => State()) + val newRules = rules.withStartingStateFactory((_, _) => State()) val players = Seq(Player("a"), Player("b")) - newRules.startingStateFactory(players) should be(EmptyDomain.State()) + newRules.startingStateFactory(GameMap(), players) should be(EmptyDomain.State()) } it should "allow to specify the starting phase" in { diff --git a/src/test/scala/scatan/model/ApplicationStateTest.scala b/src/test/scala/scatan/model/ApplicationStateTest.scala index 85341e15..8c8e73c1 100644 --- a/src/test/scala/scatan/model/ApplicationStateTest.scala +++ b/src/test/scala/scatan/model/ApplicationStateTest.scala @@ -18,19 +18,19 @@ class ApplicationStateTest extends BaseTest: it should "allow to create a game" in { val applicationState: ApplicationState = ApplicationState() - val applicationState2 = applicationState.createGame("Player 1", "Player 2", "Player 3", "Player 4") + val applicationState2 = applicationState.createGame(GameMap(), "Player 1", "Player 2", "Player 3", "Player 4") applicationState2.game should not be (Option.empty[UnkownGame]) } it should "allow to create a game with 3 players" in { val applicationState: ApplicationState = ApplicationState() - val applicationState2 = applicationState.createGame("Player 1", "Player 2", "Player 3") + val applicationState2 = applicationState.createGame(GameMap(), "Player 1", "Player 2", "Player 3") applicationState2.game should not be (Option.empty[UnkownGame]) } it should "allow to create a game with 4 players" in { val applicationState: ApplicationState = ApplicationState() - val applicationState2 = applicationState.createGame("Player 1", "Player 2", "Player 3", "Player 4") + val applicationState2 = applicationState.createGame(GameMap(), "Player 1", "Player 2", "Player 3", "Player 4") applicationState2.game should not be (Option.empty[UnkownGame]) } @@ -38,7 +38,7 @@ class ApplicationStateTest extends BaseTest: val applicationState: ApplicationState = ApplicationState() for n <- 0 to 2 yield assertThrows[IllegalArgumentException] { - applicationState.createGame((1 to n).map(i => s"Player $i")*) + applicationState.createGame(GameMap(), (1 to n).map(i => s"Player $i")*) } } @@ -46,6 +46,6 @@ class ApplicationStateTest extends BaseTest: val applicationState: ApplicationState = ApplicationState() for n <- 5 to 10 yield assertThrows[IllegalArgumentException] { - applicationState.createGame((1 to n).map(i => s"Player $i")*) + applicationState.createGame(GameMap(), (1 to n).map(i => s"Player $i")*) } } diff --git a/src/test/scala/scatan/model/GameTest.scala b/src/test/scala/scatan/model/GameTest.scala index 90a8c67f..e3a1d563 100644 --- a/src/test/scala/scatan/model/GameTest.scala +++ b/src/test/scala/scatan/model/GameTest.scala @@ -29,46 +29,46 @@ class GameTest extends BaseTest: } it should "have players" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.players should be(threePlayers) } it should "expose if the game is over" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.isOver shouldBe false } it should "have a winner when the game is over" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.winner shouldBe None } it should "take players" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.players should be(threePlayers) } it should "not allow fewer than 3 players" in { for n <- 0 to 2 yield assertThrows[IllegalArgumentException] { - Game(players(n)) + Game(GameMap(), players(n)) } } it should "not allow more than 4 players" in { for n <- 5 to 10 yield assertThrows[IllegalArgumentException] { - Game(players(n)) + Game(GameMap(), players(n)) } } it should "have a status" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.gameStatus shouldBe GameStatus(ScatanPhases.Setup, ScatanSteps.SetupSettlement) } it should "have a turn" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) game.turn.number shouldBe 1 game.turn.player shouldBe threePlayers.head } @@ -87,7 +87,7 @@ class GameTest extends BaseTest: yield newGame it should "allow to change turn" in { - val game = Game(threePlayers) + val game = Game(GameMap(), threePlayers) for newGame <- nextTurn(game) do newGame.turn.number shouldBe 2 diff --git a/src/test/scala/scatan/model/game/ScatanRulesTest.scala b/src/test/scala/scatan/model/game/ScatanRulesTest.scala index a2922f63..27b04a0b 100644 --- a/src/test/scala/scatan/model/game/ScatanRulesTest.scala +++ b/src/test/scala/scatan/model/game/ScatanRulesTest.scala @@ -4,6 +4,7 @@ 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 class ScatanRulesTest extends BaseTest: @@ -19,7 +20,8 @@ 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(players) + + 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 75954f70..e6995fdf 100644 --- a/src/test/scala/scatan/model/game/ScatanStateTest.scala +++ b/src/test/scala/scatan/model/game/ScatanStateTest.scala @@ -64,6 +64,7 @@ class ScatanStateTest extends BaseScatanStateTest: it should "have a development cards deck" in { val state = ScatanState(threePlayers) state.developmentCardsDeck shouldBe a[DevelopmentCardsDeck] - val stateWithOrderedDeck = ScatanState(threePlayers, developmentCardsDeck = DevelopmentCardsDeck.defaultOrdered) + val stateWithOrderedDeck = + ScatanState(GameMap(), threePlayers, developmentCardsDeck = DevelopmentCardsDeck.defaultOrdered) stateWithOrderedDeck.developmentCardsDeck shouldBe DevelopmentCardsDeck.defaultOrdered } diff --git a/src/test/scala/scatan/model/map/GameMapFactoryTest.scala b/src/test/scala/scatan/model/map/GameMapFactoryTest.scala new file mode 100644 index 00000000..28845fef --- /dev/null +++ b/src/test/scala/scatan/model/map/GameMapFactoryTest.scala @@ -0,0 +1,24 @@ +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: + + "A GameMapFactory" should "generate a default map" in { + val map = GameMapFactory.defaultMap + map should be(GameMap()) + } + + // If the random generate the same map, it will fail. Which is the probability of that? :) + it should "generate a random map" in { + val random = GameMapFactory.randomMap + random should not be GameMap() + } + + it should "generate a map with a permutation" in { + val permutations = (1 to 100).map(_ => GameMapFactory.nextPermutation) + permutations.toSet.size should be > 1 + } diff --git a/style.css b/style.css index 22bfb2fc..8dce7cb2 100644 --- a/style.css +++ b/style.css @@ -173,6 +173,36 @@ body { margin-top: 2.5em; } +.setup-menu-map-picker { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; +} + +.setup-menu-map-picker-left-tab { + display: flex; + padding: 0 4em; + flex-direction: column; +} + +.setup-menu-map-picker-button { + font-size: 1.5em; + padding: 0.6em 0.8em; + margin-top: 3em; + border-radius: 1em; + background-color: var(--button-color); +} + +.setup-menu-map-picker-combobox { + font-size: 3em; +} + +.setup-menu-map { + width: 25%; +} + /* Game */ .game-view-left-tab { display: inline-flex; @@ -203,9 +233,11 @@ body { .game-view-phase { font-size: 1em; } + .game-view-step { font-size: 1em; } + .game-view-buttons { margin-top: 1em; display: flex;