# How I Built a Real-Time Robot Battle Simulator from Scratch — Logic Arena

> Source: <https://dev.to/alihaggag7/how-i-built-a-real-time-robot-battle-simulator-from-scratch-logic-arena-1pnk>
> Published: 2026-05-23 20:42:31+00:00

*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!
