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
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.
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
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
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
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:
Let’s examine each file.
I guess this is pretty self-explanatory.
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.
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.
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.
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
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,
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.
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
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
Leave message, the game actor decides either to exit the game by shutting down the actor system, or a
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
Leave, but if they did match, it starts waiting for either a
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:
Before going into the game logic, let’s first implement the actors and the messages that will flow between them.
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
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.
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.
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,
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,
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.
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.
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).
If we combine it all, here’s how the final version of our game actor looks like:
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,
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:
Next up is the implementations of those actions, first of which is the
askForGuess method. We might implement is as follows:
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
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.
Alright, so if we use this
readNumericResponse method, we can simplify our
askForGuess a little bit more. Back to the player actor.
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,
stopWithError(t: Throwable) and
askAndThen[T](ask: Future[T])(then: T => Any) (all defined on the actor class, of course).
Fancy bit of code, isn’t it? Here’s how our
askForGuess becomes with these in hand:
Much, much cleaner than before! So how about
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
false, and use the resulting value in our other methods.
Moving on to our utils class:
And back to the player actor:
These look complete, so let’s combine them all and take another look at all files we’ve created so far.
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.
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.
So let’s give this one a go. Navigate to the project directory and simply run
sbt run. Here’s a sample output:
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