I ended 25th. Very happy with the result. Thanks to the creators for coming up with a great contest to spend the lockdown.
For some reason this contest failed to motivate me for 75% of the month. Not because there was anything wrong with it, but because I had other things that drew me away. I liked the information aspect of it, the tracking, eliminating paths etc. I had less fun (and less skill) with the strategic logic. Hereās the general flow of my bot:
Bitboards: I thought it was essential. For tracking I kept a 64 bit (* 15) minemap, a 16 bit map with visited cells and a single position for each possible opponent state. A quarter of my code is just a lot of helper functions for these bitboards. Copying, combining, adding, removing etc.
StartCell: My start is fully random, except that I donāt start in closed off sections of the map (you can check this with floodfill). I assume the opponent can start on any cell, meaning I start with 225
(minus islands) possible states for tracking.
OpponentOrders: The most important stage for tracking. For each state, I process all opponent orders and if one of them doesnāt match up to reality, the state is removed. There are two ways in which extra states can be created.
- Silence
- Trigger that happens on a position that has more than 1 neighbouring position from which a mine was laid
I also tracked all damage forms on a damagemap and any cell that got an amount of damage that does not correspond to opponent health lost, has its states removed. Itās also important to make sure you floodfill the cell torpedoed by the opponent to see where he could be.
A major issue in this game is chain-silence. It can time you out easily. I have a limit as to how many states are allowed to exist. If there are too many, i combine the ones with the same position and mines and combine their āvisitedā histories to only have cells that all cells share in their history. I also do this for self-tracking. I silence more than most players and if I didnāt prune, I would time myself out as well .
Mine avoidance:: Since I have a bunch of states with mines laid from various cells, I can combine this information into a mine probability map (where are mines likely to be?) and a mine probability damage map (which cells are those mines likely to damage?). I use a beamsearch to navigate the map, avoiding cells with high probability of mine damage. I also combine this with a floodfill-score (to keep room for myself to move and not have to surface) and a self-tracking score (so as not to give my position away).
Self-Tracking: Similar to opponent tracking but with a key difference: I donāt track mine-triggers. This was mostly to keep the complexity of my code smaller, but also to conserve calculation time.
Fatality!: I love this part of my bot. It simply brute forces all possible actions and checks if there is a guaranteed kill. It will also surface if necessary, silence-dash to the opponent, combine mine + torpedo damage and even damage different parts of the map if the opponent position is not yet determined. It could theoretically damage 18 cells just to do 1 guaranteed damage to the opponent and kill them if it is possible.
Ability Charging: This is the first area of the game where I didnāt really know what to do. In the end I preferred torpedo, then mine, then silence if the amount of cells where my opponent thinks I can be, drops below a certain value. Otherwise sonar.
Overall Action Logic: This is the second area where I tried stuff and did not know what was best. I think I got a reasonable set of heuristics in the end, but it could be much improved.
-
Make an opponent-probability-map. That is for each cell, calculate the likelyhood the opponent is there. This is necessary for other logic.
-
Check if moves are even possible (in case you need to surface). If not and you have more than 1 life remaining, then Surface.
-
If you have moves available (also after surface), do the earlier explained beamsearch to get a movement direction.
-
Check the best targets for a torpedo before and after the move and determine which is the best āshotā. If the score is above a certain threshold, launch the torpedo before or after the move (preferring before the move, on equal score). Score is determined by the target cell with the highest expected opponent damage (using the opponent probability map). Donāt shoot yourselfā¦
-
Check the best targets for a mine-trigger and see if the score is above the mine trigger threshold. I also check if I need to move out of the way (trigger before or after move). Score is determined the same way as torpedo score, but the threshold is a bit lower. This is because torpedoās reveal your position more strongly. I noticed a high torpedo threshold is very important.
-
If silence is available, check if it is worth using, meaning, does the opponent get more cells where it thinks I am (at least 4 more) and am I even below a certain value on this (below 30 self-tracked cells). So basically: Is it necessary to silence and does it even help me at all?
One of the last things I coded was a non zero distance silence. This to possibly avoid a bad position with mines. I donāt think it happens often, but it does happen.
-
Lay mines when if I can, but not when there is too much overlap with other mines. Try to lay mines that cover more available ocean.
-
If sonar is available (almost never). Use it only if the opponent could be in more than 1 sector and pick the sector with the most states.
-
If there are no orders recorded yet (because I canāt move and I canāt surface or I would die), I start blowing up mines.
Thatās most of it I think. Works quite well, but I think in the end I wrote a heuristic bot and the trick to being in the top 10 is probably a more flexible search-based bot. People talked about battle simulations on the chat. That is probably a necessary component. Other than that I could have watched more games and added more heuristics. One example of this that dawned on me while writing this, is that I did not consider laying a mine before moving. Maybe that would sometimes be good.