<?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: Ali Haggag</title>
    <description>The latest articles on DEV Community by Ali Haggag (@alihaggag7).</description>
    <link>https://dev.to/alihaggag7</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%2F3948212%2F51cba7b3-21d4-40a0-8a48-a257091c0c42.jpeg</url>
      <title>DEV Community: Ali Haggag</title>
      <link>https://dev.to/alihaggag7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alihaggag7"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time Robot Battle Simulator from Scratch — Logic Arena</title>
      <dc:creator>Ali Haggag</dc:creator>
      <pubDate>Sat, 23 May 2026 20:42:31 +0000</pubDate>
      <link>https://dev.to/alihaggag7/how-i-built-a-real-time-robot-battle-simulator-from-scratch-logic-arena-1pnk</link>
      <guid>https://dev.to/alihaggag7/how-i-built-a-real-time-robot-battle-simulator-from-scratch-logic-arena-1pnk</guid>
      <description>&lt;p&gt;&lt;em&gt;A CS student's journey from a blank repo to a production platform with a custom scripting language, 60fps physics engine, and 3,000+ lines of battle-tested TypeScript.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I wanted to build something that made competitive programming &lt;strong&gt;fun&lt;/strong&gt;. Not another LeetCode clone. Something where your algorithm doesn't just pass a test case — it &lt;strong&gt;fights&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The concept: players write code to control robots. The robots battle in real-time. Your logic vs the world.&lt;/p&gt;

&lt;p&gt;That's Logic Arena.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting From Zero — The Engine
&lt;/h2&gt;

&lt;p&gt;The first challenge was building a game engine from scratch. No Unity, no Phaser — pure TypeScript.&lt;/p&gt;

&lt;p&gt;Version 0.2.0 was just two robots moving in a canvas. But getting there required solving the first real technical scar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Singleton Problem.&lt;/strong&gt; Two NestJS services were sharing the game engine — or so I thought. They each had their own instance, so state was completely desynced. The fix: &lt;code&gt;@Global()&lt;/code&gt; decorator to enforce a single shared engine across the entire module.&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Global&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GameService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GameService&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;class&lt;/span&gt; &lt;span class="nc"&gt;GameModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One decorator. Three hours of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building AliScript — A Custom Scripting Language
&lt;/h2&gt;

&lt;p&gt;This was the craziest decision I made. Instead of using JavaScript or Lua for robot scripting, I built my own language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; I wanted players to think algorithmically, not just copy-paste code. AliScript had to be simple enough for beginners but powerful enough to reward expert thinking.&lt;/p&gt;

&lt;p&gt;The pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String Script → Lexer → Tokens → Parser → AST → Server Evaluator → Robot Actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First version supported basic conditionals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sqf"&gt;&lt;code&gt;&lt;span class="kr"&gt;IF&lt;/span&gt; &lt;span class="nv"&gt;CAN_SEE_ENEMY&lt;/span&gt;
  &lt;span class="nf"&gt;FIRE&lt;/span&gt;
&lt;span class="vg"&gt;END&lt;/span&gt;
&lt;span class="nf"&gt;MOVE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By v2.4, it became Turing-complete-ish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sqf"&gt;&lt;code&gt;&lt;span class="kr"&gt;WHILE&lt;/span&gt; &lt;span class="kc"&gt;TRUE&lt;/span&gt; &lt;span class="kr"&gt;DO&lt;/span&gt;
  &lt;span class="kr"&gt;IF&lt;/span&gt; &lt;span class="nv"&gt;CAN_SEE_ENEMY&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;MY_ENERGY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;30&lt;/span&gt; &lt;span class="kr"&gt;DO&lt;/span&gt;
    &lt;span class="vg"&gt;BROADCAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;NEAREST_VISIBLE_X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;BURST_FIRE&lt;/span&gt;
  &lt;span class="kr"&gt;ELSE&lt;/span&gt; &lt;span class="kr"&gt;IF&lt;/span&gt; &lt;span class="nv"&gt;IN_STASIS&lt;/span&gt; &lt;span class="kr"&gt;DO&lt;/span&gt;
    &lt;span class="vg"&gt;WAIT&lt;/span&gt;
  &lt;span class="kr"&gt;ELSE&lt;/span&gt; &lt;span class="kr"&gt;DO&lt;/span&gt;
    &lt;span class="vg"&gt;SCAN&lt;/span&gt;
    &lt;span class="vg"&gt;PATHFIND&lt;/span&gt;
  &lt;span class="vg"&gt;END&lt;/span&gt;
&lt;span class="vg"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hardest bug? &lt;strong&gt;Operator precedence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;2 + 3 * 4&lt;/code&gt; was evaluating to &lt;code&gt;20&lt;/code&gt; instead of &lt;code&gt;14&lt;/code&gt;. Every multiplication-heavy script was silently computing wrong values. The fix required splitting &lt;code&gt;parseBinaryExpression()&lt;/code&gt; into two separate precedence levels — &lt;code&gt;parseAddition()&lt;/code&gt; and &lt;code&gt;parseMultiply()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Crisis
&lt;/h2&gt;

&lt;p&gt;By v1.3.0, the arena was running at ~15fps. Profiling revealed the culprit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3,862ms scripting bottleneck&lt;/strong&gt; — the main thread was choking on &lt;code&gt;useState&lt;/code&gt; updates 60 times per second.&lt;/p&gt;

&lt;p&gt;The fix was a dual-state architecture:&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="c1"&gt;// Before: useState causes re-render on every tick&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;gameState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGameState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="c1"&gt;// After: useRef for game logic, throttled state for UI only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gameStateRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="c1"&gt;// updates at 60fps, zero re-renders&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUiState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="c1"&gt;// updates at 10fps, DOM only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scripting time dropped from 3,862ms to near zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Performance Fixes in One Release
&lt;/h2&gt;

&lt;p&gt;By v2.5.0, I ran a full profiling audit and found six simultaneous bottlenecks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Obstacles in every WebSocket payload&lt;/strong&gt; — static data sent 10x/sec for no reason. Fix: initialize once, strip from all subsequent payloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 30 WebGL draw calls for obstacles&lt;/strong&gt; — rewrote to &lt;code&gt;THREE.InstancedMesh&lt;/code&gt;. 30 draw calls → 4 draw calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 10 &lt;code&gt;useFrame&lt;/code&gt; JS callbacks for obstacle animations&lt;/strong&gt; — moved all pulse math to GPU via fragment shaders. 0 JS callbacks per frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Server memory leak&lt;/strong&gt; — replay snapshots were deep-cloning unboundedly. Added ring buffer capped at 300 objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The Ghost Match Massacre&lt;/strong&gt; — when players closed their browser, the physics engine kept running at full speed indefinitely. Hundreds of ghost matches accumulating CPU in silence. Fix: wired disconnect lifecycle to stop the engine the moment the last player leaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deployment Nightmare
&lt;/h2&gt;

&lt;p&gt;Going from localhost to production at logicarena.dev was a gauntlet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 2.57GB Docker Context Bomb&lt;/strong&gt; — first build transferred 2.57GB to the Docker daemon because &lt;code&gt;node_modules&lt;/code&gt; wasn't excluded. Fixed &lt;code&gt;.dockerignore&lt;/code&gt;, dropped build context to 15MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Prisma Ghost Engine&lt;/strong&gt; — Alpine Linux refused to execute the Windows-compiled Prisma binary. Added &lt;code&gt;linux-musl&lt;/code&gt; to &lt;code&gt;binaryTargets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The WebSocket CORS Wall&lt;/strong&gt; — every Socket.IO connection rejected because the gateway was hardcoded to &lt;code&gt;localhost:3000&lt;/code&gt;. Extended CORS array, fixed Nginx WebSocket proxy headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Silent Postman&lt;/strong&gt; — DigitalOcean silently blocks outbound SMTP on new Droplets. Zero errors, zero delivery, just void.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;logic-arena/&lt;/span&gt;
&lt;span class="s"&gt;├── apps/&lt;/span&gt;
&lt;span class="s"&gt;│   ├── client/&lt;/span&gt;     &lt;span class="c1"&gt;# Next.js 16, React Three Fiber, PWA&lt;/span&gt;
&lt;span class="s"&gt;│   └── server/&lt;/span&gt;     &lt;span class="c1"&gt;# NestJS 11, Socket.io, JWT&lt;/span&gt;
&lt;span class="s"&gt;└── packages/&lt;/span&gt;
    &lt;span class="s"&gt;├── engine/&lt;/span&gt;     &lt;span class="c1"&gt;# Custom TypeScript physics engine&lt;/span&gt;
    &lt;span class="s"&gt;└── logic-parser/&lt;/span&gt; &lt;span class="c1"&gt;# AliScript lexer, parser, evaluator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Architecture decisions compound.&lt;/strong&gt; Every "quick fix" that bypassed the module system created three bugs later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Profile before optimizing.&lt;/strong&gt; The 3,862ms bottleneck was invisible until I actually measured it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Custom languages are feasible.&lt;/strong&gt; An AST evaluator is just a recursive tree walker — intimidating name, straightforward implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Ship early, iterate fast.&lt;/strong&gt; Logic Arena went from 2 robots on a canvas to a 60-level algorithmic campaign in 4 months of solo development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://logicarena.dev" rel="noopener noreferrer"&gt;logicarena.dev&lt;/a&gt; — no account required, join as guest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ali-Haggag7/logic-arena" rel="noopener noreferrer"&gt;Ali-Haggag7/logic-arena&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Write your first script, watch your robot fight, and tell me what you think in the comments!&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
