Web Implementation Architecture

Scorched Earth v1.50 — faithfully reverse-engineered from the 415KB DOS binary and rebuilt for the web.

1. Overview

22 modules, 5,925 lines of JavaScript, zero dependencies. Every game mechanic — physics constants, weapon behaviors, AI solver, palette layout, damage formulas — was extracted from the original SCORCH.EXE binary via disassembly and reimplemented in vanilla JS with ES modules.

MetricValue
JS modules22 files
Total lines5,925
External dependencies0
Build stepNone — raw ES modules
Resolution320×200 (VGA Mode 13h)
RenderingWebGL (GPU) or Canvas2D (CPU fallback)
Weapons implemented57 (all from EXE weapon table)
AI types7 (Moron through Spoiler)

2. Module Dependency Graph

main.js ─────────────────────────────────────────── 457 lines
  ├── game.js (state machine, turn logic) ──────── 922 lines
  │   ├── physics.js (projectile simulation) ───── 358 lines
  │   ├── behaviors.js (weapon dispatch) ────────── 550 lines
  │   ├── ai.js (trajectory solver) ─────────────── 331 lines
  │   ├── explosions.js (craters, damage) ───────── 326 lines
  │   ├── shields.js (deflection, absorption) ──── 225 lines
  │   ├── score.js (scoring modes) ──────────────── 75 lines
  │   ├── shop.js (buy/sell UI) ─────────────────── 219 lines
  │   ├── talk.js (speech bubbles) ──────────────── 196 lines
  │   └── sound.js (PC speaker emulation) ───────── 84 lines
  ├── framebuffer.js (VGA VRAM emulation) ───────── 212 lines
  ├── palette.js (256-color VGA DAC) ────────────── 300 lines
  ├── terrain.js (landscape generation) ─────────── 342 lines
  ├── tank.js (player/tank management) ──────────── 309 lines
  ├── hud.js (status bar) ───────────────────────── 126 lines
  ├── menu.js (config screens) ──────────────────── 271 lines
  ├── font.js (pixel font renderer) ─────────────── 237 lines
  └── input.js (keyboard handler) ───────────────── 31 lines

Shared utilities:
  config.js ─── game settings (57 lines)
  utils.js ──── math helpers (49 lines)
  weapons.js ── weapon data table (248 lines)

3. VGA Framebuffer Emulation

The Core Idea

The original game renders to VGA Mode 13h: a linear 320×200 framebuffer at segment A000h, where each byte is a palette index (0–255) that maps through the VGA DAC to an RGB color. The web version faithfully replicates this architecture:

// The indexed pixel buffer (VGA VRAM equivalent)
const pixels = new Uint8Array(320 * 200);  // 64,000 bytes

// Set a pixel: just store a palette index
pixels[y * 320 + x] = colorIndex;

Why This Matters

In the original game, VRAM is the world model. There is no collision mesh, no spatial hash, no physics geometry. Collision detection reads pixel colors directly from the framebuffer:

Pixel ValueMeaningAction
0Black/backgroundPass through
1–79Tank body (floor(pixel/8) = player index)Hit tank
80–103Sky gradientPass through
104System black (HUD)Pass through
105–149TerrainHit terrain
170–199Explosion firePass through

Dual Renderer

The blit() function converts palette indices to RGB for display, with two backends:

WebGL (primary) — uploads the 320×200 index buffer as a LUMINANCE texture. A 256×1 RGBA palette texture acts as the VGA DAC. The fragment shader performs the lookup on the GPU:

// Fragment shader: VGA DAC lookup
float i = texture2D(u_idx, v_uv).r;
gl_FragColor = texture2D(u_pal, vec2((i*255.0+0.5)/256.0, 0.5));

Canvas2D (fallback) — CPU loop maps each palette index through a Uint32Array lookup table to an ImageData buffer:

for (let i = 0; i < 64000; i++) {
    buf32[i] = palette32[pixels[i]];
}
ctx.putImageData(imageData, 0, 0);

4. State Machine

The game loop runs via requestAnimationFrame, dispatching to the current state each frame. The state machine mirrors the original EXE's play.cpp dispatch at file offset 0x2F78A:

StatePurposeTransitions to
TITLETitle screenCONFIG
CONFIGGame settingsPLAYER_SETUP
PLAYER_SETUPPlayer names & AI typesROUND_SETUP
AIMHuman/AI aim adjustmentFLIGHT
FLIGHTProjectile physics simulationEXPLOSION
EXPLOSIONCrater animation + damageFALLING / NEXT_TURN / ROUND_OVER
FALLINGUnsupported tanks dropNEXT_TURN / ROUND_OVER
NEXT_TURNWind update, advance playerAIM / SCREEN_HIDE / SYNC_AIM
ROUND_OVERWar quote + scoringSHOP / GAME_OVER
SHOPBuy/sell equipmentROUND_SETUP
ROUND_SETUPNew terrain + place tanksAIM / SYNC_AIM
SYNC_AIMAll players aim (sync/simultaneous)SYNC_FIRE
SYNC_FIREBatch launch all shotsFLIGHT
SCREEN_HIDE"NO KIBITZING" interstitialAIM

The core gameplay cycle: AIM → FLIGHT → EXPLOSION → FALLING → NEXT_TURN, repeating until one player survives.

5. Physics Engine

EXE-Calibrated Constants

DT       = 0.02      // timestep (EXE default, calibrated via CPU MIPS benchmark)
GRAVITY  = 4.9       // px/sec² downward
WIND_SCALE = 0.15    // wind config → horizontal accel
MAX_SPEED  = 400     // px/sec at power=1000

Per-Step Integration Order

Matches EXE simulation loop at file 0x21A80–0x21D09:

  1. Mag Deflector — deflect velocity based on nearby players with Mag items
  2. Speed limit — cap velocity magnitude to MAX_SPEED
  3. Positionx += vx*dt, y -= vy*dt (screen Y inverted)
  4. Viscosity — multiplicative damping: v *= (1.0 - viscosity/10000)
  5. Gravityvy -= GRAVITY * dt
  6. Windvx += wind * WIND_SCALE * dt (horizontal only)

Wall Types (8 modes)

TypeBehavior
NoneProjectiles fly offscreen
WrapLeft edge wraps to right
PaddedReflect with 0.5× velocity
RubberReflect with 0.8× velocity
SpringReflect with 1.2× velocity
ConcreteDetonate on wall impact
RandomResolved once per round
ErraticResolved each turn

Collision Detection

Pure pixel-color sampling from the framebuffer — no geometry calculations. Each physics step calls getPixel(x, y) and checks: >0 && <80 = tank hit, ≥105 = terrain hit, anything else = sky (pass through).

6. Weapon System

57 weapons extracted from the EXE's struct array at file offset 0x056F76. Each weapon has a 52-byte struct with price, bundle quantity, arms level, behavior type code, and blast radius.

Behavior Dispatch

13 behavior handlers, dispatched by BHV type code (matching the EXE's far call through weapon struct pointers):

BHVHandlerWeapons
0x0021Standard — simple radius blastBaby Missile, Missile, Baby Nuke, Nuke
0x0002Tracer — no damage, shows pathTracer, Smoke Tracer
0x0003Roller — flight then terrain-followBaby Roller, Roller, Heavy Roller
0x0006Bounce — LeapFrog reflectionsLeapFrog
0x0239MIRV — split at apogee (vy sign flip)MIRV, Death's Head
0x01A0Napalm — fire particle spreadNapalm, Hot Napalm, Ton of Dirt
0x0009Dirt — add terrain (inverse crater)Heavy Sandhog, Dirt Clod, Dirt Ball, Dirt Tower
0x000ATunnel — dig through terrainDiggers, Sandhogs, Heavy Riot Bomb
0x000DPlasma — speed-based variable radiusPlasma Blast, Riot Charge
0x03BDRiot — earth-moving explosionRiot Blast, Riot Bomb
0x0004Disrupter — force suspended dirt to fallEarth Disrupter
0x0081Liquid — napalm-style dirt spreadLiquid Dirt
0x013EDirt Charge — explosion + dirt fillDirt Charge

Special cases: Funky Bomb (BHV=0x0000 but handler segment 0x1DCE) scatters 5–10 sub-bombs from screen top. Popcorn Bomb has no struct data at all.

7. AI System

7 AI types from Moron (wildly inaccurate) to Spoiler (strategically chaotic). Cyborg and Unknown randomize to types 1–6 each turn.

Analytic Ballistic Solver

The web AI uses iterative trajectory simulation: for each candidate angle (coarse sweep, then fine-tune), simulate the trajectory accounting for gravity and wind, and pick the angle/power combo that lands closest to the target.

Sinusoidal Noise Model

Instead of uniform random noise, the AI uses multi-harmonic sine waves to produce smooth, repeatable aim wobble. The original EXE generates 2–5 harmonics with rejection-sampled frequencies:

noise = amplitude * (sin(t*3.7)*0.5 + sin(t*7.3)*0.3 + sin(t*13.1)*0.2) * 0.15

Noise Parameters per AI Type

AI TypeAngle NoisePower NoiseWeapon NoiseCharacter
Moron505050Wildly inaccurate
Shooter232323Accurate marksman
Poolshark232323Accurate (same as Shooter)
Tosser632323Wild angle, good power
Chooser636323Wild aim, smart weapons
Spoiler636363Maximum chaos

8. VGA Palette System

256-color palette matching the original VGA DAC layout. Each entry is 6-bit RGB (0–63), upscaled to 8-bit for display.

RangeEntriesPurpose
0–7980Player colors (10 players × 8 gradient slots: dark→light body, full color, white flash, smoke)
80–10324Sky gradient (7 types: Plain, Shaded, Stars, Storm, Sunset, Cavern, Black)
1041System black (HUD background)
105–11915Unused
120–14930Terrain gradient (6 types: Blue Ice, Snow, Rock, Night, Desert, Varied)
1501Wall color (gray)
170–17910Explosion: Dark Red fire
180–18910Explosion: Orange fire
190–19910Explosion: Yellow fire
2531Laser sight green
2541Laser sight white (Plasma)

The 10 player base colors (VGA 6-bit, from DS:0x57E2): Red, Lime Green, Purple, Yellow, Cyan, Magenta, White, Orange, Sea Green, Blue.

9. Sound System

PC speaker emulation via Web Audio API. The original game programs the PC speaker timer (port 42h) for square wave tones. The web version uses OscillatorNode with frequency sweeps:

SoundStart HzEnd HzDurationEXE Source
Fire4008000.15sINT 61h timer programming
Explosion200400.1–0.8s (scales with radius)Port 42h sweep
Flight proximity1000+varies0.03s per frameDistance-based pitch
Lightning20002000.1sHostile environment
Tank death80200.3sLow frequency thud

All sounds use square wave oscillators (type: 'square') to match the harsh timbre of the original PC speaker. Audio context is lazy-initialized on first user gesture to comply with browser autoplay policies.