diff --git a/app/service/GameServer.scala b/app/service/GameServer.scala index 9395d18..fa27f16 100644 --- a/app/service/GameServer.scala +++ b/app/service/GameServer.scala @@ -1,7 +1,6 @@ package service import akka.actor._ -import scala.util.Try import play.api.libs.iteratee.{Iteratee, Enumerator, Concurrent} import scala.concurrent.duration._ import play.api.libs.concurrent.Execution.Implicits._ @@ -30,7 +29,8 @@ class GameServer extends Actor { case Create => val gameId = context.children.size + 1 - val game = context.actorOf(Props[Game], s"game$gameId") +// val game = context.actorOf(Props[Game], s"game$gameId") + val game = context.actorOf(Props(classOf[RuleGame], "S2B3"), s"game$gameId") context.watch(game) sender ! Created(game) gameListChannel push context.children.map(_.path.toString).mkString("\n") @@ -97,7 +97,6 @@ object GameServer { } // actor messages - case object Create trait Result { @@ -116,115 +115,10 @@ object GameServer { case class Subscribed(enumerator: Enumerator[String]) - -} - - -class Game extends Actor { - - import service.Game._ - - var space: Space = Space() - var rectOpt: Option[Rect] = None - var paused = true - val (gameEnumerator, gameChannel) = Concurrent.broadcast[String] - - def sendUpdate() = gameChannel push s"UPDATE ${self.path.toString}\n${space.toSet(rectOpt).map(c => s"${c.x}:${c.y}").mkString("\n")}" - - def sendJoin(username: String) = gameChannel push s"JOINED $username TO ${self.path.toString}" - - def sendClose() = gameChannel.eofAndEnd() - - def nextStep() { - if (!paused) context.system.scheduler.scheduleOnce(100 millis) { - self ! Step - } - val (newState, newRect) = space.transform().persist(rectOpt) - space = newState - rectOpt = newRect - sendUpdate() - } - - def receive = normal - - def normal: Actor.Receive = { - case Join(username) => - sender ! Joined(gameEnumerator) - sendJoin(username) - case Touch(x, y) => - //Logger.info(s"space touched at [$x:$y]") - rectOpt match { - case Some(rect) => - if (space(x, y)) - space --= Space((x, y)) - else { - space ++= Space((x, y)) - rectOpt = Some(rect.extend((x, y).toPoint)) - } - case None => - space = Space((x, y)) - rectOpt = Some(Rect(x, y, x, y)) - } - //Logger.debug(s"current state \n${space.toString(rectOpt)}") - sendUpdate() - case Pause => - paused = !paused - nextStep() - case Stop => - sendClose() - context.become(stopping) - context.system.scheduler.scheduleOnce(1 second) { - context.stop(self) - } - case Step => nextStep() - } - - def stopping: Receive = { - case _ => /* ignore all messages while stopping */ - } - } object Game { -// import scala.language.reflectiveCalls -// -// trait Rules { -// val states: Enumeration -// -// sealed case class Cell(coords: Point, state: states.Value) -// -// type State = (Int, Int) => states.Value -// type Transform = (State) => State -// type Persist = (State, Rect) => Set[Cell] -// val persistStates: Set[states.Value] -// val transform: Transform -// val persist: Persist -// } -// -// class s2b3 extends Rules { -// val states = new Enumeration { -// val DEAD, ALIVE = Value -// } -// val persistStates = Set(states.ALIVE) -// val transform = (state: State) => (x: Int, y: Int) => -// (for { -// xx <- x - 1 to x + 1 -// yy <- y - 1 to y + 1 if xx != x || yy != y -// } yield state(xx, yy)).count(_ == states.ALIVE) match { -// case 2 => state(x, y) -// case 3 => states.ALIVE -// case _ => states.DEAD -// } -// -// val persist: Persist = (state: State, rect: Rect) => -// (for { -// x <- rect.x1 to rect.x2 -// y <- rect.y1 to rect.y2 -// } yield Cell((x, y).toPoint, state(x, y))).toSet.filter(c => persistStates.contains(c.state)) -// } - - case class Touch(x: Int, y: Int) case object Step @@ -237,65 +131,4 @@ object Game { case class Joined(enumerator: Enumerator[String]) - type Space = (Int, Int) => Boolean - - object Space { - - def apply(p: Point*): Space = (x: Int, y: Int) => p.contains((x, y).toPoint) - - def apply(points: Set[Point]): Space = (x: Int, y: Int) => points.contains((x, y)) - - def apply(s: String): Space = - (x: Int, y: Int) => Try(s.split('\n')(y).charAt(x)).getOrElse(".") == 'O' - - def empty: Space = (x: Int, y: Int) => false - - } - - implicit class PrintableSpace(state: Space) { - def toString(rectOpt: Option[Rect]): String = rectOpt match { - case None => "" - case Some(rect) => - (for { - y <- rect.y1 to rect.y2 - x <- rect.x1 to rect.x2 - } yield s"${ if (state(x, y)) "O" else "." } ${ if (x == rect.x2) "\n" else "" }").mkString - } - } - - implicit class TransformableSpace(state: Space) { - def transform(): Space = - (x: Int, y: Int) => - (for { - xx <- x - 1 to x + 1 - yy <- y - 1 to y + 1 if xx != x || yy != y - } yield state(xx, yy)).count(_ == true) match { - case 2 => state(x, y) - case 3 => true - case _ => false - } - - def persist(rectOpt: Option[Rect]): (Space, Option[Rect]) = { - val table = state.toSet(rectOpt.map(_.zoom(1))) - ((x: Int, y: Int) => table.contains((x, y)), table.bounds) - } - - def ++(other: Space): Space = - (x: Int, y: Int) => state(x, y) || other(x, y) - - def --(other: Space): Space = - (x: Int, y: Int) => state(x, y) && !other(x, y) - - def toSet(rect: Rect): Set[Point] = (for { - y <- rect.y1 to rect.y2 - x <- rect.x1 to rect.x2 if state(x, y) - } yield (x, y).toPoint).toSet - - def toSet(rectOpt: Option[Rect]): Set[Point] = rectOpt match { - case None => Set.empty - case Some(rect) => state.toSet(rect) - } - - } - } diff --git a/app/service/RuleGame.scala b/app/service/RuleGame.scala new file mode 100644 index 0000000..9885c67 --- /dev/null +++ b/app/service/RuleGame.scala @@ -0,0 +1,269 @@ +package service + +import akka.actor.Actor +import scala.language.postfixOps +import play.api.libs.concurrent.Execution.Implicits._ + +import Game._ + +import RuleGame._ +import play.api.libs.iteratee.Concurrent +import scala.concurrent.duration._ +import Rules._ +import play.Logger +import scala.util.Random + +class RuleGame(rulesName: String) extends Actor { + + println(rulesName) + + implicit val rules = RuleGame.rules.get(rulesName) match { + case Some(r) => r + case None => + Logger.warn(s"Rules [$rulesName] not found, used default rules.") + RuleGame.defaultRules + } + + var currentSpace: PersistedSpace = rules.initialSpace + var paused = true + val (gameEnumerator, gameChannel) = Concurrent.broadcast[String] + + def sendUpdate() = gameChannel push s"UPDATE ${self.path.toString}\n${currentSpace.cells.map(c => s"${c._1.x}:${c._1.y}:${c._2}").mkString("\n")}" + + def sendJoin(username: String) = gameChannel push s"JOINED $username TO ${self.path.toString}" + + def sendClose() = gameChannel.eofAndEnd() + + def step() { + if (!paused) { + context.system.scheduler.scheduleOnce(100 millis) { + self ! Step + } + currentSpace = currentSpace.transform() + sendUpdate() + } + } + + def receive: Actor.Receive = working + + def working: Actor.Receive = { + case Join(username) => + sender ! Joined(gameEnumerator) + sendJoin(username) + case Touch(x, y) => + currentSpace = rules.touch(currentSpace, (x, y).toPoint) + sendUpdate() + case Pause => + paused = !paused + step() + case Stop => + sendClose() + context.become(stopping) + context.system.scheduler.scheduleOnce(1 second) { + context.stop(self) + } + case Step => step() + } + + def stopping: Receive = { + case _ => /* ignore all messages while stopping */ + } + +} + + +object RuleGame { + + trait State { + //override def toString = this.getClass.getSimpleName + } + + implicit class PointTuple(tuple: (Int, Int)) { + def toPoint: Point = Point(tuple._1, tuple._2) + + def toRect: Rect = Rect(Point(tuple._1, tuple._2), Point(tuple._1, tuple._2)) + + def min: Int = if (tuple._1 < tuple._2) tuple._1 else tuple._2 + + def max: Int = if (tuple._1 > tuple._2) tuple._1 else tuple._2 + } + + implicit class RectTuple(tuple: (Int, Int, Int, Int)) { + def toRect: Rect = Rect(Point(tuple._1, tuple._2), Point(tuple._3, tuple._4)) + } + + case class Point(x: Int, y: Int) { + def toRect: Rect = (x, y).toRect + } + + case class Rect(ul: Point, lr: Point) { + def extend(point: Point): Rect = + ((point.x, ul.x).min, (point.y, ul.y).min, (point.x, lr.x).max, (point.y, lr.y).max).toRect + + def zoom(px: Int): Rect = zoom(px, px, px, px) + + def zoom(left: Int, up: Int, right: Int, down: Int): Rect = + (ul.x - left, ul.y - up, lr.x + right, lr.y + down).toRect + } + + object Rect { + def apply(point: Point): Rect = Rect(point, point) + + def apply(x1: Int, y1: Int, x2: Int, y2: Int): Rect = Rect((x1, y1).toPoint, (x2, y2).toPoint) + } + + /** + * Abstract rules + */ + abstract class Rules { + + import Rules._ + + // transform between generations + val transform: TransformFunc + // default cell state + val defaultState: State + + // initial space state + def initialSpace: PersistedSpace + + // interaction with space + def touch(space: PersistedSpace, point: Point): PersistedSpace + + // list of states which are stored between generations + val persistStates: Set[State] + + // persist generation of cells inside fixed frame/rectangle + def persist(space: Space, rectOpt: Option[Rect]): PersistedSpace = { + val cells: Map[Point, State] = rectOpt.map(_.zoom(1)) match { + case None => Map.empty + case Some(rect) => (for { + x <- rect.ul.x to rect.lr.x + y <- rect.ul.y to rect.lr.y + p <- Some((x, y).toPoint) + s <- Some(space(p)) if persistStates.contains(s) + } yield p -> space(p)).toMap + } + val newSpace: Space = (point: Point) => cells.get(point) match { + case Some(state) => state + case None => defaultState + } + PersistedSpace(cells, newSpace) + } + } + + object Rules { + + // functional representation of space + type Space = Point => State + // transformation function type + type TransformFunc = Space => Space + // persist function type + type PersistFunc = (Space, Option[Rect]) => PersistedSpace + + // persisted space representation + sealed case class PersistedSpace(cells: Map[Point, State], space: Space) { + + lazy val bounds: Option[Rect] = cells.keys.foldLeft[Option[Rect]](None)((rectOpt, point) => rectOpt match { + case Some(rect) => Some(rect.extend(point)) + case None => Some(Rect(point)) + }) + + override def toString: String = bounds match { + case None => "" + case Some(rect) => + (for { + y <- rect.ul.y to rect.lr.y + x <- rect.ul.x to rect.lr.x + } yield s"${ if (cells.contains((x, y).toPoint)) "O" else "." } ${ if (x == rect.lr.x) "\n" else "" }").mkString + } + + } + + // utility classes + implicit class HyperSpace(space: Space)(implicit rules: Rules) { + def transform(): Space = rules.transform(space) + } + + implicit class PersistableSpace(space: Space)(implicit rules: Rules) { + def persist(rectOpt: Option[Rect]): PersistedSpace = rules.persist(space, rectOpt) + } + + implicit class TransformablePersistedSpace(space: PersistedSpace)(implicit rules: Rules) { + def transform(): PersistedSpace = rules.persist(rules.transform(space.space), space.bounds) + } + + implicit class TouchableSpace(space: PersistedSpace)(implicit rules: Rules) { + def touch(point: Point): PersistedSpace = rules.touch(space, point) + } + + } + + /** + * Standard Game of Life Conway rules + * Cases: + * 2 neighbours - stay alive + * 3 neighbours - birth + * 1 or 4+ neighbours - death + */ + abstract class S2B3 extends Rules { + + import Rules._ + + case object DEAD extends State + + case object ALIVE extends State + + val persistStates: Set[State] = Set(ALIVE) + + val transform: TransformFunc = + (space: Space) => { + (p: Point) => + (for { + xx <- p.x - 1 to p.x + 1 + yy <- p.y - 1 to p.y + 1 if xx != p.x || yy != p.y + p <- Some((xx, yy).toPoint) + } yield space(p)).count(_ == ALIVE) match { + case 2 => space((p.x, p.y).toPoint) + case 3 => ALIVE + case _ => DEAD + } + } + + val defaultState: State = DEAD + + lazy val initialSpace: PersistedSpace = persist({ + case _ => defaultState + }, None) + + def touch(space: PersistedSpace, point: Point): PersistedSpace = { + val s: Space = { + case p if p == point => if (space.space(p) == DEAD) ALIVE else DEAD + case p => space.space(p) + } + val r: Rect = space.bounds match { + case None => point.toRect + case Some(rect) => rect.extend(point) + } + persist(s, Some(r)) + } + } + + // default S2B3 rules singleton + object defaultS2B3 extends S2B3 + + object randomS2B3 extends S2B3 { + val initSize = 100 + override lazy val initialSpace: PersistedSpace = persist({ + case _ => if (Random.nextBoolean()) ALIVE else DEAD + }, Some(Rect(Point(0,0)).zoom(initSize / 2))) + } + + // list of implementations + lazy val defaultRules = defaultS2B3 + lazy val rules = Map( + "S2B3" -> defaultRules, + "Random S2B3 100x100" -> randomS2B3 + ) + +} diff --git a/app/service/package.scala b/app/service/package.scala index 0c11a2b..c8c14df 100644 --- a/app/service/package.scala +++ b/app/service/package.scala @@ -1,4 +1,4 @@ -import service.Game.Space +//import service.Game.Space import scala.language.implicitConversions package object service { @@ -70,21 +70,21 @@ package object service { } } - implicit class IntTuple2(tuple: (Int, Int)) { +// implicit class IntTuple2(tuple: (Int, Int)) { +// +// def min: Int = if (tuple._1 < tuple._2) tuple._1 else tuple._2 +// +// def max: Int = if (tuple._1 > tuple._2) tuple._1 else tuple._2 +// +// def sort: (Int, Int) = if (tuple._1 < tuple._2) (tuple._1, tuple._2) else (tuple._2, tuple._1) +// +// def toPoint: Point = Point(tuple) +// +// } - def min: Int = if (tuple._1 < tuple._2) tuple._1 else tuple._2 - - def max: Int = if (tuple._1 > tuple._2) tuple._1 else tuple._2 - - def sort: (Int, Int) = if (tuple._1 < tuple._2) (tuple._1, tuple._2) else (tuple._2, tuple._1) - - def toPoint: Point = Point(tuple) - - } - - implicit class IntTuple4(tuple: (Int, Int, Int, Int)) { - def toRect: Rect = Rect(tuple._1, tuple._2, tuple._3, tuple._4) - } +// implicit class IntTuple4(tuple: (Int, Int, Int, Int)) { +// def toRect: Rect = Rect(tuple._1, tuple._2, tuple._3, tuple._4) +// } implicit class IntList(list: List[Int]) { @@ -99,23 +99,23 @@ package object service { ).map(p => (p._1 + 1 - p._2, p._1)) } - implicit class PointSet(grid: Set[Point]) { - def bounds: Option[Rect] = grid.foldLeft[Option[Rect]](None)((rectOpt, point) => rectOpt match { - case Some(rect) => Some(rect.extend(point)) - case None => Some(Rect(point)) - }) - - def toSpace: Space = Space(grid) - - def split(borders: Rect, minGap: Int): List[(Rect, Set[Point])] = { - grid.map(_.x).toList.sorted.groupSeq(minGap) flatMap { - d => - val points = grid.filter((d._1, borders.y1, d._2, borders.y2).toRect.contains) - points.bounds.map(_ -> points) - } - } - - } +// implicit class PointSet(grid: Set[Point]) { +// def bounds: Option[Rect] = grid.foldLeft[Option[Rect]](None)((rectOpt, point) => rectOpt match { +// case Some(rect) => Some(rect.extend(point)) +// case None => Some(Rect(point)) +// }) +// +// def toSpace: Space = Space(grid) +// +// def split(borders: Rect, minGap: Int): List[(Rect, Set[Point])] = { +// grid.map(_.x).toList.sorted.groupSeq(minGap) flatMap { +// d => +// val points = grid.filter((d._1, borders.y1, d._2, borders.y2).toRect.contains) +// points.bounds.map(_ -> points) +// } +// } +// +// } implicit class RectSet(set: Set[Rect]) {