Phase 5 ยท Advanced
HU EN
Phase 5 ยท Lesson 46

๐Ÿ’พ localStorage โ€” Saving in the Browser

The browser can persistently store data โ€” without a user database, server, or internet connection. Castle Siege and Galactic Conquest use this to save level progress and the leaderboard.

โฑ 60 min
๐Ÿ“ localStorage API
๐ŸŽฏ JSON.stringify ยท JSON.parse ยท setItem ยท getItem

1What is localStorage?

๐ŸŒ Browser API

localStorage is a key-value store that lives in the browser and persists after closing the tab. It's like a mini database, accessible from JavaScript โ€” with no server required.

LocationPersists after closing?SizeGood for?
let x = 5โŒ NounlimitedRuntime data
sessionStorageโŒ No (deleted when tab closes)~5 MBTemporary session data
localStorageโœ… Yes~5 MBSaves, settings, leaderboard
Server databaseโœ… YesunlimitedMultiple users, synchronisation

The 5 core methods

localStorage โ€” the 5 core methodsJavaScript
// SAVE โ€” stores text as a key-value pair
localStorage.setItem('player_name', 'Alex');

// READ โ€” returns the value (or null if it doesn't exist)
const name = localStorage.getItem('player_name'); // 'Alex'

// DELETE โ€” removes one key
localStorage.removeItem('player_name');

// DELETE ALL โ€” dangerous! removes everything
localStorage.clear();

// COUNT KEYS
const count = localStorage.length; // how many keys are saved
Important: only stores text!

localStorage always expects and returns strings. Numbers and objects must be converted to JSON. That's why we need JSON.stringify() and JSON.parse().

2JSON โ€” saving objects

The game state is a complex object: level number, gold, towers, settings. It needs to be saved as a single string โ€” that's what JSON does.

castle_siege.html โ€” save and loadJavaScript
// โ”€โ”€โ”€ SAVE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function saveGame() {
  const saved = {
    level:     G.level,      // current level
    gold:      G.gold,       // gold
    highscore: G.highscore   // best score
  };
  // Object โ†’ string (JSON)
  const text = JSON.stringify(saved);
  // String โ†’ localStorage
  localStorage.setItem('castle_siege_save', text);
  console.log('Saved!', text);
  // e.g.: '{"level":7,"gold":340,"highscore":12500}'
}

// โ”€โ”€โ”€ LOAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function loadGame() {
  const text = localStorage.getItem('castle_siege_save');
  // If no saved data, we get null
  if (!text) {
    console.log('No saved game.');
    return;
  }
  // String โ†’ Object (JSON)
  const saved = JSON.parse(text);
  G.level     = saved.level;
  G.gold      = saved.gold;
  G.highscore = saved.highscore;
  console.log('Loaded! Level: ' + G.level);
}
JSON.stringify vs JSON.parse โ€” opposites of each other

stringify(obj) โ†’ turns an object into a string. parse(str) โ†’ turns a string back into an object. If you saved with stringify, you must read with parse.

Safe loading โ€” always use try-catch

Error-safe loadingJavaScript
function loadGame() {
  try {
    const str = localStorage.getItem('castle_siege_save');
    if (!str) return false; // no save
    const d = JSON.parse(str);
    // Validate: is the saved data valid?
    if (typeof d.level !== 'number') return false;
    G.level = d.level;
    G.gold  = d.gold || 100; // default value if missing
    return true;
  } catch(e) {
    // Corrupted JSON won't crash the game
    console.warn('Load error:', e);
    localStorage.removeItem('castle_siege_save');
    return false;
  }
}
Why do we need try-catch?

If the string in localStorage is corrupted (e.g. interrupted save, data overwritten by another page), JSON.parse() throws an exception and the game crashes. try-catch catches the error and handles it safely.

3Auto-save

๐Ÿฐ Castle Siege
castle_siege.html โ€” autosaveJavaScript
// Automatically saves on every level change
function nextLevel() {
  G.level++;
  G.gold += 50;        // reward gold
  saveGame();          // โ† autosave after every level
  genLevel(G.level);   // level generation
}

// Load on game start
window.addEventListener('load', () => {
  const ok = loadGame();
  if (ok) {
    showMessage('Welcome back! You are on level ' + G.level + '.');
  }
  genLevel(G.level);
});
โœ๏ธ Task

Open castle_siege.html in VS Code and find the localStorage calls:

  • Press F12 in the browser, then Application โ†’ Local Storage โ€” can you see the saved data?
  • Play through 2 levels, then refresh the page โ€” does it return to your level?
  • Write a clearSave() function that deletes the save and resets to level 1
  • Attach it to a "New Game" button

๐Ÿง  What does localStorage.getItem('key') return if the key doesn't exist?

Empty string ("")
0
null
undefined
Phase 5 ยท Lesson 47

๐Ÿ† Implementing a Top 10 Leaderboard

A combination of array sorting, slice(), and localStorage โ€” this is how we store the best results list with names and scores. We learn from the Galactic Conquest leaderboard.

โฑ 45 min
๐ŸŽฏ sort() ยท slice() ยท array sorting

1The leaderboard data structure

๐Ÿš€ Galactic Conquest

The leaderboard is an array where each element is an object: {name, score, date}. We store the best 10 โ€” sorted in descending order by score.

galactic_conquest.html โ€” leaderboardJavaScript
// Load leaderboard from localStorage
function loadScores() {
  try {
    const str = localStorage.getItem('galactic_scores');
    return str ? JSON.parse(str) : [];
  } catch { return []; }
}

// Add a new result to the leaderboard
function addScore(name, score) {
  const scores = loadScores();

  // 1. Add the new result
  scores.push({
    name:  name,
    score: score,
    date:  new Date().toLocaleDateString('en-GB')
  });

  // 2. Sort: descending by score (b-a = larger first)
  scores.sort((a, b) => b.score - a.score);

  // 3. Keep only the top 10
  const top10 = scores.slice(0, 10);

  // 4. Save back
  localStorage.setItem('galactic_scores', JSON.stringify(top10));
  return top10;
}
sort() โ€” how does sorting work?

The array.sort((a, b) => b.score - a.score) compares two elements. If the result is negative, no swap occurs. If positive, they are swapped. The b - a formula always puts the larger value first (descending order).

slice() โ€” only the best

slice() and sort() comparedJavaScript
const scores = [300, 1200, 850, 75, 2100, 440];

// sort: sorts in-place, modifies the original array
scores.sort((a, b) => b - a);
// โ†’ [2100, 1200, 850, 440, 300, 75]

// slice(start, end): returns a copy, does NOT modify the original
const top3 = scores.slice(0, 3); // โ†’ [2100, 1200, 850]
// The original scores array is unchanged!

// Difference between splice and slice:
// splice โ†’ modifies the original (can also remove/insert elements)
// slice  โ†’ only returns a copy, doesn't touch the original

2Displaying the leaderboard on canvas

galactic_conquest.html โ€” drawing the leaderboardJavaScript
// Draw the leaderboard onto the canvas
function drawLeaderboard() {
  const scores = loadScores();
  const medals = ['๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰'];

  ctx.fillStyle = 'rgba(0,0,0,0.85)';
  ctx.fillRect(0, 0, W, H);    // dark background

  ctx.font = 'bold 28px Inter';
  ctx.fillStyle = '#ffe066';
  ctx.fillText('๐Ÿ† TOP 10', W/2, 60);

  scores.forEach((s, i) => {
    const y = 110 + i * 36;
    // Highlight the top three
    ctx.fillStyle = i < 3 ? '#fff' : '#8895aa';
    ctx.font = i < 3 ? 'bold 18px Inter' : '16px Inter';
    // Medal for top 3, number for the rest
    const prefix = i < 3 ? medals[i] : `${i+1}. `;
    ctx.fillText(`${prefix} ${s.name} โ€” ${s.score} pts`, 60, y);
    // Date on the right
    ctx.fillStyle = '#4a6272';
    ctx.font = '13px Inter';
    ctx.fillText(s.date, W - 110, y);
  });
}
โœ๏ธ Task

Build a simple leaderboard system:

  • Make an addScore('Test', 9999) call and check F12 โ†’ Application โ†’ Local Storage for the result
  • Call it 5 times with different names and scores โ€” what remains?
  • Modify it to store Top 5 instead of Top 10
  • Add a filter: the same name can only appear once (with their best score)

๐Ÿง  What does [5,2,8,1].sort((a,b) => b - a) do?

[1, 2, 5, 8] โ€” sorts in ascending order
[8, 5, 2, 1] โ€” sorts in descending order
Doesn't modify the array, just returns a copy
Throws an error because sort() can't receive a comparison function
Phase 5 ยท Lesson 48

๐Ÿ”Š Web Audio API โ€” Music and Sound Effects

Playing sound in a browser with three approaches: HTML5 Audio element, embedded base64 audio, and the advanced AudioContext API. Why isn't the simplest solution always enough?

โฑ 75 min
๐Ÿ“ new Audio() ยท AudioContext
๐ŸŽฏ base64 audio ยท play() ยท resume()

1The simplest: new Audio()

๐Ÿธ Froggy Rush

The Audio object is the browser's built-in audio player. It takes a filename and can play it immediately.

Simple audio playbackJavaScript
// Load audio from file
const bgm = new Audio('frog_music1.mp3');
bgm.loop   = true;    // enable looping
bgm.volume = 0.6;    // volume: 0.0 โ€“ 1.0

// Playback โ€” important: user interaction required!
document.addEventListener('click', () => {
  bgm.play().catch(e => console.warn('Audio error:', e));
}, { once: true }); // only on the first click

// Stop and rewind
bgm.pause();
bgm.currentTime = 0; // rewind to the beginning
Autoplay policy โ€” why is a click required?

Since 2018, browsers block automatic audio playback โ€” the user must first interact with the page (click, key press). That's why in most games, music only starts on the first click.

2Base64 audio embedding โ€” mobile solution

๐Ÿฐ Castle Siege

On Android, the browser often fails to load external mp3 files โ€” especially if the game runs locally (from the filesystem, without a server). The solution: embed the audio directly into the HTML file using base64 encoding.

castle_siege.html โ€” base64 audioJavaScript
// The base64 code looks like this (shortened):
// data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2...
// (in practice hundreds of thousands of characters โ€” the full audio file)

// We give this to the Audio object as its source
const MUSIC_DATA = 'data:audio/mp3;base64,SUQzBAAA...'; // full data
const bgm = new Audio(MUSIC_DATA);
bgm.loop = true;

// Sound effects work the same way โ€” small sounds as base64
const SFX_HIT  = new Audio('data:audio/wav;base64,UklGRiQ...');
const SFX_GOLD = new Audio('data:audio/wav;base64,UklGRjQ...');

// Playing a sound effect (clone โ†’ multiple can play simultaneously)
function playSound(audio) {
  const clone = audio.cloneNode();
  clone.volume = 0.7;
  clone.play().catch(() => {});
}

How do we encode audio to base64?

Converting โ€” Node.js command-line scriptJavaScript
// Run with Node.js: node convert.js
const fs = require('fs');
const data = fs.readFileSync('music.mp3');
const b64  = 'data:audio/mp3;base64,' + data.toString('base64');
fs.writeFileSync('music_b64.txt', b64);
console.log('Done! Size: ' + b64.length + ' characters');

// Alternative: in the browser, drag & drop solution
// โ†’ btoa() is not suitable for mp3, because it's binary data
// โ†’ use the FileReader API in the browser:
const reader = new FileReader();
reader.onload = e => console.log(e.target.result); // the b64 string
reader.readAsDataURL(file); // file = input[type=file] .files[0]
cloneNode() โ€” why do we need it?

If you play the same Audio object twice in quick succession (e.g. two enemies die at once), the second play() jumps to the end of the first. cloneNode() creates a copy โ€” so multiple sound effects can play in parallel simultaneously.

3AudioContext โ€” advanced audio handling

The AudioContext is the browser's complete audio editing system: volume control, effects, mixing, synthesised sound generation. In game development we mainly use it for volume and musical effects.

Synthesised sound effect โ€” no audio file neededJavaScript
// Create the audio system
const actx = new AudioContext();

// Synthesised "coin collected" sound โ€” without any audio file!
function beep(hz = 440, duration = 0.15, volume = 0.3) {
  const osc  = actx.createOscillator();  // oscillator generator
  const gain = actx.createGain();        // volume controller

  osc.connect(gain).connect(actx.destination);
  osc.frequency.value = hz;       // 440 Hz = A note (A4)
  osc.type = 'sine';             // waveform: sine / square / sawtooth
  gain.gain.value = volume;       // volume level

  // Fade out at the end (envelope)
  gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + duration);

  osc.start();
  osc.stop(actx.currentTime + duration);
}

// Usage:
beep(880, 0.1);  // sharp pip โ€” score gained
beep(220, 0.4);  // deep hum โ€” life lost
beep(1047, 0.2); // high chime โ€” level complete
Which one to use when?

new Audio(file) โ€” simple sounds from external mp3/wav files.
new Audio(base64) โ€” mobile/offline compatible, no file needed.
AudioContext beep() โ€” synthesised sounds, no audio file, for small games.

โœ๏ธ Task

Add sound to your own mini game:

  • Create a beep() function based on the code above
  • Play it on score gain (high sound) and life loss (low sound)
  • Open F12 โ†’ Console โ€” do you get an "AudioContext was not allowed to start" warning?
  • Fix it: start the AudioContext from a click event

๐Ÿง  Why isn't audio.play() enough when the game starts?

Because the Audio object only accepts base64 data
Because play() only works with mp3 files
Because browsers block automatic audio playback โ€” a user interaction (click) is required first
Because the Audio object isn't available before the window.load event
Phase 5 ยท Lesson 49

๐Ÿ“ฑ Mobile D-pad and Touch Controls

Games need to be playable on mobile โ€” but there's no keyboard. The solution: directional buttons drawn over the canvas that respond to touch events. This is how we built the mobile controls for Froggy Rush and Castle Siege.

โฑ 90 min
๐ŸŽฏ touchstart ยท touchend ยท pointer events
๐Ÿ“ D-pad ยท virtual keyboard

1Touch vs Pointer events โ€” which do we use?

Event typeCompatibilityAdvantageDisadvantage
touchstart / touchendTouch screens onlyOlder devices tooSeparate mouse and touch code needed
pointerdown / pointerupMouse + touch + stylusOne code for all threeIE11 not supported
clickEverywhereSimplest~300ms delay on mobile, no multi-touch
Recommendation: pointer events

For modern games, use pointerdown / pointerup / pointermove events โ€” these work uniformly with both mouse and touchscreen, from a single code base.

2D-pad implementation

๐Ÿธ Froggy Rush
๐Ÿฐ Castle Siege
froggy_rush.html โ€” D-pad setupJavaScript
// โ”€โ”€โ”€ 1. D-PAD BUTTON DEFINITIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const DPAD = [
  { id: 'up',    x:75,  y:40,  w:50, h:50, label:'โ–ฒ' },
  { id: 'down',  x:75,  y:140, w:50, h:50, label:'โ–ผ' },
  { id: 'left',  x:20,  y:90,  w:50, h:50, label:'โ—€' },
  { id: 'right', x:130, y:90,  w:50, h:50, label:'โ–ถ' },
];
// Which buttons are currently pressed
const pressed = new Set();

// โ”€โ”€โ”€ 2. CANVAS COORDINATE CALCULATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function getCanvasPos(e) {
  const r = canvas.getBoundingClientRect();
  // Scale ratio: if the canvas has a different CSS size
  const scaleX = canvas.width  / r.width;
  const scaleY = canvas.height / r.height;
  return {
    x: (e.clientX - r.left) * scaleX,
    y: (e.clientY - r.top)  * scaleY
  };
}

// โ”€โ”€โ”€ 3. HIT-TEST: which button was tapped? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function hitDpad(px, py) {
  // D-pad is in the bottom-left corner of the canvas
  const offX = 10, offY = H - 200; // D-pad top-left anchor point
  for (const btn of DPAD) {
    if (px >= offX+btn.x && px <= offX+btn.x+btn.w &&
        py >= offY+btn.y && py <= offY+btn.y+btn.h) {
      return btn.id;  // 'up', 'down', 'left' or 'right'
    }
  }
  return null;
}

// โ”€โ”€โ”€ 4. EVENT HANDLERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
canvas.addEventListener('pointerdown', e => {
  e.preventDefault(); // block scroll
  const pos = getCanvasPos(e);
  const btn = hitDpad(pos.x, pos.y);
  if (btn) pressed.add(btn);
});

canvas.addEventListener('pointerup', e => {
  const pos = getCanvasPos(e);
  const btn = hitDpad(pos.x, pos.y);
  if (btn) pressed.delete(btn);
});

// In the game loop: pressed contains the active buttons
function update() {
  if (pressed.has('left')  || K.left)  p.vx -= 0.8;
  if (pressed.has('right') || K.right) p.vx += 0.8;
  if (pressed.has('up')    || K.up)    jump();
}
Set vs array โ€” why use Set for pressed?

A Set automatically filters duplicates: if you press "up" twice, it only appears once. The add() and has() operations are O(1) fast. The same is achievable with an array, but Set is cleaner and faster.

3Drawing the D-pad on canvas

D-pad drawingJavaScript
function drawDpad() {
  // Only show on mobile
  if (!isMobile()) return;

  const offX = 10, offY = H - 200;

  for (const btn of DPAD) {
    const bx = offX + btn.x, by = offY + btn.y;
    // Pressed button: different colour
    ctx.fillStyle = pressed.has(btn.id)
      ? 'rgba(255,154,60,0.7)'   // pressed: orange
      : 'rgba(255,255,255,0.12)'; // default: semi-transparent

    // Rounded rectangle (roundRect)
    ctx.beginPath();
    ctx.roundRect(bx, by, btn.w, btn.h, 10);
    ctx.fill();

    // Border
    ctx.strokeStyle = 'rgba(255,255,255,0.25)';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // Arrow icon
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.font = 'bold 20px Inter';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(btn.label, bx + btn.w/2, by + btn.h/2);
  }
}

// Mobile detection
function isMobile() {
  return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

Multi-touch โ€” multiple buttons at once

Pointer capture โ€” multi-touch handlingJavaScript
// Each pointer (finger) gets an ID
const activePointers = new Map(); // pointerId โ†’ dpad button

canvas.addEventListener('pointerdown', e => {
  e.preventDefault();
  const pos = getCanvasPos(e);
  const btn = hitDpad(pos.x, pos.y);
  if (btn) {
    activePointers.set(e.pointerId, btn); // remember which finger is on which button
    pressed.add(btn);
  }
});

canvas.addEventListener('pointerup', e => {
  const btn = activePointers.get(e.pointerId); // which button does this finger belong to?
  if (btn) {
    pressed.delete(btn);
    activePointers.delete(e.pointerId);
  }
});
โœ๏ธ Task

Add the mobile D-pad to your own game:

  • Import the DPAD definition and the pressed Set
  • Register the pointerdown and pointerup events
  • Call drawDpad() at the end of draw()
  • Test in Chrome DevTools โ†’ Toggle Device Toolbar (Ctrl+Shift+M) โ€” does the D-pad appear?

๐Ÿง  Why do we use a Set to store pressed buttons?

Because Set is faster than an object
Because Set automatically filters duplicates โ€” if you press the same button twice, it only appears once; and has() / add() / delete() operations are simple
Because arrays can't store string values
Because Set is asynchronous โ€” it doesn't block the animation
Phase 5 ยท Lesson 50

๐Ÿš€ Deploy โ€” Your Game Goes Live on the Internet

The finished game can be tried by anyone on their own phone via a link โ€” without a server, hosting, or developer knowledge. Netlify is free and drag-and-drop simple.

โฑ 30 min
๐ŸŽฏ Netlify ยท static hosting ยท custom URL

1What is static hosting?

Our games are static files: HTML, CSS, JavaScript โ€” no database, no server-side code. A static hosting service simply serves these files to the browser, making them accessible to everyone.

ServiceFree planDifficultyNotes
Netlifyโœ… Yes, unlimitedโญ EasiestDrag & drop, custom domain possible
GitHub Pagesโœ… Yesโญโญ EasyGitHub account needed, git push
Vercelโœ… Yesโญโญ EasyFor developers, automatic deploy
itch.ioโœ… Yesโญ EasiestSpecifically for games, community

2Netlify โ€” step by step

1
Create an account

Visit netlify.com and click the "Sign up" button. You can create a free account with Google or GitHub.

2
Prepare the folder

Make sure all the necessary files are in one folder: index.html, the game HTML files, and the mp3 music tracks.

3
Drag & Drop upload

On the Netlify home page under the "Sites" tab there is a "Drop your site folder here" area. Drag your entire project folder onto this area.

4
Get your unique URL

After a few seconds you get a link: some-name-12345.netlify.app. This is your page's URL โ€” anyone can access it!

5
Set a custom name (optional)

Site settings โ†’ Domain management โ†’ Options โ†’ Edit site name. Make it e.g. froggy-rush-alex.netlify.app.

Updating โ€” uploading a new version

If you need to re-upload after making changes: on the Netlify admin panel under the "Deploys" tab there is a "Drop your site folder here" area โ€” drag and drop the same way. The URL stays the same and updates automatically.

3Pre-deployment checklist

Before uploading, check the following:

  • The main page is named index.html โ€” this opens automatically
  • All filenames are lowercase, no accents, no spaces (froggy_rush.html โœ…, Froggy Rush.html โŒ)
  • Audio files are embedded as base64, or in the correct subdirectory
  • All relative links are correct (href="game.html" not href="C:/Users/alex/game.html")
  • Game tested in Chrome and Firefox browsers
  • Opened on mobile (Chrome DevTools โ†’ Ctrl+Shift+M) and D-pad works
  • localStorage save and load has been tested

4Itch.io โ€” sharing games with players

itch.io is an indie game platform โ€” if you want to share your game not just as a link but on a dedicated game page, this is the best choice.

itch.io upload โ€” packing into a zipbash
# Windows: right-click folder โ†’ Send to โ†’ Compressed (zip) folder
# Mac:     right-click โ†’ Compress
# Linux:   zip -r game.zip game_folder/

# On itch.io:
# 1. Register (free)
# 2. "Create new project" โ†’ Pricing: Free
# 3. Kind of project: HTML
# 4. Uploads: upload the zip, check: "This file will be played in the browser"
# 5. Viewport dimensions: 480ร—700 (portrait) or 800ร—600 (landscape)
# 6. Save & publish โ†’ done!

# Your game will be accessible at: https://username.itch.io/game-name
โœ๏ธ Task โ€” The Real Final Task

Upload your own game to the internet:

  • Sign up for Netlify (free)
  • Make sure index.html is the main page
  • Drag the folder onto the Netlify drop area
  • Copy the URL you receive and send it to a friend on mobile
  • Bonus: set a custom name for the page

๐Ÿง  Why is it important that the main page is named exactly index.html?

Because Netlify only accepts this filename
Because when a browser requests a folder (e.g. froggy-rush.netlify.app/), the web server automatically looks for and serves the index.html file โ€” this is the conventional "entry point"
Because JavaScript only loads correctly from an index.html file
No particular reason โ€” anything would work
๐Ÿ†

Congratulations โ€” you've completed the course!

Through 50 lessons, 5 phases, and 4 real games you've learned the fundamentals of web programming from scratch. That's no small thing.

โœ“What you've completed โ€” summary

PhaseTopicLessonsWhat you learned
1. HTML & CSS Web page structure, styles 1โ€“10 DOCTYPE, tags, CSS, Flexbox, responsive design
2. JavaScript Programming logic 11โ€“20 Variables, conditions, loops, functions, arrays, objects, events
3. Canvas Graphics and animation 21โ€“30 Drawing, game loop, animation, collision, camera, sprite animation
4. Games Full game analysis 31โ€“45 AI, State Machine, level generation, strategy pattern, Tower Defense
5. Advanced Save, sound, mobile, deploy 46โ€“50 localStorage, JSON, leaderboard, Web Audio, D-pad, Netlify

โ†’What's next?

๐ŸŽฎ BUILD YOUR OWN GAME

Pick an idea you love (platform, puzzle, tower defense) and build it from scratch. Anything you get stuck on โ€” Google, MDN docs, or the game source code is your reference.

๐Ÿ“š NEXT STEP: REACT

The next level of web development: React (components, state management) and Node.js (server side, real database, multiple users).

๐Ÿ”ง TOOLS AND LIBRARIES

Phaser.js โ€” professional HTML5 game engine. Three.js โ€” 3D graphics with WebGL. Howler.js โ€” more advanced audio handling.

๐ŸŒ USEFUL LINKS

MDN Web Docs โ€” the best HTML/JS documentation. JavaScript.info โ€” for deeper JS learning. itch.io โ€” share your game.

๐Ÿ’กThe most important lesson

People learn programming by programming

Not by reading, not by watching videos. After completing the course, real progress is in your hands: open a blank HTML file, think of something you want to see on screen, and start writing. The first 10 attempts won't work perfectly โ€” but the 11th will. That's how everyone learns to program.