Winter Challenge 2026 - Feedbacks and Strategies

Here is a post to share your postmortems :snake:

6 Likes

Finished mid legend.

Nice game, I really liked the uncovering of the layers of the complexity for this game behind so simple rules. I also liked the optimization part for snakes collision and bfs handling, bitset part was really fun and i spend a lot of time optimizing some part i didnt actually use in the end.

I sticked to smitsi max believing it should work for this game, but simulation count was too low and random wall collisions were too harmfull for the estimation. When i realized the deeper problems i had no motivation to try anything else since the legend was too easy this time and i got pushed from ~100 place directly to legend. I really didnt want to add complicated heuristics for snake synchronization for the same reason, and tryed more on prunning, optimization and some eval tweaks, but it did nothing and moreover broke some carefully tuned stuff, so I dumped all the improvements and skipped final weekends.

Glad to see so many familiar faces at the very top and excited to see what people came up with, probably will spend a lot of time on this one in the multi. Thanks a lot to the organizers and to the comunity for such a fun week, I hope this nice tradition will continue.

P.S. some top games were facinating to watch, especially with the blaster’s mid weekend wrecking of all the opponents, was totaly rooting for him in the end, grac for top 2 anyway.

10 Likes

17th in Legend

I used smitsimax, the MCTS tree for each snake separately. The depth was 8 at the beginning, but final version is depth 10. The selection part is heavily biased, during the selection the UCB is:
score/visits + C * sqrt(log(N)/visits) + X * isWall(move) + Y * isApple(move) + Z * isAlmostApple(move). Also there is e-greedy strategy in selection: 2% chance to choose entirely random move.
The parameters were tuned partly by CLOP, or optuna, but I ended up mostly with hand picking the values. C = 1.1, X = -1, Y = 1, Z = 0.25. I didn't entirely prune the wall moves because sometimes they could be beneficial. I tried to add other things into the seleciton, for example give lower score for snake collisions etc. but it didn't increase winrate even with trying different values.

The evaluation was neural network with tanh [-1,1] output. The network evaluation time is tiny in comparison to the whole code, and the inputs are:
- global stuff like width, height, wallDensity, apples count, rounds count
- sum of my body snakes
- sum of opp body snakes
- minManhattanDistance to nearest apple of all of my snakes
- minManhattanDistance to nearest apple of all of opp snakes
for each of my snakes, and opp snake:
- x, y position of the head
- minManhattanDistance to nearest apple for the snake
- body size of the snake
Normalized accordingly so the inputs are mostly in range (-1,1).
I also had 5 output buckets depending of the map size: height < 13 ? 0 : height < 16 ? 1 : height < 19 ? 2 : height < 22 ? 3 : 4.

The training was based self-play games and positions and win/lose labels. Win label is 1 * pow(0.999,roundsLeft), draw is 0 and lose is -1 * pow(0.999,roundsLeft), so near the end of game the scores would be more accurate. For example win is near 1 few rounds before end of the game, while in the beginning of the game the score to train is around 0.1

In the MCTS tree, I backpropagated the eval using the score from the network for my snakes, and -score for the network for opp snakes. But also for each invidual snake I modified the score based on wether it died, shrinked or got longer, something like: score = (1-delta) * score + delta * 1, delta meaning size difference of the snake x P, the P is 0.1 (also to be tuned).

The simulation part takes most of the time, then the tree traversal and lastly evaluation. I was heavily using AI chats to give me improvements on my code, to make it more efficient, and I modified the code with theirs hints that gave me 3x more simulations. I used free version of webchats, among them were deepseek, z.ai, chatgpt, claude ai and grok. They would keep telling me that having vector body in snakes is the bottleneck and they proposed deque or custom-made ring buffer, which was worse or at best neutral. In the end, I got something like 1500 iterations (MCTS traversal up to 10 turns) on medium map with 4 snakes per player.

The game is interesting, the gravity aspect make it somewhat unique among other ā€œmultiplayerā€ snakes. The handling of out of bounds coordinates was challenging. The game overall was fun, I enojyed it, but the random timeouts at the league opening or after the contest ended was bad. And I don’t know how the referee calculates the time execution, it always showed 20-30ms when my bot used at least 40ms for the search. Also while having some tie breaker is good, it would be good to mention about it in the statement. Of course I don’t read the statement and read the referee instead, but still it caught me by surprise that snakes shortening contribute to ā€˜losses’, but somehow falling snakes out of bounds don’t.

16 Likes

I view these contests as thought experiments. Every day I threw different methods and strategies at it. Started with a perfect sim throwing moves at all the snakes at once to gen a new gamestate. It worked great. But using PHP i couldn’t gen enough sims to get a decent eval. The turn 0 processing is essential for setting up distances, platforms, apple ā€˜reachability’, etc. I ended up with 25 variables holding MBs of precomputes. MCTS for 8 snakes would fail on turn>0. So I cut it to max 4, then finally 1 at a time. Even BFS for single snakes was fine… but as the score climbs, so does the body count. Around turn 17, the slowdown started becoming enormous. I setup a platform mesh optimized with A* and predefined even more, ultimately failing time constraints. but I thought late game would benefit greatly as I thought head→plat→plat→apple (and variations thereof) would shorten BFS time. trimming the plat→plat node edges made this highly efficient. But the sacrifice ended up being the ā€˜dead-end’ check and other snake intercepts. I cut code for speed over and over. I finished in the middle, which I’m unhappy about. Because I had to revert to the code I submitted on day 3. there are some very basic speedups that imo are critical, however you index it…linearize the map. Simple operators and bitwise manip. are essential. I used a setup of ((y+10)<<6) +x + 10, but it isn’t optimal either. i recommend 8 instead of 10. Outside the map is then easily detected with stuff like pos&63 > mapwidth + buffer or pos&7 and pos>0x800 kinda thing. Having to account for going ā€˜off’ the map is a challenge in itself. Congratulations to those that had their hard work pay off!! mine did not. People who play this game understand that time is NOT on our side, no slop permitted to reach gold imo. GLHF.

5 Likes

91th Legend - Beam search (width 48, depth 12) with gravity physics sim, BFS apple distances, flood-fill reachability eval, and opponent prediction heuristic.

What Worked

  • Beam search with physics-accurate gravity simulation
  • Opponent re-prediction at each beam depth instead of replaying root predictions
  • Nearby apples bonus in eval
  • Strict testing protocol (20→50→100 games) — saved us from chasing noise

What We Tried and Failed (~40 experiments)

  • Gravity-aware BFS (5 variants) — always too pessimistic, snake bodies act as dynamic support that static analysis
    can’t capture
  • Better opponent/ally prediction (beheading avoidance, ally coordination, multi-strategy) — all within noise at 50+
    games
  • Deeper/wider beam, turn-1 extra time — deeper search with a weak opponent model just compounds prediction error
  • Anti-stall fixes (WASTED move overrides, anti-oscillation penalties, climb penalties) — all regressions
  • Apple feasibility filters — static geometry is always wrong in a gravity game with moving bodies
  • Eval weight tuning (distance, contested, mobility) — exhaustively explored, already near-optimal

Neural Network Attempt

Trained a CNN (18-channel input, dilated convs) via imitation learning (72% accuracy), then PPO+GAE RL collecting with
the C++ bot. First RL model hit 38% vs our strongest beam bot and 90% vs simple_bot. Subsequent iterations regressed due
to ratio instability. Ran out of time to stabilize — submitted the beam bot.

Main Takeaway

The opponent model was the ceiling. A 1-step heuristic predicting opponents while searching 12 deep creates phantom
paths that no eval fix can solve. Needed MCTS or DUCT for a real jump — started DUCT too late.

5 Likes

Hi everyone,

I finish 487, in the second half of gold league, and 3rd in PHP, which is… fine, i guess, for the amount of time invested. When i first discovered the challenge, i thought it was underwhelming, and reminded me more of olymbits (which i disliked) than cellularena (which i liked much more), and I left it unchecked for near a week. I think its because we started directly in bronze league, with no tutorial on wood leagues, which was the main selling point of cellularena, in retrospect.

Nevertheless, i was wrong, and it is viewing the TV on what others at the top the first weekend were doing that i understood the depth of the game. Jumping from platform to gain higher heights, ignoring cells, defending others by going round and round, attacking and blocking opponents snakes until they disappear, jumping over our own snakes to gain just one cell that is one square too far, or powering a double loco snake train, the bottom snake serving as climbing platform for the higher one, that hyped me more !

For people who wanted to know what is needed to beat each league, here’s what i did.

To reach silver league :

  • Full representation of the map (no gravity, only static items and snakes)
  • Strategy pattern
  • A* pathfinding from our snakes to the nearest power cell
  • Avoid the void (out of bounds left or right, don’t care for top) at all costs wherever possible

The strategy pattern is just a way to decide what each snake will do based on the urgency of the problem, like for example :

  • 1° if you have only one move possible, make that move
  • if not, 2° avoid the void at all costs if needed and possible
  • if not, 3° try pathfinding to the nearest power cell
  • if not, 4° make a random move…

To reach gold league, i had to add :

  • voronoi separation of the power cells
  • flood fill reachability of the power cells
  • anti duplication of move, anti immediate collision
  • snake memory, so when stuck (ex: UP UP UP), do something else
  • advanced A* pathfinding with snake length as decay, so need to jump from platform to platform to get higher than snake length
  • attack on other snakes (try to find ways to block the head of opponent snakes to break them)

I’m eager to learn from those who reached the masters league which of your techniques you think had the most chances of getting best scores. I see above that Monte Carlo Tree Search or Beam Search was involved, which implies that you have a full turn simulator with gravity included, i’m open to more details on how you handled the fact that up to four of your snakes (and opp, btw) were moving without having too much tree spreading (and memory exploding). Idem for those who have used Neural Networks, or Genetic Algorithms (or other techniques I do not know), i’d be happy to be linked to an article/tutorial that explains in great details how to reproduce what you’ve done.

Last note on using AI for coding : mostly SWE-1.5 (free from windsurf at the time), it is fine to write boilerplate code but couldn’t understand the advanced A* pathfinding with platforms and i had to write it and the tests myself.

5 Likes

I finished 164 in legend, using Rust. As always I really enjoyed the challenge, and the community. Thanks Codingame for those events, and big thanks for avoiding the Chrismas holidays and post-poning it to march.

the bot

I used a beamsearch of width 50 and max depth of 20, for each individual snakes. I first run it for opponent snakes, using a total of 5ms (thus stopping between depth 5~10). The remaining 40ms are for my snakes. At each search, I retain the best sequence of moves for the snake, and use it in the next searches for other snakes. I also kept 15 random nodes per depth level trying to add some diversity to the search.

I simulate the whole game at each step, using the already computed moves of preceeding snakes, or default direction for non processed snakes (they keep going on the same direction).

For evaluation, as always it is a mess composed of the number dead snakes (only mine), the sum of my snake’s body parts and the manathan distance of each head to the closest powersource. The manathan distances are computed only once at the begining of a turn and never updated during the search.

I spent some time working on the engine, and was able to simulate around ~30000 turn in 50ms.

workflow

over the years I tried to streamline my workflow to avoid wasting time during challenges. With this challenge I am happy about the way I was able to write and test the game engine, improving its performance, test my bots locally etc…

  • I started using a bundler to split my code and make it easier to manage. big thanks to @MathieuSoysal for cg-bundler
  • I used cargo test to add unit testing for various parts, in particular for the engine. Cargo really help reducing the friction to write tests by simplifying the process. I reworked the input read part to be able to read either from stdin or from a Vec<String>, which allowed me to easily take inputs from the web IDE and transform it into a test case. This helped me a lot when I was optimizing the engine, because I was not afraid of breaking things along the way. I even used llvm-cov to assess coverage of my tests and make sure everything worked properly.
  • For local testing I use the referee with additional CommandLineInterface.java to produce an executable that I can use with psyleague. That allows to run multiple bots in a local arena and quickly see how they perform. For bot using the 50ms, the local referee often time report timeout, so to avoid that I change the turn limit in the referee to 100ms, but since it hits the global 30s maximum time limit, I had to recompile Codingame engine to increase this limit also. The only downside with local arena is that using too many similar bots sometime does not reflect how a bot can perform in real arena, so I had to often cleanup the bot pool to mitigate this.
5 Likes

Hello,

My worst result in recent contests so far, even though I put a lot of efforts, I couldn’t get a star for this one…

Again I needed days for replicating and debugging properly the game mechanics and the referee for my simulations.

I had a lot of goals. I achieved only the first one, but I’m still happy that I managed to implement A* path finding for my snakebots to the nearest power source. Starting with the longest snake and cutting the search if time is out. My implementation wasn’t enough to beat even the Silver bot. Maybe if I manage to optimize the turn simulation more it would be better.

Thanks to the organizers for the event. Keep up the good work!

1 Like

Legend #161 - Adversarial hill climbing

I tried Smitsimax early on with not much success (I found hard reusing information from previous runs, and I required much better heuristics for it to make moves with sense since I couldn’t get much depth, even though my state simulation also averaged around 30-50K turns per 50ms), and then I got the idea of using hill climbing. My theory was that, if done properly, both players should converge to a kind of ā€œstalemate positionā€, and then if the adversary made a blunder that equilibrium point would fall in my favor, while not needing to adjust excessively the long-term plan from previous iterations (so I could reuse previous information).

To get more to high depths fast and converge more easily I used ā€œrolling windowsā€: in a single run I would first optimize moves 0-10, then 5-15… in many configurations it performed good with depths of up to 100 turns. To evaluate each turn heuristics I had an exponential decay (so immediate turns would weigh more). I also made single player optimizations to find paths to get the targets, and those converged almost arbitrarily fast (in big maps, it was able to find most times a trajectory that would get all ~50 power sources in around 10ms, and at least half in less than 5ms). I used an abstract representation of movements to make both convergence and emergent behaviors more easily: I encoded moves such as going to a specific target (mostly power source) or towards any snake tail for a margin of N movements, then next action, etc…

However, those paths often lead my bot to get ā€œeasyā€ power sources first and lose in the late game (when the other player controls the remaining ones). To avoid that, I filled a ā€œDispute matrixā€ where I put the turn each player took at minimum to reach any specific power source, so that power sources where my bots have a too big advantage would have less heuristic score than those where both players arrived at the same time (or even the other player arrived before in theory). I filled that matrix initially with single player runs since I could mirror the moves and update 2 power sources at the same time, and I kept it updated during the game. Any time a simulation picked a power source, I would update those values and retrieve the current ā€œadvantageā€ for the heuristics.

In the first turn I would do several single player runs to fill that matrix, and in the rest adversarial ones. I found many difficulties to make it work until on the last day I found a terrible bug (I was mirroring moves in adversarial runs in next turns), which after I fixed made my bot perform much better, but I had no time left to test configurations and my heuristics were already pretty messed up (they were very chaotic and sometimes could explode). The problem was that in its current state it was too unstable: it would both win spectacularly at times against bots at the very top but it would also fail miserably in other games with any lower ranking players. In the end, it was still my best result in a contest, and not too bad considering I rarely do bot programming (it’s still my 2nd legend bot ever :sweat_smile: ), but I think it had more potential had I had more time, or had I found that bug earlier. Maybe I’ll need to revisit it in the near future…

1 Like

~150 legend

Only beam search + heuristic

Beam search

Beam search over my own snakes’ moves. Opponents keep their current direction (no opponent modeling).

  • Beam width: dynamically scaled based on time budget and branching factor.
  • Snake clustering: the idea is that snake far away do not interact at all, that was a big improvement in performance: snakes are grouped by proximity using union-find (threshold = 12 manhattan distance). Each cluster runs its own independent beam search with a proportional share of the time budget. This keeps branching factor manageable for the beam search.

Evaluation

The leaf evaluation is a weighted sum of multiple factors (snake size, food proximity, freedom of movement to avoid being traped, etc). Food proximity is only a Manhattan distance but I assigned one snake per food, so my snakes don’t compete with each others. BFS / gravity-aware distance were detrimonial due to lost of performance. One big improvement that moved me to legend was very simple: a tiny bonus for altitude (sum of height for all body parts). The idea is that reaching higher platform is almost always better : this alone made the snake stop being stuck under an unreachable food source, they always try to climb.

Thanks CG for this challenge!

1 Like

I finished 149th overall using Rust.

My bot went through a few phases.

  1. BFS for each snake individually until it finds the first reachable food, then take the first step along the found route. Repeat every turn. Fallback to a very basic heuristic if no reacable food is found. Treat opponent and allied snakes as static obstacles (i.e. pretend they were frozen in place). Snakes are aware of the choice of previous allied snakes before them, which avoided collisions in some situations. This was enough to get into gold. One thing I noticed is that increasing depth beyond 10ish didn’t really help much, probably becuase the static opponent assumption means that any simulation at deeper levels isn’t representative of actual gameplay.

  2. In an effort to allow snakes to cooperate, simulate all of my snakes together rather than one by one, use a BEAM search and a basic evaluation based on distance to food and snake length. I either had to sacrifice beam width or depth too much due to the increased branch factor simulating 4 snakes together. This actually performed worse that my phase 1 bot. I got some improvement by introducing dynamic BEAM width, every turn i adjusted the beam width up and down based on delta from a target depth. This improved things becuase the ideal BEAM width to use depended on how many snakes were alive among other factors.

  3. Still unhappy with simulation depth/breadth I went back to single snake simulation, however if two snakes were near each other (either head or tail <= 4 manhattan distance from an ally head/tail) I allowed them to be simulated together, branch factor for 2 snakes wasn’t too bad, and now they could cooperate to climb larger distances. It was quite rewarding to see snakes cooperating effectively after this change. For simplicity I only allowed either single snake, or snake pairs. If 3 snakes were close, they were still simulated as 2 + 1. This was actually a massive improvement, got my into plat and most of the way to my final position.

  4. Most of the rest of the contest was some perf improvements, eval tweaking, and also making the adaptive beam width consider single snake and 2 snake searches separately. This is becuase snake pairs would pop in and out of existence a lot and disrupt the ability of the adaptive beam width to maintain my desired depth.

I observed the following weaknesses in my final bot:

  • absolutely no simulation of enemy moves
  • Would often go into tunnels and die, often due to lack of enemy move simulation where I wouldn’t account for the enemy blocking the tunnel off.
  • Something was up with my eval because snakes would sometimes circle food. Presumably becuase they were scored for proximity for food, but I did try to tune the eval so that eating food is always preferable, so not sure what’s up with that.
  • Poor strategy when it comes towards choosing which food to go for, once again due to no understanding of opponents moves, and also I didn’t build in any heuristics around who’s closer to food etc.

Workflow wise, I used CG-Bundler for bundling rust code to a single file and cgarena for testing my bots locally, mainly for tweaking heuristics, beam widths, etc.

This was my first contest using a simulation based approach, in future competitions I would like to experiment with MCTS and other similar approaches

Overall seems very similar to your bot @luxcem, and we placed at a similar level too.

1 Like

Legend #231- BFS with gravity in Kotlin

I had a blast competing in this event. I really like grid based games, because they give me the opportunity to use my GraphMateKT library, that I’ve been developing for some time. I used it to represent the grid and use BFS from each snake (ordered by their proximity to the nearest food) to find food that was untargeted by my other snakes. That was enough for silver.

To reach gold I had to introduce gravity, to avoid my snakes getting stuck jumping up and down. So I added a reachable filter where I simulated the food paths from my BFS results, and if gravity made the snakes fall down, I’d do another BFS from the new position. If the snake got caught in a loop, the food was marked as unavailable. In the event of the 5 closes unreserved food cells being unavailable, the snake was tasked to clime to a cell with a higher ā€œrankā€ then all the cells it inhabited. Rank was here calculated by the y value of the nearest platform below the cell. That was enough for a top 10 placement in gold.

For legend, i mostly added minor improvements. Don’t go for food that leaves the snake in a dead end, don’t do moves that causes a friendly snake to get trapped, if there are multiple close food, go for food with close food or high altitude. And that’s pretty much it. No A*, no state eval, no machine learning, just a rule based food hunting algorithm and loads of fun :slight_smile:

3 Likes

top 7 using ppo : https://gist.github.com/Sheee-sh/ae2dd0474412d604025fbedf883e89e0

9 Likes

Legend #1

My solution uses a variant of MCTS. First, it is multi-tree MCTS / Smitsimax: each snake has its own tree. Second, the trees are not really trees: when similar states are encountered via different move sequences, they are combined into a single node. States are considered similar if the snake head is in the same location and the snake is facing the same direction (just came from the same neighbor location). When similar states are encountered on different turn numbers, I tried various approaches for whether they should use the same or different nodes, and eventually settled on using two nodes per similar state, chosen based on the turn number modulo 2. Thus the total tree size is bounded by 8 times the grid size.

Each MCTS iteration goes to a maximum depth of 12 moves, after which a static evaluation function is used. The evaluation includes win/lose, current score, distance to the nearest apple, snake elevation (preferring to be higher), and the number of apples closest to each player.

Move selection in MCTS is biased by a policy that favors certain moves over others. Moves that collide with a wall or a snake (other than the tail) are not considered at all. Otherwise, the rules consider things like whether the destination cell contains an apple, the tail of a snake that can eat an apple, or an empty cell that another snake can move into. There is a preference for moving closer to the nearest apple, for not moving too far to the left or right, and for moving to a cell that has support under it.

Distance to apples is determined by a simple BFS with no awareness of gravity, snake length, etc. On the first turn, I perform a BFS from each apple and then build a data structure where each grid cell stores a list of apples in order of increasing distance. At the beginning of each turn, I remove any eaten apples from the beginning of the lists. During the search, I have to scan the list until I find the first apple that wasn’t eaten during the search.

To calculate the number of apples closest to each player, I use Manhattan distance from each snake head to a set of up to 64 apples chosen at the beginning of the turn and not updated during the search. Calculations are done using AVX2: The x/y coordinates and distances fit in 8 bits, so I can process 32 apples at once using 256-bit registers.

My simulation uses a 64x48 grid with walls around the edges. Walls, apples, and snakes are all represented in the grid to be able to quickly check whether a cell contains those things. To make falling faster, snakes keep track of how far from the tail they are supported by a wall or apple. On CG servers I simulate around 50,000 turns per 50ms (it varies a lot). That is with only about 1/3 of my runtime spent on simulation.

17 Likes

Legend #5

9 Likes

Legend #12
you can find my pm here

1 Like

Wp, wp! That’s a smart way to handle similar states, I think that’s what many of us were missing.

Legend #153 (python)

In this approach, I used a breadth-first search (BFS) to explore all possible moves for each snake up to a depth of 10. For each snake, I evaluated all possible moves using a static evaluation function that prioritized:

  1. Number of energies eaten – The primary goal was to maximize energy consumption.

  2. Speed of energy consumption – I favored moves that allowed the snake to eat energies as quickly as possible (shallow depth rather than deep).

  3. Distance to the nearest energy – I also considered the distance to the next energy to ensure efficient movement.

After evaluating all possible moves, I sorted them and selected the best move for each snake. To prevent multiple snakes from targeting the same energy, I implemented a reservation system that assigned unique energies to each snake.

I enjoyed a lot this challenge, and it was my first time going Legend :slight_smile:

4 Likes

Legend #28

Beam Search
Each snakebot is planned sequentially using an independent Beam Search (width=60, depth=20). The key insight is that already-planned snakebots use their beam-chosen direction at depth=0 (rather than a greedy fallback), giving later snakebots a more realistic model of ally behavior.
For unplanned allies and opponents, I pre-compute a simple greedy sequence.
I also invested meaningful effort in performance improvements to support the chosen beam settings. For example by limiting the number of apples taken into account to 64.

What Did Not Work (or had marginal effect)

  • Joint beam pre-pass: running a short beam (depth=5, width=20) over all snakebots before the main search to seed better ally sequences.
  • Transposition table.
  • Altitude bonus.

Many thanks to CodinGame for the great contest, and to all contributors of cg‑brutaltester.

2 Likes

hi, thanks for sharing this. Did you treat the snakes in any particuliar order?