In Part 2 of Connect Four in Elixir we updated the board for the players’ moves. Now let’s see how to prevent errors like the same player moving twice in a row, or attempting to move in a column that is full. After that, we’ll consider how to detect a win.
Column is Full
To check whether the column is full, we only need to look at the space in the ‘last’ (topmost) row and see if it is not empty.
And handle the new possibility in game.ex:
And try it out in IEx:
And commit these changes.
Now we can detect when the column is full, but we’re allowing the same player to move over and over. They need to alternate.
Alternate Player Moves
Recall that we’re updating the state in the Game GenServer with the player who moved last. In a two-player game, it’s sufficient to check that that player who moved last isn’t trying to move again.
(If there were more players, we might want to keep track of who is expected to move next instead. Or we might want to remove the player from the incoming message altogether, and just assume that the move is intended for player who should go next.)
Initially I started trying to add another condition to the existing handle_call function, but then I realized… PATTERN MATCHING! We can match on the state being passed into handle_call, like this:
If the Game’s state (recall it was initialized as an empty map and then updated for each successful move) contains a key of :last_moved and the value is the same as the player attempting to move now, then there is a problem. It’s not their turn; the other player needs to move first.
Note that this needs to go above the original handle_call, otherwise that one will always match. (Try it below and see the compiler warning.)
We also need to handle the new case:
Now in IEx we get:
There is more error handling we could do, such as restricting the players to a defined list of :red and :black, but we’ll commit this change and move on to detecting a win.
Detect a Win
A winning move is one that connects four pieces of the same color in a vertical, horizontal or diagonal line. Starting from the most recently updated space, we need to look at most three spaces in all directions in order to check all the possible winning patterns.
We’ll start by detecting a vertical win in the current column, because that’s the easiest. There can’t be any pieces above the last one, so we only need to look down and see if there are three more of the same color.
Here’s what placing a token looks like now in lib/connect_four/game.ex/:
#1: there is no neighbor below row 1, so if we’ve gotten here without finding four adjacent pieces, it’s not going to happen.
#2: the base case – we’ve found four adjacent pieces and this player wins
#3: the neighbor is not the same, and we haven’t yet found 4, so it’s not a win.
I still don’t like all the conditional logic in this. If you see a better way to do it, add a comment!
Let’s see this work in IEx:
Detecting a win on the row is more complicated because you have to look both left and right along the row. This is left as an exercise for the reader. :)
One final commit:
We’ve added some error handling and seen how to detect the simplest winning pattern, a vertical win in a column.
In the is_full? function I originally had |> &(&1 != Empty) as the last line of pipeline. This is the shortcut function capture syntax for fn x -> x != Empty end. But if you try to use this in a pipeline, you get:
Misreading the error, I tried |> &(&1 != Empty).() but that didn’t make it happy either. Stack Overflow to the Rescue! and the answer is |> (&(&1 != Empty)).().
I opened PR 3916 to see about improving the error message – José replied that this syntax shouldn’t be encouraged, and a private method ought to be used instead. So, don’t do this! :)