
Educational
Games designed to teach you something specific, such as maths, reading, history or science, while you play. The challenge is tied to the content: to progress you have to apply what you learn.
Also known as: educational game, edutainment
Defining mechanics
- The content is the rule: the action that moves you forward is using what is being taught (adding to measure, arranging cards to solve an equation, wiring logic gates), not answering a separate question.
- Immediate feedback that explains the outcome, so that each mistake works as a hint rather than a punishment.
- Adaptive difficulty with scaffolding, that is, aids that are withdrawn once you master a step and come back if you fail.
- Representations that move from concrete to abstract, such as objects or lengths that gradually turn into numbers and symbols.
- Visible progress (levels, maps, collections) that shows how much you have learned.
Gameplay loop
See the problem, act with the content (build, arrange, combine), get an immediate response, correct yourself and move on to a slightly harder problem.
Our demo
Controls. Click or tap a block to add it to the bridge (or press 1–6). Click a piece of the bridge or press Backspace to remove it.
Open full screenAn explorer has to cross gaps, and to do that you put number blocks together until they form a bridge exactly as long as the gap. Adding is not a question before the game, it is how you play. If you overshoot, the game tells you right away by how much, and the difficulty goes up or down depending on how you are doing.
Made with Kaplay, in about 199 lines of JavaScript.
How it's made
Setup and controls
The game is created and virtual buttons are defined to start, undo and pick blocks from the keyboard, although clicking or tapping is the main input. UNIT turns numbers into pixels, which is what makes a block worth 3 three times as long as a block worth 1.
kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [28, 36, 56], // Played with the mouse or a finger; keys 1 to 6 pick a block, Backspace undoes. buttons: { start: { keyboard: ["space", "enter"] }, undo: { keyboard: ["backspace", "z"] }, pick1: { keyboard: ["1"] }, pick2: { keyboard: ["2"] }, pick3: { keyboard: ["3"] }, pick4: { keyboard: ["4"] }, pick5: { keyboard: ["5"] }, pick6: { keyboard: ["6"] }, }, }); const UNIT = 24; // pixels per unit: a block worth 3 is 3 units long const GAP_X = 80; // where the gap starts const GROUND_Y = 180; // top of the cliffs const BRIDGES = 8; // bridges to cross to winLevels and solvable problems
Each level sets the size of the numbers and which aids are shown. makePuzzle guarantees there is always a solution: it takes two or three random blocks, their sum is the gap and the remaining blocks are distractors. An educational game must never ask for something impossible.
// Higher levels ask for bigger numbers and remove help: first the unit // marks, then the running sum, and finally the far edge hides in the fog. const LEVELS = [ { min: 5, max: 10, blocks: 4, top: 5, marks: true, sum: true, fog: false }, { min: 8, max: 14, blocks: 5, top: 7, marks: false, sum: true, fog: false }, { min: 10, max: 17, blocks: 6, top: 9, marks: false, sum: false, fog: false }, { min: 12, max: 20, blocks: 6, top: 9, marks: false, sum: false, fog: true }, ]; // There is always a solution: the target is the sum of 2 or 3 of the // blocks; the rest are distractors. function makePuzzle(lv) { for (;;) { const values = Array.from({ length: lv.blocks }, () => randi(1, lv.top + 1)); const target = values.slice(0, randi(2, 4)).reduce((a, b) => a + b, 0); if (target >= lv.min && target <= lv.max) return { target, values: shuffle(values) }; } }The gap is the question
The problem is not written, it is drawn: the far cliff sits «target × UNIT» pixels away. So the same number shows up in two ways, as a figure and as a distance, and the unit marks work like a ruler.
// The gap is the question: it is exactly `target` units wide. add([rect(width(), 120), pos(0, GROUND_Y + 30), color(40, 92, 140)]); add([rect(width(), 82), pos(0, 278), color(20, 26, 42), z(1)]); add([rect(GAP_X, 90), pos(0, GROUND_Y), color(112, 92, 70), "cliff"]); const farCliff = add([rect(width(), 90), pos(0, GROUND_Y), color(112, 92, 70), "cliff"]); const explorer = add([rect(16, 28, { radius: 3 }), pos(GAP_X - 26, GROUND_Y), anchor("bot"), color(250, 200, 90), z(5)]); const fog = add([rect(width(), 110), pos(0, GROUND_Y - 30), color(190, 200, 215), opacity(0), z(3)]); const fogLabel = fog.add([text("", { size: 13, font: FONT }), pos(12, 92), color(60, 70, 90)]); const targetLabel = add([text("", { size: 22, font: FONT }), pos(GAP_X, 92), color(255, 235, 170), z(6)]); const sumLabel = add([text("", { size: 16, font: FONT }), pos(GAP_X, 124), color(190, 220, 255), z(6)]); const hud = add([text("", { size: 15, font: FONT }), pos(16, 12)]); const notice = add([text("", { size: 15, font: FONT, width: 600 }), pos(16, 40), color(255, 210, 120)]); const notify = (s, c = rgb(255, 210, 120)) => ((notice.text = s), (notice.color = c)); // Unit marks: a ruler under the gap, only on the easiest level. const marks = []; for (let i = 0; i <= 20; i++) marks.push(add([rect(2, 10), pos(GAP_X + i * UNIT - 1, GROUND_Y + 14), color(220, 230, 255), opacity(0), z(2)]));Blocks you build with
Each block shows its number and a bar of its length. When picked, it becomes a piece of the bridge with that length. The answer is built instead of being chosen from a list, and that is where intrinsic integration lies.
// The answer is built, not chosen: each tile is a piece of bridge as long // as its number. A short bar under the digit shows that length too. const tiles = []; const placedSum = () => placed.reduce((a, tile) => a + tile.value, 0); function makeTiles() { tiles.forEach(destroy); tiles.length = 0; puzzle.values.forEach((value, i) => { const tile = add([rect(80, 58, { radius: 6 }), pos(28 + i * 100, 288), color(70, 80, 120), area(), z(2), "tile", { value, used: false }]); tile.add([text(String(value), { size: 26, font: FONT }), pos(40, 22), anchor("center")]); tile.add([rect(value * 8, 6), pos(40 - value * 4, 44), color(250, 200, 90)]); tile.onClick(() => pick(tile)); tiles.push(tile); }); } for (let i = 0; i < 6; i++) onButtonPress(`pick${i + 1}`, () => tiles[i] && pick(tiles[i]));Immediate feedback
Every action gets an answer straight away: the bridge grows and the message says how much is left. If you overshoot, the last piece falls and the message explains the sum («over by 2: 9 + 4 = 13, and the gap is 11»). The mistake does not end the game but gives you information for the next try.
// Every move answers at once: the bridge grows, and if it is too long the // last piece falls and the game says by how much and why. function pick(tile) { if (busy || tile.used) return; const start = placedSum(); tile.used = true; tile.opacity = 0.3; const piece = add([rect(tile.value * UNIT - 2, 14, { radius: 2 }), pos(GAP_X + start * UNIT + 1, GROUND_Y), color(196, 150, 96), area(), z(4), "piece", { tile }]); piece.add([text(String(tile.value), { size: 12, font: FONT }), pos((tile.value * UNIT) / 2 - 1, 7), anchor("center"), color(40, 30, 20)]); piece.onClick(() => removePiece(piece)); placed.push(tile); tile.piece = piece; const sum = placedSum(); updateLabels(); if (sum > puzzle.target) return overshoot(piece, start, tile.value); if (sum === puzzle.target) return complete(); notify(t({ es: `Faltan ${puzzle.target - sum}.`, en: `${puzzle.target - sum} to go.` }), rgb(190, 220, 255)); } function removePiece(piece) { if (busy) return; destroy(piece); placed = placed.filter((tile) => tile !== piece.tile); piece.tile.used = false; piece.tile.opacity = 1; // The pieces after the removed one slide back to close the hole. let x = 0; for (const tile of placed) (tile.piece.pos.x = GAP_X + x * UNIT + 1), (x += tile.value); updateLabels(); } onButtonPress("undo", () => placed.length && removePiece(placed[placed.length - 1].piece)); function overshoot(piece, start, value) { errorsHere++; mistakes++; piece.color = rgb(220, 80, 70); notify(t({ es: `Te pasaste por ${start + value - puzzle.target}: ${start} + ${value} = ${start + value}, y el hueco mide ${puzzle.target}.`, en: `Over by ${start + value - puzzle.target}: ${start} + ${value} = ${start + value}, and the gap is ${puzzle.target}.`, }), rgb(255, 140, 120)); busy = true; tween(piece.pos.y, height() + 20, 0.6, (y) => (piece.pos.y = y)).onEnd(() => { busy = false; removePiece(piece); adapt(false); }); }Adaptive difficulty and scaffolding
Two clean bridges in a row raise the level, and each level removes an aid: first the unit marks, then the on-screen sum and finally the fog, which hides the edge so you calculate without measuring by eye. Two mistakes on the same bridge drop a level and bring the aid back. This is called scaffolding: support is withdrawn as you stop needing it.
// The difficulty follows the player: two clean bridges in a row raise the // level (fewer aids); two mistakes on the same bridge lower it again. function adapt(solved) { if (solved) { streak = errorsHere === 0 ? streak + 1 : 0; if (streak >= 2 && level < LEVELS.length - 1) { level++; streak = 0; return t({ es: " Subes de nivel: menos ayudas.", en: " Level up: fewer aids." }); } return ""; } streak = 0; if (errorsHere % 2 === 0 && level > 0) { level--; applyAids(); notify(notice.text + t({ es: " Bajamos un nivel: vuelven las ayudas.", en: " One level down: the aids come back." }), rgb(255, 140, 120)); } updateLabels(); return ""; } function applyAids() { const lv = LEVELS[level]; marks.forEach((m, i) => (m.opacity = lv.marks && i <= puzzle.target ? 0.8 : 0)); fog.opacity = lv.fog ? 1 : 0; fog.pos.x = GAP_X + 5 * UNIT; fogLabel.text = lv.fog ? t({ es: "Niebla: calcula sin mirar el borde.", en: "Fog: work it out without seeing the edge." }) : ""; }Progress and checking
The counter shows the bridge, the level and the mistakes. When a bridge is complete, the fog lifts so the answer is checked in plain view, and the explorer crosses. Then a new problem arrives with the level already adjusted.
function updateLabels() { const lv = LEVELS[level]; const sum = placedSum(); targetLabel.text = t({ es: `El hueco mide ${puzzle.target}`, en: `The gap is ${puzzle.target} long` }); sumLabel.text = !lv.sum ? t({ es: "Suma: la llevas tú", en: "Sum: keep it in your head" }) : placed.length ? `${placed.map((tile) => tile.value).join(" + ")} = ${sum}` : "0"; hud.text = t({ es: `Puente ${crossed + 1}/${BRIDGES} Nivel ${level + 1}/${LEVELS.length} Errores: ${mistakes}`, en: `Bridge ${crossed + 1}/${BRIDGES} Level ${level + 1}/${LEVELS.length} Mistakes: ${mistakes}` }); } function complete() { busy = true; const extra = adapt(true); best = Math.max(best, level + 1); notify(t({ es: `¡Exacto! ${placed.map((tile) => tile.value).join(" + ")} = ${puzzle.target}.`, en: `Exactly! ${placed.map((tile) => tile.value).join(" + ")} = ${puzzle.target}.` }) + extra, rgb(150, 230, 150)); fog.opacity = 0; // reveal the far edge: the answer is checked in plain view const goal = GAP_X + puzzle.target * UNIT + 30; tween(explorer.pos.x, goal, 1.2, (x) => (explorer.pos.x = x)).onEnd(() => { crossed++; if (crossed >= BRIDGES) go("end", mistakes, best); else newBridge(); }); } function newBridge() { destroyAll("piece"); placed = []; errorsHere = 0; busy = false; puzzle = makePuzzle(LEVELS[level]); farCliff.pos.x = GAP_X + puzzle.target * UNIT; explorer.pos.x = GAP_X - 26; makeTiles(); applyAids(); updateLabels(); } newBridge(); notify(t({ es: "Toca bloques para construir un puente que mida justo lo que el hueco.", en: "Tap blocks to build a bridge exactly as long as the gap." }), rgb(190, 220, 255));Scenes
Three scenes (title, game and end). The last one sums up bridges, mistakes and the highest level, and presents mistakes as part of learning.
function screen(title, subtitle, next) { add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 60)), anchor("center")]); add([text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(210, 220, 255)]); onButtonPress("start", next); onClick(next); } scene("title", () => screen( t({ es: "Construye el puente", en: "Build the bridge" }), t({ es: `El explorador cruza si el puente mide exactamente lo que el hueco.\nHaz clic en los bloques (o pulsa 1–6) para sumarlos; clic en una pieza o Retroceso para quitarla.\nSi aciertas seguido, el juego sube de nivel y quita ayudas; si te equivocas, las devuelve.\nCruza ${BRIDGES} puentes. Clic o espacio para empezar.`, en: `The explorer crosses if the bridge is exactly as long as the gap.\nClick the blocks (or press 1–6) to add them; click a piece or Backspace to remove it.\nGet it right in a row and the game levels up and removes aids; make mistakes and it brings them back.\nCross ${BRIDGES} bridges. Click or press space to start.`, }), () => go("game"), ), ); scene("end", (mistakes, best) => screen( t({ es: "¡Llegaste al otro lado!", en: "You made it across!" }), t({ es: `${BRIDGES} puentes, ${mistakes} errores, nivel máximo ${best}/${LEVELS.length}.\nCada error te dijo cuánto sobraba: así se aprende sumando. Clic para repetir.`, en: `${BRIDGES} bridges, ${mistakes} mistakes, top level ${best}/${LEVELS.length}.\nEach mistake told you how much was left over: that is how adding is learned. Click to play again.`, }), () => go("game"), ), ); go("title");
What we simplified
- There is only addition of small whole numbers; a real game would cover a whole syllabus (subtraction, fractions, multiplication).
- Adaptation uses a simple rule (two clean bridges in a row go up a level and two mistakes on the same bridge go down one). Real games estimate the mastery of each skill with more data.
- It does not save progress or produce a report for the teacher.
To modify it
- Change the mechanic to subtraction, so that the bridge starts too long and you have to remove blocks until it fits. What changes in the feedback?
- Make a «quiz with a prize» version (an addition question and, if you get it right, the bridge appears by itself) and compare how much thinking each one requires.
- Record which sums cause the most mistakes and make makePuzzle offer them more often.
Full code
// Educational game demo: build the bridge
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";
// #region setup
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [28, 36, 56],
// Played with the mouse or a finger; keys 1 to 6 pick a block, Backspace undoes.
buttons: {
start: { keyboard: ["space", "enter"] },
undo: { keyboard: ["backspace", "z"] },
pick1: { keyboard: ["1"] }, pick2: { keyboard: ["2"] }, pick3: { keyboard: ["3"] },
pick4: { keyboard: ["4"] }, pick5: { keyboard: ["5"] }, pick6: { keyboard: ["6"] },
},
});
const UNIT = 24; // pixels per unit: a block worth 3 is 3 units long
const GAP_X = 80; // where the gap starts
const GROUND_Y = 180; // top of the cliffs
const BRIDGES = 8; // bridges to cross to win
// #endregion
// #region levels
// Higher levels ask for bigger numbers and remove help: first the unit
// marks, then the running sum, and finally the far edge hides in the fog.
const LEVELS = [
{ min: 5, max: 10, blocks: 4, top: 5, marks: true, sum: true, fog: false },
{ min: 8, max: 14, blocks: 5, top: 7, marks: false, sum: true, fog: false },
{ min: 10, max: 17, blocks: 6, top: 9, marks: false, sum: false, fog: false },
{ min: 12, max: 20, blocks: 6, top: 9, marks: false, sum: false, fog: true },
];
// There is always a solution: the target is the sum of 2 or 3 of the
// blocks; the rest are distractors.
function makePuzzle(lv) {
for (;;) {
const values = Array.from({ length: lv.blocks }, () => randi(1, lv.top + 1));
const target = values.slice(0, randi(2, 4)).reduce((a, b) => a + b, 0);
if (target >= lv.min && target <= lv.max) return { target, values: shuffle(values) };
}
}
// #endregion
scene("game", () => {
let level = 0; // index in LEVELS
let streak = 0; // bridges in a row without mistakes
let [crossed, mistakes, best, errorsHere] = [0, 0, 0, 0];
let puzzle = null;
let placed = []; // tiles used in the bridge, in order
let busy = false; // true while a piece falls or the explorer walks
// #region world
// The gap is the question: it is exactly `target` units wide.
add([rect(width(), 120), pos(0, GROUND_Y + 30), color(40, 92, 140)]);
add([rect(width(), 82), pos(0, 278), color(20, 26, 42), z(1)]);
add([rect(GAP_X, 90), pos(0, GROUND_Y), color(112, 92, 70), "cliff"]);
const farCliff = add([rect(width(), 90), pos(0, GROUND_Y), color(112, 92, 70), "cliff"]);
const explorer = add([rect(16, 28, { radius: 3 }), pos(GAP_X - 26, GROUND_Y), anchor("bot"), color(250, 200, 90), z(5)]);
const fog = add([rect(width(), 110), pos(0, GROUND_Y - 30), color(190, 200, 215), opacity(0), z(3)]);
const fogLabel = fog.add([text("", { size: 13, font: FONT }), pos(12, 92), color(60, 70, 90)]);
const targetLabel = add([text("", { size: 22, font: FONT }), pos(GAP_X, 92), color(255, 235, 170), z(6)]);
const sumLabel = add([text("", { size: 16, font: FONT }), pos(GAP_X, 124), color(190, 220, 255), z(6)]);
const hud = add([text("", { size: 15, font: FONT }), pos(16, 12)]);
const notice = add([text("", { size: 15, font: FONT, width: 600 }), pos(16, 40), color(255, 210, 120)]);
const notify = (s, c = rgb(255, 210, 120)) => ((notice.text = s), (notice.color = c));
// Unit marks: a ruler under the gap, only on the easiest level.
const marks = [];
for (let i = 0; i <= 20; i++)
marks.push(add([rect(2, 10), pos(GAP_X + i * UNIT - 1, GROUND_Y + 14), color(220, 230, 255), opacity(0), z(2)]));
// #endregion
// #region tiles
// The answer is built, not chosen: each tile is a piece of bridge as long
// as its number. A short bar under the digit shows that length too.
const tiles = [];
const placedSum = () => placed.reduce((a, tile) => a + tile.value, 0);
function makeTiles() {
tiles.forEach(destroy);
tiles.length = 0;
puzzle.values.forEach((value, i) => {
const tile = add([rect(80, 58, { radius: 6 }), pos(28 + i * 100, 288), color(70, 80, 120), area(), z(2), "tile", { value, used: false }]);
tile.add([text(String(value), { size: 26, font: FONT }), pos(40, 22), anchor("center")]);
tile.add([rect(value * 8, 6), pos(40 - value * 4, 44), color(250, 200, 90)]);
tile.onClick(() => pick(tile));
tiles.push(tile);
});
}
for (let i = 0; i < 6; i++) onButtonPress(`pick${i + 1}`, () => tiles[i] && pick(tiles[i]));
// #endregion
// #region feedback
// Every move answers at once: the bridge grows, and if it is too long the
// last piece falls and the game says by how much and why.
function pick(tile) {
if (busy || tile.used) return;
const start = placedSum();
tile.used = true;
tile.opacity = 0.3;
const piece = add([rect(tile.value * UNIT - 2, 14, { radius: 2 }), pos(GAP_X + start * UNIT + 1, GROUND_Y), color(196, 150, 96), area(), z(4), "piece", { tile }]);
piece.add([text(String(tile.value), { size: 12, font: FONT }), pos((tile.value * UNIT) / 2 - 1, 7), anchor("center"), color(40, 30, 20)]);
piece.onClick(() => removePiece(piece));
placed.push(tile);
tile.piece = piece;
const sum = placedSum();
updateLabels();
if (sum > puzzle.target) return overshoot(piece, start, tile.value);
if (sum === puzzle.target) return complete();
notify(t({ es: `Faltan ${puzzle.target - sum}.`, en: `${puzzle.target - sum} to go.` }), rgb(190, 220, 255));
}
function removePiece(piece) {
if (busy) return;
destroy(piece);
placed = placed.filter((tile) => tile !== piece.tile);
piece.tile.used = false;
piece.tile.opacity = 1;
// The pieces after the removed one slide back to close the hole.
let x = 0;
for (const tile of placed) (tile.piece.pos.x = GAP_X + x * UNIT + 1), (x += tile.value);
updateLabels();
}
onButtonPress("undo", () => placed.length && removePiece(placed[placed.length - 1].piece));
function overshoot(piece, start, value) {
errorsHere++;
mistakes++;
piece.color = rgb(220, 80, 70);
notify(t({
es: `Te pasaste por ${start + value - puzzle.target}: ${start} + ${value} = ${start + value}, y el hueco mide ${puzzle.target}.`,
en: `Over by ${start + value - puzzle.target}: ${start} + ${value} = ${start + value}, and the gap is ${puzzle.target}.`,
}), rgb(255, 140, 120));
busy = true;
tween(piece.pos.y, height() + 20, 0.6, (y) => (piece.pos.y = y)).onEnd(() => {
busy = false;
removePiece(piece);
adapt(false);
});
}
// #endregion
// #region adaptive
// The difficulty follows the player: two clean bridges in a row raise the
// level (fewer aids); two mistakes on the same bridge lower it again.
function adapt(solved) {
if (solved) {
streak = errorsHere === 0 ? streak + 1 : 0;
if (streak >= 2 && level < LEVELS.length - 1) {
level++;
streak = 0;
return t({ es: " Subes de nivel: menos ayudas.", en: " Level up: fewer aids." });
}
return "";
}
streak = 0;
if (errorsHere % 2 === 0 && level > 0) {
level--;
applyAids();
notify(notice.text + t({ es: " Bajamos un nivel: vuelven las ayudas.", en: " One level down: the aids come back." }), rgb(255, 140, 120));
}
updateLabels();
return "";
}
function applyAids() {
const lv = LEVELS[level];
marks.forEach((m, i) => (m.opacity = lv.marks && i <= puzzle.target ? 0.8 : 0));
fog.opacity = lv.fog ? 1 : 0;
fog.pos.x = GAP_X + 5 * UNIT;
fogLabel.text = lv.fog ? t({ es: "Niebla: calcula sin mirar el borde.", en: "Fog: work it out without seeing the edge." }) : "";
}
// #endregion
// #region progress
function updateLabels() {
const lv = LEVELS[level];
const sum = placedSum();
targetLabel.text = t({ es: `El hueco mide ${puzzle.target}`, en: `The gap is ${puzzle.target} long` });
sumLabel.text = !lv.sum ? t({ es: "Suma: la llevas tú", en: "Sum: keep it in your head" })
: placed.length ? `${placed.map((tile) => tile.value).join(" + ")} = ${sum}` : "0";
hud.text = t({ es: `Puente ${crossed + 1}/${BRIDGES} Nivel ${level + 1}/${LEVELS.length} Errores: ${mistakes}`,
en: `Bridge ${crossed + 1}/${BRIDGES} Level ${level + 1}/${LEVELS.length} Mistakes: ${mistakes}` });
}
function complete() {
busy = true;
const extra = adapt(true);
best = Math.max(best, level + 1);
notify(t({ es: `¡Exacto! ${placed.map((tile) => tile.value).join(" + ")} = ${puzzle.target}.`, en: `Exactly! ${placed.map((tile) => tile.value).join(" + ")} = ${puzzle.target}.` }) + extra, rgb(150, 230, 150));
fog.opacity = 0; // reveal the far edge: the answer is checked in plain view
const goal = GAP_X + puzzle.target * UNIT + 30;
tween(explorer.pos.x, goal, 1.2, (x) => (explorer.pos.x = x)).onEnd(() => {
crossed++;
if (crossed >= BRIDGES) go("end", mistakes, best);
else newBridge();
});
}
function newBridge() {
destroyAll("piece");
placed = [];
errorsHere = 0;
busy = false;
puzzle = makePuzzle(LEVELS[level]);
farCliff.pos.x = GAP_X + puzzle.target * UNIT;
explorer.pos.x = GAP_X - 26;
makeTiles();
applyAids();
updateLabels();
}
newBridge();
notify(t({ es: "Toca bloques para construir un puente que mida justo lo que el hueco.", en: "Tap blocks to build a bridge exactly as long as the gap." }), rgb(190, 220, 255));
// #endregion
});
// #region scenes
function screen(title, subtitle, next) {
add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 60)), anchor("center")]);
add([text(subtitle, { size: 15, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(210, 220, 255)]);
onButtonPress("start", next);
onClick(next);
}
scene("title", () =>
screen(
t({ es: "Construye el puente", en: "Build the bridge" }),
t({
es: `El explorador cruza si el puente mide exactamente lo que el hueco.\nHaz clic en los bloques (o pulsa 1–6) para sumarlos; clic en una pieza o Retroceso para quitarla.\nSi aciertas seguido, el juego sube de nivel y quita ayudas; si te equivocas, las devuelve.\nCruza ${BRIDGES} puentes. Clic o espacio para empezar.`,
en: `The explorer crosses if the bridge is exactly as long as the gap.\nClick the blocks (or press 1–6) to add them; click a piece or Backspace to remove it.\nGet it right in a row and the game levels up and removes aids; make mistakes and it brings them back.\nCross ${BRIDGES} bridges. Click or press space to start.`,
}),
() => go("game"),
),
);
scene("end", (mistakes, best) =>
screen(
t({ es: "¡Llegaste al otro lado!", en: "You made it across!" }),
t({
es: `${BRIDGES} puentes, ${mistakes} errores, nivel máximo ${best}/${LEVELS.length}.\nCada error te dijo cuánto sobraba: así se aprende sumando. Clic para repetir.`,
en: `${BRIDGES} bridges, ${mistakes} mistakes, top level ${best}/${LEVELS.length}.\nEach mistake told you how much was left over: that is how adding is learned. Click to play again.`,
}),
() => go("game"),
),
);
go("title");
// #endregionDesign challenges
- Achieving intrinsic integration, that is, making learning and playing the same action. The classic mistake is the «quiz with a prize»: classroom questions and, if you get them right, a minigame that has nothing to do with them.
- Matching the challenge to each student's level, so that it is neither boring because it is too easy nor frustrating because it is too hard.
- Writing feedback that teaches (how far off you were, which rule you broke) without shaming whoever got it wrong.
- Checking that what is learned works outside the game, not just for winning it.
- Designing with teachers, so that the game fits the curriculum and the length of a lesson.
History
- 1964OriginThe Sumerian Game
The Sumerian Game (1964), created with IBM for schools in Westchester (New York), was the first computer game designed for elementary school students.
Educational video game (Wikipedia) - 1971The Oregon Trail
The Oregon Trail, created by Carleton College students, teaches about 19th-century pioneer life.
The Oregon Trail (1971 video game) (Wikipedia) - 1982Rocky's Boots
In Rocky's Boots you wire logic gates so that a boot kicks only certain objects. It is one of the first educational programs to use an interactive graphical simulation as a learning environment.
Rocky's Boots (Wikipedia) - 1985Where in the World Is Carmen Sandiego?
Where in the World Is Carmen Sandiego? has you chase thieves using geography clues and an almanac. It was not designed as an educational game, but it succeeded in schools and became the best-selling educational software in North America.
Where in the World Is Carmen Sandiego? (1985 video game) (Wikipedia) - 2005Brain Age: Train Your Brain in Minutes a Day!
Brain Age, for the Nintendo DS, offers mental exercises inspired by the work of neuroscientist Ryuta Kawashima.
Brain Age: Train Your Brain in Minutes a Day! (Wikipedia) - 2011
Habgood and Ainsworth compare versions of Zombie Division, a maths game for children aged 7 to 11: in one the content is built into the game itself, and in another the game merely coats it. With the first they learn more and, when free to choose, play it about seven times longer.
Habgood y Ainsworth (2011), Motivating Children to Learn Effectively: Exploring the Value of Intrinsic Integration in Educational Games, Journal of the Learning Sciences 20(2), manuscrito aceptado en el repositorio de Sheffield Hallam University - 2012DragonBox Algebra
DragonBox Algebra teaches solving equations with cards that start as creatures and objects, which the game gradually replaces with variables and numbers.
DragonBox (Wikipedia) - 2015Flexbox Froggy
Thomas Park releases Flexbox Froggy, a free browser game in which you write CSS code to guide frogs to their lily pads.
Learning CSS Layout with Flexbox Froggy (Thomas Park) - 2016Minecraft: Education Edition
The educational edition of Minecraft, designed for use in schools, is released.
Minecraft, Education Edition (Wikipedia) - 2017Coding for Carrots (Google Doodle)
Google celebrates 50 years of programming languages for children with a playable Doodle in which you snap together Scratch-style code blocks to guide a character to its carrots.
Celebrating 50 years of Kids Coding (Google Doodles)
Reference games
1964
The Sumerian Game
You rule the Sumerian city of Lagash and, round by round, decide how much grain is eaten, sown or stored. Teacher Mabel Addis designed it to study the use of simulations at school, and it was played on a teleprinter connected to a mainframe.
1971
The Oregon Trail
You lead a wagon of settlers to Oregon in 1847; at each stage you buy supplies, hunt and decide how much to eat while facing storms and breakdowns. Three Carleton College students wrote it for a junior high history class, and from 1975 MECC distributed it to schools.
1981
MasterType
A game for learning to type in the style of a shooter: to bring down each attacker you have to press the right key or word in time.
1982
Rocky's Boots
You wire logic gates (AND, OR, NOT) to a mechanical boot so that it kicks only the right objects off a conveyor belt; later you can build your own circuits. Logic is not asked about, it is built.
1985
Where in the World Is Carmen Sandiego?
You are a detective chasing a gang of thieves around the world; you question witnesses and use an almanac to work out which country the suspect fled to. Geography is the clue that moves the chase forward.
2005
Brain Age: Train Your Brain in Minutes a Day!
You solve sums, Stroop-style colour tests and sudoku by writing with the stylus on the touch screen or speaking into the microphone, and the game works out a «brain age». A cartoon of neuroscientist Ryuta Kawashima guides you.
2012
DragonBox Algebra
In each level you arrange cards on two trays until a box is left on its own. The cards start as creatures and objects and, level by level, turn into variables and numbers, so you end up solving equations without noticing.
2015
Flexbox Froggy
You write CSS flexbox properties to move the frogs onto their lily pads; the code you type is the only way to play. It is free, open source and translated into Spanish.
2016
Minecraft: Education Edition
The version of Minecraft designed for schools, in which teachers prepare worlds and activities for history, science or language, and students build and explore inside them.
2017
Coding for Carrots (Google Doodle)
A Google Doodle celebrating 50 years of programming languages for children. Over six levels you snap together Scratch-style code blocks so that a character collects carrots.
Exercise
Play the demo twice, once getting it right and once making mistakes on purpose, and note which aids appear and disappear. Then redesign the demo as a «quiz with a prize» (an addition question and, if you get it right, the bridge appears by itself) and discuss what is lost.
Subgenres
- Typing gameYou play by typing on the keyboard: every letter, word or phrase you type correctly fires, strikes or moves you, and mistakes or slowness are punished. The challenge is typing fast without looking at the keys while the game's pressure builds.
Sources
- Brigham et al., 2021
- Habgood, M. P. J. y Ainsworth, S. E. (2011). Motivating Children to Learn Effectively: Exploring the Value of Intrinsic Integration in Educational Games. Journal of the Learning Sciences, 20(2), 169-206
- Malone, T. W. (1981). Toward a Theory of Intrinsically Motivating Instruction. Cognitive Science, 5(4), 333-369