fibonacci-game.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fibonacci Forge</title>
<style>
:root {
color-scheme: dark;
--ink: #eef5f2;
--muted: #a8b8b1;
--panel: rgba(18, 25, 23, 0.88);
--line: rgba(255, 255, 255, 0.16);
--green: #62d394;
--gold: #f3c969;
--red: #ef6f6c;
--blue: #6ea8fe;
--bg: #0e1513;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
radial-gradient(circle at 16% 12%, rgba(98, 211, 148, 0.26), transparent 28%),
radial-gradient(circle at 86% 18%, rgba(243, 201, 105, 0.16), transparent 26%),
linear-gradient(135deg, #0a1111 0%, #17221d 48%, #101314 100%);
overflow-x: hidden;
}
.app {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(280px, 420px) minmax(320px, 1fr);
gap: 24px;
padding: clamp(16px, 3vw, 34px);
}
.side,
.stage {
min-width: 0;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
backdrop-filter: blur(18px);
}
.side {
border-radius: 8px;
padding: 22px;
display: flex;
flex-direction: column;
gap: 18px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.mark {
width: 46px;
height: 46px;
border-radius: 8px;
display: grid;
place-items: center;
color: #101314;
background: conic-gradient(from 210deg, var(--green), var(--gold), var(--blue), var(--green));
font-size: 24px;
font-weight: 900;
}
h1 {
margin: 0;
font-size: clamp(28px, 4vw, 48px);
line-height: 0.96;
letter-spacing: 0;
}
.lede {
margin: 0;
color: var(--muted);
line-height: 1.55;
max-width: 56ch;
}
.rule {
border-left: 4px solid var(--green);
background: rgba(98, 211, 148, 0.1);
padding: 12px 14px;
line-height: 1.45;
color: #dcebe5;
overflow-wrap: anywhere;
}
.formula {
display: flex;
align-items: center;
justify-content: center;
min-height: 56px;
border: 1px solid var(--line);
border-radius: 8px;
font-size: clamp(20px, 2.8vw, 28px);
font-weight: 800;
color: var(--gold);
background: rgba(255, 255, 255, 0.05);
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.stat {
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px 10px;
background: rgba(255, 255, 255, 0.045);
min-width: 0;
}
.stat span {
display: block;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
}
.stat strong {
display: block;
margin-top: 4px;
font-size: 24px;
}
.sequence {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 90px;
align-content: flex-start;
}
.chip {
min-width: 42px;
height: 42px;
display: grid;
place-items: center;
border: 1px solid rgba(98, 211, 148, 0.5);
border-radius: 8px;
background: rgba(98, 211, 148, 0.14);
color: #eafaf1;
font-weight: 800;
transition: transform 160ms ease, border-color 160ms ease;
}
.chip.new {
transform: translateY(-4px);
border-color: var(--gold);
}
.stage {
border-radius: 8px;
position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 680px;
overflow: hidden;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.035);
}
.prompt {
min-width: 0;
}
.prompt span {
display: block;
color: var(--muted);
font-size: 13px;
}
.prompt strong {
display: block;
margin-top: 2px;
font-size: clamp(20px, 3vw, 34px);
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
button {
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
min-height: 42px;
padding: 0 14px;
color: var(--ink);
background: rgba(255, 255, 255, 0.08);
font: inherit;
font-weight: 800;
cursor: pointer;
transition: transform 140ms ease, background 140ms ease, border-color 140ms ease;
}
button:hover,
button:focus-visible {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.42);
outline: none;
}
.primary {
color: #0b1512;
border-color: transparent;
background: var(--green);
}
.playfield {
position: relative;
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(240px, 380px);
gap: 0;
min-height: 0;
}
.visual {
position: relative;
min-height: 430px;
border-right: 1px solid var(--line);
background:
linear-gradient(rgba(255,255,255,0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.045) 1px, transparent 1px);
background-size: 40px 40px;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.question {
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 18px;
}
.question h2 {
margin: 0;
font-size: clamp(24px, 3vw, 36px);
line-height: 1.08;
}
.choices {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.choice {
min-height: 72px;
font-size: clamp(20px, 4vw, 30px);
background: rgba(255, 255, 255, 0.07);
}
.choice.correct {
background: rgba(98, 211, 148, 0.32);
border-color: var(--green);
}
.choice.wrong {
background: rgba(239, 111, 108, 0.25);
border-color: var(--red);
}
.feedback {
min-height: 78px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
line-height: 1.45;
color: #dcebe5;
background: rgba(255, 255, 255, 0.045);
}
.feedback strong {
color: var(--gold);
}
.bottom {
padding: 14px 16px;
border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
color: var(--muted);
background: rgba(0, 0, 0, 0.18);
}
.meter {
flex: 1;
height: 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.meter div {
width: 0%;
height: 100%;
background: linear-gradient(90deg, var(--green), var(--gold));
transition: width 250ms ease;
}
@media (max-width: 900px) {
.app {
grid-template-columns: 1fr;
}
.stage {
min-height: auto;
}
.playfield {
grid-template-columns: 1fr;
}
.visual {
min-height: 340px;
border-right: 0;
border-bottom: 1px solid var(--line);
}
}
@media (max-width: 560px) {
.app {
padding: 12px;
}
.brand {
align-items: flex-start;
}
.topbar,
.bottom {
align-items: stretch;
flex-direction: column;
}
.actions {
justify-content: stretch;
}
.actions button {
flex: 1;
}
.stats,
.choices {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="app">
<section class="side" aria-label="How the Fibonacci sequence works">
<div class="brand">
<div class="mark" aria-hidden="true">F</div>
<div>
<h1>Fibonacci Forge</h1>
</div>
</div>
<p class="lede">
Grow the sequence by choosing the next number. Every correct answer adds one new square to the spiral.
</p>
<div class="rule">
The Fibonacci sequence starts with <strong>0</strong> and <strong>1</strong>. Each new number is made by adding the two numbers before it.
</div>
<div class="formula" id="formula">0 + 1 = ?</div>
<div class="stats">
<div class="stat">
<span>Score</span>
<strong id="score">0</strong>
</div>
<div class="stat">
<span>Streak</span>
<strong id="streak">0</strong>
</div>
<div class="stat">
<span>Round</span>
<strong id="round">1</strong>
</div>
</div>
<div class="sequence" id="sequence" aria-label="Current Fibonacci sequence"></div>
</section>
<section class="stage" aria-label="Fibonacci game">
<div class="topbar">
<div class="prompt">
<span>Find the next Fibonacci number</span>
<strong id="questionText">What comes after 0, 1?</strong>
</div>
<div class="actions">
<button type="button" id="hintBtn">Hint</button>
<button type="button" class="primary" id="resetBtn">Reset</button>
</div>
</div>
<div class="playfield">
<div class="visual">
<canvas id="spiralCanvas" width="900" height="680" aria-label="Animated Fibonacci spiral"></canvas>
</div>
<div class="question">
<h2 id="challenge">Add the last two numbers.</h2>
<div class="choices" id="choices"></div>
<div class="feedback" id="feedback">
Pick a number to see how the rule works.
</div>
</div>
</div>
<div class="bottom">
<span id="progressLabel">0 of 10 spiral squares built</span>
<div class="meter" aria-hidden="true"><div id="progressBar"></div></div>
</div>
</section>
</main>
<script>
const maxRounds = 10;
const sequenceEl = document.getElementById("sequence");
const choicesEl = document.getElementById("choices");
const feedbackEl = document.getElementById("feedback");
const formulaEl = document.getElementById("formula");
const scoreEl = document.getElementById("score");
const streakEl = document.getElementById("streak");
const roundEl = document.getElementById("round");
const questionTextEl = document.getElementById("questionText");
const challengeEl = document.getElementById("challenge");
const progressLabelEl = document.getElementById("progressLabel");
const progressBarEl = document.getElementById("progressBar");
const hintBtn = document.getElementById("hintBtn");
const resetBtn = document.getElementById("resetBtn");
const canvas = document.getElementById("spiralCanvas");
const ctx = canvas.getContext("2d");
let numbers;
let score;
let streak;
let locked;
function resetGame() {
numbers = [0, 1];
score = 0;
streak = 0;
locked = false;
feedbackEl.textContent = "Pick a number to see how the rule works.";
render();
}
function currentAnswer() {
return numbers[numbers.length - 1] + numbers[numbers.length - 2];
}
function makeChoices(answer) {
const options = new Set([answer]);
const offsets = [-3, -2, -1, 1, 2, 3, 5, -5];
let offsetIndex = 0;
while (options.size < 4) {
const candidate = Math.max(0, answer + offsets[offsetIndex % offsets.length]);
options.add(candidate);
offsetIndex += 1;
}
return [...options].sort(() => Math.random() - 0.5);
}
function choose(value, button) {
if (locked) return;
const answer = currentAnswer();
const previous = numbers[numbers.length - 2];
const current = numbers[numbers.length - 1];
locked = true;
if (value === answer) {
numbers.push(answer);
score += 10 + streak * 2;
streak += 1;
button.classList.add("correct");
feedbackEl.innerHTML = `<strong>Correct.</strong> ${previous} + ${current} = ${answer}, so ${answer} joins the sequence.`;
} else {
streak = 0;
button.classList.add("wrong");
feedbackEl.innerHTML = `<strong>Close.</strong> Use the two newest numbers: ${previous} + ${current} = ${answer}.`;
[...choicesEl.children].forEach((choice) => {
if (Number(choice.dataset.value) === answer) choice.classList.add("correct");
});
}
setTimeout(() => {
if (numbers.length - 2 >= maxRounds) {
finishGame();
} else {
locked = false;
render();
}
}, value === answer ? 800 : 1300);
}
function finishGame() {
feedbackEl.innerHTML = `<strong>Spiral complete.</strong> You built ${numbers.slice(0, -1).join(", ")}. The pattern keeps going forever.`;
choicesEl.innerHTML = "";
challengeEl.textContent = "You forged the Fibonacci pattern.";
questionTextEl.textContent = "The next number is always the last two added together.";
drawSpiral();
updateStats();
}
function showHint() {
const previous = numbers[numbers.length - 2];
const current = numbers[numbers.length - 1];
feedbackEl.innerHTML = `Hint: ignore the older numbers for now. Add only <strong>${previous}</strong> and <strong>${current}</strong>.`;
}
function render() {
const answer = currentAnswer();
const previous = numbers[numbers.length - 2];
const current = numbers[numbers.length - 1];
sequenceEl.innerHTML = numbers.map((number, index) => (
`<div class="chip ${index === numbers.length - 1 ? "new" : ""}">${number}</div>`
)).join("");
formulaEl.textContent = `${previous} + ${current} = ?`;
questionTextEl.textContent = `What comes after ${previous}, ${current}?`;
challengeEl.textContent = `Choose ${previous} + ${current}.`;
choicesEl.innerHTML = "";
makeChoices(answer).forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = "choice";
button.textContent = option;
button.dataset.value = option;
button.addEventListener("click", () => choose(option, button));
choicesEl.appendChild(button);
});
updateStats();
drawSpiral();
}
function updateStats() {
const built = Math.min(numbers.length - 2, maxRounds);
scoreEl.textContent = score;
streakEl.textContent = streak;
roundEl.textContent = Math.min(built + 1, maxRounds);
progressLabelEl.textContent = `${built} of ${maxRounds} spiral squares built`;
progressBarEl.style.width = `${(built / maxRounds) * 100}%`;
}
function drawSpiral() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(width * dpr));
canvas.height = Math.max(1, Math.floor(height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);
const visibleNumbers = numbers.slice(1, Math.min(numbers.length, maxRounds + 1));
let x = 0;
let y = 0;
let direction = 0;
const unitSquares = [];
visibleNumbers.forEach((number, index) => {
const size = Math.max(1, number);
if (index === 0) {
unitSquares.push({ x, y: y - size, size, direction, number });
} else if (direction === 0) {
x += unitSquares[index - 1].size;
unitSquares.push({ x, y: y - size, size, direction, number });
} else if (direction === 1) {
y -= unitSquares[index - 1].size + size;
unitSquares.push({ x, y, size, direction, number });
} else if (direction === 2) {
x -= size;
unitSquares.push({ x, y, size, direction, number });
} else {
y += unitSquares[index - 1].size;
unitSquares.push({ x, y, size, direction, number });
}
direction = (direction + 1) % 4;
});
const bounds = unitSquares.reduce((box, square) => ({
minX: Math.min(box.minX, square.x),
minY: Math.min(box.minY, square.y),
maxX: Math.max(box.maxX, square.x + square.size),
maxY: Math.max(box.maxY, square.y + square.size)
}), { minX: 0, minY: 0, maxX: 1, maxY: 1 });
const padding = 26;
const scale = Math.max(4, Math.min(
(width - padding * 2) / Math.max(1, bounds.maxX - bounds.minX),
(height - padding * 2) / Math.max(1, bounds.maxY - bounds.minY)
));
const squares = unitSquares.map((square) => ({
...square,
x: (square.x - bounds.minX) * scale + padding,
y: (square.y - bounds.minY) * scale + padding,
size: square.size * scale
}));
ctx.save();
squares.forEach((square, index) => {
const hue = index % 2 === 0 ? "98, 211, 148" : "243, 201, 105";
ctx.fillStyle = `rgba(${hue}, ${0.12 + index * 0.025})`;
ctx.strokeStyle = `rgba(${hue}, 0.86)`;
ctx.lineWidth = 2;
ctx.fillRect(square.x, square.y, square.size, square.size);
ctx.strokeRect(square.x, square.y, square.size, square.size);
ctx.fillStyle = "rgba(238, 245, 242, 0.88)";
ctx.font = "800 16px Inter, sans-serif";
if (square.size > 28) {
ctx.fillText(square.number, square.x + 8, square.y + 22);
}
});
ctx.strokeStyle = "rgba(110, 168, 254, 0.95)";
ctx.lineWidth = 4;
ctx.beginPath();
squares.forEach((square, index) => {
const radius = square.size;
let centerX = square.x;
let centerY = square.y;
let start = 0;
let end = 0;
if (square.direction === 0) {
centerX = square.x;
centerY = square.y + radius;
start = -Math.PI / 2;
end = 0;
} else if (square.direction === 1) {
centerX = square.x;
centerY = square.y;
start = 0;
end = Math.PI / 2;
} else if (square.direction === 2) {
centerX = square.x + radius;
centerY = square.y;
start = Math.PI / 2;
end = Math.PI;
} else {
centerX = square.x + radius;
centerY = square.y + radius;
start = Math.PI;
end = Math.PI * 1.5;
}
if (index === 0) ctx.moveTo(square.x, square.y + radius);
ctx.arc(centerX, centerY, radius, start, end);
});
ctx.stroke();
ctx.restore();
}
hintBtn.addEventListener("click", showHint);
resetBtn.addEventListener("click", resetGame);
window.addEventListener("resize", drawSpiral);
resetGame();
</script>
</body>
</html>