Last active
January 23, 2019 08:29
-
-
Save jdegoes/07b27e4ef1d2f2b68f0d576bc3b68305 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
object game { | |
case class Lens[S, A](set: A => S => S, get: S => A) { self => | |
def >>> [B](that: Lens[A, B]): Lens[S, B] = | |
Lens[S, B]( | |
set = (b: B) => (s: S) => self.set(that.set(b)(self.get(s)))(s), | |
get = (s: S) => that.get(self.get(s)) | |
) | |
} | |
case class Prism[S, A](set: A => S, get: S => Option[A]) { self => | |
def >>> [B](that: Prism[A, B]): Prism[S, B] = | |
Prism[S, B]( | |
set = (b: B) => self.set(that.set(b)), | |
get = (s: S) => self.get(s).flatMap(that.get) | |
) | |
} | |
def _1[A, B]: Lens[(A, B), A] = | |
Lens[(A, B), A]((a: A) => (t: (A, B)) => (a, t._2), _._1) | |
def _2[A, B]: Lens[(A, B), B] = | |
Lens[(A, B), B]((b: B) => (t: (A, B)) => (t._1, b), _._2) | |
case class Player( | |
name : String, | |
inventory : List[Item], | |
level : Int, | |
position : (Int, Int) | |
) | |
object Player { | |
val _Name = Lens[Player, String]((name: String) => (player: Player) => player.copy(name = name), _.name) | |
val _Level = Lens[Player, Int]((level: Int) => (player: Player) => player.copy(level = level), _.level) | |
val _Position = Lens[Player, (Int, Int)]((t: (Int, Int)) => (player: Player) => player.copy(position = t), _.position) | |
} | |
case class Item(describe: String, name: String) | |
sealed trait Cell { | |
def look: String | |
def toChar: Char | |
def items: List[Item] | |
} | |
case class Land(description: String = "Green rolling hills and shrubbery", items: List[Item] = Nil) extends Cell { | |
def look = description | |
def toChar = '_' | |
} | |
case class Sea(description: String = "Crystal clear, blue ocean", items: List[Item] = Nil) extends Cell { | |
def look = description | |
def toChar = '~' | |
} | |
case class GameMap(world: Vector[Vector[Cell]]) { | |
def cellAt(tuple: (Int, Int)): Option[Cell] = { | |
val (x, y) = tuple | |
for { | |
column <- world.lift(x) | |
cell <- column.lift(y) | |
} yield cell | |
} | |
def items(position: (Int, Int)): List[Item] = cellAt(position).map(_.items).getOrElse(Nil) | |
} | |
case class GameWorld( | |
player: Player, | |
map: GameMap | |
) { | |
def render: String = { | |
val chars0: Vector[Vector[Char]] = map.world.map(_.map(_.toChar)) | |
val row = chars0(player.position._1) | |
val chars = chars0.updated(player.position._1, row.updated(player.position._2, 'X')) | |
"\n" + chars.map(_.map(_.toString()).mkString("|")).mkString("\n") + "\n" | |
} | |
} | |
object GameWorld { | |
val _Player : Lens[GameWorld, Player] = | |
Lens[GameWorld, Player]((player: Player) => (world: GameWorld) => world.copy(player = player), _.player) | |
val _Map : Lens[GameWorld, GameMap] = | |
Lens[GameWorld, GameMap]((map: GameMap) => (world: GameWorld) => world.copy(map = map), _.map) | |
val Initial = | |
GameWorld( | |
Player( | |
name = "John Doe", | |
inventory = Nil, | |
level = 1, | |
position = (0, 0) | |
), | |
GameMap( | |
Vector( | |
Vector( | |
Land("A mountain peak with a beautiful vista of the nearby city"), | |
Sea(), | |
Land() | |
), | |
Vector( | |
Land(), | |
Land(), | |
Land() | |
), | |
Vector( | |
Sea(), | |
Sea(), | |
Land() | |
) | |
) | |
) | |
) | |
} | |
case class Character() | |
sealed trait Command | |
case class Move(direction: Direction) extends Command | |
case class Attack(character: Character) extends Command | |
case class Look(what: Option[Either[Item, Character]]) extends Command | |
case class Take(what: String) extends Command | |
case class Combine(item1: Item, item2: Item) extends Command | |
case class Use(item: Item) extends Command | |
case object Exit extends Command | |
object Command { | |
val _Move = Prism[Command, Direction]((d: Direction) => Move(d), { | |
case Move(d) => Some(d) | |
case _ => None | |
}) | |
} | |
sealed trait Direction | |
case object North extends Direction | |
case object South extends Direction | |
case object West extends Direction | |
case object East extends Direction | |
object Direction { | |
val _North = Prism[Direction, Unit](_ => North, { | |
case North => Some(()) | |
case _ => None | |
}) | |
val _South = Prism[Direction, Unit](_ => South, { | |
case South => Some(()) | |
case _ => None | |
}) | |
val _West = Prism[Direction, Unit](_ => West, { | |
case West => Some(()) | |
case _ => None | |
}) | |
val _East = Prism[Direction, Unit](_ => East, { | |
case East => Some(()) | |
case _ => None | |
}) | |
} | |
def parse(line: String): Either[String, Command] = { | |
val words = line.toLowerCase.split("\\s+") | |
if (words.length == 0) Right(Look(None)) | |
else { | |
words(0) match { | |
case "exit" | "quit" | "bye" => Right(Exit) | |
case "look" => Right(Look(None)) | |
case "go" | "move" => | |
if (words.length < 2) Left("Specify a direction to move.") | |
else { | |
words(1) match { | |
case "south" => Right(Move(South)) | |
case "north" => Right(Move(North)) | |
case "west" => Right(Move(West)) | |
case "east" => Right(Move(East)) | |
case _ => Left("Unrecognized direction") | |
} | |
} | |
case "take" => | |
if (words.length < 2) Left("Specify an item to take.") | |
else Right(Take(words(1))) | |
case _ => Left("I don't recognize your command.") | |
} | |
} | |
} | |
case class Update(message: String, next: Option[GameWorld]) | |
def update(command: Command, world: GameWorld): Update = { | |
val unchanged = Update("Too lazy to implement", Some(world)) | |
command match { | |
case Move(dir) => | |
val (deltaX, deltaY) = | |
dir match { | |
case North => (0, 1) | |
case South => (0, -1) | |
case East => (1, 0) | |
case West => (-1, 0) | |
} | |
val (x, y) = world.player.position | |
val p = (x + deltaX, y + deltaY) | |
world.map.cellAt(p).map(cell => { | |
import GameWorld._ | |
import Player._ | |
val newWorld = (_Player >>> _Position).set(p)(world) | |
val render = newWorld.render | |
val around = cell.look | |
val items = "You can see the following items: " + newWorld.map.items(p).mkString(", ") + "\n" | |
Update(render + around + items, Some(newWorld)) | |
}).getOrElse( | |
Update("You cannot move past the edge of the world!", Some(world)) | |
) | |
case Attack(_) => unchanged | |
case Look(Some(_)) => unchanged | |
case Look(None) => | |
val cell: Option[Cell] = world.map.cellAt(world.player.position) | |
cell.map { cell => | |
Update(cell.look, Some(world)) | |
}.getOrElse(Update("You are located outside of time and space. Bye-bye!", None)) | |
case Take(name) => | |
val items = world.map.items(world.player.position) | |
val (matching, _) = items.partition(_.name == name) | |
if (matching.length == 0) { | |
Update("There is no " + name + " to take!", Some(world)) | |
} else { | |
Update("You have taken: " + matching.map(_.name).mkString(", "), | |
Some(world.copy( | |
player = world.player.copy(inventory = world.player.inventory ++ matching) | |
)) | |
) | |
} | |
case Combine(_, _) => unchanged | |
case Use(_) => unchanged | |
case Exit => Update("Bye, bye!", None) | |
} | |
} | |
def gameStep[F[_]: Monad: ConsoleIO](world: GameWorld): F[Option[GameWorld]] = | |
for { | |
input <- ConsoleIO[F].readLine | |
world <- parse(input) match { | |
case Left(error) => ConsoleIO[F].println(error).map(_ => Some(world)) | |
case Right(command) => | |
val Update(message, world2) = update(command, world) | |
ConsoleIO[F].println(message).map(_ => world2) | |
} | |
} yield world | |
def gameLoop[F[_]: Monad: ConsoleIO](world: GameWorld): F[Unit] = | |
gameStep(world).flatMap { | |
case None => Monad[F].point(()) | |
case Some(world) => gameLoop(world) | |
} | |
trait Monad[F[_]] { | |
def point[A](a: A): F[A] | |
def bind[A, B](fa: F[A])(afb: A => F[B]): F[B] | |
def fmap[A, B](ab: A => B): F[A] => F[B] | |
} | |
object Monad { | |
def apply[F[_]](implicit F: Monad[F]): Monad[F] = F | |
} | |
implicit class MonadSyntax[F[_], A](fa: F[A]) { | |
def map[B](ab: A => B)(implicit F: Monad[F]): F[B] = | |
F.fmap(ab)(fa) | |
def flatMap[B](afb: A => F[B])(implicit F: Monad[F]): F[B] = | |
F.bind(fa)(afb) | |
} | |
implicit class PointSyntax[A](a: A) { | |
def point[F[_]](implicit F: Monad[F]): F[A] = | |
F.point(a) | |
} | |
final abstract class Void { | |
def absurd[A]: A | |
} | |
final case class IO[E, A]( | |
unsafePerformIO: () => Either[E, A]) { self => | |
def map[B](f: A => B): IO[E, B] = | |
IO(() => { | |
self.unsafePerformIO() match { | |
case Left(e) => Left(e) | |
case Right(a) => Right(f(a)) | |
} | |
}) | |
def flatMap[B](f: A => IO[E, B]): IO[E, B] = | |
IO(() => { | |
self.unsafePerformIO() match { | |
case Left(e) => Left(e) | |
case Right(a) => f(a).unsafePerformIO() | |
} | |
}) | |
def mapError[E2](f: E => E2): IO[E2, A] = | |
IO(() => self.unsafePerformIO() match { | |
case Left(e) => Left(f(e)) | |
case Right(a) => Right(a) | |
}) | |
def attempt: IO[Void, Either[E, A]] = | |
IO(() => Right(self.unsafePerformIO())) | |
def fork: IO[Void, IO[E, A]] = IO(() => { | |
import java.util.concurrent._ | |
val task = ForkJoinTask.adapt(new Callable[Either[E, A]] { | |
def call(): Either[E, A] = self.unsafePerformIO() | |
}) | |
IO.MainPool.invoke(task) | |
Right(IO(() => task.join())) | |
}) | |
} | |
object IO { | |
import java.util.concurrent._ | |
private[IO] val MainPool: ForkJoinPool = new ForkJoinPool() | |
def sync[A](effect: => A): IO[Exception, A] = | |
IO(() => { | |
try Right(effect) | |
catch { case e : Exception => Left(e) } | |
}) | |
def point[E, A](a: A): IO[E, A] = IO(() => Right(a)) | |
def fail[E, A](e: E): IO[E, A] = IO(() => Left(e)) | |
def absolve[E, A](io: IO[Void, Either[E, A]]): IO[E, A] = | |
IO(() => { | |
io.unsafePerformIO() match { | |
case Left(void) => void.absurd[Either[E, A]] | |
case Right(e) => e | |
} | |
}) | |
implicit def MonadIO[E]: Monad[IO[E, ?]] = { | |
new Monad[IO[E, ?]] { | |
def bind[A, B](fa: IO[E, A])(afb: A => IO[E, B]): IO[E, B] = fa.flatMap(afb) | |
def fmap[A, B](ab: A => B): IO[E, A] => IO[E, B] = (fa: IO[E, A]) => fa.map(ab) | |
def point[A](a: A): IO[E, A] = IO.point(a) | |
} | |
} | |
implicit val ConsoleIOIO: ConsoleIO[IO[Exception, ?]] = { | |
new ConsoleIO[IO[Exception, ?]] { | |
def println(line: String): IO[Exception, Unit] = IO.sync(scala.Console.println(line)) | |
def readLine: IO[Exception, String] = IO.sync(scala.io.StdIn.readLine()) | |
} | |
} | |
implicit val LoggingIO: Logging[IO[Exception, ?]] = { | |
new Logging[IO[Exception, ?]] { | |
def log(level: Level)(line: => String): IO[Exception, Unit] = | |
IO.sync { | |
val logger = java.util.logging.Logger.getLogger("IO") | |
logger.log(level match { | |
case Level.Info => java.util.logging.Level.INFO | |
case Level.Debug => java.util.logging.Level.FINE | |
}, line) | |
} | |
} | |
} | |
} | |
class IORef[A] private (ref: java.util.concurrent.atomic.AtomicReference[A]) { | |
def set(a: A): IO[Void, Unit] = IO(() => Right(ref.set(a))) | |
def get: IO[Void, A] = IO(() => Right(ref.get())) | |
def modify(f: A => A): IO[Void, A] = IO(() => { | |
var loop = true | |
var next : A = null.asInstanceOf[A] | |
while (loop) { | |
val old = ref.get() | |
next = f(old) | |
loop = !ref.compareAndSet(old, next) | |
} | |
Right(next) | |
}) | |
} | |
object IORef { | |
def apply[A](a: A): IO[Void, IORef[A]] = | |
IO(() => Right(new IORef[A](new java.util.concurrent.atomic.AtomicReference(a)))) | |
} | |
trait ConsoleIO[F[_]] { | |
def println(line: String): F[Unit] | |
def readLine: F[String] | |
} | |
object ConsoleIO { | |
def apply[F[_]](implicit F: ConsoleIO[F]): ConsoleIO[F] = F | |
} | |
sealed trait Level | |
object Level { | |
case object Info extends Level | |
case object Debug extends Level | |
} | |
trait Logging[F[_]] { | |
def log(level: Level)(line: => String): F[Unit] | |
} | |
object Logging { | |
def apply[F[_]](implicit F: Logging[F]): Logging[F] = F | |
} | |
def main[F[_]: Monad: ConsoleIO]: F[Unit] = | |
for { | |
_ <- ConsoleIO[F].println("Hello. What is your name?") | |
n <- ConsoleIO[F].readLine | |
_ <- ConsoleIO[F].println("Hello, " + n + ", welcome to the game!") | |
world = GameWorld.Initial.copy(player = GameWorld.Initial.player.copy(name = n)) | |
_ <- ConsoleIO[F].println("Enter some command to begin!") | |
_ <- gameLoop(world) | |
} yield () | |
val mainIO: IO[Exception, Unit] = { | |
type IOE[A] = IO[Exception, A] | |
main[IOE] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment