# DuckHunt-JS: a browser remake of Duck Hunt built on PixiJS

> DuckHunt-JS reimplements the NES shooter in JavaScript and HTML5, rendering with PixiJS over WebGL or Canvas, driving audio through Howler and animation through GreenSock tweens. It is a good study of a small browser game pipeline, and a poor base for anything beyond a faithful remake.

**MattSurabian/DuckHunt-JS** — DuckHunt ported to JS and HTML5

- Repository: https://github.com/MattSurabian/DuckHunt-JS
- Website: https://duckhuntjs.com
- Stars: 635 · Forks: 290
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mattsurabian-duckhunt-js

## What the project is: an HTML5 remake with a deliberate small toolchain

DuckHunt-JS is an implementation of DuckHunt in JavaScript and HTML5, and the live version is playable at duckhuntjs.com. It is a port rather than a re-imagining, which shapes everything about the repository: the goal is faithful behaviour with the original's scenes, sprites and sound, and the code is organised around that fidelity rather than around extensibility.

The stack is four named libraries, and the README states the division of labour plainly. PixiJS is the rendering engine. GreenSock, in the form of GSAP, handles animation. Howler manages audio. Bluebird supplies Promises.

What the repository adds around those is the part worth noticing for anyone studying browser game architecture. There is a webpack build with a dev server, a gulpfile for the asset tasks, a vendor directory, a committed dist directory, and a set of Terraform files with an S3 policy, which means the game is built as a static bundle and deployed to object storage. That is a complete pipeline for a small game, and it is the piece most likely to be borrowed by someone building something else.

The repository's own numbers are slightly untidy in a way that tells you about its history. The README header still reads v3.0, the package manifest declares version 3.1.0, and the published releases include a 3.5.0, a 3.5.1 and a 4.0.0 named Now With AI. The last push to the repository was on 2026-06-30 and the project is not archived.

## Rendering with PixiJS: WebGL first, Canvas as the fallback

The rendering section of the README is short and specific: the game supports WebGL and Canvas rendering via the PixiJS rendering engine. That single line describes a modern browser game architecture in miniature.

WebGL gives you shaders, hardware acceleration and the ability to push a lot of sprites, and the PixiJS version in the dependency list is 8.x, so the renderer is the modern scene-graph implementation rather than the older API. The Canvas fallback matters more than it used to: it is what keeps the game playable in environments where WebGL is unavailable or blocked, including some corporate browsers and some virtualised desktops, and it is the difference between a white screen and a working game on a locked-down machine.

The topic list on the repository names both canvas and webgl, which confirms that both paths are live rather than one being aspirational. The other rendering-related topic is pixijs itself.

Sprite art comes from image sprites, and the animation system described in the README's Tweening section builds PixiJS MovieClips out of those sprite images. So the visual pipeline is: images packed into a sprite sheet by texturepacker, a MovieClip structure that sequences frames, and the PixiJS renderer drawing them. That is the classic 2D game arrangement and it explains why the render layer stays small.

## Audio and animation: Howler with a fallback, GSAP for the movement

Audio follows the same fallback pattern as rendering. The README says the game will attempt to use the WebAudioAPI and fall back to HTML5 Audio if necessary, and that audio is loaded and controlled via HowlerJS. In practice the WebAudio path is preferred because it gives lower latency and finer mixing control, and the HTML5 Audio fallback is the safety net for browsers where WebAudio is unavailable or blocked by policy.

The audio files themselves are built by a separate gulp task, and the README notes that this task has a hard dependency on ffmpeg. That is a real constraint for anyone rebuilding the game from source, because ffmpeg is a native binary you have to install, not an npm package that comes along with the clone.

Animation is where the library choice is most interesting. The README states that the animations are a combination of PixiJS MovieClips built from sprite images and tweens, and that since PixiJS does not provide a tweening API, GreenSock was used. So the split is deliberate: frame-by-frame animation is MovieClip territory, and anything that interpolates a value over time, a sprite sliding across the screen, an easing curve on a hit, is a tween, and PixiJS has no opinion about that.

The result is that GSAP is doing the work that a game engine would normally provide, which keeps the dependency list short at the cost of writing the motion curves by hand.

## Game logic as ES6 classes transpiled down to ES5

The README describes the flow of the game as managed using JavaScript, with the main chunks of business logic implemented as ES6 classes which are transpiled to ES5 using Babel. That sentence tells you the project's compatibility target, and it is a deliberate one: ES5 output means the game runs in older browsers, which fits a remake of a 1988 console game and also fits the WebAudio-to-HTML5-Audio fallback thinking.

The Babel setup in the dependency list is current rather than historical, with @babel/core and @babel/preset-env at 7.29 and babel-loader at 10, wired into webpack. The rest of the toolchain is current as well: webpack 5 with webpack-cli 6 and webpack-dev-server 5, gulp 5 for the asset tasks, and ESLint 9 with the flat config eslint.config.mjs and eslint.config.dist.mjs in the repository root.

The structure implied by this is a conventional separation: webpack bundles the JavaScript in src/, the gulp tasks handle the media pipeline, and dist holds the built output that is committed to the repository so that people can serve the game without a build step. That last point is unusual and useful: the README says the repo ships with committed dist files to make it easy for developers to get up and running.

The main.js file in the root is the browser entry point, and the src directory holds the source. The classes described as the main chunks of business logic are what a reader should open first to understand how a scene, a dog, or the shooting mechanic is modelled.

## Running it locally and rebuilding the assets

The developer workflow is documented in four steps. You must have nodejs installed. Clone the repository into a directory of your choice. Change into that directory and run npm install. Then use npm start to start a local webserver which will make the site available at http://localhost:8080/.

The npm scripts map onto that workflow:

```bash
npm install
npm start
```

Two details in the README are worth copying into your own setup. First, cross origin errors prevent the project from being accessed in the browser with the file:// protocol, so a server is mandatory and double-clicking index.html will not work. Second, npm start also triggers automatic builds and reloads of the page when changes are detected in the src directory, so webpack-dev-server is doing the watch and reload work rather than a separate tool.

To cut a build manually, run npm run build. The remaining scripts cover the asset pipeline and the code checks:

```bash
npm run build
npm run audio
npm run images
npm run lint
```

npm run audio rebuilds the audio assets and has a hard dependency on ffmpeg. npm run images rebuilds the image assets and has a hard dependency on texturepacker. npm run lint runs ESLint over the source, and the Contributing section asks that pull requests keep code style and quality compliance with that command and include any built files, which is why dist is committed.

The package manifest also contains a detail that tells you what the project does not have. Its test script is a stub that echoes an error message about no test specified and exits with a failure, and no test framework appears in the dependency list. There are no automated tests here, which for a game with a fixed behaviour spec is the most obvious gap.

## Where DuckHunt-JS is the wrong tool

The first wrong case is a new game with its own rules. Every design decision here serves one specific game: the sprite pipeline packs art for a fixed resolution, the level content is a fixed set of scenes, and the Motion Clips assume a known frame order. There is no level format, no scripting layer and no entity system to build on, so a new game means writing a new project.

The second wrong case is anything needing a server or multiplayer. The whole client is a static bundle deployed to object storage through the Terraform and S3 policy files, with no backend of its own.

The third wrong case is a project with a quality bar that includes automated tests. The package's own test script is a placeholder that fails, and there is no test dependency at all, so adopting the repository means adopting a codebase with no regression safety net. For a game whose behaviour is supposed to match a fixed original, that is a strange thing to give up.

The fourth is tooling friction on the asset side. Rebuilding audio needs a native ffmpeg install and rebuilding images needs texturepacker, so a contributor who wants to change the art or the sound needs software that a pure JavaScript developer may not have. The committed dist files exist precisely because that pipeline is not trivial.

The realistic alternative is a general 2D game framework with its own renderer, tweening, asset pipeline and test story, none of which would be DuckHunt's original assets or code. The difference in approach is exactly what the README advertises: this repository is a small, complete, faithful port with a hand-rolled pipeline, whereas a framework gives you more but asks you to rebuild the parts DuckHunt-JS already has.

## Conclusion

DuckHunt-JS is worth reading or forking if you want a complete, small browser game with a working asset pipeline, a dev server with live reload and a deployable static bundle. It is the wrong starting point for a new game, for anything with multiplayer, and for anything that needs automated tests, since the package's test script exits with an error and there is no test framework in the dependency list. Verify first that the ffmpeg and texturepacker dependencies are acceptable on your machine, since rebuilding audio and images needs both, and that the version you clone matches the one you intend to run, since the package manifest still says 3.1.0 while the releases include a 4.0.0.

## FAQ

### How do I run DuckHunt-JS locally?

Install nodejs, clone the repository, change into the directory, run npm install and then npm start, which starts a local webserver and serves the site at http://localhost:8080/. The README notes that cross origin errors prevent the project from being opened with the file:// protocol, so a server is required.

### What rendering backends does the game support?

The README says the game supports WebGL and Canvas rendering via the PixiJS rendering engine, so both paths are available and the Canvas path is the fallback when WebGL is not usable.

### How does the game handle audio?

It attempts to use the WebAudioAPI and falls back to HTML5 Audio if necessary, with audio loaded and controlled through HowlerJS. The audio files themselves are produced by the gulp audio task.

### What do I need to rebuild the audio and image assets?

The npm run audio task has a hard dependency on ffmpeg, and the npm run images task has a hard dependency on texturepacker. The repository ships with committed dist files so you can run the game without going through either task.

### Does DuckHunt-JS have automated tests?

No. The package manifest defines its test script as a stub that reports no test specified and exits with an error, and no test framework is listed among the dependencies. The quality check the project does ask for is npm run lint.

### Which libraries does DuckHunt-JS depend on?

The README names PixiJS for rendering, GreenSock for tweening, Howler for audio and Bluebird for Promises, and the manifest pins pixi.js 8, gsap 3.12, howler 2.2.4 and bluebird 3.7.2 in its devDependencies, alongside webpack 5, Babel 7 and ESLint 9.

## Sources

- [License: MIT](https://github.com/MattSurabian/DuckHunt-JS/blob/master/LICENSE)
- [MattSurabian/DuckHunt-JS on GitHub](https://github.com/MattSurabian/DuckHunt-JS)
- [Project website](https://duckhuntjs.com)
- [README](https://github.com/MattSurabian/DuckHunt-JS/blob/master/README.md)
- [Releases](https://github.com/MattSurabian/DuckHunt-JS/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mattsurabian-duckhunt-js
