Last active
April 1, 2017 18:49
-
-
Save zainab-ali/bd48e897739a4429f409336860a341d0 to your computer and use it in GitHub Desktop.
Event Handlers and State combinations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game | |
/** Game logic: | |
* | |
* The game map is a tiled grid. Each tile has something on it. | |
* | |
* Actions: | |
* When a citizen is clicked on, they can be told to do something. | |
* At the moment, they can only chop wood. | |
* | |
* When a citizen is told to chop wood, the user should click on a tree. They will walk to that tree and chop it down. | |
* | |
* When a move event is detected, the player moves a square. They can't move if the user is commanding the citizen. | |
* | |
*/ | |
case class Vec2i(x: Int, y: Int) { | |
def +(o: Vec2i): Vec2i = Vec2i(x + o.x, y + o.y) | |
def <(o: Vec2i): Boolean = x < o.x && y < o.y | |
} | |
sealed trait Thing { | |
def position: Vec2i | |
} | |
case class Tree(position: Vec2i) extends Thing | |
case class Shrub(position: Vec2i) extends Thing | |
case class Player(position: Vec2i, things: List[Thing]) | |
case class Citizen(position: Vec2i, action: CitizenAction) | |
sealed trait CitizenAction | |
case object ChoppingWood extends CitizenAction | |
case object StandingAround extends CitizenAction | |
case object PickingShrubs extends CitizenAction | |
sealed trait Command | |
case class ChopWood(citizen: Citizen, tree: Tree) extends Command | |
sealed trait State | |
case class Overview( | |
dimensions: Vec2i, | |
things: Map[Vec2i, Thing], | |
player: Player, | |
citizens: List[Citizen]) extends State | |
case class CommandingCitizen(citizen: Citizen, overview: Overview) extends State | |
case class SelectingTree(citizen: Citizen, overview: Overview) extends State | |
sealed trait Event | |
case class MoveEvent(distance: Vec2i) extends Event | |
case class KeypressEvent(character: String) extends Event | |
case class Click(position: Vec2i) extends Event | |
object GameLogic { | |
import event._ | |
import cats.implicits._ | |
val movePlayer = Handler.write[MoveEvent, Overview, Vec2i, String]( { (e, s) => | |
val next = s.player.position + e.distance | |
if(next < s.dimensions && next.x >= 0 && next.y >= 0) Some(next) | |
else None | |
}, | |
(s, p) => (s.copy(player = s.player.copy(position = p)), s"player has moved tp $p") | |
) | |
val pickShrub = Handler.write[KeypressEvent, Overview, Shrub, String]( | |
(e, s) => e.character match { | |
case "P" => s.things.get(s.player.position).flatMap { | |
case sh : Shrub => Some(sh) | |
case _ => None | |
}}, | |
(s, sh) => (s.copy(things = s.things - sh.position, player = s.player.copy(things = sh :: s.player.things)), "player has picked shrub") | |
) | |
val commandingCitizen = Handler.transition[Click, Overview, Citizen, CommandingCitizen, String]( | |
(e, s) => s.citizens.find(_.position == e.position), | |
(s, c) => (CommandingCitizen(c, s), s"commanding citizen $c") | |
) | |
val citizenShouldChopWood = Handler.transition[KeypressEvent, CommandingCitizen, Unit, SelectingTree, String]( | |
(e, _) => e.character match { | |
case "C" => Some(()) | |
case _ => None | |
}, | |
(s, _) => (SelectingTree(s.citizen, s.overview), s"citizen should chop wood") | |
) | |
val selectTreeToChop = Handler.transition[Click, SelectingTree, Tree, Overview, (String, Command)]( | |
(e, s) => s.overview.things.get(e.position).flatMap { | |
case t: Tree => | |
Some(t) | |
case _ => | |
None | |
}, | |
(s, t) => (s.overview.copy(citizens = s.citizen.copy(action = ChoppingWood) :: s.overview.citizens.filterNot(_ == s.citizen)), | |
(s"citizen should chop down tree $t", ChopWood(s.citizen, t)) | |
)) | |
val _loop = | |
(((movePlayer.widenE[Event] combine | |
pickShrub.widenE[Event]).widenIS[State].widenOS[State] combine | |
commandingCitizen.widenE[Event].widenIS[State].widenOS[State] combine | |
citizenShouldChopWood.widenE[Event].widenIS[State].widenOS[State]).mapO(o => (Some(o), Option.empty[Command])) combine | |
selectTreeToChop.widenE[Event].widenIS[State].widenOS[State].mapO { | |
case (s, c) => (Some(s), Some(c)) | |
} | |
).finalize | |
val gameLoop: Loop[Event, State, (Option[String], Option[Command])] = | |
Loop({(e, s) => | |
val (ns, o) = _loop.f(e, s) | |
(ns, o.getOrElse((Option.empty[String], Option.empty[Command]))) | |
}) | |
val start: State = Overview( | |
dimensions = Vec2i(9, 9), | |
things = Map(Vec2i(1, 1) -> Tree(Vec2i(1, 1))), | |
player = Player(position = Vec2i(5, 5), things = List.empty), | |
citizens = List(Citizen(position = Vec2i(8, 8), action = StandingAround)) | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package event | |
import scala.reflect.ClassTag | |
import cats._ | |
/** Given an input event E and an input state S, produce an output state S and an effect O */ | |
case class Loop[E, S, O](f: (E, S) => (S, O)) | |
/** Handles an event E, given an input state IS and an output state O | |
* | |
* @tparam E the event to be handled | |
* @tparam IS the input state | |
* @tparam OS the output state | |
* @tparam M the intermediate | |
* @tparam O the output | |
* | |
* @param accept the function which determines if an event and input state can be acted upon | |
* @param handle the function to act on the input state, producing an output state and output | |
*/ | |
case class Handler[E, IS, M, OS, O](accept: (E, IS) => Option[M], handle: M => (OS, O)) { | |
def prismContramapE[E1](f: E1 => Option[E]): Handler[E1, IS, M, OS, O] = | |
Handler((e1, s) => f(e1).flatMap(e => accept(e, s)), handle) | |
def widenE[E1 >: E](implicit ct: ClassTag[E]): Handler[E1, IS, M, OS, O] = prismContramapE{ | |
case e: E => Some(e) | |
case _ => None | |
} | |
def prismContramapS[IS1](f: IS1 => Option[IS]): Handler[E, IS1, M, OS, O] = | |
Handler((e, s1) => f(s1).flatMap(s => accept(e, s)), handle) | |
def widenIS[IS1 >: IS](implicit ct: ClassTag[IS]): Handler[E, IS1, M, OS, O] = prismContramapS[IS1] { | |
case s: IS => Some(s) | |
case _ => None | |
} | |
def mapOS[OS1](f: OS => OS1): Handler[E, IS, M, OS1, O] = | |
Handler(accept, { (m) => | |
val (os, o) = handle(m) | |
(f(os), o) | |
}) | |
def widenOS[OS1 >: OS] = this.asInstanceOf[Handler[E, IS, M, OS1, O]] | |
def mapO[O1](f: O => O1): Handler[E, IS, M, OS, O1] = | |
Handler(accept, { (m) => | |
val (os, o) = handle(m) | |
(os, f(o)) | |
}) | |
def combine[N](h: Handler[E, IS, N, OS, O]): Handler[E, IS, M Either N, OS, O] = | |
Handler((e, s) => accept(e, s).map(Left(_)).orElse(h.accept(e, s).map(Right(_))), { | |
case Left(m) => handle(m) | |
case Right(n) => h.handle(n) | |
}) | |
def finalize()(implicit ev: IS =:= OS): Loop[E, IS, Option[O]] = | |
Loop[E, IS, Option[O]]((e, s) => accept(e, s).map(m => handle(m).asInstanceOf[(IS, O)]) match { | |
case Some((is, o)) => (is, Some(o)) | |
case None => (s, None) | |
}) | |
} | |
object Handler { | |
def transition[E, IS, M, OS, O](accept: (E, IS) => Option[M], handle: (IS, M) => (OS, O)): Handler[E, IS, (IS, M), OS, O] = | |
Handler[E, IS, (IS, M), OS, O]((e: E, is: IS) => accept(e, is).map(m => (is, m)), { case (is, m) => handle(is, m) }) | |
def modify[E, S, M](accept: (E, S) => Option[M], handle: (S, M) => S): Handler[E, S, (S, M), S, Unit] = | |
transition[E, S, M, S, Unit](accept, (s, m) => (handle(s, m), ())) | |
def write[E, S, M, O](accept: (E, S) => Option[M], handle: (S, M) => (S, O)): Handler[E, S, (S, M), S, O] = | |
transition[E, S, M, S, O](accept, handle) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment