Zapper Mario
A homebrew ROM hack of the NES multicart Super Mario Bros. + Duck Hunt (USA) (mapper 66). It lets you shoot Mario's enemies with the Zapper, adds two new Duck Hunt games and a dog that can be shot while he laughs at you, and makes the game-select menu work with the Zapper.
No ROM images are distributed here. You need your own dump of the original cartridge and apply the IPS patch from the download section to it; see Getting the ROM.
What it does:
- One controller for both players. In a two-player game both Mario and Luigi are controlled by controller 1. The zapper stays plugged into the second controller port.
- Super Mario Bros: Pull the trigger at any time while playing and the enemy you hit takes a single hit exactly as if one of Mario's fireballs had touched it: it flips over and falls off the screen with the usual "smack" sound and score (100 for a Goomba, 1000 for a Hammer Bro, 200 otherwise), Koopa shells included, and the fireball explosion puff is drawn on the enemy; Bowser loses one hit point per shot. Fireproof enemies (Buzzy Beetles, Bullet Bills, Podoboos) shrug the shot off, but the puff still shows on them as it does for a fireball. Every shot costs one coin, taken off the coin counter in the status bar; with no coins the trigger does nothing.
- Duck Hunt gets two new games and a shootable dog.
- Game D - 3 Ducks: three ducks at once, three shots per flight, nine ducks per round (three flights). The usual pass mark applies (six hits in the early rounds, more later), capped at nine of nine. Shoot all three and the dog comes up with a duck in each paw and the third one in his mouth.
- Game E - Duck Hunt Randomizer!: each flight has one, two or three ducks (random), always with three shots; ten ducks per round as in Games A and B. The hunting ground is re-rolled after every round: up to two trees and up to five bushes, so you can end up in a clearing or in a small wood.
- The dog can be shot while he laughs at you. It costs a bullet like any other shot, so with an empty gun he is safe, and it costs 500 points.
- Bug Fix: The ducks the dog holds up have the colours of the ducks you actually shot.
- The multicart menu works with the Zapper: a shot off the screen moves the selection, a shot at the screen starts the selected game (the screen flashes black then white for a frame, as Duck Hunt does for a shot).
Everything else (the SMB game itself, Duck Hunt Games A-C) is the original code.
Screenshots:
The screenshots come from the project's own test emulator (tools/screenshots.py).
Download:
ZapperMario.ips is the patch, PATCHING.md describes it record by record, ips.py applies it (Python 3, no other dependencies), and ZapperMario-source.zip is the complete build environment: the new 6502 source, the assembler, the test emulator and the build script. No ROM images are included in any of them.
Listing directory https://download.elifulkerson.com/files/zappermario/1.0:PATCHING.md November 05 2026 02:33:53 4685 ASCII text, with very long lines (379)
PATCHING.md.md5 November 05 2026 02:33:53 46 MD5 checksum
PATCHING.md.sha1 November 05 2026 02:33:53 54 SHA1 checksum
PATCHING.md.sha256 November 05 2026 02:33:53 78 SHA256 checksum
PATCHING.md.sha512 November 05 2026 02:33:53 142 SHA512 checksum
ZapperMario-source.zip November 05 2026 02:33:53 280993 Zip archive data, at least v1.0 to extract, compression method=store
ZapperMario-source.zip.md5 November 05 2026 02:33:53 57 MD5 checksum
ZapperMario-source.zip.sha1 November 05 2026 02:33:53 65 SHA1 checksum
ZapperMario-source.zip.sha256 November 05 2026 02:33:53 89 SHA256 checksum
ZapperMario-source.zip.sha512 November 05 2026 02:33:53 153 SHA512 checksum
ZapperMario.ips November 05 2026 02:33:53 21272 IPS patch file
ZapperMario.ips.md5 November 05 2026 02:33:53 50 MD5 checksum
ZapperMario.ips.sha1 November 05 2026 02:33:53 58 SHA1 checksum
ZapperMario.ips.sha256 November 05 2026 02:33:53 82 SHA256 checksum
ZapperMario.ips.sha512 November 05 2026 02:33:53 146 SHA512 checksum
ips.py November 05 2026 02:33:53 2288 Python script, ASCII text executable
ips.py.md5 November 05 2026 02:33:53 41 MD5 checksum
ips.py.sha1 November 05 2026 02:33:53 49 SHA1 checksum
ips.py.sha256 November 05 2026 02:33:53 73 SHA256 checksum
ips.py.sha512 November 05 2026 02:33:53 137 SHA512 checksum ↩ Browse the download server
Getting the ROM:
The hack is distributed as an IPS patch, ZapperMario.ips, which turns the unmodified original ROM into the Zapper Mario ROM. You must supply your own copy of the original.
1. Check that you have the right original ROM
The patch only works on this exact file (iNES header included):
| File | Super Mario Bros. + Duck Hunt (USA).nes |
| Size | 81,936 bytes (16-byte iNES header + 64 KB PRG + 16 KB CHR, mapper 66) |
| MD5 | 1306a0286248a0851005464d7ec8d785 |
| SHA-1 | 663ddef71cc808c5382a3426ef6d03b5499a51c8 |
| CRC32 | 666ee28e |
Verify before patching, for example:
md5sum "Super Mario Bros. + Duck Hunt (USA).nes" # Linux / macOS certutil -hashfile "Super Mario Bros. + Duck Hunt (USA).nes" MD5 # Windows
If the MD5 differs (different dump, headerless ROM, trained or overdumped copy), do not apply the patch; the result will not work.
2. Apply the patch
Use any IPS patcher:
- Floating IPS or Lunar IPS (Windows): open the patcher, choose
ZapperMario.ips, then choose the original ROM. Keep a copy of the original: Lunar IPS patches in place. - In the browser: an IPS web patcher such as "ROM Patcher JS": select the ROM and the IPS file, download the result.
- The
ips.pyscript from the download section (any OS with Python 3, nothing else needed):python3 ips.py apply "Super Mario Bros. + Duck Hunt (USA).nes" ZapperMario.ips "Super Mario Bros. + Duck Hunt (Zapper Mario).nes"
3. Check the result
| Patched ROM size | 98,320 bytes (header + 64 KB PRG + 32 KB CHR) |
| Patched ROM MD5 | 055e4b5461be8e9a5b5c7ac50f4da70b |
| Patched ROM SHA-1 | 646419c775d60729b35a1e0ff49b2d3997a4100c |
| Patch file MD5 | 00d2373da03411e73f12e174f399cdac (21,272 bytes) |
The patched ROM is 16 KB larger than the original: the CHR ROM grows from two to four 8 KB banks (header byte 5 changes from 2 to 4) because the hurt-dog face needed a free sprite tile. Every IPS patcher handles this (the patch writes the whole appended area), but the result must be exactly 98,320 bytes.
4. Play it
Plug the Zapper into controller port 2 and a standard controller into port 1. Like every Zapper game it needs a CRT television, or an emulator with Zapper support (Mesen, FCEUX, Nestopia and others); the Zapper does not work on LCD screens. The hack has not yet been tried on real hardware.
- Menu: Select / Start as usual, or shoot off-screen to move the selection and at the screen to start.
- Super Mario Bros.: controller 1 plays both Mario and Luigi; shoot enemies at any time during play, one coin per shot.
- Duck Hunt: Select cycles through Games A to E on the title screen; Start begins.
Rules inherited from the fireball logic:
Because a shot is treated as a fireball hit, fireproof enemies are not harmed by shots either: Buzzy Beetles, Bullet Bills and Podoboos just get the explosion puff drawn on them (a dud, silent as in the original). Bowser's fire, fire bars and other non-enemy objects are not targets at all. A flattened Goomba cannot be shot again. One shot hits at most one enemy (the lowest enemy slot that the gun sees), and a shot costs a coin whether it hits or not.
How a shot works (Duck Hunt style):
When the trigger is pulled during normal play the game freezes for a few frames:
- one black frame (if the sensor sees light here the gun is pointed at a lamp and the shot is ignored),
- one frame per on-screen enemy showing only a solid white 32x32 box at that enemy's position (background off); the first enemy whose box the sensor sees is hit,
- the game resumes. Music keeps playing during the frozen frames.
So a shot costs between 2 and 8 frames.
Building from source:
python3 build.py # reads "Super Mario Bros. + Duck Hunt (USA).nes", writes the patched ROM
Requires only Python 3 (and the original ROM in the project directory). Everything is self-contained: tools/asm6502.py is a small two-pass 6502 assembler that understands the syntax of the doppelganger SMB disassembly (src/SMBDIS.ASM), which is assembled first to obtain all SMB symbols and to verify, byte for byte, that bank 0 of the input ROM is the expected SMB build. Every byte patch to the original code is checked against the expected original bytes before it is applied.
To regenerate the patch after a change:
python3 tools/ips.py make "Super Mario Bros. + Duck Hunt (USA).nes" "Super Mario Bros. + Duck Hunt (Zapper Mario).nes" patch/ZapperMario.ips
It prints the hashes and the record list; update the tables in README.md and in patch/PATCHING.md (which also describes what each patch record changes).
Testing:
tools/nestest.c is a tiny headless NES emulator written for this project (mapper 66 with bus-conflict checking, scanline PPU with sprite-0 hit, Zapper model similar to Mesen's, no sound). tools/play.py drives it through 56 scripted checks:
- Super Mario Bros.: title-screen trigger ignored, Goomba and Koopa knocked out by a shot, a shot costs a coin (tally and status bar), no coins means no shot, a Buzzy Beetle takes a dud puff unharmed, miss leaves the enemy alone, pause blocks shots, Mario's own movement untouched, long auto-play run with no crashes or bus conflicts.
- Duck Hunt: five-game title and top-score table; Game D three ducks, three shots, nine-icon HIT row, three-duck catch pose; the dog shot while laughing (hurt pose, palette and sound state restored, flinch then drop, hands over to the next flight, thud just after he vanishes); no shot with an empty gun, a shot costs a bullet and 500 points but never goes below zero; Game E flight sizes and scene re-roll after a round; the dog's spot clear of bushes and trunks; held-duck colours; scoring by colour; Games A-C still start normally.
- Menu: off-screen shots cycle the selection, an on-screen shot starts either game, the Start button still works.
gcc -O2 -o build/nestest tools/nestest.c python3 tools/play.py
Sound cannot be tested this way (the emulator has no APU); it was checked by ear in a full emulator.
Under the hood: the Zapper in Super Mario Bros. (for hackers):
SMB's 32 KB bank has essentially no free space (a 6-byte hole at $F2CA is the largest), so the new code lives in the unused area of bank 1 (menu / Duck Hunt bank) at $A180. See the comment block at the top of src/zapper.asm.
- A shot costs a coin: the coin tally goes down and the status bar digits are updated with SMB's own digit routine (modifier -1) and reprinted, both called across the bank boundary like the fireball hit.
- SMB bank changes: NMI vector ->
$F2CAstub (lda #$10 / sta $800D, bus-conflict safe:$800Dalready contains$10), andldx CurrentPlayerin GameCoreRoutine becomesldx #0(controller 1 drives both players). - Bank switching back to SMB is done with
lda #0 / sta $A180placed right before an address where the SMB bank happens to contain anRTSorRTI, so execution continues seamlessly in SMB after the switch. Calls from the new code into SMB routines (HandleEnemyFBallCol, SoundEngine) return through the same stub. - SMB's idle loop (
jmp EndlessLoop) is replaced at run time by a 15-byte loop in RAM ($0160) that polls the Zapper light sensor all frame long. Variables live in$0140-$0156, an area SMB clears but never uses. - The explosion puff is a real fireball object put into its exploding state. SMB only runs fireball objects while Mario is fiery, so for small/big Mario the hook calls
FireballObjCoreitself each frame, right before SMB's OAM DMA.
Under the hood: the Duck Hunt changes (for hackers):
The Duck Hunt extensions live in src/duckhunt.asm, assembled into the free area of bank 1 right after the Zapper code (about 3.5 KB at $A860-$B680), and are wired in with 36 small byte patches listed in tools/dh_build.py. Things worth knowing:
- The third duck is a full object record at
$0620(an area Duck Hunt never uses), processed like the two original ducks; its sprites use OAM$0280-$02BF. The three-duck catch pose is a new sprite set ($25); the 8-sprites-per-scanline limit is why the duck in the mouth hangs down over the dog's face and the wing tips of the outer ducks are dropped. - The shot dog is the catch pose (arms up) without the ducks, with the eye tile swapped for an X-eyed one, the paw and upper-arm tiles cleaned of the outline pixels that belonged to the duck, and an extra body row so the flinch does not lift him clear of the grass. Those are new tiles: the Duck Hunt CHR bank had no free sprite tile for Games A/B/D/E other than the clay-pigeon tiles, so the CHR ROM is doubled: bank 2 is the Duck Hunt bank with the new tiles in clay-pigeon slots (
$EB-$F3), bank 3 is a copy. (The title font has no Z, hence "RANDOM" rather than "RANDOMIZER".) Games A, B, D and E run on bank 2, Game C on the original bank 1 (mapper 66 switches CHR with the same register as PRG, written bus-conflict-safe from a byte of the new code). - The dog has a new state (
$13, "shot"); the shot routine only targets him while he is laughing and visible above the grass, and only if a shot is left. The shot sound and the laugh share sound engine variables, so the laugh is silenced first. He flinches for 12 frames while the gunshot rings, then the gunshot's noise tail is skipped ahead to its quiet part (it would otherwise bury the sample) and the yelp plays: the dog's bark sample at the top DMC rate. He drops at 4 pixels a frame with Duck Hunt's falling-duck whistle; the thud is the duck-landing sound, triggered a few frames after he has gone. The 500-point penalty goes through Duck Hunt's signed BCD score adder with a floor at zero. - Scoring is untouched: each duck's value comes from the original table indexed by colour type and round band.
- The random background is drawn by
DH_RandomScenefrom a base scene without trees and bushes plus tree and bush templates extracted from the original scene stream at build time (build/dh_tree.asm,build/dh_bush.asm). No new background tiles were needed: the trunk-base and bush-base tiles are self-contained grass variants. Trees go at column 0 and/or 22 (the message box occupies columns 10-23 of the upper rows), bushes at five possible spots; the scene is re-rolled in the round-start task, so it only changes after a completed round; the sky rows are cleared first so the previous round's trees do not linger. - The title screen stream is regenerated with five menu rows; the top score table moved to
$05D0(five entries) and "TOP SCORE" is drawn in white. - Held-duck colours: at the catch, the colour types of the hit ducks are read from the slot records and a sprite palette packet is built: exact two-palette colours when all hit ducks share a type; otherwise sprite sets
$27-$2Agive the first duck palette 0 (head) and palette 3 (white, orange, body) for the rest, the other duck palette 1 with the orange palette for its beak only (the beak tile uses a single colour), and the dog's body tiles become flat-shaded copies ($EC-$F1) on his face palette so that palette 3 is free. - The dog's spot: the random scene keeps a layout mask at
$0671(bush spots, trunks); the laugh and catch rises call a routine that picks the nearest of eight candidate X positions whose span overlaps nothing drawn. - Menu Zapper: the menu's per-frame controller read is hooked; a trigger release drives a black frame (lamp check) and a white frame (palette filled by direct PPU writes in vblank, NMI enabled only for that frame) while polling the light sensor, then restores the menu palette or jumps to the menu's own launcher.
A record-by-record description of the patch is in PATCHING.md, in the download section.
Source zip layout:
| Path | What it is |
|---|---|
patch/ZapperMario.ips, patch/PATCHING.md | the patch and its description |
build.py | builds the patched ROM from the original |
src/zapper.asm, src/duckhunt.asm | the new code |
src/SMBDIS.ASM | doppelganger's SMB disassembly, used for symbols and verification |
tools/asm6502.py, tools/dis6502.py | assembler and disassembler |
tools/dh_build.py | Duck Hunt build steps: generated tables, byte patches, CHR expansion |
tools/ips.py | IPS make / apply |
tools/nestest.c, tools/play.py | headless test emulator and the scenario suite |
tools/screenshots.py | renders the screenshots on this page with the test emulator |
tools/chr2png.py, tools/ppm2png.py | tile sheet and screenshot converters |
images/ | the screenshots |
All tools are written for this project and depend only on Python 3 (and a C compiler for the test emulator). ROM files (*.nes) are excluded from the zip and never distributed.