<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stat Phantom</title>
    <description>The latest articles on DEV Community by Stat Phantom (@stat_phantom).</description>
    <link>https://dev.to/stat_phantom</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png</url>
      <title>DEV Community: Stat Phantom</title>
      <link>https://dev.to/stat_phantom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stat_phantom"/>
    <language>en</language>
    <item>
      <title>I Built the First Purely Learned Frame-by-Frame Tetris AI: Then It Started Cheating</title>
      <dc:creator>Stat Phantom</dc:creator>
      <pubDate>Tue, 23 Jun 2026 02:15:00 +0000</pubDate>
      <link>https://dev.to/stat_phantom/i-built-the-first-purely-learned-frame-by-frame-tetris-ai-then-it-started-cheating-322k</link>
      <guid>https://dev.to/stat_phantom/i-built-the-first-purely-learned-frame-by-frame-tetris-ai-then-it-started-cheating-322k</guid>
      <description>&lt;p&gt;Greetings all! You might know me from my Snake AI ablation series where I spent an unreasonable amount of time teaching a snake to eat apples. This is a new series. Same researcher, different game, significantly worse life decisions.&lt;/p&gt;

&lt;p&gt;This post is about Tetris. Specifically, about building what is, to our knowledge, the first AI agent to play frame-by-frame NES Tetris from raw pixels with no handcrafted observations, no shaped rewards, no enumerated placements, and no warm-start (and I mean that scoped to frame-level control from pixels, not as a field-wide claim). Button presses in, pixels out, reward only.&lt;/p&gt;

&lt;p&gt;At its peak it reached NES level 21.&lt;/p&gt;

&lt;p&gt;Then it started aiming pieces directly into the stack on purpose. And when I tried to fix that, everything got worse.&lt;/p&gt;

&lt;p&gt;That's the post.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Purely Learned" Actually Means
&lt;/h2&gt;

&lt;p&gt;Before anything else I need to define the constraint, because "purely learned" does a lot of work here and the definition is what makes this hard.&lt;/p&gt;

&lt;p&gt;The standard approach to Tetris AI, the one that &lt;em&gt;actually works&lt;/em&gt;, treats each piece placement as a single action. You enumerate the ~40 legal positions and rotations for a given piece, score each one, pick the best. The agent never has to figure out how to physically move or rotate a piece because the action space just skips that entirely. It picks a destination and the piece teleports there.&lt;/p&gt;

&lt;p&gt;That approach is powerful. My own placement baseline hit an average NES score of ~210,000 (max ~5.9M) using C51 over roughly 40 enumerated actions. The pixels-to-score pipeline works fine at that level.&lt;/p&gt;

&lt;p&gt;But enumeration is a &lt;strong&gt;handcrafted prior&lt;/strong&gt;. You're injecting the knowledge that pieces have legal placements, that rotations are discrete, that the board can be abstracted into a set of possible drop positions. The agent didn't learn any of that. You handed it to them. So for this project, that's disqualified.&lt;/p&gt;

&lt;p&gt;The constraint is: raw board pixels as input, 18 discrete button-combination actions as output, reward signal only (line clears, lock nudge, death penalty), nothing else. The agent has to discover from scratch how to move pieces, how to rotate them, how to drop them, &lt;em&gt;and&lt;/em&gt; where to put them. The achievement lives entirely in the scaffolding people quietly remove. Take away that cheat sheet and you have the open problem.&lt;/p&gt;

&lt;p&gt;As of the most recent published work I could find, this remains explicitly unsolved. Liu et al. tried Dreamer, DrQ, and Plan2Explore on frame-level NES Tetris from pixels and concluded none of them learned to clear lines. Every paper that successfully trains a Tetris agent either uses engineered board features, enumerates placements, or leans on reward shaping heavy enough to constitute a curriculum.&lt;/p&gt;

&lt;p&gt;So why is it hard?&lt;/p&gt;




&lt;h2&gt;
  
  
  Every Flat Agent I Trained Died at the Same Step
&lt;/h2&gt;

&lt;p&gt;By "flat" I mean a single neural network processing the board state and emitting frame-level actions. No hierarchy, no subgoals, just one agent doing everything.&lt;/p&gt;

&lt;p&gt;I ran four separate flat Rainbow-C51 agents on frame-level NES Tetris with different reward configurations: potential-based reward shaping, half-weight shaping, no shaping at all, and a lock-nudge variant. The results were the same every time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Reward shaping&lt;/th&gt;
&lt;th&gt;Peak avg score&lt;/th&gt;
&lt;th&gt;Collapse episode&lt;/th&gt;
&lt;th&gt;Post-collapse avg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1024env_ars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uncertain (older run)&lt;/td&gt;
&lt;td&gt;2,478&lt;/td&gt;
&lt;td&gt;ep ~265k&lt;/td&gt;
&lt;td&gt;~210&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shHalf_rl2x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Half PBRS&lt;/td&gt;
&lt;td&gt;269&lt;/td&gt;
&lt;td&gt;ep 110k&lt;/td&gt;
&lt;td&gt;~175&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noShape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;352&lt;/td&gt;
&lt;td&gt;ep 600k&lt;/td&gt;
&lt;td&gt;~280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rewardshift&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lock nudge + line reward&lt;/td&gt;
&lt;td&gt;378&lt;/td&gt;
&lt;td&gt;ep 470k&lt;/td&gt;
&lt;td&gt;~235&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every single one climbed, peaked, then collapsed. Not "plateaued." Not "converged to a suboptimal policy." Collapsed. The agent that had been playing passably started playing like it had forgotten everything it knew.&lt;/p&gt;

&lt;p&gt;ALARM BELLS. &lt;strong&gt;Setting an all-time record is a warning sign, not a milestone.&lt;/strong&gt; The pattern is consistent across all four runs: peak, all-time record, collapse within 10-30k episodes. If your flat agent just hit its best-ever score, start a timer.&lt;/p&gt;

&lt;p&gt;The collapse has a distinct fingerprint. The training loss stays completely smooth across it (this is not a numerical instability). What actually crashes is the per-layer NoisyLinear σ/μ ratio, dropping ~20% in a single 10k-episode window after weeks of 1-2% smooth decay. Simultaneously, episodes-per-second falls 4-5× as the agent abandons fast play. It doesn't blow up. It just... stops knowing things.&lt;/p&gt;

&lt;p&gt;The more striking detail is &lt;em&gt;when&lt;/em&gt; it happens. The &lt;code&gt;noShape&lt;/code&gt; run collapsed at episode 600k. The &lt;code&gt;rewardshift&lt;/code&gt; run collapsed at episode 470k. Different episode counts, different reward shapes. But &lt;code&gt;noShape&lt;/code&gt; at ep 600k equals roughly 1.47M gradient steps, and &lt;code&gt;rewardshift&lt;/code&gt; at ep 470k equals roughly 1.37M gradient steps. &lt;strong&gt;There is a death clock at ~1.4M gradient steps.&lt;/strong&gt; Change the reward, the shaping, the exploration strategy: flat agents die at the same odometer reading regardless.&lt;/p&gt;

&lt;p&gt;Reward shaping changes &lt;em&gt;when&lt;/em&gt; the agent reaches its peak and &lt;em&gt;from what altitude&lt;/em&gt; it falls. It does not change &lt;em&gt;whether&lt;/em&gt; it falls.&lt;/p&gt;

&lt;p&gt;I also tried removing NoisyNet exploration, adding an ε-floor, and increasing the n-step horizon to 20. The ε-floor is the best example of what happens when you try to outsmart this thing. It was supposed to maintain exploration and prevent collapse. What it actually did: made the agent climb slower, so it collapsed later, at the exact same peak (avg 378) and episode count (470k) as the run it was meant to save. The scenic route to the identical cliff.&lt;/p&gt;

&lt;p&gt;The n=20 run is its own story. Off-policy bias with uncorrectable n-step returns broke learning entirely. The average return declined below random baseline and pinned at −4.2 for 510,000 episodes, while the loss kept &lt;em&gt;decreasing&lt;/em&gt;. The agent grew more and more confident about a policy worse than doing nothing. Incredibly wasteful.&lt;/p&gt;

&lt;p&gt;The diagnosis: &lt;strong&gt;within-piece credit assignment&lt;/strong&gt;. A piece takes tens of frames to place. The only reward (a line clear) arrives long after, attributable to a chain of low-level actions spanning the entire drop sequence. A flat agent has to bridge that full horizon directly. At around 1.4M gradient steps, whatever representation it built stops being stable enough to support continued improvement, and everything falls apart. So what's the fix?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture That Didn't Collapse
&lt;/h2&gt;

&lt;p&gt;The fix is a &lt;strong&gt;manager/worker decomposition&lt;/strong&gt;. A manager that decides &lt;em&gt;where&lt;/em&gt; a piece should go (once per piece lock), and a worker that figures out &lt;em&gt;how&lt;/em&gt; to physically get it there (every frame).&lt;/p&gt;

&lt;p&gt;The manager operates on board pixels, runs a C51 distributional head over a spatial map, and emits a goal: a target row, column, and rotation for the current piece. This goal is passed to the worker via what I'm calling the &lt;strong&gt;Feudal Goal Interface (FGI)&lt;/strong&gt; with a &lt;strong&gt;spatial codec&lt;/strong&gt;. The goal is an absolute board coordinate and rotation, not an enumerated placement index. The manager picks &lt;em&gt;anywhere&lt;/em&gt;, legal or not (this becomes relevant shortly).&lt;/p&gt;

&lt;p&gt;The worker operates on an egocentric observation of the board, receives the goal from the manager, and earns a dense per-frame &lt;strong&gt;reach reward&lt;/strong&gt;: a goal-distance gradient that fires every frame as it moves the piece closer to the target. Double-DQN, dueling scalar head, 18 actions.&lt;/p&gt;

&lt;p&gt;A sharp reader will object here: "you said no shaped rewards, but the worker gets a dense per-frame reward. That's shaping." It isn't, and the distinction matters. The reach reward contains zero Tetris knowledge. It doesn't say "holes are bad" or "keep the stack flat" or anything about piece geometry. It says "you are this far from a coordinate." The goal it points toward is generated by the manager from pixels. It's internal manager-to-worker communication, not injected knowledge from outside. The system's only external inputs are pixels and the game reward. Everything else, including the goal coordinate itself, is learned from scratch inside the hierarchy.&lt;/p&gt;

&lt;p&gt;The key is the timescale split. The manager acts once per piece lock, so its credit horizon is measured in &lt;em&gt;placements&lt;/em&gt;, not frames. The within-piece credit assignment problem that kills flat agents simply doesn't exist at the manager's level. The worker gets a dense per-frame signal that makes the low-level movement problem tractable. Each level of the hierarchy gets a horizon it can actually handle.&lt;/p&gt;

&lt;p&gt;I ran this architecture (the &lt;code&gt;miss05&lt;/code&gt; configuration, more on that shortly) to 1.38M gradient steps with no flat-style collapse. No σ crash. No speed drop. It plateaus, it does not fall apart. The hierarchy carries the within-piece horizon past the zone that kills flat nets.&lt;/p&gt;

&lt;p&gt;One caveat worth stating clearly: &lt;strong&gt;the hierarchy is not failure-proof&lt;/strong&gt;. An earlier run (&lt;code&gt;coplay_reach_noshape&lt;/code&gt;) crashed hard: clears peaked at 0.032 then fell to 0.005. The root cause was the manager's C51 value head inheriting a support range of &lt;code&gt;[-20, 1000]&lt;/code&gt; from the placement network (where scores reach into the thousands). Co-play manager returns are roughly 0-30. With 101 atoms spread over that range, the manager had maybe 3 atoms of actual resolution for the values it was seeing, effectively value-blind, lurching into the same degenerate corner-basin every ~90-100k steps and crawling back out.&lt;/p&gt;

&lt;p&gt;The fix was recalibrating the support to &lt;code&gt;[-10, 30]&lt;/code&gt; (0.40 per atom over the actual return scale). Match your support to your actual returns. Obvious in hindsight. The hierarchy avoids the &lt;em&gt;specific&lt;/em&gt; flat 1.4M-gradient-step collapse, but it has its own failure modes if misconfigured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then It Started Cheating
&lt;/h2&gt;

&lt;p&gt;As the hierarchy learned, I noticed something in the telemetry. The &lt;code&gt;reach%&lt;/code&gt; metric (the percentage of pieces where the worker actually &lt;em&gt;reached&lt;/em&gt; the manager's goal) was falling. Not spiking. Not oscillating. Steadily falling, over hundreds of thousands of episodes, as performance climbed.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;tgt_depth&lt;/code&gt; (a measure of where the manager was aiming, with negative values being legal placements above the stack and positive values being positions &lt;em&gt;inside&lt;/em&gt; the stack) was heading positive. Deep positive.&lt;/p&gt;

&lt;p&gt;The manager had discovered that aiming a piece at a spot buried inside the stack earns the same line-clear credit as a good goal, because the worker clears lines anyway. So it became the &lt;strong&gt;pointy-haired-boss of RL&lt;/strong&gt;: issues garbage orders, takes credit for the work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Episode&lt;/th&gt;
&lt;th&gt;Avg score&lt;/th&gt;
&lt;th&gt;Lines/ep&lt;/th&gt;
&lt;th&gt;Reach%&lt;/th&gt;
&lt;th&gt;Goal correlation&lt;/th&gt;
&lt;th&gt;Clears/lock&lt;/th&gt;
&lt;th&gt;Max NES level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10k&lt;/td&gt;
&lt;td&gt;157&lt;/td&gt;
&lt;td&gt;0.006&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;td&gt;0.249&lt;/td&gt;
&lt;td&gt;.0004&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50k&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;0.089&lt;/td&gt;
&lt;td&gt;6.3%&lt;/td&gt;
&lt;td&gt;0.742&lt;/td&gt;
&lt;td&gt;.0031&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90k&lt;/td&gt;
&lt;td&gt;355&lt;/td&gt;
&lt;td&gt;1.57&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;td&gt;0.573&lt;/td&gt;
&lt;td&gt;.036&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150k&lt;/td&gt;
&lt;td&gt;999&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;td&gt;0.336&lt;/td&gt;
&lt;td&gt;.156&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;180k&lt;/td&gt;
&lt;td&gt;4,585&lt;/td&gt;
&lt;td&gt;30.4&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;td&gt;0.174&lt;/td&gt;
&lt;td&gt;.275&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192k&lt;/td&gt;
&lt;td&gt;10,436&lt;/td&gt;
&lt;td&gt;49.6&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;0.139&lt;/td&gt;
&lt;td&gt;.320&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200k&lt;/td&gt;
&lt;td&gt;13,639 &lt;em&gt;(peak avg)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;58.0&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;0.126&lt;/td&gt;
&lt;td&gt;.333&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;210k&lt;/td&gt;
&lt;td&gt;13,044&lt;/td&gt;
&lt;td&gt;55.8&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;0.174&lt;/td&gt;
&lt;td&gt;.338&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;288k&lt;/td&gt;
&lt;td&gt;7,026&lt;/td&gt;
&lt;td&gt;38.4&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;0.358&lt;/td&gt;
&lt;td&gt;.297&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;coplay_cap&lt;/code&gt; as of publication. Still training at time of writing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reach drops from 6.3% to 0.2%. Goal correlation falls from 0.74 to 0.14. The manager's goals become almost completely decorrelated from where pieces actually land. The conductor waves the baton, the orchestra ignores it. Here's the part that should bother you: the music &lt;em&gt;improves&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At its peak, NES level 21. Record score 85,120. Average peaked at ~13,639 before declining as the board conditions got harder (more on that below). The avg is tail-inflated throughout (std consistently exceeds the mean, median at ep 192k was ~5,002), so the distribution is wide. But the level 21 is real.&lt;/p&gt;

&lt;p&gt;At 0.2% goal accuracy.&lt;/p&gt;

&lt;p&gt;Before you get too excited: level 21 is frantic flailing, not elegance. The board gets &lt;em&gt;dirtier&lt;/em&gt; as capability climbs, then actually cleans up slightly at the higher levels (holes peaked at 36, back down to 15 by ep 288k, though aggregate height kept rising to ~114). The avg declined from its peak of ~13,639 at ep 200k down to ~7,026 by ep 288k. That's not a flat collapse (no σ crash, no speed drop, the record score kept climbing) — it's what happens when the agent is consistently reaching harder board states and the score distribution gets wider and wilder. The cumulative clears by ep 288k: 4.5M singles, 782k doubles, 38.8k triples, 803 tetrises. Still mostly singles. Still winning by working frantically and never tidying up. The result is real. The style is not pretty.&lt;/p&gt;

&lt;p&gt;So what do you do when your manager starts cheating?&lt;/p&gt;




&lt;h2&gt;
  
  
  Every Time I Fixed It, It Got Worse
&lt;/h2&gt;

&lt;p&gt;In an earlier run I had tried &lt;code&gt;miss05&lt;/code&gt;: a configuration specifically designed to address the illegal-goal drift visible in earlier experiments. It added two mechanisms on top of the same feudal skeleton:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;reach_penalty = 0.05&lt;/code&gt;&lt;/strong&gt;: a graded distance penalty docked from the manager's reward for each piece, proportional to how far the actual landing was from the goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;miss_reward_scale = 0.5&lt;/code&gt;&lt;/strong&gt; (half-on-miss): when the worker failed to reach the goal footprint, the manager's positive reward was scaled by 0.5. Death penalty kept full.&lt;/p&gt;

&lt;p&gt;The goal was to make the manager care about whether its goals were actually reachable and legal. And it worked. &lt;code&gt;miss05&lt;/code&gt; ran to 901k episodes (1.38M gradient steps) with reach consistently 55-77%, goal correlation ~0.96, target depths near zero. Legal, reachable, coordinated placements. The well-behaved agent.&lt;/p&gt;

&lt;p&gt;It capped at NES level 2.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;miss05&lt;/code&gt; (enforce legality)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;coplay_cap&lt;/code&gt; (drop the enforcement)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reach%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55-77%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goal correlation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.96&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goal legality (tgt_depth)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Legal (~0)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Illegal (+6, buried)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clears/lock&lt;/td&gt;
&lt;td&gt;0.11 (plateau)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.32 (climbing)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max NES level&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines/episode&lt;/td&gt;
&lt;td&gt;~5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grad-steps reached&lt;/td&gt;
&lt;td&gt;1.38M&lt;/td&gt;
&lt;td&gt;359k (still training)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The well-behaved agent is the worst one.&lt;/p&gt;

&lt;p&gt;I need to be honest about a confound: &lt;code&gt;miss05&lt;/code&gt; used a conv-base manager and base-capacity worker, while &lt;code&gt;coplay_cap&lt;/code&gt; used a wider conv manager and a scaled-up worker. Capacity and legality-enforcement co-vary across these two runs. It's not a clean single-variable ablation, and I won't pretend otherwise.&lt;/p&gt;

&lt;p&gt;The cleaner evidence is &lt;em&gt;within&lt;/em&gt; &lt;code&gt;coplay_cap&lt;/code&gt; itself. At fixed capacity, reach falls from 6.3% to 0.2% &lt;em&gt;as&lt;/em&gt; capability rises. The system actively moves away from legal goals as it improves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Manager Stopped Trying
&lt;/h2&gt;

&lt;p&gt;The manager's reward is the &lt;strong&gt;outcome&lt;/strong&gt;: line clears, lock nudge, death penalty. Not whether its goal was good. Not whether it was reached. Just what happened.&lt;/p&gt;

&lt;p&gt;Once the worker is reasonably competent, it clears lines roughly &lt;em&gt;independent of the exact goal&lt;/em&gt;. If the goal is unreachable, the worker free-plays and still clears lines. Legal goals and illegal goals earn the same outcome reward. There is &lt;strong&gt;no gradient&lt;/strong&gt; pointing the manager toward legal goals. None.&lt;/p&gt;

&lt;p&gt;So the goals diffuse. Most random placements in a 20×10 grid are buried inside or above the stack. &lt;code&gt;tgt_depth&lt;/code&gt; drifting to +6 means "aim somewhere deep in the stack so the piece just falls." The manager hasn't broken anything. It found a way to emit a plausible-looking goal that the worker ignores, while still collecting full outcome credit. Wolpert and Tumer called this failure mode "Wonderful Life Utility" for a reason. COMA formalised it. I watched it happen live in the telemetry.&lt;/p&gt;

&lt;p&gt;This compounds as the worker gets more autonomous. The more the worker free-plays competently, the less the manager's goal matters, the less the manager optimises for reachable goals, the more it drifts. &lt;em&gt;shrugs&lt;/em&gt; The well-behaved run (&lt;code&gt;miss05&lt;/code&gt;) penalised the symptom and inadvertently killed the signal. The reach penalty and half-on-miss scaling forced legal goals but reduced the worker's autonomy and disconnected the dense per-frame reach gradient from actual learning. Clean, coordinated, legal, and capped at level 2.&lt;/p&gt;

&lt;p&gt;Easy to get wrong: at 0.2% reach, the obvious read is "the manager is useless, this is basically a solo worker." WRONG. No-manager flat nets collapse (see above). The manager's actual contribution was never "aim piece, land piece." It was "give the worker a &lt;em&gt;target to chase&lt;/em&gt; so the per-frame goal-distance gradient has direction." That target doesn't have to be legal. It just has to &lt;em&gt;exist&lt;/em&gt;. The manager looked vestigial. It was load-bearing the whole time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Fix (That I Haven't Run Yet)
&lt;/h2&gt;

&lt;p&gt;The principled solution is a &lt;strong&gt;counterfactual reward&lt;/strong&gt;, sometimes called a difference reward or Wonderful Life Utility (Wolpert &amp;amp; Tumer; COMA, Foerster et al. 2018). Instead of rewarding the manager for the outcome, reward it for the &lt;em&gt;difference&lt;/em&gt; between the outcome and what the worker would have achieved with no goal at all.&lt;/p&gt;

&lt;p&gt;Vacuous or illegal goals: outcome ≈ free-play baseline, &lt;strong&gt;~0 credit&lt;/strong&gt;. The drift stops being free.&lt;/p&gt;

&lt;p&gt;Reachable, genuinely-helpful goals: outcome &amp;gt; baseline, &lt;strong&gt;positive credit&lt;/strong&gt;. The manager has an actual gradient toward useful goals for the first time.&lt;/p&gt;

&lt;p&gt;The concrete plan: run ~25% of environments in worker-only / null-goal free-play to generate a running baseline B (bucketed by board height), then route &lt;code&gt;goal_advantage = task_reward - B&lt;/code&gt; to the manager instead of the raw task reward. Worker keeps the dense reach signal. Manager gets credit only when its goal &lt;em&gt;actually helped&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is designed. It is not yet run. &lt;code&gt;coplay_cap&lt;/code&gt; needs to converge first (ep 288k at time of writing, NES level 21, avg declining from peak as board conditions harden). Once it does, the counterfactual reward gets implemented.&lt;/p&gt;

&lt;p&gt;Either the manager finally learns to aim somewhere the worker can actually reach and clear from, genuine "aim then land" coordination, or it doesn't, and that tells us something equally worth writing about.&lt;/p&gt;

&lt;p&gt;The sequel exists either way.&lt;/p&gt;




&lt;p&gt;&lt;/p&gt;
  Honesty checklist — read before citing anything
  &lt;br&gt;
&lt;strong&gt;Single-seed throughout.&lt;/strong&gt; Every run in this post is n=1. The trustworthy parts are the directions and the within-run inversions, not the exact crossover numbers.

&lt;p&gt;&lt;strong&gt;"First purely learned" is harness-scoped.&lt;/strong&gt; Model-based approaches (DreamerV3, MuZero) are on the untried bench, not ruled out. The claim is specifically about frame-level control from pixels with no handcrafted features, no shaped rewards, no placement enumeration, no warm-start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;coplay_cap&lt;/code&gt; was still training at publication.&lt;/strong&gt; Numbers reflect the latest snapshot at time of writing (ep 288k). Peak avg was ~13,639 at ep 200k; the subsequent decline is not a flat collapse (no σ crash, no speed drop) but reflects harder board conditions at higher levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The flat-collapse table is from project records.&lt;/strong&gt; The original run files were cleaned up. The pattern is well-established across all four runs; the exact figures are recalled, not freshly verified from disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The miss05 vs coplay_cap comparison is confounded.&lt;/strong&gt; Capacity and legality-enforcement co-vary. The clean ablation (conv-wide + scaled worker, legality on vs off) has not been run. The within-run drift in &lt;code&gt;coplay_cap&lt;/code&gt; is the cleaner evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapse-survival evidence rests on &lt;code&gt;miss05&lt;/code&gt; (1.38M gradient steps), not &lt;code&gt;coplay_cap&lt;/code&gt; (only 359k).&lt;/strong&gt; And it's a cross-stack comparison, not an identical-stack ablation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 21 is survival-volume, not efficient Tetris.&lt;/strong&gt; 4.5M singles, 782k doubles, 38.8k triples, 803 tetrises cumulative by ep 288k. It's keeping the board alive, not building wells.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The counterfactual reward fix is designed, not run.&lt;/strong&gt; Frame it as the next experiment, not a result.&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;p&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a CS researcher. All experiments here are personal projects run on my own hardware, entirely separate from any institutional affiliation.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Wolpert, D. &amp;amp; Tumer, K.: "Wonderful Life Utility" (counterfactual/difference rewards)&lt;/li&gt;
&lt;li&gt;Foerster, J. et al. (2018): COMA: Counterfactual Multi-Agent Policy Gradients&lt;/li&gt;
&lt;li&gt;Vezhnevets, A. et al. (2017): FeUdal Networks for Hierarchical Reinforcement Learning, ICML&lt;/li&gt;
&lt;li&gt;Bairaktaris, J.A. &amp;amp; Johannssen, A. (2025): "Outsmarting algorithms: A comparative battle between Reinforcement Learning and heuristics in Atari Tetris," &lt;em&gt;Expert Systems with Applications&lt;/em&gt; 277, 127251&lt;/li&gt;
&lt;li&gt;Liu, H. &amp;amp; Liu, L.: "Learn to Play Tetris with Deep Reinforcement Learning," OpenReview&lt;/li&gt;
&lt;li&gt;Algorta, S. &amp;amp; Şimşek, Ö. (2019): "The Game of Tetris in Machine Learning," arXiv:1905.01652&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>tetris</category>
    </item>
    <item>
      <title>When Chaos Wins: Adding Noise Improved My Snake AI's Stability</title>
      <dc:creator>Stat Phantom</dc:creator>
      <pubDate>Sun, 17 May 2026 07:20:58 +0000</pubDate>
      <link>https://dev.to/stat_phantom/when-chaos-wins-adding-noise-improved-my-snake-ais-stability-1b5l</link>
      <guid>https://dev.to/stat_phantom/when-chaos-wins-adding-noise-improved-my-snake-ais-stability-1b5l</guid>
      <description>&lt;p&gt;Greetings all! Continuing the series where I build Rainbow DQN one component at a time on Snake. The &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p"&gt;first post&lt;/a&gt; covered encoding, the &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh"&gt;second&lt;/a&gt; covered memory, the &lt;a href="https://dev.to/stat_phantom/removing-per-from-rainbow-dqn-set-a-new-snake-ai-world-record-1d47"&gt;third&lt;/a&gt; covered PER hurting performance. This one is about a truly WTF?! moment I stumbled into while evaluating the models.&lt;/p&gt;

&lt;p&gt;When you evaluate a model that uses noisy networks, you turn the noise off. You're not training, so why would you keep the exploration noise active? You want the clean, deterministic policy. The model's best guess, no randomness. That's what you do, it's basically an axiom in machine learning.&lt;/p&gt;

&lt;p&gt;So I did just that. And the evaluation scores were &lt;em&gt;significantly&lt;/em&gt; worse than training. Not slightly. Significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Noisy Networks Do (Quick Recap)
&lt;/h2&gt;

&lt;p&gt;Standard DQN uses epsilon-greedy exploration: pick a random action X% of the time, decay that percentage over training. Simple, dumb, works.&lt;/p&gt;

&lt;p&gt;Noisy networks replace this with something smarter. Each linear layer in the network gets learnable noise parameters (sigma weights). During training, the network adds noise to its own weights, producing slightly different outputs each forward pass. The network &lt;em&gt;learns&lt;/em&gt; how much noise to apply. Early in training, sigma values are high and the agent explores broadly. As training progresses and the agent gets more confident, sigma can shrink. For evaluation, you set sigma to zero. Clean output. Textbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evaluation Gap
&lt;/h2&gt;

&lt;p&gt;Running evaluations across multiple training checkpoints, I noticed something was off. Not subtly off. The deterministic eval scores were wildly inconsistent.&lt;/p&gt;

&lt;p&gt;Some checkpoints averaged 78. Others averaged 18. The training curve at these same points? Perfectly stable. The model was learning consistently the whole time, but deterministic evaluation was telling a completely different story depending on which checkpoint I happened to evaluate.&lt;/p&gt;

&lt;p&gt;First instinct: it's a bug. Checked the eval pipeline, checked the checkpoint loading, checked the environment seeding. Everything was fine. The model genuinely performed this erratically when noise was turned off. So if it's not a bug... what is it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bimodal Trap
&lt;/h2&gt;

&lt;p&gt;The ep450K checkpoint was where it got properly weird. Deterministic eval produced a strongly bimodal distribution: roughly 25% of episodes scored near zero, while 75% scored above 80. The average landed at 59, but that number is completely meaningless when your distribution is two separate peaks with a canyon between them.&lt;/p&gt;

&lt;p&gt;So what's going on? The deterministic policy has traps. Specific game states where the mean-weight Q-values for two or more actions are nearly identical. Without noise, the agent picks the same action every single time it hits that state. If that action happens to be the wrong one? Stuck. It loops, it crashes, it scores zero. 25% of episodes starting from certain initial states hit these traps every time.&lt;/p&gt;

&lt;p&gt;Now. Same checkpoint, same evaluation seeds, noise turned back on:&lt;/p&gt;

&lt;p&gt;The bimodal failure mode vanished. Gone. The p25 jumped from 2 to 59. The average climbed from 59 to 73. The standard deviation dropped from 42 to 26. The noise &lt;em&gt;nudges&lt;/em&gt; the agent out of those deterministic traps. Not randomly, not chaotically, but because the learned noise provides just enough variation in the Q-values to stop the agent getting stuck in a degenerate action loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The noise isn't exploration overhead left over from training. It's a load-bearing part of the learned policy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ntteqqp45ecbfaf26yr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ntteqqp45ecbfaf26yr.png" alt="Deterministic VS Chaotic" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This wasn't a one-off. The pattern held at every checkpoint from ep50K through ep450K. Stochastic eval beat deterministic eval at every single point. Lower variance, higher consistency, fewer catastrophic zero-score episodes. The sigma values aren't residual training artifacts waiting to be zeroed out. They're doing actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Snake Makes This Worse
&lt;/h2&gt;

&lt;p&gt;Snake has a property that makes deterministic policies especially vulnerable to traps: a single wrong turn can be immediately fatal.&lt;/p&gt;

&lt;p&gt;Picture a snake at length 100+ navigating a tight corridor of its own body. The optimal action and the second-best action might differ by a tiny margin in Q-value. Deterministic policy picks the same one every time. If that action leads into a dead end three moves later, the agent dies. Every time. From that state. Noise provides enough Q-value perturbation to occasionally pick the second-best action, which might be the one that actually survives.&lt;/p&gt;

&lt;p&gt;In environments with more breathing room (wide open Atari levels, games where one wrong move doesn't kill you), deterministic policies don't develop these traps as severely. The longer the snake gets, the more traps exist, and the more the noise matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means In Practice
&lt;/h2&gt;

&lt;p&gt;If you're using noisy networks and evaluating with mean weights, your evaluation scores may not just be noisy. They can be structurally misleading. The deterministic policy can have failure modes that simply don't exist in the trained stochastic policy.&lt;/p&gt;

&lt;p&gt;Before assuming deterministic eval shows the "true" performance of your agent, run a stochastic eval comparison. If the scores diverge, your agent has learned to depend on its noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Caveats
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Single architecture, single game.&lt;/strong&gt; This was observed on C51 + dueling + noisy on Snake. Games with more forgiving state dynamics may not exhibit the same bimodal failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noise can grow too large.&lt;/strong&gt; At one late-stage checkpoint, sigma values had grown large enough that stochastic eval actually dropped below deterministic. There's a Goldilocks zone where noise is productive. Past that zone it becomes destructive. The finding is not "always evaluate with noise." The finding is "don't assume deterministic eval is automatically better."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training scores remain the most reliable metric.&lt;/strong&gt; For the ablation study, training window averages computed identically across all runs are the primary comparison, sidestepping the whole question entirely.&lt;/p&gt;

&lt;p&gt;If you've observed similar eval divergence with noisy networks, or if you have environments where deterministic eval reliably matches training performance, I'd like to hear about it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This work is part of ongoing research and the findings are planned to be submitted as a peer-reviewed paper.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Peer-Reviewed
&lt;/h3&gt;

&lt;p&gt;Fortunato et al. (2018) - "Noisy Networks for Exploration" - ICLR 2018. &lt;a href="https://arxiv.org/abs/1706.10295" rel="noopener noreferrer"&gt;arXiv: 1706.10295&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hessel et al. (2018) - "Rainbow: Combining Improvements in Deep Reinforcement Learning" - AAAI 2018. &lt;a href="https://doi.org/10.1609/aaai.v32i1.11796" rel="noopener noreferrer"&gt;DOI: 10.1609/aaai.v32i1.11796&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bellemare et al. (2017) - "A Distributional Perspective on Reinforcement Learning" - ICML 2017. &lt;a href="https://arxiv.org/abs/1707.06887" rel="noopener noreferrer"&gt;arXiv: 1707.06887&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>ai</category>
      <category>chaos</category>
    </item>
    <item>
      <title>Removing PER From Rainbow DQN Set a New Snake AI World Record</title>
      <dc:creator>Stat Phantom</dc:creator>
      <pubDate>Sat, 09 May 2026 08:32:53 +0000</pubDate>
      <link>https://dev.to/stat_phantom/removing-per-from-rainbow-dqn-set-a-new-snake-ai-world-record-1d47</link>
      <guid>https://dev.to/stat_phantom/removing-per-from-rainbow-dqn-set-a-new-snake-ai-world-record-1d47</guid>
      <description>&lt;p&gt;Greetings all! Quick context: this is part of an ongoing series where I'm building Rainbow DQN one component at a time on Snake and measuring what each piece actually does. The &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p"&gt;first post&lt;/a&gt; covered the encoding, the &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh"&gt;second&lt;/a&gt; covered a memory optimisation. This one is about the finding I've been teasing: which Rainbow component &lt;em&gt;hurts&lt;/em&gt; performance on Snake.&lt;/p&gt;

&lt;p&gt;The answer is Prioritised Experience Replay (PER). Removing it from Rainbow DQN didn't just match performance. It set a new world record of &lt;strong&gt;&lt;del&gt;153&lt;/del&gt; 156&lt;/strong&gt; on a 20×20 grid, smashing the previous record of 134 set by full Rainbow (with PER), and nearly 2.5× the best published peer-reviewed result of 62 (Sebastianelli et al., 2021).&lt;/p&gt;

&lt;p&gt;The component that Hessel et al. (2018) ranked as one of Rainbow's two most important pieces actively &lt;em&gt;hurts&lt;/em&gt; on some games such as snake.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is PER? (And Why Does Everyone Use It?)
&lt;/h2&gt;

&lt;p&gt;Prioritised Experience Replay changes how an agent samples from its replay buffer. Instead of uniform random sampling (every stored transition has equal probability of being replayed), PER assigns a priority to each transition based on its TD error. Transitions the agent got most wrong get replayed most often.&lt;/p&gt;

&lt;p&gt;The intuition is thus: why waste training steps on transitions the agent already understands well? Focus on the hard ones. Replay the failures. Learn from mistakes. Push yourself. &lt;em&gt;insert 'Just Do It!' meme here&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkqhli4u8oja8fj1yrxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkqhli4u8oja8fj1yrxy.png" alt="Just Do IT!" width="792" height="1211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To prevent this biased sampling from corrupting the gradient, PER applies importance sampling (IS) weights that mathematically correct for the non-uniform distribution. A parameter called beta controls how aggressively this correction is applied, and is annealed from a low value (0.4) toward 1.0 over training.&lt;/p&gt;

&lt;p&gt;Hessel et al.'s 2018 Rainbow paper tested each component's contribution by removing them one at a time. PER and multi-step returns were the two most impactful. Remove either one and performance dropped the most. This result, measured on Atari, became the received wisdom in the DRL Gaming community: PER is essential.&lt;/p&gt;

&lt;p&gt;And for some reason, nobody asked whether that ranking holds on tasks that look nothing like Atari.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Bug I Found First
&lt;/h2&gt;

&lt;p&gt;Before I could even evaluate PER properly, I had to fix a misconfiguration that most multi-environment setups will hit without realising.&lt;/p&gt;

&lt;p&gt;PER's beta parameter is annealed over &lt;code&gt;beta_anneal_steps&lt;/code&gt; gradient steps. The default values in most implementations are calibrated for single-environment training where roughly one gradient step happens per episode. My setup runs 2048 parallel environments with 4 gradient steps per global step. That's approximately 8,192 gradient steps per episode.&lt;/p&gt;



&lt;p&gt;The result? With a &lt;code&gt;beta_anneal_steps&lt;/code&gt; of 100,000 (a common default), beta reached 1.0 by episode ~12. Not 12,000. Yes you read that right, twelve. The IS correction was fully engaged before the agent had learned anything at all. The training wheels came off before one foot was even on the pedal. For the remaining ~300,000 episodes of training, PER was running with maximum gradient suppression against priorities that were pure noise.&lt;/p&gt;

&lt;p&gt;Gradient norms confirmed it: they were approximately 4× lower than equivalent non-PER runs. The agent was being actively throttled.&lt;/p&gt;

&lt;p&gt;After identifying this, I recalibrated &lt;code&gt;beta_anneal_steps&lt;/code&gt; to 6,000,000 (covering ~300,000 episodes at the actual gradient-steps-per-episode rate) and ran again from scratch. The corrected run did show improvement over the non-PER baseline.&lt;/p&gt;

&lt;p&gt;So, PER fixed, job done, moving on? NOPE!&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixed PER Still Underperforms
&lt;/h2&gt;

&lt;p&gt;The corrected PER run outperformed the dueling+noisy baseline by a meaningful but modest margin. Not the dramatic improvement you'd expect from one of Rainbow's "top two components." The improvement was there, it just wasn't impressive.&lt;/p&gt;

&lt;p&gt;This raised a question for me. If PER barely helps without C51 (distributional output), what happens when C51 &lt;em&gt;is&lt;/em&gt; present? C51 fundamentally changes the nature of the TD error. In standard DQN, the TD error is a scalar: predicted Q minus target Q. PER uses this scalar as its priority signal. Simple, clean, well-defined.&lt;/p&gt;

&lt;p&gt;In C51, the "error" is a KL divergence between two probability distributions. It's not a scalar residual in the same sense. Most Rainbow implementations approximate a priority from this distributional loss, but it's exactly that: an approximation. If the priority signal is noisier in the distributional setting, PER is making sampling decisions on worse information while still applying the full IS correction penalty.&lt;/p&gt;

&lt;p&gt;The only way to test this was to run Rainbow with and without PER and compare directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Head-to-Head
&lt;/h2&gt;

&lt;p&gt;Full Rainbow (with PER) vs C51 without PER. Same architecture, same hyperparameters, same encoding, same hardware, same training seed. The only difference: PER on or off.&lt;/p&gt;

&lt;p&gt;Both models evaluated at the ep50K snapshot: 10 segments × 2,000 episodes (20,000 total per model), deterministic policy, seeds 0–19,999.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ueo886dy0kc0pez0bvc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ueo886dy0kc0pez0bvc.png" alt="Rainbow VS Per Removed" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;C51 without PER outperforms full Rainbow across every single metric. Not by a little. The weakest C51 segment (avg 31.47) far exceeds the strongest Rainbow segment (avg 22.91). There is zero overlap between the two distributions. This isn't noise. This is a structural difference.&lt;/p&gt;

&lt;p&gt;At the training level, C51 overtook Rainbow in record score around episode 153K and maintained the lead through the end of both runs. The final records: &lt;strong&gt;153&lt;/strong&gt; (C51 without PER) vs &lt;strong&gt;134&lt;/strong&gt; (full Rainbow with PER).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Removing PER didn't just fail to hurt. It was the single change that pushed the model from 134 to a world record of &lt;del&gt;153&lt;/del&gt; 156.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Why PER Hurts on Snake
&lt;/h2&gt;

&lt;p&gt;This result isn't random bad luck. There are structural reasons why PER is a poor fit for Snake, and they generalise to any task with similar properties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense rewards reduce TD error variance.&lt;/strong&gt; PER's priority mechanism works best when the replay buffer contains a mix of genuinely informative rare transitions and common boring ones. In sparse-reward environments (long Atari episodes, complex RPGs), most transitions carry little signal, and PER correctly surfaces the rare valuable ones. Snake hands out food frequently. The reward signal is dense. TD errors across transitions are relatively homogeneous. There isn't enough variance in transition informativeness for priority sampling to do meaningful work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel environments already ensure diversity.&lt;/strong&gt; One of PER's core benefits in single-environment training is making rare or unusual game states available for replay more often. With 2048 environments running simultaneously, the replay buffer is already populated with massively diverse experience at every step. The agent sees rare states regularly just from the volume of parallel play. PER's diversity benefit is structurally preempted by the parallelism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IS weight correction suppresses gradients.&lt;/strong&gt; The IS correction is mathematically necessary to prevent biased gradients, but it comes at a cost: it down-weights the very transitions PER most wants to learn from. In a dense-reward setting where TD errors are already relatively uniform, this correction may be net-harmful. You pay the gradient suppression overhead without the corresponding benefit of surfacing genuinely informative transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C51 makes PER's priority signal worse.&lt;/strong&gt; In standard DQN, the TD error is a clean scalar. In C51, the "error" is derived from a KL divergence between distributions, an approximation that may not faithfully represent which transitions are most informative in the distributional sense. PER is making sampling decisions on a noisier signal while still applying the full IS penalty.&lt;/p&gt;

&lt;p&gt;These four factors compound. Each one individually would weaken PER's contribution. Together, they explain why removing PER entirely produces a better model than including it.&lt;/p&gt;
&lt;h2&gt;
  
  
  This Isn't Just My Finding
&lt;/h2&gt;

&lt;p&gt;Pan et al. and Ivgi et al. have independently documented PER underperforming in dense-reward or high-parallelism settings. Both identify that PER's advantage is largest when rewards are sparse and TD errors vary substantially across transitions. This lends external validity to what I observed here and suggests the finding is not specific to Snake or to my implementation.&lt;/p&gt;

&lt;p&gt;The practical recommendation: before including PER in your setup, ask whether your task has sparse rewards and rare informative transitions. If it doesn't, PER's overhead (IS correction, priority tracking, beta calibration complexity) may outweigh its benefit. The fact that Hessel et al. found PER essential on Atari does not mean it's essential on your task.&lt;/p&gt;
&lt;h2&gt;
  
  
  Honest Caveats
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tested across multiple seeds.&lt;/strong&gt; The primary comparison shown above is from a single training seed, but the PER vs no-PER comparison has been tested across 5 seeds. The results are somewhat chaotic at the individual seed level, with some seeds showing a smaller gap and occasional flips. But the mean across all 5 seeds shows a positive effect from removing PER. The relative ranking holds on average, even if individual seeds can be noisy. This is consistent with the structural arguments above: PER's disadvantage on dense-reward tasks is systematic, not a seed-specific fluke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense-reward specific.&lt;/strong&gt; This finding is about PER on Snake, which is a dense-reward task with frequent food collection and relatively uniform state visitation. PER may still be valuable on sparse-reward, long-horizon tasks. The claim is not "PER is useless." The claim is "PER is not universally beneficial, and the conditions under which it helps are narrower than the literature implies."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beta calibration.&lt;/strong&gt; The PER run used the corrected beta annealing schedule. The comparison is against properly-configured PER, not the misconfigured version. The misconfiguration is documented because it's a real pitfall that anyone using PER in a multi-environment setup will hit, but the head-to-head result stands on the corrected run.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The ablation study continues. The PER finding is one piece of a larger investigation into how each Rainbow component contributes in a dense-reward, parallel-environment setting. The full ablation ladder, from standard DQN through full Rainbow, is being built one component at a time.&lt;/p&gt;

&lt;p&gt;If you've observed PER underperforming on dense-reward tasks, or if you have counterexamples where PER helped significantly despite frequent rewards, I'd like to hear about it in the comments.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;This work is part of ongoing research and the findings are planned to be submitted as a peer-reviewed paper.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;If you're new to this series:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-story__hidden-navigation-link"&gt;A CNN Grid Encoding for Snake AI That DOUBLES! the Best Published Score&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/stat_phantom" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" alt="stat_phantom profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/stat_phantom" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Stat Phantom
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Stat Phantom
                
              
              &lt;div id="story-author-preview-content-3548283" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/stat_phantom" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Stat Phantom&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" id="article-link-3548283"&gt;
          A CNN Grid Encoding for Snake AI That DOUBLES! the Best Published Score
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/deeplearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;deeplearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cnn"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cnn&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;



&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh" class="crayons-story__hidden-navigation-link"&gt;2 Lines of Code Saved 6.4x Memory on My Snake AI&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/stat_phantom" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" alt="stat_phantom profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/stat_phantom" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Stat Phantom
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Stat Phantom
                
              
              &lt;div id="story-author-preview-content-3594536" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/stat_phantom" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Stat Phantom&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 1&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh" id="article-link-3594536"&gt;
          2 Lines of Code Saved 6.4x Memory on My Snake AI
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/deeplearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;deeplearning&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Peer-Reviewed
&lt;/h3&gt;

&lt;p&gt;Hessel et al. (2018) - "Rainbow: Combining Improvements in Deep Reinforcement Learning" - AAAI 2018. &lt;a href="https://doi.org/10.1609/aaai.v32i1.11796" rel="noopener noreferrer"&gt;DOI: 10.1609/aaai.v32i1.11796&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Schaul et al. (2016) - "Prioritized Experience Replay" - ICLR 2016. &lt;a href="https://arxiv.org/abs/1511.05952" rel="noopener noreferrer"&gt;arXiv: 1511.05952&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bellemare et al. (2017) - "A Distributional Perspective on Reinforcement Learning" - ICML 2017. &lt;a href="https://arxiv.org/abs/1707.06887" rel="noopener noreferrer"&gt;arXiv: 1707.06887&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sebastianelli et al. (2021) - "A Deep Q-Learning based approach applied to the Snake game" - 29th Mediterranean Conference on Control and Automation (MED). &lt;a href="https://doi.org/10.1109/MED51440.2021.9480232" rel="noopener noreferrer"&gt;DOI: 10.1109/MED51440.2021.9480232&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>cnn</category>
    </item>
    <item>
      <title>2 Lines of Code Saved 6.4x Memory on My Snake AI</title>
      <dc:creator>Stat Phantom</dc:creator>
      <pubDate>Fri, 01 May 2026 06:36:43 +0000</pubDate>
      <link>https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh</link>
      <guid>https://dev.to/stat_phantom/2-lines-of-code-saved-64-memory-on-my-snake-ai-3dhh</guid>
      <description>&lt;p&gt;Greetings all! In my &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p"&gt;previous post&lt;/a&gt; I covered Binary Plane Encoding, a 3-channel grid representation for Snake that doubled the best published score. Three binary channels: head, body, apple. For details check my previous post.&lt;/p&gt;

&lt;p&gt;But there was a fourth channel I left out. Direction. The snake's current heading, encoded as a uint8 (0 = up, 1 = right, 2 = down, 3 = left), is painted uniformly across a 20×20 plane due to matrix shape requirements. That's 400 elements carrying exactly 2 bits of information. A 1,600× overhead at the channel level.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnb8ay2ai377xpuazjvy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnb8ay2ai377xpuazjvy5.png" alt="Grid of all 2's" width="523" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worse, that one integer channel with its 2 bits was blocking the entire state from being bit-packed. The other three grid channels are binary, meaning they &lt;em&gt;could&lt;/em&gt; be packed at 1 bit per element. But the direction channel with its &lt;em&gt;scoffs&lt;/em&gt; 2 bits, can't. So the replay buffer sees the state as uint8 instead of binary. One channel, 2 bits, holding back one more step of memory optimisation, forcing 1,600 bytes per state instead of 250 (20 × 20 grid, ×4 channels, 1 byte per channel = 1,600 vs 20 × 20 grid, ×5 channels, 1 bit per element / 8 = 250).&lt;/p&gt;

&lt;p&gt;This follow-up post is about fixing that, and the pitfalls along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Attempt
&lt;/h2&gt;

&lt;p&gt;Four cardinal directions. Two bits encode four states. So the intuitive replacement is two binary channels instead of one integer channel: one bit for North/South, one bit for East/West. Compact, geometric, obvious.&lt;/p&gt;

&lt;p&gt;Except it doesn't work. Walk through it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwr0amhykrkvdoqoqsdq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwr0amhykrkvdoqoqsdq4.png" alt="NESW Diagram" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;North and West both map to 0,0 - &lt;strong&gt;Collision&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The failure is subtle because the scheme &lt;em&gt;seems&lt;/em&gt; right. Four directions, four possible bit combinations, should be a clean fit. But the scheme tries to answer "is there a north/south component?" and "is there an east/west component?" Cardinal movement is strictly one-dimensional. The perpendicular component is always exactly zero. What does the E/W bit say when the snake is moving north? It's not moving east. It's also not moving west. Both map to 0. "Not moving east" is identical to "not moving west" in a single bit.&lt;/p&gt;

&lt;p&gt;Two bits should be enough for four directions. They are. Just not &lt;em&gt;those&lt;/em&gt; two bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask Better Questions
&lt;/h2&gt;

&lt;p&gt;The collision happens because the N/S + E/W scheme asks the wrong questions for cardinal movement. The fix isn't more bits. It's better questions.&lt;/p&gt;

&lt;p&gt;The correct encoding uses two bits derived geometrically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Axis bit:&lt;/strong&gt; which axis is the snake travelling along? (0 = vertical, 1 = horizontal)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign bit:&lt;/strong&gt; which direction on that axis? (0 = negative, 1 = positive)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pkvnvgzxo3b1epiui0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pkvnvgzxo3b1epiui0a.png" alt="NESW Fixed" width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All four directions get unique codes. The axis bit answers "which axis?" and the sign bit answers "which end?" Both questions always have exactly one answer for cardinal movement. No ambiguity, no collisions. The specific sign convention (whether north is positive or negative) doesn't matter as long as it's internally consistent. The CNN will learn whatever mapping you give it.&lt;/p&gt;

&lt;p&gt;The first attempt was asking the wrong questions. Once you ask the right ones, two bits is plenty.&lt;/p&gt;

&lt;p&gt;For anyone wondering about diagonal games (8 directions), the axis + sign scheme breaks because a diagonal is on both axes simultaneously. The general solution there is a 4-channel one-hot: one binary plane per cardinal direction, with two planes active for a diagonal. But for Snake, cardinal-only, the 2-channel scheme is the right choice. Don't build the generality you don't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory Maths
&lt;/h2&gt;

&lt;p&gt;This is where the change pays off. The state goes from &lt;code&gt;(4, 20, 20)&lt;/code&gt; with one integer channel to &lt;code&gt;(5, 20, 20)&lt;/code&gt; with all binary channels. Yes, adding a channel saves memory. That sounds backwards but the maths checks out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (4-channel, uint8 storage):&lt;/strong&gt; 4 × 20 × 20 = 1,600 elements at 1 byte each = 1,600 bytes per state. A 1-million-transition replay buffer (storing both state and next state): &lt;strong&gt;3.2 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (5-channel, binary bit-packed):&lt;/strong&gt; 5 × 20 × 20 = 2,000 elements. Every value is now 0 or 1, so each element can be packed at 1 bit, 8 elements per byte. ⌈2,000 / 8⌉ = 250 bytes per state. The same buffer: &lt;strong&gt;500 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.4× reduction.&lt;/strong&gt; Adding one channel, removing 2.7 GB.&lt;/p&gt;

&lt;p&gt;To put this in perspective: the grid encoding stored naively as float32 (before any compression) would be 6,400 bytes per state, or 12.8 GB for a 1M-transition buffer. The first post's uint8 storage cut that to 3.2 GB (4× reduction). This post's binary bit-packing cuts it again to 500 MB. Across both changes, that's a &lt;strong&gt;25.6× total reduction&lt;/strong&gt; from the uncompressed float32 starting point.&lt;/p&gt;

&lt;p&gt;And compared to the pixel-based approaches from the first post? Wei et al.'s RGB inputs would need approximately 49 GB for the same buffer. Binary Plane Encoding with binary cardinal directions brings that to 500 MB. Nearly a &lt;strong&gt;98× difference&lt;/strong&gt;. A 1-million-transition replay buffer now fits comfortably in the VRAM of a gaming laptop, hell, it fits in some EPYC CPU caches (AMD's Genoa-X packs up to 1,152 MB of L3). With pixel inputs, it wouldn't fit on most workstations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Lines of Code
&lt;/h2&gt;

&lt;p&gt;The implementation change is in &lt;code&gt;snake_cnn_env.py&lt;/code&gt;. Replace the single integer direction plane with two binary planes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: one integer channel
# grid[3] = self._direction  # 0, 1, 2, or 3
&lt;/span&gt;
  &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_direction&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# axis: 0=vertical, 1=horizontal
&lt;/span&gt;  &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_direction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# sign: 0=negative, 1=positive
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Update &lt;code&gt;input_channels&lt;/code&gt; from 4 to 5 in the model config. Done. We now store 5 channels instead of 4, but each channel is 1 bit instead of 8. One extra channel, massively less storage.&lt;/p&gt;

&lt;p&gt;One real cost: changing &lt;code&gt;input_channels&lt;/code&gt; changes the shape of the first convolutional weight tensor. Existing checkpoints can't be loaded into a 5-channel model. This requires a fresh training run, so schedule the change at a natural break point, not mid-experiment.&lt;/p&gt;
&lt;h2&gt;
  
  
  torch.unpackbits Doesn't Exist
&lt;/h2&gt;

&lt;p&gt;The CPU side of bit-packing is trivial. &lt;code&gt;np.packbits&lt;/code&gt; and &lt;code&gt;np.unpackbits&lt;/code&gt; have existed in NumPy since 2010. Pack on write, unpack on read. Done.&lt;/p&gt;

&lt;p&gt;So just implement it on the GPU side right? WRONG. The natural PyTorch equivalent would be &lt;code&gt;torch.unpackbits&lt;/code&gt;, which... doesn't exist? The function is absent from the stable API entirely, and importing it raises an &lt;code&gt;AttributeError&lt;/code&gt;. This is a genuine gap in PyTorch that anyone implementing binary storage on CUDA will hit.&lt;/p&gt;

&lt;p&gt;The community workaround I found uses bitmasks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;unpacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;flip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This works. It preserves the original bit values, converts them to binary via &lt;code&gt;.bool().int()&lt;/code&gt;, and flips the bit order to match MSB-first convention. Four operations, correct output.&lt;/p&gt;

&lt;p&gt;But I don't need to preserve the original mask values, I just need 0s and 1s. I thought I could do better, and I wouldn't be a programmer if I didn't try for no other reason except... &lt;em&gt;shrugs&lt;/em&gt; I wanted to?&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;shifts&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;packed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;unpacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;packed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shifts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# (B, packed_size, 8)
&lt;/span&gt;&lt;span class="n"&gt;unpacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unpacked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;n_elems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# drop padding bits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each packed byte is broadcast against 8 shift values &lt;code&gt;[7, 6, 5, 4, 3, 2, 1, 0]&lt;/code&gt;, right-shifting to move each successive bit into the least significant position. Bitwise &amp;amp; with 1 isolates it. Two operations instead of four. No &lt;code&gt;.bool().int()&lt;/code&gt; needed because &lt;code&gt;&amp;gt;&amp;gt; shift &amp;amp; 1&lt;/code&gt; always yields binary output directly. No &lt;code&gt;.flip()&lt;/code&gt; needed because the descending shift range already produces MSB-first order. Fewer intermediate tensors in VRAM during sampling.&lt;/p&gt;

&lt;p&gt;The mask approach also has a shape bug: it's written for a 1D input (flat array of bytes) and breaks on a batched 2D input &lt;code&gt;(B, packed_size)&lt;/code&gt;. The shift approach handles batched GPU sampling correctly from the start.&lt;/p&gt;

&lt;p&gt;Both are fully device-resident with no CPU-GPU transfer. But two operations beats four, and not allocating intermediate tensors matters when batch size and state shape are large. Will reducing two ops make a difference? Probably not, but I saw the OPportunity and took it. And yes, I said that just for the joke.&lt;/p&gt;

&lt;p&gt;So, two lines of code changed the state representation to allow bit-packing and saved a lot of storage with no loss of data.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is part of an ongoing series building Rainbow DQN incrementally and measuring each component on Snake. The state representation work runs in parallel to the algorithm comparison. It doesn't change which Rainbow components help or hurt, but a 6.4× memory reduction means larger buffers, more parallel environments, or training on hardware that previously couldn't fit the buffer.&lt;/p&gt;

&lt;p&gt;The algorithm results are the next post.&lt;/p&gt;

&lt;p&gt;If you've hit the &lt;code&gt;torch.unpackbits&lt;/code&gt; gap yourself, or found a cleaner solution than bitwise shifts for GPU-side bit unpacking, I'd like to hear about it in the comments.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;This work is part of ongoing research and the findings are planned to be submitted as a peer-reviewed paper.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;If you missed the first post in this series:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-story__hidden-navigation-link"&gt;A CNN Grid Encoding for Snake AI That DOUBLES! the Best Published Score&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/stat_phantom" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" alt="stat_phantom profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/stat_phantom" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Stat Phantom
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Stat Phantom
                
              
              &lt;div id="story-author-preview-content-3548283" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/stat_phantom" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896851%2F7aa0dd7a-fa35-4366-843f-b692329de6cf.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Stat Phantom&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" id="article-link-3548283"&gt;
          A CNN Grid Encoding for Snake AI That DOUBLES! the Best Published Score
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/deeplearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;deeplearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cnn"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cnn&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>A CNN Grid Encoding for Snake AI That DOUBLES! the Best Published Score</title>
      <dc:creator>Stat Phantom</dc:creator>
      <pubDate>Sat, 25 Apr 2026 04:39:23 +0000</pubDate>
      <link>https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p</link>
      <guid>https://dev.to/stat_phantom/a-cnn-grid-encoding-for-snake-ai-that-doubles-the-best-published-score-245p</guid>
      <description>&lt;p&gt;A traditional Snake game grid has only 4 states each grid point can be in: empty, head, body, or apple. And for some reason every published Snake AI paper either throws away spatial information by condensing the game state into a handful of hand-picked numbers, or buries entity identity under layers of raw pixel data that the network has to untangle. Incredibly wasteful.&lt;/p&gt;

&lt;p&gt;The solution? Binary Plane Encoding. Using it, a CNN-based model reached a record score of 125 on a 20×20 grid in 2.5 hours on a single RTX 2070, doubling the best published result of 62 (even the average is consistently above this record). This post explains the encoding, why it works, and explores why nobody in the Snake DRL space has tried it before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Camps
&lt;/h2&gt;

&lt;p&gt;The published literature on deep reinforcement learning for Snake spans 2018 to 2025 and splits into two approaches to state representation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camp one: hand-crafted feature vectors.&lt;/strong&gt; Sebastianelli et al. (2021) and Kommalapati et al. (2025) both use 11 binary features fed to a fully-connected network. Three danger flags (is there a wall or body segment directly ahead, to the left, to the right), four direction flags (which way is the snake currently heading), and four food-relative flags (is the apple above, below, left, right of the head). The network receives a pre-digested summary of the game state. It never sees the grid. It never learns spatial relationships. A human decided what matters and encoded that decision directly into the input.&lt;/p&gt;

&lt;p&gt;This works well. Sebastianelli achieved a best score of 62 on a 20×20 grid with vanilla DQN and this 11-feature representation, and uses very little resources... at least initially, but then a hard ceiling is quickly reached. The network cannot discover and learn spatial patterns because it never sees the spatial layout. And the features themselves are Snake-specific. Those 11 binary values encode what a Snake expert thinks matters. They would be meaningless for any other game. If you want an agent that can generalise beyond a single environment, this is a dead end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camp two: raw pixels.&lt;/strong&gt; Wei et al. (2018) and Tushar &amp;amp; Siddique (2022) both train from screenshots. Wei uses 64×64 RGB frames stacked four deep, giving 64×64×12 input. Tushar converts to binary (any non-zero pixel becomes 1) at 84×84, also four frames stacked, giving 84×84×4.&lt;/p&gt;

&lt;p&gt;The pixel approach is game-agnostic, which is its strength. But the cost is significant. Tushar's binary encoding collapses head, body, and apple into a single value. In any individual frame, every occupied cell looks identical. The agent can only figure out what's what by watching how things move across four stacked frames: food stays still, the snake moves. A single frame on its own contains zero identity information. Wei's RGB encoding preserves colour and therefore identity, but at the cost of massive input dimensionality and redundant spatial resolution (64×64 pixels to represent a 20×20 logical grid).&lt;/p&gt;

&lt;p&gt;Both pixel approaches were tested on 12×12 grids, reaching best scores of 17 (Wei) and 20 (Tushar). Neither has been applied to 20×20.&lt;/p&gt;

&lt;p&gt;Beyond the peer-reviewed literature, informal projects show similar patterns. A supervised learning approach on GitHub (Huynh, 2020) uses 7 hand-crafted features with a Keras network and reaches a best of 46, average 22 on 20×20. A Medium article (Schoberg, 2020) compares deterministic algorithms rather than learned policies, reaching 67 on 20×20 with a collision-avoiding shortest-path algorithm (no neural network involved at all).&lt;/p&gt;

&lt;p&gt;Across all of it, every neural network approach uses either compressed feature vectors or raw pixel grids.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me. Multi-channel grid encoding is not a new idea. It is the standard state representation in board game AI.&lt;/p&gt;

&lt;p&gt;AlphaZero (Silver et al., 2018) represents chess, Go, and Shogi as multi-channel binary planes. Each piece type, colour, and game-state feature gets its own channel. The network receives a spatial tensor where every channel encodes a different semantic category of information about the board. MuZero extends this. The representation is well-established, well-understood, and has been proven at the highest levels of game AI.&lt;/p&gt;

&lt;p&gt;Snake fundamentally runs on a grid with set positions entities can occupy. It mirrors the exact class of problem where channel-per-entity encoding has proven effective, yet no published Snake DRL paper, and no self-published project I have found, attempts this representation. (Although this not appearing in published papers isn't surprising to me. As someone who this month had to go through over 2,100 papers, most papers just follow pre-existing trends.)&lt;/p&gt;

&lt;p&gt;All of the pre-existing Snake DRL literature either pre-computes features and discards spatial representation, or captures raw pixels and forces the network to spend capacity on visual processing before it can even begin to learn the game.&lt;/p&gt;

&lt;p&gt;This is the gap. Not a novel encoding technique, but an established one applied to a domain that has ignored it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Encoding
&lt;/h2&gt;

&lt;p&gt;The state representation is a 20×20×3 binary tensor. Three channels, each covering the full grid:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel 0 (head):&lt;/strong&gt; 1 at the head position, 0 everywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel 1 (body):&lt;/strong&gt; 1 at each body segment position, 0 elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel 2 (apple):&lt;/strong&gt; 1 at the apple position, 0 everywhere else.&lt;/p&gt;

&lt;p&gt;Every value is exactly 0 or 1. A single frame provides complete, unambiguous game state. What is the head, where is the body, where is the food. No temporal stacking required. No entity disambiguation through motion inference. No feature engineering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9aay7yiqtp8qa9vzz7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9aay7yiqtp8qa9vzz7r.png" alt="Visual Representation of Encoding Layers" width="567" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The construction from game state is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body_positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apple_pos&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Channel 0: head
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head_pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;head_pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Channel 1: body
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body_positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Channel 2: apple
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apple_pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;apple_pos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces 20×20×3 = 1,200 values per state. Compare that to the pixel approaches: Tushar's binary encoding produces 84×84×4 = 28,224 values (23× larger), and Wei's RGB produces 64×64×12 = 49,152 values (41× larger). The grid encoding captures strictly more semantic information in a fraction of the space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjsxg3bak0moekn59c3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjsxg3bak0moekn59c3u.png" alt="Memory Usage Comparison" width="713" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The information hierarchy makes this concrete:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Entity identity per frame&lt;/th&gt;
&lt;th&gt;Full spatial layout&lt;/th&gt;
&lt;th&gt;Game-agnostic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary Plane Encoding (this model)&lt;/td&gt;
&lt;td&gt;Yes, perfect&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partial (any grid game)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGB pixels (Wei et al.)&lt;/td&gt;
&lt;td&gt;Yes, via colour&lt;/td&gt;
&lt;td&gt;Approximate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary pixels (Tushar)&lt;/td&gt;
&lt;td&gt;No (needs 4 frames)&lt;/td&gt;
&lt;td&gt;Approximate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature vectors (Sebastianelli)&lt;/td&gt;
&lt;td&gt;Yes, pre-computed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (Snake-specific)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only representation in the reviewed literature that provides perfect entity identity, full spatial layout, and game-agnostic structure without additional processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CNN Architecture
&lt;/h2&gt;

&lt;p&gt;The model processing this encoding is deliberately compact:&lt;/p&gt;

&lt;p&gt;Two convolutional layers with 32 and 64 channels respectively, 3×3 kernels with same padding, followed by a single MaxPool2d that halves the spatial dimensions from 20×20 to 10×10. Two dense layers of 512 and 256 units. Mish activation throughout.&lt;/p&gt;

&lt;p&gt;The network also uses a dueling architecture (separate value and advantage streams) and NoisyLinear layers replacing standard linear layers in the fully-connected head, providing learned exploration noise instead of epsilon-greedy.&lt;/p&gt;

&lt;p&gt;This is not a large network. It doesn't need to be. The compact input representation means the convolutional backbone doesn't need depth. Two 3×3 layers with a single pooling stage are sufficient to capture the spatial relationships that matter in a 20×20 grid: proximity to walls, body segment density in nearby regions, and relative food position. The encoding has already done the hard work of structuring the information. The CNN just needs to read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Previous Records
&lt;/h2&gt;

&lt;p&gt;The meaningful comparisons are grouped by grid size, since raw scores are not directly comparable across different board dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  20×20 Grid
&lt;/h3&gt;

&lt;p&gt;The only published peer-reviewed result on a 20×20 Snake grid is Sebastianelli et al. (2021). They used an MLP with 11 hand-crafted binary features and vanilla DQN, testing 13 hyperparameter configurations across evaluation runs. Their best single score was &lt;strong&gt;62&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This work, using Binary Plane Encoding with a CNN and Rainbow DQN (incorporating C51 distributional output, dueling architecture, noisy exploration, prioritised replay, and 3-step returns), achieved a record of &lt;strong&gt;125&lt;/strong&gt; on the same grid. over double.&lt;/p&gt;

&lt;p&gt;This isn't a cherry-picked peak. Across 55,000 episodes of sustained training, the rolling average holds between 60 and 70, and the median between 64 and 74. Sebastianelli's best single game of 62 sits below this model's average. The p10 floor (the score that 90% of episodes exceed) holds around 30, meaning even the worst games routinely outperform most published baselines. The p90 reaches into the high 90s, with individual episodes regularly breaking 100. Training to this point took approximately 2.5 hours on a single RTX 2070.&lt;/p&gt;

&lt;p&gt;An important caveat: this is not an encoding-only comparison. The improvement comes from changes across multiple axes simultaneously. State representation (grid encoding vs feature vector), architecture (CNN vs MLP), algorithm (Rainbow DQN vs vanilla DQN), and training scale (2048 parallel environments vs a smaller setup). The encoding is the enabling change that made the architecture and training scale feasible on consumer hardware, but the doubling should not be attributed to the encoding alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  12×12 Grid
&lt;/h3&gt;

&lt;p&gt;Direct score comparison across grid sizes doesn't work because a 12×12 grid has a maximum possible score of approximately 141 food items versus approximately 399 for 20×20. Board coverage (score divided by maximum possible) provides a normalised metric:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;th&gt;Best Score&lt;/th&gt;
&lt;th&gt;Board Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wei et al. (2018)&lt;/td&gt;
&lt;td&gt;12×12&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;~12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tushar &amp;amp; Siddique (2022)&lt;/td&gt;
&lt;td&gt;12×12&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;~14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sebastianelli et al. (2021)&lt;/td&gt;
&lt;td&gt;20×20&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;~16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;This model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20×20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;125&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~31%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap persists across normalisation. At 31% board coverage, this approach covers roughly double the grid fraction of the nearest published result and more than double the pixel-based CNN approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Informal results (not peer-reviewed)
&lt;/h3&gt;

&lt;p&gt;For completeness: a supervised learning project (Huynh, 2020) on 20×20 achieved a best of 46, and a deterministic shortest-path algorithm (Schoberg, 2020) reached 67 on 20×20. The latter is not a learned policy. Neither is peer-reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Works
&lt;/h2&gt;

&lt;p&gt;The encoding's advantage operates on two levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Information quality.&lt;/strong&gt; The network receives exactly the information it needs to play Snake, in a spatial format that CNNs are designed to process, with zero noise or redundancy. Each channel answers one question: where is the head, where is the body, where is the food. There is no ambiguity to resolve, no motion to infer, no irrelevant visual detail to filter out.&lt;/p&gt;

&lt;p&gt;Pixel inputs have a problem where the network must first learn to segment the image (such as determining what's the snake's body and what's the background). After this it then needs to learn to interpret the spatial relationships between the segments. With Binary Plane Encoding, this segmentation is pre-constructed, leaving the network to devote its entire capacity to learning the actual game instead of learning how to see in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Information density.&lt;/strong&gt; At 1,200 values per state stored as uint8, a replay buffer holding 1,000,000 transitions fits comfortably in approximately 1.6GB of VRAM. This made a GPU-resident replay buffer and 2048 parallel environments possible on a single RTX 2070 with 8GB of VRAM.&lt;/p&gt;

&lt;p&gt;For comparison, storing Tushar's 84×84×4 binary inputs at the same buffer capacity would need approximately 28GB. Wei's 64×64×12 RGB inputs would need approximately 49GB. Neither fits on consumer hardware. You would need multiple high-end GPUs or cloud infrastructure to achieve the same training scale with pixel-based inputs.&lt;/p&gt;

&lt;p&gt;The compact encoding didn't just improve information quality. It made the training infrastructure possible. 2048 parallel environments with a GPU-resident buffer meant the replay buffer reached useful diversity faster, the distributional RL gradient signal had richer data to work with, and the agent surpassed all previous records before reaching 100,000 training episodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Caveats
&lt;/h2&gt;

&lt;p&gt;This encoding is a &lt;strong&gt;privileged state representation&lt;/strong&gt;. The agent receives information extracted directly from the game's internal data structures: exact head position, exact body segment positions, exact apple position. A human player has access to the same logical information through visual perception, but this agent receives it pre-structured without any perceptual processing.&lt;/p&gt;

&lt;p&gt;The model plateaued at 125 (over 50,000 simulations without it budging), but a subsequent run using a variant algorithm has already broken that record, so we know this isn't the ceiling for the encoding. The more interesting question is whether pixel-based approaches could ever reach these scores given enough compute. Theoretically yes, but whether it's achievable in practice is unknown. Imperfections in the visual pipeline may compound through training, but that hypothesis hasn't been tested and the performance cost of segmentation quality on Snake hasn't been quantified. Whether the gap is recoverable or structural is an open question and one worth testing properly. If you take this on, I'd love to see what you find.&lt;/p&gt;

&lt;p&gt;Cross-paper comparisons to Sebastianelli et al. and the pixel-based approaches should be read with the privileged state in mind. The improvement reflects the combined effect of encoding quality, architecture, algorithm, and training scale. Isolating each factor's individual contribution is the purpose of the ablation study this encoding supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Binary Plane Encoding is the foundation for a systematic ablation study on Rainbow DQN applied to Snake. The study adds one component at a time (Double DQN, noisy exploration, dueling architecture, prioritised experience replay, C51 distributional output), measuring each component's individual contribution in a dense-reward, vectorised-environment setting.&lt;/p&gt;

&lt;p&gt;Early results have already produced some surprises about which Rainbow components help and which ones hurt on a task like Snake. That is the next post.&lt;/p&gt;

&lt;p&gt;If you have experience with alternative state representations for grid-based game AI, or if you have seen Binary Plane Encoding applied to Snake in work I haven't found, I'd genuinely like to hear about it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This work is part of ongoing research and the findings are planned to be submitted as a peer-reviewed paper.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Peer-Reviewed
&lt;/h3&gt;

&lt;p&gt;Sebastianelli et al. (2021) - "A Deep Q-Learning based approach applied to the Snake game" - 29th Mediterranean Conference on Control and Automation (MED). &lt;a href="https://doi.org/10.1109/MED51440.2021.9480232" rel="noopener noreferrer"&gt;DOI: 10.1109/MED51440.2021.9480232&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kommalapati et al. (2025) - "Building an AI Snake Powered by Deep Reinforcement Learning and Deep Q-Learning" - IEEE 7th International Symposium on Advanced Electrical and Communication Technologies (ISAECT). &lt;a href="https://doi.org/10.1109/ISAECT68904.2025.11318716" rel="noopener noreferrer"&gt;DOI: 10.1109/ISAECT68904.2025.11318716&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wei et al. (2018) - "Autonomous Agents in Snake Game via Deep Reinforcement Learning" - IEEE International Conference on Agents (ICA), Singapore. &lt;a href="https://doi.org/10.1109/AGENTS.2018.8460004" rel="noopener noreferrer"&gt;DOI: 10.1109/AGENTS.2018.8460004&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tushar &amp;amp; Siddique (2022) - "A Memory Efficient Deep Reinforcement Learning Approach For Snake Game Autonomous Agents" - IEEE 16th International Conference on Application of Information and Communication Technologies (AICT). &lt;a href="https://doi.org/10.1109/AICT55583.2022.10013603" rel="noopener noreferrer"&gt;DOI: 10.1109/AICT55583.2022.10013603&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Silver et al. (2018) - "A general reinforcement learning algorithm that masters chess, shogi, and Go through self-play" - Science 362, 1140-1144. &lt;a href="https://doi.org/10.1126/science.aar6404" rel="noopener noreferrer"&gt;DOI: 10.1126/science.aar6404&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Informal / Community Work
&lt;/h3&gt;

&lt;p&gt;Huynh (2020) - Supervised learning Snake AI. &lt;a href="https://github.com/TimHuynh0905/snake-ai" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Schoberg (2020) - Deterministic algorithms for Snake. &lt;a href="https://medium.com/analytics-vidhya/playing-snake-with-ai-2ea68f0e914a" rel="noopener noreferrer"&gt;Medium Article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>cnn</category>
    </item>
  </channel>
</rss>
