I finished #15 in legend. My strategy was to focus on parts of the bot one after the other, not caring until my rank until it would be all done. The upside of this approach was that, in the end, every single “part” of my bot worked extraordinarily well. The downside was that when I had to put them all together, I failed.
Tracking
The most boring part; the bot just remembers all the possibilities, and when there were more than 10000, I would remove half of them: I would find pairs of possible “histories” that ended with the sub in the same cell, and I would merge them into one - the visited squares in the new history were intersection of visited squares of the two old histories; mines were union of the old mines (I remembered the square from which the mine was placed, not the squares where it could be). I used 2 bitboards to remember the visited squares and mines; apart from that there was only player’s HP (to be able to prune after torpedoes and triggers) and his position.
Starting position and early game movement
I used the first turn to precalculate the first 33 squares I would visit (unless a mine or a fight happens). I used a genetic algorithm to do this. Every individual in the population consisted of up to 32 directions, stored in a 64-bit variable (2 bits per direction). Evaluation was simply walking from every square on the map, following the directions, and for every step that was valid I added 1 point to the evaluation - this ensured that I couldn’t be found easily at the start of the game. Mutation was simply changing one direction, crossover was taking two subpaths and concatenating them into one. After the GA finished, I chose an individual from the final population and a starting position according to path’s evaluation, number of squares I could reach from the final position (so that I don’t need to surface right after those 33 squares) and number of squares that I could mine when walking along the path.
Movement / Pathfinding
I used a genetic algorithm again, but a different one. This time, the individuals consisted of 225 directions (450 bits); each direction corresponded to a cell of the map, and it was the direction in which the submarine would move next from that square. The evaluation was simply following the directions, starting at my submarine’s current position, and adding bonuses and maluses for each square I visited, with decreasing weights further down the path (decay). These bonuses and maluses were:
- a constant (1), to make sure that I would surface as late as possible
- a small bonus for being close to the center - there’s usually fewer options for your sub when it’s stuck in the corner
- a small bonus for being out of range of any of my mines - to try to claim more space
- a large bonus if the path didn’t give my position away - every turn, I precalculated how many cells I could be in (according to my opponent’s tracking) after every path of length <= 6, and used only this information.
- a large malus for going to a square that could be in range of opponent’s mine (I calculated expected mine damage for every square, based on my tracking)
To mutate, I walked randomly from a cell on the sub’s path, until I either had no available squares, or merged back to the path. Crossover was following both paths at the start until they diverged, then picking one of them randomly, following it until the paths merged, then repeating the same process until I ran out of squares. Implementing these operations efficiently was the reason why I needed entire maps as individuals. On its own, this pathfinding, along with torpedoing whenever expected damage was above 0.75 and silencing afterwards, was enough to reach legend.
Surfacing, sonar, stuff…
I used sonar whenever the opponent could be in more than 1 sector and I had it charged, and used it on the sector with the most possible opponent positions. Surfacing was based simply on the expected mine damage I mentioned earlier, with the threshold needed for surfacing depending on my HP. I charged actions in order Torpedo > Silence > Mine > Sonar, with giving Mine some additional priority on the early turns, and also preferring Sonar over Mine when the number of possible opponent’s positions was above a threshold. I placed mines whenever I came close to a square that my bot considered good enough (based on number of water cells next to it, and how many of them were already in range of one of my mines), becoming less picky over time. I triggered mines whenever the average damage caused by the explosion would exceed 0.65; having this value too low or high had very bad effects on my bot.
Fighting
I wrote a simple sim that tried every possible combination of my actions (surface, move, torpedo, silence) that made at least some sense - f. e. never torpedoing a cell if the opponent couldn’t be next to it; usually ~2000 combinations. For each one of this combinations, I would find the cells the opponent could think I am in - this would be too time-consuming, so instead of working with all the combinations of cells I could have visited before coming to each cell where my opponent thought I could be, I used an estimation - the “real” visited squares, but I would move the squares I visited since I last used silence by the difference between my actual and “pretended” position - for example, if I’m at [1,1], I visited squares [1,2], [1,3], [2,3], [3,3] since I last used silence from [4,3], and I’m calculating squares I could reach by a combination of actions starting at [2,1], I would add [1,0] to those coordinates, and get [2,2], [2,3], [3,3] (and stop there, because [4,3] was visited before using silence), and the rest of the map would be the same. Then, I made an eval (HPs, numbers of possible positions after all the actions) for every combination of possible opponent position and torpedo that made some sense, and averaged them with some weights (opponent’s torpedos that hit more squares had higher weights), added some additional bonuses and maluses based on cooldowns after the turn and distance, how useful silence was in the part of the map where the fighting took place, how many cells I could still reach after my actions, and on expected mine damage on that cell. Going deeper than 2 turns wasn’t very useful, because once the abilities were used, players only charged torpedo and eval was simple at that point and it was easy to estimate the result from just the HPs and how much information the players had about each other’s positions.
And indeed, this worked great - whenever I tested this against my final version, it won a large majority of games where the two submarines met and fought to death.
… “whenever I tested this against my final version”?
Where it went wrong
I think you can already tell that this bot had a lot of things in it. So many, that the most difficult part became deciding which of these algorithms should I believe in this specific turn. I had a precalculated path from the first turn. I had a genetic algorithm that suggested a different path - this GA had less time to calculate it, but more information, and different priorities. I had an evaluation of how good each one of my possible actions was when it came to fighting the opponent with torpedoes and silence. I also had some heuristics… and it became a mess. The fighting algorithm couldn’t tell whether the sub should try to silence and torpedo towards a possible enemy position, or keep playing calmly, and somehow trying to use differences in evaluations was futile; pathfinding algorithm kept walking into torpedo range at 2 HP, … A few days before the end I realized that by adding more stuff (assumptions about enemy positions, using mines offensively, and more things I had prototypes for), I would probably just make it worse (and I was dangerously close to 100k characters), so I just thoroughly tested what I had already made, and it turned out that a bot that used the old, bugged fighting algo (a predecessor of what I described) worked best against the other players (except kovi, who kept losing to the new bot for some reason, and winning all the games against the submitted one). Maybe I could have somehow made it work, but with all the variance on the leaderboard and even in the benchmarks, I was lost and gave up.
Congratulations to everyone who managed to make sense of this game, and thanks to creators for giving us a game that we didn’t even come close to solving despite playing it for a month.