SearchA-ZG › Gauntlet

Gauntlet

1985 Arcade Source available Online

Gauntlet (Atari Games, 1985) is the arcade game that popularised four-player cooperative play: up to four heroes - Warrior, Valkyrie, Wizard and Elf - fight through top-down mazes swarming with ghosts, grunts and demons while a digitised voice narrates ("Warrior needs food, badly"). This build runs the original Atari "Gauntlet" board in the browser - a Motorola 68010 main CPU, a MOS 6502 sound CPU and the YM2151, POKEY and TMS5220 speech chips, compiled from C++ to WebAssembly - and boots straight into attract mode. It is wired to the emulators.org in-frame debugger so you can single-step the 68000, read and write its registers and memory, and set execution breakpoints and write watchpoints.

Visit the source repository ↗

Visit the official site ↗

Runs on: Web browser

Gauntlet Online Emulator

Play Gauntlet using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
GauntletGauntletGauntletgreyOpen ⛶
Gauntlet (2 Players)GauntletGauntletgreyOpen ⛶
Gauntlet IIGauntletGauntletgreyOpen ⛶

Machine emulated

The Atari "Gauntlet" board (Atari Games, 1985) - a Motorola 68010 main CPU at ~7.16 MHz, a MOS 6502 sound CPU at ~1.79 MHz, and a trio of sound chips: a Yamaha YM2151 FM synthesiser, an Atari POKEY, and a TMS5220 speech synthesiser for the game's spoken warnings. Video is a scrolling tile playfield plus an alphanumeric layer and a motion-object (sprite) engine scanned as a 336×240 raster, and Atari's slapstic PAL scrambles the 68000 ROM addresses as copy protection. The same board hosts Gauntlet II (1986).

Chips

Notes

Embedding

The core is FinalBurn Neo (FBNeo) - a mature, accurate arcade emulator - compiled to WebAssembly with Emscripten and driving its atari/d_gauntlet driver. FBNeo normally ships as a libretro core or an SDL app; instead of pulling in either frontend we wrote a tiny standalone Emscripten frontend, gauntlet_wasm.cpp, that talks to FBNeo's OWN C++ burn-library API (BurnLibInit / BurnDrvSelect / BurnDrvInit / BurnDrvFrame) and exposes it to JavaScript through a handful of flat functions. The module is built with -s MODULARIZE=1 so gauntlet.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt gauntlet.js / gauntlet.wasm and each game as a standard FBNeo/MAME romset ZIP.

// gauntlet.js defines a global Module factory; instantiate it, stage game name + romset, boot.
var g = await Module({ locateFile: f => SRC + f });     // finds gauntlet.wasm
var np = g._wasm_game_buf(name.length); g.HEAPU8.set(name, np); // "gauntlet"
var rp = g._wasm_rom_buf(rom.length); g.HEAPU8.set(rom, rp);   // stage the romset ZIP
g._wasm_start();                                      // select driver, load ROMs, BurnDrvInit, reset

Rendering is manual. FBNeo rasters into its own bitmap (pBurnDraw); the frontend converts the visible 336×240 window to RGBA in the WASM heap. Each frame we blit that buffer to an off-screen canvas and draw it scaled to the visible canvas:

var rgba = new Uint8ClampedArray(g.HEAPU8.buffer, g._wasm_fb_ptr(), 336*240*4);
backImg.data.set(rgba); bctx.putImageData(backImg, 0, 0);
ctx.drawImage(back, 0,0, 336,240, 0,0, canvas.width, canvas.height);

Input is the Gauntlet player latch. Each of the four heroes has an 8-way stick plus a Fire and a Magic button, and there is a per-player Coin slot (Gauntlet has no Start button - inserting a coin drops a hero straight into the maze). We keep the pressed bits in JS and push them once per frame; the frontend maps them onto FBNeo's per-input pVal bytes (found by name through BurnDrvGetInputInfo) that the 68000 reads.

MemberKindWhat it does
_wasm_game_buf(n) / _wasm_rom_buf(n)exportAllocate + return a staging pointer for the driver short-name and the romset ZIP.
_wasm_start()exportSelect the Gauntlet driver by name, install a zip-backed BurnExtLoadRom, BurnDrvInit, wire input, reset.
_wasm_tick()exportRun exactly one video frame (BurnDrvFrame) and convert it to RGBA. Our frame-step and the run loop's advance.
_wasm_fb_ptr()exportPointer to the 336×240 RGBA framebuffer in the heap.
_wasm_set_input(player,bits)exportWrite a player latch (bit0 Up, bit1 Down, bit2 Left, bit3 Right, bit4 Fire, bit5 Magic; pressed=1).
_wasm_set_sys(v)exportCoin bits (bit0 P1 … bit3 P4) - needed to credit the game.
_wasm_dbg_*exportThe debug sampling API (registers, memory, step, reset) - see below.
_wasm_vw() / _wasm_vh()exportVisible picture size (336×240).

Debugger integration

This is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines - real registers, real memory, a real single-instruction step, plus execution breakpoints and memory watchpoints. FBNeo's 68000 keeps its register file in a plain C context reachable through m68k_get_reg / m68k_set_reg, and SekRun(0,1) runs exactly one instruction. We expose these through a small sampling API from our standalone frontend.

1 · Exactly what was added (the only new source). A single new file, gauntlet_wasm.cpp, is the whole frontend: it drives the burn library (BurnLibInit / BurnDrvSelect / BurnDrvInit / BurnDrvFrame) and appends a block of EMSCRIPTEN_KEEPALIVE functions that read live state through FBNeo's own 68000 interface. They are sampling functions: the debugger calls them ~10×/second while its window is open, and once per Step. There is no per-instruction or per-cycle callback anywhere - the emulator hot path (BurnDrvFrame) is byte-for-byte unchanged (the golden performance rule).

// gauntlet_wasm.cpp - sampling only, no hot-path hook
u32  wasm_dbg_reg(int i){                        // 0-7 = D0-D7, 8-15 = A0-A7
  return i<8 ? m68k_get_reg(NULL, M68K_REG_D0+i)
            : m68k_get_reg(NULL, M68K_REG_A0+(i-8)); }
u32  wasm_dbg_pc(){ return m68k_get_reg(NULL, M68K_REG_PC); }
u32  wasm_dbg_read(u32 a){                     // side-effect-free: pointer-mapped pages only
  if (a <= 0x037fff || (a>=0x040000 && a<=0x07ffff)   // 68K program ROM
      || (a>=0x800000 && a<=0x801fff))            // 68K work RAM
    return SekReadByte(a);
  return 0; }                                  // slapstic / I-O windows read 0
void wasm_dbg_step(){ SekRun(0, 1); }              // REAL one-instruction step

2 · What is REAL here. Because FBNeo keeps the 68000 register file in a plain C context that m68k_get_reg exposes, this build has:

  • Real registers - D0-D7, A0-A7, PC, SR (with X N Z V C S flags) and USP/SSP, read and written live off the 68000 register file.
  • Real single-instruction step - Step i calls wasm_dbg_step() = SekRun(0,1), which executes exactly one 68000 instruction; PC and the registers change by one instruction per click.
  • Side-effect-free memory - reads resolve only the pointer-mapped program ROM and 8 KB work RAM at $800000 via SekReadByte (returning 0 for the slapstic window at $038000 and all I-O), so auto-polling the hex/disasm view never trips the copy-protection bank switch or clears a latch.
  • Execution breakpoints & memory watchpoints - implemented host-side in the run loop (see below), so they cost nothing when the debugger is closed.

3 · Breakpoints and watchpoints without a hot-path hook. When no breakpoint or watchpoint is set, the loop runs a whole frame at native speed with _wasm_tick() (BurnDrvFrame). As soon as one is armed, the loop switches to stepping the 68000 one instruction at a time with wasm_dbg_step(), comparing the live PC against the breakpoint set and the watched addresses against their last sampled value, and pausing on a hit. This is a pure host-side check that only runs while a guard is armed - the C++ core is never modified. (While single-stepping under an armed breakpoint the 6502 sound CPU and video do not advance in lockstep with the 68000, so the picture holds on its last frame until you resume.)

4 · The controllable loop. We own the frame loop so the transport can pause/step. While running, each animation frame writes the input latch, advances one frame with _wasm_tick(), and blits. Pause stops the loop; Resume restarts it; Step (frame) runs one _wasm_tick(); Step i (instruction) calls wasm_dbg_step(); Reset calls wasm_reset().

Architecture

Atari Games' Gauntlet (1985) is the arcade game that popularised four-player cooperative play; up to four heroes - Warrior, Valkyrie, Wizard and Elf - fight through top-down mazes while a digitised voice narrates ("Warrior needs food, badly"). FBNeo emulates the whole board; our build is a thin standalone Emscripten port of its atari driver. The whole machine lives in one WASM module built from C/C++:

  • Motorola 68010 - the main CPU at ~7.16 MHz (object-code compatible with the 68000, which is why the shared m68000 decoder disassembles it). Its register context is what this integration samples via m68k_get_reg; SekRun(0,1) is the run-one-instruction primitive used for single-stepping.
  • MOS 6502 - the sound CPU at ~1.79 MHz, which drives the audio chips and answers the 68000 over a command latch.
  • YM2151 + POKEY + TMS5220 - the FM synthesiser, Atari's POKEY (tones + noise), and the TMS5220 speech chip that together make Gauntlet's music, effects and the famous spoken warnings.
  • Atari custom video - a scrolling playfield tile layer, an alphanumeric layer, and a motion-object (sprite) engine composited into a 336×240 picture; the slapstic is Atari's address-scrambling copy-protection PAL mapped into the 68000 ROM space at $038000.
  • Memory map - the 68000 bus places the program ROM at $000000-$07FFFF (with the slapstic window at $038000), 8 KB of work RAM at $800000, and the playfield / motion-object / palette RAM and I-O in the $900000 region. Games load from a standard FBNeo/MAME romset ZIP that packs the program, graphics, and audio ROMs.

How to build this exact artefact. Toolchain: Homebrew emscripten 6.0.3 (emcc on PATH).

git clone https://github.com/finalburnneo/FBNeo.git fbneo
# 1. generate m68kops.c (m68kmake) and a Gauntlet-only driverlist.h
# 2. add gauntlet_wasm.cpp (burn-library frontend + wasm_dbg_* hooks) + burner_stubs.cpp
# 3. compile the burn core + atari/d_gauntlet + 68000 + 6502 + YM2151/POKEY/TMS5220
#    + atariic/atarimo/slapstic/watchdog + the shim:
emcc -O2 <burn core + atari + cpu + snd .c/.cpp> gauntlet_wasm.cpp -o gauntlet.js      -sMODULARIZE=1 -sEXPORT_NAME=Module -sALLOW_MEMORY_GROWTH=1      -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32      -sEXPORTED_FUNCTIONS=_wasm_start,_wasm_tick,_wasm_dbg_pc,...   # -> gauntlet.js + .wasm

The build is single-threaded (no SharedArrayBuffer), so it hosts anywhere. The 68000, 6502 and sound cores all run in plain C/C++; there is no dynarec, so nothing needs writable-executable memory.