Battlezone
Battlezone (Atari, 1980), designed by Ed Rotberg, is the seminal first-person vector-graphics tank game: you drive a tank across a wireframe landscape of geometric obstacles, a distant volcano and mountains, hunting enemy tanks, saucers and guided missiles through a periscope gunsight. This build runs the original Atari board in the browser: a MOS 6502 driving the Atari Analog Vector Generator, with an AM2901 math box projecting the 3-D world and the original ROMs. It boots straight into attract mode, the tank battle demo playing out across the horizon, and is wired to the emulators.org in-frame debugger so you can single-step the 6502, read and write the registers and memory, watch the vector RAM display list build word by word, and set execution breakpoints and write watchpoints.
Visit the reference disassembly ↗
Runs on: Web browser
Battlezone Online Emulator
Play Battlezone using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Battlezone | Battlezone | Battlezone | grey | Open ⛶ |
Machine emulated
The Battlezone arcade board (Atari, 1980) is a single MOS 6502 at 1.512 MHz driving the Atari Analog Vector Generator (AVG), with an AM2901 bit-slice "math box" doing the 3-D rotate-and-project arithmetic the CPU cannot. There is no framebuffer: the CPU builds a list of lines in the RAM it shares with the AVG and the AVG steers the beam to draw them on an X-Y monitor. It was the first mass-market first-person 3-D video game, and a variant was built for the US Army as a gunnery trainer.
Chips
Notes
Embedding
Battlezone is a MOS 6502 at 1.512 MHz driving the Atari Analog Vector Generator (AVG), with an AM2901 bit-slice math box doing the 3-D rotate-and-project arithmetic the CPU can't. There is no framebuffer: the 6502 builds a display list of vectors in the RAM it shares with the AVG, then strobes it to draw. This build vendors a self-hosted machine (no CDN): a JavaScript port of Mike Chambers' public-domain Fake6502, our ports of MAME's AVG state machine and math box, and the original Atari ROMs, driven from a host-owned loop so the debugger can control it.
Boot. The six program ROMs load at $5000–$7FFF (reset/NMI vectors at $7FFA–$7FFF, reached through the hardware's 15-bit address mirror) and the two vector ROMs at $3000; the 6502 starts from $FFFC and drops straight into attract mode:
var m = new BattlezoneMachine(); // loads ROMs, builds 6502 + AVG + math box
m.reset(); // pulls the reset vector at $FFFC
// one display field: 6 NMIs (~246 Hz) of 6144 cycles, then draw the AVG list
for (var s = 0; s < 6; s++) { m.cpu.nmi(); runCycles(6144); }
m.renderTo(ctx, W, H);
The machine is plain objects. Everything the host and debugger need is a field or method on the machine or its CPU:
| Member | Kind | What it does |
|---|---|---|
m.cpu.step() | method | Fetch, decode and run one 6502 instruction; returns its cycle count. The single-step primitive. |
m.cpu.nmi() | method | Pulse the non-maskable interrupt -- the ~246 Hz tick that drives the whole game. |
m.avgGo() | method | Run the AVG state machine over vector RAM/ROM and collect the frame's line segments. Triggered when the CPU strobes 200. |
m.mathbox.go(o,v) | method | The math box command window (860–87F): pokes an operand and, on the last write of a group, runs a projection whose result the CPU reads at 810/818. |
m.dbgRead(a) / m.memWrite(a,v) | method | Side-effect-free read and the CPU write path -- what the debugger's memory views read and poke. |
m.setInput(id,down) | method | Drive a control (lfwd/lback/rfwd/rback/fire/coin/start1) into the input latches. |
m.cpu.a/x/y/sp/pc/flags | field | The live 6502 registers, read and written directly. |
Video. Each field the loop runs one display's worth of 6502 (six NMI slices), then walks the AVG display list the game left in vector RAM and strokes the green wireframe lines to the canvas.
Debugger integration
The debugger drives a host-owned run loop: because the 6502 interpreter runs one instruction at a time, the loop can pause, single-step and check breakpoints between any two instructions -- no changes to the CPU core are needed. window.EMU_BOOT.transport exposes the controls the shared debugger calls:
- pause / resume / isPaused -- stop or restart the
requestAnimationFrameloop. - stepInsn(n) -- call
cpu.step()exactly n times and redraw, so a single step advances the PC by one instruction. - step(n) -- advance n whole display fields (each with its six NMIs).
- breakpoints -- a
Setof PC values. When non-empty the loop runs instruction-by-instruction and pauses before executing a watched address; when empty it runs a field at speed, so an idle debugger costs nothing. - watchpoints -- the machine's
memWriteis the single write path; it flags a hit when a watched address is written and the loop pauses on it. Because the 6502's registers and the whole map are ordinary JavaScript, registers are read and written directly and memory is exposed side-effect-free.
The plug-in (battlezone-debug.js) reads these hooks and calls EmuKit.defineMachine with the 6502 register set bound to the shared mos6502 disassembler, four memory chips (the program space, the program ROM, the vector RAM display list and the vector ROM shape library), and the dual-stick control panel. A neat trick for this machine: point the Vector RAM hex window at $2000 and single-step through a frame -- you can watch the 6502 (fed by the math box) build the tank and horizon into the display list word by word before it strobes the AVG at 200.
Architecture
Battlezone has no video RAM in the usual sense. A single MOS 6502, helped by a math-box coprocessor, builds a list of vectors in the RAM it shares with the AVG, then strobes it; the AVG reads that list and draws a first-person wireframe on an X-Y monitor. Modelled here as plain JavaScript objects:
- CPU -- a port of Fake6502 (NMOS 6502), reaching memory only through the machine's
memRead/memWrite, so the 15-bit bus and I/O live in one place. - AVG -- the Analog Vector Generator, a state machine clocked through a 256×4 PROM (
036408-01.k7): each step the PROM maps the current state, op-code and halt flag to the next state, three of whose bits fire the strobe handlers that latch vector words and integrate the beam, with Battlezone's clipping window so the 3-D view never overwrites the gunsight and radar. Ported straight from MAME'savg_bzone_device. - Math box -- an AM2901 bit-slice unit that does the rotate/project multiplies; modelled by Eric Smith's functional reimplementation (all 16-bit signed, exact two's-complement wrap via an
Int16Array) rather than its microcode PROMs. - Bus / I/O -- 1 KB RAM, IN0 (coins, service, VG-halt, 3 kHz clock), two DIP ports, the POKEY window (the two 2-way tank sticks read through ALLPOT, its RANDOM byte, and now its four audio voices), the AVG GO/reset strobes, the math-box command window and the
840sound latch driving the motor rumble. Sound is modelled (see below). - ROMs -- 12 KB of 6502 program (six mask ROMs) and 4 KB of vector ROM holding the letters, digits and shapes the display list calls into.
The one piece of timing the game truly needs is its ~246 Hz NMI (the 12.096 MHz master clock divided by 4096 then 12); the host loop reproduces exactly that, six NMIs per ~41 Hz display field, and renders the AVG list at the end of each field.
Sound
Pattern S — authored synthesis. The core emulates no sound hardware, so the sound is produced by authored models of the cabinet's two sources: an Atari POKEY (C012294) for the game's tones, and a synthesised approximation of the discrete analogue motor rumble. Both are self-contained files under the emulator (bzone-pokey.js, bzone-motor.js) and are mixed to one stereo stream.
POKEY. Battlezone's POKEY sits at 820–82F; the board already reads the two 2-way tank sticks through its ALLPOT pins and takes a RANDOM byte from it, and now its four audio voices sound too. Each voice is an 8-bit frequency divider (AUDF) plus a control byte (AUDC: 4-bit volume, distortion select, volume-only bit), with a global AUDCTL (clock-speed select and 16-bit voice pairing). The model runs the chip's 1.512 MHz input clock (12.096 MHz / 8): the 4/5/9/17-bit polynomial counters advance every tick, a divider makes the 64 kHz (or 15 kHz) reference clock, and each voice's divider is clocked by that reference or, when AUDCTL selects it, directly by the 1.5 MHz clock; a voice's flip-flop is re-evaluated on each underflow (toggle for a pure tone, else the 4-bit or 17/9-bit poly, gated by the 5-bit poly) and the four 4-bit volumes are summed. The rarely-used per-voice high-pass filters are the only part not modelled.
Motor. The cabinet's iconic idle rumble is not a POKEY voice but a discrete analogue circuit — a low VCO plus filtered noise — gated and pitched by the CPU's sound latch at 840. There is no chip to emulate register-for-register, so bzone-motor.js is a faithful approximation: a low sawtooth VCO (harmonically rich, so it carries on small speakers though its ~40–70 Hz fundamental is below many of them) whose pitch tracks the latch's "rev" bits, plus low-pass-filtered noise for the diesel grit, a slow idle wobble so a static latch still breathes, and a smoothed enable envelope. It idles whenever the machine runs, so attract mode already rumbles.
Hook & rate. The single write path (memWrite) forwards each write in 820–82F into the POKEY and each write to 840 into the motor. Both are built at EmuAudio.sampleRate (the POKEY converts its 1.5 MHz base clock with a fractional accumulator; the motor's phase increments are per-sample), so pitch is correct at any device rate with no resampling. Each video frame the loop calls machine.generateAudio(Math.round(EmuAudio.sampleRate/60)), which sums POKEY + motor, clamps and returns interleaved-stereo Int16 (mono duplicated to L/R); the loop EmuAudio.push()es it.
Mute contract. window.EMU_BOOT.transport exposes isMuted() and setMute(bool), delegating to EmuAudio. It starts muted (browsers block audio before a user gesture); the shell's Sound button unmutes from a real click. Once unmuted the motor is already idling in attract mode; insert a coin and press 1P-Start to play, and firing and explosions add the POKEY tones on top of the rumble.