Plain HTML + Canvas — no build tools, no frameworks
All game logic runs in NES pixel space (256×240). A single SCALE constant (= 3) maps NES pixels to CSS pixels at render time. Changing it resizes everything without touching any game code.
| Constant | Value | Meaning |
|---|---|---|
NES_W / NES_H | 256 / 240 | Full NES PPU output size |
SCALE | 3 | CSS pixels per NES pixel |
FX / FY | 16 / 16 | Playfield top-left in NES pixels (2-tile border) |
GW / GH | 13 / 13 | Playfield width/height in metatiles |
META | 16 | Pixels per metatile (2×2 CHR tiles) |
TANK_SZ | 16 | Tank sprite size in NES pixels |
MoveGridSnap routine.The renderer faithfully reproduces the NES PPU pipeline order: background first, then sprites on top.
chr_all.png is loaded once into an offscreen canvas (chrOff)drawCHRTile(tileAbs, palIdx, x, y) — reads an 8×8 pixel block from the sheet, applies the NES palette, writes into a scaled cached canvas, then blits to the main canvas"tileAbs_palIdx_transparent" — avoids re-processing identical tilesdrawMetatile composes 4 CHR tiles into a 16×16 BG metatiledrawSprite16 composes two 8×16 OAM entries into a 16×16 metasprite, with transparency (color-0 = skip)$D565 BGPaletteTable — indexed by palSet$D555 SpritePaletteDataC.FIELD)Runs via requestAnimationFrame locked to 60 fps. Each tick: read input → update all subsystems → render.
| Subsystem | ROM reference | Notes |
|---|---|---|
| Input | $9689 ReadControllers | Keyboard → D-pad + A/B; gamepad API supported |
| Entity movement | $DD30 MoveGridSnap | 8px snap grid; leading-edge collision probes |
| AI | $DC7C EntityMovementAI | Hardcoded direction sequences + target seeking |
| Firing | $E162 EnemyFireTick | 1/32 chance per frame per enemy; player fire on button press |
| Bullet movement | $E604 BulletTerrainCollision | 4 px/frame; nametable shadow read for tile type |
| Bullet–entity hit | $E70C EnemyBulletPlayerCollision | Proximity ≤ 10px; armor hit counting |
| Bullet–bullet cancel | — | Opposing bullets within 8px destroy each other |
| Power-ups | $EA7E | 6 types; spawned on kill of flagged enemy |
| Shield / freeze | — | Frame-count timers; freeze stops all enemy movement and AI |
| Spawn animation | $DDA0 | Cycling tile pattern over ~60 frames before tank appears |
| Death animation | $DE64 DeathAnimTick | Expanding ring of colored rects, 4 phases |
| Stage clear | $C728 CheckGameOver | All 20 enemies defeated → victory scroll |
| Game over | $C728 CheckGameOver | Eagle destroyed or all lives lost → GAME OVER sprite scroll |
Each metatile cell stores a 4-bit mask (brickBits) representing which 8×8 quadrants remain: TL=bit0, TR=bit1, BL=bit2, BR=bit3. When a bullet hits, the mask for the struck quadrant(s) is cleared. The tile is re-classified based on which bits remain:
EMPTYBRICK_RBRICK_BBRICK_LBRICK_TThis exactly mirrors the ROM's $D745 SubTileBitmask system.
All sound effects are synthesized at runtime using the Web Audio API — no audio files. Each SFX recreates the 2A03 channel behavior:
$030B–$030DA level editor (Famicom-exclusive feature) is implemented as a game state. The 13×13 grid is editable with cursor navigation; the current tile type cycles through all terrain types. Custom maps are stored in JS memory and passed directly to the stage loader — no serialization.
max(50, 186 − stageIndex × 4 − (2P ? 20 : 0)) frames — ROM $C3A2: $BE − stage × 4; 2P penalty $C3B0: SBC #$14$EA51 EnemyTypeTable (20 entries per stage)$D9E1 SetupScoreDigits BCD encoding