Free browser games with source code

Play JavaScript Games Online & Download the Source

Start a game instantly, inspect the HTML, CSS, and JavaScript, or build a variation from a local template. No sign-up and no code upload.

Build a game
● Runs locally● No account● Source included

Choose your game

Four games. Full source included.

Pick one to load it in the workspace below. Nothing starts—or makes sound—until you press Play.

← → or A D · drag on touch

Neon Breakout

Bounce the ball, clear the wall, and protect your last life.

↑ ↓ or W S · drag on touch

Pixel Pong

Hold your side of the court against a compact local opponent.

Arrow keys or WASD · swipe on touch

Grid Snake

Collect every spark without crossing your own trail.

Space, click, or tap

Sky Hopper

Time each lift and pass through a moving skyline.

Play first · inspect next

Neon Breakout

Open in maker
loading
Score0
Lives3
Level1
Device best0

← → or A D · drag on touch

Game preview is loading.

Version 1.0.0

Read the working project

Every visible game is assembled from the files below. Copy a file, open an editable version, or download the complete project.

/* JS Breakouts Neon Breakout — original geometry-only canvas implementation */
const CONFIG = {"game":"breakout","framework":"vanilla","difficulty":"normal","controls":"keyboard-touch","theme":"neon","sound":true,"reducedEffects":false,"variant":"classic","palette":"arcade","speed":1,"lives":3,"size":1,"colors":{"bg":"#08111f","panel":"#111e33","primary":"#2ad1c9","secondary":"#8b5cf6","text":"#f4f7ff","danger":"#fb7185"}};
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 800;
const H = 500;
const keys = new Set();
let status = 'ready';
let muted = !CONFIG.sound;
let score = 0;
let lives = CONFIG.lives;
let level = 1;
let last = 0;
let raf = 0;
let pointerStart = null;
let world = {};
let audioContext = null;
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let reducedEffects = Boolean(CONFIG.reducedEffects || reducedMotionQuery.matches);
const ui = {
  score: document.getElementById('score'),
  best: document.getElementById('best'),
  lives: document.getElementById('lives'),
  level: document.getElementById('level'),
  status: document.getElementById('status'),
  play: document.getElementById('play'),
  stop: document.getElementById('stop'),
  restart: document.getElementById('restart'),
  mute: document.getElementById('mute')
};
const highScoreKey = 'jsbreakouts:high:' + CONFIG.game;
let highScore = readStoredNumber(highScoreKey);
try { muted = localStorage.getItem('jsbreakouts:muted') === 'true' || !CONFIG.sound; } catch {}

function readStoredNumber(key) {
  try { return Number(localStorage.getItem(key) || 0); } catch { return 0; }
}

function storeValue(key, value) {
  try { localStorage.setItem(key, String(value)); } catch {}
}

function resizeCanvas() {
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.round(W * ratio);
  canvas.height = Math.round(H * ratio);
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}

function tone(kind) {
  if (muted || !CONFIG.sound) return;
  const AudioCtor = window.AudioContext || window.webkitAudioContext;
  if (!AudioCtor) return;
  audioContext ||= new AudioCtor();
  if (audioContext.state === 'suspended') audioContext.resume();
  const frequencies = { paddle: 220, brick: 520, score: 660, life: 130, hop: 360, gameover: 90 };
  const oscillator = audioContext.createOscillator();
  const gain = audioContext.createGain();
  oscillator.type = kind === 'gameover' ? 'sawtooth' : 'square';
  oscillator.frequency.value = frequencies[kind] || 300;
  gain.gain.setValueAtTime(0.035, audioContext.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + (kind === 'gameover' ? 0.22 : 0.075));
  oscillator.connect(gain);
  gain.connect(audioContext.destination);
  oscillator.start();
  oscillator.stop(audioContext.currentTime + (kind === 'gameover' ? 0.23 : 0.08));
}

function syncUi() {
  if (score > highScore) {
    highScore = score;
    storeValue(highScoreKey, highScore);
  }
  if (ui.score) ui.score.textContent = String(score);
  if (ui.best) ui.best.textContent = String(highScore);
  if (ui.lives) ui.lives.textContent = String(lives);
  if (ui.level) ui.level.textContent = String(level);
  if (ui.status) {
    ui.status.dataset.state = status;
    ui.status.textContent = status === 'ready' ? 'Ready to play' : status === 'playing' ? 'Playing' : status === 'paused' ? 'Paused' : status === 'stopped' ? 'Stopped' : 'Game over';
  }
  if (ui.play) ui.play.textContent = status === 'playing' ? 'Pause' : status === 'paused' ? 'Resume' : status === 'gameover' ? 'Play again' : 'Play';
  if (ui.mute) {
    ui.mute.textContent = muted ? 'Unmute' : 'Mute';
    ui.mute.setAttribute('aria-pressed', String(muted));
  }
}

const send = (type, payload = {}) => {
  if (window.parent !== window) window.parent.postMessage({ source: 'js-breakouts-preview', type, payload }, '*');
};

function resetWorld() {
  score = 0;
  lives = CONFIG.lives;
  level = 1;
  if (CONFIG.game === 'breakout') resetBreakout();
  if (CONFIG.game === 'pong') resetPong();
  if (CONFIG.game === 'snake') resetSnake();
  if (CONFIG.game === 'sky-hopper') resetHopper();
  emitScore();
  draw();
}

function emitScore() {
  syncUi();
  send('SCORE_UPDATE', { score, lives, level });
}

function start() {
  if (status === 'gameover') resetWorld();
  status = 'playing';
  canvas.focus();
  syncUi();
  send('STATE_CHANGE', { status });
}

function pause(reason = 'user') {
  if (status !== 'playing') return;
  status = 'paused';
  syncUi();
  send('STATE_CHANGE', { status, reason });
}

function restart() {
  status = 'ready';
  resetWorld();
  syncUi();
  send('STATE_CHANGE', { status });
}

function stop() {
  status = 'stopped';
  resetWorld();
  status = 'stopped';
  syncUi();
  send('STATE_CHANGE', { status });
}

function gameOver() {
  status = 'gameover';
  tone('gameover');
  syncUi();
  send('GAME_OVER', { score });
  send('STATE_CHANGE', { status });
}

function difficultyScale() {
  return CONFIG.difficulty === 'easy' ? 0.8 : CONFIG.difficulty === 'hard' ? 1.22 : 1;
}

function resetBreakout() {
  const wide = CONFIG.variant === 'wide-paddle' ? 1.35 : 1;
  const fast = CONFIG.variant === 'fast-ball' ? 1.25 : 1;
  world = {
    paddle: { x: W / 2 - 66 * wide, y: H - 42, w: 132 * wide * CONFIG.size, h: 14 },
    ball: { x: W / 2, y: H - 65, r: 9 * CONFIG.size, vx: 250 * CONFIG.speed * fast, vy: -250 * CONFIG.speed * fast },
    bricks: []
  };
  const rows = CONFIG.difficulty === 'hard' ? 7 : 6;
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < 10; col++) {
      world.bricks.push({ x: 34 + col * 74, y: 56 + row * 30, w: 64, h: 18, alive: true, row });
    }
  }
}

function updateBreakout(dt) {
  const p = world.paddle;
  const b = world.ball;
  const move = 430 * dt;
  if (keys.has('arrowleft') || keys.has('a')) p.x -= move;
  if (keys.has('arrowright') || keys.has('d')) p.x += move;
  p.x = Math.max(10, Math.min(W - p.w - 10, p.x));
  b.x += b.vx * dt;
  b.y += b.vy * dt;
  if (b.x < b.r || b.x > W - b.r) { b.vx *= -1; b.x = Math.max(b.r, Math.min(W - b.r, b.x)); }
  if (b.y < b.r) { b.vy = Math.abs(b.vy); }
  if (b.vy > 0 && b.y + b.r >= p.y && b.y - b.r <= p.y + p.h && b.x >= p.x && b.x <= p.x + p.w) {
    b.y = p.y - b.r;
    b.vy = -Math.abs(b.vy);
    b.vx += ((b.x - (p.x + p.w / 2)) / p.w) * 120;
    tone('paddle');
  }
  for (const brick of world.bricks) {
    if (!brick.alive) continue;
    if (b.x + b.r > brick.x && b.x - b.r < brick.x + brick.w && b.y + b.r > brick.y && b.y - b.r < brick.y + brick.h) {
      brick.alive = false;
      b.vy *= -1;
      score += 10;
      tone('brick');
      emitScore();
      break;
    }
  }
  if (world.bricks.every((brick) => !brick.alive)) {
    level += 1;
    const oldScore = score;
    resetBreakout();
    score = oldScore;
    emitScore();
  }
  if (b.y > H + b.r) {
    lives -= 1;
    tone('life');
    emitScore();
    if (lives <= 0) gameOver();
    else {
      b.x = W / 2; b.y = H - 65; b.vy = -Math.abs(b.vy);
      status = 'paused'; syncUi(); send('STATE_CHANGE', { status, reason: 'life-lost' });
    }
  }
}

function drawBreakout() {
  const p = world.paddle;
  const b = world.ball;
  for (const brick of world.bricks) {
    if (!brick.alive) continue;
    ctx.fillStyle = brick.row % 2 ? CONFIG.colors.secondary : CONFIG.colors.primary;
    ctx.globalAlpha = 0.82 + (brick.row % 3) * 0.06;
    ctx.fillRect(brick.x, brick.y, brick.w, brick.h);
    ctx.globalAlpha = 1;
  }
  ctx.fillStyle = CONFIG.colors.text;
  ctx.fillRect(p.x, p.y, p.w, p.h);
  ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fillStyle = CONFIG.colors.primary; ctx.fill();
}

function resetPong() {
  world = {
    player: { x: 28, y: H / 2 - 55, w: 14, h: 110 * CONFIG.size },
    ai: { x: W - 42, y: H / 2 - 55, w: 14, h: 110 },
    ball: { x: W / 2, y: H / 2, r: 9, vx: 285 * CONFIG.speed, vy: 180 },
    aiScore: 0
  };
}

function updatePong(dt) {
  const p = world.player, ai = world.ai, b = world.ball;
  const move = 430 * dt;
  if (keys.has('arrowup') || keys.has('w')) p.y -= move;
  if (keys.has('arrowdown') || keys.has('s')) p.y += move;
  p.y = Math.max(0, Math.min(H - p.h, p.y));
  const aiScale = CONFIG.variant === 'easy-ai' ? 0.55 : CONFIG.difficulty === 'hard' ? 0.95 : 0.72;
  ai.y += Math.sign(b.y - (ai.y + ai.h / 2)) * 340 * aiScale * dt;
  ai.y = Math.max(0, Math.min(H - ai.h, ai.y));
  b.x += b.vx * dt; b.y += b.vy * dt;
  if (b.y < b.r || b.y > H - b.r) b.vy *= -1;
  const hit = (pad) => b.x + b.r > pad.x && b.x - b.r < pad.x + pad.w && b.y + b.r > pad.y && b.y - b.r < pad.y + pad.h;
  if (b.vx < 0 && hit(p)) { b.vx = Math.abs(b.vx) * 1.025; b.vy += (b.y - (p.y + p.h / 2)) * 3; tone('paddle'); }
  if (b.vx > 0 && hit(ai)) { b.vx = -Math.abs(b.vx) * 1.025; tone('paddle'); }
  if (b.x > W + 20) { score += 1; tone('score'); emitScore(); b.x = W / 2; b.y = H / 2; b.vx = -285 * CONFIG.speed; }
  if (b.x < -20) { world.aiScore += 1; lives = Math.max(0, 5 - world.aiScore); tone('life'); emitScore(); b.x = W / 2; b.y = H / 2; b.vx = 285 * CONFIG.speed; }
  if (score >= 5 || world.aiScore >= 5) gameOver();
}

function drawPong() {
  ctx.setLineDash([12, 14]); ctx.strokeStyle = CONFIG.colors.secondary; ctx.beginPath(); ctx.moveTo(W/2, 18); ctx.lineTo(W/2, H-18); ctx.stroke(); ctx.setLineDash([]);
  ctx.fillStyle = CONFIG.colors.primary; ctx.fillRect(world.player.x, world.player.y, world.player.w, world.player.h);
  ctx.fillStyle = CONFIG.colors.secondary; ctx.fillRect(world.ai.x, world.ai.y, world.ai.w, world.ai.h);
  ctx.beginPath(); ctx.arc(world.ball.x, world.ball.y, world.ball.r, 0, Math.PI*2); ctx.fillStyle = CONFIG.colors.text; ctx.fill();
}

function resetSnake() {
  world = {
    snake: [{ x: 12, y: 10 }, { x: 11, y: 10 }, { x: 10, y: 10 }],
    dir: { x: 1, y: 0 },
    next: { x: 1, y: 0 },
    food: { x: 17, y: 10 },
    timer: 0
  };
}

function snakeDirection(x, y) {
  if (x + world.dir.x === 0 && y + world.dir.y === 0) return;
  world.next = { x, y };
}

function updateSnake(dt) {
  world.timer += dt;
  const step = (CONFIG.variant === 'speed-up' ? Math.max(0.065, 0.15 - score * 0.002) : 0.14) / CONFIG.speed / difficultyScale();
  if (world.timer < step) return;
  world.timer = 0; world.dir = world.next;
  const head = { x: world.snake[0].x + world.dir.x, y: world.snake[0].y + world.dir.y };
  const wrap = CONFIG.variant === 'wrap';
  if (wrap) { head.x = (head.x + 25) % 25; head.y = (head.y + 15) % 15; }
  const hitWall = head.x < 0 || head.x >= 25 || head.y < 0 || head.y >= 15;
  const hitSelf = world.snake.some((part) => part.x === head.x && part.y === head.y);
  if (hitWall || hitSelf) { lives = 0; emitScore(); gameOver(); return; }
  world.snake.unshift(head);
  if (head.x === world.food.x && head.y === world.food.y) {
    score += 10; tone('score'); emitScore();
    do { world.food = { x: Math.floor(Math.random()*25), y: Math.floor(Math.random()*15) }; }
    while (world.snake.some((part) => part.x === world.food.x && part.y === world.food.y));
  } else world.snake.pop();
}

function drawSnake() {
  const cellW = W / 25, cellH = H / 15;
  ctx.fillStyle = CONFIG.colors.secondary; ctx.beginPath(); ctx.arc((world.food.x+.5)*cellW, (world.food.y+.5)*cellH, 10, 0, Math.PI*2); ctx.fill();
  world.snake.forEach((part, index) => {
    ctx.fillStyle = index === 0 ? CONFIG.colors.text : CONFIG.colors.primary;
    ctx.fillRect(part.x*cellW+2, part.y*cellH+2, cellW-4, cellH-4);
  });
}

function resetHopper() {
  world = { bird: { x: 190, y: H/2, vy: 0, r: 18 * CONFIG.size }, pipes: [], spawn: 0 };
}

function hop() {
  if (status !== 'playing') start();
  world.bird.vy = -370 / difficultyScale();
  tone('hop');
}

function updateHopper(dt) {
  const bird = world.bird;
  const relaxed = CONFIG.variant === 'relaxed';
  bird.vy += (relaxed ? 760 : 920) * dt * difficultyScale();
  bird.y += bird.vy * dt;
  world.spawn -= dt;
  if (world.spawn <= 0) {
    const gap = relaxed ? 190 : CONFIG.difficulty === 'hard' ? 135 : 165;
    const center = 150 + Math.random() * 200;
    world.pipes.push({ x: W + 40, center, gap, passed: false });
    world.spawn = 1.65 / CONFIG.speed;
  }
  for (const pipe of world.pipes) {
    pipe.x -= (CONFIG.variant === 'fast' ? 250 : 205) * CONFIG.speed * dt;
    if (!pipe.passed && pipe.x < bird.x) { pipe.passed = true; score += 1; tone('score'); emitScore(); }
    const nearX = bird.x + bird.r > pipe.x && bird.x - bird.r < pipe.x + 70;
    if (nearX && (bird.y - bird.r < pipe.center - pipe.gap/2 || bird.y + bird.r > pipe.center + pipe.gap/2)) gameOver();
  }
  world.pipes = world.pipes.filter((pipe) => pipe.x > -80);
  if (bird.y < bird.r || bird.y > H - bird.r) gameOver();
}

function drawHopper() {
  ctx.fillStyle = CONFIG.colors.secondary;
  for (const pipe of world.pipes) {
    ctx.fillRect(pipe.x, 0, 70, pipe.center - pipe.gap/2);
    ctx.fillRect(pipe.x, pipe.center + pipe.gap/2, 70, H);
  }
  const bird = world.bird;
  ctx.fillStyle = CONFIG.colors.primary; ctx.beginPath(); ctx.arc(bird.x, bird.y, bird.r, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = CONFIG.colors.text; ctx.beginPath(); ctx.arc(bird.x+7, bird.y-5, 4, 0, Math.PI*2); ctx.fill();
}

function update(dt) {
  if (CONFIG.game === 'breakout') updateBreakout(dt);
  if (CONFIG.game === 'pong') updatePong(dt);
  if (CONFIG.game === 'snake') updateSnake(dt);
  if (CONFIG.game === 'sky-hopper') updateHopper(dt);
}

function drawOverlay() {
  if (status === 'playing') return;
  ctx.fillStyle = 'rgba(8, 17, 31, .72)'; ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center'; ctx.fillStyle = CONFIG.colors.text; ctx.font = '700 30px system-ui';
  const title = status === 'ready' ? 'Ready to play' : status === 'paused' ? 'Paused' : status === 'stopped' ? 'Stopped' : 'Game over';
  ctx.fillText(title, W/2, H/2 - 8);
  ctx.fillStyle = CONFIG.colors.primary; ctx.font = '16px system-ui';
  ctx.fillText(status === 'gameover' ? 'Press Space to play again' : 'Press Space or use Play', W/2, H/2 + 26);
}

function draw() {
  ctx.fillStyle = CONFIG.colors.bg; ctx.fillRect(0, 0, W, H);
  if (!reducedEffects) {
    ctx.strokeStyle = CONFIG.colors.panel; ctx.lineWidth = 1;
    for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
    for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  }
  if (CONFIG.game === 'breakout') drawBreakout();
  if (CONFIG.game === 'pong') drawPong();
  if (CONFIG.game === 'snake') drawSnake();
  if (CONFIG.game === 'sky-hopper') drawHopper();
  drawOverlay();
}

function loop(now) {
  const dt = Math.min(0.034, (now - last) / 1000 || 0);
  last = now;
  if (status === 'playing') update(dt);
  draw();
  raf = requestAnimationFrame(loop);
}

canvas.addEventListener('keydown', (event) => {
  if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' ','a','d','w','s'].includes(event.key)) event.preventDefault();
  keys.add(event.key.toLowerCase());
  if (event.key === ' ') CONFIG.game === 'sky-hopper' ? hop() : status === 'playing' ? pause() : start();
  if (CONFIG.game === 'snake') {
    if (event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a') snakeDirection(-1,0);
    if (event.key === 'ArrowRight' || event.key.toLowerCase() === 'd') snakeDirection(1,0);
    if (event.key === 'ArrowUp' || event.key.toLowerCase() === 'w') snakeDirection(0,-1);
    if (event.key === 'ArrowDown' || event.key.toLowerCase() === 's') snakeDirection(0,1);
  }
  if (event.key === 'Escape') canvas.blur();
  if (event.key.toLowerCase() === 'r') restart();
});
canvas.addEventListener('keyup', (event) => keys.delete(event.key.toLowerCase()));
canvas.addEventListener('pointerdown', (event) => {
  pointerStart = { x: event.offsetX, y: event.offsetY };
  canvas.setPointerCapture(event.pointerId);
  if (CONFIG.game === 'sky-hopper') hop();
});
canvas.addEventListener('pointermove', (event) => {
  if (!pointerStart) return;
  const x = event.offsetX * W / canvas.clientWidth;
  const y = event.offsetY * H / canvas.clientHeight;
  if (CONFIG.game === 'breakout') world.paddle.x = x - world.paddle.w/2;
  if (CONFIG.game === 'pong') world.player.y = y - world.player.h/2;
});
canvas.addEventListener('pointerup', (event) => {
  if (CONFIG.game === 'snake' && pointerStart) {
    const dx = event.offsetX - pointerStart.x, dy = event.offsetY - pointerStart.y;
    if (Math.abs(dx) > Math.abs(dy)) snakeDirection(dx > 0 ? 1 : -1, 0);
    else snakeDirection(0, dy > 0 ? 1 : -1);
  }
  pointerStart = null;
});

window.addEventListener('message', (event) => {
  const data = event.data;
  if (!data || data.source !== 'js-breakouts-parent') return;
  if (data.type === 'PLAY') start();
  if (data.type === 'PAUSE') pause();
  if (data.type === 'STOP') stop();
  if (data.type === 'RESTART') restart();
  if (data.type === 'MUTE') { muted = Boolean(data.payload && data.payload.muted); syncUi(); }
});
document.addEventListener('visibilitychange', () => { if (document.hidden) pause('hidden'); });
reducedMotionQuery.addEventListener?.('change', (event) => { reducedEffects = Boolean(CONFIG.reducedEffects || event.matches); });
ui.play?.addEventListener('click', () => status === 'playing' ? pause() : start());
ui.stop?.addEventListener('click', stop);
ui.restart?.addEventListener('click', restart);
ui.mute?.addEventListener('click', () => {
  muted = !muted;
  storeValue('jsbreakouts:muted', muted);
  syncUi();
});
window.addEventListener('error', (event) => send('RUNTIME_ERROR', { message: event.message, line: event.lineno || 0 }));
window.addEventListener('unhandledrejection', (event) => send('RUNTIME_ERROR', { message: String(event.reason || 'Unhandled promise rejection') }));
setInterval(() => send('HEARTBEAT'), 2000);
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
resetWorld();
raf = requestAnimationFrame(loop);
syncUi();
send('READY', { game: CONFIG.game });

Template-powered builder

Build a browser game variation

Choose clear settings, rebuild the project, then play, inspect, or download the result.

Sandboxed preview

Neon Breakout

vanilla
loading
Score0
Lives3
Level1
Device best0

← → or A D · drag on touch

Game preview is loading.

Editable project

Source code

A short, visible path

From first play to a runnable project

  1. 01

    Pick a game

    Start with Breakout, Pong, Snake, or Sky Hopper.

  2. 02

    Play or customize

    Adjust clear settings and rebuild the local project.

  3. 03

    Inspect the source

    Read the HTML, CSS, and JavaScript behind the game.

  4. 04

    Download the project

    Get a runnable ZIP with a README and license.

Built to be understood

A small game is a useful piece of code.

Every template favors readable source, browser-native APIs, and a complete handoff over a flashy black box.

Why JS Breakouts is play-first
  • Learn from working codeCompact modules and a plain-language README.
  • Start without setupPlay before choosing a framework or changing settings.
  • Keep work localSettings, edits, and scores stay in your browser.
  • Download a real projectNot a snippet—a complete, licensed package.
  • No account or limitsNo login, credits, email capture, or cloud dependency.

Questions, answered plainly

Before you play, edit, or download

Can I play the games without signing up?

Yes. Every game starts in this page and there is no account, email gate, or usage limit.

Can I download the full source code?

Yes. Each download includes the working HTML, CSS, JavaScript, README, license, and third-party notices.

How do I customize a game?

Choose the template, framework, difficulty, controls, theme, and advanced settings. The builder regenerates the preview and source files locally.

Which frameworks are supported?

You can download Vanilla, native Phaser, or native PixiJS projects. The live preview stays network-free while framework downloads include npm dependencies and matching source structure.

Will my code be uploaded?

No. Settings, edits, drafts, scores, and preview code stay on your device.

Can I use the downloaded code in my own project?

The original template code is provided under the MIT License. Keep the included license and check any third-party notices.

Why use a local server for downloaded projects?

Browsers can restrict modules and local resources opened through file://. A small static server gives you the same behavior as a normal website.

Do the games work on mobile?

Yes. All four templates include touch controls and resize inside the available screen.