πΈ Froggy Rush β Level Generation
How do we place eggs, rocks, and seagulls fairly across the whole level? The evenSpread() function solves this β not randomly, but evenly distributed.
1The problem: why not Math.random()?
If we place objects completely randomly, we might end up with 20 eggs in the first 100 metres and nothing in the last 400. The evenSpread() function solves this.
function spawnObjects(level) { const objs = []; const span = LEVEL_DIST * 8; // total level width in px // evenSpread: distribute objects evenly across the level function evenSpread(count, S_start, typeObj) { const S_end = span - 200; // last 200px = finish area const gap = (S_end - S_start) / (count - 1); for (let i = 0; i < count; i++) { // Small randomness within the gap (not perfectly even) const S = S_start + i*gap + (Math.random()-.5)*gap*.5; // World Y: reached by the player when screen_y = o.y + scrollY = PLAYER_Y const y = PLAYER_Y - S; objs.push({ ...typeObj, y }); } } evenSpread(20+level*4, 150, { type:'egg', r:9 }); evenSpread(lv.rocks*3, 250, { type:'rock', r:13 }); evenSpread(lv.gulls*3, 300, { type:'gull', r:15 }); // X position: random (within the level width) objs.forEach(o => { o.x = TL + 14 + Math.random()*(TW-28); }); return objs; }
An object reaches the player when o.y + scrollY = PLAYER_Y. So if we want the player to reach it when scrollY=S: o.y = PLAYER_Y - S. This is the essence of world-space spawning.
Why the S_start parameter?
Different obstacles appear at different distances β eggs come earlier (after 150px), rocks a little later (250px), seagulls even later (300px). This way the beginning is easier and the end is harder.
| Type | S_start | Count (Level 1) | Why? |
|---|---|---|---|
| π₯ Egg | 150 | 24 | Player gets a reward early |
| πͺ¨ Rock | 250 | 12 | A little preparation time needed |
| π¦ Seagull | 300 | 6 | The hardest hazard appears last |
| β‘ Boost | 500 | 5 | Only in the second half of the level |
Open froggy_rush.html in VS Code:
- Find the
spawnObjectsfunction (Ctrl+F) - Observe the evenSpread calls β how many eggs appear on level 2?
- Change the egg S_start from 150 to 50 β what changes? (first egg comes sooner)
- Add another evenSpread call: 3 "energy" items with S_start of 400
π§ Why is y = PLAYER_Y - S the spawn formula?
πΈ Froggy Rush β Surfaces and Speed
The SURFACES object holds everything about grass, sand, ice, mud, and road surfaces β from their speed to their slipperiness. All in one place, easy to modify.
1The SURFACES object β strategy pattern
// All surface properties in one place const SURFACES = { grass: { name:'πΏ Grass', col:'#4aaa22', spd:1.00, slip:0.0 }, sand: { name:'ποΈ Sand', col:'#e8c84a', spd:0.58, slip:0.0 }, ice: { name:'βοΈ Ice', col:'#b8e8ff', spd:1.22, slip:0.88 }, mud: { name:'π§ Mud', col:'#8B5a30', spd:0.40, slip:0.0 }, road: { name:'π€οΈ Road', col:'#777', spd:1.38, slip:0.0 }, }; // Usage: speed depends on the surface const surf = SURFACES[getSurface(dist, level)]; const scrollSpd = BASE_SPD * surf.spd * lv.spdMul; // Slipping on ice: vx doesn't slow down quickly if (!K.left && !K.right) { p.vx *= (surf.slip > 0 ? surf.slip : 0.74); // On ice slip=0.88 β slows slowly (slides) // On grass slip=0 β multiplied by 0.74, stops quickly }
If there were separate if (surface === 'ice') spd *= 1.22 etc. statements, 5 surfaces would need 10+ conditions. With the SURFACES object you only change the data β the logic stays the same. This is called the strategy pattern.
getSurface() β which surface are we on?
function getSurface(dist, level) { const lv = LEVELS[level - 1]; const segH = LEVEL_DIST / lv.segs.length; // segment length const idx = Math.min( Math.floor(dist / segH), lv.segs.length - 1 ); return lv.segs[idx]; // e.g. 'mud' } // e.g. level 2 = ['grass','mud','grass','mud','grass'] // dist=180m β segment=120m β idx=1 β 'mud'
Experiment with surfaces:
- Open
turtle_race.html, find the SURFACES object - Change the mud speed from 0.40 to 0.10 β what effect does it have?
- Add a new surface:
'lava': { spd: 1.8, slip: 0.0 } - Add a "lava" segment to level 5 (Volcano) array
π§ What is the role of the slip property in SURFACES?
πΈ Froggy Rush β Jump Physics
The frog's parabolic jump is implemented using the sin() function. It rises and returns in exactly 22 frames β giving a perfect arc without complex physics calculations.
1The complete jump code
// 1. START jump β when key is pressed if (K.up && !p.jumping && !p.inShell) { p.jumping = true; p.jumpT = 0; // reset timer to zero } // 2. ANIMATE jump β every frame if (p.jumping) { p.jumpT++; // sin(0)=0 β sin(Ο/2)=1 (peak) β sin(Ο)=0 (back down) p.jumpH = Math.sin(p.jumpT / 22 * Math.PI) * 38; // β returns in 22 frames β 38px high if (p.jumpT >= 22) { // 22 frames elapsed p.jumping = false; p.jumpH = 0; } } // 3. When drawing: frog is raised by jumpH ctx.translate(p.x, PLAYER_Y - p.jumpH); // 4. For collision: frog is "higher" while jumping const frogScreenY = PLAYER_Y - p.jumpH; const d = Math.hypot(p.x - rock.x, frogScreenY - sy);
22 frames β 0.37 seconds at 60fps. This is long enough to look like a nice arc visually, but short enough not to be frustrating. It can be changed: a larger number = slower jump, smaller = faster.
The effect of jumping on collision
For rocks, collision is only active when NOT jumping. For seagulls however, jumping doesn't protect β only the shell does:
// Rock: !p.jumping required β avoidable by jumping if (o.type === 'rock' && !p.jumping && pd < o.r+11) { loseLife(p); } // Seagull: !p.inShell required β only avoidable with shell if (o.type === 'gull' && !p.inShell && pd < o.r+12) { loseLife(p); }
- Find the jumpH and jumpT variables in froggy_rush.html
- Change 22 to 40 β how does the jump change?
- Change 38 to 70 β what is the result?
- Bonus: add a double jump β if jumping and the key is pressed again, jump once more
π§ Why is sin() ideal for a parabolic jump?
πΈ Froggy Rush β State Machine
The game isn't always "in play" β sometimes the frog is dead, sometimes levelling up, sometimes it's game over. The state machine manages what can happen when and what cannot.
1States and transitions
// Possible values of G.phase: // 'play' β normal gameplay in progress // 'dead' β frog is dead, animation plays // 'levelup' β level completed, transition // 'gameover'β no more lives // In update(): only the relevant state runs function update() { G.frame++; if (G.phase === 'play') updatePlay(); if (G.phase === 'dead') updateDead(); if (G.phase === 'levelup') updateLevelUp(); } // Death β state transition function dieFrog() { if (G.frog.dead) return; // already dead β don't run twice G.frog.dead = true; G.phase = 'dead'; // switch to dead state G.phTimer = 80; // stays in dead state for 80 frames G.lives--; } // Dead state: counts down then resets function updateDead() { G.phTimer--; if (G.phTimer <= 0) { if (G.lives <= 0) { G.phase = 'gameover'; } else { G.phase = 'play'; // return to playing resetFrog(); // frog back to start position } } }
Without it, the code would be full of conditions like: if (!dead && !levelup && !gameover) everywhere. With a state machine, all conditions are in one place, and update() automatically runs the right logic.
- Find all occurrences of
G.phasein froggy_rush.html (Ctrl+F) - Draw a state diagram on paper: circles for states, arrows for transitions
- Add a new "pause" state: stops on spacebar, continues on spacebar again
- Bonus: write your own mini state machine: traffic light (redβyellowβgreenβred)
π§ What is the role of phTimer in the dead state?
πΈ Froggy Rush β Difficulty Levels
Kid, Slow, Normal, Fast β with a single DIFF variable the entire game's behaviour changes. The DIFF_CFG object holds all the differences.
1The DIFF_CFG configuration object
// Difficulty level configuration const DIFF_CFG = [ { name:'π Kid', carSpd:0.45, logSpd:0.45, logGap:0.7 }, { name:'π’ Slow', carSpd:0.75, logSpd:0.75, logGap:0.85 }, { name:'βοΈ Normal', carSpd:1.0, logSpd:1.0, logGap:1.0 }, { name:'π Fast', carSpd:1.6, logSpd:1.5, logGap:1.2 }, ]; // Index of selected difficulty (0-3) let DIFF = 2; // Normal is the default // Usage β just read from the config: function makeCars(level, diff) { const dc = DIFF_CFG[diff]; const spdMul = dc.carSpd * (1 + level * .08); // spdMul = base difficulty Γ level multiplier // Kid, level 1: 0.45 Γ 1.0 = 0.45 // Fast, level 5: 1.6 Γ 1.4 = 2.24 }
If every difficulty had separate if-else statements, you'd need to modify 20+ places in the code when you want to make a change. With DIFF_CFG, you change any value for any difficulty in a single line and it applies throughout the whole game.
- Find DIFF_CFG in froggy_rush.html β how many places reference it?
- Add a 5th difficulty level:
'π₯ Impossible'β carSpd: 2.5, logSpd: 2.2 - Add a 5th button among the buttons and link it to DIFF = 4
- Observe what changes in the game β is the change proportional?
π§ If carSpd = 1.6 and level = 3, what will the spdMul value be?
π’ Turtle Race β AI Opponent
The AI turtle avoids seagulls, collects eggs, and isn't always perfect β a little randomness makes it feel more human to the player.
1AI decision making
function updateAI() { // Doesn't decide every frame β only every 25-65 frames a.aiTimer--; if (a.aiTimer <= 0) { a.aiTimer = 25 + Math.floor(Math.random() * 40); let best = W/2, bestSc = -Infinity; // Evaluates every object G.objects.forEach(o => { if (o.done) return; const sy = o.y + G.scrollY; if (sy < AI_Y+100 && sy > AI_Y-60) { if (o.type === 'egg' || o.type === 'boost') { const sc = 90 - Math.abs(o.x - a.x); // closer = better if (sc > bestSc) { bestSc = sc; best = o.x; } } if (o.type === 'gull') { if (Math.abs(o.x - a.x) < 55) best = a.x + (a.x < W/2 ? 55 : -55); } } }); // aiErr: small error β AI is not perfect a.aiTargetX = best + a.aiErr * TW * .4; } // Moves smoothly towards the target const diff = a.aiTargetX - a.x; a.vx += Math.sign(diff) * Math.min(Math.abs(diff)*.09, .85); }
The aiErr = (Math.random()-.5) * .3 value is assigned once to the AI and never changes. This means the AI systematically "drifts" slightly left or right β it feels more human than making a random error every frame.
AI jumping β reacts when a rock is ahead
// AI automatically jumps if a rock is close ahead if (!a.jumping) { G.objects.forEach(o => { if (o.type !== 'rock' || o.done) return; const sy = o.y + G.scrollY; if (sy > AI_Y-60 && sy < AI_Y+20 && Math.abs(o.x - a.x) < 35) { a.jumping = true; a.jumpT = 0; } }); }
- Find the
updateAIfunction β observe the aiTimer value - Make the AI "smarter": reduce the aiTimer minimum from 25 to 5
- Make it "dumber": increase aiErr from 0.3 to 1.0
- Bonus: implement a simple AI: a rectangle that smoothly follows the mouse X position
π§ Why doesn't the AI decide every frame?
π’ Turtle Race β Energy System
Energy is the game's secondary resource β it drains fast in mud, recharges on road, and a water pack fills it back up. If it runs out, a life is lost. This forces the player to make decisions.
1Energy drain and recharge logic
// Drain/fill rate per surface (per frame) const drainMap = { grass:0.04, sand:0.08, ice:0.05, mud:0.08, road:0 }; const fillMap = { grass:0, sand:0, ice:0, mud:0, road:0.06 }; // Which surface are we on? const surfName = getSurface(G.totalDist, G.level); // Energy update: drain - fill, clamped between 0 and 100 p.energy = Math.max(0, Math.min(100, p.energy - drainMap[surfName] + fillMap[surfName] )); // If it runs out β lose a life, energy resets to 100 if (p.energy <= 0) { p.energy = 100; loseLife(p); addFloat(p.x, PLAYER_Y-40, 'β‘ Energy depleted!', '#e03030'); } // Water pack pickup if (o.type === 'energy' && pd < o.r+13) { o.done = true; if (p.energy >= 100) { G.score += 25 * G.level; // full β bonus points } else { p.energy = Math.min(100, p.energy + 40); // recharges } }
Energy creates decision pressure: the player weighs whether to avoid mud (also slower due to energy!) or take the shorter route. The water pack can also give bonus points if energy is full β that's another decision.
- Find drainMap in turtle_race.html
- Change the mud drain value from 0.08 to 0.25 β level 2 becomes much harder
- Add energy recharge on sand too: fillMap.sand = 0.03
- Bonus: draw an HP bar on canvas showing the energy value (with gradient: greenβyellowβred)
π§ What happens if the player's energy = 100 and they pick up a water pack?
π’ Turtle Race β World-space Coordinates
Eggs, rocks, and seagulls are fixed in place on the level β the camera moves, not them. This is the key distinction between world-space and screen-space.
1The complete coordinate system
// SPAWN: calculating object world Y // Reaches the player when scrollY = S // screen_y = o.y + scrollY = PLAYER_Y // β o.y = PLAYER_Y - S const y = PLAYER_Y - S; // negative number (above the screen) // UPDATE: only scrollY grows β objects do NOT move! G.scrollY += scrollSpd; // this is the "progress" G.totalDist = G.scrollY / 8; // converted to metres // DRAW: calculating screen position G.objects.forEach(o => { const sy = o.y + G.scrollY; // screen_y if (sy < -40 || sy > H+40) return; // off screen: skip drawEgg(o.x, sy); // drawn at screen coordinates }); // COLLISION: screen_y must be compared const d = Math.hypot(p.x - o.x, PLAYER_Y - sy);
If you also write o.y += scrollSpd when moving objects, they move twice β once from scrollY and once by themselves. We actually made this mistake in an earlier version! Objects are fixed in world-space, only scrollY changes.
- Press F12 in turtle_race.html, type in the Console:
G.scrollYβ what is the current value? - Print the world and screen position of the first egg:
G.objects[0].yandG.objects[0].y + G.scrollY - Watch how scrollY grows and screen_y changes β which one reaches PLAYER_Y?
π§ If an egg's world Y is -800 and scrollY = 600, where does it appear on screen?
π’ Turtle Race β Drawing Characters in Layers
The turtle isn't an image β it's built from lines, ellipses, and circles, layer by layer. The order matters: first the shadow, then the legs, then the shell, finally the head.
1Structure of drawTurtle() in layers
function drawTurtle(x, y, t, frame) { if (t.stunned > 0 && Math.floor(frame/5) % 2) return; // flicker const jy = t.jumpH || 0; ctx.save(); ctx.translate(x, y - jy); // origin at turtle's centre ctx.rotate(tilt); // lean based on vx // LAYER 1: shadow (always below) ctx.fillStyle = 'rgba(0,0,0,.18)'; ctx.ellipse(2, 18, 14, 4, 0, 0, Math.PI*2); // LAYER 2: legs (go under the shell) drawLegs(walk); // LAYER 3: shell (above the legs) drawShell(t.inShell); // LAYER 4: head (above the shell) if (!t.inShell) drawHead(walk); ctx.restore(); // LAYER 5: label (separate save/restore) ctx.save(); ctx.translate(x, y - jy); ctx.fillText(t.isAI ? 'π€ AI' : 'π’ You', 0, -28); ctx.restore(); }
Drawing the shell β with radial gradient
// Shell: radial gradient + hex pattern const g2 = ctx.createRadialGradient(-3,-4,2, 0,1,14); g2.addColorStop(0, '#3a9a28'); // light inside g2.addColorStop(1, '#1a5a10'); // dark outside ctx.fillStyle = g2; ctx.beginPath(); ctx.ellipse(0, 1, 14, 11, 0, 0, Math.PI*2); ctx.fill(); // Hex pattern with lines ctx.strokeStyle = 'rgba(0,0,0,.22)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.ellipse(0,1,8,6,0,0,Math.PI*2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,-8); ctx.lineTo(0,10); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-14,1); ctx.lineTo(14,1); ctx.stroke();
if (t.stunned > 0 && Math.floor(frame/5) % 2) return; β if stunned and frame/5 is odd, we don't draw the turtle. Every 5th frame it alternates visible/invisible = flicker.
- Find the
drawTurtlefunction β count how many ctx.save/restore pairs are in it - Draw your own character in layers: shadow β body β clothing β head β eyes in that order
- Add flickering: after taking damage, flicker for 60 frames
- Bonus: change the AI turtle's colour to red (different col variable based on isAI)
π§ Why do we draw the legs BEFORE the shell?
π° Castle Siege β Tower Defense Architecture
Castle Siege is our most complex game. The G object holds all game elements β hero, castle, towers, enemies, projectiles, gold. Everything in one place.
1The G game state object
function initLevel(lvNum) { G = { // Game elements hero: { x:155, y:300, hp:100, maxHp:100, spd:2, vx:0, vy:0 }, castle: { hp:200, maxHp:200, x:0, w:80 }, towers: [], // array of built towers enemies: [], // active enemies hProj: [], // hero's projectiles tProj: [], // tower projectiles floats: [], // floating text elements // Resources gold: 50, score: 0, // Level state level: lvNum, wave: 0, phase: 'play', frame: 0, }; }
The 4 main systems and their relationships
| System | Input | Output | Function |
|---|---|---|---|
| Hero movement | K object (keys) | hero.x, hero.y change | moveHero() |
| Enemies | Level data, wave number | Enemies move, damage the castle | moveEnemies() |
| Towers | Enemy positions | Projectiles are spawned | towerAI() |
| Collision | Projectiles + enemies | HP reduction, gold earned | checkCollisions() |
function update() { if (!G || G.phase !== 'play') return; G.frame++; // Runs in sequence every frame: moveHero(G.hero); // 1. Hero moves spawnWave(); // 2. New enemies arriving? moveEnemies(); // 3. Enemies move towerAI(); // 4. Towers shoot moveProjectiles(); // 5. Projectiles move checkCollisions(); // 6. Check collisions checkWinLose(); // 7. Won/lost? }
- Open castle_siege.html F12 β Console, type:
G.heroβ can you see the hero's data? - Try:
G.castle.hp = 5β what happens? - Try:
G.gold = 9999β can you build the most towers? - Find the update() function β in what order does it call its parts?
π§ Why is it good that all game data is in the G object?
π° Castle Siege β Dynamic Level Generation
50 different levels from a single genLevel() function. Based on the level number, it calculates the number of enemies, their strength, and the gold reward using maths.
1The logic of genLevel()
function genLevel(lvNum) { // Number of waves: grows from 5 to 10 const waves = 5 + Math.floor(lvNum / 5); // Enemy types β more and stronger at higher levels const ENEMY_TYPES = [ { hp: 20+lvNum*4, spd: 0.8+lvNum*0.02, gold: 5, col: '#c83030' }, { hp: 60+lvNum*8, spd: 0.5+lvNum*0.01, gold: 12, col: '#8030c8' }, { hp: 150+lvNum*15, spd: 0.35, gold: 25, col: '#303080' }, ]; // Generating waves const waveData = []; for (let w = 0; w < waves; w++) { // Enemy count: grows with level and wave const count = 3 + Math.floor(lvNum/2) + Math.floor(w/2); // Type: stronger types can appear in higher waves const maxType = Math.min(2, Math.floor(lvNum/10) + Math.floor(w/3)); waveData.push({ count, maxType }); } return { waves, waveData, ENEMY_TYPES }; }
If you had to define all 50 levels manually, it would be a huge amount of work and hard to modify. With maths-based generation, changing a single formula automatically makes all levels harder.
- Find the
genLevelfunction β how many enemies appear in wave 1 of level 1? - Calculate: how many enemies are there in wave 1 of level 50?
- Modify: from level 10, a 4th enemy type (boss) appears: HP = lvNum*30, gold = 50
- Bonus: visualise on a canvas graph how enemy HP scales from levels 1β50
π§ How many enemies are in wave 3 of level 10? (count = 3 + floor(lvNum/2) + floor(w/2))
π° Castle Siege β Tower AI and Projectiles
Every tower finds the nearest enemy within its range, takes aim, and fires a projectile. The projectile predicts where to shoot β not where the enemy is, but where it will be.
1Tower AI logic
function towerAI() { G.towers.forEach(tower => { // Cooldown decreases if (tower.cooldown > 0) { tower.cooldown -= DT; return; } // Find nearest enemy within range let best = null, bestDist = tower.range; G.enemies.forEach(e => { const d = Math.hypot(e.x - tower.x, e.y - tower.y); if (d < bestDist) { bestDist = d; best = e; } }); if (!best) return; // no target // Calculate projectile direction const angle = Math.atan2(best.y - tower.y, best.x - tower.x); G.tProj.push({ x: tower.x, y: tower.y, vx: Math.cos(angle) * tower.projSpd, vy: Math.sin(angle) * tower.projSpd, dmg: tower.dmg, life: 120 }); tower.cooldown = tower.fireRate; }); }
Math.atan2(dy, dx) gives the angle between two points in radians. Then Math.cos(angle) and Math.sin(angle) give the vx and vy velocity components β the projectile heads exactly toward the target.
- Find the towerAI function in castle_siege.html
- Notice: if you set tower.range to a small value (e.g. 50), the tower doesn't shoot β why?
- Add a "splash damage" tower: when a projectile hits, all enemies within 50px take 30% damage
- Bonus: draw a range circle around the tower (ctx.arc with tower.range radius, low opacity)
π§ What is Math.atan2(dy, dx) used for when targeting?
π° Castle Siege β Projectile Prediction
Basic targeting shoots at the enemy's current position β but by the time the projectile arrives, the enemy has moved elsewhere. Predictive targeting solves this.
1The mathematics of predictive targeting
// BASIC targeting: shoots WHERE THE ENEMY IS const angle = Math.atan2(e.y - t.y, e.x - t.x); // PREDICTIVE targeting: shoots WHERE THE ENEMY WILL BE // 1. How long does it take the projectile to reach the enemy? const dist = Math.hypot(e.x - t.x, e.y - t.y); const travelTime = dist / t.projSpd; // in frames // 2. Where will the enemy be in travelTime frames? const predX = e.x + e.vx * travelTime; const predY = e.y + e.vy * travelTime; // 3. Aim at the predicted position const angle = Math.atan2(predY - t.y, predX - t.x); // Fire the projectile at the predicted angle: G.tProj.push({ vx: Math.cos(angle) * t.projSpd, vy: Math.sin(angle) * t.projSpd, });
If an enemy moves fast and the projectile is slow, basic targeting almost never hits. Predictive targeting is much more accurate β but it also makes the AI more predictable. In Castle Siege, the cannon tower uses this.
- From the console, observe:
G.enemies[0].vxβ how fast is the enemy moving? - Calculate on paper: if an enemy is at x=300, vx=1 and the projectile takes 60 frames to arrive, where do we aim?
- Add a console.log to towerAI to log when the tower fires
- Bonus: implement predictive targeting in a simple mini projectile game
π§ Why is predictive targeting better when an enemy moves fast?
π° Castle Siege β Save System and Leaderboard
Using localStorage, the game saves progress and the Top 10 results. JSON.stringify and JSON.parse convert objects to text and back.
1Saving and loading with localStorage
// SAVE: object β JSON text β localStorage function saveGame(name) { const saveData = { level: G.level, score: G.score, heroSpd: G.hero.spd, gold: G.gold }; localStorage.setItem('cs_save_'+name, JSON.stringify(saveData)); } // LOAD: localStorage β JSON text β object function loadGame(name) { const raw = localStorage.getItem('cs_save_'+name); if (!raw) return null; return JSON.parse(raw); } // Top 10 list management function saveTop(name, score, level) { let top = getTop(); top.push({ name, score, level, date: new Date().toLocaleDateString('en') }); top.sort((a, b) => b.score - a.score); // descending order localStorage.setItem('cs_top10', JSON.stringify(top.slice(0, 10))); }
Only text can be stored (hence JSON.stringify). Max ~5MB per browser. Deleted in private mode. Works on Netlify too β no server needed.
- F12 β Application tab β Local Storage β can you see the Castle Siege saves?
- In Console:
localStorage.getItem('cs_top10')β what is the content? - Clear it and see what happens:
localStorage.clear() - Bonus: write your own save/load system for turtle_race β save the level and score
π§ Why do we need JSON.stringify before saving to localStorage?
π Project Wrap-up β Your Own Mini Game
The closing of Phase 4: a mini game independently designed by you. Based on what you've learned from the three games β but following your own ideas.
1Project requirements
β Canvas-based, requestAnimationFrame loop
β At least 2 game elements (player + something to interact with)
β Keyboard or touch controls
β Score calculation and display (ctx.fillText)
β Game Over state (state machine!)
β Sin()-based animation on at least one element
β Collision detection (Math.hypot or AABB)
Ideas β pick one or combine:
| Idea | Core mechanic | Example from the course |
|---|---|---|
| π Snake game | Growing snake, food collection | Array management, collision |
| π Asteroid | Rotating ship, projectiles, rocks | Transforms, atan2 targeting |
| π Pong | Two paddles, bouncing ball | AABB, AI opponent |
| π§οΈ Collector | Items fall, catch them | Spawn, scroll, collision |
| π¦ Flappy Bird | Gravity, obstacles | Physics, state machine |
| π§± Breakout | Paddle, ball, bricks | AABB, array management |
- Design phase: sketch a game screen on paper and list the elements
- Structure: G object, update(), draw(), loop() kept separate
- State machine: at least play and gameover states
- Character drawn with save/translate/restore
- Sin() animation: at least one animated element
- Spawn system: at least 5 objects appear on the level
- Top 10 leaderboard in localStorage
- Mobile D-pad buttons (keyboard-only is fine too)
- Everything packed into a single HTML file
- Uploaded to Netlify and tested on mobile
β Random level generation (based on evenSpread pattern)
ββ Difficulty levels (based on DIFF_CFG pattern)
ββ Predictive targeting for a tower / enemy
βββ Music embedded as base64, Android-compatible audio unlock
βββ Save and load with JSON, name-identified save slots