From 50d3d4a9208f901b0d852a711cf1ef6ab7b36a01 Mon Sep 17 00:00:00 2001 From: Manuel Andruccioli Date: Sat, 9 Sep 2023 17:33:53 +0200 Subject: [PATCH 1/9] wip(game-view): display model with svgs --- .../scala/scatan/views/game/GameView.scala | 95 +++++++++++++++++-- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index c2a96df9..c4646c05 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -5,6 +5,9 @@ import scatan.controllers.game.GameController import scatan.Pages import com.raquo.laminar.api.L.* import scatan.mvc.lib.{ScalaJSView, View} +import scatan.model.Spot +import scatan.model.map.Hexagon +import scatan.model.GameMap trait GameView extends View @@ -14,11 +17,89 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container with ScalaJSView(container): val numberOfUsers: Int = 3 - override def element: Element = div( - h1("Game"), - p("This is a ScalaJS view"), - button( - "Back", - onClick --> (_ => controller.goToHome()) + val gameMap = GameMap(2) + + def fromHexToPoint(hex: Hexagon): (Double, Double) = + val size = 100.0 + val x = size * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) + val y = size * (3.0 / 2 * hex.row) + (x, y) + + def getPointsOfHex(hex: Hexagon): Set[(DoubleWithPrecision, DoubleWithPrecision)] = + val size = 100.0 + val x = size * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) + val y = size * (3.0 / 2 * hex.row) + val points = Set( + (DoubleWithPrecision(x), DoubleWithPrecision(y + size)), + (DoubleWithPrecision(x + size * math.sqrt(3) / 2), DoubleWithPrecision(y + size / 2)), + (DoubleWithPrecision(x + size * math.sqrt(3) / 2), DoubleWithPrecision(y - size / 2)), + (DoubleWithPrecision(x), DoubleWithPrecision(y - size)), + (DoubleWithPrecision(x - size * math.sqrt(3) / 2), DoubleWithPrecision(y - size / 2)), + (DoubleWithPrecision(x - size * math.sqrt(3) / 2), DoubleWithPrecision(y + size / 2)) + ) + points + + def getPointOfSpot(spot: Spot): Option[(DoubleWithPrecision, DoubleWithPrecision)] = + (getPointsOfHex(spot._1) & getPointsOfHex(spot._2) & getPointsOfHex(spot._3)).headOption + + case class DoubleWithPrecision(value: Double): + override def equals(x: Any): Boolean = + x match + case that: DoubleWithPrecision => + (this.value - that.value).abs < 0.001 + case _ => false + + override def hashCode: Int = (value * 1000).toInt.hashCode + + override def element: Element = + div( + display := "block", + width := "70%", + margin := "auto", + svg.svg( + svg.viewBox := "-500 -500 1000 1000", + for hex <- gameMap.tiles.toList + yield + val (x, y) = fromHexToPoint(hex) + svg.g( + svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", + svg.polygon( + svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", + svg.cls := "hexagon" + ), + onClick --> (_ => println(hex)) + ) + , + for + spots <- gameMap.edges.toList + (x1, y1) <- getPointOfSpot(spots._1) + (x2, y2) <- getPointOfSpot(spots._2) + yield svg.g( + svg.line( + svg.x1 := s"${x1.value}", + svg.y1 := s"${y1.value}", + svg.x2 := s"${x2.value}", + svg.y2 := s"${y2.value}", + svg.className := "road", + onClick --> (_ => println(spots)) + ), + svg.circle( + svg.cx := s"${x1.value + (x2.value - x1.value) / 2}", + svg.cy := s"${y1.value + (y2.value - y1.value) / 2}", + svg.r := "20", + svg.className := "road", + onClick --> (_ => println(spots)) + ) + ), + for + spot <- gameMap.nodes.toList + (x, y) <- getPointOfSpot(spot) + yield svg.circle( + svg.cx := s"${x.value}", + svg.cy := s"${y.value}", + svg.r := "10", + svg.className := "spot", + onClick --> (_ => println(spot)) + ) + ) ) - ) From f48929b4cfe1106381c7c82d6eade24ff12595dd Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sat, 9 Sep 2023 18:27:42 +0200 Subject: [PATCH 2/9] refactor(game-view): extract some methods --- .../scala/scatan/views/game/GameView.scala | 123 +++++++++++------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index c4646c05..90c0856d 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -15,33 +15,55 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container extends GameView with View.Dependencies(requirements) with ScalaJSView(container): - val numberOfUsers: Int = 3 + val hexSize = 100.0 val gameMap = GameMap(2) + /** Convert hexagon to point, the point is the center of the hexagon + * + * @param hex, + * the hexagon + * @return + * the point of the hexagon + */ def fromHexToPoint(hex: Hexagon): (Double, Double) = - val size = 100.0 - val x = size * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) - val y = size * (3.0 / 2 * hex.row) + val x = this.hexSize * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) + val y = this.hexSize * (3.0 / 2 * hex.row) (x, y) + /** Get the points of the hexagon, starting from the center of the hexagon, and then clockwise + * + * @param hex, + * the hexagon + * @return + * the points of the hexagon + */ def getPointsOfHex(hex: Hexagon): Set[(DoubleWithPrecision, DoubleWithPrecision)] = - val size = 100.0 - val x = size * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) - val y = size * (3.0 / 2 * hex.row) + val (x, y) = fromHexToPoint(hex) val points = Set( - (DoubleWithPrecision(x), DoubleWithPrecision(y + size)), - (DoubleWithPrecision(x + size * math.sqrt(3) / 2), DoubleWithPrecision(y + size / 2)), - (DoubleWithPrecision(x + size * math.sqrt(3) / 2), DoubleWithPrecision(y - size / 2)), - (DoubleWithPrecision(x), DoubleWithPrecision(y - size)), - (DoubleWithPrecision(x - size * math.sqrt(3) / 2), DoubleWithPrecision(y - size / 2)), - (DoubleWithPrecision(x - size * math.sqrt(3) / 2), DoubleWithPrecision(y + size / 2)) + (DoubleWithPrecision(x), DoubleWithPrecision(y + this.hexSize)), + (DoubleWithPrecision(x + this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y + this.hexSize / 2)), + (DoubleWithPrecision(x + this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y - this.hexSize / 2)), + (DoubleWithPrecision(x), DoubleWithPrecision(y - this.hexSize)), + (DoubleWithPrecision(x - this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y - this.hexSize / 2)), + (DoubleWithPrecision(x - this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y + this.hexSize / 2)) ) points + /** Get the point of the spot, the point is the center of the spot + * @param spot, + * the spot + * @return + * the point of the spot + */ def getPointOfSpot(spot: Spot): Option[(DoubleWithPrecision, DoubleWithPrecision)] = (getPointsOfHex(spot._1) & getPointsOfHex(spot._2) & getPointsOfHex(spot._3)).headOption + /** Double with precision, used to compare double with precision 0.001 + * + * @param value, + * the value of the double + */ case class DoubleWithPrecision(value: Double): override def equals(x: Any): Boolean = x match @@ -51,6 +73,39 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container override def hashCode: Int = (value * 1000).toInt.hashCode + private def drawHexagon(hex: Hexagon): Element = + val (x, y) = fromHexToPoint(hex) + svg.g( + svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", + svg.polygon( + svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", + svg.cls := "hexagon" + ) + ) + + private def drawRoad( + spot1: (DoubleWithPrecision, DoubleWithPrecision), + spot2: (DoubleWithPrecision, DoubleWithPrecision) + ): Element = + print(spot1, spot2) + svg.line( + svg.x1 := s"${spot1._1.value}", + svg.y1 := s"${spot1._2.value}", + svg.x2 := s"${spot2._1.value}", + svg.y2 := s"${spot2._2.value}", + svg.className := "road", + svg.stroke := "red", + svg.strokeWidth := "10" + ) + + private def drawSpot(x: DoubleWithPrecision, y: DoubleWithPrecision): Element = + svg.circle( + svg.cx := s"${x.value}", + svg.cy := s"${y.value}", + svg.r := "10", + svg.className := "spot", + svg.fill := "white" + ) override def element: Element = div( display := "block", @@ -59,47 +114,15 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container svg.svg( svg.viewBox := "-500 -500 1000 1000", for hex <- gameMap.tiles.toList - yield - val (x, y) = fromHexToPoint(hex) - svg.g( - svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", - svg.polygon( - svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", - svg.cls := "hexagon" - ), - onClick --> (_ => println(hex)) - ) - , + yield drawHexagon(hex), for spots <- gameMap.edges.toList - (x1, y1) <- getPointOfSpot(spots._1) - (x2, y2) <- getPointOfSpot(spots._2) - yield svg.g( - svg.line( - svg.x1 := s"${x1.value}", - svg.y1 := s"${y1.value}", - svg.x2 := s"${x2.value}", - svg.y2 := s"${y2.value}", - svg.className := "road", - onClick --> (_ => println(spots)) - ), - svg.circle( - svg.cx := s"${x1.value + (x2.value - x1.value) / 2}", - svg.cy := s"${y1.value + (y2.value - y1.value) / 2}", - svg.r := "20", - svg.className := "road", - onClick --> (_ => println(spots)) - ) - ), + pointsOfSpot1 <- getPointOfSpot(spots._1) + pointsOfSpot2 <- getPointOfSpot(spots._2) + yield drawRoad(pointsOfSpot1, pointsOfSpot2), for spot <- gameMap.nodes.toList (x, y) <- getPointOfSpot(spot) - yield svg.circle( - svg.cx := s"${x.value}", - svg.cy := s"${y.value}", - svg.r := "10", - svg.className := "spot", - onClick --> (_ => println(spot)) - ) + yield drawSpot(x, y) ) ) From 377fd4b1c25914410ba663a2b63a609607a7cda4 Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sat, 9 Sep 2023 18:47:45 +0200 Subject: [PATCH 3/9] refactor(game-view): extract methods --- .../scala/scatan/views/game/GameView.scala | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 90c0856d..65895387 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -73,7 +73,14 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container override def hashCode: Int = (value * 1000).toInt.hashCode - private def drawHexagon(hex: Hexagon): Element = + /** Generate the hexagon graphic + * + * @param hex, + * the hexagon + * @return + * the hexagon graphic + */ + private def generateHexagon(hex: Hexagon): Element = val (x, y) = fromHexToPoint(hex) svg.g( svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", @@ -83,28 +90,54 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container ) ) - private def drawRoad( + /** Generate the road graphic + * @param spot1, + * the first spot + * @param spot2, + * the second spot + * @return + * the road graphic + */ + private def generateRoad( spot1: (DoubleWithPrecision, DoubleWithPrecision), spot2: (DoubleWithPrecision, DoubleWithPrecision) ): Element = - print(spot1, spot2) - svg.line( - svg.x1 := s"${spot1._1.value}", - svg.y1 := s"${spot1._2.value}", - svg.x2 := s"${spot2._1.value}", - svg.y2 := s"${spot2._2.value}", - svg.className := "road", - svg.stroke := "red", - svg.strokeWidth := "10" + svg.g( + svg.line( + svg.x1 := s"${spot1._1.value}", + svg.y1 := s"${spot1._2.value}", + svg.x2 := s"${spot2._1.value}", + svg.y2 := s"${spot2._2.value}", + svg.className := "road", + svg.stroke := "red", + svg.strokeWidth := "10" + ), + svg.circle( + svg.cx := s"${spot1._1.value + (spot2._1.value - spot1._1.value) / 2}", + svg.cy := s"${spot1._2.value + (spot2._2.value - spot1._2.value) / 2}", + svg.r := "15", + svg.className := "road", + svg.fill := "yellow", + onClick --> (_ => println((spot1, spot2))) + ) ) - private def drawSpot(x: DoubleWithPrecision, y: DoubleWithPrecision): Element = + /** Generate the spot graphic + * @param x, + * the x coordinate of the spot + * @param y, + * the y coordinate of the spot + * @return + * the spot graphic + */ + private def generateSpot(x: DoubleWithPrecision, y: DoubleWithPrecision): Element = svg.circle( svg.cx := s"${x.value}", svg.cy := s"${y.value}", svg.r := "10", svg.className := "spot", - svg.fill := "white" + svg.fill := "white", + onClick --> (_ => println((x, y))) ) override def element: Element = div( @@ -114,15 +147,15 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container svg.svg( svg.viewBox := "-500 -500 1000 1000", for hex <- gameMap.tiles.toList - yield drawHexagon(hex), + yield generateHexagon(hex), for spots <- gameMap.edges.toList pointsOfSpot1 <- getPointOfSpot(spots._1) pointsOfSpot2 <- getPointOfSpot(spots._2) - yield drawRoad(pointsOfSpot1, pointsOfSpot2), + yield generateRoad(pointsOfSpot1, pointsOfSpot2), for spot <- gameMap.nodes.toList (x, y) <- getPointOfSpot(spot) - yield drawSpot(x, y) + yield generateSpot(x, y) ) ) From e63aca7937e409c95a85b186c5942819174212fe Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sat, 9 Sep 2023 18:48:45 +0200 Subject: [PATCH 4/9] chore(doc): change indentation --- src/main/scala/scatan/views/game/GameView.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 65895387..5197c2f5 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -73,13 +73,13 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container override def hashCode: Int = (value * 1000).toInt.hashCode - /** Generate the hexagon graphic - * - * @param hex, - * the hexagon - * @return - * the hexagon graphic - */ + /** Generate the hexagon graphic + * + * @param hex, + * the hexagon + * @return + * the hexagon graphic + */ private def generateHexagon(hex: Hexagon): Element = val (x, y) = fromHexToPoint(hex) svg.g( From 2472da9116e0ef04a3ad00c6262dece4b9520f5b Mon Sep 17 00:00:00 2001 From: Manuel Andruccioli Date: Sun, 10 Sep 2023 12:52:32 +0200 Subject: [PATCH 5/9] refactor: extract some interaface and extension methods --- build.sbt | 1 + .../scala/scatan/views/game/GameView.scala | 162 +++++++++++------- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/build.sbt b/build.sbt index 498b950f..6176b4ed 100644 --- a/build.sbt +++ b/build.sbt @@ -2,6 +2,7 @@ ThisBuild / scalaVersion := "3.3.0" wartremoverWarnings ++= Warts.all wartremoverWarnings -= Wart.Equals +wartremoverWarnings -= Wart.ImplicitParameter lazy val scatan = (project in file(".")) .enablePlugins(ScalaJSPlugin) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 5197c2f5..3b2a00e5 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -8,70 +8,101 @@ import scatan.mvc.lib.{ScalaJSView, View} import scatan.model.Spot import scatan.model.map.Hexagon import scatan.model.GameMap +import scatan.views.game.Coordinates.center +import scatan.views.game.Coordinates.coordinates trait GameView extends View -class ScalaJsGameView(requirements: View.Requirements[GameController], container: String) - extends GameView - with View.Dependencies(requirements) - with ScalaJSView(container): +/** @param value + */ +final case class DoubleWithPrecision(value: Double): + override def equals(x: Any): Boolean = + x match + case that: DoubleWithPrecision => + (this.value - that.value).abs < 0.001 + case _ => false - val hexSize = 100.0 - val gameMap = GameMap(2) + override def hashCode: Int = (value * 1000).round.toInt.hashCode + +/** A trait to represent cartesian coordinates. + */ +trait Coordinates: + def x: DoubleWithPrecision + def y: DoubleWithPrecision + +object Coordinates: - /** Convert hexagon to point, the point is the center of the hexagon + /** Create a new Coordinates object. * - * @param hex, - * the hexagon + * @param x + * the x coordinate + * @param y + * the y coordinate * @return - * the point of the hexagon + * the new Coordinates object */ - def fromHexToPoint(hex: Hexagon): (Double, Double) = - val x = this.hexSize * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) - val y = this.hexSize * (3.0 / 2 * hex.row) - (x, y) + def apply(x: DoubleWithPrecision, y: DoubleWithPrecision): Coordinates = CoordinatesImpl(x, y) - /** Get the points of the hexagon, starting from the center of the hexagon, and then clockwise + /** Extract the x and y coordinates from a Coordinates object, unwrapping them to double. * - * @param hex, - * the hexagon + * @param coordinates + * the coordinates to extract from * @return - * the points of the hexagon + * a pair of doubles. */ - def getPointsOfHex(hex: Hexagon): Set[(DoubleWithPrecision, DoubleWithPrecision)] = - val (x, y) = fromHexToPoint(hex) - val points = Set( - (DoubleWithPrecision(x), DoubleWithPrecision(y + this.hexSize)), - (DoubleWithPrecision(x + this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y + this.hexSize / 2)), - (DoubleWithPrecision(x + this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y - this.hexSize / 2)), - (DoubleWithPrecision(x), DoubleWithPrecision(y - this.hexSize)), - (DoubleWithPrecision(x - this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y - this.hexSize / 2)), - (DoubleWithPrecision(x - this.hexSize * math.sqrt(3) / 2), DoubleWithPrecision(y + this.hexSize / 2)) - ) - points + def unapply(coordinates: Coordinates): (Double, Double) = + (coordinates.x.value, coordinates.y.value) - /** Get the point of the spot, the point is the center of the spot - * @param spot, - * the spot - * @return - * the point of the spot + private case class CoordinatesImpl(x: DoubleWithPrecision, y: DoubleWithPrecision) extends Coordinates + + /** Convert a pair of doubles to a Coordinates object. */ - def getPointOfSpot(spot: Spot): Option[(DoubleWithPrecision, DoubleWithPrecision)] = - (getPointsOfHex(spot._1) & getPointsOfHex(spot._2) & getPointsOfHex(spot._3)).headOption + given Conversion[(Double, Double), Coordinates] with + def apply(pair: (Double, Double)): Coordinates = + Coordinates(DoubleWithPrecision(pair._1), DoubleWithPrecision(pair._2)) - /** Double with precision, used to compare double with precision 0.001 - * - * @param value, - * the value of the double + /** Extension methods to handle hexagon in cartesian plane. */ - case class DoubleWithPrecision(value: Double): - override def equals(x: Any): Boolean = - x match - case that: DoubleWithPrecision => - (this.value - that.value).abs < 0.001 - case _ => false + extension (hex: Hexagon) + /** Get the center, based on cantesian coordinates of the Hexagon. + * + * @return + * the center point of the hexagon + */ + def center(using hexSize: Int): Coordinates = + val x = hexSize * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) + val y = hexSize * (3.0 / 2 * hex.row) + (x, y) - override def hashCode: Int = (value * 1000).toInt.hashCode + /** Get the vertices of the hexagon. + * + * @return + * the points of the hexagon + */ + def vertices(using hexSize: Int): Set[Coordinates] = + val Coordinates(x, y) = center + for + i <- (0 to 5).toSet + angle_deg = 60 * i - 30 + angle_rad = (math.Pi / 180.0) * angle_deg + yield ( + x + hexSize * math.cos(angle_rad), + y + hexSize * math.sin(angle_rad) + ) + + /** Extension methods to handle spot in cartesian plane. + */ + extension (spot: Spot) + def coordinates(using hexSize: Int): Option[Coordinates] = + (spot._1.vertices & spot._2.vertices & spot._3.vertices).headOption + +class ScalaJsGameView(requirements: View.Requirements[GameController], container: String) + extends GameView + with View.Dependencies(requirements) + with ScalaJSView(container): + + given hexSize: Int = 100 + val gameMap = GameMap(2) /** Generate the hexagon graphic * @@ -81,7 +112,7 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container * the hexagon graphic */ private def generateHexagon(hex: Hexagon): Element = - val (x, y) = fromHexToPoint(hex) + val Coordinates(x, y) = hex.center svg.g( svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", svg.polygon( @@ -98,23 +129,22 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container * @return * the road graphic */ - private def generateRoad( - spot1: (DoubleWithPrecision, DoubleWithPrecision), - spot2: (DoubleWithPrecision, DoubleWithPrecision) - ): Element = + private def generateRoad(spot1: Coordinates, spot2: Coordinates): Element = + val Coordinates(x1, y1) = spot1 + val Coordinates(x2, y2) = spot2 svg.g( svg.line( - svg.x1 := s"${spot1._1.value}", - svg.y1 := s"${spot1._2.value}", - svg.x2 := s"${spot2._1.value}", - svg.y2 := s"${spot2._2.value}", + svg.x1 := s"${x1}", + svg.y1 := s"${y1}", + svg.x2 := s"${x2}", + svg.y2 := s"${y2}", svg.className := "road", svg.stroke := "red", svg.strokeWidth := "10" ), svg.circle( - svg.cx := s"${spot1._1.value + (spot2._1.value - spot1._1.value) / 2}", - svg.cy := s"${spot1._2.value + (spot2._2.value - spot1._2.value) / 2}", + svg.cx := s"${x1 + (x2 - x1) / 2}", + svg.cy := s"${y1 + (y2 - y1) / 2}", svg.r := "15", svg.className := "road", svg.fill := "yellow", @@ -130,15 +160,17 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container * @return * the spot graphic */ - private def generateSpot(x: DoubleWithPrecision, y: DoubleWithPrecision): Element = + private def generateSpot(coordinate: Coordinates): Element = + val Coordinates(x, y) = coordinate svg.circle( - svg.cx := s"${x.value}", - svg.cy := s"${y.value}", + svg.cx := s"${x}", + svg.cy := s"${y}", svg.r := "10", svg.className := "spot", svg.fill := "white", onClick --> (_ => println((x, y))) ) + override def element: Element = div( display := "block", @@ -150,12 +182,12 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container yield generateHexagon(hex), for spots <- gameMap.edges.toList - pointsOfSpot1 <- getPointOfSpot(spots._1) - pointsOfSpot2 <- getPointOfSpot(spots._2) + pointsOfSpot1 <- spots._1.coordinates + pointsOfSpot2 <- spots._2.coordinates yield generateRoad(pointsOfSpot1, pointsOfSpot2), for spot <- gameMap.nodes.toList - (x, y) <- getPointOfSpot(spot) - yield generateSpot(x, y) + coordinates <- spot.coordinates + yield generateSpot(coordinates) ) ) From f943472b4891a8a6b2bdf4471daf410ee270485f Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sun, 10 Sep 2023 17:08:51 +0200 Subject: [PATCH 6/9] refactor(game-view): extract Coordinate module --- src/main/scala/scatan/views/Coordinate.scala | 87 ++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/scala/scatan/views/Coordinate.scala diff --git a/src/main/scala/scatan/views/Coordinate.scala b/src/main/scala/scatan/views/Coordinate.scala new file mode 100644 index 00000000..bb4f8a4a --- /dev/null +++ b/src/main/scala/scatan/views/Coordinate.scala @@ -0,0 +1,87 @@ +package scatan.views + +import scatan.model.map.Hexagon +import scatan.model.Spot + +/** @param value + */ +final case class DoubleWithPrecision(value: Double): + override def equals(x: Any): Boolean = + x match + case that: DoubleWithPrecision => + (this.value - that.value).abs < 0.001 + case _ => false + + override def hashCode: Int = (value * 1000).round.toInt.hashCode + +/** A trait to represent cartesian coordinates. + */ +trait Coordinates: + def x: DoubleWithPrecision + def y: DoubleWithPrecision + +object Coordinates: + + /** Create a new Coordinates object. + * + * @param x + * the x coordinate + * @param y + * the y coordinate + * @return + * the new Coordinates object + */ + def apply(x: DoubleWithPrecision, y: DoubleWithPrecision): Coordinates = CoordinatesImpl(x, y) + + /** Extract the x and y coordinates from a Coordinates object, unwrapping them to double. + * + * @param coordinates + * the coordinates to extract from + * @return + * a pair of doubles. + */ + def unapply(coordinates: Coordinates): (Double, Double) = + (coordinates.x.value, coordinates.y.value) + + private case class CoordinatesImpl(x: DoubleWithPrecision, y: DoubleWithPrecision) extends Coordinates + + /** Convert a pair of doubles to a Coordinates object. + */ + given Conversion[(Double, Double), Coordinates] with + def apply(pair: (Double, Double)): Coordinates = + Coordinates(DoubleWithPrecision(pair._1), DoubleWithPrecision(pair._2)) + + /** Extension methods to handle hexagon in cartesian plane. + */ + extension (hex: Hexagon) + /** Get the center, based on cantesian coordinates of the Hexagon. + * + * @return + * the center point of the hexagon + */ + def center(using hexSize: Int): Coordinates = + val x = hexSize * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) + val y = hexSize * (3.0 / 2 * hex.row) + (x, y) + + /** Get the vertices of the hexagon. + * + * @return + * the points of the hexagon + */ + def vertices(using hexSize: Int): Set[Coordinates] = + val Coordinates(x, y) = center + for + i <- (0 to 5).toSet + angle_deg = 60 * i - 30 + angle_rad = (math.Pi / 180.0) * angle_deg + yield ( + x + hexSize * math.cos(angle_rad), + y + hexSize * math.sin(angle_rad) + ) + + /** Extension methods to handle spot in cartesian plane. + */ + extension (spot: Spot) + def coordinates(using hexSize: Int): Option[Coordinates] = + (spot._1.vertices & spot._2.vertices & spot._3.vertices).headOption From 98b71357d347d52cc1e872faaee027e1871bce55 Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sun, 10 Sep 2023 17:22:58 +0200 Subject: [PATCH 7/9] refactor(game-view): extract GameMapComponent --- .../scala/scatan/views/game/GameView.scala | 169 +----------------- .../game/components/GameMapComponent.scala | 91 ++++++++++ 2 files changed, 94 insertions(+), 166 deletions(-) create mode 100644 src/main/scala/scatan/views/game/components/GameMapComponent.scala diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 3b2a00e5..9fc83250 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -8,94 +8,11 @@ import scatan.mvc.lib.{ScalaJSView, View} import scatan.model.Spot import scatan.model.map.Hexagon import scatan.model.GameMap -import scatan.views.game.Coordinates.center -import scatan.views.game.Coordinates.coordinates +import scatan.views.game.components.GameMapComponent +import scatan.views.game.components.GameMapComponent.getMapComponent trait GameView extends View -/** @param value - */ -final case class DoubleWithPrecision(value: Double): - override def equals(x: Any): Boolean = - x match - case that: DoubleWithPrecision => - (this.value - that.value).abs < 0.001 - case _ => false - - override def hashCode: Int = (value * 1000).round.toInt.hashCode - -/** A trait to represent cartesian coordinates. - */ -trait Coordinates: - def x: DoubleWithPrecision - def y: DoubleWithPrecision - -object Coordinates: - - /** Create a new Coordinates object. - * - * @param x - * the x coordinate - * @param y - * the y coordinate - * @return - * the new Coordinates object - */ - def apply(x: DoubleWithPrecision, y: DoubleWithPrecision): Coordinates = CoordinatesImpl(x, y) - - /** Extract the x and y coordinates from a Coordinates object, unwrapping them to double. - * - * @param coordinates - * the coordinates to extract from - * @return - * a pair of doubles. - */ - def unapply(coordinates: Coordinates): (Double, Double) = - (coordinates.x.value, coordinates.y.value) - - private case class CoordinatesImpl(x: DoubleWithPrecision, y: DoubleWithPrecision) extends Coordinates - - /** Convert a pair of doubles to a Coordinates object. - */ - given Conversion[(Double, Double), Coordinates] with - def apply(pair: (Double, Double)): Coordinates = - Coordinates(DoubleWithPrecision(pair._1), DoubleWithPrecision(pair._2)) - - /** Extension methods to handle hexagon in cartesian plane. - */ - extension (hex: Hexagon) - /** Get the center, based on cantesian coordinates of the Hexagon. - * - * @return - * the center point of the hexagon - */ - def center(using hexSize: Int): Coordinates = - val x = hexSize * (math.sqrt(3) * hex.col + math.sqrt(3) / 2 * hex.row) - val y = hexSize * (3.0 / 2 * hex.row) - (x, y) - - /** Get the vertices of the hexagon. - * - * @return - * the points of the hexagon - */ - def vertices(using hexSize: Int): Set[Coordinates] = - val Coordinates(x, y) = center - for - i <- (0 to 5).toSet - angle_deg = 60 * i - 30 - angle_rad = (math.Pi / 180.0) * angle_deg - yield ( - x + hexSize * math.cos(angle_rad), - y + hexSize * math.sin(angle_rad) - ) - - /** Extension methods to handle spot in cartesian plane. - */ - extension (spot: Spot) - def coordinates(using hexSize: Int): Option[Coordinates] = - (spot._1.vertices & spot._2.vertices & spot._3.vertices).headOption - class ScalaJsGameView(requirements: View.Requirements[GameController], container: String) extends GameView with View.Dependencies(requirements) @@ -104,90 +21,10 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container given hexSize: Int = 100 val gameMap = GameMap(2) - /** Generate the hexagon graphic - * - * @param hex, - * the hexagon - * @return - * the hexagon graphic - */ - private def generateHexagon(hex: Hexagon): Element = - val Coordinates(x, y) = hex.center - svg.g( - svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", - svg.polygon( - svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", - svg.cls := "hexagon" - ) - ) - - /** Generate the road graphic - * @param spot1, - * the first spot - * @param spot2, - * the second spot - * @return - * the road graphic - */ - private def generateRoad(spot1: Coordinates, spot2: Coordinates): Element = - val Coordinates(x1, y1) = spot1 - val Coordinates(x2, y2) = spot2 - svg.g( - svg.line( - svg.x1 := s"${x1}", - svg.y1 := s"${y1}", - svg.x2 := s"${x2}", - svg.y2 := s"${y2}", - svg.className := "road", - svg.stroke := "red", - svg.strokeWidth := "10" - ), - svg.circle( - svg.cx := s"${x1 + (x2 - x1) / 2}", - svg.cy := s"${y1 + (y2 - y1) / 2}", - svg.r := "15", - svg.className := "road", - svg.fill := "yellow", - onClick --> (_ => println((spot1, spot2))) - ) - ) - - /** Generate the spot graphic - * @param x, - * the x coordinate of the spot - * @param y, - * the y coordinate of the spot - * @return - * the spot graphic - */ - private def generateSpot(coordinate: Coordinates): Element = - val Coordinates(x, y) = coordinate - svg.circle( - svg.cx := s"${x}", - svg.cy := s"${y}", - svg.r := "10", - svg.className := "spot", - svg.fill := "white", - onClick --> (_ => println((x, y))) - ) - override def element: Element = div( display := "block", width := "70%", margin := "auto", - svg.svg( - svg.viewBox := "-500 -500 1000 1000", - for hex <- gameMap.tiles.toList - yield generateHexagon(hex), - for - spots <- gameMap.edges.toList - pointsOfSpot1 <- spots._1.coordinates - pointsOfSpot2 <- spots._2.coordinates - yield generateRoad(pointsOfSpot1, pointsOfSpot2), - for - spot <- gameMap.nodes.toList - coordinates <- spot.coordinates - yield generateSpot(coordinates) - ) + getMapComponent(gameMap) ) diff --git a/src/main/scala/scatan/views/game/components/GameMapComponent.scala b/src/main/scala/scatan/views/game/components/GameMapComponent.scala new file mode 100644 index 00000000..d8e74ddb --- /dev/null +++ b/src/main/scala/scatan/views/game/components/GameMapComponent.scala @@ -0,0 +1,91 @@ +package scatan.views.game.components + +import scatan.model.map.Hexagon +import scatan.model.GameMap +import com.raquo.laminar.api.L.* +import scatan.views.Coordinates.* +import scatan.views.Coordinates + +object GameMapComponent: + /** Generate the hexagon graphic + * + * @param hex, + * the hexagon + * @return + * the hexagon graphic + */ + private def generateHexagon(hex: Hexagon)(using hexSize: Int): Element = + val Coordinates(x, y) = hex.center + svg.g( + svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", + svg.polygon( + svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", + svg.cls := "hexagon" + ) + ) + + /** Generate the road graphic + * @param spot1, + * the first spot + * @param spot2, + * the second spot + * @return + * the road graphic + */ + private def generateRoad(spot1: Coordinates, spot2: Coordinates): Element = + val Coordinates(x1, y1) = spot1 + val Coordinates(x2, y2) = spot2 + svg.g( + svg.line( + svg.x1 := s"${x1}", + svg.y1 := s"${y1}", + svg.x2 := s"${x2}", + svg.y2 := s"${y2}", + svg.className := "road", + svg.stroke := "red", + svg.strokeWidth := "10" + ), + svg.circle( + svg.cx := s"${x1 + (x2 - x1) / 2}", + svg.cy := s"${y1 + (y2 - y1) / 2}", + svg.r := "15", + svg.className := "road", + svg.fill := "yellow", + onClick --> (_ => println((spot1, spot2))) + ) + ) + + /** Generate the spot graphic + * @param x, + * the x coordinate of the spot + * @param y, + * the y coordinate of the spot + * @return + * the spot graphic + */ + private def generateSpot(coordinate: Coordinates): Element = + val Coordinates(x, y) = coordinate + svg.circle( + svg.cx := s"${x}", + svg.cy := s"${y}", + svg.r := "10", + svg.className := "spot", + svg.fill := "white", + onClick --> (_ => println((x, y))) + ) + + def getMapComponent(gameMap: GameMap)(using hexSize: Int): Element = + svg.svg( + svg.viewBox := "-500 -500 1000 1000", + for hex <- gameMap.tiles.toList + yield generateHexagon(hex), + for + spots <- gameMap.edges.toList + pointsOfSpot1 <- spots._1.coordinates + pointsOfSpot2 <- spots._2.coordinates + yield generateRoad(pointsOfSpot1, pointsOfSpot2), + for + spot <- gameMap.nodes.toList + coordinates <- spot.coordinates + yield generateSpot(coordinates) + ) From 0633567dada791aa2136d072aaa3f98210061b44 Mon Sep 17 00:00:00 2001 From: luigi-borriello00 Date: Sun, 10 Sep 2023 17:31:42 +0200 Subject: [PATCH 8/9] docs(game-map-component): add doc to object --- src/main/scala/scatan/views/game/GameView.scala | 1 - .../scala/scatan/views/game/components/GameMapComponent.scala | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 9fc83250..0cef1eae 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -8,7 +8,6 @@ import scatan.mvc.lib.{ScalaJSView, View} import scatan.model.Spot import scatan.model.map.Hexagon import scatan.model.GameMap -import scatan.views.game.components.GameMapComponent import scatan.views.game.components.GameMapComponent.getMapComponent trait GameView extends View diff --git a/src/main/scala/scatan/views/game/components/GameMapComponent.scala b/src/main/scala/scatan/views/game/components/GameMapComponent.scala index d8e74ddb..b39a80cc 100644 --- a/src/main/scala/scatan/views/game/components/GameMapComponent.scala +++ b/src/main/scala/scatan/views/game/components/GameMapComponent.scala @@ -6,6 +6,8 @@ import com.raquo.laminar.api.L.* import scatan.views.Coordinates.* import scatan.views.Coordinates +/** A component to display the game map. + */ object GameMapComponent: /** Generate the hexagon graphic * From 3a2bf2a5818eb45038d6d2e352678973a09169b0 Mon Sep 17 00:00:00 2001 From: Manuel Andruccioli Date: Sun, 10 Sep 2023 18:41:29 +0200 Subject: [PATCH 9/9] wip(map-view): add style and refactor --- .../scala/scatan/views/game/GameView.scala | 50 +++++++++++++++---- style.css | 25 ++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/main/scala/scatan/views/game/GameView.scala b/src/main/scala/scatan/views/game/GameView.scala index 3b2a00e5..052dc472 100644 --- a/src/main/scala/scatan/views/game/GameView.scala +++ b/src/main/scala/scatan/views/game/GameView.scala @@ -10,6 +10,8 @@ import scatan.model.map.Hexagon import scatan.model.GameMap import scatan.views.game.Coordinates.center import scatan.views.game.Coordinates.coordinates +import scatan.model.map.Terrain.* +import com.raquo.airstream.core.Signal trait GameView extends View @@ -114,10 +116,11 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container private def generateHexagon(hex: Hexagon): Element = val Coordinates(x, y) = hex.center svg.g( - svg.transform := s"translate($x, $y) rotate(30) scale(0.95)", + svg.transform := s"translate($x, $y)", svg.polygon( - svg.points := "100,0 50,-87 -50,-87 -100,-0 -50,87 50,87", - svg.cls := "hexagon" + svg.points := "0,100 87,50 87,-50 0,-100 -87,-50 -87,50", // TODO: refactor? + svg.cls := "hexagon", + svg.fill := s"url(#img-${resources.head._1.toString.toLowerCase})" // TODO: add map from model terrain ) ) @@ -138,16 +141,13 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container svg.y1 := s"${y1}", svg.x2 := s"${x2}", svg.y2 := s"${y2}", - svg.className := "road", - svg.stroke := "red", - svg.strokeWidth := "10" + svg.className := "road" ), svg.circle( svg.cx := s"${x1 + (x2 - x1) / 2}", svg.cy := s"${y1 + (y2 - y1) / 2}", - svg.r := "15", - svg.className := "road", - svg.fill := "yellow", + svg.className := "road-center", + svg.r := "25", onClick --> (_ => println((spot1, spot2))) ) ) @@ -165,18 +165,46 @@ class ScalaJsGameView(requirements: View.Requirements[GameController], container svg.circle( svg.cx := s"${x}", svg.cy := s"${y}", - svg.r := "10", + svg.r := "25", svg.className := "spot", - svg.fill := "white", onClick --> (_ => println((x, y))) ) + val resources = Map( + WOOD -> "res/img/hexagonal/wood.jpg", + SHEEP -> "res/img/hexagonal/sheep.jpg", + GRAIN -> "res/img/hexagonal/wheat.jpg", + ROCK -> "res/img/hexagonal/ore.jpg", + BRICK -> "res/img/hexagonal/clay.jpg", + DESERT -> "res/img/hexagonal/desert.jpg" + ) + + private val svgImages: Element = + svg.svg( + svg.defs( + for (terrain, path) <- resources.toList + yield svg.pattern( + svg.idAttr := s"img-${terrain.toString.toLowerCase}", + svg.width := "100%", + svg.height := "100%", + svg.patternContentUnits := "objectBoundingBox", + svg.image( + svg.href := path, + svg.width := "1", + svg.height := "1", + svg.preserveAspectRatio := "none" + ) + ) + ) + ) + override def element: Element = div( display := "block", width := "70%", margin := "auto", svg.svg( + svgImages, svg.viewBox := "-500 -500 1000 1000", for hex <- gameMap.tiles.toList yield generateHexagon(hex), diff --git a/style.css b/style.css index 1e20b1d4..6c754150 100644 --- a/style.css +++ b/style.css @@ -5,7 +5,32 @@ body { /* Set the background size to cover the entire body */ background-size: cover; + --hover: #f2f2f2; + --road: #363636; } + +.spot { + /* fill: var(--spot); */ + opacity: 0; +} + +.spot:hover, .road-center:hover { + fill: var(--hover); + transition: all 0.2s ease-out; + opacity: 0.4; +} + +.road { + stroke: var(--road); + stroke-width: 10px; + stroke-linecap: round; +} + +.road-center { + fill: white; + opacity: 0; +} + .home-view{ margin-top: 1em; margin-bottom: 1em;