
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 completaTres 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
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);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#", "####################", "####################", ], }, ];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)]);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")));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)); });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; });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");
// #endregionRetos 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
- 1980Space Panic
Space Panic, donde cavas agujeros en las plataformas para atrapar criaturas, anticipa el género.
Puzzle video game (Wikipedia) - 1983Origen
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) - 1983Lode Runner
Lode Runner, uno de los primeros juegos con editor de niveles, te hace recoger oro mientras te persiguen.
Lode Runner (Wikipedia) - 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) - 1993The Lost Vikings
The Lost Vikings obliga a combinar a tres personajes con habilidades distintas.
The Lost Vikings (Wikipedia) - 2007Portal
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) - 2008Braid
Braid, un juego independiente, construye sus puzles sobre la manipulación del tiempo.
Braid (video game) (Wikipedia) - 2010Limbo
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) - 2012Fez
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) - 2018ONOFF
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
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
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
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
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
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
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
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
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
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.