<?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: Tomer Levy</title>
    <description>The latest articles on DEV Community by Tomer Levy (@tomerl1).</description>
    <link>https://dev.to/tomerl1</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F183062%2F2c19b4cd-eddd-4555-9a08-ce50ea0ae363.jpeg</url>
      <title>DEV Community: Tomer Levy</title>
      <link>https://dev.to/tomerl1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomerl1"/>
    <language>en</language>
    <item>
      <title>i know rust</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Mon, 13 Apr 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/i-know-rust-1jpi</link>
      <guid>https://dev.to/tomerl1/i-know-rust-1jpi</guid>
      <description>&lt;p&gt;In part 1 I wrote that I wanted to learn Rust by building something real. That I'd use AI as a mentor. That the measure of success was whether, at the end of it, I could honestly say "I know Rust."&lt;/p&gt;

&lt;p&gt;Nine posts later, I can.&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%2F2p2dx0p4bgzvtqfhhnx5.jpg" 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%2F2p2dx0p4bgzvtqfhhnx5.jpg" alt="I know Rust" width="666" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not in the "I could pass a Rust interview tomorrow" sense. In the "I now catch JavaScript bugs by thinking like the borrow checker" sense. The moment it actually landed was debugging client-side prediction in &lt;a href="https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-8-the-algorithm-that-saved-online-gaming-573e"&gt;part 8&lt;/a&gt;. I reassigned a buffer reference in TypeScript and my brain said &lt;em&gt;no, that's a dangling borrow.&lt;/em&gt; JavaScript doesn't have borrows. But the pattern-recognition was already there.&lt;/p&gt;

&lt;p&gt;That's the point. Not syntax. Intuitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  the numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18 days&lt;/strong&gt; from first commit to last. 15 of those were actual working days (nights actually).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;74 commits&lt;/strong&gt; across the server, client, shared protocol, and docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,295 lines of Rust&lt;/strong&gt; across 10 modules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,220 lines of TypeScript&lt;/strong&gt; across 16 client files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 journal entries&lt;/strong&gt; that became 8 technical blog posts (plus this one).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not huge numbers. But every line was fought for. Every &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; got picked apart. Every &lt;code&gt;match&lt;/code&gt; on a &lt;code&gt;GameState&lt;/code&gt; enum came from thinking about what should exist in which phase. The code is small because I refused to write code I didn't understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  what rust actually taught me
&lt;/h2&gt;

&lt;p&gt;Not the syntax. I can google syntax. Three things moved into my bones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The borrow checker as a thinking tool.&lt;/strong&gt; Ownership isn't just a compiler thing. It's a way of asking "who is allowed to change this, and when?" That question turns out to be useful in every language. The stale array reference bug from part 8 is the clearest example. A module held a reference to an array that had been reassigned out from under it. Classic dangling reference. Rust would have caught it at compile time. My JS brain caught it too, because I'd been trained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enums-with-data as the default way to model state.&lt;/strong&gt; In TypeScript you'd use a discriminated union. Close, but nothing stops you from accessing a field that only exists in one variant. In Rust, &lt;code&gt;match&lt;/code&gt; forces you to handle every case, and fields that don't exist in a variant literally don't exist. Once you've modeled state this way, going back feels sloppy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"If it compiles, it's probably right" is real.&lt;/strong&gt; I didn't believe it when people said it. Now I do. Not because the compiler is magic, but because by the time you've satisfied it, you've already answered every question about lifetimes, ownership, and error handling that the code implies. The kind of bug left over is usually a logic bug, not a wiring bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  the AI-mentor experience
&lt;/h2&gt;

&lt;p&gt;Claude wrote zero lines of the production code. That was the rule. Claude explained concepts, proposed architectures, reviewed code, debugged at the wire. I typed. (I cheated a little and had Claude write some tests when I got lazy.)&lt;/p&gt;

&lt;p&gt;What worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concept explanations in JS terms.&lt;/strong&gt;"An &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; is the Rust equivalent of a shared mutable reference, except you have to be explicit about both sharing and mutation." That framing unlocked me faster than any Rust Book chapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging heuristics.&lt;/strong&gt; In part 6, when I saw ghost explosions, Claude didn't guess. It suggested "start at the wire." Turned out to be a visual rendering quirk, not a data bug. The investigation structure was the real value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec writing.&lt;/strong&gt; ADRs, phase plans, protocol definitions. Claude is relentless about writing down the "why" before the "what." I built a &lt;code&gt;docs/&lt;/code&gt; folder I can still read 18 days later and understand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What didn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing idiomatic Rust on the first try.&lt;/strong&gt; Claude would suggest imperative loops that worked. They passed tests. They were also not how Rust people write Rust. I had to learn the idioms by reading the standard library and asking "is there a shorter way?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replacing understanding.&lt;/strong&gt; Any bug I fixed by pasting into Claude and pasting the answer back came back as a different bug a week later. The fixes that stuck were the ones I understood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loop that worked was: Claude proposes, I question, we refine, I write, we review. Nine journal entries captured that loop in real time. The journal became the blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  the repo is open
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tomerlevy1/bomberman-multiplayer" rel="noopener noreferrer"&gt;github.com/tomerlevy1/bomberman-multiplayer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's not a tutorial. It's not polished. But it has all the commits in sequence, the ADRs, the journal entries, the specs, and the mentorship rules I wrote for Claude. If you want to see what "AI-assisted learning" actually looked like for one JS dev learning Rust, it's all there.&lt;/p&gt;

&lt;p&gt;The sprites aren't. Those were Atomic Bomberman's and I can't share them. The placeholder is well-documented — bring your own tile sheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  what's next
&lt;/h2&gt;

&lt;p&gt;I don't know yet. The game is playable. It works on a LAN. Four players can connect and fight it out in the four corners, and there's no hard cap on connections. It has all the mechanics a round of Bomberman needs. I could add bots (there's a design note for FSM-driven virtual clients in the ADRs), add proper matchmaking, port the client to native, or start something else in Rust since I now have a language I enjoy.&lt;/p&gt;

&lt;p&gt;The honest answer is "we'll see." I set out to learn Rust. I learned Rust.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>learning</category>
      <category>rust</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>the algorithm that saved online gaming</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-8-the-algorithm-that-saved-online-gaming-573e</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-8-the-algorithm-that-saved-online-gaming-573e</guid>
      <description>&lt;p&gt;I have a Quake tattoo. The logo, not the netcode architecture diagram, though I considered it. The netcode is the one John Carmack figured out in 1996 that made online FPS possible on dial-up connections. Client-side prediction with server reconciliation. Before QuakeWorld, Quake multiplayer was unplayable over the internet. After it, online gaming existed.&lt;/p&gt;

&lt;p&gt;This week I implemented that same algorithm in my Bomberman clone. In a browser. In 2026. The physics are simpler (grid-based movement instead of 3D trajectories) but the fundamental problem is identical: how do you make a game feel responsive when the server is 50-100ms away?&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%2F85bits.pages.dev%2Fimages%2Fatomic-bomberman%2Fquake.jpeg" 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%2F85bits.pages.dev%2Fimages%2Fatomic-bomberman%2Fquake.jpeg" alt="Quake logo tattoo" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  the problem nobody notices on localhost
&lt;/h2&gt;

&lt;p&gt;The game worked perfectly on localhost. Movement was instant, bombs exploded on time, everything felt responsive. That's because the round trip between client and server was under 1ms.&lt;/p&gt;

&lt;p&gt;Add real network latency, even 50ms, and there's a visible gap between pressing an arrow key and seeing your character move. You press right, wait, then your character slides right. It feels like you're piloting a submarine. Or playing the original Quake over a 28.8k modem.&lt;/p&gt;

&lt;p&gt;The server-authoritative model from &lt;a href="https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-2-the-server-is-the-boss-1lhb"&gt;part 2&lt;/a&gt; is the cause. The client sends input to the server, the server processes it, the server broadcasts the new state, the client renders it. Every action takes a full round trip before the player sees it. Correct, but sluggish.&lt;/p&gt;

&lt;h2&gt;
  
  
  the Carmack model
&lt;/h2&gt;

&lt;p&gt;The idea is deceptively simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Predict.&lt;/strong&gt; When you press a key, the client moves your character &lt;em&gt;immediately&lt;/em&gt; using the same physics the server will use. Don't wait for permission. Just move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify.&lt;/strong&gt; The server is still the boss. It processes your input, runs its own physics, and broadcasts the authoritative state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile.&lt;/strong&gt; When the server's answer arrives, the client compares its prediction to reality. If they match — and they usually do, because both sides run the same physics — nothing visible happens. If they don't match, the client snaps to the server's position.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick that makes it work: &lt;strong&gt;sequence numbers.&lt;/strong&gt; Every input the client sends gets a monotonically increasing sequence number. The server echoes back "I've processed up to seq 42." The client keeps a buffer of all unconfirmed inputs. When the server confirms seq 42, the client:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Discards inputs 1–42 (the server has processed them, the prediction was correct).&lt;/li&gt;
&lt;li&gt;Takes the server position as ground truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replays&lt;/strong&gt; inputs 43–48 on top of that position, re-predicting the inputs the server hasn't confirmed yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This replay is what prevents the snap-back from erasing your recent movement. Without it, every server update would teleport you backwards by ~100ms of movement, then your next input would push you forward again. Visible jitter, 60 times a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  rethinking the input loop
&lt;/h2&gt;

&lt;p&gt;The old input system was event-driven: send a message on keydown, send another on keyup. Simple, and fine for a server-authoritative model where the client is just a dumb terminal.&lt;/p&gt;

&lt;p&gt;Prediction needs something different. The client has to simulate movement at the same rate as the server, 60 times per second, with matching physics. Event-driven input doesn't give you that. You need a tick.&lt;/p&gt;

&lt;p&gt;The input system moved from events to a PixiJS ticker callback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keydown/keyup&lt;/strong&gt; now only updates a &lt;code&gt;heldKeys&lt;/code&gt; set. No network sends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tick&lt;/strong&gt; (60fps): read held keys, compute dx/dy, send input with sequence number to server, push to pending buffer, call the prediction callback to move the sprite immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a bigger refactor than expected. The input module became a &lt;code&gt;Client&lt;/code&gt; class that owns the ticker callback, the pending buffer, the sequence counter, and the WebSocket connection. The old functional approach couldn't hold all that state cleanly. Sometimes a class is the right tool, especially when multiple pieces of state need to be mutated together on a shared cadence.&lt;/p&gt;

&lt;h2&gt;
  
  
  the bugs that teach
&lt;/h2&gt;

&lt;p&gt;Three bugs, each teaching a different lesson about state management in JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  the stale array reference
&lt;/h3&gt;

&lt;p&gt;The reset function for a new round:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pendingInputBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks innocent. But the players module held a reference to the &lt;em&gt;old&lt;/em&gt; array. After reset, it was reading from a detached, empty buffer that would never get new inputs. The prediction worked for one round, then silently broke.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;pendingInputBuffer.length = 0&lt;/code&gt;. Mutates in place, all references stay valid.&lt;/p&gt;

&lt;p&gt;This is the JavaScript version of Rust's ownership problem. In Rust, the compiler would catch this: you can't reassign a reference out from under a borrower. In JavaScript, it silently works until it doesn't. The failure mode isn't an error. It's correct behavior on stale data.&lt;/p&gt;

&lt;h3&gt;
  
  
  the reconciliation that didn't accumulate
&lt;/h3&gt;

&lt;p&gt;First attempt at replaying unconfirmed inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pendingInputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setPlayerPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;playerData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each iteration spread the &lt;em&gt;server position&lt;/em&gt; with one input's dx/dy. The last input won. Earlier ones were lost. The player would snap to server position + one tick of movement instead of server position + six ticks.&lt;/p&gt;

&lt;p&gt;Fix: accumulate across the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;myPlayerSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myPlayerSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;serverData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pendingInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;myPlayerSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;myPlayerSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start from the server's truth, apply each unconfirmed input in order. The position builds up incrementally. That's what replay means.&lt;/p&gt;

&lt;h3&gt;
  
  
  the one-frame snap-back
&lt;/h3&gt;

&lt;p&gt;The update loop called &lt;code&gt;setPlayerPosition(serverData)&lt;/code&gt; for all players, including the local one, before running reconciliation. For one frame — 16ms — the local player would visibly jump backwards to the stale server position. Then reconciliation would push it forward. The result: visible jitter on every server tick.&lt;/p&gt;

&lt;p&gt;Fix: skip &lt;code&gt;setPlayerPosition&lt;/code&gt; entirely for the local player. Let reconciliation handle their position exclusively. The local player's visual position is always the result of server truth + replayed inputs, never raw server data.&lt;/p&gt;

&lt;h2&gt;
  
  
  what's missing
&lt;/h2&gt;

&lt;p&gt;No client-side collision. The client predicts movement through walls and bombs. It doesn't know they're solid. The server rejects the move, and reconciliation snaps the player back. With low latency it's imperceptible. With 100ms+ latency near a wall, there's a visible rubber-band effect: you walk into the wall for a frame or two, then get pulled back.&lt;/p&gt;

&lt;p&gt;The proper fix is duplicating the server's collision logic in TypeScript so the client predicts correctly. That means sharing constants (tile size, player size, grid layout) and reimplementing &lt;code&gt;can_player_move&lt;/code&gt;. Deferred for now. The game is playable without it, and the architecture is ready for it when it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  testing without a network
&lt;/h2&gt;

&lt;p&gt;Chrome's network throttling doesn't affect WebSockets reliably. I needed a way to simulate latency in development.&lt;/p&gt;

&lt;p&gt;The solution was crude but effective: wrap the &lt;code&gt;ws.onmessage&lt;/code&gt; handler with &lt;code&gt;setTimeout(100)&lt;/code&gt;. Every server message arrives 100ms late. Prediction felt instant: you press a key, your character moves immediately. Other players lagged behind by 100ms, which looked correct. Reconciliation corrected smoothly. Removed the setTimeout before committing.&lt;/p&gt;

&lt;p&gt;No fancy network simulation tool. Just a &lt;code&gt;setTimeout&lt;/code&gt; and your eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  what prediction taught me
&lt;/h2&gt;

&lt;p&gt;The algorithm itself is ~30 lines of meaningful code. The sequence number protocol, the pending buffer, the replay loop. But getting it right required understanding three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;References vs values.&lt;/strong&gt; Reassigning an array creates a new object. Anything holding the old reference is now reading dead state. Mutate in place when shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accumulation vs replacement.&lt;/strong&gt; Replay means applying inputs &lt;em&gt;cumulatively&lt;/em&gt; on top of a base position. Each step builds on the last. Spreading properties replaces; adding to coordinates accumulates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering authority.&lt;/strong&gt; The local player's position should come from exactly one source: reconciliation. Not from the server directly, not from prediction alone. One authoritative path prevents flicker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Carmack solved this in C, on a 28.8k modem, in 1996. I solved it in TypeScript, on a broadband connection, in 2026, with an AI explaining the concepts. The tools change. The problem doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The game plays well now. Movement is responsive even with latency. Rounds have structure. The core loop (lobby, play, die, rematch) works end to end.&lt;/p&gt;

&lt;p&gt;Next time: what I actually learned building this thing, and the gap between "it works" and "it's a game."&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>when your game needs to care about not being a game</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Thu, 09 Apr 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-7-when-your-game-needs-to-care-about-not-being-a-game-1eac</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-7-when-your-game-needs-to-care-about-not-being-a-game-1eac</guid>
      <description>&lt;p&gt;Up until now, launching the server meant everyone was immediately in the game. Open two browser tabs, both players are already moving and placing bombs. When someone dies, you restart the server. No countdown, no waiting for other players, no concept of rounds.&lt;/p&gt;

&lt;p&gt;The core gameplay loop was working. But a game that starts without asking and ends without telling you isn't a game. It's a tech demo. This week I added the thing that turns a tech demo into something you'd actually want to play with friends: a lobby, a ready system, and a round loop.&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%2Fhnqghp1zpfuownlfkshe.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%2Fhnqghp1zpfuownlfkshe.png" alt="Lobby" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  the state machine
&lt;/h2&gt;

&lt;p&gt;The game now has three phases, and I modeled them with a Rust enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;GameState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Lobby&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Playing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RoundOver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;winner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Lobby&lt;/code&gt;: players connect, see each other, press R to ready up. &lt;code&gt;Playing&lt;/code&gt;: the actual game. &lt;code&gt;RoundOver&lt;/code&gt;: brief pause showing the winner, then back to Lobby.&lt;/p&gt;

&lt;p&gt;The interesting part is &lt;code&gt;RoundOver&lt;/code&gt; carrying its own data. The countdown timer and winner ID only exist in that phase. They're not floating around as fields on some god struct waiting to be accidentally read during the wrong phase. When you &lt;code&gt;match&lt;/code&gt; on the enum, the compiler forces you to handle every variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RoundOver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;winner_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timer&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;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coming from TypeScript, where you'd model this with a &lt;code&gt;type&lt;/code&gt; discriminant and a union type, the Rust version feels more enforced. You can't access &lt;code&gt;timer&lt;/code&gt; during &lt;code&gt;Playing&lt;/code&gt; because it doesn't exist during &lt;code&gt;Playing&lt;/code&gt;. In TypeScript, you'd narrow the type with an &lt;code&gt;if&lt;/code&gt; check — but nobody stops you from skipping the check and accessing &lt;code&gt;timer&lt;/code&gt; anyway. In Rust, it's structurally impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  the deadlock returns
&lt;/h2&gt;

&lt;p&gt;I hit a deadlock during the refactor. Third time in this project, same class of mistake.&lt;/p&gt;

&lt;p&gt;The game loop acquired the players mutex at the top of the loop body and held it across every &lt;code&gt;interval.tick().await&lt;/code&gt;. Meanwhile, the connection handler was trying to lock the same mutex to insert new players. The game loop never released the lock between ticks, so &lt;code&gt;accept_connections&lt;/code&gt; blocked forever.&lt;/p&gt;

&lt;p&gt;The symptom was subtle: players "connected" — the WebSocket handshake succeeded — but never appeared in the lobby. Zero players, every tick. No errors, no panics, no crash. Just silence.&lt;/p&gt;

&lt;p&gt;The fix was straightforward: lock per tick, not per loop. Acquire the mutex, do the work, drop the guard, then &lt;code&gt;await&lt;/code&gt; the next tick. The lock is held for microseconds instead of forever.&lt;/p&gt;

&lt;p&gt;The rule I keep relearning: &lt;strong&gt;lock late, release early.&lt;/strong&gt; If you just moved a lock acquisition to a wider scope, and something stopped updating, check whether you're holding the lock across an await point.&lt;/p&gt;

&lt;h2&gt;
  
  
  the "alive = false" trick
&lt;/h2&gt;

&lt;p&gt;During Lobby, players shouldn't be able to move or place bombs. My first instinct was to add phase checks to every input handler: if we're not in &lt;code&gt;Playing&lt;/code&gt;, ignore the input.&lt;/p&gt;

&lt;p&gt;Then I realized: I already had a mechanism that blocks all player actions. Every input handler starts with &lt;code&gt;if !player.alive { continue; }&lt;/code&gt;. So I just set &lt;code&gt;alive: false&lt;/code&gt; on player creation. The existing guards block everything. When all players ready up, &lt;code&gt;set_all_players_alive()&lt;/code&gt; flips them on right before transitioning to &lt;code&gt;Playing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reusing an existing field to enforce a new constraint. No new code paths, no phase checks scattered across handlers. It felt like the right kind of lazy.&lt;/p&gt;

&lt;h2&gt;
  
  
  the inverted condition
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;update_players_state&lt;/code&gt; function handles both building the player state for broadcast and updating positions. I added a &lt;code&gt;freeze_positions: bool&lt;/code&gt; param so RoundOver could broadcast positions without letting the winner walk around during the victory screen.&lt;/p&gt;

&lt;p&gt;First attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;freeze_positions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;update_player_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moves players when they should be frozen. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;freeze_positions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;update_player_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classic. Classic. A unit test caught it immediately: just checked that &lt;code&gt;x&lt;/code&gt; didn't change when &lt;code&gt;freeze_positions&lt;/code&gt; was true. The kind of bug that would have been confusing in production ("why is the winner teleporting during the victory screen?") but trivial to catch with a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  the vanishing explosion
&lt;/h2&gt;

&lt;p&gt;When the last player died, the server immediately transitioned to RoundOver. But RoundOver wasn't broadcasting game state. The client got one last frame of the killing explosion, then nothing. The explosion vanished instantly, no death played out.&lt;/p&gt;

&lt;p&gt;The fix: broadcast game state during RoundOver too, keep ticking explosions so they expire naturally, and only start the 3-second countdown after the fireworks finish. The round-ending kill now plays out in full before the winner banner appears.&lt;/p&gt;

&lt;p&gt;This is the kind of thing you don't think about when you're designing state transitions on paper. Phase transition means "stop doing game stuff." But the visual consequences of the last game event still need to resolve. The state machine moved on; the player's eyes haven't.&lt;/p&gt;

&lt;h2&gt;
  
  
  finally using React for something
&lt;/h2&gt;

&lt;p&gt;Up until now, React was a shell around the PixiJS canvas — everything happened imperatively inside a &lt;code&gt;useEffect&lt;/code&gt;. The lobby was the first time React state actually earned its keep.&lt;/p&gt;

&lt;p&gt;The game phase (&lt;code&gt;lobby&lt;/code&gt;, &lt;code&gt;playing&lt;/code&gt;, &lt;code&gt;roundOver&lt;/code&gt;) lives in &lt;code&gt;useState&lt;/code&gt;, and the WebSocket message handler updates it. Overlays render conditionally based on the phase. Standard React pattern, except for the stale closure.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;onmessage&lt;/code&gt; callback is created inside &lt;code&gt;useEffect&lt;/code&gt; and captures the initial &lt;code&gt;phase&lt;/code&gt; value from the closure. So this check is always true inside the handler, even after the game starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lobby&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// always true — phase was captured at effect creation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is the functional form of setState:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lobby&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads the latest state at the time of the update, sidestepping the stale closure entirely. It's the kind of thing you know intellectually but still trip over when you're wiring up a WebSocket handler inside a useEffect.&lt;/p&gt;

&lt;p&gt;The overlays themselves are simple. DOM elements positioned absolutely over the canvas. Lobby shows a player list with color-coded borders matching sprite colors, ready/not-ready status, and a "Press R" prompt. Round-over shows the winner's name in their player color, or "DRAW" in grey. No CSS files, just inline styles. It's a game prototype, not a design system.&lt;/p&gt;

&lt;h2&gt;
  
  
  what the lobby taught me
&lt;/h2&gt;

&lt;p&gt;The lobby has zero gameplay code. No physics, no collision, no timers. But it touched almost everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The game loop&lt;/strong&gt; needed to branch on phase and handle state transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connection handler&lt;/strong&gt; needed to interact with game state without deadlocking it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The player struct&lt;/strong&gt; needed to pull double duty (alive = false as a lobby guard).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The client&lt;/strong&gt; needed React state for the first time, plus stale-closure awareness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The broadcast&lt;/strong&gt; needed to keep running during RoundOver so explosions could finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The non-gameplay parts of a game — lobby, menus, transitions — are where the integration complexity lives. The gameplay code is self-contained: physics in, state out. The lobby code has to coordinate across every system.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The game has a loop now. Lobby, play, die, see the winner, play again. It works perfectly on localhost.&lt;/p&gt;

&lt;p&gt;But everything works perfectly on localhost. The round trip between client and server is under 1ms. Add real network latency and suddenly there's a gap between pressing an arrow key and seeing your character move. The game feels like you're piloting a submarine.&lt;/p&gt;

&lt;p&gt;Next time: implementing the algorithm John Carmack pioneered in 1996 to save online gaming. Client-side prediction with server reconciliation, in a browser, with a Rust server.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>react</category>
    </item>
    <item>
      <title>the three rules that make bombs feel fair</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Tue, 07 Apr 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-6-the-three-rules-that-make-bombs-feel-fair-4cco</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-6-the-three-rules-that-make-bombs-feel-fair-4cco</guid>
      <description>&lt;p&gt;At the end of the last post, the game looked like Atomic Bomberman. Real sprites, real animations, directional explosions. But there were two problems you could spot in the first ten seconds of playing: movement felt stiff (walk diagonally into a wall and you'd freeze in place) and bombs didn't do anything. You could walk straight through them. Your own, your opponent's, it didn't matter.&lt;/p&gt;

&lt;p&gt;That's not Bomberman. Half the strategy of the game is trapping people with bomb placement. If bombs don't block movement, you're just dropping fireworks on a walking simulator.&lt;/p&gt;

&lt;p&gt;This week I made movement feel right, implemented the three bomb rules that every Bomberman game gets right, extracted the collision system into its own module, and debugged a ghost that turned out to be a feature.&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%2F5ic6cbucoaxx0ea7mdqg.gif" 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%2F5ic6cbucoaxx0ea7mdqg.gif" alt="Bomb Trap" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  wall sliding: the feature that isn't a feature
&lt;/h2&gt;

&lt;p&gt;Before tackling bombs, I had a movement problem. Walking diagonally into a wall just...stopped you dead. Hold up-right, hit a wall on the right side, and your character freezes. No sliding along the wall face, no partial movement on the unblocked axis. It felt stiff.&lt;/p&gt;

&lt;p&gt;The fix lives in &lt;code&gt;calc_player_new_position&lt;/code&gt;, and it's shorter than you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;can_player_move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resolved_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;can_player_move&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="n"&gt;player&lt;/span&gt;&lt;span class="py"&gt;.y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resolved_x&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="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;can_player_move&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resolved_x&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;resolved_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three checks, each axis tested independently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try moving on Y only (keep old X). If clear, accept Y.&lt;/li&gt;
&lt;li&gt;Try moving on X only (keep old Y). If clear, accept X.&lt;/li&gt;
&lt;li&gt;Try moving on both. If clear, accept both — full diagonal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're moving diagonally into a wall that blocks X but not Y, step 1 accepts the Y movement, step 2 rejects X, and step 3 also fails. Result: you slide along the wall on the Y axis.&lt;/p&gt;

&lt;p&gt;The thing I didn't expect: wall sliding and diagonal movement aren't two features. They're one algorithm. Axis-independent resolution handles both cases identically. Diagonal input where one axis is blocked &lt;em&gt;is&lt;/em&gt; wall sliding. No special-casing, no branching on direction combinations. The behavior emerges from the structure rather than being hand-coded for each case.&lt;/p&gt;

&lt;p&gt;One trade-off I left alone: I noticed that moving diagonally felt noticeably faster than moving in a straight line. The reason is simple. Cardinal movement updates one axis per tick, so you travel 1 unit. Diagonal movement updates both axes, so the actual distance is √(1² + 1²) = √2 ≈ 1.41 units per tick. That's 41% faster. Classic Bomberman normalizes the diagonal vector to unit length, but on a small grid the speed boost isn't game-breaking. If it becomes a problem during playtesting, normalization is a one-line fix. For now, simpler wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  collision outgrows the map
&lt;/h2&gt;

&lt;p&gt;Before bombs could block anything, I needed to answer a question: where does collision logic live?&lt;/p&gt;

&lt;p&gt;The existing &lt;code&gt;is_blocked&lt;/code&gt; function was in &lt;code&gt;map.rs&lt;/code&gt;. It checked whether a tile was a wall or a destructible block — pure map concerns. But bomb collision isn't a map concern. Bombs are dynamic game objects, not part of the grid topology. Stuffing bomb checks into the map module would be mixing responsibilities.&lt;/p&gt;

&lt;p&gt;I considered &lt;code&gt;util.rs&lt;/code&gt;, but that's a dumping ground waiting to happen. If you can't name what a module does in one sentence, it's probably not a real module.&lt;/p&gt;

&lt;p&gt;The answer was &lt;code&gt;collision.rs&lt;/code&gt; — a module that owns the question "can this entity move here?" It combines map solidity checks with bomb position checks. &lt;code&gt;is_blocked&lt;/code&gt; and &lt;code&gt;is_solid&lt;/code&gt; moved out of &lt;code&gt;map.rs&lt;/code&gt;. Grid constants like &lt;code&gt;TILE_SIZE&lt;/code&gt; and &lt;code&gt;PLAYER_SIZE&lt;/code&gt; stayed where they were — those describe the map, not collision behavior.&lt;/p&gt;

&lt;p&gt;I also extracted &lt;code&gt;get_player_edges&lt;/code&gt;, a helper that converts a player's pixel coordinates into the four tile indices their bounding box touches. Both &lt;code&gt;is_blocked&lt;/code&gt; and the new &lt;code&gt;is_bomb_blocking&lt;/code&gt; need this math, and duplicating it would be asking for a subtle off-by-one bug later.&lt;/p&gt;

&lt;h2&gt;
  
  
  rule 1: no stacking
&lt;/h2&gt;

&lt;p&gt;The simplest rule. You can't place a bomb on a tile that already has one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&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="n"&gt;b&lt;/span&gt;&lt;span class="py"&gt;.row&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="py"&gt;.col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// tile already has a bomb&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line in &lt;code&gt;try_place_bomb&lt;/code&gt;. I'd actually implemented this one before the session started. It was the most obvious missing behavior. Without it, mashing the bomb key stacks infinite bombs on the same tile, and the explosion chain reaction becomes absurd.&lt;/p&gt;

&lt;h2&gt;
  
  
  rule 2: bombs are walls
&lt;/h2&gt;

&lt;p&gt;Bombs block movement for all players. This is what makes Bomberman a strategy game: you can trap an opponent (or yourself) with a well-placed bomb.&lt;/p&gt;

&lt;p&gt;The implementation: &lt;code&gt;is_bomb_blocking&lt;/code&gt; iterates the bomb list, converts the player's position to edges using &lt;code&gt;get_player_edges&lt;/code&gt;, and checks if any bomb occupies a tile the player touches. This combines with the existing &lt;code&gt;is_blocked&lt;/code&gt; into a new &lt;code&gt;can_player_move&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;can_player_move&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="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TileType&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Bomb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;player_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_blocked&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_bomb_blocking&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One function, one question: can this player move here? The caller doesn't need to know whether it was a wall, a destructible block, or a bomb that stopped them.&lt;/p&gt;

&lt;h2&gt;
  
  
  rule 3: walk off your own bomb
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. When you place a bomb, you're standing on it. If rule 2 applied immediately, you'd be trapped by your own bomb every single time. That's not how Bomberman works. You need to be able to walk away first.&lt;/p&gt;

&lt;p&gt;The solution: an &lt;code&gt;walkthrough_player&lt;/code&gt; field on the &lt;code&gt;Bomb&lt;/code&gt; struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Bomb&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;fire_range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;walkthrough_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a bomb is placed, &lt;code&gt;walkthrough_player&lt;/code&gt; is set to &lt;code&gt;Some(player_id)&lt;/code&gt;. The bomb doesn't block its placer. Each tick, &lt;code&gt;update_bombs_owner_blocking&lt;/code&gt; checks whether the immune player has moved off the bomb's tile. Once they have, it clears to &lt;code&gt;None&lt;/code&gt; and the bomb becomes solid for everyone — including the player who placed it.&lt;/p&gt;

&lt;p&gt;The naming took a few rounds. Started with &lt;code&gt;blocking_owner&lt;/code&gt;, which reads like the owner is doing the blocking. Then &lt;code&gt;owner_exempt&lt;/code&gt;, which is accurate but clinical. Landed on &lt;code&gt;walkthrough_player&lt;/code&gt;. It says what it means: this player can still walk through this bomb.&lt;/p&gt;

&lt;p&gt;This rule is invisible when it works. You place a bomb, walk away, and never think about it. But without it, bombs would be unusable. It's the kind of game design that exists purely to prevent a terrible experience, and you only notice it when it's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  an idiomatic rust moment
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;is_bomb_blocking&lt;/code&gt; function started as an imperative habit I brought from JavaScript, even though both languages have a better option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bomb&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bombs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// check if bomb blocks this position&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;blocked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutable flag, loop, early break, return the flag. It works, but Rust has a better way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;bombs&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;bomb&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// check if bomb blocks this position&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question was: how do you &lt;code&gt;continue&lt;/code&gt; inside &lt;code&gt;.any()&lt;/code&gt;? You don't — you &lt;code&gt;return false&lt;/code&gt; from the closure. &lt;code&gt;continue&lt;/code&gt; skips to the next iteration in a loop; &lt;code&gt;return false&lt;/code&gt; in &lt;code&gt;.any()&lt;/code&gt; does the same thing, because &lt;code&gt;.any()&lt;/code&gt; stops at the first &lt;code&gt;true&lt;/code&gt;. Same semantics, less ceremony.&lt;/p&gt;

&lt;p&gt;This is the kind of refactor that doesn't change behavior but changes how the code reads. The &lt;code&gt;any()&lt;/code&gt; version says what it means: "is any bomb blocking this position?" The loop version makes you read the whole body to understand the intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  the ghost in the machine
&lt;/h2&gt;

&lt;p&gt;With all three rules implemented, I started testing chain reactions — the satisfying Bomberman moment where one bomb's explosion triggers another. And that's when I saw the ghosts.&lt;/p&gt;

&lt;p&gt;A chain of three bombs would produce "extra" explosions — bomb center sprites appearing on tiles where no bombs were placed. My first instinct: stale state. Maybe the bomb-to-explosion transition is leaking old positions. Maybe the client isn't cleaning up properly.&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%2Fp91lxak8ksci9o4ompvw.gif" 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%2Fp91lxak8ksci9o4ompvw.gif" alt="Ghost Explosions" width="480" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the kind of bug that looks terrifying in a networked game. State corruption in a 60Hz game loop? That could be anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  following the wire
&lt;/h3&gt;

&lt;p&gt;My AI mentor (Claude) suggested the senior engineer's approach to networked bugs: start at the wire. Before you blame the logic, figure out if the server is sending bad data or the client is misinterpreting good data.&lt;/p&gt;

&lt;p&gt;Three investigation threads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wire truth&lt;/strong&gt; — add protocol logging to capture the raw JSON. Is the server sending explosion tiles that shouldn't be there?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust collection integrity&lt;/strong&gt; — check &lt;code&gt;bomb.rs&lt;/code&gt; and &lt;code&gt;map.rs&lt;/code&gt; for the bomb-to-explosion transition. Are we indexing into a &lt;code&gt;Vec&lt;/code&gt; that's being mutated during iteration? Stale IDs from a removed bomb?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PixiJS lifecycle&lt;/strong&gt; — with &lt;code&gt;AnimatedSprite&lt;/code&gt;, the visual lifecycle is more complex than raw &lt;code&gt;Graphics&lt;/code&gt;. Are "fire and forget" explosion sprites actually being forgotten by the renderer?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  the discovery
&lt;/h3&gt;

&lt;p&gt;The ghosts weren't stale data. They weren't leaked sprites. They were &lt;strong&gt;visual intersections&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Remember the explosion direction inference from &lt;a href="https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-5-your-game-server-shouldnt-know-what-a-sprite-is-253k"&gt;part 5&lt;/a&gt;? For each explosion tile, the client checks its neighbors to decide which sprite to use. Two horizontal neighbors → horizontal arm. Two vertical → vertical arm. Multiple directions → center.&lt;/p&gt;

&lt;p&gt;When two explosion arms from different bombs cross at a 90-degree angle, the tile at the intersection has neighbors in all four directions. The inference logic sees "neighbors everywhere" and defaults to the center sprite — the one that looks like a bomb origin.&lt;/p&gt;

&lt;p&gt;The "ghost bombs" were cross-shaped intersections being rendered as centers. The server was correct. The networking was correct. The state management was correct. What looked like a data corruption bug was emergent visual behavior from a rendering heuristic.&lt;/p&gt;

&lt;h3&gt;
  
  
  the lesson
&lt;/h3&gt;

&lt;p&gt;Deterministic reproduction was the key. Once I placed bombs in specific patterns and watched the "ghosts" appear at exactly the coordinates where blast paths crossed, the pattern was obvious. The bug wasn't in the data — it was in the interpretation.&lt;/p&gt;

&lt;p&gt;The fix (if I want one) is either having the server flag which tiles are true bomb centers, or adding a "cross-intersection" sprite to the client's inference logic. For now, I left it. The networking and state management are working perfectly, and that's what I was actually worried about.&lt;/p&gt;

&lt;h2&gt;
  
  
  what this session taught me
&lt;/h2&gt;

&lt;p&gt;The wall sliding algorithm and the three bomb rules were each a handful of lines. But they surfaced real design questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let behavior emerge from structure.&lt;/strong&gt; Wall sliding isn't a feature — it's a side effect of testing each axis independently. The less you special-case, the fewer bugs you write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where does collision live?&lt;/strong&gt; Not in the map module. In its own module, because the question "can I move here?" combines multiple concerns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you handle temporary immunity?&lt;/strong&gt; With an explicit state field that gets cleared by a condition, not by a timer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you debug a visual bug in a networked game?&lt;/strong&gt; Start at the wire. Isolate server vs client. Reproduce deterministically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the ghost explosions reminded me of something I keep relearning: in a system with emergent behavior, not every unexpected result is a bug. Sometimes the code is doing exactly what you told it to, and the problem is that you didn't anticipate the combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;Movement feels right. Bombs block, trap, and punish. Chain reactions cascade. The core gameplay loop is there — place bombs, dodge explosions, collect power-ups, kill your friends.&lt;/p&gt;

&lt;p&gt;But right now, two players connect and immediately start playing. There's no lobby, no ready check, no round structure. The game just... begins. And when someone dies, nothing happens. There's no "you win," no rematch, no waiting for the next round.&lt;/p&gt;

&lt;p&gt;Next time: building a lobby and ready system, and what happens when your game needs to care about state that isn't gameplay.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>programming</category>
    </item>
    <item>
      <title>your game server shouldn't know what a sprite is</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Mon, 06 Apr 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-5-your-game-server-shouldnt-know-what-a-sprite-is-253k</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-5-your-game-server-shouldnt-know-what-a-sprite-is-253k</guid>
      <description>&lt;p&gt;For four posts, my Bomberman clone looked like a game designed by a spreadsheet. Players were colored squares. Bombs were black squares. Explosions were yellow rectangles. It worked — the game logic was solid — but it looked like a developer art museum.&lt;/p&gt;

&lt;p&gt;This week I swapped every &lt;code&gt;Graphics&lt;/code&gt; primitive for actual Atomic Bomberman sprites. And in doing so, I learned something I didn't expect: the sprite swap itself was the easy part. The interesting part was discovering how cleanly the rendering layer separated from the game logic — and what broke when it didn't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before:&lt;/em&gt;&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%2F85bits.pages.dev%2Fvideos%2Fatomic-bomberman%2Fgameplay01.gif" 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%2F85bits.pages.dev%2Fvideos%2Fatomic-bomberman%2Fgameplay01.gif" alt="Before: colored rectangles" width="560" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After:&lt;/em&gt;&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%2F85bits.pages.dev%2Fvideos%2Fatomic-bomberman%2Fgameplay02.webp" 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%2F85bits.pages.dev%2Fvideos%2Fatomic-bomberman%2Fgameplay02.webp" alt="After: real sprites" width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  the thesis
&lt;/h2&gt;

&lt;p&gt;Here's the rule I accidentally followed and now consciously endorse: &lt;strong&gt;your game server should never know what a sprite is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The server in this project manages game state: positions, timers, collision, death. It sends a &lt;code&gt;game_state&lt;/code&gt; message 60 times a second. Here's what that message contains for explosions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ExplosionState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;tiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A flat list of tile coordinates. The server doesn't know which tile is the center of a blast, which is an arm, which is a tip. It doesn't know that explosions have direction. It placed bombs, counted down timers, and computed which tiles should be on fire. Its job ends there.&lt;/p&gt;

&lt;p&gt;Every rendering decision lives on the client. Which sprite to use, how to animate it, whether to flip a texture. And this separation made the entire sprite overhaul possible without touching a single line of Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  finding the assets
&lt;/h2&gt;

&lt;p&gt;I spent time on itch.io and OpenGameArt looking for Bomberman-style sprites. Plenty of generic top-down packs, nothing that felt right. Then I remembered: years ago I'd started a CraftyJS Bomberman clone using the original Atomic Bomberman sprites. I never finished it, but the sprites were still sitting in the old project folder. Sometimes the best asset is the one you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  the sprite pipeline
&lt;/h2&gt;

&lt;p&gt;The loader module ended up being small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadSheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Texture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scaleMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nearest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadTexture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Texture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;spriteWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;spriteHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Texture&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spriteHeight&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loadSheet&lt;/code&gt; fetches a PNG and sets nearest-neighbor scaling, which is essential for pixel art. Without it, PixiJS interpolates when scaling up and everything looks like vaseline on a camera lens. &lt;code&gt;loadTexture&lt;/code&gt; slices a sub-rectangle from the sheet.&lt;/p&gt;

&lt;p&gt;All original sprites are 16px. The game renders at 64px per tile. PixiJS handles the scaling through &lt;code&gt;sprite.width&lt;/code&gt; and &lt;code&gt;sprite.height&lt;/code&gt;, so no image preprocessing needed. 192 grid tiles, 4 textures. Sprites are cheap instances pointing at shared image data.&lt;/p&gt;

&lt;h3&gt;
  
  
  the async cascade
&lt;/h3&gt;

&lt;p&gt;Before sprites, creating a manager was synchronous. &lt;code&gt;new Graphics()&lt;/code&gt;, draw some rectangles, done. But &lt;code&gt;Assets.load()&lt;/code&gt; is async. That single change meant every manager factory that loads a spritesheet became &lt;code&gt;async&lt;/code&gt;, and the &lt;code&gt;useEffect&lt;/code&gt; in &lt;code&gt;App.tsx&lt;/code&gt; that wires everything together had to &lt;code&gt;await&lt;/code&gt; each one before connecting the WebSocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gridManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createGridManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gridLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playersManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPlayersManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bombsManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createBombsManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explosionsManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createExplosionsManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gridLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gridManager&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the actual logic changed, just the function signatures and the init order. But touching every manager's factory function to add &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; felt like the kind of refactor where you question whether sprites were worth it. (They were.)&lt;/p&gt;

&lt;h2&gt;
  
  
  the pixel measurement problem
&lt;/h2&gt;

&lt;p&gt;The power-up spritesheet was 117x33 pixels with inconsistent spacing between icons. I couldn't eyeball where one frame ended and the next began. You could open it in GIMP and zoom in with a pixel grid, but Claude suggested something I wouldn't have thought of. &lt;code&gt;ffmpeg&lt;/code&gt; can scale pixel art with nearest-neighbor filtering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; powerups.png &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=iw*8:ih*8:flags=neighbor"&lt;/span&gt; powerups_8x.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scaled 8x, every original pixel became an 8x8 block. It's still eyeballing, but at 8x the boundaries between sprite frames become obvious. You can clearly see where the background color changes and where the 1px gaps are. Turns out the frames were 16x16 with 1px gaps between them. The bottom row (red backgrounds) are debuffs. Those went into the stretch goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  explosions: client-side direction inference
&lt;/h2&gt;

&lt;p&gt;This was the most interesting rendering problem, and the clearest example of the server/client split paying off.&lt;/p&gt;

&lt;p&gt;The server sends a flat set of explosion tile positions. No metadata about shape. But explosions need different sprites: a center piece, horizontal and vertical arms, tips at the ends. The client has to figure this out.&lt;/p&gt;

&lt;p&gt;The solution: for each explosion tile, check which neighbors are also in the explosion set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tilesSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tilesSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tilesSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tilesSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExplosionDirection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;topEdge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leftEdge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rightEdge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bottomEdge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both horizontal neighbors? Horizontal arm. Both vertical? Vertical arm. Only one neighbor? Tip pointing away from it. Multiple directions? Center. Pure function, no graph traversal, O(1) lookups per tile using a Set.&lt;/p&gt;

&lt;p&gt;The server didn't need to change. It still sends &lt;code&gt;tiles: Vec&amp;lt;(f64, f64)&amp;gt;&lt;/code&gt;, just coordinates. The client derives the visual structure from the spatial relationship between tiles. If I later want fancier explosion sprites or different animation styles, I change client code only.&lt;/p&gt;

&lt;h3&gt;
  
  
  the flip gotcha
&lt;/h3&gt;

&lt;p&gt;PixiJS lets you mirror sprites with &lt;code&gt;scale.x = -1&lt;/code&gt;, which saves you from needing separate left-facing and right-facing assets. But there's a catch: the flip happens around the sprite's anchor point, which defaults to the top-left corner. So flipping a sprite doesn't just mirror it, it also jumps its position.&lt;/p&gt;

&lt;p&gt;Fix: set &lt;code&gt;anchor(0.5, 0.5)&lt;/code&gt; to flip around the center, then offset &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; by half the tile size to compensate. Small thing, but it took longer to debug than the entire direction inference algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  players: animation from velocity
&lt;/h2&gt;

&lt;p&gt;The server sends this for each player:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PlayerState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Position, alive/dead, and a direction vector. No animation frame, no facing direction enum, no sprite index. The client's &lt;code&gt;PlayerSprite&lt;/code&gt; class maps this to animations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FrameType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentFrameType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;frameType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentFrameType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frameType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;frameType&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;down&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;left&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;right&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;play()&lt;/code&gt; method early-returns if the requested animation is already active. This matters because the game loop calls &lt;code&gt;player.right()&lt;/code&gt; on every tick while the player moves right. Without the guard, it would restart the walk animation 60 times a second.&lt;/p&gt;

&lt;p&gt;Vertical movement takes priority over horizontal in the update logic, matching how classic Bomberman looks during diagonal movement. That's a rendering opinion the server has no business holding.&lt;/p&gt;

&lt;p&gt;The player spritesheet was the most complex. 4 colors, each with walk cycles in 4 directions plus a death sequence. The frame layout came from reverse-engineering my old CraftyJS project: &lt;code&gt;Crafty.sprite(16, 26, ...)&lt;/code&gt; told me each frame was 16px wide and 26px tall, with the actual character occupying 16x24 and the rest being padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  bombs: the easy win
&lt;/h2&gt;

&lt;p&gt;Bombs were the simplest sprite swap. PixiJS &lt;code&gt;AnimatedSprite&lt;/code&gt; takes a texture array and cycles through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animatedSprite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AnimatedSprite&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// [0, 1, 2, 1, 0] for ping-pong&lt;/span&gt;
  &lt;span class="na"&gt;animationSpeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoPlay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three frames, reversed for a ping-pong effect (small → big → small). Set &lt;code&gt;loop: true&lt;/code&gt; and forget about it. The server sends bomb positions and timers; the client handles the pulsing animation independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  what the server sends, what the client decides
&lt;/h2&gt;

&lt;p&gt;Here's the full picture of the boundary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Server sends&lt;/th&gt;
&lt;th&gt;Client decides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Players&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;id, x, y, alive, dx, dy&lt;/td&gt;
&lt;td&gt;Which animation to play, sprite color, death sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bombs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;x, y, timer, owner&lt;/td&gt;
&lt;td&gt;Pulse animation, sprite size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explosions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;tile coordinates&lt;/td&gt;
&lt;td&gt;Center vs arm vs tip, direction, flip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power-ups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;x, y, kind&lt;/td&gt;
&lt;td&gt;Sprite frame, pickup flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;tile types&lt;/td&gt;
&lt;td&gt;Wall/floor/destructible sprites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;game_state&lt;/code&gt; message is ~200 bytes per tick. No texture references, no animation state, no rendering hints. The server is a physics engine that happens to know about game rules. The client is a renderer that happens to know about game state.&lt;/p&gt;

&lt;p&gt;This meant the entire visual overhaul was a client-only change. Every &lt;code&gt;Graphics&lt;/code&gt; rectangle replaced with a sprite, every animation added, every directional inference implemented. The server kept running its 60Hz loop, unchanged, unaware that the game had gone from looking like a developer prototype to looking like Atomic Bomberman.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The game looks like Atomic Bomberman now. But there's a problem: you can walk straight through your own bomb. And another player's bomb. Bombs are supposed to trap people — that's half the strategy of Bomberman. Right now they're decoration.&lt;/p&gt;

&lt;p&gt;Next time: the three rules that make bombs feel fair, and why implementing them in a server-authoritative multiplayer game is harder than it sounds.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>rust</category>
      <category>gamedev</category>
      <category>pixijs</category>
    </item>
    <item>
      <title>react vs. the game loop</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Thu, 02 Apr 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-4-react-vs-the-game-loop-2fg8</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-4-react-vs-the-game-loop-2fg8</guid>
      <description>&lt;p&gt;The server was running. The Rust was making sense. But on the client side, I had a problem I hadn't anticipated: React and real-time rendering don't want the same things.&lt;/p&gt;

&lt;p&gt;React is built around a simple idea — your UI is a function of state. State changes, React re-renders, the DOM updates. It's elegant, and it's the mental model I've used for years. But a game renderer running at 60fps doesn't work this way. You don't want to trigger a React re-render every 16 milliseconds. You want to reach into a canvas and move pixels directly.&lt;/p&gt;

&lt;p&gt;This post is about mounting an imperative game engine inside a declarative framework, and all the places where the two models clash.&lt;/p&gt;

&lt;h2&gt;
  
  
  the escape hatch
&lt;/h2&gt;

&lt;p&gt;React gives you exactly one way to say "I need to touch something outside the React tree": &lt;code&gt;useRef&lt;/code&gt; plus &lt;code&gt;useEffect&lt;/code&gt;. The ref gives you a DOM node. The effect gives you a place to run setup code that React won't interfere with.&lt;/p&gt;

&lt;p&gt;Here's the skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvasRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GRID_COLUMNS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GRID_ROWS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// everything else happens here — outside React&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// cleanup: destroy the PixiJS app&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[]&lt;/code&gt; dependency array means this runs once on mount. Inside &lt;code&gt;init&lt;/code&gt;, we create a PixiJS &lt;code&gt;Application&lt;/code&gt;, append its canvas to the div, and from that point forward, React has nothing to do with rendering. The game lives entirely in the &lt;code&gt;useEffect&lt;/code&gt; closure.&lt;/p&gt;

&lt;p&gt;One thing that caught me off guard: &lt;code&gt;app.init()&lt;/code&gt; is async. PixiJS v8 moved initialization to a promise-based API, which means you can't just construct and go — you need the &lt;code&gt;async&lt;/code&gt; wrapper inside the effect. A &lt;code&gt;useEffect&lt;/code&gt; callback can't be async directly, so you define &lt;code&gt;init&lt;/code&gt; inside it and call it immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  the StrictMode trap
&lt;/h2&gt;

&lt;p&gt;React's &lt;code&gt;StrictMode&lt;/code&gt; intentionally double-invokes effects in development to help you find missing cleanup logic. For most components, this is fine — you clean up and re-run, no harm done.&lt;/p&gt;

&lt;p&gt;For a game renderer that creates a canvas and opens a WebSocket, it's a disaster. Two canvases. Two connections. Two sets of everything, fighting each other.&lt;/p&gt;

&lt;p&gt;I spent an embarrassing amount of time debugging ghost players and duplicate renderings before realizing the issue. The fix was removing &lt;code&gt;StrictMode&lt;/code&gt; from &lt;code&gt;main.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — double mount in dev&lt;/span&gt;
&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after — single mount&lt;/span&gt;
&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels wrong if you've internalized the React best practices. StrictMode is supposed to catch bugs, and removing it feels like giving up. But the reality is: StrictMode assumes your side effects can safely run twice with no consequences. A PixiJS canvas and a WebSocket connection can't. The "correct" fix would be elaborate teardown and re-initialization logic, but for a game client that mounts once and stays mounted, removing StrictMode is the pragmatic choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  why &lt;code&gt;setState&lt;/code&gt; doesn't work here
&lt;/h2&gt;

&lt;p&gt;My first instinct was to store game state in React state. Players move? &lt;code&gt;setPlayers(newPositions)&lt;/code&gt;. Bombs appear? &lt;code&gt;setBombs(newBombs)&lt;/code&gt;. It's how I'd build any other React app.&lt;/p&gt;

&lt;p&gt;The problem is frame rate. The server sends game state 60 times per second. Each &lt;code&gt;setState&lt;/code&gt; call triggers a re-render — React diffs the virtual DOM, calculates what changed, and updates. That's a lot of machinery for something that needs to happen in under 16 milliseconds, every frame.&lt;/p&gt;

&lt;p&gt;More importantly, the rendering model is wrong. React re-renders by destroying and recreating DOM elements (or diffing to avoid it). PixiJS works by mutating existing objects in place — you change &lt;code&gt;sprite.x = 100&lt;/code&gt; and it's done. There's no diff step. There's no reconciliation. You just move the thing.&lt;/p&gt;

&lt;p&gt;So the answer is: don't use React state for game state. Use React to mount the canvas and set up the WebSocket. After that, everything is direct mutation.&lt;/p&gt;

&lt;h2&gt;
  
  
  the factory pattern
&lt;/h2&gt;

&lt;p&gt;Each game system — players, bombs, explosions, power-ups — is a factory function that takes a PixiJS &lt;code&gt;Container&lt;/code&gt; and returns an update method. No React. No hooks. Just closures over mutable state.&lt;/p&gt;

&lt;p&gt;Here's the core of the players manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createPlayersManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Graphics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;updatePlayers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;playersData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlayerData&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;playersData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Graphics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rect&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;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PLAYER_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PLAYER_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
          &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;players&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// + cleanup for disconnected players&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state is a plain &lt;code&gt;Map&amp;lt;number, Graphics&amp;gt;&lt;/code&gt; inside a closure — not React state. When &lt;code&gt;updatePlayers&lt;/code&gt; is called 60 times per second, it mutates Graphics objects directly. &lt;code&gt;g.x = player.x&lt;/code&gt; — that's it. No diffing, no reconciliation. New player? Create a graphic and add it to the container. Existing player? Update two properties. Player disconnects? Destroy the graphic and remove it from the map.&lt;/p&gt;

&lt;p&gt;This pattern repeated across every system. Bombs, explosions, power-ups — they all follow the same shape: a collection of PixiJS objects, an update function that syncs them with server state, and cleanup for removed entities.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;App.tsx&lt;/code&gt; became pure wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;ServerMessageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;WELCOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;gridManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;ServerMessageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GAME_STATE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;bombsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateBombs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bombs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;playersManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updatePlayers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;players&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;explosionsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateExplosions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;explosions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;powerupsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updatePowerUps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;powerups&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Message comes in, dispatch to the right manager, done. No &lt;code&gt;setState&lt;/code&gt;, no re-renders, no virtual DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  the z-order problem
&lt;/h2&gt;

&lt;p&gt;Here's a bug that's obvious in hindsight but drove me crazy at the time: game objects kept rendering in the wrong order. A player would walk behind a bomb instead of in front of it. Explosions would hide behind the grid.&lt;/p&gt;

&lt;p&gt;PixiJS renders children in &lt;code&gt;addChild&lt;/code&gt; order. The first child added to a container is drawn first (behind everything else). When you have multiple systems adding graphics at different points in time — grid on startup, bombs when placed, players when they connect — the order depends on when each object was created, not on any logical layering.&lt;/p&gt;

&lt;p&gt;The fix is explicit layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gridLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entityLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playerLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gridLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerLayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grid on the bottom, entities (bombs, explosions) in the middle, players on top. Each manager gets the container it should draw into. No matter when a bomb is placed or a player connects, the z-order is correct because the layer containers are added to the stage in the right order.&lt;/p&gt;

&lt;p&gt;If you've worked with CSS &lt;code&gt;z-index&lt;/code&gt;, this is the same idea — but without the ability to set a number on each element. You create the ordering through container hierarchy instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  the destructible tile problem
&lt;/h2&gt;

&lt;p&gt;The grid started as a single &lt;code&gt;Graphics&lt;/code&gt; object — one draw call for all 195 tiles (15 columns x 13 rows). Efficient and simple. Walls, floors, pillars, all drawn into one shape.&lt;/p&gt;

&lt;p&gt;Then I added destructible blocks. When a bomb explodes, it destroys certain tiles. And you can't partially erase a &lt;code&gt;Graphics&lt;/code&gt; object. It's one shape — you'd have to clear and redraw the entire grid every time a block is destroyed.&lt;/p&gt;

&lt;p&gt;The solution: destructible tiles are separate &lt;code&gt;Graphics&lt;/code&gt; objects, each stored in a Map keyed by position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destructibleTiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Graphics&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drawGrid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TileType&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;GRID_ROWS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;GRID_COLUMNS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;GRID_COLUMNS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destructible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Graphics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TILE_COLORS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;TILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;destructibleTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;destructibleTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;removeDestructible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destructible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;destructibleTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;destructible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;destructibleTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The static grid (walls, floors, pillars) stays as one &lt;code&gt;Graphics&lt;/code&gt; object — cheap and never changes. Destructible tiles are individual objects that can be independently removed. When the explosion manager processes a blast, it calls &lt;code&gt;removeDestructible&lt;/code&gt; for each affected tile, and only that tile disappears.&lt;/p&gt;

&lt;p&gt;This is the kind of decision you don't think about in a DOM-based app. React handles element creation and removal for you. In a canvas renderer, you're managing every object's lifecycle yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  React as a shell
&lt;/h2&gt;

&lt;p&gt;The irony is that this is a React app where React barely does anything. It mounts a div. It runs one effect. After that, it's PixiJS closures and direct mutation all the way down.&lt;/p&gt;

&lt;p&gt;If I were starting over, I might skip React entirely and just use vanilla TypeScript. But having React there means I can add a lobby UI, a settings screen, or a HUD later without building a UI framework from scratch. The game canvas is one component in what could become a larger app. For now, React owns the page lifecycle, and the game engine owns everything inside the canvas. They coexist because they don't try to share control.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The server and client were both working. I had multiplayer movement, bombs, explosions, and power-ups. But everything was colored rectangles. Players were squares. Bombs were black squares. The game logic was solid — it just looked like a developer art museum.&lt;/p&gt;

&lt;p&gt;Next time: swapping every &lt;code&gt;Graphics&lt;/code&gt; primitive for actual Atomic Bomberman sprites, and discovering that the hardest part isn't the sprites themselves — it's everything that breaks around them.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ownership hell</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Tue, 31 Mar 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-3-ownership-hell-2p3b</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-3-ownership-hell-2p3b</guid>
      <description>&lt;p&gt;In JavaScript, if two functions need the same object, you just... pass it. Nobody asks who owns it. Nobody cares. The garbage collector handles the mess, and you move on with your life.&lt;/p&gt;

&lt;p&gt;Rust does not work this way.&lt;/p&gt;

&lt;p&gt;This post is about the first week of writing Rust as someone who's spent most of their career in JavaScript — specifically, the ownership model and all the ways it made me feel like I was fighting the language before I understood it was protecting me.&lt;/p&gt;

&lt;h2&gt;
  
  
  the first wall: &lt;code&gt;async move&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I needed to spawn an async task for each WebSocket connection. In JS, this is a closure that captures whatever it needs from the surrounding scope. In Rust, it's an &lt;code&gt;async move&lt;/code&gt; block — and &lt;code&gt;move&lt;/code&gt; means what it says. The closure &lt;em&gt;takes ownership&lt;/em&gt; of every variable it captures. The original scope can't use them anymore.&lt;/p&gt;

&lt;p&gt;Here's where it got me: I was cloning a shared player map before a loop, then trying to move that clone into each spawned task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;players_clone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clone&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;players&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="nf"&gt;.accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_clone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles on the first iteration. On the second, the compiler yells at you — &lt;code&gt;players_clone&lt;/code&gt; was moved into the first task. It's gone. You can't move the same thing twice.&lt;/p&gt;

&lt;p&gt;The fix: clone inside the loop, so each task gets its own copy. And &lt;code&gt;Arc::clone&lt;/code&gt; is cheap — it just increments a reference counter, it doesn't deep-copy the data behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="nf"&gt;.accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;players_clone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clone&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;players&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players_clone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JS, this problem doesn't exist. Closures share references freely, and the runtime sorts it out. In Rust, you have to think about who owns what, and the compiler won't let you pretend otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  sharing state: &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;HashMap&amp;gt;&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In a Node.js server, shared state is just... a variable. You declare an object at the top of your file, and every request handler reads and writes to it. Single-threaded, no locks, no problem.&lt;/p&gt;

&lt;p&gt;Rust makes you earn shared mutable state. My game server uses &lt;a href="https://tokio.rs/" rel="noopener noreferrer"&gt;Tokio&lt;/a&gt; as its async runtime — think of it as the event loop you get for free in Node, except in Rust you choose and install one yourself. I have multiple async tasks — a connection handler per player and a central game loop — all needing access to the same player map. The pattern for this is &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;HashMap&amp;gt;&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/strong&gt; (atomic reference counting) — lets multiple tasks hold a reference to the same data. Like a shared pointer that knows when everyone's done with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/strong&gt; — a lock. Only one task can access the data at a time. You call &lt;code&gt;.lock().await&lt;/code&gt;, do your work, and the lock releases when it goes out of scope. You need both because &lt;code&gt;Arc&lt;/code&gt; lets everyone &lt;em&gt;reach&lt;/em&gt; the data, but &lt;code&gt;Mutex&lt;/code&gt; makes sure only one of them gets in at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HashMap&lt;/code&gt;&lt;/strong&gt; — the actual data. In this case, player ID to player state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time I wanted to read or write a player, I had to lock the mutex first. Coming from JS where you just do &lt;code&gt;players[id].x += speed&lt;/code&gt;, the ceremony felt heavy. But then I realized: in a concurrent system, JS &lt;em&gt;doesn't&lt;/em&gt; protect you. If you had actual threads sharing a mutable object, you'd get data races. Rust just forces you to handle it upfront instead of discovering it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  everything is private
&lt;/h2&gt;

&lt;p&gt;In JavaScript, everything is accessible by default. Export what you want, import what you need, and if you're lazy, just export everything.&lt;/p&gt;

&lt;p&gt;Rust is the opposite. Everything is private by default. You have to explicitly mark things as &lt;code&gt;pub&lt;/code&gt; to expose them. The module system works like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mod player;&lt;/code&gt; — declares that a module exists (like registering a file)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pub struct Player&lt;/code&gt; — makes the struct visible outside its module&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;use crate::player::Player;&lt;/code&gt; — imports it where you need it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first time I split my server into modules, nothing compiled. Every struct, every function, every field — the compiler told me it was private. I went through and added &lt;code&gt;pub&lt;/code&gt; everywhere until it worked, which felt like I was doing it wrong. I was. You're supposed to think about what &lt;em&gt;should&lt;/em&gt; be public. The default is intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  the deadlock that taught me the most
&lt;/h2&gt;

&lt;p&gt;Phase 3 introduced bombs and explosions. The game loop needed to read the map to calculate blast radius, then write to it to destroy blocks. Seemed straightforward — lock the map, read it, calculate, write, unlock.&lt;/p&gt;

&lt;p&gt;Except I locked it twice. First an immutable read to calculate the blast, then a mutable write to destroy tiles. Tokio's mutex doesn't support re-entrancy — if you try to lock something you already hold, it doesn't error. It just waits. Forever.&lt;/p&gt;

&lt;p&gt;The game would start, a bomb would explode, and everything would freeze. No error message. No crash. Just silence.&lt;/p&gt;

&lt;p&gt;The fix was simple: take a single &lt;code&gt;mut&lt;/code&gt; lock at the start of the tick and use it for both reads and writes. But finding the bug took longer than it should have, because "everything freezes silently" is not a helpful error message.&lt;/p&gt;

&lt;p&gt;This was the moment Rust's ownership model clicked for me. The language didn't prevent this particular bug at compile time — deadlocks are a runtime problem. But the fact that I had to explicitly lock and unlock resources, that I had to &lt;em&gt;think&lt;/em&gt; about who holds what and when, meant the bug was localized. I knew it was a locking issue because there was nowhere else it could be.&lt;/p&gt;

&lt;h2&gt;
  
  
  the rules that made it work
&lt;/h2&gt;

&lt;p&gt;I didn't learn all this alone. I had an AI mentor — Claude — and we established a set of rules early on to make sure the learning actually stuck.&lt;/p&gt;

&lt;p&gt;The core idea was "challenge-first": the AI explains new concepts and specs out a goal, then I write the code. No code dumps. If I'm stuck, we talk through it, but I'm the one typing. There's also a mandatory pause — the AI has to stop and wait for me to review before moving on. No steamrolling through steps I haven't digested yet.&lt;/p&gt;

&lt;p&gt;After I write the code, we review it together. That's where I pick up Rust idioms I wouldn't find on my own — like using &lt;code&gt;HashMap.retain()&lt;/code&gt; to filter in-place instead of building a new collection, or knowing when a local &lt;code&gt;Vec&lt;/code&gt; is enough and &lt;code&gt;Arc&amp;lt;Mutex&amp;gt;&lt;/code&gt; is overkill.&lt;/p&gt;

&lt;p&gt;These rules sound obvious, but without them the default mode of AI-assisted coding is: you describe a problem, the AI writes the solution, you paste it in. You ship faster but you don't learn anything. For a project where the whole point is learning Rust, that's a trap.&lt;/p&gt;

&lt;p&gt;The deadlock bug was a good test of this. The temptation was to paste the error (or lack of error — just a frozen game) and let the AI diagnose it. Instead, I talked through what I knew: the game freezes when a bomb explodes, explosions touch the map, the map is behind a mutex. That was enough to find it myself. The AI confirmed the fix, but the understanding was mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  what I'd tell a JS dev starting Rust
&lt;/h2&gt;

&lt;p&gt;After a week, here's what I wish someone had told me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop thinking in references.&lt;/strong&gt; In JS, everything is a reference and the GC handles lifetime. In Rust, every value has exactly one owner. When you need to share, you make explicit copies (&lt;code&gt;clone&lt;/code&gt;) or use smart pointers (&lt;code&gt;Arc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compiler is your pair programmer.&lt;/strong&gt; Those error messages aren't obstacles — they're catching bugs you'd ship in JS and discover at 3am.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not everything needs &lt;code&gt;Arc&amp;lt;Mutex&amp;gt;&lt;/code&gt;.&lt;/strong&gt; My first instinct was to wrap everything in shared state because that's how JS works — one global scope. But if only one task touches a piece of data, a local &lt;code&gt;Vec&lt;/code&gt; is simpler and faster. Rust taught me to ask "who actually needs this?" instead of making everything globally accessible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;let ... else&lt;/code&gt; is your friend.&lt;/strong&gt; Rust's version of early returns and guard clauses. Instead of nesting &lt;code&gt;match&lt;/code&gt; inside &lt;code&gt;match&lt;/code&gt; inside &lt;code&gt;match&lt;/code&gt;, you write &lt;code&gt;let Some(value) = expression else { continue; };&lt;/code&gt; and keep going at the top level. Flattens deeply nested error handling into something readable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The server was running and the Rust was making sense. But on the client side, I had a different problem: how do you mount a 60fps imperative game renderer inside a declarative React app?&lt;/p&gt;

&lt;p&gt;Next post: bridging React and PixiJS, and why everything I knew about state management was wrong for real-time rendering.&lt;/p&gt;

&lt;p&gt;If there's a Rust concept you want me to dig into in a future post, or if you think I skipped over something that deserves more detail drop a comment. I'm writing these as I learn, and I'd rather go deeper on the things that actually trip people up.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>rust</category>
    </item>
    <item>
      <title>the server is the boss</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Tue, 31 Mar 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-2-the-server-is-the-boss-1lhb</link>
      <guid>https://dev.to/tomerl1/building-an-atomic-bomberman-clone-part-2-the-server-is-the-boss-1lhb</guid>
      <description>&lt;p&gt;Before I wrote a single line of game logic, I had to answer one question: who decides what's true?&lt;/p&gt;

&lt;p&gt;In a multiplayer game, two players might press a button at the same time. Their clocks don't agree. Their internet connections have different latency. If each client runs its own simulation, they'll disagree on what happened — and in a game where a split-second determines whether you dodge an explosion, disagreement means broken gameplay.&lt;/p&gt;

&lt;p&gt;The answer: the server is the boss. The client is just a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  the authoritative model
&lt;/h2&gt;

&lt;p&gt;Here's the deal the client and server make: the client sends &lt;em&gt;intent&lt;/em&gt; ("I want to move down", "I want to place a bomb"), and the server decides what actually happens. The client never runs game logic. It never calculates movement or collision. It just renders whatever the server tells it to.&lt;/p&gt;

&lt;p&gt;This is called an authoritative server model, and it gives you two things for free:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No cheating.&lt;/strong&gt; If the client doesn't decide where a player is, a modified client can't teleport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One source of truth.&lt;/strong&gt; Every player sees the same game state because it all comes from the same place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tradeoff is latency — the client has to wait for the server to respond before anything feels "real." We'll deal with that later (client-side prediction is on the roadmap). For now, on localhost, it's instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  the game loop
&lt;/h2&gt;

&lt;p&gt;The server runs a loop at 60 ticks per second. Every 16.66 milliseconds, it does the same thing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move players&lt;/strong&gt; — read each player's current direction and speed, calculate a new position, check for wall collision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tick bombs&lt;/strong&gt; — decrement every bomb's timer. When it hits zero, boom&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve explosions&lt;/strong&gt; — calculate blast radius in four directions (stop at walls, destroy breakable blocks), check for chain reactions where one explosion triggers another bomb&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the map&lt;/strong&gt; — destroyed blocks become floor tiles, with a 40% chance to spawn a power-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for death&lt;/strong&gt; — any player standing on an explosion tile dies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick up power-ups&lt;/strong&gt; — players walking over a power-up get the bonus (extra bomb, bigger blast, faster speed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast&lt;/strong&gt; — pack up the entire game state and send it to every connected client&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step is the key. The server doesn't send individual events ("player 2 moved left"). It sends the whole world, 60 times per second. Every client gets the same snapshot, and they all render it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 16.66ms:
  move players → new positions
  tick bombs → countdowns, detonations
  resolve blasts → chain reactions, map changes
  check deaths → players on explosion tiles
  pick up power-ups → extra bombs, bigger blasts, more speed
  broadcast state → all clients get the full picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  client as a dumb terminal
&lt;/h2&gt;

&lt;p&gt;The client's job is simple: send input up, render state down.&lt;/p&gt;

&lt;p&gt;When you press an arrow key, the client sends a message like &lt;code&gt;{"type": "input", "direction": "down"}&lt;/code&gt;. That's it. It doesn't move the player locally. It waits for the next game state from the server and renders the player wherever the server says they are.&lt;/p&gt;

&lt;p&gt;When you press the bomb key, the client sends &lt;code&gt;{"type": "place_bomb"}&lt;/code&gt;. No coordinates — the server looks up where you are and snaps the bomb to the nearest grid tile. The client doesn't even know where the bomb will end up until the server tells it.&lt;/p&gt;

&lt;p&gt;This feels weird coming from frontend development, where the UI is in charge. Here, the UI is a display. The keyboard is a suggestion box. The server makes all the decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  the protocol
&lt;/h2&gt;

&lt;p&gt;The whole conversation between client and server is JSON over WebSockets. Two messages go up, two come down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client → Server:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;input&lt;/code&gt; — a direction (up, down, left, right, or none)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;place_bomb&lt;/code&gt; — no payload, server figures out the rest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server → Client:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;welcome&lt;/code&gt; — sent once on connect. Contains the player's assigned ID and the full map (a flat array of 195 tiles — walls, floors, destructibles)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;game_state&lt;/code&gt; — sent every tick. Contains the tick number, all player positions, all bomb positions and timers, all explosions, and all power-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That &lt;code&gt;game_state&lt;/code&gt; message is the entire game. The client doesn't maintain any state of its own — it throws away the last frame and renders the new one. Dead simple to debug because there's only one place the truth lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  the map
&lt;/h2&gt;

&lt;p&gt;The arena is a 15x13 grid. Some tiles are indestructible walls (the border and a checkerboard of pillars), some are destructible blocks (randomly placed at ~70% density), and the rest is open floor.&lt;/p&gt;

&lt;p&gt;The map is stored as a flat array — &lt;code&gt;row * 15 + col&lt;/code&gt; gives you the index. No 2D arrays, no nested structures. Collision detection checks the four corners of a player's bounding box against the map to see if any of them land on a solid tile.&lt;/p&gt;

&lt;p&gt;One design detail: players are 48 pixels wide, but tiles are 64 pixels. That 16-pixel gap is intentional — it gives you enough forgiveness to navigate between walls without requiring pixel-perfect alignment. Without it, the game feels frustrating. With it, movement feels natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  what connects to what
&lt;/h2&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%2F85bits.pages.dev%2Fimages%2Fserver-architecture.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%2F85bits.pages.dev%2Fimages%2Fserver-architecture.png" alt="Server architecture diagram showing the game loop, shared state, WebSocket layer, and clients" width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The connection handlers and the game loop are separate async tasks that share state through &lt;code&gt;Arc&amp;lt;Mutex&amp;gt;&lt;/code&gt; — Rust's pattern for safe concurrent access. The connection handler writes player input, the game loop reads it and writes back the updated positions. The broadcast channel is a one-to-many pipe: the game loop sends once, every client receives.&lt;/p&gt;

&lt;h2&gt;
  
  
  next up
&lt;/h2&gt;

&lt;p&gt;The architecture works. The server is authoritative. The protocol is simple. But all of this had to be written in Rust — a language I'd never touched before this project.&lt;/p&gt;

&lt;p&gt;Next post: what it's actually like to write Rust when you've spent most of your career in JavaScript.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>gamedev</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>why rust, why now</title>
      <dc:creator>Tomer Levy</dc:creator>
      <pubDate>Mon, 30 Mar 2026 15:39:05 +0000</pubDate>
      <link>https://dev.to/tomerl1/im-rebuilding-a-90s-lan-game-in-rust-to-finally-learn-it-5eo8</link>
      <guid>https://dev.to/tomerl1/im-rebuilding-a-90s-lan-game-in-rust-to-finally-learn-it-5eo8</guid>
      <description>&lt;p&gt;In the late 90s, my brother, my cousin, and I would crowd around a single PC and play Atomic Bomberman for hours. Arrow keys and WASD, elbows bumping, trash talk flying. It was chaotic and it was perfect.&lt;/p&gt;

&lt;p&gt;I've been thinking about that game a lot lately. Not just the nostalgia, but the idea of playing it again — with them, over the internet this time. So I decided to build it.&lt;/p&gt;

&lt;p&gt;My first instinct was to vibe code the whole thing. Pick a framework, lean on AI, ship something in a weekend. But as I started talking through the idea with Gemini — and later Claude, who became my main AI mentor — I realized I was holding something more interesting: a real excuse to learn Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  the hard path
&lt;/h2&gt;

&lt;p&gt;I'm a senior fullstack engineer. Most of my career has been in the JavaScript ecosystem — browser, Node, React, the whole stack. I could build this in TypeScript and have it running by Sunday.&lt;/p&gt;

&lt;p&gt;But I've been curious about what happens below the abstraction layers I usually work in. What if I didn't have to hope the garbage collector stays out of the way at the wrong moment? Rust has been on my list for a long time — a multiplayer game with a 60Hz server loop felt like the right reason to finally jump in.&lt;/p&gt;

&lt;p&gt;So the plan became: Rust on the server, React and PixiJS on the client, WebSockets in between. Multiplayer-first from day one — no single-player prototype that I'd have to rip apart later.&lt;/p&gt;

&lt;h2&gt;
  
  
  learning with AI, not through AI
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised me. I started this project with Claude as my AI mentor, and the first problem we hit wasn't technical — it was process.&lt;/p&gt;

&lt;p&gt;Claude moved fast. Too fast. It would jump straight into code before I'd even digested the architecture. I'd end up with working code I didn't understand, which is the opposite of the point. We had to stop, take a step back, and establish rules.&lt;/p&gt;

&lt;p&gt;We ended up with a "mentor-review" loop: the AI specs the goal and introduces new concepts, I write the code, then we review together. No writing code on my behalf unless I'm stuck and ask for it. We established rules to keep things on track.&lt;/p&gt;

&lt;p&gt;The goal was never to get the game done as fast as possible. The goal was to learn Rust — really learn it — while building something I actually want to play.&lt;/p&gt;

&lt;h2&gt;
  
  
  what I'm building
&lt;/h2&gt;

&lt;p&gt;The game is a multiplayer Atomic Bomberman clone. Here's what's already working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; Rust + Tokio, running a 60Hz authoritative game loop over WebSockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client:&lt;/strong&gt; React + PixiJS, rendering the game state the server sends down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gameplay:&lt;/strong&gt; Players move around a 15x13 grid, place bombs, blow up destructible walls, collect power-ups, and die in explosions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; Client sends intent (direction, place bomb), server decides truth. No cheating possible because the client doesn't run any game logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's playable on localhost. Two players can connect, run around, and bomb each other. It's not pretty — everyone's a colored square — but the mechanics work.&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%2Fv2neupoyvqqvy8dw8594.gif" 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%2Fv2neupoyvqqvy8dw8594.gif" alt="Atomic Bombermap gameplay" width="560" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  what's ahead
&lt;/h2&gt;

&lt;p&gt;There's a big gap between "works on localhost" and "a game I can actually play with my brother and cousin." Movement needs to feel better. There's no lobby or game flow. And the big one: client-side prediction, so it doesn't feel sluggish over a real internet connection.&lt;/p&gt;

&lt;p&gt;This series will cover the journey — the Rust I'm learning, the multiplayer architecture decisions, the moments where things click and the moments where a deadlock silently freezes your game and you spend an hour staring at code that looks perfectly fine.&lt;/p&gt;

&lt;p&gt;Next up: how the server actually works — the authoritative model, the game loop, and why the client doesn't get to make any decisions.&lt;/p&gt;

&lt;p&gt;If any of this sounds interesting — the Rust, the networking, the nostalgia — stick around. This is going to be a fun ride. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>rust</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
