BINX FORGEALL YOU NEED IS AN IDEA.Support BINX ↗
PRACTICAL TUTORIAL · POCKET ENGINE 1.0 · 10 OCTOBER 2026

How to Build Your Own Game Engine Using AI: A Small 2D Core

Reuse a working browser game, split it into clear modules and run two rooms with optional sound and local checkpoints.

Difficulty: intermediate beginner. Goal: turn a small working browser game into reusable modules, then run two rooms through the same core. You need basic functions, objects, imports and a text editor. Start with the simpler three-file browser tutorial if those ideas are new.

The build preset keeps Puzzle, Collecting, 2D, Stylised, Browser, top-down, solo, free-only and a custom/browser engine. Review the generated plan, copy it to your AI, and keep your selected stack. Forge provides a plan and source-reviewed resources; it does not run an AI build or inspect unprovided game code.

What should your first engine do?

An engine is reusable code that supports a game. For this lesson, the deliverable is a small Canvas 2D core: input, drawing, movement, collision, simple visual animation, room state, optional sound and a local checkpoint. It has no editor, rigid-body physics, 3D renderer, networking or native exporter. It is separate from the production BINX Engine.

If your goal is to finish a game quickly, need an editor or want established export tooling, inspect Godot, Defold and the engine comparison first. These are alternative project paths, not drop-in modules for this Canvas example. Keep a working compatible engine. Choose this lesson when understanding and reusing a small browser loop is your actual goal.

The genuine original Coin Dash 1.0 reference interface
Genuine Coin Dash 1.0 reference capture · 10 October 2026. Pocket Engine reuses its movement and shapes; this image does not show the new modules, second room, sound or checkpoint controls. A new browser capture remains unverified.

Build a small reusable engine, one boundary at a time

  1. Download a complete baseline

    Extract the ZIP into an empty pocket-engine folder. Keep the modules directory intact. Open README.md and retain LICENSE.txt. No npm install, paid asset, API key or account is required. Node 24.19.0 was used for the shipped helper/tests; use a current browser with Canvas, ES modules and Pointer Events. Native browser/device acceptance is separate from the automated checks.

    Before editing, duplicate the folder. If you already have a game, copy its working version and inspect its existing systems instead of replacing it with this example.

  2. Run from HTTP and check the files

    Open a terminal in the extracted folder, run node check.mjs, then node serve.mjs. Open http://127.0.0.1:8080. The checks should report 13 passes; the page should show Courtyard, six-coin goal and Start game. Stop the local helper with Ctrl+C. If port 8080 is in use, run node serve.mjs 8081 and open that port.

    Do not double-click index.html: ES modules need HTTP and JavaScript MIME. This is different from the original Coin Dash classic script. The helper serves only the listed files on your own computer; it is a development convenience, not public hosting. Browser-control was unavailable for this release, so check the visible result yourself on your target browser.

  3. Identify the one frame owner

    Read main.mjs. Its frame callback reads input, calls scenes.update, draws, then schedules the next frame. Start, restart and next-room buttons replace state; they never create another frame loop. A hidden tab or blur clears input and pauses. Resume resets the previous timestamp. Page hide cancels scheduling and page show resumes one paused loop.

    The shared advanceFrame accepts at most 0.1 seconds and splits it into at most ten updates, each no larger than 0.01 seconds. Normal simulated 15/30/60/120 FPS movement covers 180 world pixels per second. This is timing consistency, not a real-device FPS result. Longer stalls deliberately drop elapsed time; the 30-second timer is simulated active time, not a wall-clock competition deadline.

  4. Keep input separate from movement

    In input.mjs, keyboard handlers only capture game keys while the canvas or direction control has focus. Held pointers are tracked by pointer ID and removed on release, cancel and lost capture. Blur/hidden-tab handling clears them. The module exposes read(), clear() and destroy(); destroying removes its listeners.

    In world.mjs, input becomes a direction. The vector is normalised so diagonals do not move faster. Player coordinates are clamped within the 640×360 world using the collision radius. Test left/right/up/down and a diagonal; then press against each boundary. Do not use CSS display dimensions as world coordinates.

  5. Make collision and outcomes simulation responsibilities

    step moves the player and horizontal patrol, reduces remaining time, tests circle overlap and collects uncollected coins. A collision or timeout ends the run; all six coins produce a win. Rendering cannot collect a coin. The triangle is visual only: its collision shape is a circle of radius 18, so check the forgiving geometry before designing narrow passages.

    Use the actual outer-edge route to win: move up to the top row, across to the right, down to the bottom row, then left. The enemy stays along the centre. Test touching it, running out of time and returning to an already collected coin. Scores should change once per pickup.

  6. Draw without mutating the world

    render.mjs draws the grid, player, patrol and coins from state. Its four-phase animation changes the coin centre at six phases per second, using simulation age. Coin collision radius stays 10. Pausing stops animation age; a reduced-motion preference uses the static first phase. This teaches frame selection without importing a sprite sheet or rig.

    After it works, replace one visual with a permitted image while keeping its collision body. A sprite sheet also needs exact frame rectangles, an importer and loading failures; this example does not claim those features. Keep state changes out of draw functions.

  7. Reuse the core for a second room

    Read rooms.mjs: Courtyard and Gallery contain different coin positions. scenes.mjs owns the current state. start() makes fresh data; pause() and resume() change the active mode. next() is allowed only after winning Courtyard and starts Gallery with a fresh timer and score.

    Win Courtyard, choose Next room, win Gallery, restart and repeat. The world/input/render implementations are shared. To practise, change one Gallery coin coordinate within the world, run checks and the actual browser, then restore the baseline. For a third room, update scene progression, UI and save compatibility together; adding data alone is not enough while the interface deliberately names two rooms.

  8. Add optional sound through a user action

    Open Local checkpoint and sound, then enable sound. audio.mjs creates/resumes an AudioContext through that click and synthesises a short original tone. Sound starts off. The module caps active voices at four, releases oscillator/gain nodes when they end and stops them on mute/pause. Failed or missing audio does not stop the game.

    Check enable, pickup, mute, tab-away and repeated pickups on your actual device. The automated check uses an audio test double; it cannot verify audible output or autoplay policy. No third-party audio, microphone permission or external sound download is involved.

  9. Save only a small, versioned checkpoint

    During play, Save checkpoint pauses and writes one explicitly chosen checkpoint to this browser's localStorage. It stores version 1, room, elapsed/remaining simulation time, player/enemy position/direction and collected flags. Load validates supported fields, bounds coordinates, recalculates score and restores paused; Resume is a separate action. The decoder never evaluates code.

    Save after a pickup, reload the page, load, check the same room/score and resume. Try blocked/full storage and an invalid save. The existing run should remain usable. Delete removes only this example's key, never localStorage.clear(). This local save is editable, can be lost, stays at this origin/browser and is unsuitable for trusted online scores. It is separate from My Forge or cloud account saving.

  10. Publish the static files and retest

    Keep the relative paths. Upload index.html, CSS, main.mjs, files.mjs and the modules directory to your existing HTTPS static host. No bundle build is required. Ensure .mjs responses use a JavaScript MIME type. Node's local helper is not a deployed backend. Check the exact published URL, including under a repository subdirectory.

    Repeat keyboard/touch cancellation, two-room win/loss/restart, tab pause, reduced motion, opt-in/mute and save/load/denial on the actual host. Local checkpoints at a different origin do not transfer automatically. Keep the MIT notice with redistributed code and review separately licensed additions before release.

Build prompt for your AI

Review the prompt and add only project context you choose to share.

Refactor your existing browser game

A module should own one responsibility

Pocket Engine 1.0 module boundaries
ModuleOwnsCheck
world.mjsMovement, bounds, circle collision, patrol, score and bounded updatesCompare one second of straight input; test collision and timeout.
input.mjsFocused keyboard, pointer holds/cancellation and listener cleanupUse WASD on canvas, type in another field, cancel a held touch.
render.mjsCanvas drawing and four-phase coin animationRendering leaves state unchanged; reduced motion fixes the phase.
scenes.mjsReady, running, paused, won/lost and next-room stateWin the Courtyard, load Gallery, lose, restart and pause.
audio.mjsOpt-in original tones and a four-voice limitEnable/mute; blocked audio leaves the game playable.
storage.mjsExplicit versioned local checkpoint and validationSave, reload paused, deny storage, delete only this key.
rooms.mjsTwo room definitions separated from implementationChange coin positions without duplicating movement or input.

main.mjs connects those APIs to the interface. The room data imports no browser features. The pure world can run in Node. Input, Canvas and audio are browser adapters; their module tests use doubles and do not prove native interaction.

Read the complete room, scene and world source

modules/rooms.mjs

/* Pocket Engine1.0 · Copyright (c) 2026 BINX Forge · MIT. */
// Game data stays outside the reusable movement/timing module.
export const ROOMS = Object.freeze([
  {id:'courtyard', name:'Courtyard', coins:[[80,75],[260,75],[550,75],[550,285],[260,285],[80,285]]},
  {id:'gallery', name:'Gallery', coins:[[80,55],[320,55],[560,55],[560,305],[320,305],[80,305]]}
].map(room=>Object.freeze({...room,coins:Object.freeze(room.coins.map(p=>Object.freeze(p)))})));

modules/scenes.mjs

/* Pocket Engine1.0 · Copyright (c) 2026 BINX Forge · MIT. */
import {newGame,advanceFrame} from './world.mjs';
import {ROOMS} from './rooms.mjs';
export function createScenes() {
  let state=newGame();
  return Object.freeze({
    get state(){return state;},
    start(room=state.room){const definition=ROOMS.find(r=>r.id===room);if(!definition)throw Error('Choose a listed room.');state=newGame(definition);state.status='running';return state;},
    pause(){if(state.status==='running')state.status='paused';},
    resume(){if(state.status==='paused')state.status='running';},
    next(){if(state.status!=='won')return false;const index=ROOMS.findIndex(r=>r.id===state.room);if(index===ROOMS.length-1)return false;this.start(ROOMS[index+1].id);return true;},
    restore(checkpoint){state=structuredClone(checkpoint);state.status='paused';return state;},
    update(input,elapsed){return advanceFrame(state,input,elapsed);}
  });
}

modules/world.mjs

/* Pocket Engine1.0 · adapted from BINX Forge Coin Dash Timing1.1 · MIT; see ../LICENSE.txt. */
import {ROOMS} from './rooms.mjs';
const WIDTH = 640, HEIGHT = 360, SPEED = 180;
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const overlaps = (a, b) => Math.hypot(a.x - b.x, a.y - b.y) <= a.r + b.r;
function newGame(scene = ROOMS[0]) {
  return {
    status: 'ready', score: 0, remaining: 30, room: scene.id, age: 0,
    player: { x: 60, y: 180, r: 14 },
    enemy: { x: 320, y: 180, r: 18, direction: 1 },
    coins: scene.coins
      .map(([x, y]) => ({ x, y, r: 10, collected: false }))
  };
}
// All simulation changes live here. Rendering and input do not award points.
function step(state, input, elapsed) {
  if (state.status !== 'running') return state;
  const dt = clamp(Number.isFinite(elapsed) ? elapsed : 0, 0, 0.05);
  let dx = (input.right ? 1 : 0) - (input.left ? 1 : 0);
  let dy = (input.down ? 1 : 0) - (input.up ? 1 : 0);
  const length = Math.hypot(dx, dy);
  if (length) { dx /= length; dy /= length; }
  const p = state.player;
  p.x = clamp(p.x + dx * SPEED * dt, p.r, WIDTH - p.r);
  p.y = clamp(p.y + dy * SPEED * dt, p.r, HEIGHT - p.r);
  const e = state.enemy;
  e.x += e.direction * 110 * dt;
  if (e.x >= 510) { e.x = 510; e.direction = -1; }
  if (e.x <= 130) { e.x = 130; e.direction = 1; }
  state.age += dt;
  state.remaining = Math.max(0, state.remaining - dt);
  if (overlaps(p, e) || state.remaining === 0) { state.status = 'lost'; return state; }
  for (const coin of state.coins) {
    if (!coin.collected && overlaps(p, coin)) { coin.collected = true; state.score++; }
  }
  if (state.score === state.coins.length) state.status = 'won';
  return state;
}
// Process ordinary slow frames without discarding their elapsed time.
// At most ten updates of at most 0.01 seconds; longer stalls deliberately lose time.
function advanceFrame(state, input, elapsed) {
  if (state.status !== 'running') return state;
  const dt = clamp(Number.isFinite(elapsed) ? elapsed : 0, 0, 0.1);
  const updates = Math.ceil(dt / 0.01);
  for (let i = 0; i < updates && state.status === 'running'; i++) step(state, input, dt / updates);
  return state;
}

export {WIDTH,HEIGHT,SPEED,clamp,overlaps,newGame,step,advanceFrame};
All 15 source and helper files

Download the complete original source ZIP. Retain the folder structure and MIT notice. Individual files and the ZIP contain the same source.

Prove the loop before expanding it

node check.mjs executes 13 checks against the shipped modules: frame-rate movement, actual wins in both rooms, pause/restart isolation, invalid/stall time and bounds, collision/timeout/pickup, render/animation purity, checkpoint round-trip, invalid-save refusal, storage failure isolation, input release/cleanup, bounded audio ownership and complete local HTTP/MIME delivery.

Run it again after one small change. Keep automated simulation/adaptor checks separate from real browser, audible sound, physical phone, screen reader and published-host checks. Independent AI build/debug/upgrade prompt outcomes were not executed for this tutorial. The public files let you reproduce the tests without this repository or an account.

Common failures and the first useful check

Start stays disabled or an import is blocked

Run the local HTTP helper; do not double-click HTML. Keep every module under its exact path/case. Inspect the first Console and Network error; verify JavaScript MIME.

Speed increases after restarting

Check for duplicate frame scheduling or listeners. Scene changes must replace state, not run startup again. Reset the previous timestamp on resume.

Keys move the game while typing

Keep capture scoped to the focused canvas/direction controls. Clear held input on blur and hidden tabs. Test pointercancel and lost capture.

Next room is unavailable

Finish all six coins in Courtyard first. Losing does not advance; Gallery is the final room.

Sound is silent

Use the enable button, then inspect browser autoplay/device output and AudioContext availability. Optional sound failure must not block play.

A checkpoint disappeared or cannot load

Confirm browser and origin, version and room. Storage can be denied/cleared or private-session scoped. Start a fresh run; do not overwrite unrelated keys or treat a malformed save as executable code.

New coins break the save or win test

Update the room definition, compatible schema/version and test expectations together. Existing version1 validates exactly six flags; do not silently accept incompatible arrays.

Debug one module

Building blocks from original creators

Start with the free edition where it fits. Paid editions and services are optional; inspect current prices, licence conditions and engine versions. Preview credits appear on each resource card.

Creator previewOpen the original page for real screenshots.
Free
GoldFire Studios / James Simpson

howler.js

JavaScript playback, sound sprites and spatial audio for browser games.

MITCode library
Official licence ↗Licensing summary, not legal advice. Check the exact edition and use.

Starter code included. Library setup still required.

Review code & AI brief

Review or copy the code here. No account needed.

See previews & details

Check compatibility before importing

For each listing, review the separate permissions to sell a finished game, redistribute files and include files in a source/template product. No pack is implied to contain a complete game or all the mechanics below.

Make Your Game Better

Keep the tested core. Choose one missing capability, preserve a baseline and test the smallest addition. No creator packs or code libraries are bundled in this example.

  • Kenney UI Pack: free CC0 visual controls from the creator. Inspect exact formats and separate font/branding terms; change visuals without changing input state.
  • howler.js: optional MIT browser audio when real audio-file playback fits better than tones. Reuse the existing one-button sound example and retain separate sound-file licences.
  • nippleJS: optional browser joystick input. Try the existing walkthrough first. Its vector still needs movement/collision integration.
  • Godot or Defold: consider established authoring/runtime tools when your requirements exceed a small browser core. Review exact engine/dependency rights; Defold's licence has engine-product commercialisation restrictions, so do not treat it as MIT or copy it into a commercial engine product.

The sample requires no purchase. For a paid editor or pack, identify the missing capability first and check the exact current edition and permitted target. Price is not proof of a better engine. Compare free and plan-dependent alternatives before spending.

Plan one compatible resource upgrade

Improve a working game with before/after tests · Plan a small puzzle · Return to Build a Game

Guests can read, build and copy without an account. Production sign-in is currently unavailable; keep prompts and source on your device. This example’s local checkpoint is not My Forge saving.

Original instructions and licence

Original code, drawn shapes and generated tones use the included MIT notice. Retain it for redistribution of covered source or substantial portions. Added code/art/audio/fonts and engine binaries have separate terms; game sales, raw-file redistribution and source/template products are different permissions. Forge does not own third-party creator resources.

These sources explain the browser APIs. The example, test evidence and limitations belong to this exact Pocket Engine release; source documentation alone does not establish its browser or device acceptance.