Writing an Effective Game Brief
- Identify the four essential elements of a game brief
- Write a clear brief that an AI assistant can act on without follow-up questions
- Recognize and avoid the most common briefing mistakes
What is a game brief?
A game brief is a short written document that defines what you want built. It is the first artifact of any software project. With AI-assisted development, the quality of your brief directly determines the quality of your first working prototype.
The four essential elements
Every effective game brief specifies four things:
- Platform โ where the game runs (browser, mobile app, desktop, console)
- Controls โ how the player interacts (keyboard, touch, gamepad, mouse)
- Mechanics โ what the player can do and what challenges them (jump, shoot, dodge, collect)
- Style / Vibe โ the look and feel you want (cartoony, realistic, pixel art, premium 3D)
"Build a browser-based side-scrolling racing game. Player controls a red monster truck using arrow keys (left/right for movement, up for jump, spacebar for nitrous boost). Side profile view with realistic computer graphics. Obstacles include bridges and water holes โ falling in water ends the game. Spacebar gives a 2-second speed boost; player has 5 nitrous charges that replenish every 15 seconds. The HUD at the top should look like a premium, million-dollar video game."
This brief specifies all four elements without ambiguity. An AI assistant can begin work immediately and the result will closely match the writer's intent.
Common briefing mistakes
| Mistake | Why it fails | Better approach |
|---|---|---|
| "Build me a fun game" | No platform, no controls, no mechanics, no style | Specify all four elements |
| "Make it like [popular game]" | Copyright risk + the AI can't actually copy the game | Describe specific mechanics instead |
| Writing 5 pages of specifications upfront | You can't predict every detail before seeing v1 | Specify v1 essentials, iterate from there |
| Skipping the visual style | You get a generic look that disappoints | Reference a known style or attach an inspiration image |
Cost considerations to include
Strong briefs also include a budget instruction. If your project will use paid APIs (image generation, 3D models, music), ask the AI to confirm cost estimates before spending. A simple line like "tell me the approx cost of any API usage before implementing" prevents surprises.
Choosing Your Tech Stack
- Choose between 2D and 3D engines based on your game's needs
- Compare the trade-offs of major browser game frameworks
- Justify why "simplest stack that works" is almost always correct
The first decision: 2D or 3D?
If your game can work as a side-view, top-down, or fixed-angle 2D presentation, choose 2D. 2D games are faster to build, run on weaker devices, look great with the right art, and cost far less to produce.
Only choose 3D if the gameplay requires three-dimensional movement that cannot be faked in 2D. Examples: first-person shooters, free-roaming explorers, racing simulators with corner-turning camera angles.
Comparing browser game frameworks
| Framework | Type | Best for | Learning curve |
|---|---|---|---|
| HTML5 Canvas + Vanilla JS | 2D, no library | Simple games, learning fundamentals | Low |
| PixiJS | 2D, WebGL-accelerated | Polished 2D games needing shaders | Low-Medium |
| Phaser | 2D framework | Game-specific conveniences (scenes, physics) | Medium |
| Three.js | 3D, low-level | Custom 3D experiences | High |
| Babylon.js | 3D, game-focused | 3D games with built-in features | High |
The case for Canvas + Vanilla JS
For your first project, choose HTML5 Canvas with vanilla JavaScript. Here's why:
- Zero installation. No npm, no webpack, no build configuration. Open an HTML file in any browser and it runs.
- Universal compatibility. Works on every device with a browser, including old phones and tablets.
- Instant feedback loop. Edit a file, refresh the browser, see the change. No compile step means faster iteration.
- Deployment is just file copying. Upload your folder to any web server.
When to graduate to a framework
You'll know it's time when:
- Your game has 20+ entities and managing them is becoming chaotic
- You need physics that would take hours to write yourself (use Matter.js)
- You need WebGL effects like bloom, blur, or particle systems at scale (use PixiJS)
- You're certain about the project's scope and stakes (use Phaser for serious 2D, Three.js for 3D)
Domain, Server, and HTTPS Setup
- Explain how a browser request reaches your web server
- Set up DNS records to point a domain to a server
- Configure a web server to serve your game files
- Add free HTTPS using Let's Encrypt
The four-step web delivery chain
When someone visits yourgame.example.com, four systems hand off:
- Registrar (where you bought the domain) tells the world which nameserver controls it
- Nameserver (DNS provider) holds the records that say "this name โ this IP address"
- Web server (running on a virtual machine at that IP) listens for HTTP requests
- Web server config matches the requested hostname to a folder of files
Many beginners confuse steps 2 and 3 โ DNS is not on your web server. DNS lives at your registrar or DNS provider. You configure it through their control panel or API.
Adding a DNS record
To make yourgame.example.com point to a server with IP 138.197.145.209, add an A record:
Type: A
Name: yourgame
Value: 138.197.145.209
TTL: 3600 (1 hour)
DNS changes can take 5 minutes to an hour to propagate worldwide. Use a tool like dnschecker.org to monitor.
Configuring your web server
If you're using Apache, create a virtual host file telling Apache where to serve files from:
<VirtualHost *:80>
ServerName yourgame.example.com
DocumentRoot /var/www/yourgame/public
<Directory /var/www/yourgame/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Then enable the site and reload Apache:
a2ensite yourgame.example.com.conf
systemctl reload apache2
Adding free HTTPS with Let's Encrypt
HTTPS is required for any modern web app. Let's Encrypt provides free certificates that auto-renew. After DNS propagates, run:
certbot --apache -d yourgame.example.com \
--non-interactive --agree-tos -m your@email.com \
--redirect
Certbot does three things: requests a certificate from Let's Encrypt, installs it in your web server config, and sets up automatic renewal. The certificate is valid for 90 days and renews itself.
Cost summary
| Item | Typical cost |
|---|---|
| Domain registration | $10-15/year |
| DNS hosting | $0 (included with registrar or DO) |
| Virtual server (DigitalOcean droplet, etc.) | $6-12/month |
| HTTPS certificate | $0 (Let's Encrypt) |
| Bandwidth (for typical small site) | $0 (included in server) |
game1.yourdomain.com). Note down the IP of any server you plan to use. You don't need to configure anything yet โ just have the pieces ready.
Generating Game Art with AI
- Compare the cost and capabilities of major AI image generators
- Write image prompts that consistently produce usable results
- Choose between transparent and opaque backgrounds appropriately
- Estimate the total art budget for a small game
Why AI image generation for games
Hiring a professional illustrator costs $50โ$200 per image with a multi-day turnaround. AI image generation costs $0.05โ$0.30 per image and takes 20โ60 seconds. For prototypes, indie projects, and learning, AI is the obvious choice.
OpenAI image model pricing (2026)
| Model | Quality | Size | Price per image |
|---|---|---|---|
| gpt-image-1 | Low | 1024ร1024 | $0.011 |
| gpt-image-1 | Medium | 1024ร1024 | $0.042 |
| gpt-image-1 | Medium | 1536ร1024 (wide) | ~$0.063 |
| gpt-image-1 | High | 1536ร1024 (wide) | ~$0.25 |
| DALL-E 3 | Standard | 1024ร1024 | $0.040 |
| DALL-E 3 | HD | 1792ร1024 (wide) | $0.120 |
For game backgrounds, the bolded option (gpt-image-1 medium wide) is the sweet spot โ good enough quality for parallax backdrops at low cost.
Anatomy of a strong image prompt
A reliable prompt has six elements:
- Style anchor โ reference a known visual style ("Sonic Superstars Hill Top Zone", "Studio Ghibli")
- Subject โ what's in the image
- Composition โ "wide landscape", "side view", "top 60% blank"
- Color palette โ "vibrant saturated", "muted earth tones"
- Exclusions โ "no characters, no vehicles, no text"
- Quality words โ "premium", "illustrative", "cinematic"
"Premium children's book illustration of a bright sunny desert sky with fluffy white clouds and distant rolling purple-blue hills in soft flat vector style. No characters, no vehicles, no foreground. Cheerful color palette: warm peach sky fading to baby blue. Wide landscape composition, flat design, smooth gradients, no text. Style: Sago Mini, Toca Boca, modern children's app illustration."
Handling the transparency problem
AI image models don't reliably produce transparent backgrounds even when asked. Two workarounds:
- Solid chroma-key background. Ask for "solid magenta background" and remove the magenta in code at load time.
- Composition instruction. Ask for "the top 60% should be blank/transparent" and accept the result.
Budgeting art for a small game
| Project size | Image count | Cost range |
|---|---|---|
| Tiny game (1 background, 2 sprites) | ~5 with retries | $0.30โ$0.80 |
| Small game (3-5 backgrounds, UI, sprites) | ~10-15 with retries | $1โ$3 |
| Medium game (multiple levels, characters) | ~30-50 with retries | $5โ$15 |
Organizing Your Code
- Apply the "separation of concerns" principle to game code
- Design a file structure that scales with your project
- Explain why most small projects don't need build tools
The principle: one file, one responsibility
Each file in your project should do one thing well. When responsibilities are mixed across files, changes ripple unpredictably and bugs hide in unexpected places.
Recommended file structure for a 2D game
your-game/
โโโ index.html Page structure, HUD, modals, canvas element
โโโ style.css Visual styles for the HUD and overlays
โโโ assets/
โ โโโ bg-layer1.png Background art
โ โโโ bg-layer2.png
โ โโโ music.mp3
โโโ src/
โโโ audio.js Sound effects and music
โโโ input.js Keyboard and touch event handlers
โโโ parallax.js Background scrolling
โโโ terrain.js Ground/level data
โโโ particles.js Visual effects
โโโ obstacles.js Hazards and collectibles
โโโ player.js Player entity
โโโ enemy.js Enemy entities (often extends player)
โโโ game.js Main loop, state management
How the modules communicate
Each module exposes its functionality on the global window object as a class or namespace:
// In truck.js:
(function() {
class Truck { /* ... */ }
window.Truck = Truck;
})();
// In game.js:
const player = new window.Truck({ x: 100, y: 200 });
The main game.js file orchestrates everything: it instantiates entities, runs the game loop, dispatches events. Individual modules don't reach across to each other directly.
Why no build tools?
Build tools (webpack, Vite, npm, TypeScript compilation) add power but multiply complexity. For most browser games, the trade is not worth it. Plain <script> tags in your HTML are sufficient:
<script src="src/audio.js"></script>
<script src="src/input.js"></script>
<script src="src/player.js"></script>
<script src="src/game.js"></script>
Order matters: load dependencies before the files that use them.
Naming conventions
- Files: lowercase with hyphens or single-word (
audio.js,player-controller.js) - Classes: PascalCase (
Truck,ParticleSystem) - Functions: camelCase (
updateScore,checkCollisions) - Constants: UPPER_SNAKE_CASE (
MAX_SPEED,GROUND_Y)
Player Movement and Physics
- Implement variable jump height (tap = short, hold = high)
- Create procedural rolling terrain with sine waves
- Tilt a player's body to match terrain slope
- Apply gravity and ground collision correctly
The basic physics loop
Every frame, three things happen for each moving entity:
- Apply forces (gravity, jump impulses, player input)
- Update velocity from forces
- Update position from velocity
function update(dt) {
// 1. Forces
player.vy += GRAVITY;
if (jumpPressed && player.onGround) {
player.vy = -JUMP_IMPULSE;
}
// 2. Velocity already updated above
// 3. Position
player.y += player.vy;
player.x += player.vx;
// 4. Resolve collisions
if (player.y >= groundY) {
player.y = groundY;
player.vy = 0;
player.onGround = true;
}
}
Variable jump height (a platformer classic)
To make tap = short jump and hold = high jump, apply a continuous upward force while the key is held โ up to a maximum duration:
// When jump first pressed:
player.vy = -INITIAL_IMPULSE // -14
player.jumpHoldTime = 0
player.isJumping = true
// Each frame while key held AND jumpHoldTime < MAX:
if (player.isJumping && jumpHeld && player.jumpHoldTime < 0.45) {
player.vy -= ADDITIONAL_FORCE // -0.6 per frame
player.jumpHoldTime += dt
}
// Always:
player.vy += GRAVITY // 0.7
The cap (0.45 seconds in this example) prevents the player from holding the button to fly forever. Tune these three values โ initial impulse, additional force, max hold โ until the jump feels right.
Procedural rolling terrain
Instead of a flat ground line, layer three sine waves to create natural-looking rolling hills:
function groundY(worldX) {
return baseY - (
Math.sin(worldX * 0.0035) * 55 +
Math.sin(worldX * 0.008 + 1.7) * 22 +
Math.sin(worldX * 0.018 + 3.3) * 8
);
}
The three layers have different frequencies and offset phases. The result is hills that don't repeat predictably โ they feel natural.
Following the terrain
When the player is grounded, their Y position should match the terrain at their X:
const groundHere = groundY(player.x);
if (player.y >= groundHere) {
player.y = groundHere;
player.vy = 0;
player.onGround = true;
}
Body tilt with slope
To make the player visually lean forward going downhill and back going uphill, compute the slope (rise over run) and convert to a rotation angle:
const dx = 5;
const slope = (groundY(player.x + dx) - groundY(player.x - dx)) / (2 * dx);
player.bodyTilt = Math.atan(slope);
When you draw the player, rotate the canvas context by player.bodyTilt before drawing.
src/truck.js to see the exact code implementing this lesson.
Building AI Opponents
- Implement rubber-band AI that keeps races feeling close
- Use adaptive difficulty that responds to player skill
- Enforce minimum spacing between multiple AI entities
- Recognize that "game AI" is rules-based, not machine learning
Game AI is rules, not intelligence
When someone says "the game has good AI," they usually mean the rules produce behavior that feels intelligent. Almost no commercial game uses machine learning for opponents. Game AI is finite state machines, decision trees, and tuned numerical rules.
The rubber-banding pattern
In racing games, you want opponents to stay close to the player โ exciting races require the outcome to feel uncertain. The technique is called rubber-banding: invisible elastic that pulls opponents toward the player.
function rivalAI(rival, player, idealOffset) {
// Where we want the rival to be (relative to player)
const desiredX = player.x - idealOffset;
// How fast should we move to get there?
const xDelta = desiredX - rival.x;
rival.targetVx = player.vx + xDelta * 0.04;
// Smooth velocity approach (no instant teleport)
rival.vx += (rival.targetVx - rival.vx) * 0.07;
}
Adaptive difficulty
The interesting part: adjust the rival's behavior based on how well the player is doing.
// Sample player's recent average speed
const recentAvgVx = average(playerSpeedSamplesLast60Frames);
if (recentAvgVx > expectedSpeed * 0.95) {
// Player is doing well โ rival keeps up but stays behind
rival.targetVx = player.vx * 0.95;
} else if (recentAvgVx < expectedSpeed * 0.5) {
// Player is struggling โ rival slows down to let them catch up (kid mode)
// OR pulls ahead and threatens to win (adult mode)
rival.targetVx = mode === 'kid' ? recentAvgVx * 0.8 : expectedSpeed;
}
Multiple rivals โ enforce minimum spacing
If you have several AI opponents, they'll naturally cluster at the same target position. Push them apart:
for (const other of allRivals) {
if (other === this) continue;
const gap = this.x - other.x;
if (Math.abs(gap) < MIN_GAP) {
const push = (MIN_GAP - Math.abs(gap)) * 0.5;
desiredX += (gap > 0 ? 1 : -1) * push;
}
}
"Never lose" mode for young players
For games aimed at very young players, you might want to guarantee the player can win. Hard rules near the finish line:
const inFinalStretch = (timeLimit - gameTime) < 5; // last 5 seconds
if (inFinalStretch && mode === 'kid') {
// Hard cap: rival cannot exceed player's X position
if (rival.x > player.x - 40) {
rival.x = player.x - 40;
rival.vx = Math.min(rival.vx, player.vx * 0.9);
}
}
Predictive behaviors
AI opponents should react to upcoming obstacles. For a jumping AI:
for (const obstacle of obstacles) {
if (!obstacle.requiresJump) continue;
const distance = obstacle.x - rival.x;
if (distance > 50 && distance < 200 && rival.onGround) {
rival.jump();
break;
}
}
Sound Design with the Web Audio API
- Choose between synthesized sounds and audio files
- Synthesize basic game sound effects in code
- Find legally usable music for your project
- Add background music with fade in/out
Two routes for game audio
| Approach | Pros | Cons |
|---|---|---|
| Web Audio API synthesis | Free, no file loading, infinite variations | Limited to electronic/synthesized sounds |
| MP3/OGG audio files | Real instruments, recorded voices, any sound | Larger downloads, need to source legally |
A common hybrid: synthesize SFX (jump, hit, coin), use MP3 for music.
Synthesizing a jump sound
const ctx = new AudioContext();
function playJump() {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(220, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(680, ctx.currentTime + 0.18);
gain.gain.setValueAtTime(0.25, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.22);
osc.connect(gain).connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.25);
}
The pattern: create an oscillator (the tone) and a gain node (the volume envelope), connect them, then start and stop the oscillator. Sweep the frequency for "boing" effects; sweep volume down for natural decay.
Standard game sound recipes
| Effect | Wave | Frequency sweep | Duration |
|---|---|---|---|
| Jump | Sine | 220 โ 680 Hz | 0.18s |
| Land | Sine | 120 โ 60 Hz | 0.12s |
| Coin pickup | Two sines (880 + 1320 Hz) | Constant | 0.15s + 0.04s offset |
| Hit / bonk | Square | 180 โ 80 Hz | 0.08s |
| Nitrous / hiss | White noise | 800 โ 3000 Hz bandpass | 0.5s |
| Win fanfare | Triangle | 523 โ 1047 Hz arpeggio | 0.6s |
Legal music sources
For background music, never use copyrighted commercial songs. Use one of these instead:
- freepd.com โ CC0, no attribution required
- incompetech.com โ Kevin MacLeod, CC-BY (attribution required)
- pixabay.com/music โ large library, free with credit
- Original compositions you commission from a musician on Fiverr (~$50-200)
Playing music with fade in/out
const music = document.createElement('audio');
music.src = 'assets/music.mp3';
music.loop = true;
music.volume = 0;
music.play();
// Fade in over ~2 seconds
let v = 0;
const fadeIn = setInterval(() => {
v = Math.min(v + 0.02, 0.4);
music.volume = v;
if (v >= 0.4) clearInterval(fadeIn);
}, 50);
Iterating on Feedback
- Write feedback that triggers precise fixes rather than guesswork
- Use screenshots and reference images effectively
- Prioritize bugs vs polish vs scope additions
The feedback formula
A good feedback message contains three things:
- The observation โ what you see now
- The intent โ what should happen instead
- Optional: a reference โ screenshot, example URL, or sketch
"BUG: when I press shift, the speed jumps from 3 to 6 and stays there. Speed should be constant in kid mode. Also, SHIFT shouldn't be the nitrous key โ use the spacebar for jump only."
This message names the bug, describes the trigger, and specifies the fix. It can be acted on immediately without follow-up questions.
What makes weak feedback
| Weak feedback | Why it fails | Better version |
|---|---|---|
| "It doesn't look right" | No information about what's wrong | "The trucks look too small relative to the trees" |
| "Can you fix it?" | What's broken? | "When I click jump twice fast, the truck floats. Should only jump once per ground touch." |
| "Make it better" | Better in what way? | "The coins are too dim against the bright background โ add a glow effect or change them to a darker shade." |
Using reference images
One good reference image is worth a thousand words of description. If you can find an existing game or app that has the look you want, paste a screenshot with the instruction "make it more like this." The AI will absorb visual style faster than you can describe it.
Iteration vs scope additions
Two different requests to keep separate:
- Iteration โ refining something that already exists ("the coin is too small")
- Scope addition โ asking for something new ("add 3 more trucks")
Bundle iteration into one message; introduce scope additions one at a time. This keeps the AI's attention focused.
Planning Project Costs
- Estimate the cost of common project types
- Identify which costs are one-time vs recurring
- Use AI capabilities tactically to stay under budget
One-time vs recurring costs
| Item | Type | Typical cost |
|---|---|---|
| Domain registration | Annual | $10-15/year |
| Virtual server | Monthly | $6-12/month |
| HTTPS certificate | Free | $0 (Let's Encrypt) |
| AI image generation | Per-image | $0.04-0.25 |
| AI 3D model generation (Meshy.ai) | Per-model | $0.50-2 |
| AI assistant subscription | Monthly | $20/month (Claude Pro) |
| Music (royalty-free) | Free or one-time | $0-$50 |
Project-type cost estimates
| Project type | One-time cost | Recurring |
|---|---|---|
| Static landing page | $0 | $0 (use GitHub Pages) |
| Simple 2D browser game | $1-5 (images) | $0 if hosted free |
| 2D game with custom art | $5-15 | ~$6/month server |
| 2.5D fighting game | $10-30 (DALL-E) or $50-200 (Meshy) | ~$6/month server |
| Full 3D adventure game | $200-1000 (3D models) | ~$12/month server |
| AI chatbot for business | $0 dev | $2-50/month API based on usage |
| Multiplayer game (small scale) | $0 dev | $12-50/month server + DB |
Strategies to stay under budget
- Use medium quality, not HD. For prototype art, gpt-image-1 medium is 4x cheaper than high and looks great.
- Use procedural art where possible. Anything geometric (UI icons, simple sprites) can be drawn in code at $0.
- Cache and reuse images. Pull one strong asset, use it across multiple game elements.
- Start with free tools. Mixamo for 3D character animations, freepd.com for music, Web Audio for SFX.
- Set a hard ceiling. Tell the AI "spend no more than $0.50 on this." Limits force creative reuse.
Deploying Your Game
- Choose a deployment method matched to your project size
- Upload your game files to a web server
- Test the deployed version before sharing
Three deployment options
| Option | Cost | Best for |
|---|---|---|
| GitHub Pages | Free | Static games, public source |
| Netlify / Vercel | Free tier | Static or serverless, fast CDN |
| Your own server (DigitalOcean, Linode) | $6-12/mo | Multiple projects, full control, server-side features |
Deploying to your own server
If you already have a server set up from Lesson 3, deployment is just copying files:
# From your local machine:
scp -r ./my-game/* user@your-server-ip:/var/www/my-game/public/
# Set proper permissions on the server:
ssh user@your-server-ip
chown -R www-data:www-data /var/www/my-game/
chmod -R 755 /var/www/my-game/public/
Deploying to GitHub Pages (free)
- Create a new GitHub repository
- Upload your game folder contents
- Go to Settings โ Pages โ enable Pages from the main branch
- Your game is live at
https://username.github.io/repo-name/
Pre-launch checklist
- โ Test in Chrome, Firefox, and Safari
- โ Test on a phone (or use Chrome DevTools mobile emulator)
- โ
Verify all asset URLs work (no
file://or local paths) - โ Verify the HTTPS certificate is valid (no browser warnings)
- โ Hard-refresh the deployed version (Ctrl+Shift+R) โ your browser may have cached the old version
Sharing your game
Once live, share the URL. Real feedback from real users is the only reliable way to know if your game is good. Watch someone play it without explaining anything โ their confusion shows you what to improve next.
Making Your Game Mobile-Friendly
- Add on-screen touch buttons for mobile players
- Detect touch devices and conditionally show controls
- Make your layout responsive across screen sizes
- Handle touch events correctly (avoiding common pitfalls)
Why mobile matters
Over half of casual game players are on phones. If your game requires a keyboard, you exclude that audience entirely. Adding touch support takes about an hour and unlocks 5ร the potential players.
Two layers of touch support
- Tap anywhere on canvas = jump (simplest, works without UI changes)
- Dedicated on-screen buttons (better UX, supports continuous actions like "hold to accelerate")
Implement both. The on-canvas tap is a safety net; the dedicated buttons are the primary interface.
Canvas tap handler
const canvas = document.getElementById('gameCanvas');
const tapHandler = (e) => {
// Don't fire if the touch landed on a UI button
if (e.target && e.target.tagName === 'BUTTON') return;
e.preventDefault();
Input.jumpQueued = true;
};
canvas.addEventListener('touchstart', tapHandler, { passive: false });
canvas.addEventListener('mousedown', tapHandler); // works for desktop too
The { passive: false } option lets us call preventDefault(), which stops the browser from also scrolling or zooming on the tap.
On-screen buttons (HTML)
<div id="touch-controls">
<button id="btn-gas">
<span class="touch-icon">โก</span>
<span class="touch-label">GAS</span>
</button>
<button id="btn-jump">
<span class="touch-icon">โฌ</span>
<span class="touch-label">JUMP</span>
</button>
</div>
Held-button pattern (for "hold to accelerate")
const gasBtn = document.getElementById('btn-gas');
gasBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
Input.touchGasHeld = true;
});
gasBtn.addEventListener('touchend', (e) => {
e.preventDefault();
Input.touchGasHeld = false;
});
// Also handle touchcancel โ fires when the OS interrupts (incoming call, etc.)
gasBtn.addEventListener('touchcancel', () => {
Input.touchGasHeld = false;
});
// Then in your game logic:
isAccelerating() {
return this.keys['ArrowRight'] || this.touchGasHeld;
}
CSS: only show buttons on touch devices
/* Hide by default (desktop) */
#touch-controls { display: none; }
/* Show on touch-only devices */
@media (hover: none) and (pointer: coarse) {
#touch-controls { display: flex; }
}
The (hover: none) and (pointer: coarse) queries together reliably detect touch-primary devices without false positives on hybrid laptops.
Button styling for thumbs
- Minimum 80ร80px tap target (60ร60 is acceptable, smaller is hostile)
- Position in bottom corners โ natural thumb reach when holding a phone
- Semi-transparent backgrounds so they don't block the game view
- Use
:activeCSS state to give immediate visual feedback on tap - Set
touch-action: manipulationto disable double-tap-to-zoom on the buttons
Responsive layout
Make your canvas scale to fill the screen while maintaining aspect ratio:
#game-container {
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
aspect-ratio: 16 / 9;
}
This sizes the container to the largest 16:9 rectangle that fits in the viewport, regardless of phone or tablet dimensions.
Test the basics
- Tap a button โ does the action fire?
- Hold a button โ does it stay active?
- Rotate the phone โ does the layout still work?
- Open and close on-screen keyboard (focus an input) โ does the canvas resize correctly?
- Try in airplane mode (after first load) โ service worker should serve from cache
e.preventDefault() on touch handlers. Without it, mobile browsers will double-fire (touch + synthesized click) and your game receives every jump twice.
Installing on Phone: PWA & APK
- Convert your browser game into a Progressive Web App (PWA)
- Add an app manifest and service worker for offline support
- Generate an installable .apk file using PWABuilder
- Decide between PWA-only and APK distribution
What is a PWA?
A Progressive Web App is a regular website with three additional ingredients:
- manifest.json โ tells the browser the app's name, icon, color, and how to display it
- service worker โ a background script that caches files for offline use
- HTTPS โ required (you already have this from Lesson 3)
With those three pieces, your game becomes installable from any browser. Users tap "Add to Home Screen" and it appears as an app icon โ opens fullscreen, no browser chrome, indistinguishable from a native app to the user.
Writing manifest.json
{
"name": "Monster Truck Hill Top Rally",
"short_name": "Monster Truck",
"description": "A premium kid-friendly browser racing game.",
"start_url": "./",
"scope": "./",
"display": "fullscreen",
"orientation": "landscape",
"background_color": "#0a3d4f",
"theme_color": "#0a3d4f",
"categories": ["games", "kids"],
"icons": [
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
Save as manifest.json in your public folder. Link it in your HTML:
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#0a3d4f">
<link rel="apple-touch-icon" href="icon-192.png">
Required: app icons
You need at least two PNG icons: 192ร192 and 512ร512. Generate them via AI (one prompt, $0.01 with gpt-image-1 low quality), then resize. The 512px version should be designed to look good both as a square and a circle (for adaptive icons on Android).
Writing the service worker
// sw.js
const CACHE = 'my-game-v1';
const FILES = [
'./',
'index.html',
'style.css',
'src/game.js',
'assets/bg.png',
'assets/music.mp3'
];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(FILES)));
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached =>
cached || fetch(e.request)
)
);
});
Register the service worker from your HTML:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js');
});
}
Testing installation
- Open the game URL in Chrome on Android (or Safari on iPhone)
- Chrome shows an "Install app" prompt OR a menu option in โฎ โ "Install app"
- iOS Safari: tap Share โฌ โ "Add to Home Screen"
- The app appears on your home screen โ launches fullscreen, runs offline after first load
Generating a real .apk file (PWABuilder route)
For Google Play Store submission or sideloading via APK file, use Microsoft's free PWABuilder:
- Visit pwabuilder.com
- Paste your PWA URL
- PWABuilder analyzes your manifest and service worker, scores readiness
- Click "Package For Stores" โ Android
- Download the
.apkfile (small โ a few MB, contains a WebView wrapper around your URL) - Sideload by emailing the APK to yourself and tapping it (enable "Install from Unknown Sources" first), OR submit to Play Store
PWA vs APK โ which to choose?
| Approach | Pros | Cons |
|---|---|---|
| PWA only | Free, instant install, no app store, updates push automatically | iOS treats it differently from "real" apps (some limitations) |
| PWA + APK sideload | Real .apk file for sharing/distribution | Users must enable Unknown Sources, no auto-updates |
| PWA + Play Store | Discoverable on Play Store, auto-updates | $25 one-time Google fee, review process, must use Trusted Web Activities |
App Store (iOS)
Apple does not allow PWAs to be submitted to the App Store as PWAs. To publish on iOS, you would need to wrap your web app with Capacitor or React Native โ significantly more work. For most indie projects, "Add to Home Screen" on iOS is sufficient.
install event. Only invest in APK / app store distribution if you have evidence the audience wants a "real app."
Advanced Prompts REGISTERED ONLY
Copy-paste these prompts into your AI assistant to add advanced features to your game. Each one is a real prompt that was used to extend this exact Monster Truck game.
Register to unlock
Advanced prompts are exclusive to registered students. Register your name (used for your certificates and progress tracking).
โ Welcome, Student! Here are your advanced prompts:
๐ Advanced Prompt #1 โ Fix the "game speeds up on restart" bug
A classic game-loop bug. If you click "Play Again" multiple times, the game runs faster every time. Here's the exact prompt that fixed it:
There's a bug where when I play again, the game goes 2x speed (or more after multiple restarts). My game loop uses requestAnimationFrame. Fix this so the game always runs at normal speed regardless of how many times the user restarts.
โถ See what the AI does with this prompt
_loopRunning flag so the loop is only started ONCE in init():
startGame() {
// ... reset state, etc ...
this.state = 'playing';
if (!this._loopRunning) {
this._loopRunning = true;
requestAnimationFrame((t) => this.loop(t));
}
}
The loop checks state === 'playing' each frame and skips work when paused.
โ๏ธ Advanced Prompt #2 โ Add a cheering airplane fly-by
A delightful kid-friendly engagement feature. Every 60 seconds during play, a cute red biplane flies across the screen with a banner shouting the player's name. Adds personality and rewards persistence.
Add a graphic of a cute red airplane flying from right to left across the top of the screen, pulling a banner that says "GO {playerName} GO!!". Trigger it every 60 seconds during play. Add a swooshing sound effect as it passes (Web Audio API white noise with a bandpass filter sweep). Generate a cute red biplane sprite via DALL-E if needed. The plane should bob slightly as it flies. Despawn when off-screen.
โถ See what the AI does with this prompt
- Generates a transparent-background red biplane sprite via gpt-image-1 (~$0.01)
- Adds an
airplanes[]array to game state - Spawns one every 60s by appending
{x, y, vx, height} - Updates positions in the game loop, despawns off-screen
- Renders the sprite + overlays the player's name as banner text on top
- Triggers
playAirplaneSwoosh()โ a 2.5s bandpass noise sweep that crescendos and fades
๐ฐ Advanced Prompt #3 โ Casino-style countdown + COIN ROUND interlude
Builds tension in the last minute and rewards persistence with a special "COIN ROUND" bonus screen. Uses the Web Speech API for kid-voice countdown โ no audio files needed.
First, by default make the timer HUD bigger and more lively/kid-friendly with bevels and bold borders. When the game time reaches 60 seconds remaining, make the countdown HUD stand out like a Vegas casino: pulse the numbers, change to a vivid red, scale up. At 45 seconds remaining, pause the game and show a full-screen "COIN ROUND!" interlude with big kid-friendly font. Do a 3-second countdown ("3", "2", "1") where each number is spoken by a high-pitched childlike voice using the Web Speech API. Then announce "COIN ROUND!" with the same childlike voice plus a cheering crowd sound effect (Web Audio synthesized). Then resume the game. During the coin round (15 seconds), spawn 3x as many coins, with some at higher Y positions so the player must jump to grab them. Speed up the background music to ~1.35x playback rate during the coin round for a more intense feel โ but keep it cute and kid-friendly. Revert music speed when the round ends.
โถ See what the AI does with this prompt
- Casino HUD: CSS class
.urgentadded attimer โค 60โ switches color to red, applies apulsekeyframe animation (scale 1 โ 1.15 โ 1), enlarges the digital font - COIN ROUND interlude: at
timer โค 45, setstate = 'coinround'to pause physics, render a bigCOIN ROUND!overlay with rainbow gradient text - Voice countdown:
speechSynthesis.speak()with pitch=2 rate=1.1 โ most browsers ship a default voice that sounds childlike at that pitch - Cheers: burst of 8 high-frequency Web Audio tones with overlapping envelopes mimics "kids cheering"
๐ Advanced Prompt #4 โ Celebration screen + Get Ready/Set/Go restart countdown
After every race, show a celebratory screen that addresses the player by name, announces their place, and asks if they want to play again. If yes, run a three-step "Get Ready ยท Set ยท Go!" countdown with a kid voice and a starting horn โ turning every restart into a moment of anticipation.
After the game ends, before showing the scoreboard, show a celebration screen that says "Good Job {name}!" dynamically using the entered name. Display their finishing place ("You're in 2nd Place!") and a motivational quote ("Practice Makes Perfect!"). Show two big buttons: YES and NO ("Do you want to try again?"). If YES, hide the celebration and run a Get Ready / Set / Go! countdown โ each step 1 second apart, each spoken by the kid voice (Web Speech API). When "Go!" is shown, play a brassy starting horn (Web Audio API: 4 stacked sawtooth/square oscillators at harmonic frequencies through a bandpass filter, ~0.7s) plus cheering. Then restart the game. If NO, show the leaderboard as usual.
โถ See what the AI does with this prompt
- Adds two new overlays:
#celebration-screenand#start-countdown - Splits the old
endGame()intoshowCelebration()+showScoreboard() - Customizes copy by placement: champion / good job / nice try / whoops splash
- Kid voice via
speechSynthesis.speak()at pitch 2.0 - Starting horn synthesized as a 4-oscillator brass chord through a bandpass filter
๐ช Advanced Prompt #5 โ Mario-style coin counter + auto-dimming HUD
Replaces a boxy "STARS 0" HUD element with the unmistakable arcade-classic pattern: coin icon ร count, gold accent border, animated coin spin, and a satisfying bump every time the player picks one up. Plus the HUD auto-dims when there's something more important to look at on screen.
Replace the stacked "STARS 0" HUD box with a Mario-style inline counter: a coin emoji (๐ช) followed by "ร N", placed beside the timer. Add a gold accent border, golden glow, and animate the coin doing a quick flip every 2 seconds. When the player picks up a coin, scale the number up briefly ("bump") for satisfying feedback. Default HUD opacity should be 0.9 so it doesn't visually dominate. When the airplane fly-by appears, smoothly dim the entire HUD to 0.2 opacity, and fade back to 0.9 (0.6s ease) when the airplane leaves.
โถ See what the AI does with this prompt
- New
.coin-counterCSS module replaces the score.hud-item - CSS keyframe
coin-spin-counterrotates the emoji on the Y axis - JS adds
.bumpclass to score element on pickup, removed after 180ms #hudgets defaultopacity: 0.9and a.dimmedclass withopacity: 0.2+ 0.6s transition
๐ซ Advanced Prompt #6 โ Takeoff + landing ramps on water hazards
Young players struggle to time jumps over flat-ground water holes. This prompt makes water hazards naturally readable: a hill rises before the water (telegraphing the jump and giving extra airtime), and a hill descends after it (lengthening the safe landing zone). Same gameplay, dramatically more forgiving for ages 4โ7.
Currently the water hazards are carved out of flat terrain โ kids have trouble judging when to jump. Modify the terrain so each water hole has a natural ramp UP on the left edge (takeoff) and a slope DOWN on the right edge (landing). The middle 50% of the obstacle width is the actual void/water. Make the takeoff ramp rise to about 70px above baseline, then a clear void, then a matching descending ramp. Update the water rendering to sample the terrain at the void edges (not the obstacle edges) so the canyon walls connect to the ramp tops. Also boost the kid-mode jump impulse by 30% so late presses still clear the hazard.
โถ See what the AI does with this prompt
- Modifies
terrain.addWaterHole()sampler โ left 25% is aMath.pow(t, 1.4) * 70lift curve, middle 50% returns void (9999), right 25% is a mirror descent - Increases water obstacle width from 140 โ 200 px to accommodate the ramps
- Updates
drawWater()to sampleterrain.groundY(o.x + w * 0.25)instead of obstacle edges for canyon-wall positioning - In kid mode,
this.player.applyJump(1.3)instead of1.0โ 30% more vy = ~30% more air time = ~30% more horizontal reach
โธ Advanced Prompt #7 โ Pause button + flashing kid-friendly play overlay
A pause feature designed for young children: a clearly labeled pause icon, a big bouncing green play button in the center when paused, and a personalized voice prompt using their name.
Add a pause button in the top corner next to the sound icon. Clicking it should pause the game, darken the screen, and show a big fun kids-style play button (โถ) in the center that pulses/flashes. Use the same kid voice as the 3-2-1 countdown to speak dynamically: "{name}, Press Play when ready!" โ substituting the entered player name. Click the play button (or press P) to resume. While paused, freeze physics and quiet the engine sound, but leave the music playing.
โถ See what the AI does with this prompt
- Adds
#pause-btnto the HUD button group with a โธ icon - Adds
#pause-overlaywith a circular gradient-green play button using thepause-pulsekeyframe (scale 1 โ 1.08 + glow expansion) - New game state
'paused'โ the main loop skipsupdate()+render()while paused, freezing the canvas - On pause:
speechSynthesis.speak("{name}, Press Play when ready!")dynamically inserts the player's name - On resume: resets
this.lastTime = performance.now()so the next frame doesn't get a huge dt spike (which would cause physics to jump) - P key on keyboard also toggles pause
๐ Course complete
You've built, deployed, and installed a browser game. Earn your certificate or continue with another course.
๐ Get Certificate Browse all courses Practice projects