Projects / Personal / 2026

Building a Retro Arcade Game With No Engine and No Asset Files Using HTML Canvas and TypeScript

Benefits

  • 8 levelseach one a real project from his career
  • ~20 secondsto clear a level, against a 30-second limit
  • 6.5 KBof compressed game code
  • 0 assetsno engine, image files or audio files
  • 8 of 8levels verified winnable by a scripted player

Overview

Moiz Ahmed is a full-stack engineer with eight years at Google, Apple and Amazon. For his personal site he wanted something visitors would enjoy and remember, and a way to walk them through his career that didn't feel like reading a list.

He built Fix the Timeline, a browser game in which the timeline has broken and the site's mascot, MoBot, travels back to repair it. Each of the eight levels is a real project: the player flies past obstacles and drops the project into the year it was built. Clearing every level reveals the prize, which is his résumé, pieced together from the projects the player has just placed. The whole game is 6.5 KB of compressed code with no engine and no asset files.

Opportunity | Making a Portfolio People Remember

Most portfolios are read once and forgotten. A recruiter might spend two minutes on a site, skim a list of roles and move on, and nothing about that visit distinguishes one engineer from the next.

"I want people to remember they had fun with the site and want to come back to it."

Moiz Ahmed, Software Development Engineer

The constraint was that the same recruiter still has only two minutes. Anything playful had to be quick to start, impossible to get stuck in, and never stand between a visitor and the information they came for.

Solution | Building a Lightweight Game Where Every Level Is a Real Project

Moiz made the product decisions first.

  • The content is the game. Each level is one real project, and the goal is to place it in the right year. Playing teaches the career history as a side effect.
  • Thirty seconds a level. Short enough that a recruiter can try one between other tabs.
  • The year is never a guessing game. The intro and the top bar both state the target year. The challenge is flying and timing the drop.
  • A surprise ending. Players are told only that a gift is waiting. The résumé is revealed after the final level.
  • Optional. Every fact the game unlocks is on the Projects page, and the résumé has its own download button.
  • Works on a phone. On-screen direction and drop buttons appear on touch devices.
  • Original artwork and sound. The style nods to arcade games and space-opera intros, with nothing borrowed from either.

The rest of this section covers the system design in detail.

Requirements and constraints

Page weightMust not slow down a site that is otherwise static HTML. No engine download.
FairnessEvery level must be winnable, and the same on every play, so skill improves with repetition.
MaintenanceAdding a project to the site should add a level, with no level editor and no art pipeline.
InputsKeyboard on desktop, touch on phones, with the same simulation behind both.
Frame rateSmooth at 60 frames a second and correct at any other rate, including 120 Hz displays and throttled background tabs.
AccountsNone. Progress is local; the leaderboard takes a nickname.

Architecture

Game architectureProject files become level data at build time. A shell on the page feeds input to a simulation called World, then passes World to a renderer and an audio module. A scripted player can drive World directly for testing.Project filesone Markdown file eachLevel dataJSON in the pageKeyboard and touchfive boolean inputsShellframe loop, screens,progress, leaderboardowns the DOMWorldpure simulationupdate(dt, input)returns eventsRenderercanvasAudiooscillatorsScripted playerdrives World with no browserbuilddt, inputevents
The simulation knows nothing about the browser. That one boundary is what makes the game testable and keeps the rendering code trivial.

The code is four modules with one-way dependencies.

ModuleResponsibilityDepends on
WorldAll game rules: movement, obstacle spawning, collisions, the drop, lives, the timer and scoring.Nothing
RendererDraws one frame of a World onto a canvas.World (read only)
AudioSynthesises music and sound effects.Nothing
ShellThe frame loop, input listeners, menus and overlays, saved progress and the leaderboard.All three

World exposes one method, update(dt, input), which advances the level and returns a list of events such as "hit" or "win". The shell turns events into sounds and screens. Because World never calls out, there are no callbacks to mock and no hidden dependency on time or the DOM.

The frame loop and time step

The loop runs on requestAnimationFrame with a variable time step: each frame passes the elapsed time to the simulation, and all movement is expressed in pixels per second. The game therefore plays at the same speed on a 60 Hz laptop and a 120 Hz phone.

The elapsed time is clamped to 50 milliseconds. Without the clamp, a tab that was in the background for ten seconds would hand the simulation a ten-second step on return, and the ship would teleport through every obstacle. With it, a stalled game slows down instead of skipping ahead.

A fixed time step with an accumulator would be more rigorous, and matters for physics that must be exactly reproducible. Here the only physics is constant velocity and one falling object, where the variable step's error is far below a pixel, so the simpler loop was chosen.

Level lifecycle

Level state machineA level moves from the menu to an intro, then a run phase, then a drop phase. A correct drop wins and shows the project card. Losing all lives or running out of time, in either phase, leads to a retry screen.MenuIntroskippableRundodgeDroppick the yearWonproject card, next levelLostno lives, or out of timetry again
Run and Drop are states inside World. Menu, Intro and the result screens belong to the shell.

The run phase ends when its spawn window has closed and the last obstacle has left the screen, so the player never has to dodge and aim at once. In the drop phase a row of year vaults scrolls along the ground and wraps around, so a missed year comes back. A wrong drop crosses that year out and costs a life, which turns a mistake into information.

Deterministic levels from data

Levels are not hand-built. At build time, each project file contributes its title, company and year. At run time the level number seeds a small pseudo-random generator, and everything about the layout comes from that stream: which obstacle appears when, where each gap sits, and which decoy years stand beside the real one.

  • Same seed, same level. A level is identical on every play and on every device, so players can learn it and scores are comparable.
  • No layout data to maintain. A level is described by one integer. Adding a project file adds a level.
  • Difficulty is a formula. Scroll speed, the gap in each wall, the time between obstacles and the number of vaults are all functions of the level number.
ParameterLevel 1Level 8
Scroll speed131 px/s208 px/s
Gap in a wall100 px72 px
Time between obstacles1.19 s0.77 s
Year vaults to choose from36
Vault scroll speed69 px/s118 px/s

Decoy years are drawn from a window around the correct year and capped at the present, then sorted, so the row reads left to right in time order and never offers a year that hasn't happened.

Collision and fairness

Everything is an axis-aligned rectangle, so collision is four comparisons per obstacle with at most a handful on screen. No spatial index is needed at this scale.

  • A forgiving hit box. The ship's collision rectangle is a few pixels smaller than its sprite on every side, so a near miss looks like a near miss.
  • Invulnerability after a hit. The ship blinks and cannot be hit again for just over a second, which stops one wall from taking all three lives.
  • A generous landing. The dropped project falls under constant gravity and lands in whichever vault is under it. The whole width of the vault counts.

Rendering

The canvas has a fixed logical size of 480 by 270 pixels and is stretched to fit with nearest-neighbour scaling, which keeps pixels sharp at any size and makes the layout arithmetic independent of the screen. Every sprite, including MoBot's ship and the bugs, is a few filled rectangles drawn in code. There is no sprite sheet, so there is nothing to load and nothing to go blurry.

Text inside the game uses the site's pixel font drawn onto the canvas. Menus, the intro, project cards and the leaderboard are ordinary HTML laid over the canvas, which keeps them selectable, responsive and readable by screen readers, and avoids re-implementing text layout in canvas code.

Input

Keyboard and touch both write to the same five-field input object: four directions and a drop flag. The simulation reads that object and never sees an event. On-screen buttons use pointer events and handle release, leave and cancel, so a finger sliding off a button can't leave the ship stuck moving.

Arrow keys and space scroll a web page by default. The game suppresses that only while a level is in flight, so the page scrolls normally on menus and result screens. The drop is edge-triggered and ignores key repeat, so holding space drops once.

Audio without files

Music and effects are generated with the Web Audio API: square and triangle oscillators with short gain envelopes, a four-bar bass and arpeggio loop, and pitch slides for hits and drops. The entire soundtrack is about a hundred lines of code and adds nothing to the download.

Browsers block audio until the user interacts, so sound starts muted and the audio context is created on the first press of the sound button.

State, persistence and the leaderboard

StateLives inNotes
A level in progressMemoryDiscarded on leaving the page. A level is 30 seconds, so nothing is lost.
Unlocked levels and best scoresBrowser local storageNo account. Read defensively, because storage can be blocked or stale.
LeaderboardOne key in a key-value store, behind a Worker endpointA sorted list capped at fifty entries; the top ten are shown.

The leaderboard's trust model is deliberately modest. Scores are computed in the browser and submitted by it, so a determined visitor can post one they didn't earn. The endpoint rejects scores above the maximum the game can award, restricts nicknames to a short safe character set, filters offensive names and limits posts per visitor per day.

Making it tamper-proof would mean recording inputs and replaying the level on the server, which the deterministic simulation makes possible. For a board that exists for fun on a personal site, that was more machinery than the problem deserved.

The list is read, modified and written back as a whole, so two simultaneous posts can race and one may be lost. At this traffic that is acceptable; at higher volume it would move to a store with atomic updates.

Living inside a site that doesn't reload

The site swaps pages without a full reload, so the game can't assume it starts with a fresh page or ends when the visitor leaves. It mounts when the game page arrives and returns a single cleanup function that cancels the frame loop, removes every listener through one abort signal and closes the audio context. Without that, each visit would leave a loop running in the background.

The loop also pauses itself when the tab is hidden or when the chat sidebar opens, so the timer never runs while the player is looking elsewhere.

Testing

  • A scripted player. Because World has no browser dependencies, a short script can play it directly: steer toward the gap in the nearest wall, away from the nearest bug, and drop when the correct vault is under the ship allowing for fall time. It cleared all eight levels with three lives each.
  • An idle player. The same harness run with no input confirms that every level can be lost, and that the timer ends a level nobody is playing.
  • End to end in a browser. The scripted player was also run through real keyboard events against the page, covering the intro, the win and loss screens, saved progress, the ending and the leaderboard.
  • Touch. The on-screen controls were exercised at phone width with pointer events.

A script with perfect reflexes proves that levels are possible, not that they are fun. Difficulty was tuned by playing, and that judgement doesn't automate.

Alternatives considered

ChosenAlternativeWhy not
Plain canvas and TypeScriptA game engine such as PhaserHundreds of kilobytes to move a few rectangles.
Rectangles drawn in codeSprite sheetsAn art pipeline and image downloads for a handful of shapes.
Synthesised soundAudio filesLarger downloads and licensing questions.
Seeded generationHand-designed levelsEvery new project would need a level built for it.
Variable time step with a clampFixed step with an accumulatorExtra complexity with no visible benefit for constant-velocity motion.
HTML overlays for menusMenus drawn on the canvasWould lose accessibility and responsive text for no gain.
Browser-reported scoresServer-side replay verificationOut of proportion for a just-for-fun leaderboard.

Known limitations and next steps

  • The game itself is not accessible to someone who can't see the canvas or use directional input. This is why everything it unlocks is available elsewhere on the site.
  • No measurement yet of how many visitors start, finish or abandon a level. Anonymous counters for those events are the next addition.
  • The leaderboard can be spoofed, as described above.
  • Difficulty is tuned by feel and has only been played by a few people.
  • Progress is per device, since there are no accounts.

Outcome | A Career History Players Finish in Under Three Minutes

Fix the Timeline went live on meetmoiz.com in October 2026. A complete run of all eight levels takes under three minutes of play, and ends with the visitor holding a résumé they assembled themselves.

MeasuredResult
Levels8, generated from the same files as the Projects page
Time to clear a level (scripted player)19 to 22 seconds, against a 30-second limit
Levels verified winnable8 of 8, with three lives remaining
Game code6.5 KB compressed; about 870 lines of TypeScript
Image and audio files0
Cost to run$0; the leaderboard fits within free tiers

First-month projections

These figures are projections, not results. They will be replaced with measured data once the site has run for a month.

MeasureProjectionBasis
Visitors who start a levelabout 1 in 5The game is linked from the home page's main buttons.
Starters who finish level 1about 70%Level 1 is slow, with three vaults and three lives.
Starters who finish all eightabout 25%Roughly three minutes of play; the gift panel shows progress.
Leaderboard entries20 to 40A minority of finishers post a score.
Résumé downloads from the ending10 to 20Most finishers see the download prompt.

Next, Moiz plans to add anonymous counters for level starts and completions, so the projections above can be checked and the difficulty tuned with data.

About Moiz Ahmed

Moiz Ahmed is a full-stack software engineer based in Seattle with eight years at Google, Apple and Amazon. He builds distributed services, AI-powered tools and the automation that helps engineering teams ship faster.

Technologies used

HTML Canvas

A 480 by 270 pixel surface scaled up with crisp pixels. Every sprite is drawn from rectangles in code.

TypeScript

About 870 lines across simulation, rendering, audio and the page shell, with no game engine.

Web Audio API

Music and effects synthesised from oscillators at run time, so there are no sound files to download.

Astro content collections

The same project files that render the Projects page generate the levels at build time.

Cloudflare Workers and KV

A small endpoint and one stored list hold the nickname leaderboard.

Browser local storage

Saves unlocked levels and best scores on the device, with no sign-in.

Built with Claude Code as a pair programmer: the product and design decisions are Moiz's, and much of the code was written in that collaboration.

← More projects