First off, thanks to Entelect for running another great challenge! Also, congrats to all the other finalists!
I found this one trickier than most of the previous challenges, but a lot of fun. Dealing with 360 degree movement + the real-time nature of the game forced me to rethink my approach.
On that note - I’m curious to know what strategy and approach everyone else used.
I went with something inspired by utility-based AI (although definitely not textbook utility-based AI). It only evaluates one move deep.
For movement, my bot evaluated moves in between 3 - 6 degree angles (based on its size and a few other variables). It would then assign a score to each target position based on something that was conceptually like a heatmap based a bunch of different criteria like the distance from food (closer is better), distance from gas (further is better), distance to opponents, distance to the edge, distance to supernova pickup, whether it’s in the path of enemy torpedoes, etc. Values decrease exponentially based on the distance (e.g food-score/2^rounds-to-food). I also gave it a slight bias towards food in the centre. Where my bot checked distance, it was distance expressed in the number of rounds if would have to travel - this would sometimes make it use the afterburner to get out of trouble quickly. It would then simply, out of the 120 odd options, choose the best spot to move to. This is also why my bot would wiggle like crazy - it had no clear “target” that it was moving towards.
For torpedoes, it would wait for enemies to be relatively close before firing so that they didn’t have room to dodge. I think this lead to the observation that my bot was patiently watching and waiting. Instead of rushing out of the way of incoming torpedoes, my bot would rather carry on grabbing food and doing what it was doing and shoot down incoming torpedoes when they got too close (one tick away). I figure it’s better to sacrifice some mass and have the opponent gain nothing instead of losing mass anyway and having an opponent steal it in the process. Interestingly, during testing, I noticed that counter-attacking torpedoes also causes a bot to grow in mass (see this post: Phase 2 Issues - #14 by rfnel). That made it into the final release. I suspect this may have helped in a couple of matches. Any ML-based bot that was trained by playing the game would probably have picked up on this as well (I’m curious as to whether anyone had a bot like that). To prevent opponents from shooting down my own torpedoes when we attack each other (e.g. I fire at 0 degrees and they fire at 180 degrees), I fired my own torpedoes at slightly offset angles.
I struggled a bit with the teleporter, until I realized it makes for a very effective weapon - I run a loop looking at all opponents, find all of the ones smaller than my bot + teleporter cost, and then evaluate the benefit of firing a teleporter at them. I also used the teleporter to quickly move around the map to gather points and powerups. I do this by dividing the map into quadrants, choosing a random position in each quadrant, and then evaluating it - if I randomly find a good option (i.e. the value of the position exceeds the cost of the teleporter), I fire a teleporter at it.
When trying to decide whether or not to shoot down torpedoes, my bot checks the number of torpedoes it has vs. the number of inbound torpedoes. If it doesn’t have enough torpedoes to counter, it raises a shield.
My supernova logic had two different approaches - I evaluate firing a supernova directly at each opponent, but I also check the midpoints between all pairs of opponents in the game. (A + B, A + C, B + C, etc.) If I find something that allows my bot to hit two different opponents at the same time, I use that. Before detonating a supernova, my bot checks a couple of future positions in the direction that the supernova is travelling. If the position is expected to improve, my bot waits before detonating it.
My wormhole logic was introduced quite late, but it basically assigns a score to each wormhole based on what sits on the other side (I map between wormholes of the same size - if I find more than one match, I ignore it until someone passes through a wormhole and it reduces in size so that I can then map it). I then evaluate moving towards a wormhole in a similar fashion to my “heatmap” for everything else.
I think what really helped me was to write a ton of test cases and whenever I found a scenario where my bot did something silly, I did some TDD - write a test that basically says “don’t be stupid” and keep changing the code until the test passes. It took hours of manually tweaking numeric values. Of course, this didn’t prevent a supernova suicide in one of the matches.
How did you do it?