Actor-based Number Guessing Game in Akka
Now that the basics of the actor model is out of the way (thanks to the previous article), it’s time to make something with it! We’ll begin by creating an Akka project from scratch, and then move on to implementing a number guessing game based on actors, which will give us an easy start in modeling actor-based systems.
The rule of the game is quite simple; it’ll pick a random integer between 1 and 100, and user will try to guess it. Each time the user provides a guess, the game with reply with a message that tells the player whether or not they matched. All interaction will be through the console, so no unnecessary complication there.
Note: I won’t go into any details about the model nor Akka’s implementation of it, so if you’re ever in need of assistance, feel free to use the previous article as a reference.
See the code at Github: https://github.com/ygunayer/guessing-game
Preparing the Environment
We’ll need to install a few things before we can get started with our project: JDK, Scala, Scala Build Tool (or SBT for short) and Lightbend Activator (it’s not really a dependency, but it’s very convenient to install, and we might use it in future articles). You’ll have to install JDK on your own, and as for Scala and SBT, you can go ahead and install Activator right away because it can install them for you (provided that you have JDK installed).
For reference, here’s a list of what I had installed at the time of this writing:
When it comes to IDEs, the choices are pretty much the same as Java, it’s either Scala IDE (Eclipse-based) or IntelliJ IDEA (Community Edition will do), but they both require some preparation before you can use them.
Scala IDE
The Eclipse-based Scala IDE does not have a built-in way to import SBT-based projects, so you’ll have to install the SBT plugin sbteclipse. As documented in the repository page, you can install it directly on your computer, or add it as a dependency to your project so that it gets installed once you’ve started building the project with sbt
. I recommend the former method, because an IDE plugin is not technically a prerequisite of a project, but rather, a convenience for programmers.
When you’ve got it installed, all you have to do import your project into Scala IDE is to launch up a terminal, go into the project folder, and run
1 | sbt eclipse |
IntelliJ IDEA
In terms of Scala support, the default installation of IDEA is even less capable than Scala IDE, and requires you to install an official plugin. All you have to do to get it is to just go into the plug-in options and install the Scala plugin from there. Since it also comes with the ablity to import SBT projects (and create projects based on Activator templates), you’ll be all set once you’ve got the plugin installed. See Creating and Running Your Scala Application at JetBrains website for more detailed instructions.
One thing to note here is that IDEA likes to keep its own Ivy cache (where SBT downloads the dependencies of all projects and stores them for later use), so it’ll re-download any dependency you might have pre-installed using the SBT’s own command line tool (i.e. sbt run
or sbt build
), or vice-versa. If that bothers you, here’s how you can get IDEA to use the same cache folder as the default one: http://stackoverflow.com/questions/23845357/changing-ivy-cache-location-for-sbt-projects-in-intellij-idea
Creating the Project
While scaffolding tools like Activator are great, I think it’s important to be able to create projects manually, and from scratch, because it helps you understand how things really work.
The outline of a Scala project is pretty much the same as a regular Java project. At the root folder, it has the build file (aka build.sbt
), the source folder (aka src
), and if using a SCM tool, its ignore file (aka .gitignore
). Under the src
folder are the main
and test
folders, each of which contain a hierarchy of folders that mirrors the package structure of the project.
I encourage you to take the time and follow these steps by yourself, but if you’re not interested, feel free to clone the starter project at its initial commit at https://github.com/ygunayer/guessing-game/tree/0bb9fc5dd96160c15ad98a8446973106c22a5056
I’ll be using com.yalingunayer.actors.guess
as the package name, and guessing-game
as the root folder of my project, so here’s what the entire project structure looks like:
1 | (workspace folder) |
Let’s examine each file.
.gitignore
I guess this is pretty self-explanatory.
.gitkeep
Although we don’t have any tests yet, I’ve included this file so we have an idea on where our test classes will end up in in the future.
build.sbt
The SBT build file defines the name of our project, its version, the Scala compile version that it requires, and also the dependencies that are to be installed. In this example, our only dependencies are Akka and both Akka’s and Scala’s test libraries.
1 | name := """guessing-game""" |
Application.scala
This is our main source file, and it contains the obligatory “Hello, world!” thing which we’ll replace once we’ve started with our game.
1 | package com.yalingunayer.guess |
Once you’ve laid out the project structure, all that’s left is to import the project into your IDE. To rephrase:
- For IntelliJ IDEA, just import the folder directly
- For Scala IDE, navigate into the project folder, run
sbt eclipse
, and then import the project as an existing project
Planning
Let’s not jump right into the implementation and instead plan our approach first. As with any problem in programming, we’ll first model its domains, determine the way data flows, and then move on to the actual implementation.
Let’s recall our game flow and try to determine what our domains are:
- Pick a random integer between 1 and 100
- Ask the player for a guess or an exit request
- If the player provides a guess, compare it with the number that is kept, and if they match, go to step 5, if not, go back to step 2
- If the player provides an exit request, stop the game
- Congratulate the player, and ask if they want another round of game, or just finish playing
- If the player wants another round, go to step 1, if not, stop the game
There are countless ways to implement this flow, all depending on various decisions that we can make at various points. One such point is the domain model, and one decision we can make for that is to split up our program into three main domains: one to cover the program flow and maintaining the actor system, one to handle the game logic and another to handle player interaction. In an actor-based world, these domains translate (pretty much directly) into three actors, Application
(or Program
), Game
and Player
, but for simplicity’s sake, we’ll refrain from implementing the application domain as an actor, and just stick with an object class instead.
This decision also affects our choice on how we’ll let the player and the game actors know each other. Since we won’t have a common ancestor (at least on a user actor level) for our actors, we’ll just let our game actor initialize the player actor and become its parent in the process. In a more complicated scenario, especially when networking is present, we can implement our actors in a more detached way, letting them discover each other through a common ancestor, and possibly having more complex state transitions using ready and idle states to prevent any possible dead letters, but that’s a subject matter for perhaps a future article.
The Implementation
Based on our decisions, here’s how our actors will behave: the game actor boots up, generates a number, initializes the player actor whilst also passing its own ActorRef
(i.e. self
) to it, sends the player actor a Ready
message, and starts waiting for a guess or a request to exit.
As soon as the player actor initializes, it starts to wait until a Ready
message is received, after which it turns to the player itself and expects a guess. Based on the player’s reply, it either sends a Guess(n: Int)
message and starts waiting for the next step, or sends a Leave
message and shuts itself down - if the player wants to exit.
Upon receiving the Guess
or Leave
message, the game actor decides either to exit the game by shutting down the actor system, or a Win
or TryAgain
message based on whether the guess matches the number that was kept. If the numbers didn’t match, it goes back to waiting for a Guess
or Leave
, but if they did match, it starts waiting for either a Restart
or Leave
message.
Upon receiving a Win
message, the player actor asks the player if they would like to play another round, or exit the game. If the player chooses to restart, it sends out a Restart
message, and if not, a Leave
message like before. Similarly, if it receives a TryAgain
message, it asks the player for another guess, and like before, sends out either a Guess
or a Leave
, depending on the player’s decision.
So to summarize, here’s a list of all messages that we need to create, and which actor they originate from:
Game Actor
Ready
Win
TryAgain
Player Actor
Guess(n: Int)
Restart
Leave
Before going into the game logic, let’s first implement the actors and the messages that will flow between them.
Laying the Groundwork
So, first up, the game actor. Based on our game flow, there are two variables that are stored in our game actor: the number that is picked for the current round, and an ActorRef
to the player, both of which we can store as scoped variables in our class. We’ll have to declare the number to guess as a var
because it’ll change from round to round, but we can safely declare our player actor as a val
and re-use it between rounds since it’ll persist as long as the game actor does. It’s worth noting here that while our game actor will end up as the parent of our player actor, and therefore will be accessible by its parent
field, I find it better to explicitly pass it as an extra parameter. Again, in a more complicated scenario, we could have passed the references to the number and the player actor by using become()
and unbecome()
to transition into parameterized states, but there’s no need to over-complicate things just yet.
Another thing to note is that it’s a very common practice to create message classes in the relevant object classes for every actor class so that they’re both semantically separated, and easily accessible.
As such, here’s how our game actor looks like without any state transitions or game logic.
Game.scala
1 | /** |
Next, the player actor. Our player actor holds no state (except for the ActorRef
to the game actor which is parameterized), so the only thing that we need to implement aside from the actor’s logic is the messages.
Player.scala
1 | /* |
Game Logic
Now that our actors are set up, it’s time to implement the game logic. Since the behavior of an actor is determined by its receive
method, which is a partial function, we’ll implement different behaviors for every message they need to handle.
We’ll first start with the game actor since it’s easier to implement. If we remember from before, there are three messages it can handle, Guess
, Restart
and Leave
, and here’s how it reacts to them:
When it receives a guess, it compares it with the stored number, and replies with a Win
message if they match, or a TryAgain
message if they don’t. In other words,
1 | case Player.Guess(n: Int) => { |
When it receives a restart request, it generates a new number and replies with a Ready
message so as to inform the player that a new round has begun.
1 | case Player.Restart => { |
When the player leaves, it just shuts down the actor system so that the program can terminate. There are other ways to do this, of course, but simplicity is key.
1 | case Player.Leave => { |
Aside from handling incoming messages, our game actor also needs to inform the player actor that the game has started by sending it a Ready
message. We’ll do this at the preStart
stage (remember the actor lifecycle from the previous article).
1 | override def preStart() = { |
If we combine it all, here’s how the final version of our game actor looks like:
1 | /** |
The behavior of our player actor is much more complicated than that, and has a stateful nature even though it doesn’t keep any state variables. Looking back at our game flow, we see that there are three distinct states that our player actor goes into: waiting for a game to start, waiting for a round result, and a catch-all idle state for situations where we wait for the user’s input. Any unexpected messages in one of these states can result in even more unexpected behavior, so we need to split our behaviors into multiple Receive
implementations that each represent those three states. Namely, initializing
, waitingForRoundResult
and idle
.
Also, let’s separate the actions our actor will take upon receiving certain messages into aptly-named functions: askForGuess
to ask for a guess, askForRestart
to ask whether the player wants to restart the game for another round, and askForRetry
to ask for another guess after they’ve provided an incorrect guess. Based on these decisions, here’s how we can implement our actor’s states:
1 | // the default behavior or state is the `initializing` state |
Next up is the implementations of those actions, first of which is the askForGuess
method. We might implement is as follows:
1 | def askForGuess = { |
But… we really shouldn’t. Not only does this code look bad, it smells bad too! If you remember from the first article, one of the most important aspects of the actor model is concurrency, and we’ve completely obliterated that principle by synchronously calling readLine()
, a heavyweight blocking operation. Let’s wrap that in a Future
and make it non-blocking. Keep in mind that in order to do this, we’ll need an ExecutionContext
, and we can implicity access one by importing scala.concurrent.ExecutionContext.Implicits.global
.
1 | def askForGuess = { |
This does look promising, but it’s still very unreadable, especially so since we’ll be using this ask-and-reply pattern a couple more times. So let’s just write a utility function that reads a line from StdIn
and parses it into an Int
, and responds with an Option[Int]
. We can do this by mapping the result of the initial Future
(which only affects the Success
case) to create a transformed version of it.
Utils.scala
1 | object Utils { |
Alright, so if we use this readNumericResponse
method, we can simplify our askForGuess
a little bit more. Back to the player actor.
1 | def askForGuess = { |
Yeah, a little bit better… kinda… So how about we move the error handler and the stopping logic into methods of their own, and create a generic method that performs an ask
, invokes a method that we pass into it on success, and that error handler on failure? As in, stop
, stopWithError(t: Throwable)
and askAndThen[T](ask: Future[T])(then: T => Any)
(all defined on the actor class, of course).
1 | // perform an `ask` operation, and continue with `then` on success, or `stopWithError` on failure |
Fancy bit of code, isn’t it? Here’s how our askForGuess
becomes with these in hand:
1 | // ask for the player's guess and act based on the outcome |
Much, much cleaner than before! So how about askForRestart
or askForRetry
? We’ll be asking a yes/no question in both cases, so we can implement another utility method to convert the user’s response into true
or false
, and use the resulting value in our other methods.
Moving on to our utils class:
1 | // convert a yes/no response to boolean for easier use |
And back to the player actor:
1 | // ask the player if they'd like to take another try |
These look complete, so let’s combine them all and take another look at all files we’ve created so far.
Game.scala
1 | package com.yalingunayer.guess |
Utils.scala
1 | package com.yalingunayer.guess |
Player.scala
1 | package com.yalingunayer.guess |
Yep, they do look complete. The only thing left to do now is to update our Application
object so that initializes the actor system and the game actor.
Application.scala
1 | package com.yalingunayer.guess |
It doesn’t get any simpler than that. Akka is smart enough to keep the program running until the actor system is shut down so we don’t have to do anything else.
“Gameplay”
So let’s give this one a go. Navigate to the project directory and simply run sbt run
. Here’s a sample output:
1 | Pick a number between 1 and 100 (inclusive) |
Conclusion
I admit that this looks a bit intimidating, especially for a trivial application like a guessing game, but it does give some insight into how a request-and-reply flow is implemented in an actor-based system. We haven’t been able to go into implementing a multitude of actors (as if even this much wasn’t complicated enough), but this is something I intend to cover in a future article where we implement a game with multiple players, probably a card game with AI opponents, so stay tuned!
Finally, in case you’ve missed the link to the codebase, here it is:
See the code at Github: https://github.com/ygunayer/guessing-game