How I Built a Real-Time Robot Battle Simulator from Scratch — Logic Arena This article details the development of Logic Arena, a real-time robot battle simulator built from scratch by a computer science student using TypeScript. The project features a custom scripting language called AliScript, a 60fps physics engine, and required solving numerous technical challenges including state synchronization, performance bottlenecks, and production deployment issues. The platform allows players to write code that controls robots in real-time combat, transforming competitive programming into an interactive battle experience. 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. The Idea I wanted to build something that made competitive programming fun . Not another LeetCode clone. Something where your algorithm doesn't just pass a test case — it fights . The concept: players write code to control robots. The robots battle in real-time. Your logic vs the world. That's Logic Arena. Starting From Zero — The Engine The first challenge was building a game engine from scratch. No Unity, no Phaser — pure TypeScript. Version 0.2.0 was just two robots moving in a canvas. But getting there required solving the first real technical scar: The Singleton Problem. 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: @Global decorator to enforce a single shared engine across the entire module. @Global @Module { providers: GameService , exports: GameService , } export class GameModule {} One decorator. Three hours of debugging. Building AliScript — A Custom Scripting Language This was the craziest decision I made. Instead of using JavaScript or Lua for robot scripting, I built my own language. Why? 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. The pipeline: String Script → Lexer → Tokens → Parser → AST → Server Evaluator → Robot Actions First version supported basic conditionals: IF CAN SEE ENEMY FIRE END MOVE By v2.4, it became Turing-complete-ish: WHILE TRUE DO IF CAN SEE ENEMY AND MY ENERGY 30 DO BROADCAST NEAREST VISIBLE X BURST FIRE ELSE IF IN STASIS DO WAIT ELSE DO SCAN PATHFIND END END The hardest bug? Operator precedence. 2 + 3 4 was evaluating to 20 instead of 14 . Every multiplication-heavy script was silently computing wrong values. The fix required splitting parseBinaryExpression into two separate precedence levels — parseAddition and parseMultiply . The Performance Crisis By v1.3.0, the arena was running at ~15fps. Profiling revealed the culprit: 3,862ms scripting bottleneck — the main thread was choking on useState updates 60 times per second. The fix was a dual-state architecture: // Before: useState causes re-render on every tick const gameState, setGameState = useState null ; // After: useRef for game logic, throttled state for UI only const gameStateRef = useRef null ; // updates at 60fps, zero re-renders const uiState, setUiState = useState null ; // updates at 10fps, DOM only Scripting time dropped from 3,862ms to near zero. Six Performance Fixes in One Release By v2.5.0, I ran a full profiling audit and found six simultaneous bottlenecks: 1. Obstacles in every WebSocket payload — static data sent 10x/sec for no reason. Fix: initialize once, strip from all subsequent payloads. 2. 30 WebGL draw calls for obstacles — rewrote to THREE.InstancedMesh . 30 draw calls → 4 draw calls. 3. 10 useFrame JS callbacks for obstacle animations — moved all pulse math to GPU via fragment shaders. 0 JS callbacks per frame. 4. Server memory leak — replay snapshots were deep-cloning unboundedly. Added ring buffer capped at 300 objects. 5. The Ghost Match Massacre — 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. The Deployment Nightmare Going from localhost to production at logicarena.dev was a gauntlet. The 2.57GB Docker Context Bomb — first build transferred 2.57GB to the Docker daemon because node modules wasn't excluded. Fixed .dockerignore , dropped build context to 15MB. The Prisma Ghost Engine — Alpine Linux refused to execute the Windows-compiled Prisma binary. Added linux-musl to binaryTargets . The WebSocket CORS Wall — every Socket.IO connection rejected because the gateway was hardcoded to localhost:3000 . Extended CORS array, fixed Nginx WebSocket proxy headers. The Silent Postman — DigitalOcean silently blocks outbound SMTP on new Droplets. Zero errors, zero delivery, just void. The Architecture Today logic-arena/ ├── apps/ │ ├── client/ Next.js 16, React Three Fiber, PWA │ └── server/ NestJS 11, Socket.io, JWT └── packages/ ├── engine/ Custom TypeScript physics engine └── logic-parser/ AliScript lexer, parser, evaluator What I Learned 1. Architecture decisions compound. Every "quick fix" that bypassed the module system created three bugs later. 2. Profile before optimizing. The 3,862ms bottleneck was invisible until I actually measured it. 3. Custom languages are feasible. An AST evaluator is just a recursive tree walker — intimidating name, straightforward implementation. 4. Ship early, iterate fast. Logic Arena went from 2 robots on a canvas to a 60-level algorithmic campaign in 4 months of solo development. Try It Live: logicarena.dev https://logicarena.dev — no account required, join as guest. GitHub: Ali-Haggag7/logic-arena https://github.com/Ali-Haggag7/logic-arena Write your first script, watch your robot fight, and tell me what you think in the comments