Skip to content

Instantly share code, notes, and snippets.

@zainab-ali
Last active April 1, 2017 18:49
Show Gist options
  • Save zainab-ali/bd48e897739a4429f409336860a341d0 to your computer and use it in GitHub Desktop.
Save zainab-ali/bd48e897739a4429f409336860a341d0 to your computer and use it in GitHub Desktop.
Event Handlers and State combinations
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))
)
}
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