Connect Four in Elixir (Part 1)
After watching the Erlang Solutions webinar on game logic in Elixir with Torben Hoffmann and looking through the Acquirex code, I thought I would try something similar. Let’s take a look at the Connect Four game, which involves dropping colored pieces into the top of a 7-column, 6-row vertically suspended grid, and see how it might be done in Elixir.
I worked on the Connect Four logic last year in Ruby and then started on it in JavaScript when I was thinking of attending Hacker School (now Recurse Center) and needed some code for the interview.
My Elixir knowledge is limited at this point. I’ve read books and documentation, typed in lots of examples, and solved a few exercises on my own, but I don’t immediately know what language constructs to use to solve an arbitrary problem. Going into this I think I should default to creating a process for most things, and keep an eye out for places that pattern matching and recursion might be used to solve a problem.
First Version
My first attempt involved generating a project with a supervision tree and then adding in some modules like “Game” and “Player” and “Board” and “Space”. I just made them Agents as I knew they would need to hold some state, and I had no reason to pick anything else.
Then I got stuck trying to figure out how to talk to them. When the children get started, how do I find the process IDs? Am I supposed to store those? Can I look them up somehow?
I asked on Slack and was pointed to gproc
which is a generic process registry. Interesting (and it is used in the Acquirex code) but it seems like overkill here. I learned about registered names for processes.
Moving on to sending messages to those processes… well, they’re Agents. They hold state. They don’t listen for messages. Which told me that they ought to be GenServers instead!
I switched most of them over. So now I can start them all up and they look very pretty in the Observer… but they don’t do anything. How do you begin the game? Should it prompt for the player name? Ask for a move? If so, which process does that?
I went back to Acquirex and looked around, and figured out that you need to Acquirex.Player_Supervisor.new_player(:wendy)
and then Acquirex.Game.begin
… then what? Someone pointed out the test that serves as a usage example.
So… the “game” here is simply the game state and accepting messages to modify the state. It isn’t combined with the client code that sends those messages into the game, after prompting the human user however it’s going to do that. Currently in the Acquirex code, you can use IEx to call the functions that cause the messages to be sent.
Second Version
Armed with a bit more knowledge, let’s start over by generating a project with a supervision tree:
And put it under version control:
Now what? I suppose in a perfect world I would write some tests, but at the moment I have no idea what I would be testing. So let’s write some code instead and see what errors we get!
Here’s the generated ConnectFour
module in lib/connect_four.ex
:
Game
It looks like we should define some children. How about a Game?
If you try to start this up right now, it will complain:
You can see that it expects the ConnectFour.Game
module to exist, and to have a start_link/0
function. If we had specified any arguments instead of the empty list []
then it would be looking for start_link/1 or start_link/2, etc., depending on how many arguments there were.
So now we need to define the module and function it’s looking for. Convention says that a module named ConnectFour.Game
will go in a game.ex
file in the lib/connect_four
directory.
What should it be? The Game will probably need to keep track of some sort of state, which means Agent
is a possibility, but it will definitely need to receive messages like “Red player drops a game piece in column 3” – because of the messages, let’s go with GenServer
.
In lib/connect_four/game.ex
:
GenServer is a module that abstracts the loop that holds the state as well as the receive loop that listens for messages. The parameters for the start_link
function are:
- the name of the module that will contain the callbacks (this one –
__MODULE__
is a macro that resolves at compile time to the name of the current module), - an empty Map for the initial state, and
- a list of configuration. In this case we’re registering the process with a name so we can find it later.
Now you should be able to start this up…
… and look at it in the observer:
Note that the registered name is displayed in the Observer. This comes from name: @registered_name
(substituted as name: Elixir.ConnectFourGame
at compile time). ConnectFourGame
is an atom, and uppercase atoms automatically get an Elixir
prefix.
CTRL-C twice to get out of IEx, and commit your changes.
Board and Space
The next thing I’d like to do is print the board grid to make it easier to see the players’ moves. Well, that means we need a Board module, and probably some Spaces!
But first, how is this going to work? Let’s say you’ve started up the project in IEx. Maybe you’ll type ConnectFour.Game.print_board
and expect to see the 7-by-6 grid. We’ll go with that for now.
Rather than representing the spaces as an array (or list), each space will be a process. Maybe in the future we’ll want to implement “Infinite Connect Four” which is not limited to six rows and seven columns. In that case, an array might not fit in memory. So the Board will need to keep track of the Spaces – that means it needs to be a Supervisor.
Create the files, again following the convention that the modules will be named ConnectFour.Board
and ConnectFour.space
and live in the connect_four
directory as board.ex
and space.ex
.
Now comes a part that I probably would have gotten stuck on without Torben’s example.
Here is the Acquirex.Space.Supervisor
(equivalent to our ConnectFour.Board
): https://github.com/lehoff/acquirex/blob/master/lib/space_sup.ex
And here is the extended_all
function that returns all of the row/column combinations: https://github.com/lehoff/acquirex/blob/master/lib/tiles.ex#L19
Curious about that question mark in extended_all
? It returns the code point’s value for the character that follows. See http://stackoverflow.com/questions/26995608/what-does-do-in-elixir and http://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#utf-8-and-unicode.
The backtick `
is not anything special here– it’s simply the character that precedes a
in the numerical list of character codes. In the Acquirex source code, the board was extended by one space on each side of the square, so columns a through i became columns ` through j.
The for ... <- ... do ... end
syntax is a list comprehension. You may have used for loops in an imperative language, and in its simplest form, this is similar, (but it can do much more.)
Let’s start with a simple board that looks a lot like the game:
In lib/connect_four/board.ex
:
The parameters for the start_link
function are:
- the name of the module that will contain the callbacks (this one –
__MODULE__
is a macro that resolves at compile time to the name of the current module), - an empty List for the parameters, (note that a Supervisor does not hold state,) and
- a List of configuration. Again we’re registering the process with a name so we can find it later.
Let’s add the Board to the list of workers in the top-level ConnectFour module
And try to start it up:
Unlike with the GenServer, a Supervisor module with only a start_link
function DOESN’T work. It expects to find an init/1
function that describes what needs to be supervised.
That’s because we’re using Supervisor.start_link/3
which says “To start the supervisor, the init/1 callback will be invoked in the given module.”
Here’s the full Board implementation, based on Torben’s Acquirex code.
In lib/connect_four/board.ex
:
And try this again to see what errors we get.
See what’s happening? The overall Application ConnectFour
couldn’t start because Board
couldn’t start, and Board
couldn’t start because there is no ConnectFour.Space
module available with a start_link/1
function that expects a two-element tuple.
Let’s add the ConnectFour.Space
module that is mentioned above, so that Board
can create its workers and supervise them.
In lib/connect_four/space.ex
:
This will register a process for each Space as R1C1, R3C5, etc., up to R6C7. The name itself is arbitrary, but if you don’t name them something you can re-construct later, you’ll have a hard time finding them again to get and/or update the state. Also, each space starts out with a state of Empty
.
Let’s explore this a bit in IEx:
This is the result of the list comprehension that produces all the combinations of row and column. Each tuple is then passed in the call to ConnectFour.Space.start_link, and the row and column elements are used to construct the registered name for the Agent.
Take another look at the Applications tab in the Observer:
Now you can see the board and its list of children. Click on one of the nodes such as R3C5 and look at the State tab:
Here you can see that the state of this node is Elixir.Empty
.
Go ahead and commit your changes.
Print Board Grid
Now that all the spaces are started under known registered names, we can find them again when we need them. Let’s print out the board grid. We said earlier we wanted to call ConnectFour.Game.print_board, so let’s add that function to the Game module:
We’re delegating the printing to the Board itself.
In lib/connect_four/board.ex
:
# 1
: I’m printing the rows in reverse, because when I worked on the logic for this last year I discovered that it’s easier to think of the bottom row as row #1. This will be clearer when we look at what happens during a player’s turn as they choose a column and drop a game piece into it.
# 2
: For each row, we’ll print the columns left to right and then a linebreak.
# 3
: For each space, we look up the agent by its registered name, get the state, and convert it to either “.”, “R”, “B” or ? for display.
# 4
: Recall that the pipe operator |>
sends the result of each line into the next as the first function parameter. The first two lines of print_space
could be written as: Process.whereis( agent_name(row,col) )
(and in fact they originally were!)
And let’s see this in action:
The dots indicate empty spaces. If there were red or black pieces they would be represented by R or B. (And if there is anything else in a space, a question mark will be displayed.)
Conclusion
This concludes Part 1 of Connect Four in Elixir. We’ve generated a project with a supervision tree and filled in the Game, Board and Space modules. Next we’ll see how to handle the players’ moves and update the board.
The code for this example is available at https://github.com/wsmoak/connect_four/tree/20151022 and is Apache licensed.
Copyright 2015 Wendy Smoak - This post first appeared on http://wsmoak.github.io and is CC BY-NC licensed.