OddThinking

A blog for odd things and odd thoughts.

Virus 2: A Puzzle

Introducing the Puzzle

Virus 2 is a flash game with a disturbing name, but quite simple game-play.

On a grid of randomly-coloured hexagons, you control a contiguous area of like-coloured cells. You can change the colours of every cell under your control, and where you match the colours of neighbouring cells, you absorbed them.

I think it is supposed to be reminiscent of how a virus grows, but I suspect the authors are confusing virus with amoebae.

The aim is to absorb all the cells on the 20×16 playing board in as few moves as possible. (As the game cycles, the target count drops until you fail to meet it.)

Go ahead and play it for a while. When you get back, it will be time to try to truly solve it.

Rules of Engagement

So how do you go about solving it? The obvious solution is a brute-force search. But I am a bit over that right now – there’s no finesse.

So, I decided to aspire to some rather arbitrary rules of engagement. I wanted to maintain the algorithmic interest.

Let me break the tension now by warning you up front. This project was a bit of a fizzer. It was strictly a success. It has pretty pictures. However, the solution isn’t as interesting as I hoped.

The Rules

  • The solver algorithm should be based on examination of the current board state. It should not do any exhaustive searching. (I didn’t want to be too limiting, so I permitted myself one or two moves worth of look-ahead if I needed it.)
  • Each move on a given grid should be assigned a metric value, and the highest value should be chosen as the next move.
  • The metric algorithm should consist of the weighted averages of a number of sub-metrics. This was an design choice based on elegance rather than appropriateness, and I reserved the right to revisit it, as the solution became more clear.
  • Sub-metrics that I came up with included: number of neighbour cells of that colour, the least recently used colour, some function of the percentage of cells of that colour that are neighbours, the number of neighbours to the cells of that colour that are neighbours to the virus (effectively, looking ahead 1 move.) As I discovered which sub-metrics worked, I expected to come up with more.
  • While I wasn’t allowed to search during the puzzle-solving phase, I was allowed to search during the software-development phase. My interesting idea was to search through the space of weightings of the sub-metrics, finding the best overall metric that combined the advice of the sub-metrics. I wanted Simulated Annealing and everything! Once the best weightings were found (which could take a lot of processing time,) I would have the parameters to plug into the algorithm (which would then execute quickly). That algorithm might not find the optimal solution to a particular puzzle, but it would, on average, quickly find good (perhaps great?) solutions.

Again, these are rather arbitrary restrictions designed to make the software solution fun to produce, rather than strictly finding the optimal solution to a given problem.

Chatting to Andrew D., who introduced me to the puzzle, I learnt that he solves the puzzle (by hand) in a number of phases. For example, in the early phases, he aims to grow the virus towards the center. In the middle phases, he concentrates on growing the virus as quickly as possible. My arbitrary restrictions largely preclude me from such a solution.

Setting a Goal

What’s my goal?

Well, my goal isn’t to find the optimal solution for any puzzle. If I wanted to do that, I would do a search.

My goal isn’t to find the best ever solution to any puzzle – e.g. “Look, I solved this is 20 moves!” There’s too much dependency on the random configuration of the puzzle for this to have meaning. A puzzle could be solved in zero moves if you are lucky enough to have every cell the same colour.

So, I guess my goal is to play better than a human player. It seems an expert human player solves the puzzle generally in mid-to-high-twenties. If I can average, say, 25 moves, I shall declare it a success.

Implementation of the Basic Game Rules

I created some familiar data-structures. Grid objects represents the grid; it is a container of HexCells. A HexCell object represents on cell in the grid. It knows its colour, co-ordinates and neighbours. Given a GameWindow object, a HexCell can render itself graphically.

This was largely based on my experience with Kakuro, Nonograms, etc.

After a lot of high-school geometry, here’s the result – it is an 8×8 test sample.

Virus 2 - Basic Layout of Small Example Puzzle

Pretty, isn’t it!

You will notice the “virus” in the bottom left. Unlike the original, I always colour the virus black. It’s real colour is irrelevant to the puzzle, and I thought making it distinct would help.

From Hex Cells to Clusters

The next step was to abstract away from individual hex cells. A cluster is any group of adjacent cells of the same colour. They work together. So rather than dealing with cells, the final algorithm manipulates the clusters.

One key advantage here is that the number of nodes that the algorithm must deal with is reduced from (16×20=) 320 to an average of 200. Another is that it is easier to work out the true value of matching a particular colour.

Initially, each cell creates its own cluster with a unique id and a colour. The cluster is now delegated the responsibility for storing the colour on behalf of the cell.

Then the cell informs the clusters of its neighbours, and the clusters start to merge. If a cluster finds its neighbour is the same colour as it, they merge together, giving control to the lower numbered cluster.

Each cluster still exists after a merge, but when asked for its data (e.g. by its corresponding cell), it refers to its associated cluster recursively. (A cluster keeps a cache of its best associated cluster so it doesn’t have to recurse through the entire merge history each time it is accessed.)

Virus 2 - Debug Layout of Small Example Puzzle

The picture isn’t quite so pretty now. The numbers represent the co-ordinates of the lowest numbered cluster associated with that cell. You can see neighbouring cells with the same colour all share the same cluster.

The virus cluster is represented by “****”.

Also on this diagram, there are thin white circles around the cells in “susceptible” clusters. Susceptible clusters are the clusters that are direct neighbours of the virus cluster. They are the ones that may be absorbed in the next move.

First Sub-Metric: Random

Before I could write the bit metric that combined all the sub-metrics, I needed some sub-metrics to base it on.

The first one was a “control” sub-metric – random choice. I wrote a function that, given a grid, randomly chose one of the colours of a susceptible cluster.

Not only was this a quick test sub-metric, but I hoped to find, during the parameter searching phase, that the random metric was weighted very low by my searching algorithm. That would provide some evidence that the searching algorithm was working.

Using the Random sub-metric alone, I ran a sample of 500 runs. It showed a mean solution time of 41.0 moves (ranging from 29 to 59 moves).

End Of The Line for the Graphics

About this point, all the work I did on the graphics was discarded. It was useful, because it helped me debug the clustering, but now it no longer had much point, and just slowed down the execution.

Before I go on the ignore it, here is one more image – a full-sized puzzle mid-solution.

Virus 2 - Basic Layout of Full-Sized Example Puzzle

Second Sub-Metric: Least Recent

Next, I implemented an algorithm that I thought would be useful. From the choice of susceptible cluster, it selected the least recently chosen colour. It makes intuitive sense that the least recently chosen colour is likely to have accumulated the most nearby neighbours. It was also rather simple to implement.

Using the Least Recent sub-metric alone, I ran a sample of 500 runs. It showed a mean solution time of 37.6 moves (ranging from 25 to 49 moves)

That’s a good improvement already over the Random sub-metric baseline.

Third Sub-Metric: Most Neighbours

The next algorithm was the most obvious one: chose the colour that has the most matching clusters nearby.

I hoped it would supersede the Least Recent used algorithm by using facts rather than heuristics to find the colour with the biggest immediate growth.

Using the Most Neighbours sub-metric alone, I ran a sample of 500 runs. It showed a mean solution time of 33.2 moves (ranging from 25 to 41 moves).

That’s another good improvement, and it is reflected not only in a smaller average, but a smaller range. It is a more consistent result, which is reassuring.

Fourth Sub-Metric: Most Neighbours’ Neighbours

I had given myself permission to use some limited look-ahead. It was time to exercise that permission to implement a single move look-ahead.

The algorithm chose the colour of the neighbouring clusters that themselves had the most neighbouring clusters. That is, what is the best move this turn to maximise the best move next turn?

This sub-metric was still a bit naive. It didn’t work out “If I go Blue this time, I will have 5 Green neighbours, so that’s the best move.” It worked out “If I go Blue this time, I will have 25 neighbours, so there is probably at least one move that is good from there.” The look-ahead heuristic was simplistic.

Using the Most Neighbours’ Neighbours sub-metric alone, I ran a sample of 500 runs. It showed a mean solution time of 27.8 moves (ranging from 21 to 34 moves).

That was a good improvement again. A very good improvement. I wasn’t still expecting big jumps like that at this point – basically, it is already a better player than me (I’ve only player about 10 or 15 games, manually.)

In a way, this was a disappointingly high improvement. Boring, old look-ahead was proving to be very successful. I guess that tells me what the next algorithm should be.

Fifth Sub-Metric: Most Neighbours’ Neighbours’ Neighbours

I went ahead and implemented two levels of lookahead. What colour’s neighbours have the most neighbour’s neighbours?

Again, this sub-metric was naive. It wasn’t working out “If I go Blue then Green I will have 5 neighbours.” It was working out “If I go blue, and then add up all the neighbours of those neighbours, I get a total of 15 neighbours.”

Nonetheless, this sub-metric had me feeling a little bit dirty. Look-ahead is like searching, but wearing a leash so you don’t go too far. Two levels of look-ahead (in a puzzle that only requires 20-30 moves) is just starting to smell a little bit exhaustive.

Using the Most Neighbours’ Neighbours’ Neighbours sub-metric alone, I ran a sample of 500 runs. It showed a mean solution time of 24.9 moves (ranging from 20 to 31 moves).

Note: 24.9 < 25 moves, which was my original target.

Conclusion

What a let-down! I already reached my goal and I didn’t need to implement any parameter searching, simulated-annealing, parameterised sub-metrics or nothing.

I just had to stray towards the dullness of lookahead (which is just a cut-down version of an exhaustive search). If I wanted to improve it any further, I knew what I needed to do. Make the look-ahead less naive and let out the leash to another level of look-ahead.

Boring.

I stopped instead.

I declare this a fizzer. I reached my stated goal, but without enough fun.

One more pretty picture before I go – a histogram showing the results in more detail.

Histogram of Results of 500 Game Sample Runs, by Algorithm


Comments

  1. I think it is supposed to be reminiscent of how a virus grows, but I suspect the authors are confusing virus with amoebae.

    Maybe it’s trying to be reminiscent of how a virus spreads, with each hex being a larger organism which is susceptible to infection from a certain strain of the virus, signified by its color. You get to control how the virus mutates, and your goal is to infect the entire population of larger organisms in as few mutations as possible. (Maybe the name of the game should be “Contagion” rather than “Virus”?)

  2. I’m surprised you didn’t try “control area” i.e. ignoring color completely in favour of connecting to the biggest number of hexes.

  3. Sunny,

    I think I did.

    Firstly, I abstracted up from hex cells to clusters. Two hex cells of the same colour can be treated as one cluster, and reasoned about at that level. If your aim is to absorb the largest number of hex cells to improve your reach into untouched cells, then you should first realise touching two adjacent cells of the same colour doesn’t extent your coverage – what matters is the largest number of clusters.

    Once that abstraction was made, then the Most Neighbours sub-metric looks for the move that covers the biggest number of clusters – which I think maps to the same basic conceptual approach as “biggest number of hexes”.

  4. If you wanted to go further with this, the next thing to try is a minimum distance algorithm. At each step, it is easy to calculate the minimum number of color changes to get from your current holdings to a given hex. So, work out which hex is furthest away from you, and make a move that reduces it. If there are ties, then go to your other measures.

    The thinking here is that in the process of going to the furthest away hex (taking into account cluster topology) you will make changes that include lots of other hexes, and you may not have to worry about any other line. It’s not an analytic solution, but if some of your movers make big changes, the recalculation of minimum distance will sort it out.

    The calculations themselves are O(n^3) I think – minimum distance for a single cluster is O(n), and there are O(n) clusters to calculate, and you need O(n) steps… or possibly less, would that be O(logn)?

    This algorithm would tend to grow the cluster towards the center early on, in the manner of Andrew D.

  5. Andrew,

    I actually did give that idea some thought during the analysis of this puzzle.

    I figured that if I could find the worst possible cluster – the one that took the most colour changes to absorb it – by using a similar technique to the one used to calculate the matrix, D in the typical diff algorithm. I think each of the three stages – calculating minimum distance for all nodes, finding the most distant node, and backtracking the colour steps could be done in O(n), and they are done sequentially, so the whole solution is O(n). </handwaving>

    I was patting myself on the back, prematurely, thinking I could use that technique to solve the puzzle completely! But then I realised that just because I had coloured the hardest possible cell, that didn’t necessarily mean I had solved every single cell.

    I could use it to find an underestimate of the minimum possible solution time, which might be useful as a heuristic for an A*-like solution, but I didn’t want to go that direction.

    Yours is an interesting idea: repeat the process after every move – it won’t necessarily find the best possible solution, but it is still a fruitful area to consider.

Leave a comment

You must be logged in to post a comment.

Web Mentions

  1. OddThinking » Happy Third Anniversary, OddThinking!