Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
package io.scalac
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.scaladsl.Behaviors.Receive
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.Future
import scala.util.Random
// goals:
// 1) player can only submit moves as himself
// 2) player cannot submit invalid moves
// 3) system should honor move order
// 4) we can use actor code from non actor code
// 5) should support request-reply from the client code
object TicTacToe2 extends App {
println("Starting TicTacToe2")
//primitives and utils
sealed trait Pos {
def asInt: Int
}
case object Zero extends Pos {
def asInt: Int = 0
}
case object One extends Pos {
def asInt: Int = 1
}
case object Two extends Pos {
def asInt: Int = 2
}
sealed trait Player
case object XPlayer extends Player
case object CirclePlayer extends Player
type Board = Seq[Seq[Option[Player]]]
def randomMove() = {
Random.nextInt(3) match {
case 0 =>
Zero
case 1 =>
One
case _ =>
Two
}
}
// player proxies
case class YourTurnX(b: Board, replyTo: ActorRef[MoveX])
val playerXProxy = Behaviors.receiveMessage[YourTurnX]{ msg =>
msg.replyTo ! MoveX(randomMove(), randomMove())
Behaviors.same
}
case class YourTurnCircle(b: Board, replyTo: ActorRef[MoveCircle])
val playerCircleProxy = Behaviors.receiveMessage[YourTurnCircle]{ msg =>
msg.replyTo ! MoveCircle(randomMove(), randomMove())
Behaviors.same
}
case class MoveX(x: Pos, y: Pos)
case class MoveCircle(x: Pos, y: Pos)
// translators
def xTranslator(parent: ActorRef[GenericMove]) = Behaviors.receiveMessage[MoveX]{ msg =>
val translated = GenericMove(XPlayer, msg.x, msg.y)
parent ! translated
Behaviors.stopped // usable one time
}
def circleTranslator(parent: ActorRef[GenericMove]) = Behaviors.receiveMessage[MoveCircle]{ msg =>
val translated = GenericMove(CirclePlayer, msg.x, msg.y)
parent ! translated
Behaviors.stopped // usable one time
}
// manager
sealed trait MoveCommand
case class GenericMove(p: Player, x: Pos, y: Pos) extends MoveCommand
sealed trait MoveResult extends MoveCommand {
def currentBoard: Board
def currentlyPlaced: Int = currentBoard.flatten.flatten.size
}
case class ConfirmedMove(currentBoard: Board) extends MoveResult
case class RejectedMove(currentBoard: Board) extends MoveResult
trait Winner
case object XWon extends Winner
case object CircleWon extends Winner
case object Draw extends Winner
def getWinner(currentBoard: Board): Option[Winner] = {
val check1 = for {
c <- Seq(Zero, One, Two)
if currentBoard(Zero.asInt)(c.asInt) == currentBoard(One.asInt)(c.asInt)
if currentBoard(Zero.asInt)(c.asInt) == currentBoard(Two.asInt)(c.asInt)
} yield {
currentBoard(Zero.asInt)(c.asInt)
}
val check2 = for {
c <- Seq(Zero, One, Two)
if currentBoard(c.asInt)(Zero.asInt) == currentBoard(c.asInt)(One.asInt)
if currentBoard(c.asInt)(Zero.asInt) == currentBoard(c.asInt)(Two.asInt)
} yield {
currentBoard(c.asInt)(Zero.asInt)
}
//TODO: include wins going from corner to corner
val piecesPlaced = currentBoard.flatten.flatten.size
val winningPiece = (check1 ++ check2).flatten.headOption
winningPiece match {
case None if piecesPlaced < 9 =>
None // the game is on
case None if piecesPlaced >= 9 =>
Option(Draw)
case Some(XPlayer) =>
Option(XWon)
case Some(CirclePlayer) =>
Option(CircleWon)
}
}
def managerBehaviour(reportResultTo: ActorRef[Winner],
board: ActorRef[UpdateBoard],
x: ActorRef[YourTurnX],
circle: ActorRef[YourTurnCircle],
currentMove: Player): Receive[MoveCommand] = Behaviors.receive[MoveCommand] {
case (ctx, msg) =>
msg match {
case m: MoveResult if getWinner(m.currentBoard).isDefined =>
getWinner(m.currentBoard) match {
case Some(Draw) =>
println("Nobody won")
reportResultTo ! Draw
Behaviors.stopped
case Some(w) =>
println(s"${w} is the winner!")
reportResultTo ! w
Behaviors.stopped
case None => //TODO: this should NOT happen
println("Game is on!")
Behaviors.same
}
case evt: ConfirmedMove if currentMove == XPlayer => // x has successfully placed a piece
askForCircleMove(circle, ctx, evt)
managerBehaviour(reportResultTo, board, x, circle, CirclePlayer)
case evt: ConfirmedMove if currentMove == CirclePlayer => // o has successfully placed a piece
askForXMove(x, ctx, evt)
managerBehaviour(reportResultTo, board, x, circle, XPlayer)
case evt: RejectedMove if currentMove == XPlayer => // x could NOT place a piece
askForXMove(x, ctx, evt)
managerBehaviour(reportResultTo, board, x, circle, XPlayer)
case evt: RejectedMove if currentMove == CirclePlayer => // o could NOT place a piece
askForCircleMove(circle, ctx, evt)
managerBehaviour(reportResultTo, board, x, circle, CirclePlayer)
case GenericMove(p, x, y) =>
board ! UpdateBoard(p, x, y, ctx.self)
Behaviors.same
}
}
private def askForXMove(x: ActorRef[YourTurnX], ctx: ActorContext[MoveCommand], evt: MoveResult) = {
val translator = ctx.spawn(xTranslator(ctx.self), "x_" + System.currentTimeMillis() + "_" + Random.nextInt())
x ! YourTurnX(evt.currentBoard, translator)
}
private def askForCircleMove(circle: ActorRef[YourTurnCircle], ctx: ActorContext[MoveCommand], evt: MoveResult) = {
val translator = ctx.spawn(circleTranslator(ctx.self), "o_" + System.currentTimeMillis() + "_" + Random.nextInt())
circle ! YourTurnCircle(evt.currentBoard, translator)
}
// board
def updateTheBoard(oldBoard: Board, x: Pos, y: Pos, p: Player): Either[Throwable, Board] = {
oldBoard(x.asInt)(y.asInt) match {
case None =>
val updatedRow = oldBoard(x.asInt).patch(y.asInt, Seq(Option(p)), 1)
val updatedBoard = oldBoard.patch(x.asInt, Seq(updatedRow), 1)
Right(updatedBoard)
case Some(takenBy) =>
Left(new Exception(s"Collision with ${takenBy}"))
}
}
def boardAsString(board: Board): String = board.map(_.map{
case Some(XPlayer) => "x"
case Some(CirclePlayer) => "o"
case None => " "
}.mkString("|")).mkString("\n")
def emptyBoard() = Seq.fill(3)(Seq.fill(3)(None))
case class UpdateBoard(p: Player, x: Pos, y: Pos, replyTo: ActorRef[MoveResult])
def boardUpdateBehaviour(board: Board): Behavior[UpdateBoard] = Behaviors.receiveMessage { msg =>
updateTheBoard(board, msg.x, msg.y, msg.p) match {
case Left(_) =>
msg.replyTo ! RejectedMove(board)
boardUpdateBehaviour(board)
case Right(updated) =>
println(s"Updating the Board ${msg}\n${boardAsString(updated)}")
msg.replyTo ! ConfirmedMove(board)
boardUpdateBehaviour(updated)
}
}
/// DEMO starts here
case class StartDemo(replyTo: ActorRef[Winner])
val mainBehaviour = Behaviors.setup[StartDemo] { ctx =>
val initialBoardState = emptyBoard()
val board = ctx.spawn(boardUpdateBehaviour(initialBoardState), "game-board")
val x = ctx.spawn(playerXProxy, "x-proxy")
val circle = ctx.spawn(playerCircleProxy, "circle-proxy")
val whoWillGoFirst = XPlayer
Behaviors.receiveMessage { msg =>
val manager = ctx.spawn(managerBehaviour(msg.replyTo, board, x, circle, whoWillGoFirst), "game-manager")
manager ! RejectedMove(initialBoardState) // this will kick start the process
Behaviors.empty
}
}
trait External {
def startTheGame(): Future[Winner]
}
case object ExternalAkkaTyped extends External {
override def startTheGame(): Future[Winner] = {
import akka.actor.typed.scaladsl.AskPattern._
val system = ActorSystem(mainBehaviour, "hello")
implicit val timeout: Timeout = 3.seconds
implicit val scheduler = system.scheduler
implicit val ec = system.executionContext
system ? (ref => StartDemo(ref)) // this is untyped!
}
}
import scala.concurrent.ExecutionContext.Implicits.global
ExternalAkkaTyped.startTheGame().onComplete { r =>
println(s"Completed the game with ${r}")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.