First of all thanks to organizers for intersesting contest. I’m glad that this time they did not find a reason to remove me from the leaderboard (yet). 
About strategy
Reverse engineering
Detecting enemies in the fog is a must-have feature.
From all possible positions, I place the opponent’s units where they have optimum advantage, disadvantaging myself (based on evaluation function). After that, we get a usual minimax game with full information.
Minimax
I used a variant of minimax - negamax with alpha-beta pruning and iterative deepening. As the branching factor of this game is around 100 on each ply, I’m sorting all possible moves by the evaluation function, and only keep the best 12 moves, discarding the rest.
This way I usually achieved search depths between 4 and 8 ply.
Evaluate function
I calculated the shortest distances from my units to each cell and the same for the opponent’s units. The player whose unit in closest proximity owns the cell (Voronoi?).
int res = 100*score;
for(int y=0; y<game.Y; ++y)
for(int x=0; x<game.X; ++x)
{
int diff = d2[y][x] - d1[y][x];
int val = 20*max(-3,min(3, diff));
if(0 < diff)
val += 50 + (3-game.maze[y][x])*20;
else if(diff < 0)
val -= 50 + (3-game.maze[y][x])*20;
res += val;
}
Where d1 and d2 are shortest distances from first and second player.
In addition I added some bonus for possible move directions:
foreach(x,y in neighbours of unit)
{
if(game.is_valid_move(unit, x, y))
{
res += 20*game.maze[y][x];
}
}
Testing
Most of times I tested bot locally against previous versions. Thanks Magus and Kevin for tester and referee.
What did not work
- Rear brake on my bicycle
- Placing enemies after my first move based on full depth search (if possible places <= 3).
- Add bonus to score if units position is undetermined for opponent.
- Fine tuning evaluation function.
- Hardcoded solution.