Before you think that my blog subject is some kind of boop boop lazors noise from a space movie, I want to point out that it is my own cleverness in writing “three-in-a-row.” Yes, I am fine. No, I am not sorry. I am not going to blink twice, I am not in danger.
Today, I want to talk about three-in-a-row. I have selfish reasons to avoid calling it by its proper name. If you have ever gone past the “let’s send John a resume” phase of a professional relationship with me, you would be on the receiving end of an obnoxious IYKYK. You can Google that if it confuses you.
Why are we talking about three-in-a-row, aka tic to the tac to the toe?
Because when other people ask me, “Hey John, how do you learn a new programming language?” This is the first program I write.
The first thing I do is build a three-in-a-row game.
I do not have an unusual affection for three-in-a-row. Using the popular minimax algorithm correctly would make an unbeatable puzzle that fails to define a game. I love implementing three-in-a-row as the first thing I build in a language because it has a few abstractions that are good ways to flex your ability to use a new programming language.
Multi-dimensional arrays: You should store the board in an array of [x] and [y] values.
Some business logic: Deciding where an AI player should make a move is a fun little exercise.
Inputs and outputs: Hello world is nice but you are just printing things. Making a full command line (or graphical) game requires more mastery. Are you processing clicks? Are you converting input to numbers? Is your input good? Are the moves valid?
The classes: You can put the board and the player into objects, even with a little inheritance from GenericPlayer to AIPlayer and PersonPlayer.
When I do this, I generally do not use minimax. I like to make a reasonably simple set of functions that iterate over the board, reusing a common function to see if a row is complete or is about to be complete.
I also added a random chance for the AI to make a useless move to ensure the game does not always end in a hideous tie.
So here is a little pseudocode logic for you on the AI’s turn.
if( MakeWinningMove() == FALSE ) {
if( MakeDefensiveMove() == FALSE ) {
MakeRandomMove( );
}
}
This seems pretty straightforward. Try to win the game. If you cannot win the game, try not to lose the game. If neither of those are serious issues, then make some random move and be done with it.
In the function MakeWinningMove(), for example:
We will make a list or set of all the empty pieces.
We will loop over this list and temporarily add an AIPiece.
Does this give us three AIPieces in a row?
Return TRUE!
Otherwise, remove the AIPiece from this location and restart the loop with the next value.
I reuse an iterator function that checks horizontal, diagonal, vertical, and reverse diagonal lines in a 1 to N loop. I think it is important to avoid magic numbers. Suppose someone sends me an implementation of a three-in-a-row game. I look for a variable or constant declaration of BoardSize set to 3 and almost immediately increment it to 4 just to see what happens. Sometimes, it works, and sometimes, it doesn’t.
The logic for MakeDefensiveMove (and MakeRandomMove) is largely the same. Avoiding DRY (Don’t Repeat Yourself) Violations is a good practice.
By the way, this is not the best implementation for three-in-a-row. It is fast and easy to test and does not crush players’ souls by being totally unwinnable.
Quite a bit of this post is about hints for what I look for when conducting software code screens. I was trying not to say that in easily searchable terms for future engineering candidates. I have made this obscure enough that the bonus points for anyone improving their code submissions after finding this article will be worth it.
So now you know how I learn new programming languages. I try to do this frequently enough to stay a lifelong learner and I think this is better “gud brain” exercise for you than tugging at the chunks of wood and metal that you can buy from Amazon at this affiliated link right here. See you next week!