Saltar al contenido
GameGenresEnglish
Puzle

Plataformas y puzles

Usas los movimientos de un juego de plataformas (correr, saltar, trepar) para resolver los acertijos de cada nivel: activar mecanismos, mover bloques o combinar habilidades. El reto está más en pensar el camino que en ejecutar el salto.

También se llama: plataformas de puzles, puzle de plataformas

Mecánicas que lo definen

  • Movimientos de plataformas con límites fijos y fáciles de aprender. Si el salto alcanza un bloque y nunca dos, esa altura decide qué partes de la sala puedes alcanzar.
  • Objetos que se manipulan y cambian la sala, como cajas que se empujan o se cargan, interruptores, palancas y puertas.
  • Muchas veces, una regla propia que altera el mundo, como rebobinar el tiempo, abrir portales, girar la perspectiva, alternar capas del escenario o turnarse entre varios personajes.
  • Salas o niveles cortos y cerrados, cada uno un problema, con reinicio inmediato porque equivocarse es parte de resolver.

Bucle de juego

Mirar la sala entera, descubrir por qué parece imposible, probar una idea con los movimientos que ya conoces, ver dónde falla, reiniciar y ejecutar la solución.

Demo propia

Controles. Flechas o A/D para moverte, espacio, flecha arriba o W para saltar, R para reiniciar la sala. Para empujar una caja, camina contra ella. En móvil, los botones en pantalla.

Abrir en pantalla completa

Tres salas pequeñas de plataformas en las que el salto es fácil y lo difícil es pensar. Empujas cajas, las dejas caer y las pones sobre interruptores que abren puertas. A diferencia de la demo de plataformas, no hay vacíos ni enemigos ni prisa, y a diferencia de la de puzle, te mueves y saltas en tiempo real con gravedad, así que la altura de tu salto es una regla más del puzle.

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

Cómo está hecha

  1. Configuración y la regla del salto

    Se crea el juego y se definen los botones virtuales. La constante más importante es JUMP_FORCE, porque decide que el salto alcance un bloque y nunca dos. En un juego de plataformas esa cifra es cuestión de sensación, pero aquí es una regla del diseño de niveles, porque determina qué partes de la sala son alcanzables y para qué necesitas una caja.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [24, 26, 40],
      buttons: {
        left: { keyboard: ["left", "a"] },
        right: { keyboard: ["right", "d"] },
        jump: { keyboard: ["space", "up", "w"] },
        restart: { keyboard: ["r"] },
        start: { keyboard: ["enter"] }, // space is already "jump", which also starts
      },
    });
    touchControls(k, [
      { button: "left", label: "◀" },
      { button: "right", label: "▶" },
      { button: "restart", label: "⟲", side: "right" },
      { button: "jump", label: "⤒", side: "right" },
    ]);
    
    const TILE = 32;
    const SPEED = 170;
    // Jumping reaches about 1.7 tiles: enough to climb one box, never two.
    // That single number is a level-design rule: it decides what is reachable.
    const JUMP_FORCE = 420;
    const PUSH_DELAY = 0.15; // seconds leaning on a box before it moves one cell
    setGravity(1600);
  2. Tres salas, tres ideas

    Cada sala es un mapa de texto y enseña una sola idea. En la primera, un interruptor solo abre la puerta mientras algo lo pisa, y si lo pisas tú no puedes cruzar. En la segunda solo puedes empujar, así que hay que saltar la caja para empujarla hacia el lado correcto, y si cae al foso queda perdida. En la tercera, una caja que se queda sin suelo cae, y necesitas una caja como escalón y otra como peso.

    // # wall · B box · _ switch · D door · E exit · @ player.
    // Each room teaches one idea, and the jump is never the hard part.
    const ROOMS = [
      {
        hint: { es: "Sala 1 · El interruptor abre la puerta mientras algo lo pisa.", en: "Room 1 · The switch opens the door while something stands on it." },
        map: [
          "####################",
          "#..............#...#",
          "#..............#...#",
          "#..............#...#",
          "#..............#...#",
          "#..............#...#",
          "#..............D...#",
          "#..............D...#",
          "#.@...B...._...D.E.#",
          "####################",
          "####################",
        ],
      },
      {
        hint: { es: "Sala 2 · Solo puedes empujar, nunca tirar. Piensa antes de mover.", en: "Room 2 · You can only push, never pull. Think before you move." },
        map: [
          "####################",
          "#..................#",
          "#..................#",
          "#..................#",
          "#..................#",
          "#..................#",
          "#E.................#",
          "###................#",
          "###....@..B........#",
          "#############.######",
          "####################",
        ],
      },
      {
        hint: { es: "Sala 3 · Una caja cae si no tiene nada debajo.", en: "Room 3 · A box falls when there is nothing under it." },
        map: [
          "####################",
          "#...............#..#",
          "#...............#..#",
          "#...............#..#",
          "#...............#..#",
          "#...............#..#",
          "#......B........D..#",
          "#.....####......D..#",
          "#...._####..B.@.D.E#",
          "####################",
          "####################",
        ],
      },
    ];
  3. Construir la sala

    Cada carácter del mapa se convierte en un objeto. Paredes, puertas y cajas son sólidos estáticos, y por eso el personaje puede pararse sobre una caja sin que tiemble. Las cajas guardan además su celda (box.cell), porque la lógica del puzle piensa en celdas y el personaje se mueve en píxeles.

    // Walls, doors and boxes are solid. Boxes also live in a grid (box.cell):
    // the puzzle logic thinks in cells, while the player moves freely in pixels.
    room.map.forEach((row, y) =>
      [...row].forEach((ch, x) => {
        const p = vec2(x * TILE, y * TILE);
        if (ch === "#") {
          walls.add(`${x},${y}`);
          add([rect(TILE, TILE), pos(p), color(60, 66, 96), area(), body({ isStatic: true })]);
        } else if (ch === "D") doors.push(add([rect(TILE, TILE), pos(p), color(200, 80, 80), area(), body({ isStatic: true }), "door", { cell: vec2(x, y) }]));
        else if (ch === "_") plates.push(add([rect(TILE, 6), pos(p.add(0, TILE - 6)), color(230, 190, 60), area(), "plate", { cell: vec2(x, y) }]));
        else if (ch === "B")
          boxes.push(add([rect(TILE, TILE), pos(p), color(176, 120, 72), outline(3, rgb(110, 70, 40)), area(), body({ isStatic: true }), "box", { cell: vec2(x, y) }]));
        else if (ch === "E") add([rect(TILE - 8, TILE), pos(p.add(4, 0)), color(120, 230, 150), area(), "exit"]);
        else if (ch === "@") player = add([rect(22, 28), pos(p.add(TILE / 2, TILE)), anchor("bot"), color(120, 200, 255), area(), body(), "player"]);
      }),
    );
    add([text(t(room.hint), { size: 15, font: FONT }), pos(10, 8), z(10)]);
    add([text(t({ es: "R reinicia la sala", en: "R restarts the room" }), { size: 13, font: FONT }), pos(630, 344), anchor("right"), color(170, 180, 220)]);
  4. El personaje y el reinicio

    El movimiento horizontal se calcula en el paso fijo de la física (onFixedUpdate), 50 veces por segundo, como la gravedad. Así un fotograma largo en una computadora lenta no te hace atravesar una caja. Reiniciar con R es instantáneo y no cuesta nada, porque en un puzle equivocarse debe salir barato.

    // Walking runs in the fixed physics step (50 times per second), like gravity:
    // on a slow computer a long frame could otherwise carry you through a box.
    onFixedUpdate(() => {
      const dir = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
      player.moveBy(dir * SPEED * fixedDt(), 0);
    });
    onButtonPress("jump", () => {
      if (player.isGrounded()) player.jump(JUMP_FORCE);
    });
    // Restarting is instant and costs nothing: in a puzzle, a mistake should be cheap.
    onButtonPress("restart", () => go("game", roomIndex));
    player.onCollide("exit", () => (roomIndex + 1 < ROOMS.length ? go("game", roomIndex + 1) : go("end")));
  5. Empujar y dejar caer

    Si caminas contra una caja durante un instante (PUSH_DELAY), se mueve una celda, pero solo si esa celda está libre, si la caja está apoyada y si no tiene otra encima. La gravedad de las cajas también es lógica de cuadrícula, porque una caja quieta con la celda de abajo libre baja una celda. El dibujo se desliza hacia la celda nueva para que veas el movimiento.

    const boxAt = (x, y) => boxes.find((b) => b.cell.x === x && b.cell.y === y);
    const isDoor = (x, y) => doors.some((d) => d.cell.x === x && d.cell.y === y);
    // A box cannot enter a cell the player is standing in.
    const playerIn = (x, y) => player.pos.x + 11 > x * TILE && player.pos.x - 11 < (x + 1) * TILE && player.pos.y > y * TILE + 2 && player.pos.y - 28 < (y + 1) * TILE;
    const blocked = (x, y) => walls.has(`${x},${y}`) || !!boxAt(x, y) || (isDoor(x, y) && !doorsOpen) || playerIn(x, y);
    const settled = (b) => b.pos.dist(b.cell.scale(TILE)) < 1;
    
    // Pushing: lean against a box for a moment and it slides one cell, if that
    // cell is free and nothing is sitting on top of the box (too heavy).
    let pushTime = 0;
    player.onUpdate(() => {
      const dir = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
      const row = Math.floor((player.pos.y - 4) / TILE);
      const box = dir && player.isGrounded() ? boxAt(Math.floor((player.pos.x + dir * 14) / TILE), row) : null;
      // Only a box that is resting on something can be pushed.
      pushTime = box && settled(box) && blocked(box.cell.x, box.cell.y + 1) ? pushTime + dt() : 0;
      if (pushTime < PUSH_DELAY) return;
      pushTime = 0;
      const [x, y] = [box.cell.x + dir, box.cell.y];
      if (!boxAt(box.cell.x, y - 1) && !blocked(x, y)) box.cell = vec2(x, y);
    });
    
    // Gravity for boxes is grid logic: a settled box with a free cell below drops one cell.
    // The sprite then glides towards its cell, so the player sees it move.
    onUpdate("box", (b) => {
      if (settled(b) && !blocked(b.cell.x, b.cell.y + 1)) b.cell = b.cell.add(0, 1);
      b.pos = b.pos.lerp(b.cell.scale(TILE), Math.min(1, dt() * 14));
    });
  6. Interruptores y puertas

    La puerta está abierta mientras todos los interruptores estén pisados por una caja o por el personaje. Abierta, deja de chocar (preventResolution) y se vuelve casi transparente. Además sigue abierta mientras haya algo dentro, para que nunca se cierre encima de ti. El color del interruptor cambia al pisarlo, así ves la regla sin leer nada.

    // A door is open while EVERY switch is pressed by a box or by the player.
    // It also stays open while something is inside it, so it never crushes you.
    player.onBeforePhysicsResolve((col) => {
      if (doorsOpen && col.target.is("door")) col.preventResolution();
    });
    onUpdate(() => {
      const pressed = (pl) => !!boxAt(pl.cell.x, pl.cell.y) || player.isOverlapping(pl);
      const occupied = doors.some((d) => player.isOverlapping(d) || boxAt(d.cell.x, d.cell.y));
      doorsOpen = plates.every(pressed) || (doorsOpen && occupied);
      for (const pl of plates) pl.color = pressed(pl) ? rgb(110, 230, 120) : rgb(230, 190, 60);
      for (const d of doors) d.opacity = doorsOpen ? 0.15 : 1;
    });
  7. Escenas

    Tres escenas (title, game y end). La escena game recibe el número de sala, y tocar la salida llama a go("game", sala + 1), o a end después de la última.

    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
      add([text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255)]);
      onButtonPress("start", next);
      onButtonPress("jump", next);
      onMousePress(next);
    }
    
    scene("title", () =>
      screen(
        t({ es: "Plataformas y puzles", en: "Puzzle-platformer" }),
        t({
          es: "Flechas para moverte y saltar. Empuja cajas, pisa interruptores y llega a la salida verde.\nAquí no importa la rapidez: piensa antes de empujar. R reinicia la sala.\nPulsa espacio o toca para empezar.",
          en: "Arrows to move and jump. Push boxes, press switches and reach the green exit.\nSpeed does not matter here: think before you push. R restarts the room.\nPress space or tap to start.",
        }),
        () => go("game", 0),
      ),
    );
    
    scene("end", () =>
      screen(
        t({ es: "¡Resolviste las tres salas!", en: "You solved all three rooms!" }),
        t({ es: "Pulsa espacio o toca para jugar otra vez.", en: "Press space or tap to play again." }),
        () => go("game", 0),
      ),
    );
    
    go("title");

Qué simplificamos

  • Solo hay tres salas y una mecánica (empujar cajas sobre interruptores); los juegos del género suelen sumar una regla propia, como rebobinar el tiempo o abrir portales.
  • Las cajas viven en una cuadrícula y se mueven de celda en celda, aunque el personaje se mueve libre. Así el puzle es exacto y no depende de la física.
  • No hay deshacer, solo reiniciar la sala; deshacer en tiempo real exige guardar el estado de cada momento, como hace Braid.

Para modificarla

  • Diseña una cuarta sala con dos interruptores y una sola puerta. ¿Cuántas cajas hacen falta si el personaje también puede pisar uno?
  • Permite empujar una caja que tiene otra encima, moviendo las dos a la vez, y busca qué sala se vuelve más fácil.
  • Añade una caja de hielo que, al empujarla, se desliza hasta chocar con algo. ¿Qué puzles nuevos permite?
Código completo
// Puzzle-platformer demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, touchControls, pixelDensity } from "../common.js";

// #region setup
const k = kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [24, 26, 40],
  buttons: {
    left: { keyboard: ["left", "a"] },
    right: { keyboard: ["right", "d"] },
    jump: { keyboard: ["space", "up", "w"] },
    restart: { keyboard: ["r"] },
    start: { keyboard: ["enter"] }, // space is already "jump", which also starts
  },
});
touchControls(k, [
  { button: "left", label: "◀" },
  { button: "right", label: "▶" },
  { button: "restart", label: "⟲", side: "right" },
  { button: "jump", label: "⤒", side: "right" },
]);

const TILE = 32;
const SPEED = 170;
// Jumping reaches about 1.7 tiles: enough to climb one box, never two.
// That single number is a level-design rule: it decides what is reachable.
const JUMP_FORCE = 420;
const PUSH_DELAY = 0.15; // seconds leaning on a box before it moves one cell
setGravity(1600);
// #endregion

// #region rooms
// # wall · B box · _ switch · D door · E exit · @ player.
// Each room teaches one idea, and the jump is never the hard part.
const ROOMS = [
  {
    hint: { es: "Sala 1 · El interruptor abre la puerta mientras algo lo pisa.", en: "Room 1 · The switch opens the door while something stands on it." },
    map: [
      "####################",
      "#..............#...#",
      "#..............#...#",
      "#..............#...#",
      "#..............#...#",
      "#..............#...#",
      "#..............D...#",
      "#..............D...#",
      "#.@...B...._...D.E.#",
      "####################",
      "####################",
    ],
  },
  {
    hint: { es: "Sala 2 · Solo puedes empujar, nunca tirar. Piensa antes de mover.", en: "Room 2 · You can only push, never pull. Think before you move." },
    map: [
      "####################",
      "#..................#",
      "#..................#",
      "#..................#",
      "#..................#",
      "#..................#",
      "#E.................#",
      "###................#",
      "###....@..B........#",
      "#############.######",
      "####################",
    ],
  },
  {
    hint: { es: "Sala 3 · Una caja cae si no tiene nada debajo.", en: "Room 3 · A box falls when there is nothing under it." },
    map: [
      "####################",
      "#...............#..#",
      "#...............#..#",
      "#...............#..#",
      "#...............#..#",
      "#...............#..#",
      "#......B........D..#",
      "#.....####......D..#",
      "#...._####..B.@.D.E#",
      "####################",
      "####################",
    ],
  },
];
// #endregion

scene("game", (roomIndex) => {
  const room = ROOMS[roomIndex];
  const walls = new Set();
  const boxes = [];
  const doors = [];
  const plates = [];
  let player;
  let doorsOpen = false;

  // #region build
  // Walls, doors and boxes are solid. Boxes also live in a grid (box.cell):
  // the puzzle logic thinks in cells, while the player moves freely in pixels.
  room.map.forEach((row, y) =>
    [...row].forEach((ch, x) => {
      const p = vec2(x * TILE, y * TILE);
      if (ch === "#") {
        walls.add(`${x},${y}`);
        add([rect(TILE, TILE), pos(p), color(60, 66, 96), area(), body({ isStatic: true })]);
      } else if (ch === "D") doors.push(add([rect(TILE, TILE), pos(p), color(200, 80, 80), area(), body({ isStatic: true }), "door", { cell: vec2(x, y) }]));
      else if (ch === "_") plates.push(add([rect(TILE, 6), pos(p.add(0, TILE - 6)), color(230, 190, 60), area(), "plate", { cell: vec2(x, y) }]));
      else if (ch === "B")
        boxes.push(add([rect(TILE, TILE), pos(p), color(176, 120, 72), outline(3, rgb(110, 70, 40)), area(), body({ isStatic: true }), "box", { cell: vec2(x, y) }]));
      else if (ch === "E") add([rect(TILE - 8, TILE), pos(p.add(4, 0)), color(120, 230, 150), area(), "exit"]);
      else if (ch === "@") player = add([rect(22, 28), pos(p.add(TILE / 2, TILE)), anchor("bot"), color(120, 200, 255), area(), body(), "player"]);
    }),
  );
  add([text(t(room.hint), { size: 15, font: FONT }), pos(10, 8), z(10)]);
  add([text(t({ es: "R reinicia la sala", en: "R restarts the room" }), { size: 13, font: FONT }), pos(630, 344), anchor("right"), color(170, 180, 220)]);
  // #endregion

  // #region player
  // Walking runs in the fixed physics step (50 times per second), like gravity:
  // on a slow computer a long frame could otherwise carry you through a box.
  onFixedUpdate(() => {
    const dir = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
    player.moveBy(dir * SPEED * fixedDt(), 0);
  });
  onButtonPress("jump", () => {
    if (player.isGrounded()) player.jump(JUMP_FORCE);
  });
  // Restarting is instant and costs nothing: in a puzzle, a mistake should be cheap.
  onButtonPress("restart", () => go("game", roomIndex));
  player.onCollide("exit", () => (roomIndex + 1 < ROOMS.length ? go("game", roomIndex + 1) : go("end")));
  // #endregion

  // #region boxes
  const boxAt = (x, y) => boxes.find((b) => b.cell.x === x && b.cell.y === y);
  const isDoor = (x, y) => doors.some((d) => d.cell.x === x && d.cell.y === y);
  // A box cannot enter a cell the player is standing in.
  const playerIn = (x, y) => player.pos.x + 11 > x * TILE && player.pos.x - 11 < (x + 1) * TILE && player.pos.y > y * TILE + 2 && player.pos.y - 28 < (y + 1) * TILE;
  const blocked = (x, y) => walls.has(`${x},${y}`) || !!boxAt(x, y) || (isDoor(x, y) && !doorsOpen) || playerIn(x, y);
  const settled = (b) => b.pos.dist(b.cell.scale(TILE)) < 1;

  // Pushing: lean against a box for a moment and it slides one cell, if that
  // cell is free and nothing is sitting on top of the box (too heavy).
  let pushTime = 0;
  player.onUpdate(() => {
    const dir = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
    const row = Math.floor((player.pos.y - 4) / TILE);
    const box = dir && player.isGrounded() ? boxAt(Math.floor((player.pos.x + dir * 14) / TILE), row) : null;
    // Only a box that is resting on something can be pushed.
    pushTime = box && settled(box) && blocked(box.cell.x, box.cell.y + 1) ? pushTime + dt() : 0;
    if (pushTime < PUSH_DELAY) return;
    pushTime = 0;
    const [x, y] = [box.cell.x + dir, box.cell.y];
    if (!boxAt(box.cell.x, y - 1) && !blocked(x, y)) box.cell = vec2(x, y);
  });

  // Gravity for boxes is grid logic: a settled box with a free cell below drops one cell.
  // The sprite then glides towards its cell, so the player sees it move.
  onUpdate("box", (b) => {
    if (settled(b) && !blocked(b.cell.x, b.cell.y + 1)) b.cell = b.cell.add(0, 1);
    b.pos = b.pos.lerp(b.cell.scale(TILE), Math.min(1, dt() * 14));
  });
  // #endregion

  // #region switches
  // A door is open while EVERY switch is pressed by a box or by the player.
  // It also stays open while something is inside it, so it never crushes you.
  player.onBeforePhysicsResolve((col) => {
    if (doorsOpen && col.target.is("door")) col.preventResolution();
  });
  onUpdate(() => {
    const pressed = (pl) => !!boxAt(pl.cell.x, pl.cell.y) || player.isOverlapping(pl);
    const occupied = doors.some((d) => player.isOverlapping(d) || boxAt(d.cell.x, d.cell.y));
    doorsOpen = plates.every(pressed) || (doorsOpen && occupied);
    for (const pl of plates) pl.color = pressed(pl) ? rgb(110, 230, 120) : rgb(230, 190, 60);
    for (const d of doors) d.opacity = doorsOpen ? 0.15 : 1;
  });
  // #endregion
});

// #region scenes
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
  add([text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(200, 210, 255)]);
  onButtonPress("start", next);
  onButtonPress("jump", next);
  onMousePress(next);
}

scene("title", () =>
  screen(
    t({ es: "Plataformas y puzles", en: "Puzzle-platformer" }),
    t({
      es: "Flechas para moverte y saltar. Empuja cajas, pisa interruptores y llega a la salida verde.\nAquí no importa la rapidez: piensa antes de empujar. R reinicia la sala.\nPulsa espacio o toca para empezar.",
      en: "Arrows to move and jump. Push boxes, press switches and reach the green exit.\nSpeed does not matter here: think before you push. R restarts the room.\nPress space or tap to start.",
    }),
    () => go("game", 0),
  ),
);

scene("end", () =>
  screen(
    t({ es: "¡Resolviste las tres salas!", en: "You solved all three rooms!" }),
    t({ es: "Pulsa espacio o toca para jugar otra vez.", en: "Press space or tap to play again." }),
    () => go("game", 0),
  ),
);

go("title");
// #endregion

Retos de diseño

  • Separar la dificultad de ejecución de la de razonamiento. Si la solución exige un salto al píxel, quien juega no sabe si falló la idea o la mano.
  • Evitar soluciones no previstas. El movimiento continuo (saltos, empujes, física) abre atajos que una cuadrícula no tendría, así que cada sala hay que probarla intentando romperla.
  • Reconocer los estados sin salida (una caja contra la pared o perdida en un foso) y que reiniciar cueste poco.
  • Presentar cada regla nueva en una sala segura y luego combinarla con las anteriores, sin texto o con muy poco.

Historia

  1. 1980
    Space Panic

    Space Panic, donde cavas agujeros en las plataformas para atrapar criaturas, anticipa el género.

    Puzzle video game (Wikipedia)
  2. 1983
    Origen

    Door Door (1983), de Enix, y Doki Doki Penguin Land (1985), de Sega, son quizá los primeros ejemplos; en Door Door encierras alienígenas tras puertas correderas.

    Platformer (Wikipedia)
  3. 1983
    Lode Runner

    Lode Runner, uno de los primeros juegos con editor de niveles, te hace recoger oro mientras te persiguen.

    Lode Runner (Wikipedia)
  4. 1985

    En Doki Doki Penguin Land, de Sega, guías un huevo hacia el fondo del nivel moviendo y rompiendo bloques, sin dejar que caiga desde demasiado alto.

    Doki Doki Penguin Land (Wikipedia)
  5. 1993
    The Lost Vikings

    The Lost Vikings obliga a combinar a tres personajes con habilidades distintas.

    The Lost Vikings (Wikipedia)
  6. 2007
    Portal

    Portal lleva el género a la primera persona con una pistola que abre dos portales enlazados por los que pasan tu cuerpo, los objetos y su impulso.

    Portal (video game) (Wikipedia)
  7. 2008
    Braid

    Braid, un juego independiente, construye sus puzles sobre la manipulación del tiempo.

    Braid (video game) (Wikipedia)
  8. 2010
    Limbo

    Limbo, del estudio independiente Playdead, mezcla puzles y terror y espera que mueras muchas veces mientras descubres cómo superar cada trampa.

    Limbo (video game) (Wikipedia)
  9. 2012
    Fez

    En Fez giras un mundo 3D para verlo desde uno de sus cuatro lados planos, y al girar se conectan plataformas que antes estaban fuera de tu alcance.

    Fez (video game) (Wikipedia)
  10. 2018
    ONOFF

    ONOFF, hecho para js13kGames, te hace alternar entre dos capas del escenario con un botón, y se juega en el navegador.

    ONOFF — a js13kGames 2018 game (GitHub)

Juegos de referencia

1980

Space Panic

Universal. arcade

Arcade de escaleras y plataformas en el que se cavan agujeros para atrapar a los enemigos. Suele citarse como el primer juego de plataformas.

1983

Lode Runner

Doug Smith (Broderbund). Apple II, Atari 8-bit, Commodore 64

Recoges todo el oro de cada pantalla de escaleras y plataformas mientras te persiguen guardias, y puedes cavar agujeros en el suelo para atraparlos o abrirte paso. Fue uno de los primeros juegos con editor de niveles.

1993

The Lost Vikings

Silicon & Synapse (hoy Blizzard Entertainment). Super Nintendo, MS-DOS, Amiga

Alternas el control entre tres vikingos con habilidades distintas, uno que corre y rompe paredes, otro que lucha con espada y arco, y otro que se cubre con un escudo que también le sirve de planeador y de plataforma. Cada nivel solo se supera si combinas a los tres.

2007

Portal

Valve. Windows, Xbox 360, PlayStation 3

Con una pistola que abre dos portales enlazados, atraviesas salas de pruebas moviendo tu cuerpo y los objetos de formas imposibles. Cada sala presenta una idea nueva y luego la combina con las anteriores.

2008

Braid

Jonathan Blow (Number None). Xbox 360

Plataformas de rompecabezas en el que puedes rebobinar el tiempo sin límite, incluso después de morir, y cada mundo cambia las reglas del tiempo. Su éxito sin editorial abrió el interés por los juegos independientes.

2010

Limbo

Playdead. Xbox 360

Un niño cruza un bosque en blanco y negro lleno de trampas mortales. Sus creadores lo llamaron un juego de «prueba y muerte», porque esperan que mueras muchas veces mientras averiguas cómo resolver cada puzle.

2012

Fez

Polytron Corporation (Phil Fish). Xbox 360

Ves un mundo en 3D como si fuera plano, desde uno de sus cuatro lados, y puedes girarlo. Al cambiar de lado se conectan plataformas que antes estaban separadas, y esa es la base de todos sus puzles.

2018

ONOFF

Daniel Marino y Brad Dunbar. navegador

Veinticinco niveles cortos en los que saltas pinchos y vacíos y, con un botón, alternas entre dos capas del escenario que aparecen y desaparecen. Se hizo para js13kGames 2018, donde todo el juego cabe en 13 kB, y su código es libre.

2022

Dying Dreams

Jani Nykänen. navegador

Trece puzles en vista lateral, con plataformas y escaleras, en los que mueves a todas las figuras de la pantalla a la vez con las mismas flechas, y puedes deshacer y reiniciar. Se hizo para js13kGames 2022 y quedó entre los mejores de esa edición.

Ejercicio

Resuelve las tres salas de la demo y anota qué idea enseña cada una. Luego sube JUMP_FORCE hasta que el personaje salte dos bloques y comprueba qué salas dejan de ser un puzle. Por último, diseña en el mapa de texto una cuarta sala con dos interruptores y pruébala con un compañero sin darle pistas.

Fuentes