Saltar al contenido
GameGenresEnglish
Juego serio

Educativo

Juegos diseñados para que aprendas algo concreto, como matemáticas, lectura, historia o ciencias, mientras juegas. El reto está atado al contenido: para avanzar tienes que aplicar lo que aprendes.

También se llama: videojuego educativo, edutenimiento

Mecánicas que lo definen

  • El contenido es la regla: la acción que te hace avanzar es usar lo que se enseña (sumar para medir, ordenar cartas para despejar una ecuación, conectar compuertas lógicas), no contestar una pregunta aparte.
  • Retroalimentación inmediata que explica el resultado, para que cada error funcione como una pista y no como un castigo.
  • Dificultad adaptativa con andamiaje, es decir, ayudas que se retiran cuando dominas un paso y vuelven si fallas.
  • Representaciones que van de lo concreto a lo abstracto, como objetos o longitudes que poco a poco se convierten en números y símbolos.
  • Un progreso visible (niveles, mapas, colecciones) que muestra cuánto has aprendido.

Bucle de juego

Ver el problema, actuar con el contenido (construir, ordenar, combinar), recibir una respuesta inmediata, corregir y pasar a un problema un poco más difícil.

Demo propia

Controles. Haz clic o toca un bloque para añadirlo al puente (o pulsa 1–6). Haz clic en una pieza del puente o pulsa Retroceso para quitarla.

Abrir en pantalla completa

Un explorador tiene que cruzar huecos, y para eso juntas bloques numéricos hasta formar un puente que mida exactamente lo que pide el hueco. Sumar no es una pregunta antes del juego, es la forma de jugar. Si te pasas, el juego te dice al instante por cuánto, y la dificultad sube o baja según cómo te va.

Hecha con Kaplay, en unas 199 líneas de JavaScript.

Cómo está hecha

  1. Configuración y controles

    Se crea el juego y se definen botones virtuales para empezar, deshacer y elegir bloques con el teclado, aunque lo principal es el clic o el toque. UNIT convierte números en píxeles, y es lo que permite que un bloque que vale 3 mida tres veces más que uno que vale 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 win
  2. Niveles y problemas con solución

    Cada nivel define el tamaño de los números y qué ayudas se muestran. makePuzzle garantiza que siempre haya solución: toma dos o tres bloques al azar, su suma es el hueco y los demás bloques son distractores. Un juego educativo nunca debe pedir algo imposible.

    // 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) };
      }
    }
  3. El hueco es la pregunta

    El problema no se escribe, se dibuja: el acantilado lejano está a «objetivo × UNIT» píxeles. Así el mismo número se ve de dos maneras, como cifra y como distancia, y las marcas de unidad funcionan como una regla.

    // 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)]));
  4. Bloques que se construyen

    Cada bloque muestra su número y una barra de su largo. Al elegirlo se convierte en una pieza del puente con esa longitud. La respuesta se construye en lugar de elegirse de una lista, y en eso está la integración intrínseca.

    // 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]));
  5. Retroalimentación inmediata

    Cada acción responde al momento: el puente crece y el mensaje dice cuánto falta. Si te pasas, la última pieza cae y el mensaje explica la cuenta («te pasaste por 2: 9 + 4 = 13, y el hueco mide 11»). El error no termina la partida, sino que te da información para el siguiente intento.

    // 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);
      });
    }
  6. Dificultad adaptativa y andamiaje

    Dos puentes seguidos sin errores suben el nivel, y cada nivel retira una ayuda: primero las marcas de unidad, luego la suma en pantalla y al final la niebla, que tapa el borde para que calcules sin medir con la vista. Dos errores en el mismo puente bajan un nivel y devuelven la ayuda. A esto se le llama andamiaje: el apoyo se retira a medida que dejas de necesitarlo.

    // 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." }) : "";
    }
  7. Progreso y comprobación

    El marcador muestra el puente, el nivel y los errores. Al completar un puente la niebla se levanta, de modo que la respuesta se comprueba a la vista, y el explorador cruza. Después llega un problema nuevo con el nivel ya ajustado.

    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));
  8. Escenas

    Tres escenas (title, game y end). La final resume puentes, errores y nivel máximo, y presenta los errores como parte del aprendizaje.

    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");

Qué simplificamos

  • Solo hay sumas de números enteros pequeños; un juego real recorrería un temario completo (restas, fracciones, multiplicación).
  • La adaptación usa una regla simple (dos aciertos seguidos suben un nivel y dos errores en el mismo puente lo bajan). Los juegos reales estiman el dominio de cada habilidad con más datos.
  • No guarda el progreso ni genera un informe para el docente.

Para modificarla

  • Cambia la mecánica a resta, de modo que el puente empiece demasiado largo y tengas que quitar bloques hasta que encaje. ¿Qué cambia en la retroalimentación?
  • Haz una versión «quiz con premio» (una pregunta de suma y, si aciertas, el puente aparece solo) y compara cuánto se piensa en cada una.
  • Registra qué sumas causan más errores y haz que makePuzzle las proponga más a menudo.
Código completo
// 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");
// #endregion

Retos de diseño

  • Lograr la integración intrínseca, es decir, que aprender y jugar sean la misma acción. El error clásico es el «quiz con premio»: preguntas de clase y, si aciertas, un minijuego que no tiene nada que ver.
  • Ajustar el reto al nivel de cada estudiante, para que no aburra por fácil ni frustre por difícil.
  • Escribir retroalimentación que enseñe (cuánto te pasaste, qué regla rompiste) sin humillar a quien se equivoca.
  • Comprobar que lo aprendido sirve fuera del juego, y no solo para ganarlo.
  • Diseñar con docentes, para que el juego encaje en el programa y en el tiempo de una clase.

Historia

  1. 1964
    OrigenThe Sumerian Game

    The Sumerian Game (1964), creado con IBM para escuelas de Westchester (Nueva York), fue el primer juego de computadora diseñado para estudiantes de primaria.

    Educational video game (Wikipedia)
  2. 1971
    The Oregon Trail

    The Oregon Trail, creado por estudiantes de Carleton College, enseña la vida de los colonos del siglo XIX.

    The Oregon Trail (1971 video game) (Wikipedia)
  3. 1982
    Rocky's Boots

    En Rocky's Boots conectas compuertas lógicas para que una bota patee solo ciertos objetos. Es uno de los primeros programas educativos que usan una simulación gráfica interactiva como entorno de aprendizaje.

    Rocky's Boots (Wikipedia)
  4. 1985
    Where in the World Is Carmen Sandiego?

    Where in the World Is Carmen Sandiego? te pone a perseguir ladrones con pistas de geografía y un almanaque. No nació como juego educativo, pero triunfó en las escuelas y se volvió el software educativo más vendido de Norteamérica.

    Where in the World Is Carmen Sandiego? (1985 video game) (Wikipedia)
  5. 2005
    Brain Age: Train Your Brain in Minutes a Day!

    Brain Age, para Nintendo DS, propone ejercicios mentales inspirados en el trabajo del neurocientífico Ryuta Kawashima.

    Brain Age: Train Your Brain in Minutes a Day! (Wikipedia)
  6. 2011

    Habgood y Ainsworth comparan versiones de Zombie Division, un juego de matemáticas para niños de 7 a 11 años: en una el contenido forma parte del propio juego y en otra el juego solo lo recubre. Con la primera aprenden más y, cuando pueden elegir, la juegan unas siete veces más tiempo.

    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
  7. 2012
    DragonBox Algebra

    DragonBox Algebra enseña a despejar ecuaciones con cartas que al principio son criaturas y objetos, y que el juego reemplaza poco a poco por variables y números.

    DragonBox (Wikipedia)
  8. 2015
    Flexbox Froggy

    Thomas Park publica Flexbox Froggy, un juego gratuito para el navegador en el que escribes código CSS para llevar a las ranas a sus hojas.

    Learning CSS Layout with Flexbox Froggy (Thomas Park)
  9. 2016
    Minecraft: Education Edition

    Sale la edición educativa de Minecraft, pensada para usarse en las escuelas.

    Minecraft, Education Edition (Wikipedia)
  10. 2017
    Coding for Carrots (Google Doodle)

    Google celebra 50 años de los lenguajes de programación para niños con un Doodle jugable en el que encajas bloques de código, al estilo de Scratch, para guiar a un personaje hacia sus zanahorias.

    Celebrating 50 years of Kids Coding (Google Doodles)

Juegos de referencia

1964

The Sumerian Game

Mabel Addis y William McKay (IBM y BOCES de Westchester). IBM 7090

Gobiernas la ciudad sumeria de Lagash y, ronda a ronda, decides cuánto grano se come, se siembra o se guarda. Lo diseñó la maestra Mabel Addis para estudiar el uso de simulaciones en la escuela, y se jugaba con un teleimpresor conectado a un computador central.

1971

The Oregon Trail

Don Rawitsch, Bill Heinemann y Paul Dillenberger. HP 2100

Guías una caravana de colonos hacia Oregón en 1847; en cada etapa compras provisiones, cazas y decides cuánto comer mientras enfrentas tormentas y averías. Tres estudiantes de Carleton College lo escribieron para una clase de historia de secundaria, y desde 1975 lo distribuyó el MECC en las escuelas.

1981

MasterType

Bruce Zweig (Broderbund). Apple II, Atari 8-bit, IBM PC, VIC-20

Juego para aprender mecanografía al estilo de los de disparos: para derribar a cada atacante tienes que pulsar a tiempo la tecla o la palabra correcta.

1982

Rocky's Boots

Warren Robinett y Leslie Grimm (The Learning Company). Apple II, Commodore 64, IBM PC

Conectas compuertas lógicas (Y, O, NO) a una bota mecánica para que patee de una cinta solo los objetos correctos; después puedes armar tus propios circuitos. La lógica no se pregunta, se construye.

1985

Where in the World Is Carmen Sandiego?

Broderbund. Apple II, Commodore 64, MS-DOS

Eres detective y persigues a una banda de ladrones por el mundo; interrogas testigos y usas un almanaque para deducir a qué país huyó el sospechoso. La geografía es la pista que hace avanzar la persecución.

2005

Brain Training del Dr. Kawashima ¿Cuántos años tiene tu cerebro?(Brain Age: Train Your Brain in Minutes a Day!)

Nintendo. Nintendo DS

Resuelves cálculos, pruebas de colores tipo Stroop y sudokus escribiendo con el lápiz en la pantalla táctil o hablando al micrófono, y el juego calcula una «edad cerebral». Lo guía una caricatura del neurocientífico Ryuta Kawashima.

2012

DragonBox Algebra

WeWantToKnow. iOS, Android

En cada nivel ordenas cartas en dos bandejas hasta dejar sola una caja. Las cartas empiezan como criaturas y objetos y, nivel a nivel, se convierten en variables y números, así que terminas despejando ecuaciones sin haberlo notado.

2016

Minecraft: Education Edition

Mojang Studios. Windows, iPadOS

La versión de Minecraft pensada para las escuelas, en la que los docentes preparan mundos y actividades de historia, ciencias o lengua, y los estudiantes construyen y exploran dentro de ellos.

2017

Coding for Carrots (Google Doodle)

Google (equipos de Doodles y Blockly) y MIT Scratch. navegador

Un Doodle de Google que celebra los 50 años de los lenguajes de programación para niños. En seis niveles encajas bloques de código, al estilo de Scratch, para que un personaje recoja zanahorias.

Ejercicio

Juega la demo dos veces, una vez acertando y otra equivocándote a propósito, y anota qué ayudas aparecen y desaparecen. Después rediseña la demo como un «quiz con premio» (una pregunta de suma y, si aciertas, el puente aparece solo) y piensa qué se pierde.

Subgéneros

  • Juego de mecanografíaJuegas escribiendo en el teclado: cada letra, palabra o frase que tecleas bien dispara, ataca o te mueve, y los errores o la lentitud se castigan. El reto es escribir rápido y sin mirar las teclas mientras la presión del juego aumenta.

Fuentes