π§© Weekend Project: JavaScript Fundamentals
This is where the week's pieces snap together. Over a weekend you'll build a complete, playable quiz game in plain JavaScript β no frameworks, no libraries β and prove to yourself that variables, scope, control flow, and functions are enough to ship something real. We'll build it in milestones, using George Polya's four-step problem-solving method to keep the work honest and organized.
π― Learning Objectives
By the end of this project, you will be able to:
- Apply Polya's 4-step framework (understand β plan β execute β review) to a self-directed build
- Structure a small app as a data layer, a state object, and single-purpose functions
- Drive game flow with control flow β conditionals, loops, and array methods
- Use variable scope deliberately, keeping shared state in one place instead of scattered globals
- Render and read the DOM to make the game actually playable in a browser
- Self-assess your work against a concrete "what good looks like" rubric
Estimated Time: 3β6 hours across a weekend β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a working browser quiz game through five checked milestones, then extend it with a feature of your own.
In This Lesson
What You're Building
Your target is an interactive quiz game that runs in the browser. A player picks a category, answers a series of questions, gets instant feedback on each one, and sees a final score with a performance summary. It's small enough to finish in a weekend, but big enough to force every fundamental you learned this week to earn its place.
Here's the important reframe: this is not a tutorial you copy line by line. It's a guided build. Each milestone gives you a goal, the shape of the code, and a checkpoint to verify before moving on. You write the code. When you get stuck, the reference snippets are there β but reach for them second, not first.
π The three jobs of the app
Data: the questions themselves, plus which category and difficulty each belongs to.
State: the moving parts of a game in progress β current question, score, and whether the game is over.
Behavior: the functions that read state, change it, and reflect it back to the player on screen.
π‘ Why a quiz game? A quiz is a loop over a list with a score. That's the skeleton of countless real programs β checkout flows, onboarding wizards, survey tools, flashcard apps. Learn to build one cleanly and you've learned a pattern you'll reuse for years.
Polya's Framework as a Build Plan
George Polya was a mathematician whose 1945 book How to Solve It distilled problem-solving into four steps. They map perfectly onto software: rushing to code (step 3) before understanding (step 1) and planning (step 2) is the number-one reason beginner projects sprawl into a mess.
Applied to this weekend:
- Understand β What exactly is a "quiz game"? Inputs are the player's category choice and their answers; outputs are questions, feedback, and a final score. Constraints: at least three categories, two question types, and a running score.
- Devise a plan β Split the work into the five milestones below. Decide your data shape before writing a single function.
- Execute β Build milestone by milestone, checking each one works before starting the next.
- Review & extend β Test edge cases, then add one improvement that's genuinely yours.
β οΈ The most common trap
Beginners skip steps 1 and 2 and start typing. Fifteen functions later, nothing connects. Spend the first 20 minutes deciding your data shape and your function list on paper. That plan is what turns a weekend of frustration into a weekend of progress.
The Milestone Roadmap
Each milestone is a checkpoint you can run and verify. Don't move on until the current one works β a half-finished feature stacked on another half-finished feature is impossible to debug.
π‘ Setup: three files
Create a folder and three files: index.html (structure), styles.css (optional look), and quiz.js (all your logic). Open index.html directly in your browser β no build tools, no server needed. Refresh after each change.
Milestone 1 β Data & State
Goal: define the questions your game asks and the single object that tracks a game in progress. Nothing plays yet β you're laying the foundation.
Design the question shape first
Every question is an object with the same fields. Deciding these fields up front is the single most important design choice in the whole project β everything else reads from this shape.
// quiz.js
// --- DATA: the questions ---
const QUESTIONS = [
{
category: 'JavaScript',
difficulty: 'easy',
prompt: 'Which keyword declares a block-scoped variable that can be reassigned?',
options: ['var', 'let', 'const', 'def'],
answer: 'let',
explanation: '`let` is block-scoped and reassignable. `const` is block-scoped but cannot be reassigned.',
points: 10,
},
{
category: 'JavaScript',
difficulty: 'easy',
prompt: 'True or false: JavaScript and Java are the same language.',
options: ['True', 'False'],
answer: 'False',
explanation: 'They are unrelated languages with different designs; the shared name is a historical marketing choice.',
points: 10,
},
{
category: 'JavaScript',
difficulty: 'medium',
prompt: 'Which array method adds an element to the END of an array?',
options: ['push()', 'unshift()', 'pop()', 'shift()'],
answer: 'push()',
explanation: '`push()` appends to the end; `unshift()` adds to the start; `pop()`/`shift()` remove.',
points: 20,
},
{
category: 'HTML',
difficulty: 'easy',
prompt: 'Which tag creates a hyperlink?',
options: ['<link>', '<a>', '<href>', '<nav>'],
answer: '<a>',
explanation: 'The <a> (anchor) tag, with an href attribute, creates a hyperlink.',
points: 10,
},
{
category: 'CSS',
difficulty: 'medium',
prompt: 'Which value of the display property enables a flexible box layout?',
options: ['block', 'grid', 'flex', 'inline'],
answer: 'flex',
explanation: 'Setting `display: flex` turns an element into a flex container.',
points: 20,
},
];
β Why one shape for every question?
If every question has the same keys, one function can render any question. The moment two questions have different shapes, you need special-case code β and special cases are where bugs live. Consistency in your data is what keeps your functions simple.
Create the state object
A game in progress has moving parts: which question we're on, the score, and whether it's over. Bundle them into one object rather than scattering loose global variables. This is a deliberate scope decision β one shared source of truth.
// --- STATE: the game in progress ---
let state = {
questions: [], // the questions for THIS game (filtered + shuffled)
index: 0, // which question we're showing
score: 0, // running points
correct: 0, // count of right answers
answered: false, // has the player answered the current question?
over: false, // is the game finished?
};
π Checkpoint 1
Open the browser console (F12) and type QUESTIONS.length. You should see 5. Type state and confirm the object prints. If both work, your data layer is solid. Move on only now.
Milestone 2 β Game Engine
Goal: the pure logic of the game, with no screen output yet. These functions read and change state. You'll test them from the console before touching the DOM β separating logic from display makes both easier to debug.
Start, shuffle, and advance
// --- ENGINE ---
// Fisher-Yates shuffle so each game differs. Returns a NEW array.
function shuffle(list) {
const copy = [...list];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]]; // swap via destructuring
}
return copy;
}
// Unique category names, derived from the data (never hard-coded).
function getCategories() {
return [...new Set(QUESTIONS.map(q => q.category))];
}
// Begin a game. Pass a category, or null for "all categories".
function startGame(category = null) {
const pool = category
? QUESTIONS.filter(q => q.category === category)
: QUESTIONS;
state = {
questions: shuffle(pool),
index: 0,
score: 0,
correct: 0,
answered: false,
over: false,
};
}
// The question currently on screen.
function currentQuestion() {
return state.questions[state.index];
}
// Advance to the next question, or end the game.
function advance() {
if (state.index < state.questions.length - 1) {
state.index++;
state.answered = false;
} else {
state.over = true;
}
}
Grade an answer
This is the heart of the loop: compare the player's choice to the correct answer, update the score, and guard against double-answering with the answered flag.
// Returns true if the answer was correct. Ignores repeat answers.
function submitAnswer(choice) {
if (state.answered || state.over) return false;
const q = currentQuestion();
const isCorrect = choice === q.answer;
if (isCorrect) {
state.score += q.points;
state.correct++;
}
state.answered = true;
return isCorrect;
}
β οΈ The answered guard matters
Without it, a fast player could click twice and score the same question twice. A single boolean flag in state prevents a whole class of cheating and double-counting bugs. Small guards, big payoff.
π Checkpoint 2 β test in the console
You can play the entire game with no UI yet:
startGame('JavaScript');
currentQuestion().prompt; // shows a JS question
submitAnswer(currentQuestion().answer); // true β correct!
state.score; // 10 or 20
advance();
state.index; // 1
If the score moves and advance() steps forward, your engine is correct. That's a real, testable milestone β reached before a single pixel was drawn.
Milestone 3 β Render to the DOM
Goal: make it playable. Add the HTML shell, then write display functions that read state and paint the screen. The engine already works β this milestone only shows what it's doing.
The HTML shell
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Quiz</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main id="app">
<div id="category-screen"></div>
<div id="quiz-screen" hidden>
<p id="progress"></p>
<h2 id="prompt"></h2>
<div id="options"></div>
<p id="feedback"></p>
<button id="next-btn" hidden>Next β</button>
</div>
<div id="results-screen" hidden></div>
</main>
<script src="quiz.js"></script>
</body>
</html>
Render functions
Each function grabs an element, reads state, and updates the page. Note the pattern: the DOM never stores game truth β it only reflects state.
// --- RENDER ---
function renderCategories() {
const screen = document.getElementById('category-screen');
screen.innerHTML = '<h1>Pick a category</h1>';
[...getCategories(), 'All'].forEach(cat => {
const btn = document.createElement('button');
btn.textContent = cat;
btn.addEventListener('click', () => {
startGame(cat === 'All' ? null : cat);
show('quiz-screen');
renderQuestion();
});
screen.appendChild(btn);
});
}
function renderQuestion() {
const q = currentQuestion();
document.getElementById('progress').textContent =
`Question ${state.index + 1} of ${state.questions.length} Β· ${q.difficulty}`;
document.getElementById('prompt').innerHTML = q.prompt;
document.getElementById('feedback').textContent = '';
document.getElementById('next-btn').hidden = true;
const box = document.getElementById('options');
box.innerHTML = '';
q.options.forEach(opt => {
const btn = document.createElement('button');
btn.className = 'option';
btn.innerHTML = opt;
btn.addEventListener('click', () => handleChoice(opt, q.answer));
box.appendChild(btn);
});
}
function handleChoice(choice, answer) {
const wasCorrect = submitAnswer(choice);
const q = currentQuestion();
const fb = document.getElementById('feedback');
fb.textContent = (wasCorrect ? 'β
Correct! ' : 'β Not quite. ') + q.explanation;
// Lock the buttons so you can't answer twice.
document.querySelectorAll('.option').forEach(b => b.disabled = true);
document.getElementById('next-btn').hidden = false;
}
// Show one screen, hide the others.
function show(screenId) {
['category-screen', 'quiz-screen', 'results-screen'].forEach(id => {
document.getElementById(id).hidden = (id !== screenId);
});
}
// Wire up the Next button and kick things off.
document.getElementById('next-btn').addEventListener('click', () => {
advance();
if (state.over) { renderResults(); show('results-screen'); }
else renderQuestion();
});
renderCategories(); // start the app
π Checkpoint 3
Open index.html. You should see category buttons, be able to pick one, answer a question, see feedback, and click Next. It's a game now. (renderResults comes in the next milestone β clicking Next on the last question will error until you add it. That's expected.)
Milestone 4 β Scoring & Results
Goal: close the loop with a results screen that reports score, accuracy, and a performance message. This is where control flow shines β an if/else if ladder turns a raw percentage into human feedback.
// --- RESULTS ---
function performanceMessage(percent) {
if (percent >= 90) return 'Outstanding β you know this cold!';
if (percent >= 70) return 'Great work, solid understanding.';
if (percent >= 50) return 'Good effort β keep practicing.';
return 'Early days β review and try again!';
}
function renderResults() {
const total = state.questions.length;
const percent = Math.round((state.correct / total) * 100);
document.getElementById('results-screen').innerHTML = `
<h1>Game over</h1>
<p>Score: <strong>${state.score}</strong> points</p>
<p>Correct: ${state.correct} / ${total} (${percent}%)</p>
<p>${performanceMessage(percent)}</p>
<button id="again">Play again</button>
`;
document.getElementById('again').addEventListener('click', renderCategories);
document.getElementById('again').addEventListener('click', () => show('category-screen'));
}
Example results output
Game over
Score: 40 points
Correct: 3 / 5 (60%)
Good effort β keep practicing.
[ Play again ]
π Checkpoint 4
Play a full round start to finish. Confirm the results screen shows the right totals, the percentage matches, and Play again returns you to the category picker with a fresh score. If all four are true, you have a complete, working game.
Milestone 5 β Polish & Extend
Goal: this is Polya's step 4 β review, then extend. Pick at least one extension and make it yours. This is what turns a followed tutorial into a project you can talk about.
Choose an extension
- Countdown timer β give each question 15 seconds; time-out counts as wrong.
- High score in localStorage β persist the best score across page reloads.
- More categories & questions β add your own subject (music, history, your hobby).
- Streak bonus β award extra points for consecutive correct answers.
- Difficulty filter β let the player choose easy / medium / hard.
Worked extension: a per-question timer
A timer is a great exercise in setInterval, closures, and cleanup. The trick beginners miss is clearing the interval so timers don't stack up.
let timerId = null;
function startTimer(seconds, onExpire) {
let remaining = seconds;
const label = document.getElementById('progress');
clearInterval(timerId); // cancel any previous timer
timerId = setInterval(() => {
remaining--;
label.dataset.time = `β± ${remaining}s`;
if (remaining <= 0) {
clearInterval(timerId);
onExpire(); // e.g. auto-submit a wrong answer
}
}, 1000);
}
// When rendering a question:
// startTimer(15, () => handleChoice('__timeout__', currentQuestion().answer));
// Remember to clearInterval(timerId) inside handleChoice so answering stops the clock.
Persist a high score
function saveHighScore(score) {
const best = Number(localStorage.getItem('quizHighScore')) || 0;
if (score > best) {
localStorage.setItem('quizHighScore', String(score));
return true; // new record!
}
return false;
}
// Call saveHighScore(state.score) inside renderResults().
β Refactor challenge (optional)
Once it works, try rebuilding the engine as a class QuizGame that keeps score and index as private-ish instance fields. Compare it to the plain-object version. There's no single "right" style β feeling the trade-offs is the lesson.
Completion Checklist
Tick every box before you call it done. Each one maps back to a fundamental from this module.
π Requirements checklist
- β Questions live in a data array with a consistent object shape
- β Game state is held in one object, not scattered globals
- β At least 5 single-purpose functions with clear names
- β Uses conditionals and a loop (or array method) for game logic
- β Includes at least one higher-order function or closure (e.g.
map,filter,forEach, or a callback) - β Deliberate variable scope β no accidental globals leaking out of functions
- β The game is playable in the browser start to finish
- β Score and results display correctly
- β At least one extension of your own from Milestone 5
- β A short README documenting your four Polya steps
ποΈ Your assignment
Objective: ship the quiz game above, or an equivalent app of your choice (text adventure, number-guessing game, flashcards, a small task manager) that hits every checklist item.
Deliverables:
- Your
index.html,quiz.js, and optionalstyles.css. - A
README.mdwith a short paragraph per Polya step: what you understood, how you planned, what you built, and what you'd improve. - One extension you added, named in the README.
π‘ Stuck getting started?
Don't open the code first. Open a blank note and write, in plain English: "The game shows a question, the player clicks an answer, I check it, update the score, and show the next one." That sentence is your function list β one function per verb. Now name them, then fill them in.
β Reference solution structure
A clean solution has three commented regions in quiz.js: DATA (the QUESTIONS array), STATE + ENGINE (startGame, currentQuestion, submitAnswer, advance), and RENDER (renderCategories, renderQuestion, renderResults, show). If a function does two of those jobs at once, split it. The engine functions should not touch the DOM; the render functions should not compute scores.
What Good Looks Like
Self-assessment is a skill. Grade your own project against this rubric β being honest here is worth more than any grade someone else gives you.
| Dimension | Needs work | Good | Excellent |
|---|---|---|---|
| Structure | One giant function; logic and display tangled together | Separate functions with clear names | Clean DATA / ENGINE / RENDER separation; engine never touches the DOM |
| State | Loose globals mutated from everywhere | Most state in one object | Single source of truth; DOM only reflects state, never stores it |
| Control flow | Copy-pasted branches; magic numbers | Sensible conditionals and loops | Data-driven β categories & options derived from the array, not hard-coded |
| Robustness | Breaks on double-clicks or an empty category | Handles the happy path reliably | Guards edge cases (double-answer, last question, no questions) |
| Ownership | Copied verbatim, no changes | One working extension | A thoughtful extension plus a clear README of your reasoning |
β Signs you've genuinely leveled up
You can explain why state lives in one object. You reach for filter and map without thinking. You tested your engine in the console before writing any UI. And when something broke, you found it by reading your own code β not by deleting and rewriting at random. That debugging confidence is the real prize of this weekend.
Summary & Quiz
π Key Takeaways
- Polya's four steps β understand, plan, execute, review β keep a build organized and prevent sprawl.
- Structure a small app as three jobs: data, state, and behavior, and keep them separate.
- Hold shared game state in one object; let the DOM only reflect it.
- Build in runnable milestones β verify each checkpoint before starting the next.
- Real learning shows up in the extension you add and your ability to debug your own code.
π― Quick Quiz
Question 1: Why does this project keep all game state (score, current index, game-over flag) inside a single state object?
Question 2: In Polya's framework, which mistake causes most beginner projects to sprawl into a mess?
Question 3: Why is the answered boolean flag checked at the top of submitAnswer?
π Further Reading
- MDN β JavaScript reference
- The Modern JavaScript Tutorial (javascript.info)
- Eloquent JavaScript (free online book)
- MDN β Document Object Model (DOM)
π What's Next?
You've built a real program from raw fundamentals. Next we move up a level of abstraction into objects β how to create them with literals and constructors, the topic that will let you model richer things than a flat list of questions.
π You shipped something!
A weekend ago these were separate ideas β variables, scope, control flow, functions. Now they're a game people can play. That's what "fundamentals" were for all along.