Cover image generated by Nano Banana 2
Preamble
(Feel free to skip this section, as it mainly contains personal stories and opinions.)
Readers may have noticed that there were no new blog posts in May and June. This was because I participated in Kaggle’s “Orbit Wars” simulation competition and spent almost the entire month of May on it, and then I had a family matter to deal with in June.
Orbit Wars is the first Kaggle competition I’ve spent a significant amount of time on in at least five years. I’ve grown weary of Kaggle competitions because of the increasing demand for computing resources, the brittle, low-performance inference environment, and the growing responsibilities at work. Admittedly, Kaggle is very addictive and intellectually challenging. However, it is also very anxiety-inducing, which is problematic when I have other priorities to attend to. The marginal effect of pushing toward the gold medal zone diminishes sharply. I am way past the stage where I need to prove my technical capabilities to my peers, and honestly, most non-technical stakeholders I’ve met don’t care whether you are a Kaggle Master or Grandmaster. Reading papers and doing independent empirical research have been more beneficial to both my intellectual curiosity and my team’s growth (via technical sharing sessions).
After giving myself some time to recharge late last year, one of my goals has been to participate in at least one Kaggle competition to see how things have changed, both for me and for the community. I finally picked Orbit Wars, a simulation competition, because I had a blast competing in Google Research Football, the last simulation competition I participated in, back in 2020. Despite having to drop the competition in early June because of the aforementioned family matter, I still had a lot of fun (not as much fun as last time, though) and learned a lot from the experience. Now that the competition has ended, a lot of good write-ups have been published. I want to briefly document my experience and lessons learned from this competition for future reference. I hope some readers will find it helpful as well.
Game Description
- Goal: Conquer planets rotating around a sun in continuous 2D space. A real-time strategy game for 2 or 4 players.
- Overview: Players start with a single home planet and compete to control the map by sending fleets to capture neutral and enemy planets. The board is a 100x100 continuous space with a sun at the center. Planets orbit the sun, comets fly through on elliptical trajectories, and fleets travel in straight lines. The game lasts 500 turns. The player with the most total ships (on planets + in fleets) at the end wins.
You can find the full official description of the game in the kaggle-environments GitHub repository.
Fleet Movement
Collision detection between fleets and planets uses a swept-pair (tunnel-sweep) model — a single continuous check that treats both the fleet and the planet as moving simultaneously over the course of one turn.
Turn N (swept-pair paradigm):
Step 2 — Pre-compute planet paths: record (old_pos, new_pos) for every planet
and comet without applying the move yet.
Step 3 — Fleet movement + swept-pair:
For each fleet moving A→B:
For each planet with path P0→P1:
if swept_pair_hit(A, B, P0, P1, planet_radius):
→ fleet hits planet
Step 4 — Apply planet movement: all planet positions are updated to new_pos.
The detailed official implementation can be found here in the kaggle-environments repository.
This swept-pair collision model is one of the aspects of the game that require participants to make trade-offs between precision and performance, as the agent is given only one second to respond on each turn (with a global 60-second reserve buffer for the 500 turns).
My Experience
My Initial Goals
-
Build a Rust library that accurately and efficiently reproduces the game mechanics, so I can project the game state given a list of launched fleets and planet garrisons, and construct a table of potential fleet traffic between planets.
- Public heuristic-based agents have flawed aiming logic, which sometimes makes launched fleets miss their targets.
- Many of the public agents also use a trial-and-error approach to decide the exact number of ships that should be included in a launched fleet, which is highly inefficient.
- Implementing the simulation engine in Rust also allows me to exhaustively calculate all potential traffic between planets for the current turn and the next few turns. This provides two major benefits: (1) hyper-accurate planning for our fleets, and (2) an overview of the potential fleets that could be launched toward our planets (threat assessment).
-
Create a strong heuristic-based agent based on the projected game state and the fleet traffic table. The agent should avoid complicated rules and instead rank fleet launch candidates using a value function that estimates the value that each launch provides, so I can build a machine learning model based on the same set of features.
- I personally don’t like overly complicated and hard-to-maintain rule-based agents. My agent should be limited to under a dozen global constants or hyperparameters to tune.
- The agent should also be structured in a way that lets us build a machine learning model on top of the same framework, so the time spent building the heuristic-based agent would be amortized during the machine learning model training stage.
-
Create a machine learning model based on imitation learning (making the model learn how the top agents play), built on top of the heuristic-based agent’s feature-generation framework.
- I had no intention of building a complicated reinforcement-learning model (most likely PPO-trained) for this competition, as I believed it would definitely cost me hundreds of US dollars in GPU rental before I could get a decent result.
- Not spending money on renting GPUs for Kaggle competitions has always been my thing. If I believe training models at a particular scale (mainly in terms of VRAM size) would be beneficial to me in the long term, I’d buy a suitable GPU and run it locally. If training at that scale would not be beneficial in the long term, it is not worth renting, either.
- I have a card with 16 GB of VRAM locally. Imitation-learning-based models should comfortably fit on this GPU.
Things That Went Well
- I successfully built a Rust library that runs well within the time limit while providing much more powerful projection capabilities than the engine used by public heuristic-based agents.
- There were some hiccups along the way. For example, when implementing the logic for calculating the fleet traffic table while accounting for the sweeping mechanism, the original algorithm was so inefficient that even Rust could not complete it within the time limit. Fixing this kind of performance issue has been a great learning opportunity.
- I started learning Rust only half a year ago. This is the first project in which I successfully used Rust to significantly improve the performance of a Python program. It greatly boosted my confidence that continuing to learn and use Rust is a good investment of time. (My previous Hacker News Reader project uses Rust for its better support for high concurrency, not for raw performance.)
- I managed to build a heuristic-based agent under the desired framework that performed quite well on the leaderboard before the Producer agent was published, fluctuating around rank 100, well within the silver medal range.
- The main idea of my agent is very similar to the one used by the Producer agent: calculating the net ship counts for each fleet launch candidate. This means that I had already come up with an approach that is almost empirically optimal for this game.
- The geometry involved in this game is quite complicated. I took the opportunity to use AI agents to help me review the core mathematics behind some of the key calculations, such as the distance from a point to a line segment and the intersection of a tunnel and a line. The process helped me refresh my memory on some of the key geometric concepts, such as the normal form equation of a line. I’ve also posted AI-generated learning material on GitHub Gist.
Obstacles and Challenges
- I had to travel in June to deal with a family matter, so I had to abandon my plan to train a machine learning model using imitation learning because I would not have access to my GPU, and the network would be unstable during that time.
- The Producer agent was published right before my trip. I had very little time to review the agent and come up with improvements, and I ended up giving up entirely since I barely had time to code during my trip.
- I switched from OpenCode Go to Crof.ai in early May. Crof.ai was great for the first few days but was hit by multiple major incidents that made it basically unusable or severely degraded over the next few weeks. I already knew I had to travel at that time, so I was unwilling to purchase an OpenAI or Claude subscription that would be left unused for half of the subscription period. I sometimes had to manually deal with the low-quality code generated while the AI agents were degraded.
- The sweep collision calculation changed midway through the competition (and became much more complicated), which forced me to rewrite an entire section of the Rust library and rendered my original design for fleet traffic calculations less efficient.
- There were some subtle bugs in the Rust library that took me a while to uncover and fix. The coding agent was not very helpful. On the bright side, my grasp of the Rust implementation became much better through the process.
- I left many of the delta calculations in the Python portion of the agent as I improved the heuristics. This helped move things forward faster; however, it also made the code very buggy. I ended up spending a lot of time debugging and building a tool to visualize the agent’s decisions.
Lessons Learned
-
Although the Producer agent is based on the same core idea as my final agent, it is implemented much more elegantly. It makes some simplifications (e.g., ignoring comets and using the same fleet size for all candidate fleets from a source planet) while putting significant effort into optimizing and accurately implementing parts of the game logic.
- Sparse flow diff algorithm: This is brilliantly done to avoid processing irrelevant planets and speed things up. I implemented my version of the logic in Python and calculated the net ship count on a per-operation basis (a group of fleets targeting the same planet) for the same reason — simply rerunning the entire Rust state simulation process would have been a waste of resources. In hindsight, I should have implemented the algorithm in Rust once I verified that this idea worked very well empirically. There would have been far fewer bugs for me to deal with, and the code structure would have been much cleaner.
- The Axis-Aligned Bounding Box (AABB) filtering: This reduces the number of planets to consider during the sweep collision detection process significantly. I had a more complicated spatial-indexing-based optimization planned, but this simple AABB approach already works pretty well, so I copied it in my implementation.
-
I spent a significant amount of time building a process to evaluate threats from enemies for each planet. The fleet traffic simulation spent half its time (for 2-player games) or three-quarters of its time (for 4-player games) on this purpose. The Producer V2 agent does something very similar, but as some commenters pointed out, this idea does not seem to work very well and offers no visible advantage over the baseline. This was very likely a failed feature, and it made me over-engineer a significant part of my codebase to support it.
-
The Producer agent’s candidate generation is fairly straightforward. It also allows only one fleet per target planet in the final list of fleets to be launched. This significantly reduces the complexity for the planner and does not seem to affect the competitiveness of the agent. My agent, on the other hand, produces complicated operation candidates that sometimes consist of multiple fleets for the same target planet. In theory, this should produce better results with the same ship budget, but the greedy selection process at the end may not be the best fit for this design.
-
A repeating theme in these reflections is that I still overcomplicated things in my solution despite my intention to keep it simple. The Producer agent, despite being conceptually much simpler, completely destroys my agent. This may have something to do with an undiscovered bug in my code, but it may also simply be that some of my overcomplicated designs (e.g., attempting to capture opponents’ preferences for certain planets) backfired.
-
I built my solution using the general framework from the earlier public agents and tried to reduce the complexity step by step (replacing some of the core components with Rust and refactoring the structure to support the planned machine learning model). This may have been a bad decision. Starting from scratch, as the Producer agent did, could have been a better approach. I should not be afraid to stop at any point in the development cycle, review the situation, and decide that it may be much harder to continue building on the current architecture than to start from scratch with the help of a coding agent. At the very least, asking the coding agent to write a plan or build a prototype for evaluation would not hurt.
-
Prioritizing and long-term thinking are very important. I got hooked on the dopamine rush of getting a marginal leaderboard jump from minor tweaks. That time should have been spent on more ambitious things that would benefit me more in the long term. I believe this is actually the most important lesson I can take from participating in Kaggle competitions and apply to other parts of my life. Apparently, I still have much to learn in this regard.
Future Work
There are a lot of interesting things shared in post-competition write-ups that I’d like to explore. To be perfectly candid, some of the code shared in the write-ups is very hard to read. In one case, I found a single Python file with seven thousand lines, including a function of more than 1,000 lines. This is the reality after the advent of coding agents, and people who still like to read and understand code (like me) have to adapt to this reality. Hopefully, I can successfully crack the code and share my findings and lessons learned in a later blog post.