In a world with , chapter 3.6.

THE PATH · SESSION 16 OF 66 · UNIT 3 · ACT I

The Arena

Build and Deploy Your Browser Game

55 min read5 diagrams4 side quests1 runnable4 live stages4,250 words12 code blocks

At a glance

PACE
Session 16 of 66 · suggested week 6. Continue when you are ready.
UNIT
Unit 3 · My Cheat Code Turned Out to Be JavaScript
ACT
Act I · Foundation
BEFORE
3.5 Casting on the World
AFTER
4.1 The Runtime Beyond the Browser
ARTIFACT
A browser game of your choice (Memory, Whack-a-Mole, Quiz, RPG Battle, or your own) in index.html + style.css + app.js, with a win, a lose, a restart, one real class, two event types, a README, and a live GitHub Pages URL
READ
55 min · 5 diagrams · 12 code blocks

The hook#

This is it. Build day. Arena day. The training arc is over and the tournament arc begins now.

Over four sessions you learned to store data, write reusable functions, build object-oriented systems, manipulate the DOM and handle events. Each of those came with a lab and a quest, and each quest was a piece of a game pretending to be something else. The Monster Manual was a set of enemies. The Quest Log was a UI with state. Today you stop pretending and forge all of it into one thing: a real, playable, deployed game. Not a tutorial. Not a code-along. Your game, your rules, your code, with a live URL that anyone on the planet can open.

Word has spread across the realm. The Grand Arena is hosting its first-ever Builder's Championship, a tournament where adventurers prove their worth not with swords, but with code. Every competitor must enter the arena with a fully functional browser game, built from scratch using nothing but the three sacred arts: HTML, CSS, and JavaScript. No enchanted frameworks. No borrowed libraries. Just raw skill and three hours in the forge.

Four sessions ago you did not know what a variable was. Today you ship a product. That is not normal progress; keep that in mind at the two-hour mark when the win check is wrong for the third time.

This session is shaped differently from the earlier sessions in this unit. The teaching part is short, about twenty minutes of reading, because the teaching is already done; what is new today is the architecture every browser game shares and the handful of utilities you have not met yet (timers, random numbers, shuffling, deployment). After that the session is three build sprints, a deploy, and playtesting. The page gives you a complete reference game you can read and run, a half-built one you finish, and a stage where the two bugs that eat project days are allowed to happen in front of you.

Every demo here runs live, as in 3.5: the HTML is a fenced-off corner of this page, Run live executes the script against it, and the console under the code keeps logging while you play. One new thing: games have timers, and a timer started by a stage keeps ticking until something stops it. On this page, pressing Run live again stops whatever the previous run left running and says so in the console. Your own page will not be so polite, which is the point of the last stage.

The idea#

Every browser game is the same five parts. Game state, a render function, event handlers, a win/lose checker and a reset. The state is an object or a set of variables that describes everything about the game at this moment: score, lives, timer, board, whose turn. render() reads that state and makes the DOM match it. Handlers respond to clicks and keys by changing state and then calling render(). The win/lose checker looks at state and decides whether the game is over. Reset puts state back to the start and renders. That is an event-driven architecture: user actions trigger events, events call handlers, handlers update state, state updates re-render the DOM.

Five boxes in a row joined by green arrows: event (click, keydown, submit, tick), handler (guard, update state, render), state (board, turn, winner, score, timer, lives), render() (renderBoard, renderScore, renderMessage) and DOM (what the player sees); a loop-back line under them reads the player sees the new screen and acts again
Fig 1 · The game loop. An event arrives, the handler guards and updates state, render() writes the DOM, the player reacts, and the loop turns again. State is the only source of truth.

Keep the picture. It answers the question that stalls most first games, "where does this code go?", every single time. A click happened: that is an event, so a handler. The handler needs to know if the game is over: that is a guard, reading state. Something changed: that is state. The screen needs to catch up: that is render(). If you find yourself writing DOM code inside a handler, stop and ask whether it belongs in render(). Almost always, it does.

The rules of the arena. These are the battle-tested practices that separate a game that works on Wednesday from one that still works when you add a feature on Friday.

Two panels of wrong and right code: rule one, the DOM is a display not a database, contrasts parseInt of scoreEl.textContent with state.score += 1 then render(); rule two, never store what you can compute, contrasts a moveCount variable with isTie computed from board.every; below, three one-line habits: constants, grouped state, cached elements
Fig 2 · The rules of the arena. Never read game data back out of the DOM, never keep a second copy of something you can compute, and name your magic numbers.

State is the single source of truth. Never use the DOM to store game data. If you are reading a score out of an element's text to do maths on it, stop: the score lives in a variable, and the DOM merely displays it. When handling an event, ask "what state changes?" first; the DOM catches up when render() runs.

Do not store what you can compute. In tic-tac-toe you do not need a moveCount to detect a tie; the board array already knows. No winner and no null squares means tie. Redundant state drifts out of sync. Computed values never lie.

Use constants for fixed values. if (numGuesses > MAX_GUESSES) reads like English and changes in one place. if (numGuesses > 5) is a magic number waiting to become a bug.

Group related state into objects. Not player1Name, player1Score, player2Name, player2Score: a players object. Fewer variables, and features like "save game" become one JSON.stringify.

Cache DOM elements you touch more than once. Query once at the top, store in a variable, reuse. Your render() runs after every move; it should not be searching the tree each time.

Every handler follows the same skeleton. Guard clause first, then update state, then render last:

JS
function handleMove(evt) {  if (winner) return;   // guard: ignore input when the game is over  // ...update all affected state...  render();             // always the last line}

Split big renders. When render() grows, break it into renderBoard(), renderScore(), renderMessage() and have render() call all three.

Three utilities you have not met. setInterval(callback, ms) calls a function again and again at a fixed interval and returns an id; clearInterval(id) stops it. That is your countdown clock and, in Whack-a-Mole, your mole scheduler. Math.random() returns a float from 0 up to but not including 1, so Math.floor(Math.random() * (max - min + 1)) + min is a random integer from min to max inclusive; the + 1 is the part people forget, and without it the top value never appears. The Fisher-Yates shuffle randomises an array in place by walking from the end and swapping each element with a random earlier one; it is the right way to shuffle a deck or a question list, and array.sort(() => Math.random() - 0.5) is the wrong way, because it is biased: the comparator breaks sort's rules, and the order it gives is visibly skewed.

Deployment. GitHub Pages serves static files straight from a repository. The main file must be index.html, in the root of the repo. Push, then in the repo's Settings → Pages set the source to Deploy from a branch, pick main and / (root), save, and the site is live at https://USERNAME.github.io/REPO-NAME/ within a minute or two. Every push after that redeploys automatically.

The battle plan. Staring at an empty file is where most projects die. Veterans do not start by coding; they start by planning, and the plan has a fixed order.

Left, an app.js file with five labelled comment sections: constants, state variables, cached DOM elements, event listeners, functions, each with one or two example lines; right, seven numbered steps in the proven order, from declaring state through init and render to handlers, ending with commit at every milestone in yellow
Fig 3 · The skeleton and the order. Five sections in every game file, and the seven steps that take you from an empty file to a working first screen without stalling.

Write user stories first, one per feature, from the player's side: "As a player, I want to see my remaining lives so that I know how close I am to losing." Each story is a to-do item. Wireframe the screen on paper: boxes and labels. The wireframe tells you what HTML elements you need and, more importantly, what data the game must track. Pseudocode the tricky bits, the win check and the turn flow, in plain language before any JavaScript. List your state: everything the game must remember while running. Then organise app.js into labelled sections, always the same five, in this order: constants, state variables, cached DOM elements, event listeners, functions. Then code in the proven order: declare (but do not initialise) the state variables; write init(), which sets every state value and ends by calling render(); stub render(); call init() so there is a starting screen; register the listeners; write each handler as guard → update → render(); one user story at a time. Commit at every milestone. Board renders? Commit. Win check works? Commit. Your history should tell the story of the build.

The demonstration#

Four stages and one worker demo. The first stage is the architecture in miniature: a complete game in forty lines with the five labelled sections. The second is the same architecture written as a class, with a real timer. The third is a full reference game you can read top to bottom. The worker demo proves the utilities. The fourth stage is saved for Where it breaks.

The stage stylesheet gains a few classes today, for game pieces:

CSS
/* additions to stage.css for boards */.square     { width: 52px; height: 52px; justify-content: center; padding: 0; font: 600 22px/1 monospace; }button.lit  { background: rgb(232 216 0 / .2); box-shadow: 0 0 0 1px #E8D800; color: #E8D800; }button.win  { background: rgb(0 168 104 / .2); box-shadow: 0 0 0 1px #00A868; color: #3DD68C; }.win        { color: #3DD68C; }   .lose { color: #F03038; }.log        { max-height: 168px; overflow: auto; }

1. The skeleton, as a game#

Guess the Rune: six buttons, one secret, three guesses. Small enough to read in one go, complete enough to have every one of the five parts. Read the comment headers first, then the functions in the order the plan says to write them.

HTMLskeleton.html
<h3>Guess the Rune</h3><p id="gr-message" class="muted">One of these six runes opens the door. Three guesses.</p><div id="gr-board" class="row">  <button data-idx="0">Fire</button>  <button data-idx="1">Water</button>  <button data-idx="2">Earth</button>  <button data-idx="3">Air</button>  <button data-idx="4">Light</button>  <button data-idx="5">Void</button></div><div class="row">  <span id="gr-guesses" class="counter">0 / 3 guesses</span>  <button id="gr-reset">Reset</button></div>
Stageskeleton.html, renderednot run yet

Guess the Rune

One of these six runes opens the door. Three guesses.

0 / 3 guesses
JSskeleton.js
/*----- constants -----*/const RUNES = ["Fire", "Water", "Earth", "Air", "Light", "Void"];const MAX_GUESSES = 3; /*----- state variables -----*/let secret;    // index into RUNESlet guesses;   // indexes already triedlet outcome;   // null while playing, then "win" or "lose" /*----- cached DOM elements -----*/const boardEl = document.getElementById("gr-board");const buttons = Array.from(boardEl.querySelectorAll("button[data-idx]"));const messageEl = document.getElementById("gr-message");const guessesEl = document.getElementById("gr-guesses");const resetBtn = document.getElementById("gr-reset"); /*----- event listeners -----*/boardEl.addEventListener("click", handleGuess);   // one listener for six buttons: delegationresetBtn.addEventListener("click", init);          // reset is just init again /*----- functions -----*/init(); function init() {  secret = Math.floor(Math.random() * RUNES.length);  guesses = [];  outcome = null;  console.log(`init → the secret is RUNES[${secret}] = ${RUNES[secret]}. State knows; the screen shows only what render() decides.`);  render();} function render() {  renderBoard();  renderMessage();  renderCounter();} function renderBoard() {  buttons.forEach((btn, idx) => {    btn.classList.toggle("completed", guesses.includes(idx) && idx !== secret);    btn.classList.toggle("win", outcome !== null && idx === secret);    btn.disabled = outcome !== null;  });} function renderMessage() {  if (outcome === "win") messageEl.textContent = `${RUNES[secret]}! The door opens. Reset to play again.`;  else if (outcome === "lose") messageEl.textContent = `Out of guesses. It was ${RUNES[secret]}. Reset to try again.`;  else messageEl.textContent = guesses.length ? "Not that one. Guess again." : "One of these six runes opens the door. Three guesses.";  messageEl.classList.toggle("win", outcome === "win");  messageEl.classList.toggle("lose", outcome === "lose");} function renderCounter() {  guessesEl.textContent = `${guesses.length} / ${MAX_GUESSES} guesses`;} function handleGuess(evt) {  const btn = evt.target.closest("button[data-idx]");  if (!btn) return;                    // guard: a click on the gap between buttons  if (outcome !== null) return;        // guard: the game is over, ignore input  const idx = Number(btn.dataset.idx);  if (guesses.includes(idx)) return;   // guard: already tried that one  guesses.push(idx);                   // update state  if (idx === secret) outcome = "win";  else if (guesses.length >= MAX_GUESSES) outcome = "lose";  console.log(`guess ${RUNES[idx]} → state:`, { guesses: guesses.map((i) => RUNES[i]), outcome });  render();                            // always the last line} console.log("ok: skeleton");

Play it once to a win and once to a loss, then read the console. The first line gives the secret away on purpose: the state knows the answer from the first millisecond, and the screen shows only what render() chooses to show. That gap, between what the game knows and what it displays, is the whole architecture. Notice that handleGuess never touches a class or a text node. It checks three guards, changes two variables, and calls render(). Notice that init is the reset: there is no separate reset function because resetting is initialising. And notice how little renderBoard has to think: it does not know which button was just clicked; it simply makes every button match the state, every time. That is slower than updating one button, and for nine or sixteen or a hundred elements it does not matter at all, while the simplicity matters enormously.

2. The class shape, with a timer#

Stage 2 is the same architecture written as a class, the shape to copy for Option B or D. Dungeon Whacker: nine holes, a ten-second round, a mole that moves every second. This is the shape to copy for anything with a clock.

HTMLarena-timer.html
<h3>Dungeon Whacker</h3><div class="row">  <button id="wm-start">Start</button>  <span id="wm-timer" class="counter">10 s</span>  <span id="wm-score" class="counter">score 0</span>  <span id="wm-best" class="muted">best 0</span></div><div id="wm-holes" class="row">  <button class="square" data-hole="0">·</button>  <button class="square" data-hole="1">·</button>  <button class="square" data-hole="2">·</button>  <button class="square" data-hole="3">·</button>  <button class="square" data-hole="4">·</button>  <button class="square" data-hole="5">·</button>  <button class="square" data-hole="6">·</button>  <button class="square" data-hole="7">·</button>  <button class="square" data-hole="8">·</button></div><p id="wm-message" class="muted">Press Start. The mole lights one hole every second; hit it.</p>
Stagearena-timer.html, renderednot run yet

Dungeon Whacker

10 s score 0 best 0

Press Start. The mole lights one hole every second; hit it.

JSarena-timer.js
/*----- constants -----*/const HOLES = 9;const ROUND_SECONDS = 10; /*----- utilities -----*/function randomInt(min, max) {  return Math.floor(Math.random() * (max - min + 1)) + min;   // min … max, both inclusive} /*----- the game -----*/class Game {  constructor() {    // state    this.score = 0;    this.best = 0;    this.timeLeft = ROUND_SECONDS;    this.activeHole = null;    this.isPlaying = false;    this.intervalId = null;              // the timer's id lives in state, so endGame() can find it    // cached DOM elements    this.startBtn = document.getElementById("wm-start");    this.timerEl = document.getElementById("wm-timer");    this.scoreEl = document.getElementById("wm-score");    this.bestEl = document.getElementById("wm-best");    this.holesEl = document.getElementById("wm-holes");    this.holeEls = Array.from(this.holesEl.querySelectorAll("button[data-hole]"));    this.messageEl = document.getElementById("wm-message");    // event listeners (arrow functions keep `this` as the game, see 3.4)    this.startBtn.addEventListener("click", () => this.start());    this.holesEl.addEventListener("click", (evt) => this.handleWhack(evt));    this.render();  }   start() {    if (this.isPlaying) { console.warn("start() ignored: a round is already running"); return; }  // guard    clearInterval(this.intervalId);     // never two timers, even if something went wrong before    this.score = 0;    this.timeLeft = ROUND_SECONDS;    this.isPlaying = true;    this.moveMole();    this.intervalId = setInterval(() => this.tick(), 1000);    console.log(`round started, interval id ${this.intervalId}`);    this.render();  }   tick() {    this.timeLeft -= 1;    if (this.timeLeft <= 0) { this.endGame(); return; }    this.moveMole();    this.render();  }   moveMole() {    let next;    do { next = randomInt(0, HOLES - 1); } while (next === this.activeHole);   // always a different hole    this.activeHole = next;  }   handleWhack(evt) {    const hole = evt.target.closest("button[data-hole]");    if (!hole || !this.isPlaying) return;                       // guard    const idx = Number(hole.dataset.hole);    if (idx === this.activeHole) {      this.score += 1;                                          // update state      this.moveMole();      console.log(`hit! score ${this.score}`);    } else {      console.log(`miss: hole ${idx}, the mole is in ${this.activeHole}`);    }    this.render();                                              // last line  }   endGame() {    clearInterval(this.intervalId);     // the line everyone forgets    this.intervalId = null;    this.isPlaying = false;    this.activeHole = null;    this.best = Math.max(this.best, this.score);    console.log(`time's up: ${this.score} whacked, best ${this.best}`);    this.render();  }   render() {    this.timerEl.textContent = `${this.timeLeft} s`;    this.scoreEl.textContent = `score ${this.score}`;    this.bestEl.textContent = `best ${this.best}`;    this.holeEls.forEach((el, idx) => {      const up = this.isPlaying && idx === this.activeHole;      el.textContent = up ? "M" : "·";      el.classList.toggle("lit", up);    });    this.startBtn.textContent = this.isPlaying ? "Running…" : this.best > 0 ? "Play again" : "Start";    if (this.isPlaying) this.messageEl.textContent = "Hit the lit hole.";    else if (this.timeLeft === 0) this.messageEl.textContent = `Time's up. ${this.score} whacked. Best ${this.best}.`;    else this.messageEl.textContent = "Press Start. The mole lights one hole every second; hit it.";  }} const game = new Game();console.log("ok: arena-timer");

Start a round and watch the console count hits and misses while the clock runs down. Then press Start again mid-round and read the warning: the guard refused. Three things in this class are worth stealing exactly as written. The interval's id is state (this.intervalId), so endGame() can find and clear it; a timer whose id you threw away cannot be stopped. start() calls clearInterval before setInterval, so even if a bug somewhere left a timer running, starting a round cannot create a second one. And the listeners use arrow functions, () => this.start(), because a method handed straight to addEventListener loses its this, which was Rule 2 of this in session 3.4; here is where it bites in practice.

3. The reference game: Tic-Tac-Toe#

A whole game, in the labelled-sections style, that you can read in five minutes and that passes the architecture must-haves on the project checklist (state in JavaScript, DOM manipulation, win, lose, restart); your game must add a class, a second event type, a phone layout and a deploy. It is also where "do not store what you can compute" stops being a slogan: there is no moveCount, and the tie is a one-liner.

HTMLtictactoe.html
<h3>Tic-Tac-Toe</h3><p id="ttt-message" class="counter">X's turn</p><div id="ttt-board">  <div class="row">    <button class="square muted" data-idx="0">·</button>    <button class="square muted" data-idx="1">·</button>    <button class="square muted" data-idx="2">·</button>  </div>  <div class="row">    <button class="square muted" data-idx="3">·</button>    <button class="square muted" data-idx="4">·</button>    <button class="square muted" data-idx="5">·</button>  </div>  <div class="row">    <button class="square muted" data-idx="6">·</button>    <button class="square muted" data-idx="7">·</button>    <button class="square muted" data-idx="8">·</button>  </div></div><div class="row">  <button id="ttt-replay">Play again</button>  <span id="ttt-record" class="muted">X 0 · O 0 · ties 0</span></div>
Stagetictactoe.html, renderednot run yet

Tic-Tac-Toe

X's turn

X 0 · O 0 · ties 0
JStictactoe.js
/*----- constants -----*/const MARKS = { 1: "X", "-1": "O" };const WIN_LINES = [  [0, 1, 2], [3, 4, 5], [6, 7, 8],   // rows  [0, 3, 6], [1, 4, 7], [2, 5, 8],   // columns  [0, 4, 8], [2, 4, 6],              // diagonals]; /*----- state variables -----*/let board;    // 9 squares: 1 (X), -1 (O) or nulllet turn;     // 1 or -1let winner;   // null while playing; 1 or -1 for a win; "T" for a tielet record;   // { "1": wins, "-1": wins, T: ties }, kept across games on purpose /*----- cached DOM elements -----*/const boardEl = document.getElementById("ttt-board");const squareEls = Array.from(boardEl.querySelectorAll("button[data-idx]"));const messageEl = document.getElementById("ttt-message");const recordEl = document.getElementById("ttt-record");const replayBtn = document.getElementById("ttt-replay"); /*----- event listeners -----*/boardEl.addEventListener("click", handleMove);replayBtn.addEventListener("click", init); /*----- functions -----*/record = { 1: 0, "-1": 0, T: 0 };init(); function init() {  board = Array(9).fill(null);  turn = 1;  winner = null;  render();} function render() {  renderBoard();  renderMessage();  renderRecord();} function renderBoard() {  const line = winningLine();  board.forEach((square, idx) => {    const el = squareEls[idx];    el.textContent = square === null ? "·" : MARKS[square];    el.classList.toggle("muted", square === null);    el.classList.toggle("win", line !== null && line.includes(idx));  });} function renderMessage() {  if (winner === "T") messageEl.textContent = "Tie game. Play again?";  else if (winner) messageEl.textContent = `${MARKS[winner]} wins!`;  else messageEl.textContent = `${MARKS[turn]}'s turn`;} function renderRecord() {  recordEl.textContent = `X ${record[1]} · O ${record[-1]} · ties ${record.T}`;} function handleMove(evt) {  const el = evt.target.closest("button[data-idx]");  if (!el) return;                                       // guard: not a square  const idx = Number(el.dataset.idx);  if (winner !== null || board[idx] !== null) return;    // guard: game over, or square taken  board[idx] = turn;                                     // update state  winner = getWinner();  if (winner !== null) record[winner] += 1;  else turn *= -1;  console.log(`${MARKS[board[idx]]} takes ${idx} →`, board.map((s) => (s === null ? "." : MARKS[s])).join(""), "winner:", winner);  render();                                              // last line} function winningLine() {  // A line of three X is 3; three O is -3. Empty squares count as 0.  return WIN_LINES.find((line) => Math.abs(line.reduce((sum, i) => sum + (board[i] || 0), 0)) === 3) || null;} function getWinner() {  if (winningLine()) return turn;                        // only the player who just moved can have won  return board.includes(null) ? null : "T";              // the tie is computed, never stored} console.log("ok: tictactoe");

Play a game to a win and one to a tie, then read getWinner. There is no counter of moves anywhere in the file; "no winner and no null squares" is the tie, read straight from the board. Now look at record. It is stored, and that is correct: the record across games cannot be computed from the current board, so it has to be remembered. The rule is not "store nothing"; the rule is "store only what you cannot compute". Then look at how X and O are encoded: 1 and -1, so a winning line sums to 3 or -3. That trick turns eight if statements into one reduce, and it is the kind of representation choice that makes the rest of a game easy or hard; spend five minutes on it before you write the handlers for your own.

4. The utilities, proven#

Random numbers and shuffles are easy to get almost right. This runs in the worker and checks the two classic mistakes.

JSutils.js
// Random integer between min and max, both inclusive.function randomInt(min, max) {  return Math.floor(Math.random() * (max - min + 1)) + min;} // Fisher-Yates shuffle: copies the array, then swaps from the end down.function shuffle(array) {  const arr = [...array];                       // copy, so the caller's array is untouched  for (let i = arr.length - 1; i > 0; i--) {    const j = Math.floor(Math.random() * (i + 1));   // 0 … i, inclusive    [arr[i], arr[j]] = [arr[j], arr[i]];  }  return arr;} // --- randomInt: every value in range, nothing outside it ---const seen = new Set();for (let n = 0; n < 10000; n++) {  const r = randomInt(1, 6);  if (r < 1 || r > 6 || !Number.isInteger(r)) throw new Error(`randomInt left the range: ${r}`);  seen.add(r);}console.log("10,000 dice rolls, values seen:", [...seen].sort().join(" "));// The classic off-by-one: forget the +1 and the top value never appears.const noSix = new Set();for (let n = 0; n < 10000; n++) noSix.add(Math.floor(Math.random() * (6 - 1)) + 1);console.log("without the +1, values seen:   ", [...noSix].sort().join(" "), "← 6 never comes up"); // --- shuffle: same cards, new order, original untouched ---const deck = ["A", "A", "B", "B", "C", "C", "D", "D"];const dealt = shuffle(deck);console.log("deck: ", deck.join(" "));console.log("dealt:", dealt.join(" "));if (deck.join("") !== "AABBCCDD") throw new Error("shuffle mutated the original");if ([...dealt].sort().join("") !== "AABBCCDD") throw new Error("shuffle lost or invented a card");let moved = 0;for (let n = 0; n < 1000; n++) if (shuffle(deck).join("") !== deck.join("")) moved++;console.log(`shuffled 1,000 times: ${moved} orders differed from the original`); // --- Math.random itself: 0 ≤ x < 1, never 1 ---let max = 0;for (let n = 0; n < 100000; n++) max = Math.max(max, Math.random());console.log("largest of 100,000 Math.random() calls:", max.toFixed(6), "(always below 1)"); console.log("ok: utils");

The second line of output is the one to remember: drop the + 1 and a six-sided die never rolls six, and nothing crashes to tell you. The shuffle copies first ([...array]) so the original deck is untouched, which matters when you want to shuffle the same question list again for the next playthrough. If shuffle ever lost or duplicated a card, the throw would have ended the run before the ok line.

Where it breaks#

Your turn#

The quest#

The crowd gathers. The guild masters watch from the royal box. The announcer reads your name. This is your moment. Build something the crowd will remember. Build something that works. Build something you are proud of. Then deploy it to the Crystal Network so the entire realm can play it. Win or lose, every competitor who ships a working game earns the title of Builder. But those who go above and beyond? They earn Legendary status.

The Arena Championship: Build Your Browser Game. This is the Unit 3 project. Pick one arena and build it from scratch; no frameworks, no jQuery, no libraries.

Four cards lettered A to D: Memory Card Game with class Card, Whack-a-Mole with class Mole, Quiz Game with class Quiz, RPG Battle with Character, Player, Enemy and Battle; each card lists the state it tracks, a green win condition, a red lose condition and the utilities it leans on, such as shuffle, setInterval or Math.max
Fig 5 · Four arenas. Each option is the same loop with a different state list; the card tells you the class you will write, what you must track, and how the game is won and lost.

  • Option A, Memory Card Game (The Matching Spell). A grid of 12 to 20 face-down cards (6 to 10 pairs). Click two to flip them; after a short delay they stay up if they match and flip back if they do not. Track attempts and time. Win when every pair is matched. Suggested class: Card with value, isFlipped, isMatched, element. You will want shuffle and a setTimeout for the flip-back. A finished example to read after you try: the game/ folder of https://github.com/arthurbernierjr/dtc-landing.
  • Option B, Whack-a-Mole (Dungeon Whacker). 9 to 12 holes; moles appear at random for one to two seconds and come faster as time goes on; a 30 to 60 second limit; score and high score. Suggested class: Mole with holeIndex, isVisible, speed, element. You will want setInterval, clearInterval and randomInt, and you have the shape in stage 2.
  • Option C, Quiz Game (The Sage's Trial). At least ten questions with four choices each, shuffled every playthrough; a correct answer gives +10 and green feedback; a wrong answer costs one of three lives and shows red. Win by finishing the questions, lose by running out of lives, then show the final score and a play-again. Suggested class: Quiz with questions, currentIndex, score, lives.
  • Option D, Simple RPG Battle (Arena Combat Simulator). Create a character (name, class); fight three to five enemies in turns; each turn choose normal attack, special, defend or potion; enemies pick attacks at random; damage is attack minus defence (never below zero); potions are limited; a battle log shows every action. Win by defeating every enemy; lose when HP hits zero. Suggested classes: Character as the base, Player and Enemy extending it, and Battle. Your Monster Manual from 3.4 is most of this.
  • Option E, your own idea, if something fires you up more than these; it only has to meet the technical requirements below at the same size.

Must have (required to pass): HTML, CSS and vanilla JavaScript only; at least one ES6 class used meaningfully; DOM manipulation that creates, modifies or removes elements; at least two different event types (click, submit, keydown…); game state tracked in JavaScript (score, turns, lives, timer or equivalent); a win condition and a lose condition; restart without refreshing; a responsive layout playable on a phone; deployed on GitHub Pages; clean, readable code with comments on the key logic. Should have: styled beyond browser defaults; feedback for every action; smooth play with no layout jumps; a README with title, description, how to play and the live link; no major bugs. Could have: sound, CSS animations, difficulty levels, a localStorage high score, a leaderboard, touch controls, an instruction screen, a dark-mode toggle.

How to score yourself. Before you call it done, score the game against this list. 70 points for the boss clear: loads and runs (10), win and lose conditions (10), restart without refresh (5), DOM manipulation (10), event handling with delegation where it fits (10), a meaningful class (10), state management (10), deployment (5). 20 points of epic loot: visual design, user feedback, code quality, README, five each. 10 points of legendary status: advanced features and professional polish, five each. 90 and up is Legendary (S-Rank); 80 to 89 Epic (A); 70 to 79 Boss Clear (B); 60 to 69 Approaching (C); below that, retry. Read the list before you start, not after: half of it is architecture you get for free by following the skeleton.

Your evidence. A public repo named for the game (memory-game, not project1); the GitHub Pages URL, verified in an incognito window; and a README.md with the game title, a one- or two-sentence description, a screenshot or GIF, the live link, the technologies used and how to play. Both URLs go in the README, which is the one place anyone, including you in six months, will look. Finish it before you start 4.1.

Side quests#

Four pages in the library go deeper than one session can. Read them in this order; each is a short reference sheet with code that runs in the page.

Side quests from this session

  • Stateidle → playing → over is a state machine, and every `if (winner) return` guard is one of its transitions; this sheet shows each state as an object, which is where your game goes when the ifs pile up.
    Design Patterns
  • Template MethodA `Game` class whose `start()` runs the fixed sequence and whose `buildBoard()` is filled in by each specific game is the Template Method; this sheet names the parts so you can explain your own architecture.
    Design Patterns
  • Numbers & Math`randomInt` is `Math.floor(Math.random() * (max - min + 1)) + min`, and every off-by-one in a dice roll or a timer lives in that line; this sheet covers floor versus round versus trunc, parsing, and why `parseInt` of a label is NaN.
    Languages
  • Grit: The Long ReturnProject day is where a copied demonstration stops being enough. This page says copying is the third step. The fourth is your own idea, and it will not work the first time. Stay with that hour.
    Mindset

Before next session#

Before Unit 4 begins, in this order.

First, fix what playtesting found. Every note someone gave you is a bug report from a real user; that is the most valuable thing you will get this week.

Second, add one feature you did not get to: a difficulty selector, a high-score display with localStorage, a sound, an animation on the win. One, finished, beats three half-done.

Third, make it playable on a phone. Open the live URL on your own phone. Buttons under 44 pixels, text that needs zooming, a board that scrolls sideways: fix those. The rubric says responsive, and the person you send the link to will open it on a phone.

Fourth, write the README: title, description, how to play, technologies used, the live link, a screenshot or GIF. It is five points on the rubric and the first thing anyone, including a future employer, sees in the repo.

Fifth, push, verify in an incognito window, and check that both URLs are in the README.

Then rest for a day. Unit 4 is a new arc, and 4.1, The Runtime Beyond the Browser, is where you leave the browser for the first time and run JavaScript on a server with Node, npm and Express. Nothing to read ahead; arrive with a game you can show. Nothing will still be running: the game is static files on GitHub Pages, and there is no server to stop.

You came into Unit 3 not knowing what a variable was. You leave it with a game on the internet. Builder.

See also