In my last post, I introduced Mzinga, my attempt at an open-source AI for the board game Hive. In this post, I want to go through how the Mzinga AI works and what I’m doing to make it better.
Like many chess AIs, the algorithm I used for the Mzinga AI is called Minimax. At a high level, the decision making goes something like this:
- Determine how many moves ahead I want to look (let’s say 2).
- List out all of the valid moves that I, the current player could play.
- Try a move.
- After I’ve tried a move, calculate the board’s “score” with an evaluation function. Higher (positive) numbers mean I’m doing better than my opponent, and positive infinity means that I’ve won the game. Lower (negative) scores mean my opponent is doing better than me, and negative infinity means my opponent has won the game. (We’ll come back to this in Part III).
- If the game isn’t over, list out all of the moves that my opponent could play at this point.
- Try a move.
- Again, calculate the board’s “score” with the evaluation function.
- Repeat steps 6-7 for each of my opponent’s possible moves (from that 2nd turn)
- Repeats steps 3-8 for each of my original possible moves (from that 1st turn)
The algorithm is called Minimax, because for each player’s turn, the AI assumes that the player will take the move that gets them the best score. I’m called the “maximizing” player because for my turns, I’m going to take the move that maximizes the score. My opponent is called the “minimizing” player, because I assume they’re going to take the move that minimizes the score.
By looking ahead, I can modify the score of an earlier move – ie. a move on my first turn might look great for me, but it may turn out that it opens up a really great move for my opponent. Since I assume they’re always going to take the best move, I replace the score of my move to equal the score of the move they’re most likely going to take. That way I don’t take the move that will screw me later.
After all of the calculations are done, I look at the original set of moves available to me, and I pick the one with the best score and actually play it.
Now, a key part of this working is the part where I “calculate” a board’s score. I’ll get into that next post, because there’s a few more things to note. First, doing that calculation, whatever it is, usually takes some time. So the more moves you look at (whether because a player has lots of moves to choose from, or because you’re searching more moves ahead) the longer it takes to come to a decision. So a lot of science goes into reducing how long it takes to calculate a board’s score, and how to avoid looking at some moves all together.
To reduce how long it takes to calculate a board’s score, I use a Transposition Table. Basically, since I’m playing a game where you take turns moving one piece at a time, there’s a good chance that I’m going to end up in the same layout of pieces, or position, at some point in the future. So, as I’m going along calculating board scores, I save them off. That way, when I see a position that I’ve seen before, I can just reuse value I’ve saved. As the game progresses, that table gets bigger and bigger, and I have more saved off values to reuse.
To reduce how many moves I look at, I use Alpha-Beta Pruning. Essentially, it means that if I’m looking through a player’s moves, there are times when I know that there’s no point in looking anymore. If I do move A and while looking through my opponent’s possible responses I see a move that wins the game for them, I don’t need to look at any more moves that would come after playing move A. I assume that playing move A will lose me the game.
The final thing I do is something called an iterative search. The deeper the depth of your search, the exponentially longer it takes. The game wouldn’t be very fun to play if the AI takes hours to decide what to do. Conversely, if the depth is too low, then the AI will be considerably weaker, since it doesn’t “think ahead”. So instead we do an iterative search.
It turns out that Alpha-Beta Pruning is most effective when you look at the “best” moves for each turn first. Only, if we already knew the best moves, we wouldn’t need to search! To help Alpha-Beta pruning out, what I do is:
- Search to a depth of one, ie. just looking at my move and not my opponent’s response.
- Sort my moves from best to worst.
- Search again but now to a depth of two, looking at both my moves and my opponent’s responses.
- Sort my moves again from best to worst.
- Search again but now to a depth of three, looking at both my moves, my opponent’s responses, and my next move.
- And so on, and so on…
Instead of having a set depth to search, I instead set a time limit on my search, say five seconds. I start searching deeper and deeper, each time resorting the moves better and better, until I run out of time. Combined with the fact that I’m saving off board scores as I go, each search iteration gets faster and faster as I see the same board positions over and over.
Now, a lot of code is spent making these operations go as fast as possible, but at the end of the day, the everything hinges on how good your AI is at evaluating a board’s position. The faster and more accurately you can rate who’s “ahead”, the stronger your AI will be.
Stay tuned for Part III, where I go more into detail about how the AI evaluates board positions and how I’m trying to improve it.
Try out Mzinga today!
Update (15-JUL-2016): Creating an AI to play Hive with Mzinga, Part III is up.