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 2f7f7e8b..faa811ca 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 4c9f1bc4..1aa5080d 100644 --- a/src/main/scala/scatan/model/game/ScatanGame.scala +++ b/src/main/scala/scatan/model/game/ScatanGame.scala @@ -13,6 +13,9 @@ import scatan.model.game.ops.RobberOps.playersOnRobber 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 /** The status of a game of Scatan. It contains all the data without any possible action. * @param game @@ -108,5 +111,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/main/scala/scatan/views/home/AboutView.scala b/src/main/scala/scatan/views/home/AboutView.scala index 8622bf48..bb26c89b 100644 --- a/src/main/scala/scatan/views/home/AboutView.scala +++ b/src/main/scala/scatan/views/home/AboutView.scala @@ -17,9 +17,50 @@ private class ScalaJSAboutView(container: String, requirements: View.Requirement with AboutView: override def element: Element = div( + cls := "about-view-container", h1("About"), - p("This is a ScalaJS view"), + p( + "The goal of the project is to create a clone of the board game 'Settlers of Catan', a game for 3 or 4 players. " + ), + p( + "In the game, each participant takes on the role of a settler trying to establish themselves on the island of Catan. " + ), + p( + "The game is played on a board consisting of hexagonal tiles, which are arranged randomly at the beginning of the game. " + ), + p( + "The primary objective is to accumulate essential resources, including wood, clay, wheat, wool, and ore, through the construction of strategically placed settlements, cities, and roads on the island. These resources are obtained based on the results of dice rolls and the location of the structures built." + ), + h2("Built with"), + ul( + li("Scala 3"), + li("Scala.js"), + li("Laminar"), + li("ScalaTest") + ), + h2("Authors"), + ul( + // add links to github profiles + li( + a( + cls := "about-link", + "Luigi Borriello", + href := "https://github.com/luigi-borriello00" + ) + ), + li( + a( + cls := "about-link", + "Manuel Andruccioli", + href := "https://github.com/manuandru" + ) + ), + li( + a(cls := "about-link", "Alessandro Mazzoli", href := "https://github.com/alemazzo") + ) + ), button( + cls := "home-menu-button", "Back", onClick --> (_ => this.navigateBack()) ) 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 4d559af1..654c3a8a 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 838fed79..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; @@ -336,3 +368,16 @@ body { border-radius: 5px; z-index: 1000; } + +.about-view-container{ + color: white; + font-size: x-large; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 1em; + padding: 1em; +} + +.about-link{ + color: white; + text-decoration: none; +} \ No newline at end of file