Phase 4 Β· Games
HU EN
Phase 4 Β· Lesson 1

🐸 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.

⏱ 60 min
πŸ“ spawnObjects
🎯 evenSpread · world-space spawn

1The problem: why not Math.random()?

🐸 Froggy Rush

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.

froggy_rush.html β€” spawnObjects()JavaScript
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;
}
The key formula: y = PLAYER_Y - S

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.

TypeS_startCount (Level 1)Why?
πŸ₯š Egg15024Player gets a reward early
πŸͺ¨ Rock25012A little preparation time needed
πŸ¦… Seagull3006The hardest hazard appears last
⚑ Boost5005Only in the second half of the level
✏️ Task

Open froggy_rush.html in VS Code:

  • Find the spawnObjects function (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?

Because the canvas Y axis is inverted
Because the object reaches the player when scrollY=S β€” and screen_y = o.y + scrollY = PLAYER_Y β†’ o.y = PLAYER_Y - S
Because objects move from top to bottom
Random β€” anything would work
Phase 4 Β· Lesson 2

🐸 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.

⏱ 45 min
🎯 SURFACES · strategy pattern

1The SURFACES object β€” strategy pattern

🐸 Froggy Rush
turtle_race.html β€” SURFACES objectJavaScript
// 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
}
Why is this better than many if-else statements?

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?

turtle_race.htmlJavaScript
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'
✏️ Task

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?

It controls the slipperiness visually when drawing
If > 0, vx is multiplied by this factor when the key is released β€” a high value means the turtle slides for a long time
It modifies the scroll speed
Visual effect only, no effect on movement
Phase 4 Β· Lesson 3

🐸 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.

⏱ 45 min
🎯 jumpT · jumpH · sin() parabolic arc

1The complete jump code

🐸 Froggy Rush
froggy_rush.html β€” updatePlayer()JavaScript
// 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);
Why 22 frames?

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:

froggy_rush.html β€” checkCollisions()JavaScript
// 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);
}
✏️ Task
  • 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?

Because sin() is faster than other calculations
Because sin(0)=0, sin(Ο€/2)=1, sin(Ο€)=0 β€” it describes exactly the arc we want to see during a jump: starts, reaches the peak, returns
Because the canvas only understands sin()
No difference, anything else would work too
Phase 4 Β· Lesson 4

🐸 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.

⏱ 60 min
🎯 phase · state machine · phTimer

1States and transitions

🐸 Froggy Rush
froggy_rush.html β€” statesJavaScript
// 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
    }
  }
}
Why is a State Machine useful?

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.

✏️ Task
  • Find all occurrences of G.phase in 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?

It measures how many points the player loses
It counts down: stays in the dead state for N frames, then automatically switches to play (if lives remain)
It controls the speed of the death animation
Only used in froggy rush, not elsewhere
Phase 4 Β· Lesson 5

🐸 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.

⏱ 45 min
🎯 DIFF_CFG · configuration object

1The DIFF_CFG configuration object

🐸 Froggy Rush
froggy_rush.htmlJavaScript
// 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
}
The advantage of a config object

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.

✏️ Task
  • 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?

1.6
1.84
1.984  (1.6 Γ— (1 + 3Γ—0.08) = 1.6 Γ— 1.24)
2.24
Phase 4 Β· Lesson 6

🐒 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.

⏱ 75 min
🎯 AI targeting · aiTimer · aiErr

1AI decision making

🐒 Turtle Race
turtle_race.html β€” updateAI()JavaScript
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 trick

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

turtle_race.htmlJavaScript
// 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;
    }
  });
}
✏️ Task
  • Find the updateAI function β€” 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?

Because it would slow down the game
Because human decision-making isn't instant either β€” the delayed decision feels more natural, and the random interval makes it less predictable
Technical limitation: the canvas can't handle it
The AI always decides, it just doesn't always act
Phase 4 Β· Lesson 7

🐒 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.

⏱ 45 min
🎯 drainMap · fillMap · resource management

1Energy drain and recharge logic

🐒 Turtle Race
turtle_race.html β€” updatePlayer()JavaScript
// 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
  }
}
Why is this interesting from a game design perspective?

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.

✏️ Task
  • 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?

Energy rises to 140
Nothing, the pack disappears but has no effect
They receive bonus points (25 Γ— level) but energy stays at 100
They gain an extra life
Phase 4 Β· Lesson 8

🐒 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.

⏱ 45 min
🎯 screen_y = o.y + scrollY

1The complete coordinate system

🐒 Turtle Race
turtle_race.htmlJavaScript
// 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);
The most common mistake

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.

✏️ Task
  • 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].y and G.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?

Y = -800 (above the screen)
screen_y = -800 + 600 = -200 (just above the screen, entering soon)
Y = 800 - 600 = 200
Cannot be calculated without scrollY
Phase 4 Β· Lesson 9

🐒 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.

⏱ 75 min
🎯 Layered drawing · save/translate/restore

1Structure of drawTurtle() in layers

🐒 Turtle Race
turtle_race.html β€” drawTurtle()JavaScript
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

turtle_race.html β€” drawShell()JavaScript
// 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();
The flickering damage effect

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.

✏️ Task
  • Find the drawTurtle function β€” 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?

No difference, the order doesn't matter
Because the canvas builds up in layers: whatever is drawn later is on top. If we drew legs after the shell, the legs would appear outside the shell
Because the ellipse size means the shell doesn't cover everything
The legs are transparent so either order works
Phase 4 Β· Lesson 10

🏰 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.

⏱ 75 min
🎯 G state · initLevel · component structure

1The G game state object

🏰 Castle Siege
castle_siege.html β€” initLevel()JavaScript
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

SystemInputOutputFunction
Hero movementK object (keys)hero.x, hero.y changemoveHero()
EnemiesLevel data, wave numberEnemies move, damage the castlemoveEnemies()
TowersEnemy positionsProjectiles are spawnedtowerAI()
CollisionProjectiles + enemiesHP reduction, gold earnedcheckCollisions()
castle_siege.html β€” update()JavaScript
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?
}
✏️ Task
  • 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?

Because global variables are faster
Because it's easily saved/loaded (JSON.stringify(G)), has a clear structure, and is simple to debug from the console
Because JavaScript only handles one object at a time
For performance reasons
Phase 4 Β· Lesson 11

🏰 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.

⏱ 60 min
🎯 genLevel · procedural generation · scaling

1The logic of genLevel()

🏰 Castle Siege
castle_siege.html β€” genLevel()JavaScript
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 };
}
The advantage of procedural generation

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.

✏️ Task
  • Find the genLevel function β€” 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))

8
9  (3 + floor(10/2) + floor(2/2) = 3 + 5 + 1 = 9)
10
7
Phase 4 Β· Lesson 12

🏰 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.

⏱ 75 min
🎯 towerAI · targeting · projectile movement

1Tower AI logic

🏰 Castle Siege
castle_siege.html β€” towerAI()JavaScript
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 β€” calculating the angle between two points

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.

✏️ Task
  • 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?

It calculates the distance
It gives the angle between the tower and the target in radians, from which the projectile's vx and vy speed can be calculated
It returns the enemy's speed
It only works for circles
Phase 4 Β· Lesson 13

🏰 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.

⏱ 60 min
🎯 lead prediction · travel time · target prediction

1The mathematics of predictive targeting

🏰 Castle Siege
castle_siege.html β€” predictiveCelzas()JavaScript
// 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,
});
When is it worth using?

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.

✏️ Task
  • 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?

Because the projectile also gets faster
Because basic targeting shoots at the current position, but by the time the projectile arrives the enemy has moved β€” prediction aims at the future position
It requires fewer calculations
Only works for stationary enemies
Phase 4 Β· Lesson 14

🏰 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.

⏱ 45 min
🎯 localStorage · JSON · Top 10 list

1Saving and loading with localStorage

🏰 Castle Siege
castle_siege.html β€” save systemJavaScript
// 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)));
}
localStorage limitations

Only text can be stored (hence JSON.stringify). Max ~5MB per browser. Deleted in private mode. Works on Netlify too β€” no server needed.

✏️ Task
  • 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?

Because localStorage encrypts the data
Because localStorage only stores text β€” JavaScript objects must be converted to text with JSON.stringify, then converted back with JSON.parse
Because it makes storage faster
Not required, objects can be put in directly
Phase 4 Β· Lesson 15

πŸ† 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.

⏱ 5–8 hours
🎯 Independent design and development
πŸ“ Single HTML file

1Project requirements

Required elements

βœ“ 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:

IdeaCore mechanicExample from the course
🐍 Snake gameGrowing snake, food collectionArray management, collision
🌌 AsteroidRotating ship, projectiles, rocksTransforms, atan2 targeting
πŸ“ PongTwo paddles, bouncing ballAABB, AI opponent
🌧️ CollectorItems fall, catch themSpawn, scroll, collision
🐦 Flappy BirdGravity, obstaclesPhysics, state machine
🧱 BreakoutPaddle, ball, bricksAABB, array management
✏️ Project Checklist
  • 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
Bonus challenges

⭐ 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