From b44d84c118d3a61583199ae08d596a89c3cf44bb Mon Sep 17 00:00:00 2001 From: Zhen-hao Date: Sun, 31 May 2020 12:12:45 +0200 Subject: [PATCH] use akka 2.6.5 and korolev 0.15.1 --- .gitignore | 17 +- .scalafmt.conf | 18 + build.sbt | 8 +- .../match3/client/Application.scala | 163 +++---- .../match3/client/PlayerProxyExtension.scala | 108 +++-- .../com/tenderowls/match3/client/State.scala | 17 +- .../client/components/BoardComponent.scala | 435 +++++++++--------- .../match3/client/view/BoardView.scala | 9 +- .../scala/com/tenderowls/match3/Board.scala | 94 ++-- .../com/tenderowls/match3/BoardAdviser.scala | 112 ++--- .../tenderowls/match3/BoardGenerator.scala | 14 +- .../com/tenderowls/match3/AdviserSpec.scala | 6 +- .../com/tenderowls/match3/BoardSpec.scala | 67 +-- .../com/tenderowls/match3/GeneratorSpec.scala | 19 +- project/build.properties | 2 +- project/metals.sbt | 4 + project/plugins.sbt | 3 +- .../match3/server/actors/BoardActor.scala | 32 +- .../match3/server/actors/GameActor.scala | 68 ++- .../match3/server/actors/LobbyActor.scala | 4 +- .../match3/server/actors/PlayerActor.scala | 13 +- 21 files changed, 562 insertions(+), 651 deletions(-) create mode 100755 .scalafmt.conf create mode 100644 project/metals.sbt diff --git a/.gitignore b/.gitignore index 612c5bc..586be67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ -target +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +.history +.cache +.lib/ .idea -*.iml +.bloop* +.metals* +*.db +*.log +.idea +*.iml \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100755 index 0000000..83ceade --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,18 @@ +version = "2.2.1" + +style = defaultWithAlign + +docstrings = ScalaDoc + +align = more + +maxColumn = 120 +rewrite.rules = [RedundantParens, SortImports, PreferCurlyFors] +unindentTopLevelOperators = true +align.tokens = [{code = "=>", owner = "Case"}] +align.openParenDefnSite = false +align.openParenCallSite = false +optIn.breakChainOnFirstMethodDot = false +optIn.configStyleArguments = false +danglingParentheses = false +spaces.inImportCurlyBraces = true diff --git a/build.sbt b/build.sbt index 1b74178..2a9f4a2 100644 --- a/build.sbt +++ b/build.sbt @@ -1,10 +1,10 @@ -val akkaVersion = "2.5.26" -val korolevVersion = "0.14.0" +val akkaVersion = "2.6.5" +val korolevVersion = "0.15.1" val commonSettings = Seq( scalacOptions ++= Seq("-Yrangepos", "-deprecation"), organization := "com.tenderowls", version := "1.0.0-SNAPSHOT", - scalaVersion := "2.13.1" + scalaVersion := "2.13.2" ) lazy val match3 = project @@ -37,7 +37,7 @@ lazy val client = project dockerUpdateLatest := true, normalizedName := "match3-client", libraryDependencies ++= Seq( - "com.github.fomkin" %% "korolev-server-akkahttp" % korolevVersion, + "com.github.fomkin" %% "korolev-akka" % korolevVersion, "org.slf4j" % "slf4j-simple" % "1.7.+" ) ) diff --git a/client/src/main/scala/com/tenderowls/match3/client/Application.scala b/client/src/main/scala/com/tenderowls/match3/client/Application.scala index a4125a6..b03f4c4 100644 --- a/client/src/main/scala/com/tenderowls/match3/client/Application.scala +++ b/client/src/main/scala/com/tenderowls/match3/client/Application.scala @@ -10,24 +10,23 @@ import com.tenderowls.match3._ import com.tenderowls.match3.client.components.BoardComponent import com.tenderowls.match3.client.components.BoardComponent.Rgb import com.tenderowls.match3.server.actors.LobbyActor -import com.tenderowls.match3.server.data.{ColorCell, PlayerInfo, Score} -import korolev.akkahttp._ -import korolev.execution._ +import com.tenderowls.match3.server.data.{ ColorCell, PlayerInfo, Score } +import scala.concurrent.ExecutionContext.Implicits.global import korolev.server._ import korolev.state.javaSerialization._ import levsha.dsl._ import levsha.dsl.html._ - +import korolev.akka._ import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.Random +import korolev.akka.AkkaHttpServerConfig object Application extends App { import State.globalContext._ - private implicit val actorSystem = ActorSystem("match3", defaultExecutionContext = Some(defaultExecutor)) - private implicit val materializer: ActorMaterializer = ActorMaterializer() + private implicit val actorSystem = ActorSystem("match3") private implicit val askTimeout: Timeout = 1.second private implicit val akkaScheduler: actor.Scheduler = actorSystem.scheduler @@ -67,9 +66,10 @@ object Application extends App { head = { _ => Seq( link(href := "static/main.css", rel := "stylesheet", `type` := "text/css"), - meta(name :="viewport", content := "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"), - script(language := "javascript", src := "static/gestures.js") - ) + meta( + name := "viewport", + content := "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"), + script(language := "javascript", src := "static/gestures.js")) }, render = { case State.Login => @@ -83,121 +83,97 @@ object Application extends App { optimize { body( - div(display @= "flex", + div( + display @= "flex", alignItems @= "center", flexDirection @= "column", h3(textAlign @= "center", "Multiplayer match-three"), - h6(textAlign @= "center", fontStyle @= "italic", + h6( + textAlign @= "center", + fontStyle @= "italic", "Written in Scala and Korolev by Aleksey Fomkin", - a(fontStyle @= "italic", display @= "block", href := "https://github.com/tenderowls/match3", "https://github.com/tenderowls/match3") - ), - form(clazz := "panel", marginTop @= "15px", display @= "flex", + a( + fontStyle @= "italic", + display @= "block", + href := "https://github.com/tenderowls/match3", + "https://github.com/tenderowls/match3")), + form( + clazz := "panel", + marginTop @= "15px", + display @= "flex", input(nameInputId, `type` := "text", placeholder := "Your nickname"), button("Enter lobby"), - event("submit")(onSubmit) - ) - ) - ) + event("submit")(onSubmit)))) } case State.LoggedIn(name, State.YouWin) => optimize { - body( - div(clazz := "panel", - h2("You win! ❤️"), - enterLobbyButton(name) - ) - ) + body(div(clazz := "panel", h2("You win! ❤️"), enterLobbyButton(name))) } case State.LoggedIn(name, State.YouLose) => optimize { - body( - div(clazz := "panel", - h2("You lose. \uD83D\uDCA9"), - enterLobbyButton(name) - ) - ) + body(div(clazz := "panel", h2("You lose. \uD83D\uDCA9"), enterLobbyButton(name))) } case State.LoggedIn(name, State.Draw) => optimize { - body( - div(clazz := "panel", - h2("Draw"), - enterLobbyButton(name) - ) - ) + body(div(clazz := "panel", h2("Draw"), enterLobbyButton(name))) } case State.LoggedIn(_, State.Lobby) => optimize { body( - div(clazz := "panel", + div( + clazz := "panel", h2("Looking for opponent..."), - div( - button( - "Play with bot", - event("click")(_.publish(ClientEvent.PlayWithBot)) - ) - ) - ) - ) + div(button("Play with bot", event("click")(_.publish(ClientEvent.PlayWithBot)))))) } case State.LoggedIn(_, State.Game(gameInfo, boardParams)) => - def moveIndicator(player: PlayerInfo, score: Score) = { val thisMove = gameInfo.currentPlayer == player optimize { div( clazz := ( - if (thisMove) "move-indicator move-indicator__current" - else "move-indicator" - ), + if (thisMove) "move-indicator move-indicator__current" + else "move-indicator" + ), div( display @= "flex", justifyContent @= "space-between", player.name, - when(thisMove)(div(gameInfo.timeRemaining.map(s => s.toSeconds.toString))) - ), - renderScore(score) - ) + when(thisMove)(div(gameInfo.timeRemaining.map(s => s.toSeconds.toString)))), + renderScore(score)) } } - val board = BoardComponent.create(boardParams) { (access, event) => - event match { - case BoardComponent.Event.Move(swap) => - access.publish(ClientEvent.MakeMove(swap)) - case BoardComponent.Event.AnimationEnd => - access.publish(ClientEvent.SyncAnimation) - case BoardComponent.Event.AddScore(score) => - access.maybeTransition { - case state@State.LoggedIn(_, game@State.Game(info, _)) if info.currentPlayer == info.you => - state.copy(state = game.copy(info = info.copy(yourScore = info.yourScore + score))) - case state@State.LoggedIn(_, game@State.Game(info, _)) if info.currentPlayer == info.opponent => - state.copy(state = game.copy(info = info.copy(opponentScore = info.opponentScore + score))) - } - } + val board = BoardComponent.create(boardParams) { + (access, event) => + event match { + case BoardComponent.Event.Move(swap) => + access.publish(ClientEvent.MakeMove(swap)) + case BoardComponent.Event.AnimationEnd => + access.publish(ClientEvent.SyncAnimation) + case BoardComponent.Event.AddScore(score) => + access.maybeTransition { + case state @ State.LoggedIn(_, game @ State.Game(info, _)) if info.currentPlayer == info.you => + state.copy(state = game.copy(info = info.copy(yourScore = info.yourScore + score))) + case state @ State.LoggedIn(_, game @ State.Game(info, _)) if info.currentPlayer == info.opponent => + state.copy(state = game.copy(info = info.copy(opponentScore = info.opponentScore + score))) + } + } } optimize { - body( - delay(1.second) { access => - access.maybeTransition { - case state@State.LoggedIn(_, game: State.Game) => - state.copy(state = game.copy(info = game.info.copy(timeRemaining = game.info.timeRemaining.map(_ - 1.second)))) - } - }, - div(clazz := "game", - moveIndicator(gameInfo.you, gameInfo.yourScore), - board, - moveIndicator(gameInfo.opponent, gameInfo.opponentScore) - ) - ) + body(delay(1.second) { access => + access.maybeTransition { + case state @ State.LoggedIn(_, game: State.Game) => + state.copy( + state = game.copy(info = game.info.copy(timeRemaining = game.info.timeRemaining.map(_ - 1.second)))) + } + }, div(clazz := "game", moveIndicator(gameInfo.you, gameInfo.yourScore), board, moveIndicator(gameInfo.opponent, gameInfo.opponentScore))) } - } - ) + }) private val route = akkaHttpService(serviceConfig).apply(AkkaHttpServerConfig()) @@ -214,21 +190,16 @@ object Application extends App { } yield () optimize { - button( - "Enter lobby", - event("click")(onClick) - ) + button("Enter lobby", event("click")(onClick)) } } private def renderScore(score: Score): Node = optimize { - div( - score.data.map { - case (colorCell, count) => - val color = BoardComponent.cellToColor(colorCell) - renderScoreLine(color, 3, count.toDouble / maxScore.toDouble) - } - ) + div(score.data.map { + case (colorCell, count) => + val color = BoardComponent.cellToColor(colorCell) + renderScoreLine(color, 3, count.toDouble / maxScore.toDouble) + }) } private def renderScoreLine(color: Rgb, h: Int, progress: Double): Node = { @@ -241,9 +212,7 @@ object Application extends App { clazz := "score-bar", width @= s"${Math.min(progress * 100, 100)}%", height @= s"${h}px", - backgroundColor @= color.toString - ) - ) + backgroundColor @= color.toString)) } } -} \ No newline at end of file +} diff --git a/client/src/main/scala/com/tenderowls/match3/client/PlayerProxyExtension.scala b/client/src/main/scala/com/tenderowls/match3/client/PlayerProxyExtension.scala index 2088b73..1fb5890 100644 --- a/client/src/main/scala/com/tenderowls/match3/client/PlayerProxyExtension.scala +++ b/client/src/main/scala/com/tenderowls/match3/client/PlayerProxyExtension.scala @@ -2,35 +2,32 @@ package com.tenderowls.match3.client import akka.actor.typed.scaladsl.AskPattern._ import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.{ActorRef, Behavior, Terminated} -import akka.actor.{ActorSystem, Scheduler} +import akka.actor.typed.{ ActorRef, Behavior, Terminated } +import akka.actor.{ ActorSystem, Scheduler } import akka.util.Timeout import com.tenderowls.match3.client.State.GameInfo import com.tenderowls.match3.client.components.BoardComponent import com.tenderowls.match3.server.actors._ -import com.tenderowls.match3.server.data.{PlayerInfo, Score} -import com.tenderowls.match3.{BoardGenerator, BoardOperation, Rules} -import korolev.execution.defaultExecutor -import korolev.{Context, Extension} +import com.tenderowls.match3.server.data.{ PlayerInfo, Score } +import com.tenderowls.match3.{ BoardGenerator, BoardOperation, Rules } +import scala.concurrent.ExecutionContext.Implicits.global +import korolev.{ Context, Extension } import akka.actor.typed.scaladsl.adapter._ - +import akka.util.Timeout import scala.collection.immutable.Queue import scala.concurrent.Future import scala.concurrent.duration._ -final class PlayerProxyExtension(lobby: Lobby, - rules: Rules, - gameTimeout: FiniteDuration, - maxScore: Int) - (implicit actorSystem: ActorSystem, - scheduler: Scheduler) extends Extension[Future, State, ClientEvent] { +final class PlayerProxyExtension(lobby: Lobby, rules: Rules, gameTimeout: FiniteDuration, maxScore: Int)( + implicit actorSystem: ActorSystem, + scheduler: Scheduler) + extends Extension[Future, State, ClientEvent] { type Access = Context.BaseAccess[Future, State, ClientEvent] class Actors(playerId: String, access: Access)(implicit scheduler: Scheduler) { type Proxy = ActorRef[ClientEvent] - object Proxy { // We can't create a PlayerActor because we don't know @@ -42,7 +39,7 @@ final class PlayerProxyExtension(lobby: Lobby, Proxy(playerName) case (ctx, event) => ctx.log.error(s"Unexpected client event $event in default state") - Behavior.stopped + Behaviors.stopped } // Now when we know player name, @@ -99,7 +96,10 @@ final class PlayerProxyExtension(lobby: Lobby, def apply(player: Player, game: Game): Behavior[Event] = Behaviors.setup { ctx => def aux(animated: Boolean, queue: Queue[Batch]): Behavior[Event] = - default orElse Behaviors.receiveMessage { + Behaviors.receiveMessage { + case Event.InGame(player, game) => + apply(player, game) + case Event.MakeMove(swap) => game ! GameActor.Event.MakeMove(player, swap) Behaviors.same @@ -134,18 +134,19 @@ final class PlayerProxyExtension(lobby: Lobby, } /** - * Actor transforms application state according to player events. - * - * Lifecycle: - * Start -> WhatsYourName, GameStarted) - * -> SyncAnimation, MoveResult, CurrentScore, - * YourTurn, OpponentTurn, WhatsYourName, - * Stop <- YouWin, YouLose, Draw, EndOfTurn - */ - def Player(playerName: String, - access: Access, - mediator: PlayerMediator) - (implicit scheduler: Scheduler): Behavior[PlayerActor.Event] = Behaviors.setup { ctx => + * Actor transforms application state according to player events. + * + * Lifecycle: + * Start -> WhatsYourName, GameStarted) + * -> SyncAnimation, MoveResult, CurrentScore, + * YourTurn, OpponentTurn, WhatsYourName, + * Stop <- YouWin, YouLose, Draw, EndOfTurn + */ + def Player(playerName: String, access: Access, mediator: PlayerMediator)( + implicit scheduler: Scheduler): Behavior[PlayerActor.Event] = Behaviors.setup { ctx => + implicit val akkaScheduler: akka.actor.typed.Scheduler = actorSystem.scheduler.toTyped + + implicit val timeout: Timeout = 3.seconds val playerRespondNameBehavior: Behavior[PlayerActor.Event] = Behaviors.receiveMessage { @@ -161,21 +162,27 @@ final class PlayerProxyExtension(lobby: Lobby, } lazy val playerAwaitGameBehavior: Behavior[PlayerActor.Event] = - playerRespondNameBehavior orElse Behaviors.receiveMessagePartial { + Behaviors.receiveMessagePartial { + case PlayerActor.Event.WhatsYourName(replyTo) => + replyTo ! playerName + Behaviors.same + case _: PlayerActor.Event.CurrentScore => + // Ignore score event. + // For the view score accruing synchronized with animation. + Behaviors.same + case PlayerActor.Event.GameStarted(yourTurn, board, gameRef, opponent) => implicit val timeout: Timeout = Timeout(5.seconds) - opponent - .ask(PlayerActor.Event.WhatsYourName) - .map { opponentName: String => - transformState { _ => - val you = PlayerInfo(playerName) - val enemy = PlayerInfo(opponentName) - val currentPlayer = if (yourTurn) you else enemy - val info = GameInfo(currentPlayer, you, enemy, Score.empty, Score.empty, None) - val params = BoardComponent.Params(board, Nil, 0) - State.Game(info, params) - } + opponent.ask(PlayerActor.Event.WhatsYourName).map { opponentName: String => + transformState { _ => + val you = PlayerInfo(playerName) + val enemy = PlayerInfo(opponentName) + val currentPlayer = if (yourTurn) you else enemy + val info = GameInfo(currentPlayer, you, enemy, Score.empty, Score.empty, None) + val params = BoardComponent.Params(board, Nil, 0) + State.Game(info, params) } + } mediator ! PlayerMediator.Event.InGame(ctx.self, gameRef) playerInGameBehavior(gameRef) case event => @@ -184,7 +191,16 @@ final class PlayerProxyExtension(lobby: Lobby, } def playerInGameBehavior(game: Game): Behavior[PlayerActor.Event] = - playerRespondNameBehavior orElse Behaviors.receiveMessagePartial { + Behaviors.receiveMessagePartial { + + case PlayerActor.Event.WhatsYourName(replyTo) => + replyTo ! playerName + Behaviors.same + case _: PlayerActor.Event.CurrentScore => + // Ignore score event. + // For the view score accruing synchronized with animation. + Behaviors.same + case PlayerActor.Event.MoveResult(batch) => mediator ! PlayerMediator.Event.MoveResult(batch) Behaviors.same @@ -236,10 +252,7 @@ final class PlayerProxyExtension(lobby: Lobby, private def applyBatch(batch: Batch) = transformGameState { case state @ State.Game(_, params) => - val updatedParams = params.copy( - animationNumber = params.animationNumber + 1, - batch = batch - ) + val updatedParams = params.copy(animationNumber = params.animationNumber + 1, batch = batch) state.copy(boardParams = updatedParams) // TODO error state } @@ -247,15 +260,14 @@ final class PlayerProxyExtension(lobby: Lobby, def setup(access: Access): Future[Extension.Handlers[Future, State, ClientEvent]] = { access.sessionId.map { qsi => - val id = s"${qsi.deviceId}-${qsi.id}" + val id = s"${qsi.deviceId}-${qsi.sessionId}" val actors = new Actors(id, access) val proxy = actorSystem.spawnAnonymous(actors.Proxy(access)) Extension.Handlers( onDestroy = () => Future.successful(actorSystem.stop(proxy.toClassic)), onMessage = clientEvent => { Future.successful(proxy ! clientEvent) - } - ) + }) } } } diff --git a/client/src/main/scala/com/tenderowls/match3/client/State.scala b/client/src/main/scala/com/tenderowls/match3/client/State.scala index c749a53..31e2e8e 100644 --- a/client/src/main/scala/com/tenderowls/match3/client/State.scala +++ b/client/src/main/scala/com/tenderowls/match3/client/State.scala @@ -1,10 +1,10 @@ package com.tenderowls.match3.client -import com.tenderowls.match3.server.data.{PlayerInfo, Score} +import com.tenderowls.match3.server.data.{ PlayerInfo, Score } import com.tenderowls.match3.client.components.BoardComponent.Params import korolev._ import korolev.state.javaSerialization._ -import korolev.execution._ +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration.FiniteDuration @@ -24,13 +24,12 @@ object State { case object Draw extends LoggedInState case class GameInfo( - currentPlayer: PlayerInfo, - you: PlayerInfo, - opponent: PlayerInfo, - yourScore: Score, - opponentScore: Score, - timeRemaining: Option[FiniteDuration] - ) + currentPlayer: PlayerInfo, + you: PlayerInfo, + opponent: PlayerInfo, + yourScore: Score, + opponentScore: Score, + timeRemaining: Option[FiniteDuration]) val globalContext = Context[Future, State, ClientEvent] } diff --git a/client/src/main/scala/com/tenderowls/match3/client/components/BoardComponent.scala b/client/src/main/scala/com/tenderowls/match3/client/components/BoardComponent.scala index 3b588bd..fc476aa 100644 --- a/client/src/main/scala/com/tenderowls/match3/client/components/BoardComponent.scala +++ b/client/src/main/scala/com/tenderowls/match3/client/components/BoardComponent.scala @@ -1,14 +1,13 @@ package com.tenderowls.match3.client.components -import com.tenderowls.match3.server.data.{ColorCell, Score} -import com.tenderowls.match3.BoardOperation.{Swap, Transition, Update} +import com.tenderowls.match3.server.data.{ ColorCell, Score } +import com.tenderowls.match3.BoardOperation.{ Swap, Transition, Update } import com.tenderowls.match3.Cell.EmptyCell -import com.tenderowls.match3.{Board, BoardOperation, Cell, Direction, Point} +import com.tenderowls.match3.{ Board, BoardOperation, Cell, Direction, Point } import korolev.Component - +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ -import korolev.execution._ import korolev.state.javaSerialization._ import levsha.dsl._ import html._ @@ -41,7 +40,7 @@ object BoardComponent { object Rgb { val Blue = Rgb(0x00, 0xA3, 0xFF) val Green = Rgb(0x0C, 0xE8, 0x42) - val Red = Rgb(0xFF, 0x0D, 0x2A) + val Red = Rgb(0xFF, 0x0D, 0x2A) val Yellow = Rgb(0xFF, 0xF7, 0x00) val Orange = Rgb(0xE8, 0x86, 0x0C) val Dark = Rgb(0x33, 0x33, 0x33) @@ -69,253 +68,236 @@ object BoardComponent { val animationState = AttrDef("animation-state") - val create: Component[Future, State, Params, Event] = Component[Future, State, Params, Event](State.Static(None, None, 0)) { (context, parameters, state) => - - import BoardViewConfig.default._ - import context.{Event => DomEvent, _} - - def screenPos(n: Int): Double = { - n * (cellWidth + cellGap) - } - - def neighbours(p: Point): List[Point] = { - List( - p.left, - p.right, - p.top, - p.bottom, - p.top.left, - p.top.right, - p.bottom.left, - p.bottom.right - ) - } + val create: Component[Future, State, Params, Event] = + Component[Future, State, Params, Event](State.Static(None, None, 0)) { (context, parameters, state) => + import BoardViewConfig.default._ + import context.{ Event => DomEvent, _ } - def calculateMove(ops: List[BoardOperation], point: Point) = ops.collectFirst { - case Transition(`point`, to) => to - case Swap(`point`, to) => to - case Swap(to, `point`) => to - } + def screenPos(n: Int): Double = { + n * (cellWidth + cellGap) + } - def renderAnimatedCell(ops: List[BoardOperation], point: Point, cell: Cell) = { - val move = calculateMove(ops, point) - val isEmptyCell = cell == Cell.EmptyCell - val updateToNonEmpty = ops.collectFirst { - case Update(p, c) if p == point && c != EmptyCell => c + def neighbours(p: Point): List[Point] = { + List(p.left, p.right, p.top, p.bottom, p.top.left, p.top.right, p.bottom.left, p.bottom.right) } - val color = cellToColor(updateToNonEmpty.getOrElse(cell)) - val vsd = viewSide.toDouble - val wh = { - if (ops.contains(Update(point, Cell.EmptyCell))) 0 - else if (updateToNonEmpty.nonEmpty) cellWidth - else if (isEmptyCell) 0 - else cellWidth - } / vsd * 100 - val xyk = if (wh == 0) cellRadius else 0 - val x = s"${((move.fold(screenPos(point.x))(p => screenPos(p.x)) + xyk) / vsd * 100)}%" - val y = s"${((move.fold(screenPos(point.y))(p => screenPos(p.y)) + xyk) / vsd * 100)}%" - optimize { - div( - clazz := "circle circle-touchable circle-movable", - left @= x, - top @= y, - width @= (s"${wh}%"), - height @= (s"${wh}%"), - backgroundColor @= color.toStringWithAlpha(1.0) - ) + def calculateMove(ops: List[BoardOperation], point: Point) = ops.collectFirst { + case Transition(`point`, to) => to + case Swap(`point`, to) => to + case Swap(to, `point`) => to } - } - def renderAnimatedBoard(board: Board, - ops: List[BoardOperation]) - (onAnimationEnd: Access => Future[Unit]) = optimize { - div( - clazz := "board", - // Fake circle need to track transition end - div( - animationState := "animated", - clazz := "circle circle-movable", - left @= s"${(Random.nextInt(49) + 51)}%", - top @= "0px", - width @= "20px", - height @= "20px", - backgroundColor @= "#FFFFFF", - event("transitionend")(onAnimationEnd) - ), - board.data.map { - case (point, cell) => - renderAnimatedCell(ops, point, cell) + def renderAnimatedCell(ops: List[BoardOperation], point: Point, cell: Cell) = { + val move = calculateMove(ops, point) + val isEmptyCell = cell == Cell.EmptyCell + val updateToNonEmpty = ops.collectFirst { + case Update(p, c) if p == point && c != EmptyCell => c } - ) - } - - def renderStaticBoard(board: Board, - selectedCellOpt: Option[Point], - circleClass: Option[String], - bindings: Seq[Binding] = Nil) - (cellClick: Point => Option[Access => Future[Unit]], - cellSwipe: (Point, Swipe) => Option[Access => Future[Unit]], - onAnimationEnd: Option[Access => Future[Unit]]) = { - - def renderStaticCell(point: Point, cell: Cell) = { - val clickHandler = cellClick(point) - val swipeUpHandler = cellSwipe(point, Swipe.Up) - val swipeDownHandler = cellSwipe(point, Swipe.Down) - val swipeLeftHandler = cellSwipe(point, Swipe.Left) - val swipeRightHandler = cellSwipe(point, Swipe.Right) + val color = cellToColor(updateToNonEmpty.getOrElse(cell)) val vsd = viewSide.toDouble val wh = { - if (cell == Cell.EmptyCell) 0 - else if (selectedCellOpt.contains(point)) cellWidth + cellGap + if (ops.contains(Update(point, Cell.EmptyCell))) 0 + else if (updateToNonEmpty.nonEmpty) cellWidth + else if (isEmptyCell) 0 else cellWidth } / vsd * 100 val xyk = if (wh == 0) cellRadius else 0 - val x = s"${((screenPos(point.x) + xyk) / vsd * 100)}%" - val y = s"${((screenPos(point.y) + xyk) / vsd * 100)}%" - val opacity = selectedCellOpt.fold(1d) { selectedCell => - if (point == selectedCell) 1d - else if (neighbours(selectedCell).contains(point)) 1d - else 0.5d - } + val x = s"${((move.fold(screenPos(point.x))(p => screenPos(p.x)) + xyk) / vsd * 100)}%" + val y = s"${((move.fold(screenPos(point.y))(p => screenPos(p.y)) + xyk) / vsd * 100)}%" optimize { div( + clazz := "circle circle-touchable circle-movable", left @= x, top @= y, - backgroundColor @= cellToColor(cell).toStringWithAlpha(opacity), width @= (s"${wh}%"), height @= (s"${wh}%"), - clazz := "circle " + circleClass.getOrElse(""), - clickHandler.map(f => event("mouseup")(f)), - swipeUpHandler.map(f => event("swipeup")(f)), - swipeDownHandler.map(f => event("swipedown")(f)), - swipeLeftHandler.map(f => event("swipeleft")(f)), - swipeRightHandler.map(f => event("swiperight")(f)) - ) + backgroundColor @= color.toStringWithAlpha(1.0)) } } - optimize { - div( - clazz := "board", - // Fake circle need to track transition end + def renderAnimatedBoard(board: Board, ops: List[BoardOperation])(onAnimationEnd: Access => Future[Unit]) = + optimize { div( - animationState := "static", - clazz := "circle circle-movable", - left @= s"${Random.nextInt(50)}%", - top @= "0px", - width @= "20px", - height @= "20px", - backgroundColor @= "#FFFFFF", - onAnimationEnd.map(event("transitionend")(_)) - ), - board.data.map { - case (point, cell) => - renderStaticCell(point, cell) - }, - bindings - ) + clazz := "board", + // Fake circle need to track transition end + div( + animationState := "animated", + clazz := "circle circle-movable", + left @= s"${(Random.nextInt(49) + 51)}%", + top @= "0px", + width @= "20px", + height @= "20px", + backgroundColor @= "#FFFFFF", + event("transitionend")(onAnimationEnd)), + board.data.map { + case (point, cell) => + renderAnimatedCell(ops, point, cell) + }) + } + + def renderStaticBoard( + board: Board, + selectedCellOpt: Option[Point], + circleClass: Option[String], + bindings: Seq[Binding] = Nil)( + cellClick: Point => Option[Access => Future[Unit]], + cellSwipe: (Point, Swipe) => Option[Access => Future[Unit]], + onAnimationEnd: Option[Access => Future[Unit]]) = { + + def renderStaticCell(point: Point, cell: Cell) = { + val clickHandler = cellClick(point) + val swipeUpHandler = cellSwipe(point, Swipe.Up) + val swipeDownHandler = cellSwipe(point, Swipe.Down) + val swipeLeftHandler = cellSwipe(point, Swipe.Left) + val swipeRightHandler = cellSwipe(point, Swipe.Right) + val vsd = viewSide.toDouble + val wh = { + if (cell == Cell.EmptyCell) 0 + else if (selectedCellOpt.contains(point)) cellWidth + cellGap + else cellWidth + } / vsd * 100 + val xyk = if (wh == 0) cellRadius else 0 + val x = s"${((screenPos(point.x) + xyk) / vsd * 100)}%" + val y = s"${((screenPos(point.y) + xyk) / vsd * 100)}%" + val opacity = selectedCellOpt.fold(1d) { selectedCell => + if (point == selectedCell) 1d + else if (neighbours(selectedCell).contains(point)) 1d + else 0.5d + } + + optimize { + div( + left @= x, + top @= y, + backgroundColor @= cellToColor(cell).toStringWithAlpha(opacity), + width @= (s"${wh}%"), + height @= (s"${wh}%"), + clazz := "circle " + circleClass.getOrElse(""), + clickHandler.map(f => event("mouseup")(f)), + swipeUpHandler.map(f => event("swipeup")(f)), + swipeDownHandler.map(f => event("swipedown")(f)), + swipeLeftHandler.map(f => event("swipeleft")(f)), + swipeRightHandler.map(f => event("swiperight")(f))) + } + } + + optimize { + div( + clazz := "board", + // Fake circle need to track transition end + div( + animationState := "static", + clazz := "circle circle-movable", + left @= s"${Random.nextInt(50)}%", + top @= "0px", + width @= "20px", + height @= "20px", + backgroundColor @= "#FFFFFF", + onAnimationEnd.map(event("transitionend")(_))), + board.data.map { + case (point, cell) => + renderStaticCell(point, cell) + }, + bindings) + } } - } - (parameters, state) match { - case (Params(origBoard, _, an), State.Static(boardOpt, selectedCellOpt, lan)) if an <= lan => - val board = boardOpt.getOrElse(origBoard) - renderStaticBoard(board, selectedCellOpt, Some("circle-touchable"))( - cellClick = p2 => Some( - access => { - val swapOpt = selectedCellOpt.flatMap { p1 => - if (neighbours(p1).contains(p2)) Some(Swap(p1, p2)) - else None - } - swapOpt match { - case Some(swap) => access.publish(Event.Move(swap)) - case None if selectedCellOpt.contains(p2) => access.maybeTransition { - case s: State.Static => s.copy(selectedCell = None) + (parameters, state) match { + case (Params(origBoard, _, an), State.Static(boardOpt, selectedCellOpt, lan)) if an <= lan => + val board = boardOpt.getOrElse(origBoard) + renderStaticBoard(board, selectedCellOpt, Some("circle-touchable"))( + cellClick = p2 => + Some(access => { + val swapOpt = selectedCellOpt.flatMap { p1 => + if (neighbours(p1).contains(p2)) Some(Swap(p1, p2)) + else None } - case None => access.maybeTransition { - case s: State.Static => s.copy(selectedCell = Some(p2)) + swapOpt match { + case Some(swap) => access.publish(Event.Move(swap)) + case None if selectedCellOpt.contains(p2) => + access.maybeTransition { + case s: State.Static => s.copy(selectedCell = None) + } + case None => + access.maybeTransition { + case s: State.Static => s.copy(selectedCell = Some(p2)) + } } - } - } - ), - cellSwipe = (p1, swipe) => Some( - access => { - val p2 = swipe match { - case Swipe.Up => p1.top - case Swipe.Down => p1.bottom - case Swipe.Left => p1.left - case Swipe.Right => p1.right - } - val swap = Swap(p1, p2) - val move = Event.Move(swap) - access.publish(move) - } - ), - onAnimationEnd = None - ) - case (Params(origBoard, ops :: batch, an), State.Static(boardOpt, _, lan)) if an > lan => - val board = boardOpt.getOrElse(origBoard) - // Enter animation - val newBoard = board.applyOperations(ops) - renderAnimatedBoard(board, ops) { access => - access.sessionId.flatMap { qsid => - println(s"${qsid.deviceId.take(4)}: transition end (remove or add circles ${batch.length}") - access.transition(_ => State.AnimationEnd(an, newBoard, batch)) - } - } - case (_, State.AnimationStart(an, board, ops :: batch)) => - // Do animation - renderAnimatedBoard(board, ops) { access => + }), + cellSwipe = (p1, swipe) => + Some(access => { + val p2 = swipe match { + case Swipe.Up => p1.top + case Swipe.Down => p1.bottom + case Swipe.Left => p1.left + case Swipe.Right => p1.right + } + val swap = Swap(p1, p2) + val move = Event.Move(swap) + access.publish(move) + }), + onAnimationEnd = None) + case (Params(origBoard, ops :: batch, an), State.Static(boardOpt, _, lan)) if an > lan => + val board = boardOpt.getOrElse(origBoard) + // Enter animation val newBoard = board.applyOperations(ops) - val score = ops.foldLeft(Score.empty) { - case (total, BoardOperation.Update(point, Cell.EmptyCell)) => - board.get(point).fold(total) { - case EmptyCell => total - case cell: ColorCell => total.inc(cell) - } - case (total, _) => total + renderAnimatedBoard(board, ops) { access => + access.sessionId.flatMap { qsid => + println(s"${qsid.deviceId.take(4)}: transition end (remove or add circles ${batch.length}") + access.transition(_ => State.AnimationEnd(an, newBoard, batch)) + } } - for { - qsid <- access.sessionId - _ = println(s"${qsid.deviceId.take(4)}: transition end (remove or add circles ${batch.length}") - _ <- if (score.sum > 0) access.publish(Event.AddScore(score)) else Future.unit - _ <- access.transition(_ => State.AnimationEnd(an, newBoard, batch)) - } yield () - } - case (_, State.AnimationEnd(an, board, batch)) => - // // End animation - // val effect = delay(animationDelay) { access => - // if (batch.isEmpty) { - // println("animation end") - // for { - // _ <- access.transition(_ => State.Static(Some(board), None, an)) - // _ <- access.publish(Event.AnimationEnd) - // } yield () - // } else { - // access.transition(_ => State.AnimationStart(an, board, batch)) - // } - // } - // renderStaticBoard(board, None, None, Seq(effect))(_ => None) - renderStaticBoard(board, None, None, Nil)( - cellClick = _ => None, - cellSwipe = (_, _) => None, - onAnimationEnd = Some { - access: Access => { - if (batch.isEmpty) { - for { - _ <- access.transition(_ => State.Static(Some(board), None, an)) - _ <- access.publish(Event.AnimationEnd) - } yield () - } else { - access.transition(_ => State.AnimationStart(an, board, batch)) - } + case (_, State.AnimationStart(an, board, ops :: batch)) => + // Do animation + renderAnimatedBoard(board, ops) { access => + val newBoard = board.applyOperations(ops) + val score = ops.foldLeft(Score.empty) { + case (total, BoardOperation.Update(point, Cell.EmptyCell)) => + board.get(point).fold(total) { + case EmptyCell => total + case cell: ColorCell => total.inc(cell) + } + case (total, _) => total } + for { + qsid <- access.sessionId + _ = println(s"${qsid.deviceId.take(4)}: transition end (remove or add circles ${batch.length}") + _ <- if (score.sum > 0) access.publish(Event.AddScore(score)) else Future.unit + _ <- access.transition(_ => State.AnimationEnd(an, newBoard, batch)) + } yield () } - ) + case (_, State.AnimationEnd(an, board, batch)) => + // // End animation + // val effect = delay(animationDelay) { access => + // if (batch.isEmpty) { + // println("animation end") + // for { + // _ <- access.transition(_ => State.Static(Some(board), None, an)) + // _ <- access.publish(Event.AnimationEnd) + // } yield () + // } else { + // access.transition(_ => State.AnimationStart(an, board, batch)) + // } + // } + // renderStaticBoard(board, None, None, Seq(effect))(_ => None) + renderStaticBoard(board, None, None, Nil)( + cellClick = _ => None, + cellSwipe = (_, _) => None, + onAnimationEnd = Some { access: Access => + { + if (batch.isEmpty) { + for { + _ <- access.transition(_ => State.Static(Some(board), None, an)) + _ <- access.publish(Event.AnimationEnd) + } yield () + } else { + access.transition(_ => State.AnimationStart(an, board, batch)) + } + } + }) + } } - } type PositionPolicy = (Point, Cell) => (Double, Double) type OpacityPolicy = (Point, Cell) => Double @@ -323,12 +305,11 @@ object BoardComponent { type Batch = List[List[BoardOperation]] case class BoardViewConfig( - side: Int = 9, - cellRadius: Int = 15, - cellGap: Int = 4, - animationDuration: FiniteDuration = 200.millis, - animationDelay: FiniteDuration = 100.millis - ) { + side: Int = 9, + cellRadius: Int = 15, + cellGap: Int = 4, + animationDuration: FiniteDuration = 200.millis, + animationDelay: FiniteDuration = 100.millis) { val cellWidth: Int = cellRadius * 2 val viewSide: Int = side * (cellWidth + cellGap) } diff --git a/client/src/main/scala/com/tenderowls/match3/client/view/BoardView.scala b/client/src/main/scala/com/tenderowls/match3/client/view/BoardView.scala index 8b03e1f..1f76466 100644 --- a/client/src/main/scala/com/tenderowls/match3/client/view/BoardView.scala +++ b/client/src/main/scala/com/tenderowls/match3/client/view/BoardView.scala @@ -1,10 +1,5 @@ package com.tenderowls.match3.client.view -class BoardView { +class BoardView {} -} - -object BoardView { - - -} +object BoardView {} diff --git a/match3/src/main/scala/com/tenderowls/match3/Board.scala b/match3/src/main/scala/com/tenderowls/match3/Board.scala index 313a41c..ea6bf0b 100644 --- a/match3/src/main/scala/com/tenderowls/match3/Board.scala +++ b/match3/src/main/scala/com/tenderowls/match3/Board.scala @@ -14,7 +14,7 @@ final case class Board(rules: Rules, rawData: BoardData) { Point(i % rules.width, i / rules.width) -> rawData(i) } } - + def mapData[T](f: (Point, Int) => T): Iterable[T] = { val range = rawData.indices range.view map { i => @@ -23,9 +23,7 @@ final case class Board(rules: Rules, rawData: BoardData) { } @tailrec - final private def genSeq(lst: List[MatchedCell], - nx: Inc = x => x, - ny: Inc = y => y): List[MatchedCell] = { + final private def genSeq(lst: List[MatchedCell], nx: Inc = x => x, ny: Inc = y => y): List[MatchedCell] = { val prev = lst.head val x = nx(prev.pos.x) val y = ny(prev.pos.y) @@ -51,12 +49,10 @@ final case class Board(rules: Rules, rawData: BoardData) { } def fillEmptyCells: Board = { - buildWithData( - rawData map { + buildWithData(rawData map { case EmptyCell => rules.randomValue case x => x - } - ) + }) } @tailrec @@ -64,9 +60,8 @@ final case class Board(rules: Rules, rawData: BoardData) { matchedSequences().toList match { case Nil => this case sequences => - val replace = sequences.map(sequence => sequence.head).groupBy { - cell => - (cell.pos.x, cell.pos.y) + val replace = sequences.map(sequence => sequence.head).groupBy { cell => + (cell.pos.x, cell.pos.y) } val newBoard = mapData { case (Point(x, y), i) => @@ -96,10 +91,7 @@ final case class Board(rules: Rules, rawData: BoardData) { genSeq(mr, ny = y => y + 1) } val sequences = rs ++ bs - sequences - .toSeq - .filter(_.size >= minLength) - .sortBy(-_.size) + sequences.toSeq.filter(_.size >= minLength).sortBy(-_.size) } /** @@ -109,49 +101,43 @@ final case class Board(rules: Rules, rawData: BoardData) { def matchedSequence: Option[List[MatchedCell]] = matchedSequences().headOption - def calculateRemoveSequenceOperations( - seq: List[MatchedCell]): List[BoardOperation] = { + def calculateRemoveSequenceOperations(seq: List[MatchedCell]): List[BoardOperation] = { // Create board without cells present in sequence - val cleanBoard = buildWithData( - mapData { (point, i) => - val exists = seq.exists { - case MatchedCell(`point`, _) => true - case _ => false - } - if (exists) EmptyCell - else rawData(i) - }.toVector - ) + val cleanBoard = buildWithData(mapData { (point, i) => + val exists = seq.exists { + case MatchedCell(`point`, _) => true + case _ => false + } + if (exists) EmptyCell + else rawData(i) + }.toVector) // Calculate cell transition operations val transitionOps = seq filter { - // First of all, let's keep only those cells which - // don't have empty neighbour to bottom - case MatchedCell(Point(x, y), _) => - cleanBoard.get(x, y + 1) match { - case Some(EmptyCell) => false - case _ => true + // First of all, let's keep only those cells which + // don't have empty neighbour to bottom + case MatchedCell(Point(x, y), _) => + cleanBoard.get(x, y + 1) match { + case Some(EmptyCell) => false + case _ => true + } + } map { + case MatchedCell(Point(x, y), _) => + // Find top boundary for cell from sequence. It can be + // top boundary of board or BadCell + val boundary = (-1 to y).reverse find { yy => + yy == -1 || cleanBoard.getUnsafe(x, yy) == BadCell + } + // Find y coordinates of cells upwards from cell + val topYs = ((boundary.get + 1) to y).reverse.filter { yy => + cleanBoard.getUnsafe(x, yy) match { + case EmptyCell => false + case _ => true + } + } + topYs.indices map { i => + Transition(Point(x, topYs(i)), Point(x, y - i)) + } } - } map { - case MatchedCell(Point(x, y), _) => - // Find top boundary for cell from sequence. It can be - // top boundary of board or BadCell - val boundary = (-1 to y).reverse find { yy => - yy == -1 || cleanBoard.getUnsafe(x, yy) == BadCell - } - // Find y coordinates of cells upwards from cell - val topYs = ((boundary.get + 1) to y).reverse.filter { yy => - cleanBoard.getUnsafe(x, yy) match { - case EmptyCell => false - case _ => true - } - } - topYs.indices map { i => - Transition( - Point(x, topYs(i)), - Point(x, y - i) - ) - } - } val updateOps = seq map { matched => Update(matched.pos, EmptyCell) } diff --git a/match3/src/main/scala/com/tenderowls/match3/BoardAdviser.scala b/match3/src/main/scala/com/tenderowls/match3/BoardAdviser.scala index 5e2d7bb..2b87290 100644 --- a/match3/src/main/scala/com/tenderowls/match3/BoardAdviser.scala +++ b/match3/src/main/scala/com/tenderowls/match3/BoardAdviser.scala @@ -6,33 +6,29 @@ object BoardAdviser { import BoardOperation._ import Cell._ - def normalHeuristic(brd:Board, swp:Swap):Int = { + def normalHeuristic(brd: Board, swp: Swap): Int = { brd.applyOperations(List(swp)).matchedSequences().flatten.size } - implicit class BoardAdviserMethods(val board:Board) { + implicit class BoardAdviserMethods(val board: Board) { - private def f(matched:MatchedCell)(lookup: Point => Point) = { + private def f(matched: MatchedCell)(lookup: Point => Point) = { board.get(lookup(matched.pos)) match { case Some(cell) if cell matchWith matched.value => true - case _ => false + case _ => false } } - def adviceSpecificSequences:Iterable[List[MatchedCell]] = { - val raw = board.mapData { (firstPoint:Point, i) => + def adviceSpecificSequences: Iterable[List[MatchedCell]] = { + val raw = board.mapData { (firstPoint: Point, i) => board.get(firstPoint) match { case Some(firstCell) => - def genSeq(secondPoint:Point):List[MatchedCell] = { + def genSeq(secondPoint: Point): List[MatchedCell] = { board.get(secondPoint) match { case Some(secondCell) => if (firstCell matchWith secondCell) { - List( - MatchedCell(secondPoint, secondCell), - MatchedCell(firstPoint, firstCell) - ) - } - else { + List(MatchedCell(secondPoint, secondCell), MatchedCell(firstPoint, firstCell)) + } else { Nil } case None => Nil @@ -45,11 +41,11 @@ object BoardAdviser { raw.flatten.filter(_.size == 2) } - def advices:Iterable[Swap] = { + def advices: Iterable[Swap] = { advices(board.matchedSequences(2) ++ adviceSpecificSequences) } - def advices(sequences:Iterable[List[MatchedCell]]):Iterable[Swap] = { + def advices(sequences: Iterable[List[MatchedCell]]): Iterable[Swap] = { val ret = sequences map { seq => val reversedSeq = seq.reverse val fst = reversedSeq.head @@ -59,17 +55,16 @@ object BoardAdviser { if (snd.pos.x - fst.pos.x > 1) { lookupTornHorizontal.find(f(fst)(_)) match { case Some(lookup) => List(Swap(lookup(fst.pos), fst.pos.right)) - case None => List.empty[Swap] + case None => List.empty[Swap] } - } - else { + } else { val left = lookupLeft.find(f(fst)(_)) match { case Some(lookup) => List(Swap(lookup(fst.pos), fst.pos.left)) - case None => List.empty[Swap] + case None => List.empty[Swap] } val right = lookupRight.find(f(snd)(_)) match { case Some(lookup) => List(Swap(lookup(snd.pos), snd.pos.right)) - case None => List.empty[Swap] + case None => List.empty[Swap] } left ++ right } @@ -77,17 +72,16 @@ object BoardAdviser { if (snd.pos.y - fst.pos.y > 1) { lookupTornVertical.find(f(fst)(_)) match { case Some(lookup) => List(Swap(lookup(fst.pos), fst.pos.bottom)) - case None => List.empty[Swap] + case None => List.empty[Swap] } - } - else { + } else { val top = lookupTop.find(f(fst)(_)) match { case Some(lookup) => List(Swap(lookup(fst.pos), fst.pos.top)) - case None => List.empty[Swap] + case None => List.empty[Swap] } val bottom = lookupBottom.find(f(snd)(_)) match { case Some(lookup) => List(Swap(lookup(snd.pos), snd.pos.bottom)) - case None => List.empty[Swap] + case None => List.empty[Swap] } top ++ bottom } @@ -96,67 +90,37 @@ object BoardAdviser { ret.flatten } - def bestAdvice(depth:Int, - advices:Iterable[Swap], - heuristic: (Board, Swap) => Int):Option[Swap] = { + def bestAdvice(depth: Int, advices: Iterable[Swap], heuristic: (Board, Swap) => Int): Option[Swap] = { val sliced = advices.grouped(depth) if (sliced.hasNext) { - val withWeights = sliced.next map { - (swap) => (heuristic(board, swap), swap) + val withWeights = sliced.next map { (swap) => + (heuristic(board, swap), swap) } - withWeights - .toList - .sortBy(_._1) - .reverse - .map(_._2) - .headOption - } - else None + withWeights.toList.sortBy(_._1).reverse.map(_._2).headOption + } else None } - def bestAdvice:Option[Swap] = bestAdvice(default_depth, advices, normalHeuristic) + def bestAdvice: Option[Swap] = bestAdvice(default_depth, advices, normalHeuristic) - def bestAdvice(depth:Int):Option[Swap] = bestAdvice(depth, this.advices, normalHeuristic) + def bestAdvice(depth: Int): Option[Swap] = bestAdvice(depth, this.advices, normalHeuristic) - def bestAdvice(depth:Int, advices:Iterable[Swap]):Option[Swap] = { + def bestAdvice(depth: Int, advices: Iterable[Swap]): Option[Swap] = { bestAdvice(depth, advices, normalHeuristic) } } private val default_depth = 9 - private val lookupLeft = List( - (p:Point) => p.left(2), - (p:Point) => p.left.top, - (p:Point) => p.left.bottom - ) - - private val lookupRight = List( - (p:Point) => p.right(2), - (p:Point) => p.right.top, - (p:Point) => p.right.bottom - ) - - private val lookupTop = List( - (p:Point) => p.top(2), - (p:Point) => p.top.left, - (p:Point) => p.top.right - ) - - private val lookupBottom = List( - (p:Point) => p.bottom(2), - (p:Point) => p.bottom.left, - (p:Point) => p.bottom.right - ) - - private val lookupTornHorizontal = List( - (p:Point) => p.right.top, - (p:Point) => p.right.bottom - ) - - private val lookupTornVertical = List( - (p:Point) => p.bottom.left, - (p:Point) => p.bottom.right - ) + private val lookupLeft = List((p: Point) => p.left(2), (p: Point) => p.left.top, (p: Point) => p.left.bottom) + + private val lookupRight = List((p: Point) => p.right(2), (p: Point) => p.right.top, (p: Point) => p.right.bottom) + + private val lookupTop = List((p: Point) => p.top(2), (p: Point) => p.top.left, (p: Point) => p.top.right) + + private val lookupBottom = List((p: Point) => p.bottom(2), (p: Point) => p.bottom.left, (p: Point) => p.bottom.right) + + private val lookupTornHorizontal = List((p: Point) => p.right.top, (p: Point) => p.right.bottom) + + private val lookupTornVertical = List((p: Point) => p.bottom.left, (p: Point) => p.bottom.right) } diff --git a/match3/src/main/scala/com/tenderowls/match3/BoardGenerator.scala b/match3/src/main/scala/com/tenderowls/match3/BoardGenerator.scala index b75fef3..fb5aab5 100644 --- a/match3/src/main/scala/com/tenderowls/match3/BoardGenerator.scala +++ b/match3/src/main/scala/com/tenderowls/match3/BoardGenerator.scala @@ -14,22 +14,20 @@ object BoardGenerator { def board(args: Any*)(implicit rndVal: () => Cell): Board = { val s = sc.parts.mkString - val rows = s.split("\n") - .map(row => row.trim) - .filter(_.length > 0) + val rows = s.split("\n").map(row => row.trim).filter(_.length > 0) val cells = rows.indices map { y => val values = rows(y).split(" ") 0 until values.length filter { x => values(x) match { case "" => false - case _ => true + case _ => true } } map { x => values(x) match { - case "_" => BadCell - case "*" => EmptyCell - case "?" => rndVal() + case "_" => BadCell + case "*" => EmptyCell + case "?" => rndVal() case ValuePattern(value) => IntCell(value.toInt) case s: String => val code = s.charAt(0).toByte @@ -47,7 +45,7 @@ object BoardGenerator { } } - def square(makeStable:Boolean = true)(implicit rules: Rules): Board = { + def square(makeStable: Boolean = true)(implicit rules: Rules): Board = { val raw = 0 until rules.height map { _ => 0 until rules.width map { _ => diff --git a/match3/src/test/scala/com/tenderowls/match3/AdviserSpec.scala b/match3/src/test/scala/com/tenderowls/match3/AdviserSpec.scala index af8a7a1..018b6c3 100644 --- a/match3/src/test/scala/com/tenderowls/match3/AdviserSpec.scala +++ b/match3/src/test/scala/com/tenderowls/match3/AdviserSpec.scala @@ -9,8 +9,8 @@ import org.specs2._ import scala.util.Random /** - * @author Aleksey Fomkin - */ + * @author Aleksey Fomkin + */ object AdviserSpec extends Specification { val rnd = new Random() @@ -58,7 +58,7 @@ object AdviserSpec extends Specification { _ _ _ _ _ _ 4 _ """ - board.bestAdvice.get mustEqual Swap(Point(6,2), Point(5, 2)) + board.bestAdvice.get mustEqual Swap(Point(6, 2), Point(5, 2)) } } diff --git a/match3/src/test/scala/com/tenderowls/match3/BoardSpec.scala b/match3/src/test/scala/com/tenderowls/match3/BoardSpec.scala index 73479cd..dbb8e54 100644 --- a/match3/src/test/scala/com/tenderowls/match3/BoardSpec.scala +++ b/match3/src/test/scala/com/tenderowls/match3/BoardSpec.scala @@ -44,10 +44,9 @@ object BoardSpec extends Specification { def horizontalMatch = board"0 0 1 1 2 2 2 0".matchedSequence.get.toSet mustEqual Set( - MatchedCell(Point(6, 0), IntCell(2)), - MatchedCell(Point(5, 0), IntCell(2)), - MatchedCell(Point(4, 0), IntCell(2)) - ) + MatchedCell(Point(6, 0), IntCell(2)), + MatchedCell(Point(5, 0), IntCell(2)), + MatchedCell(Point(4, 0), IntCell(2))) def verticalMatch = { @@ -64,8 +63,7 @@ object BoardSpec extends Specification { MatchedCell(Point(2, 1), IntCell(1)), MatchedCell(Point(2, 2), IntCell(1)), MatchedCell(Point(2, 3), IntCell(1)), - MatchedCell(Point(2, 4), IntCell(1)) - ) + MatchedCell(Point(2, 4), IntCell(1))) } def doubleMatch = { @@ -81,26 +79,23 @@ object BoardSpec extends Specification { Set( MatchedCell(Point(0, 0), IntCell(1)), MatchedCell(Point(0, 1), IntCell(1)), - MatchedCell(Point(0, 2), IntCell(1)) - ), + MatchedCell(Point(0, 2), IntCell(1))), Set( MatchedCell(Point(0, 0), IntCell(1)), MatchedCell(Point(1, 0), IntCell(1)), - MatchedCell(Point(2, 0), IntCell(1)) - ) - ) + MatchedCell(Point(2, 0), IntCell(1)))) } - def separateOps(ops:List[BoardOperation]) = { + def separateOps(ops: List[BoardOperation]) = { val updates = ops filter { - case x: Update => true - case _ => false - } + case x: Update => true + case _ => false + } val transitions = ops filter { - case x: Transition => true - case _ => false - } + case x: Transition => true + case _ => false + } (updates.toSet, transitions) } @@ -124,14 +119,12 @@ object BoardSpec extends Specification { Update(Point(2, 3), EmptyCell), Update(Point(2, 4), EmptyCell), Update(Point(2, 5), EmptyCell), - Update(Point(2, 6), EmptyCell) - ) + Update(Point(2, 6), EmptyCell)) ops._2 mustEqual List( Transition(Point(2, 2), Point(2, 6)), Transition(Point(2, 1), Point(2, 5)), - Transition(Point(2, 0), Point(2, 4)) - ) + Transition(Point(2, 0), Point(2, 4))) } def testSequenceOperationsCalculator2 = { @@ -150,11 +143,7 @@ object BoardSpec extends Specification { val seq = board.matchedSequence.get val ops = separateOps(board.calculateRemoveSequenceOperations(seq)) - ops._1 mustEqual Set( - Update(Point(3, 2), EmptyCell), - Update(Point(4, 2), EmptyCell), - Update(Point(5, 2), EmptyCell) - ) + ops._1 mustEqual Set(Update(Point(3, 2), EmptyCell), Update(Point(4, 2), EmptyCell), Update(Point(5, 2), EmptyCell)) ops._2 mustEqual List( Transition(Point(5, 1), Point(5, 2)), @@ -162,8 +151,7 @@ object BoardSpec extends Specification { Transition(Point(4, 1), Point(4, 2)), Transition(Point(4, 0), Point(4, 1)), Transition(Point(3, 1), Point(3, 2)), - Transition(Point(3, 0), Point(3, 1)) - ) + Transition(Point(3, 0), Point(3, 1))) } def testSequenceOperationsCalculator3 = { @@ -182,18 +170,12 @@ object BoardSpec extends Specification { val seq = board.matchedSequence.get val ops = separateOps(board.calculateRemoveSequenceOperations(seq)) - ops._1 mustEqual Set( - Update(Point(2, 4), EmptyCell), - Update(Point(2, 5), EmptyCell), - Update(Point(2, 6), EmptyCell) - ) + ops._1 mustEqual Set(Update(Point(2, 4), EmptyCell), Update(Point(2, 5), EmptyCell), Update(Point(2, 6), EmptyCell)) - ops._2 mustEqual List( - Transition(Point(2, 3), Point(2, 6)) - ) + ops._2 mustEqual List(Transition(Point(2, 3), Point(2, 6))) } - def testApplyOperations(board:Board, expect:Board) = { + def testApplyOperations(board: Board, expect: Board) = { val seq = board.matchedSequence.get val ops = board.calculateRemoveSequenceOperations(seq) val newBoard = board.applyOperations(ops) @@ -220,8 +202,7 @@ object BoardSpec extends Specification { 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 8 7 6 5 4 3 2 1 - """ - ) + """) def testApplyOperations2 = testApplyOperations( board = board""" @@ -243,8 +224,7 @@ object BoardSpec extends Specification { 8 7 6 5 4 3 2 1 1 2 5 4 5 6 7 8 8 7 6 5 4 3 2 1 - """ - ) + """) def testApplyOperations3 = testApplyOperations( board = board""" @@ -266,6 +246,5 @@ object BoardSpec extends Specification { 8 7 * 5 4 3 2 1 1 2 6 4 5 6 7 8 8 7 6 5 4 3 2 1 - """ - ) + """) } diff --git a/match3/src/test/scala/com/tenderowls/match3/GeneratorSpec.scala b/match3/src/test/scala/com/tenderowls/match3/GeneratorSpec.scala index 39b7b04..785f4b8 100644 --- a/match3/src/test/scala/com/tenderowls/match3/GeneratorSpec.scala +++ b/match3/src/test/scala/com/tenderowls/match3/GeneratorSpec.scala @@ -23,11 +23,7 @@ object GeneratorSpec extends Specification { However there is no more important cases """ - def simpleBoard = board"0 0 1".rawData mustEqual Vector( - IntCell(0), - IntCell(0), - IntCell(1) - ) + def simpleBoard = board"0 0 1".rawData mustEqual Vector(IntCell(0), IntCell(0), IntCell(1)) def board2d = { val board = board""" @@ -36,10 +32,15 @@ object GeneratorSpec extends Specification { 0 0 _ """ board.rawData mustEqual Vector( - IntCell(0), IntCell(0), IntCell(1), - EmptyCell, EmptyCell, IntCell(0), - IntCell(0), IntCell(0), BadCell - ) + IntCell(0), + IntCell(0), + IntCell(1), + EmptyCell, + EmptyCell, + IntCell(0), + IntCell(0), + IntCell(0), + BadCell) } } diff --git a/project/build.properties b/project/build.properties index 8522443..5a9ed92 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.2 +sbt.version=1.3.4 diff --git a/project/metals.sbt b/project/metals.sbt new file mode 100644 index 0000000..e36d9e9 --- /dev/null +++ b/project/metals.sbt @@ -0,0 +1,4 @@ +// DO NOT EDIT! This file is auto-generated. +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1-229-b7c15aa9") diff --git a/project/plugins.sbt b/project/plugins.sbt index cd34b09..f34fe36 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,4 @@ addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15") \ No newline at end of file +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.2.1") diff --git a/server/src/main/scala/com/tenderowls/match3/server/actors/BoardActor.scala b/server/src/main/scala/com/tenderowls/match3/server/actors/BoardActor.scala index 14a2de5..f4d51b5 100644 --- a/server/src/main/scala/com/tenderowls/match3/server/actors/BoardActor.scala +++ b/server/src/main/scala/com/tenderowls/match3/server/actors/BoardActor.scala @@ -2,8 +2,8 @@ package com.tenderowls.match3.server.actors import akka.actor.typed._ import akka.actor.typed.scaladsl.Behaviors -import com.tenderowls.match3.server.data.{ColorCell, Score} -import com.tenderowls.match3.BoardOperation.{Swap, Update} +import com.tenderowls.match3.server.data.{ ColorCell, Score } +import com.tenderowls.match3.BoardOperation.{ Swap, Update } import com.tenderowls.match3._ object BoardActor { @@ -25,14 +25,13 @@ object BoardActor { acc = acc :+ removeOps :+ transitOps, board = board.applyOperations(ops), score = score + removeOps.foldLeft(Score.empty) { - case (total, BoardOperation.Update(point, Cell.EmptyCell)) => - board.get(point).fold(total) { - case cell: ColorCell => total.inc(cell) - case _ => total - } - case (total, _) => total - } - ) + case (total, BoardOperation.Update(point, Cell.EmptyCell)) => + board.get(point).fold(total) { + case cell: ColorCell => total.inc(cell) + case _ => total + } + case (total, _) => total + }) } } aux(Nil, Score.empty, board.applyOperations(swapOperation)) @@ -43,25 +42,24 @@ object BoardActor { val ops = resultBoard .mapData { case (p, i) if resultBoard.rawData(i) == Cell.EmptyCell => Some(p) - case _ => None + case _ => None } .toList .flatten - .map { p => Update(p, rules.randomValue) } + .map { p => + Update(p, rules.randomValue) + } // Test board is stable (no matched sequences) resultBoard.applyOperations(ops).matchedSequence match { case None => ops - case _ => aux() + case _ => aux() } } aux() } - Result( - swapOperation +: resultOperations :+ fillOperations, - score - ) + Result(swapOperation +: resultOperations :+ fillOperations, score) } Behaviors.receive { diff --git a/server/src/main/scala/com/tenderowls/match3/server/actors/GameActor.scala b/server/src/main/scala/com/tenderowls/match3/server/actors/GameActor.scala index 025802d..abcc26a 100644 --- a/server/src/main/scala/com/tenderowls/match3/server/actors/GameActor.scala +++ b/server/src/main/scala/com/tenderowls/match3/server/actors/GameActor.scala @@ -1,6 +1,6 @@ package com.tenderowls.match3.server.actors -import akka.actor.typed.{Behavior, Terminated} +import akka.actor.typed.{ Behavior, Terminated } import akka.actor.typed.scaladsl.Behaviors import com.tenderowls.match3.server.data.Score import com.tenderowls.match3._ @@ -9,31 +9,29 @@ import scala.concurrent.duration._ object GameActor { - def apply(leftPlayer: Player, - rightPlayer: Player, - initialBoard: Board, - timeout: FiniteDuration, - match3Rules: Rules, - maxScore: Int): Behavior[Event] = { + def apply( + leftPlayer: Player, + rightPlayer: Player, + initialBoard: Board, + timeout: FiniteDuration, + match3Rules: Rules, + maxScore: Int): Behavior[Event] = { Behaviors.setup[Event] { ctx => - ctx.watch(leftPlayer) ctx.watch(rightPlayer) leftPlayer ! PlayerActor.Event.GameStarted( yourTurn = true, board = initialBoard, - game = ctx.self,//ctx.spawnAdapter((op: BoardOperation.Swap) => Event.MakeMove(leftPlayer, op)), - opponent = rightPlayer - ) + game = ctx.self, //ctx.spawnAdapter((op: BoardOperation.Swap) => Event.MakeMove(leftPlayer, op)), + opponent = rightPlayer) rightPlayer ! PlayerActor.Event.GameStarted( yourTurn = false, board = initialBoard, - game = ctx.self,//ctx.spawnAdapter((op: BoardOperation.Swap) => Event.MakeMove(rightPlayer, op)), - opponent = leftPlayer - ) + game = ctx.self, //ctx.spawnAdapter((op: BoardOperation.Swap) => Event.MakeMove(rightPlayer, op)), + opponent = leftPlayer) val boardActor = { val proxy = ctx.messageAdapter { result: BoardActor.Result => @@ -45,21 +43,24 @@ object GameActor { def awaitAnimation(turn: Behavior[Event], counter: Int = 1): Behavior[Event] = { Behaviors.receive[Event] { case (_, Event.AnimationFinished) if counter == 2 => turn - case (_, Event.AnimationFinished) => awaitAnimation(turn, counter + 1) - case _ => Behaviors.same + case (_, Event.AnimationFinished) => awaitAnimation(turn, counter + 1) + case _ => Behaviors.same } } receiveSignal { - case (_, Terminated(`leftPlayer`)) => - rightPlayer ! PlayerActor.Event.YouWin - Behaviors.stopped - case (_, Terminated(`rightPlayer`)) => - leftPlayer ! PlayerActor.Event.YouWin - Behaviors.stopped - } + case (_, Terminated(`leftPlayer`)) => + rightPlayer ! PlayerActor.Event.YouWin + Behaviors.stopped + case (_, Terminated(`rightPlayer`)) => + leftPlayer ! PlayerActor.Event.YouWin + Behaviors.stopped + } - def turn(turnNumber: Int, currentPlayer: Player, leftPlayerScore: Score, rightPlayerScore: Score): Behavior[Event] = { + def turn( + turnNumber: Int, + currentPlayer: Player, + leftPlayerScore: Score, + rightPlayerScore: Score): Behavior[Event] = { Behaviors.setup[Event] { ctx => - val endOfTurn = turnNumber % 2 == 0 val leftPlayerWins = leftPlayerScore.exists(_ >= maxScore) val rightPlayerWins = rightPlayerScore.exists(_ >= maxScore) @@ -68,18 +69,15 @@ object GameActor { leftPlayer ! PlayerActor.Event.Draw rightPlayer ! PlayerActor.Event.Draw Behaviors.stopped - } - else if (endOfTurn && leftPlayerWins) { + } else if (endOfTurn && leftPlayerWins) { leftPlayer ! PlayerActor.Event.YouWin rightPlayer ! PlayerActor.Event.YouLose Behaviors.stopped - } - else if (endOfTurn && rightPlayerWins) { + } else if (endOfTurn && rightPlayerWins) { leftPlayer ! PlayerActor.Event.YouLose rightPlayer ! PlayerActor.Event.YouWin Behaviors.stopped - } - else { + } else { if (currentPlayer == leftPlayer) { leftPlayer ! PlayerActor.Event.YourTurn(timeout) rightPlayer ! PlayerActor.Event.OpponentTurn(timeout) @@ -92,10 +90,10 @@ object GameActor { Behaviors.receive[Event] { case (_, Event.TimeIsOut) => - leftPlayer ! PlayerActor.Event.EndOfTurn + leftPlayer ! PlayerActor.Event.EndOfTurn rightPlayer ! PlayerActor.Event.EndOfTurn currentPlayer match { - case `leftPlayer` => turn(turnNumber + 1, rightPlayer, leftPlayerScore, rightPlayerScore) + case `leftPlayer` => turn(turnNumber + 1, rightPlayer, leftPlayerScore, rightPlayerScore) case `rightPlayer` => turn(turnNumber + 1, leftPlayer, leftPlayerScore, rightPlayerScore) } case (_, Event.MoveResult(batch, score)) => @@ -108,12 +106,12 @@ object GameActor { currentPlayer match { case `leftPlayer` => val newLeftPlayerScore = leftPlayerScore + score - leftPlayer ! PlayerActor.Event.CurrentScore(newLeftPlayerScore, rightPlayerScore) + leftPlayer ! PlayerActor.Event.CurrentScore(newLeftPlayerScore, rightPlayerScore) rightPlayer ! PlayerActor.Event.CurrentScore(rightPlayerScore, newLeftPlayerScore) awaitAnimation(turn(turnNumber + 1, rightPlayer, newLeftPlayerScore, rightPlayerScore)) case `rightPlayer` => val newRightPlayerScore = rightPlayerScore + score - leftPlayer ! PlayerActor.Event.CurrentScore(leftPlayerScore, newRightPlayerScore) + leftPlayer ! PlayerActor.Event.CurrentScore(leftPlayerScore, newRightPlayerScore) rightPlayer ! PlayerActor.Event.CurrentScore(newRightPlayerScore, leftPlayerScore) awaitAnimation(turn(turnNumber + 1, leftPlayer, leftPlayerScore, newRightPlayerScore)) } diff --git a/server/src/main/scala/com/tenderowls/match3/server/actors/LobbyActor.scala b/server/src/main/scala/com/tenderowls/match3/server/actors/LobbyActor.scala index a8581d5..dd0bb67 100644 --- a/server/src/main/scala/com/tenderowls/match3/server/actors/LobbyActor.scala +++ b/server/src/main/scala/com/tenderowls/match3/server/actors/LobbyActor.scala @@ -1,8 +1,8 @@ package com.tenderowls.match3.server.actors -import akka.actor.typed.{Behavior, Terminated} +import akka.actor.typed.{ Behavior, Terminated } import akka.actor.typed.scaladsl.Behaviors -import com.tenderowls.match3.{BoardGenerator, Rules} +import com.tenderowls.match3.{ BoardGenerator, Rules } import scala.concurrent.duration.FiniteDuration import scala.util.Random diff --git a/server/src/main/scala/com/tenderowls/match3/server/actors/PlayerActor.scala b/server/src/main/scala/com/tenderowls/match3/server/actors/PlayerActor.scala index cb3ed96..053c5a7 100644 --- a/server/src/main/scala/com/tenderowls/match3/server/actors/PlayerActor.scala +++ b/server/src/main/scala/com/tenderowls/match3/server/actors/PlayerActor.scala @@ -1,10 +1,10 @@ package com.tenderowls.match3.server.actors -import akka.actor.typed.{ActorRef, Behavior, Terminated} +import akka.actor.typed.{ ActorRef, Behavior, Terminated } import akka.actor.typed.scaladsl.Behaviors -import com.tenderowls.match3.BoardOperation.{Swap, Update} +import com.tenderowls.match3.BoardOperation.{ Swap, Update } import com.tenderowls.match3.server.data.Score -import com.tenderowls.match3.{Board, BoardAdviser} +import com.tenderowls.match3.{ Board, BoardAdviser } import scala.concurrent.duration._ @@ -27,12 +27,7 @@ object PlayerActor { } aux(0, board.applyOperations(List(swap))) -> swap } - board.advices - .toList - .map(applySwap) - .sortBy(-_._1) - .headOption - .map(_._2) + board.advices.toList.map(applySwap).sortBy(-_._1).headOption.map(_._2) } def localPlayer[U](name: String)(onEvent: PartialFunction[Event, U]): Behavior[Event] = {