State of Mzinga, March 2021

Mzinga is my collection of open-source software to play the board game Hive, and I last posted about it back in November 2018.

A lot has happened since then, so this feels almost like a re-launch than anything. Essentially, Mzinga development stalled at the end of 2019. I made a variety of internal performance improvements, but the biggest changes were polishing the UI of the Viewer, including adding the ability to save and review games later, in a new review mode.

Nothing happened for Mzinga in 2020, though I did spend some time getting acquainted with .Net Core and Avalonia, an exciting cross-platform UI framework similar to WPF.

I did some smaller projects with this “new” way to write C#, and when Avalonia released their big 0.10.0 update in December 2020, I decided it was time to try porting one of my bigger apps to this new platform.

So in January of this year I ported Mzinga to .NET 5, and rewrote the Viewer in Avalonia instead of WPF. The end result has been more than worth the effort.

I got to keep 99% of my existing code, and with a just couple weekends of work, starting with Mzinga v0.10.0, the entire project is cross-platform, with releases on Windows, MacOS, and Linux.

Here’s the announcement on BoardGameGeek: Mzinga, open-source Hive, now cross-platform (Windows, MacOS, Linux)

Having the code cross-platform really opens up the opportunity for getting other developers working on their own AIs. By having the Viewer available on Mac and Linux, more developers are willing to invest in writing their own UHP engines and Hive AIs, which was the whole point of Mzinga in the first place.

I’m in conversations with several such developers- one even found a bug in Mzinga that’s been there for years! They all appreciate the level of documentation and tools I’ve provided to help give them a place to get started.

To that end, I’ve also taken some stabs at building some more engines myself. I started a UHP Sample Code repo, with simple sample engines to get people started. They only implement the base game (no expansions), have no AI, and are not optimized, but they meet the minimum requirements for an engine, and therefore are yet another starting point for developers. I’ve got versions in both C++ and C#, and I’ll probably also create versions in Python and JavaScript too.

For Mzinga itself, it’s a big, complicated codebase, and I don’t see any “easy wins” for making it faster or the AI stronger.

So on top of everything else, I’ve also started MzingaCpp, which is a brand new engine written completely in C++. Here I’m taking all that I’ve learned and focusing on making the fastest possible engine, with hopefully the strongest AI. Right now it’s got the base game and expansions up and running, and while it doesn’t have any AI yet, its move generator is already 1.5x -2x the speed of Mzinga, which is a great start.

That’s all for now! I’ve you’re interested in playing a game of Hive on your computer, give Mzinga a try. If you’re interested in developing your own Hive AI, drop me a line!

/jon

 

State of Mzinga, November 2018

Mzinga is my open-source AI for the board game, Hive, and I last posted about it back in February 2018.

So what’s changed in the last nine months?

From an end user standpoint, the high-level highlights are:

  • The official expansion pieces (Mosquito, Ladybug, Pillbug) are now fully supported
  • Lots of Viewer updates, including:
    • Graphical bug pieces, rather than just the text names
    • Sound effects
    • Progress bars while the AI is thinking
    • Improved layout to maximize the playing area
    • Ability to “lift” pieces that are stacked to see underneath them
    • “BoardSpace” notation (the defacto way to record Hive games)
  • The Viewer now includes an “Internal” implementation of the Mzinga Engine, in case no engine executable is available
  • Engines can now expose their options to be set by the Viewer
  • There is now a “portable” release, ie. a simple zip file for users who can’t/won’t install
  • The “updater” now only checks for updates but doesn’t offer to download and install them (security issue)

From an engine / AI standpoint, the highlights are:

  • Switched to a Principal Variation Search, ie., assume that the best move from a previous, shallow search, is the best place to when starting a deeper search
  • Pre-sort moves to try moves with the most potential to upset the game first (ie moves that affect how surrounded the queens are)
  • Switched to tapered evaluation, ie. how you should play near the start of the game can be different than how you should play near the end of the game, so evaluate the board with different metric weights as the game progresses
  • Different metrics for different game types: one general set of metric weights for all game types (all the combinations of with/without expansions pieces) just isn’t effective, so the AI now has different specialized metric weights for each game type
  • Better usage of multi-threaded searches
  • Various perf improvements

I think the biggest upgrades for users are the inclusion of the expansion pieces, the much friendlier Viewer, and the fact that it’s now portable. Technically all you need to play Hive is the single Mzinga.Viewer.exe from the portable release. You can throw it on a flash drive and play Hive on any Windows (Vista or higher) computer.

On the research front, the biggest discovery for me was of the TreeStrap self-training algorithm. I won’t go into details here, (I talk about it a little in this BoardGameGeek post), but suffice it to say that it provided a more targeted way to improve Mzinga’s AI. Rather than rely solely on my evolution-based model (which, due to the limitations of my population sizes and my “rough” implementation of genetics, produced an often homogeneous pool of AIs) TreeStrap let me force an individual AI to self-train by playing games against itself and intelligently updating its own metric weights.

It was through using TreeStrap that I created a decent AI for playing games with both the Mosquito and Ladybug expansion pieces. However, it was only in testing that AI against my existing pool of evolved AIs that I learned that it only excelled in that game type. In fact that AI did horribly in other game types, which lead me to believe that it might not be possible to create one set of weights that works in all game types. This propelled me to finally expand Mzinga to support specialized weights for each game type.

But now I have a pretty robust “pipeline” for improving Mzinga’s AI. First, I have eight special AIs, each self-training against for one particular game type. Then I push those AIs into my pool of other AIs and have them all fight each other (no more evolution, just the fighting) to evaluate their strength. Then I take the top AIs for each game type (usually, but not always, the self-trained AIs) and inject their weights into Mzinga’s Engine.

In theory, if I keep repeating that loop then Mzinga’s AI should get stronger and stronger. Also I can keep “snapshot” the AI every time I release a new version and keep it in the AI pool, so I can track that the newer AIs are objectively stronger than the older ones.

As this self-improvement loop is already starting to pan out, I’ve begun spreading the load across multiple computers. I can now have one set of computers that continuously run the self-training algorithm on the special AIs and push the results to OneDrive, while another computer regularly grabs those AIs and tests them in combat.

/jon

 

Creating an AI to play Hive with Mzinga, Part IV

Mzinga is my attempt at an open-source AI for the board game Hive. So far in this series of posts, I’ve given an overview of the project, a dive into Mzinga’s AI, and then a deeper dive into Mzinga’s board evaluation function. All together, I have quite the framework for executinga decent Hive AI, but the question remains: How do I make it better?

How do people get better at games? Study and competition. Trial and error. Strategies that win us games are remembered and repeated while plays that lose us games are remembered so that we don’t repeat them. But in a game with so many things to consider, where there is no “right” move each turn, you can’t just memorize the answer, you have to actually play.

What are the things to consider? The metrics I talked about in the last post, like how many moves are available and what pieces are surrounded or pinned. Mzinga looks at some ~90 different metrics. For each there is a corresponding weight, measuring how much each metric should impact the board’s score.

How an AI plays is largely determined by what their metric weights are. AIs with different weights will make different decisions on which move to play. Creating the best AI then, is an exercise in determining the best metric weights. Since we don’t know what are the best numbers, our best option is to:

  1. Create a bunch of AIs with different metric weights
  2. Have them play a ton of games against one another
  3. Pick the one that proves that it’s the best

To pick the best one, I need a way of rating each AI. So I followed in chess’s footsteps and adopted the Elo rating system. It does a few of things I like:

  1. Each player has only one number to keep track of, so it’s easy to see who’s the “best”
  2. A player’s rating is just an estimate of how strong the player is:
    1. It goes up and down as they win and lose and their rating is recalculated
    2. Points are taken form the loser and given to the winner, so there’s a finite amount of “rating points” to go around
    3. More games mean a more accurate estimate of their strength, so no one game can make or break a player’s rating
  3. When recalculated a player’s rating takes into consideration who was playing:
    1. A good player beating a bad player is not news, so the winner only takes a few points from the loser, ie. player’s can’t easily inflate their ratings by beating awful players
    2. A bad player beating a good player is an upset, so to reflect that the ratings were wrongly estimated, the winner get lots of points from the loser
    3. Two players with similar scores are expected to draw, so if one wins, it’s a medium amount of points transferred to separate their ratings a little more accurately

Now the Elo system isn’t perfect. The biggest being that it only shows the relative strength of the players involved – you need lots and lots of players participating for the ratings to mean anything. There are lots of criticisms and variants when talking about real people playing, but it’s fine for what we need it for.

So now we have a method for finding the best player in a population of AIs. Create a bunch of AIs, have them fight one another to improve the accuracy of their Elo rating, and pick the one with the highest rating. But like I said earlier, Elo ratings only show relative strength in a given population. What if I don’t have a good population? What if 99% of them are so awful that an AI with some basic strategy completely dominates? That’s no guarantee that the winning AI is actually all that good.

How do we improve a population of players so that we can be sure that we’re getting the best of the best?

It turns out we’re surrounded by an excellent model on how to make a population of something better and better: natural selection and evolution.

In the real world, living things need to fight to survive. The creatures that can’t compete die off, while those with the best traits survive long enough to pass on their traits to their offspring. Offspring inherit both a mix of their parents’ traits, but they’re more then the sum of their parts. New DNA comes into the population as new members join or mutations occur.

We can absolutely simulate evolution in our population of AIs. The life-cycle goes something like this:

  1. Create a bunch of different AIs with different weights (traits)
  2. Have them all fight each other to sort out a ranking of who’s the strongest
  3. Kill off the weakest AIs
  4. Have some of the remaining AIs mate and produce new AIs to join the population
  5. Repeat 1-4

Now the first three steps seem pretty straight forward, but AI mating? It’s actually not that hard to implement. For each metric weight, simply give the offspring the average value from each parent. To make sure that AI’s children aren’t all identical, add a little variance, or mutation, by tweaking the result a little bit.

For example, if parent A has a particular weight of 5, and parent B a weight of 3, rather than giving every child the weight of 4, give them each something random between 3.9 and 4.1. Just like in real life, we don’t necessarily know which traits were the most important in making sure that the parent survived, and we don’t get to pick which traits get passed on. So we pass on everything, good and bad, and let life (in this case lots of fighting) determine who’s better overall.

Now we can start a new generation and have them all start fighting again, so we can suss out everyone’s rankings in the presence of these (presumably better) offspring. Add in some new AIs every now and then to prevent too much inbreeding (where one AI’s traits, good and bad, start to dominate and everyone starts looking more and more like clones of one another) and we now have a true population of AIs, all fighting to be the best.

Now, how exactly am I doing all of this?

With the Mzinga Trainer.

It’s a simple, yet highly configurable little tool for creating, maintaining, and evolving a population of AIs. I started with several seeds of simple AI weights that I handcrafted, plus a slew of randomly generated ones. Then I set up different populations on different computers with different parameters, and have them fighting, dying, and mating with each other for over a week.

It has made Mzinga into one of my more interesting projects, as I’ve made improvements to the tool, I’ve spun up new populations, mixed existing ones, and run life-cycles with different parameters. Some run under really harsh conditions, where so many get killed that the re-population comes from the offspring of very few survivors. When I started noticing that one small population had become so inbred as to appear like a clones of one another, I added in some new blood, so to speak. Then I reduced the harshness to give different AIs, and ultimately different play styles and strategies, a chance to survive and procreate without getting mercilessly killed off.

It’s an ongoing process, and like life itself, something that takes a lot of time to happen.

The Mzinga Trainer tool is included with the install of Mzinga. Now, not only can you try your hand against the Mzinga AI I’ve created, but you can even try to make a better AI of your own. So come on and try out Mzinga today!

/jon

Creating an AI to play Hive with Mzinga, Part III

In the first post in this series, I introduced Mzinga, my attempt at an open-source AI for the board game Hive. Then in my last post, I gave a high-level overview of how the Mzinga AI works. Now I’d like to discuss Mzinga’s board evaluation function, which is at the core of the Mzinga AI.

As I said in the last post, the purpose of the board evaluation function is to determine, based on the current state of the board, which player is “ahead” in the game. The function returns a number from negative infinity to positive infinity, where negative infinity means the current player has lost, and positive infinity means the current player has won.

The first thing that needs to happen, is I need to collect some information about the state of the board. So I go through the board and calculate a variety of metrics – that is to say – I count a bunch of situations. For example, for each piece on the board, I count how many moves it can make, how many neighbors it has touching it, and flag  whether or not it’s “pinned” (can’t move because other pieces won’t allow it).

I also add those numbers up for each player – how many pieces does the player have in play vs. still in their hand, how many pieces do they have that are pinned, and how many total moves they have available.

I do all of this for both players, and it takes a while, relatively speaking. In chess you have the benefit of a set board size, and despite the enormous number of positions that the board could be in, it’s still in a square grid, which can benefit from some fancy programming (see Bitboards) to make it computer friendly and faster.

Hive, on the other hand, is played on a potentially infinite grid of hexes. A typical chess board might determine there are 25 valid moves for a player – in Hive I’ve seen upwards of 150.

Anyway, once I have calculated a board’s metrics, the next step is to determine what they mean. So I know Black has more pieces pinned than White, but White’s Queen Bee is starting to get surrounded, who is ahead? Black still has more pieces to play, but White has a Beetle working its way toward a key spot, who is ahead? How do I evaluate all of the different situations against each other?

The answer is to use a weighted score. Basically, I take each of the metrics and multiply the number by its matching weight. The weight can be any number, positive or negative, but every metric has it’s own matching weight, with bigger weights meaning that the matching metric has more impact on the final score.

So for example, if I think that having more available moves each turn is better for a player, then for the metric Player.ValidMoveCount, I might give a high positive value to Player.ValidMoveWeight. Since the object of the game is to surround the opponent’s Queen Bee, for the Opponent.QueenBee.NeighborCount metric I might also set the corresponding Opponent.QueenBee.NeighborWeight very high. And since I don’t want to leave my Queen Bee unprotected, for my own Player.QueenBee.NeighborCount metric, I would want to give a negative value to Player.QueenBee.NeighborWeight

My board evaluation function simply adds all of these products (metric x metric weight) together to get the board’s final score. With this kind of generic function, the question now becomes: What are the weights that make up the best AI player? The more accurately the board score actually describes how far ahead or behind a player is, the better the AI has a chance of making moves that keep it ahead until it wins.

So, what are the best weights? I don’t know. No one does. Even in a game like chess, with centuries of study, where players have a much better grasp of what pieces are the most important, and what positions are better than others, there’s still no perfect, definitive answer. Great sophisticated starting points, sure, not but not perfect.

Now for a young game like Hive, the starting point may be as simple as the “surround the enemy queen but also keep your queen from getting surrounded” with some simple guesses for those weights, and ignoring everything else on the board. And in fact, that’s where the Mzinga AI is at today. Smart enough to actually play the game toward winning, and much better than just randomly picking moves.

So how do we make it better? How do we get better weights?

Here is where the “real” fun begins, and that’s a topic for the next post. So stay tuned for Part IV, where I go more into detail about how I’m trying to improve the metric weights.

Try out Mzinga today!

/jon

Creating an AI to play Hive with Mzinga, Part II

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:

  1. Determine how many moves ahead I want to look (let’s say 2).
  2. List out all of the valid moves that I, the current player could play.
  3. Try a move.
  4. 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).
  5. If the game isn’t over, list out all of the moves that my opponent could play at this point.
  6. Try a move.
  7. Again, calculate the board’s “score” with the evaluation function.
  8. Repeat steps 6-7 for each of my opponent’s possible moves (from that 2nd turn)
  9. 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:

  1. Search to a depth of one, ie. just looking at my move and not my opponent’s response.
  2. Sort my moves from best to worst.
  3. Search again but now to a depth of two, looking at both my moves and my opponent’s responses.
  4. Sort my moves again from best to worst.
  5. Search again but now to a depth of three, looking at both my moves, my opponent’s responses, and my next move.
  6. 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!

/jon

Update (15-JUL-2016): Creating an AI to play Hive with Mzinga, Part III is up.