Saltar al contenido
GameGenresEnglish
Puzle

Puzle

Resuelves problemas siguiendo reglas fijas, como ordenar piezas, abrir un camino o colocar objetos en su sitio. El reto está sobre todo en deducir la solución, más que en los reflejos.

También se llama: Lógica, Rompecabezas, Puzzle, juego de lógica, juego de ingenio

Mecánicas que lo definen

  • Pocas reglas, claras y deterministas, que el jugador puede aprender por completo.
  • Niveles o problemas separados, cada uno con una solución (o pocas) que hay que descubrir.
  • Estados sin salida a los que se llega por una mala jugada, y herramientas para deshacer o reiniciar.
  • Una progresión que introduce las reglas de una en una y luego las combina.

Bucle de juego

Observar el problema, formular una hipótesis, probarla, ver por qué falla, deshacer y volver a intentar hasta que la idea encaja.

Demo propia

Controles. Flechas o WASD para moverte, Z para deshacer y R para reiniciar el nivel. Con ratón o tacto, toca hacia dónde quieres ir respecto del jugador y usa los botones Deshacer y Reiniciar.

Abrir en pantalla completa

Tres niveles cortos de empujar cajas, al estilo de Sokoban, que crecen en dificultad. Las reglas caben en una frase y no hay azar ni reflejos, así que todo el reto está en pensar antes de mover.

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

Cómo está hecha

  1. Configuración y controles

    Se definen botones virtuales para moverse, deshacer y reiniciar. El resto del código pregunta por esos botones, así el mismo juego funciona con teclado o con pantalla táctil.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [34, 28, 48],
      // Virtual buttons: the game asks about "undo", not about a specific key.
      buttons: {
        left: { keyboard: ["left", "a"] },
        right: { keyboard: ["right", "d"] },
        up: { keyboard: ["up", "w"] },
        down: { keyboard: ["down", "s"] },
        undo: { keyboard: ["z", "backspace"] },
        restart: { keyboard: ["r"] },
        start: { keyboard: ["space", "enter"] },
      },
    });
    touchControls(k, [
      { button: "left", label: "◀" },
      { button: "up", label: "▲" },
      { button: "down", label: "▼" },
      { button: "right", label: "▶" },
      { button: "undo", label: "↶", side: "right" },
      { button: "restart", label: "⟲" },
    ]);
    
    const TILE = 36;
    const DIRECTIONS = { left: vec2(-1, 0), right: vec2(1, 0), up: vec2(0, -1), down: vec2(0, 1) };
  2. Los niveles como texto

    Cada nivel es una lista de cadenas en la notación estándar de Sokoban, la que usa la comunidad para compartir miles de niveles. readLevel() la convierte en un estado con muros, metas, cajas y jugador. Cada nivel enseña una idea antes de que el siguiente la combine con otra.

    // Standard Sokoban notation: # wall · @ player · $ box · . goal
    // * box on goal · + player on goal. Each level teaches something:
    // 1) you can only push, so you must walk around the box to change its direction;
    // 2) a box in a corner can never move again (a dead-end state);
    // 3) several boxes: the order in which you move them matters.
    const LEVELS = [
      ["#######", "#@    #", "# $   #", "#   . #", "#######"],
      ["  ####", "###  #", "# $  #", "# #. ##", "# .$ @#", "#    ##", "######"],
      ["#######", "#.   .#", "# $#$ #", "#  @  #", "# $#  #", "#.    #", "#######"],
    ];
    
    // Turns the text into the game state: positions of walls, goals, boxes and the player.
    function readLevel(rows) {
      const board = { walls: new Set(), floor: new Set(), goals: new Set(), boxes: [], player: vec2(0), width: 0, height: rows.length };
      rows.forEach((row, y) => {
        board.width = Math.max(board.width, row.length);
        [...row].forEach((c, x) => {
          const key = `${x},${y}`;
          if (c === "#") board.walls.add(key);
          else if (x > row.indexOf("#") && x < row.lastIndexOf("#")) board.floor.add(key);
          if (".*+".includes(c)) board.goals.add(key);
          if ("$*".includes(c)) board.boxes.push(vec2(x, y));
          if ("@+".includes(c)) board.player = vec2(x, y);
        });
      });
      return board;
    }
    const keyOf = (p) => `${p.x},${p.y}`;
  3. Reglas simples y deterministas

    Te mueves una casilla; si hay una caja delante la empujas, salvo que detrás haya un muro u otra caja, y nunca puedes tirar de ella. La misma jugada da siempre el mismo resultado, así que el jugador puede razonar el puzle entero sin depender de la suerte.

    // The rules are few and deterministic: the same move always gives the same result.
    // You move one cell; if there is a box ahead, you push it, unless there is a
    // wall or another box behind it. You can never pull a box.
    function tryMove(d) {
      if (solved) return;
      const target = b.player.add(d);
      if (isWall(target)) return;
      const i = boxAt(target);
      if (i >= 0) {
        const behind = target.add(d);
        if (isWall(behind) || boxAt(behind) >= 0) return;
      }
      // Before changing anything, save a snapshot of the state so it can be undone.
      history.push({ player: b.player, boxes: [...b.boxes] });
      if (i >= 0) b.boxes[i] = target.add(d);
      b.player = target;
      checkWin();
    }
    for (const [button, d] of Object.entries(DIRECTIONS)) onButtonPress(button, () => tryMove(d));
  4. Deshacer

    Antes de cada movimiento se guarda una foto del estado, y deshacer es volver a la anterior. Hace que equivocarse sea barato, así el jugador prueba ideas en vez de temer cada paso. El contador de movimientos es el tamaño de ese historial.

    // Undoing means going back to the previous snapshot. In a puzzle it makes
    // mistakes cheap: the player tries out ideas instead of fearing every move.
    function undo() {
      const previous = history.pop();
      if (!previous || solved) return;
      b.player = previous.player;
      b.boxes = previous.boxes;
    }
    onButtonPress("undo", undo);
    onButtonPress("restart", () => go("game", n, total));
  5. Estados sin salida

    Una caja fuera de su meta y metida en una esquina ya no se puede mover, y el nivel queda sin solución aunque el juego siga. La demo la pinta de rojo y lo avisa, porque aprender a reconocer esos estados antes de llegar a ellos es buena parte del reto.

    // A box off its goal with a wall above or below AND another to one side is
    // in a corner: it can never be pushed out. The level has become unsolvable,
    // and pointing it out teaches you to spot these states before reaching them.
    function isStuck(c) {
      if (b.goals.has(keyOf(c))) return false;
      const v = isWall(c.add(0, -1)) || isWall(c.add(0, 1));
      const h = isWall(c.add(-1, 0)) || isWall(c.add(1, 0));
      return v && h;
    }
  6. Comprobar la solución

    El nivel está resuelto cuando todas las cajas están sobre una meta. Entonces se pasa al siguiente y se suman los movimientos, que invitan a buscar soluciones más cortas.

    function checkWin() {
      if (!b.boxes.every((c) => b.goals.has(keyOf(c)))) return;
      solved = true;
      const sum = total + history.length;
      wait(1.2, () => (n + 1 < LEVELS.length ? go("game", n + 1, sum) : go("end", sum)));
    }
  7. Dibujar a partir del estado

    El tablero se dibuja en cada fotograma a partir de los datos con onDraw. Como no hay objetos que sincronizar, deshacer y reiniciar solo tienen que cambiar los datos.

    // The board is drawn every frame from the state: there are no objects to
    // keep in sync, so undoing only has to change the data.
    const cellPos = (p) => origin.add(p.scale(TILE));
    onDraw(() => {
      for (let y = 0; y < b.height; y++)
        for (let x = 0; x < b.width; x++) {
          const p = vec2(x, y);
          if (isWall(p)) drawRect({ pos: cellPos(p), width: TILE, height: TILE, color: rgb(92, 78, 120) });
          if (b.floor.has(keyOf(p))) drawRect({ pos: cellPos(p), width: TILE, height: TILE, color: rgb(52, 44, 70) });
          if (b.goals.has(keyOf(p)))
            drawCircle({ pos: cellPos(p).add(TILE / 2), radius: 7, color: rgb(255, 205, 80) });
        }
      for (const c of b.boxes) {
        const onGoal = b.goals.has(keyOf(c));
        const tint = onGoal ? rgb(110, 210, 120) : isStuck(c) ? rgb(230, 80, 70) : rgb(200, 140, 80);
        drawRect({ pos: cellPos(c).add(3), width: TILE - 6, height: TILE - 6, radius: 4, color: tint });
      }
      drawCircle({ pos: cellPos(b.player).add(TILE / 2), radius: TILE / 2 - 5, color: rgb(120, 200, 255) });
    });
    
    const hud = add([text("", { size: 16, font: FONT, width: 200 }), pos(440, 40)]);
    onUpdate(() => {
      const warning = b.boxes.some(isStuck)
        ? t({ es: "\n\nUna caja quedó atascada. Pulsa Z para deshacer.", en: "\n\nA box is stuck. Press Z to undo." })
        : "";
      const done = solved ? t({ es: "\n\n¡Resuelto!", en: "\n\nSolved!" }) : "";
      hud.text =
        t({ es: `Nivel ${n + 1} de ${LEVELS.length}\nMovimientos: `, en: `Level ${n + 1} of ${LEVELS.length}\nMoves: ` }) +
        history.length + warning + done;
    });
  8. Jugar con ratón o tacto

    Un toque mueve al jugador en la dirección del toque, y dos botones en pantalla repiten Z y R. Como el juego es por turnos, no hace falta precisión ni rapidez para jugar así.

    // With a mouse or touch: tap where you want to go relative to the player.
    // Two on-screen buttons repeat Z and R.
    function screenButton(label, y, action) {
      const btn = add([rect(170, 32, { radius: 6 }), pos(440, y), color(70, 60, 100), area()]);
      btn.add([text(label, { size: 15, font: FONT }), pos(12, 8)]);
      btn.onClick(action);
    }
    screenButton(t({ es: "Deshacer (Z)", en: "Undo (Z)" }), 250, undo);
    screenButton(t({ es: "Reiniciar (R)", en: "Restart (R)" }), 292, () => go("game", n, total));
    onMousePress(() => {
      if (mousePos().x > 430) return; // clicks on the buttons do not move the player
      const d = mousePos().sub(cellPos(b.player).add(TILE / 2));
      if (Math.max(Math.abs(d.x), Math.abs(d.y)) < TILE / 2) return;
      tryMove(Math.abs(d.x) > Math.abs(d.y) ? vec2(Math.sign(d.x), 0) : vec2(0, Math.sign(d.y)));
    });
  9. Escenas

    El juego tiene tres escenas (title, game y end). La escena de juego recibe el número de nivel y los movimientos acumulados, y go() la vuelve a llamar para pasar de nivel o reiniciar.

    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: 540 }),
        pos(center().add(0, 30)),
        anchor("center"),
        color(220, 200, 255),
      ]);
      onButtonPress("start", next);
      onClick(next);
    }
    
    scene("title", () =>
      screen(
        t({ es: "Empuja las cajas", en: "Push the boxes" }),
        t({
          es: "Lleva cada caja a una meta amarilla. Solo puedes empujar, nunca tirar.\nFlechas para moverte, Z deshace, R reinicia.\nPulsa espacio o toca para empezar.",
          en: "Move every box onto a yellow goal. You can only push, never pull.\nArrows to move, Z undoes, R restarts.\nPress space or tap to start.",
        }),
        () => go("game", 0, 0),
      ),
    );
    
    scene("end", (total) =>
      screen(
        t({ es: "¡Resolviste los tres niveles!", en: "You solved all three levels!" }),
        t({
          es: `Movimientos en total: ${total}. ¿Puedes hacerlo con menos?\nPulsa espacio para repetir.`,
          en: `Total moves: ${total}. Can you do it in fewer?\nPress space to play again.`,
        }),
        () => go("game", 0, 0),
      ),
    );
    
    go("title");

Qué simplificamos

  • Solo hay tres niveles pequeños; los juegos del género tienen decenas o cientos, con una curva de dificultad mucho más larga.
  • El aviso de atasco solo detecta cajas en esquinas; hay muchos otros estados sin salida que no reconoce.
  • Las cajas y el jugador son formas simples, sin animación ni sonido.

Para modificarla

  • Diseña un cuarto nivel en el mapa de texto y pruébalo con alguien que no conozca la demo. ¿Qué tiene que entender para resolverlo?
  • Amplía el aviso de atasco para detectar dos cajas juntas contra un muro, otro estado sin salida típico.
  • Añade una casilla de hielo en la que la caja sigue deslizándose hasta chocar, y piensa qué niveles nuevos permite esa regla.
Código completo
// Puzzle 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: [34, 28, 48],
  // Virtual buttons: the game asks about "undo", not about a specific key.
  buttons: {
    left: { keyboard: ["left", "a"] },
    right: { keyboard: ["right", "d"] },
    up: { keyboard: ["up", "w"] },
    down: { keyboard: ["down", "s"] },
    undo: { keyboard: ["z", "backspace"] },
    restart: { keyboard: ["r"] },
    start: { keyboard: ["space", "enter"] },
  },
});
touchControls(k, [
  { button: "left", label: "◀" },
  { button: "up", label: "▲" },
  { button: "down", label: "▼" },
  { button: "right", label: "▶" },
  { button: "undo", label: "↶", side: "right" },
  { button: "restart", label: "⟲" },
]);

const TILE = 36;
const DIRECTIONS = { left: vec2(-1, 0), right: vec2(1, 0), up: vec2(0, -1), down: vec2(0, 1) };
// #endregion

// #region levels
// Standard Sokoban notation: # wall · @ player · $ box · . goal
// * box on goal · + player on goal. Each level teaches something:
// 1) you can only push, so you must walk around the box to change its direction;
// 2) a box in a corner can never move again (a dead-end state);
// 3) several boxes: the order in which you move them matters.
const LEVELS = [
  ["#######", "#@    #", "# $   #", "#   . #", "#######"],
  ["  ####", "###  #", "# $  #", "# #. ##", "# .$ @#", "#    ##", "######"],
  ["#######", "#.   .#", "# $#$ #", "#  @  #", "# $#  #", "#.    #", "#######"],
];

// Turns the text into the game state: positions of walls, goals, boxes and the player.
function readLevel(rows) {
  const board = { walls: new Set(), floor: new Set(), goals: new Set(), boxes: [], player: vec2(0), width: 0, height: rows.length };
  rows.forEach((row, y) => {
    board.width = Math.max(board.width, row.length);
    [...row].forEach((c, x) => {
      const key = `${x},${y}`;
      if (c === "#") board.walls.add(key);
      else if (x > row.indexOf("#") && x < row.lastIndexOf("#")) board.floor.add(key);
      if (".*+".includes(c)) board.goals.add(key);
      if ("$*".includes(c)) board.boxes.push(vec2(x, y));
      if ("@+".includes(c)) board.player = vec2(x, y);
    });
  });
  return board;
}
const keyOf = (p) => `${p.x},${p.y}`;
// #endregion

scene("game", (n, total) => {
  const b = readLevel(LEVELS[n]);
  const origin = vec2(220 - (b.width * TILE) / 2, 180 - (b.height * TILE) / 2);
  const isWall = (p) => b.walls.has(keyOf(p));
  const boxAt = (p) => b.boxes.findIndex((c) => c.eq(p));
  let history = [];
  let solved = false;

  // #region rules
  // The rules are few and deterministic: the same move always gives the same result.
  // You move one cell; if there is a box ahead, you push it, unless there is a
  // wall or another box behind it. You can never pull a box.
  function tryMove(d) {
    if (solved) return;
    const target = b.player.add(d);
    if (isWall(target)) return;
    const i = boxAt(target);
    if (i >= 0) {
      const behind = target.add(d);
      if (isWall(behind) || boxAt(behind) >= 0) return;
    }
    // Before changing anything, save a snapshot of the state so it can be undone.
    history.push({ player: b.player, boxes: [...b.boxes] });
    if (i >= 0) b.boxes[i] = target.add(d);
    b.player = target;
    checkWin();
  }
  for (const [button, d] of Object.entries(DIRECTIONS)) onButtonPress(button, () => tryMove(d));
  // #endregion

  // #region undo
  // Undoing means going back to the previous snapshot. In a puzzle it makes
  // mistakes cheap: the player tries out ideas instead of fearing every move.
  function undo() {
    const previous = history.pop();
    if (!previous || solved) return;
    b.player = previous.player;
    b.boxes = previous.boxes;
  }
  onButtonPress("undo", undo);
  onButtonPress("restart", () => go("game", n, total));
  // #endregion

  // #region dead-ends
  // A box off its goal with a wall above or below AND another to one side is
  // in a corner: it can never be pushed out. The level has become unsolvable,
  // and pointing it out teaches you to spot these states before reaching them.
  function isStuck(c) {
    if (b.goals.has(keyOf(c))) return false;
    const v = isWall(c.add(0, -1)) || isWall(c.add(0, 1));
    const h = isWall(c.add(-1, 0)) || isWall(c.add(1, 0));
    return v && h;
  }
  // #endregion

  // #region win
  function checkWin() {
    if (!b.boxes.every((c) => b.goals.has(keyOf(c)))) return;
    solved = true;
    const sum = total + history.length;
    wait(1.2, () => (n + 1 < LEVELS.length ? go("game", n + 1, sum) : go("end", sum)));
  }
  // #endregion

  // #region drawing
  // The board is drawn every frame from the state: there are no objects to
  // keep in sync, so undoing only has to change the data.
  const cellPos = (p) => origin.add(p.scale(TILE));
  onDraw(() => {
    for (let y = 0; y < b.height; y++)
      for (let x = 0; x < b.width; x++) {
        const p = vec2(x, y);
        if (isWall(p)) drawRect({ pos: cellPos(p), width: TILE, height: TILE, color: rgb(92, 78, 120) });
        if (b.floor.has(keyOf(p))) drawRect({ pos: cellPos(p), width: TILE, height: TILE, color: rgb(52, 44, 70) });
        if (b.goals.has(keyOf(p)))
          drawCircle({ pos: cellPos(p).add(TILE / 2), radius: 7, color: rgb(255, 205, 80) });
      }
    for (const c of b.boxes) {
      const onGoal = b.goals.has(keyOf(c));
      const tint = onGoal ? rgb(110, 210, 120) : isStuck(c) ? rgb(230, 80, 70) : rgb(200, 140, 80);
      drawRect({ pos: cellPos(c).add(3), width: TILE - 6, height: TILE - 6, radius: 4, color: tint });
    }
    drawCircle({ pos: cellPos(b.player).add(TILE / 2), radius: TILE / 2 - 5, color: rgb(120, 200, 255) });
  });

  const hud = add([text("", { size: 16, font: FONT, width: 200 }), pos(440, 40)]);
  onUpdate(() => {
    const warning = b.boxes.some(isStuck)
      ? t({ es: "\n\nUna caja quedó atascada. Pulsa Z para deshacer.", en: "\n\nA box is stuck. Press Z to undo." })
      : "";
    const done = solved ? t({ es: "\n\n¡Resuelto!", en: "\n\nSolved!" }) : "";
    hud.text =
      t({ es: `Nivel ${n + 1} de ${LEVELS.length}\nMovimientos: `, en: `Level ${n + 1} of ${LEVELS.length}\nMoves: ` }) +
      history.length + warning + done;
  });
  // #endregion

  // #region mouse
  // With a mouse or touch: tap where you want to go relative to the player.
  // Two on-screen buttons repeat Z and R.
  function screenButton(label, y, action) {
    const btn = add([rect(170, 32, { radius: 6 }), pos(440, y), color(70, 60, 100), area()]);
    btn.add([text(label, { size: 15, font: FONT }), pos(12, 8)]);
    btn.onClick(action);
  }
  screenButton(t({ es: "Deshacer (Z)", en: "Undo (Z)" }), 250, undo);
  screenButton(t({ es: "Reiniciar (R)", en: "Restart (R)" }), 292, () => go("game", n, total));
  onMousePress(() => {
    if (mousePos().x > 430) return; // clicks on the buttons do not move the player
    const d = mousePos().sub(cellPos(b.player).add(TILE / 2));
    if (Math.max(Math.abs(d.x), Math.abs(d.y)) < TILE / 2) return;
    tryMove(Math.abs(d.x) > Math.abs(d.y) ? vec2(Math.sign(d.x), 0) : vec2(0, Math.sign(d.y)));
  });
  // #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: 540 }),
    pos(center().add(0, 30)),
    anchor("center"),
    color(220, 200, 255),
  ]);
  onButtonPress("start", next);
  onClick(next);
}

scene("title", () =>
  screen(
    t({ es: "Empuja las cajas", en: "Push the boxes" }),
    t({
      es: "Lleva cada caja a una meta amarilla. Solo puedes empujar, nunca tirar.\nFlechas para moverte, Z deshace, R reinicia.\nPulsa espacio o toca para empezar.",
      en: "Move every box onto a yellow goal. You can only push, never pull.\nArrows to move, Z undoes, R restarts.\nPress space or tap to start.",
    }),
    () => go("game", 0, 0),
  ),
);

scene("end", (total) =>
  screen(
    t({ es: "¡Resolviste los tres niveles!", en: "You solved all three levels!" }),
    t({
      es: `Movimientos en total: ${total}. ¿Puedes hacerlo con menos?\nPulsa espacio para repetir.`,
      en: `Total moves: ${total}. Can you do it in fewer?\nPress space to play again.`,
    }),
    () => go("game", 0, 0),
  ),
);

go("title");
// #endregion

Retos de diseño

  • Diseñar niveles que enseñen sin texto, de modo que cada uno presente una idea y el siguiente la ponga a prueba en otra situación.
  • Comprobar que cada nivel tiene solución y que no hay atajos que la arruinen, a menudo con un programa que lo resuelva por fuerza bruta.
  • Que equivocarse sea barato, con deshacer y reiniciar inmediatos, para que el jugador experimente sin miedo.
  • Ajustar la dificultad para que el momento de comprender la solución llegue tras un esfuerzo justo, ni regalado ni imposible.

Historia

  1. 1982
    OrigenSokoban

    Las computadoras ya adaptaban juegos de lógica tradicionales, como el Nim o el ahorcado. Sokoban, de 1982, dio nombre a todo un tipo de puzle de empujar cajas.

    Puzzle video game (Wikipedia)
  2. 1985
    Tetris

    Tetris revoluciona y populariza el género, y lleva los puzles a un público masivo.

    Puzzle video game (Wikipedia)
  3. 1991
    Lemmings

    Lemmings plantea puzles de control indirecto, en los que guías a criaturas que caminan solas repartiendo habilidades limitadas.

    Lemmings (video game) (Wikipedia)
  4. 2007
    Portal

    Portal se cita como ejemplo de andamiaje didáctico, porque cada sala enseña una idea antes de combinarla con otras.

    Portal (video game) (Wikipedia)
  5. 2013

    Stephen Lavelle publica PuzzleScript, un lenguaje libre para crear puzles por turnos en el navegador, que acerca el diseño de puzles a cualquiera.

    Increpare (Wikipedia)
  6. 2019
    Baba Is You

    Baba Is You, nacido en la Nordic Game Jam de 2017, convierte las propias reglas en bloques que el jugador empuja y reescribe.

    Baba Is You (Wikipedia)

Juegos de referencia

1982

Sokoban

Thinking Rabbit (Hiroyuki Imabayashi). NEC PC-8801

Empujas cajas por un almacén visto desde arriba hasta dejar cada una sobre su lugar. Solo puedes empujar, nunca tirar, así que un mal movimiento puede dejar el nivel sin solución.

1991

Lemmings

DMA Design. Amiga

Unas criaturas caminan sin pensar hacia el peligro y tú no las controlas directamente; repartes un número limitado de habilidades (cavar, bloquear, construir) para que bastantes lleguen a la salida.

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.

2015

Bokosan

Gabor Héja. navegador

Un Sokoban al revés, en el que tiras de las cajas en lugar de empujarlas para devolverlas a su sitio. Se hizo para la competición js13kGames de 2015, que limita el juego a 13 kilobytes.

2019

Baba Is You

Hempuli (Arvi Teikari). Windows, Nintendo Switch

Las reglas del nivel son bloques de palabras («BABA IS YOU», «WALL IS STOP») que puedes empujar; al romper o formar frases cambias qué eres, qué te detiene y qué te hace ganar.

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 los tres niveles de la demo y anota qué idea enseña cada uno. Luego diseña en el mapa de texto un nivel nuevo que enseñe una sola idea y pruébalo con un compañero sin darle pistas.

Subgéneros

  • Bloques que caenLas piezas caen desde arriba y tú las mueves y giras para encajarlas abajo; al completar una línea o un grupo, desaparecen. El reto es ordenar rápido sin dejar huecos mientras la caída se acelera.
  • Combinación de fichasMueves, intercambias o eliges fichas en una cuadrícula para juntar piezas iguales, que desaparecen y dejan caer otras. El reto es leer el tablero y preparar reacciones en cadena.
  • Objetos ocultosBuscas en una escena llena de detalles una lista de objetos escondidos y los señalas con el cursor o con el dedo. El reto es la observación: distinguir lo que buscas entre el desorden, a veces contra el reloj.
  • Plataformas y puzlesUsas 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.
  • Puzles de físicaResuelves cada nivel aprovechando una física coherente: lanzas, apilas, equilibras o armas artefactos para que la gravedad, el impulso y los choques hagan el trabajo. El reto es prever cómo reaccionarán los objetos.
  • Puzles de palabrasFormas, adivinas o completas palabras a partir de letras sueltas o de pistas, como en anagramas, crucigramas o sopas de letras. El reto es tu vocabulario y tu ortografía.
  • SokobanEmpujas cajas u otros bloques por una cuadrícula hasta dejarlos en su sitio; como solo puedes empujar, nunca tirar, tienes que planear varios movimientos por adelantado para no dejar una pieza atascada contra la pared.

Fuentes