Saltar al contenido
GameGenresEnglish
Aventura

Aventura

Avanzas explorando lugares, hablando con personajes y resolviendo puzles con los objetos que recoges, sin que importen tus reflejos. El reto es entender cómo funciona el mundo de la historia.

También se llama: videojuego de aventura, aventura gráfica

Mecánicas que lo definen

  • Exploración de un mundo dividido en lugares (habitaciones, pantallas o descripciones) que recorres a tu ritmo.
  • Inventario, es decir, recoger objetos y usarlos sobre el escenario o combinarlos entre sí.
  • Una interfaz para dar órdenes: escribirlas («abre la puerta») en las aventuras de texto, o elegir un verbo y hacer clic en las point-and-click.
  • Diálogos con personajes, a menudo con opciones, que dan pistas y hacen avanzar la historia.

Bucle de juego

Observas un lugar, imaginas una solución («¿y si uso esto aquí?»), la pruebas, el mundo te responde y se abre un lugar o una parte nueva de la historia.

Demo propia

Controles. Todo con el ratón o el dedo. Elige el verbo Mirar o Tomar y haz clic en las cosas. Para usar un objeto, tócalo en el inventario y luego toca dónde quieres usarlo. Las flechas llevan a otro lugar.

Abrir en pantalla completa

Una aventura point-and-click de tres lugares (jardín, taller y estudio) para recuperar el cuaderno que una inventora dejó en su estudio cerrado con llave. Tiene dos puzles de inventario encadenados, y para llegar al cuaderno hay que resolver los dos.

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

Cómo está hecha

  1. Configuración

    Se crea el juego con un tamaño fijo. El teclado solo sirve para salir de la pantalla de título, porque el resto se juega con clics, como las aventuras point-and-click.

    // Everything is played with the mouse or a finger; the keyboard only starts the game.
    // That is why there are no on-screen touch buttons: you tap the scene itself.
    kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [24, 22, 40],
      buttons: { start: { keyboard: ["space", "enter"] } },
    });
    const FLOOR_Y = 200; // height where the floor of every room begins
  2. El mundo como datos

    Cada habitación es una lista de objetos con su nombre, su forma, el texto que se dice al mirarlo y, si corresponde, si se puede tomar o a qué lugar lleva. Algunos textos son funciones que cambian según el estado. Añadir un lugar o un objeto es escribir datos, no programar.

    // The world is DATA: rooms with objects. Each object has a name, a shape,
    // what you are told when you look at it and, if relevant, whether you can
    // take it ("take") or which room it leads to ("exit").
    let gameState; // created at the start of every game (region "state")
    const ROOMS = {
      garden: {
        name: { es: "Jardín", en: "Garden" },
        enter: { es: "El jardín de la casa. La puerta del estudio está al fondo.", en: "The garden of the house. The study door is at the back." },
        sky: [120, 170, 210],
        floor: [90, 140, 80],
        objects: [
          { id: "tree", shape: "tree", x: 30, y: 20, w: 150, h: 210,
            name: { es: "árbol", en: "tree" },
            look: () => gameState.keyInTree
              ? { es: "Un árbol alto. ¡Hay una llave colgada de una rama!", en: "A tall tree. There's a key hanging from a branch!" }
              : { es: "Un árbol alto. Da buena sombra.", en: "A tall tree. It gives good shade." },
            noTake: { es: "La llave está muy alta. Necesito algo largo para alcanzarla.", en: "The key is too high. I need something long to reach it." } },
          { id: "key", shape: "key", x: 196, y: 214, w: 30, h: 16, take: true, color: [250, 210, 70],
            name: { es: "llave", en: "key" }, visible: () => gameState.keyFell,
            look: { es: "Una llave de hierro.", en: "An iron key." } },
          { id: "door", shape: "door", x: 330, y: 70, w: 80, h: 140, exit: "study",
            name: { es: "puerta del estudio", en: "study door" }, locked: () => !gameState.doorOpen,
            look: { es: "La puerta del estudio. Está cerrada con llave.", en: "The study door. It's locked." } },
          { id: "to-workshop", shape: "arrow", x: 566, y: 110, w: 60, h: 60, exit: "workshop", dir: 1, name: { es: "ir al taller", en: "go to the workshop" } },
        ],
      },
      workshop: {
        name: { es: "Taller", en: "Workshop" },
        enter: { es: "El taller huele a aceite y a madera.", en: "The workshop smells of oil and wood." },
        sky: [190, 170, 140],
        floor: [110, 90, 70],
        objects: [
          { id: "bench", x: 100, y: 130, w: 150, h: 80, color: [120, 90, 60],
            name: { es: "mesa de trabajo", en: "workbench" },
            look: { es: "Una mesa llena de tornillos y engranajes.", en: "A bench covered in screws and gears." } },
          { id: "pole", shape: "pole", x: 310, y: 40, w: 20, h: 170, take: true, color: [200, 160, 80],
            name: { es: "palo largo", en: "long pole" },
            look: { es: "Un palo largo de madera, apoyado en la pared.", en: "A long wooden pole, leaning on the wall." } },
          { id: "to-garden", shape: "arrow", x: 16, y: 110, w: 60, h: 60, exit: "garden", dir: -1, name: { es: "volver al jardín", en: "back to the garden" } },
        ],
      },
      study: {
        name: { es: "Estudio", en: "Study" },
        enter: { es: "El estudio, en penumbra.", en: "The study, in the half-light." },
        sky: [90, 70, 110],
        floor: [70, 50, 60],
        objects: [
          { id: "drawer", x: 110, y: 140, w: 200, h: 70, color: [150, 100, 70],
            name: { es: "cajonera", en: "chest of drawers" },
            look: { es: "Los cajones solo guardan lápices sin punta.", en: "The drawers only hold blunt pencils." } },
          { id: "notebook", shape: "notebook", x: 410, y: 160, w: 70, h: 44, take: true, color: [70, 130, 200],
            name: { es: "cuaderno", en: "notebook" },
            look: { es: "¡El cuaderno de la inventora! Es lo que vine a buscar.", en: "The inventor's notebook! This is what I came for." } },
          { id: "to-garden", shape: "arrow", x: 16, y: 110, w: 60, h: 60, exit: "garden", dir: -1, name: { es: "volver al jardín", en: "back to the garden" } },
        ],
      },
    };
  3. Los puzles

    Un puzle de inventario es un par «objeto > destino». Usar el palo en el árbol tira la llave, y usar la llave en la puerta la abre. Como un puzle prepara el siguiente, se forma una cadena de dependencias, que es el esqueleto de cualquier aventura.

    // Puzzles are "inventory item > scene object" pairs.
    // Each one changes the world state and returns what the narrator says.
    const COMBOS = {
      "pole>tree": () => {
        if (!gameState.keyInTree) return { es: "Ya no hay nada en la rama.", en: "There's nothing left on the branch." };
        gameState.keyInTree = false;
        gameState.keyFell = true;
        return { es: "Sacudes la rama con el palo... ¡la llave cae al suelo!", en: "You shake the branch with the pole... the key falls to the ground!" };
      },
      "key>door": () => {
        gameState.doorOpen = true;
        gameState.inventory = gameState.inventory.filter((i) => i !== "key");
        return { es: "La llave gira con un clic. La puerta se abre.", en: "The key turns with a click. The door opens." };
      },
    };
  4. El estado del mundo

    Todo lo que cambia mientras juegas (dónde estás, qué llevas, qué tomaste y qué puertas abriste) vive en un solo objeto. Así es fácil reiniciar la partida y, más adelante, guardarla.

    // Everything that changes while you play fits in one object: that makes saving easy.
    gameState = { room: "garden", inventory: [], taken: [], keyInTree: true, keyFell: false, doorOpen: false };
    let [verb, selected] = ["look", null]; // selected: the inventory item you are using
    const value = (x) => (typeof x === "function" ? x() : x); // fixed text or text that depends on the state
    const findObject = (id) => Object.values(ROOMS).flatMap((r) => r.objects).find((o) => o.id === id);
  5. Dibujar los lugares

    Cada objeto se dibuja con formas simples y encima lleva un rectángulo invisible que recibe los clics: el hotspot, la zona sensible. Al cambiar el estado, la habitación se borra y se vuelve a construir, así lo que se ve siempre coincide con los datos.

    // Each object is an invisible rectangle that receives clicks (the "hotspot")
    // plus a few simple shapes that draw it.
    function draw(o, open) {
      const c = rgb(...(o.color ?? [140, 100, 70]));
      const d = (comps) => add([...comps, "room"]);
      const { x, y, w, h } = o;
      if (o.shape === "tree") {
        d([rect(26, h - 90), pos(x + w / 2 - 13, y + 90), color(110, 75, 45)]);
        d([circle(68), pos(x + w / 2, y + 70), color(60, 130, 70)]);
        if (gameState.keyInTree) d([rect(18, 8), pos(x + w - 40, y + 40), color(250, 210, 70)]);
      } else if (o.shape === "door") {
        d([rect(w + 140, h + 20), pos(x - 70, y - 20), color(190, 180, 170)]); // house wall
        d([rect(w, h), pos(x, y), color(open ? rgb(20, 15, 25) : rgb(120, 70, 40))]);
        if (!open) d([circle(5), pos(x + w - 14, y + h / 2), color(240, 200, 80)]);
      } else if (o.shape === "arrow") {
        const p = o.dir > 0 ? [vec2(0, 0), vec2(w, h / 2), vec2(0, h)] : [vec2(w, 0), vec2(0, h / 2), vec2(w, h)];
        d([polygon(p), pos(x, y), color(255, 255, 255), opacity(0.6)]);
      } else if (o.shape === "pole") {
        d([rect(8, h), pos(x + w / 2 - 4, y), color(c)]);
      } else if (o.shape === "key") {
        d([circle(7), pos(x + 7, y + 8), color(c)]);
        d([rect(w - 10, 5), pos(x + 10, y + 6), color(c)]);
      } else if (o.shape === "notebook") {
        d([rect(w, h), pos(x, y), color(c)]);
        d([rect(6, h), pos(x, y), color(40, 70, 120)]); // spine
      } else {
        d([rect(w, h), pos(x, y), color(c), outline(2, rgb(0, 0, 0))]);
      }
    }
    
    function buildRoom() {
      destroyAll("room");
      const room = ROOMS[gameState.room];
      add([rect(640, FLOOR_Y), color(...room.sky), "room"]);
      add([rect(640, 270 - FLOOR_Y), pos(0, FLOOR_Y), color(...room.floor), "room"]);
      add([text(t(room.name), { size: 14, font: FONT }), pos(628, 60), anchor("topright"), opacity(0.8), "room"]);
      for (const o of room.objects) {
        if ((o.visible && !o.visible()) || gameState.taken.includes(o.id)) continue;
        draw(o, o.locked && !o.locked()); // fill: false → invisible, but its area still receives clicks
        add([rect(o.w, o.h, { fill: false }), pos(o.x, o.y), area(), "room", "hotspot", { data: o }]);
      }
    }
  6. Verbos e inventario

    Abajo están los verbos y las casillas del inventario, y arriba el texto de la narración. Es la interfaz clásica de las point-and-click, en la que primero dices qué hacer y luego con qué.

    // Bottom: the verbs (look, take) and the inventory. Top: the narration.
    add([rect(640, 90), pos(0, 270), color(30, 28, 50), z(5)]);
    add([rect(624, 44), pos(8, 8), color(0, 0, 0), opacity(0.6), z(5)]);
    const message = add([text("", { size: 15, font: FONT, width: 604 }), pos(18, 14), z(6)]);
    const statusLine = add([text("", { size: 14, font: FONT }), pos(320, 252), anchor("center"), z(6), color(255, 240, 180)]);
    const say = (txt) => (message.text = t(txt));
    
    const VERBS = [{ id: "look", name: { es: "Mirar", en: "Look" } }, { id: "take", name: { es: "Tomar", en: "Take" } }];
    VERBS.forEach((v, i) => {
      const b = add([rect(96, 32, { radius: 6 }), pos(14, 280 + i * 38), color(70, 70, 120), area(), z(6), "verb", { verb: v.id }]);
      b.add([text(t(v.name), { size: 16, font: FONT }), pos(48, 16), anchor("center")]);
    });
    
    function drawInventory() {
      destroyAll("slot");
      gameState.inventory.forEach((id, i) => {
        const o = findObject(id);
        const s = add([rect(110, 60, { radius: 6 }), pos(130 + i * 120, 285), color(selected === id ? rgb(120, 110, 40) : rgb(55, 52, 85)), area(), z(6), "slot", { item: id }]);
        s.add([circle(12), pos(55, 22), color(...o.color)]);
        s.add([text(t(o.name), { size: 13, font: FONT }), pos(55, 46), anchor("center")]);
      });
    }
  7. Qué pasa al hacer clic

    Un solo manejador de clics decide qué tocaste y la función interact resuelve la acción: combinar un objeto, cruzar una salida, tomar o mirar. La línea de estado anticipa lo que harías («Usar palo largo con árbol»), una ayuda pequeña que evita muchos clics a ciegas.

    // A single click handler decides what you touched: a verb, the inventory
    // or an object in the scene (the topmost one if they overlap).
    function interact(o) {
      if (selected) {
        const combo = COMBOS[`${selected}>${o.id}`];
        say(combo ? combo() : { es: "Eso no sirve ahí.", en: "That doesn't work there." });
        selected = null;
      } else if (o.exit && !(o.locked && o.locked())) {
        gameState.room = o.exit;
        say(ROOMS[o.exit].enter);
      } else if (verb === "take" && o.take) {
        gameState.taken.push(o.id);
        if (o.id === "notebook") return go("end");
        gameState.inventory.push(o.id);
        say({ es: `Tomas: ${t(o.name)}.`, en: `You take: ${t(o.name)}.` });
      } else if (verb === "take") {
        say(o.noTake ?? { es: "No puedo llevarme eso.", en: "I can't take that." });
      } else {
        say(value(o.look) ?? o.name);
      }
      buildRoom();
      drawInventory();
    }
    
    onClick(() => {
      const verbButton = get("verb").find((b) => b.isHovering());
      const slot = get("slot").find((s) => s.isHovering());
      const hotspot = get("hotspot").reverse().find((h) => h.isHovering());
      if (verbButton) [verb, selected] = [verbButton.verb, null];
      else if (slot) selected = selected === slot.item ? null : slot.item;
      else if (hotspot) return interact(hotspot.data);
      else selected = null;
      drawInventory();
    });
    
    // The status line says what a click would do: "Use long pole with tree".
    onUpdate(() => {
      const hotspot = get("hotspot").reverse().find((h) => h.isHovering());
      for (const b of get("verb")) b.color = b.verb === verb && !selected ? rgb(110, 110, 190) : rgb(70, 70, 120);
      const name = hotspot ? t(hotspot.data.name) : "";
      if (selected) statusLine.text = t({ es: "Usar ", en: "Use " }) + t(findObject(selected).name) + t({ es: " con ", en: " with " }) + name;
      else statusLine.text = hotspot?.data.exit ? name : `${t(VERBS.find((v) => v.id === verb).name)} ${name}`;
      setCursor(hotspot || get("verb").concat(get("slot")).some((b) => b.isHovering()) ? "pointer" : "default");
    });
    
    say({ es: "La inventora dejó su cuaderno en el estudio, y la puerta está cerrada. ¿Dónde estará la llave?", en: "The inventor left her notebook in the study, and the door is locked. Where could the key be?" });
    buildRoom();
    drawInventory();
  8. Escenas

    El juego tiene tres escenas (title, game y end). Tomar el cuaderno lleva a la escena de fin, y desde ahí se puede empezar de nuevo.

    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);
      onClick(next);
    }
    
    scene("title", () => screen(t({ es: "La casa de la inventora", en: "The inventor's house" }),
      t({
        es: "Elige un verbo (Mirar o Tomar) y haz clic en las cosas. Para usar algo del inventario, tócalo y luego toca dónde quieres usarlo.\nHaz clic o pulsa espacio para empezar.",
        en: "Pick a verb (Look or Take) and click on things. To use something from your inventory, tap it and then tap where you want to use it.\nClick or press space to start.",
      }),
      () => go("game")));
    
    scene("end", () => screen(t({ es: "¡Encontraste el cuaderno!", en: "You found the notebook!" }),
      t({ es: "Ahora se puede terminar el invento. Haz clic o pulsa espacio para jugar otra vez.", en: "Now the invention can be finished. Click or press space to play again." }),
      () => go("game")));
    
    go("title");

Qué simplificamos

  • Solo hay dos verbos, Mirar y Tomar, más usar objetos. Las aventuras clásicas de Lucasfilm tenían una docena o más.
  • No hay protagonista que camine por la escena ni diálogos con personajes; el clic actúa al instante.
  • Los lugares y objetos son formas simples, sin ilustraciones, y no se puede guardar la partida.

Para modificarla

  • Agrega un tercer puzle, por ejemplo una regadera del taller que hay que llenar en un grifo del jardín para regar una planta que esconde algo.
  • Traduce todos los textos a un tercer idioma. Añade una clave nueva (por ejemplo fr o qu) a cada texto y haz que la función t la use cuando la URL lo pida.
  • Añade un personaje con el que se pueda hablar y que dé una pista cuando el jugador lleve un rato sin avanzar.
Código completo
// Adventure demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";

// #region setup
// Everything is played with the mouse or a finger; the keyboard only starts the game.
// That is why there are no on-screen touch buttons: you tap the scene itself.
kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [24, 22, 40],
  buttons: { start: { keyboard: ["space", "enter"] } },
});
const FLOOR_Y = 200; // height where the floor of every room begins
// #endregion

// #region world
// The world is DATA: rooms with objects. Each object has a name, a shape,
// what you are told when you look at it and, if relevant, whether you can
// take it ("take") or which room it leads to ("exit").
let gameState; // created at the start of every game (region "state")
const ROOMS = {
  garden: {
    name: { es: "Jardín", en: "Garden" },
    enter: { es: "El jardín de la casa. La puerta del estudio está al fondo.", en: "The garden of the house. The study door is at the back." },
    sky: [120, 170, 210],
    floor: [90, 140, 80],
    objects: [
      { id: "tree", shape: "tree", x: 30, y: 20, w: 150, h: 210,
        name: { es: "árbol", en: "tree" },
        look: () => gameState.keyInTree
          ? { es: "Un árbol alto. ¡Hay una llave colgada de una rama!", en: "A tall tree. There's a key hanging from a branch!" }
          : { es: "Un árbol alto. Da buena sombra.", en: "A tall tree. It gives good shade." },
        noTake: { es: "La llave está muy alta. Necesito algo largo para alcanzarla.", en: "The key is too high. I need something long to reach it." } },
      { id: "key", shape: "key", x: 196, y: 214, w: 30, h: 16, take: true, color: [250, 210, 70],
        name: { es: "llave", en: "key" }, visible: () => gameState.keyFell,
        look: { es: "Una llave de hierro.", en: "An iron key." } },
      { id: "door", shape: "door", x: 330, y: 70, w: 80, h: 140, exit: "study",
        name: { es: "puerta del estudio", en: "study door" }, locked: () => !gameState.doorOpen,
        look: { es: "La puerta del estudio. Está cerrada con llave.", en: "The study door. It's locked." } },
      { id: "to-workshop", shape: "arrow", x: 566, y: 110, w: 60, h: 60, exit: "workshop", dir: 1, name: { es: "ir al taller", en: "go to the workshop" } },
    ],
  },
  workshop: {
    name: { es: "Taller", en: "Workshop" },
    enter: { es: "El taller huele a aceite y a madera.", en: "The workshop smells of oil and wood." },
    sky: [190, 170, 140],
    floor: [110, 90, 70],
    objects: [
      { id: "bench", x: 100, y: 130, w: 150, h: 80, color: [120, 90, 60],
        name: { es: "mesa de trabajo", en: "workbench" },
        look: { es: "Una mesa llena de tornillos y engranajes.", en: "A bench covered in screws and gears." } },
      { id: "pole", shape: "pole", x: 310, y: 40, w: 20, h: 170, take: true, color: [200, 160, 80],
        name: { es: "palo largo", en: "long pole" },
        look: { es: "Un palo largo de madera, apoyado en la pared.", en: "A long wooden pole, leaning on the wall." } },
      { id: "to-garden", shape: "arrow", x: 16, y: 110, w: 60, h: 60, exit: "garden", dir: -1, name: { es: "volver al jardín", en: "back to the garden" } },
    ],
  },
  study: {
    name: { es: "Estudio", en: "Study" },
    enter: { es: "El estudio, en penumbra.", en: "The study, in the half-light." },
    sky: [90, 70, 110],
    floor: [70, 50, 60],
    objects: [
      { id: "drawer", x: 110, y: 140, w: 200, h: 70, color: [150, 100, 70],
        name: { es: "cajonera", en: "chest of drawers" },
        look: { es: "Los cajones solo guardan lápices sin punta.", en: "The drawers only hold blunt pencils." } },
      { id: "notebook", shape: "notebook", x: 410, y: 160, w: 70, h: 44, take: true, color: [70, 130, 200],
        name: { es: "cuaderno", en: "notebook" },
        look: { es: "¡El cuaderno de la inventora! Es lo que vine a buscar.", en: "The inventor's notebook! This is what I came for." } },
      { id: "to-garden", shape: "arrow", x: 16, y: 110, w: 60, h: 60, exit: "garden", dir: -1, name: { es: "volver al jardín", en: "back to the garden" } },
    ],
  },
};
// #endregion

// #region puzzles
// Puzzles are "inventory item > scene object" pairs.
// Each one changes the world state and returns what the narrator says.
const COMBOS = {
  "pole>tree": () => {
    if (!gameState.keyInTree) return { es: "Ya no hay nada en la rama.", en: "There's nothing left on the branch." };
    gameState.keyInTree = false;
    gameState.keyFell = true;
    return { es: "Sacudes la rama con el palo... ¡la llave cae al suelo!", en: "You shake the branch with the pole... the key falls to the ground!" };
  },
  "key>door": () => {
    gameState.doorOpen = true;
    gameState.inventory = gameState.inventory.filter((i) => i !== "key");
    return { es: "La llave gira con un clic. La puerta se abre.", en: "The key turns with a click. The door opens." };
  },
};
// #endregion

scene("game", () => {
  // #region state
  // Everything that changes while you play fits in one object: that makes saving easy.
  gameState = { room: "garden", inventory: [], taken: [], keyInTree: true, keyFell: false, doorOpen: false };
  let [verb, selected] = ["look", null]; // selected: the inventory item you are using
  const value = (x) => (typeof x === "function" ? x() : x); // fixed text or text that depends on the state
  const findObject = (id) => Object.values(ROOMS).flatMap((r) => r.objects).find((o) => o.id === id);
  // #endregion

  // #region drawing
  // Each object is an invisible rectangle that receives clicks (the "hotspot")
  // plus a few simple shapes that draw it.
  function draw(o, open) {
    const c = rgb(...(o.color ?? [140, 100, 70]));
    const d = (comps) => add([...comps, "room"]);
    const { x, y, w, h } = o;
    if (o.shape === "tree") {
      d([rect(26, h - 90), pos(x + w / 2 - 13, y + 90), color(110, 75, 45)]);
      d([circle(68), pos(x + w / 2, y + 70), color(60, 130, 70)]);
      if (gameState.keyInTree) d([rect(18, 8), pos(x + w - 40, y + 40), color(250, 210, 70)]);
    } else if (o.shape === "door") {
      d([rect(w + 140, h + 20), pos(x - 70, y - 20), color(190, 180, 170)]); // house wall
      d([rect(w, h), pos(x, y), color(open ? rgb(20, 15, 25) : rgb(120, 70, 40))]);
      if (!open) d([circle(5), pos(x + w - 14, y + h / 2), color(240, 200, 80)]);
    } else if (o.shape === "arrow") {
      const p = o.dir > 0 ? [vec2(0, 0), vec2(w, h / 2), vec2(0, h)] : [vec2(w, 0), vec2(0, h / 2), vec2(w, h)];
      d([polygon(p), pos(x, y), color(255, 255, 255), opacity(0.6)]);
    } else if (o.shape === "pole") {
      d([rect(8, h), pos(x + w / 2 - 4, y), color(c)]);
    } else if (o.shape === "key") {
      d([circle(7), pos(x + 7, y + 8), color(c)]);
      d([rect(w - 10, 5), pos(x + 10, y + 6), color(c)]);
    } else if (o.shape === "notebook") {
      d([rect(w, h), pos(x, y), color(c)]);
      d([rect(6, h), pos(x, y), color(40, 70, 120)]); // spine
    } else {
      d([rect(w, h), pos(x, y), color(c), outline(2, rgb(0, 0, 0))]);
    }
  }

  function buildRoom() {
    destroyAll("room");
    const room = ROOMS[gameState.room];
    add([rect(640, FLOOR_Y), color(...room.sky), "room"]);
    add([rect(640, 270 - FLOOR_Y), pos(0, FLOOR_Y), color(...room.floor), "room"]);
    add([text(t(room.name), { size: 14, font: FONT }), pos(628, 60), anchor("topright"), opacity(0.8), "room"]);
    for (const o of room.objects) {
      if ((o.visible && !o.visible()) || gameState.taken.includes(o.id)) continue;
      draw(o, o.locked && !o.locked()); // fill: false → invisible, but its area still receives clicks
      add([rect(o.w, o.h, { fill: false }), pos(o.x, o.y), area(), "room", "hotspot", { data: o }]);
    }
  }
  // #endregion

  // #region interface
  // Bottom: the verbs (look, take) and the inventory. Top: the narration.
  add([rect(640, 90), pos(0, 270), color(30, 28, 50), z(5)]);
  add([rect(624, 44), pos(8, 8), color(0, 0, 0), opacity(0.6), z(5)]);
  const message = add([text("", { size: 15, font: FONT, width: 604 }), pos(18, 14), z(6)]);
  const statusLine = add([text("", { size: 14, font: FONT }), pos(320, 252), anchor("center"), z(6), color(255, 240, 180)]);
  const say = (txt) => (message.text = t(txt));

  const VERBS = [{ id: "look", name: { es: "Mirar", en: "Look" } }, { id: "take", name: { es: "Tomar", en: "Take" } }];
  VERBS.forEach((v, i) => {
    const b = add([rect(96, 32, { radius: 6 }), pos(14, 280 + i * 38), color(70, 70, 120), area(), z(6), "verb", { verb: v.id }]);
    b.add([text(t(v.name), { size: 16, font: FONT }), pos(48, 16), anchor("center")]);
  });

  function drawInventory() {
    destroyAll("slot");
    gameState.inventory.forEach((id, i) => {
      const o = findObject(id);
      const s = add([rect(110, 60, { radius: 6 }), pos(130 + i * 120, 285), color(selected === id ? rgb(120, 110, 40) : rgb(55, 52, 85)), area(), z(6), "slot", { item: id }]);
      s.add([circle(12), pos(55, 22), color(...o.color)]);
      s.add([text(t(o.name), { size: 13, font: FONT }), pos(55, 46), anchor("center")]);
    });
  }
  // #endregion

  // #region actions
  // A single click handler decides what you touched: a verb, the inventory
  // or an object in the scene (the topmost one if they overlap).
  function interact(o) {
    if (selected) {
      const combo = COMBOS[`${selected}>${o.id}`];
      say(combo ? combo() : { es: "Eso no sirve ahí.", en: "That doesn't work there." });
      selected = null;
    } else if (o.exit && !(o.locked && o.locked())) {
      gameState.room = o.exit;
      say(ROOMS[o.exit].enter);
    } else if (verb === "take" && o.take) {
      gameState.taken.push(o.id);
      if (o.id === "notebook") return go("end");
      gameState.inventory.push(o.id);
      say({ es: `Tomas: ${t(o.name)}.`, en: `You take: ${t(o.name)}.` });
    } else if (verb === "take") {
      say(o.noTake ?? { es: "No puedo llevarme eso.", en: "I can't take that." });
    } else {
      say(value(o.look) ?? o.name);
    }
    buildRoom();
    drawInventory();
  }

  onClick(() => {
    const verbButton = get("verb").find((b) => b.isHovering());
    const slot = get("slot").find((s) => s.isHovering());
    const hotspot = get("hotspot").reverse().find((h) => h.isHovering());
    if (verbButton) [verb, selected] = [verbButton.verb, null];
    else if (slot) selected = selected === slot.item ? null : slot.item;
    else if (hotspot) return interact(hotspot.data);
    else selected = null;
    drawInventory();
  });

  // The status line says what a click would do: "Use long pole with tree".
  onUpdate(() => {
    const hotspot = get("hotspot").reverse().find((h) => h.isHovering());
    for (const b of get("verb")) b.color = b.verb === verb && !selected ? rgb(110, 110, 190) : rgb(70, 70, 120);
    const name = hotspot ? t(hotspot.data.name) : "";
    if (selected) statusLine.text = t({ es: "Usar ", en: "Use " }) + t(findObject(selected).name) + t({ es: " con ", en: " with " }) + name;
    else statusLine.text = hotspot?.data.exit ? name : `${t(VERBS.find((v) => v.id === verb).name)} ${name}`;
    setCursor(hotspot || get("verb").concat(get("slot")).some((b) => b.isHovering()) ? "pointer" : "default");
  });

  say({ es: "La inventora dejó su cuaderno en el estudio, y la puerta está cerrada. ¿Dónde estará la llave?", en: "The inventor left her notebook in the study, and the door is locked. Where could the key be?" });
  buildRoom();
  drawInventory();
  // #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);
  onClick(next);
}

scene("title", () => screen(t({ es: "La casa de la inventora", en: "The inventor's house" }),
  t({
    es: "Elige un verbo (Mirar o Tomar) y haz clic en las cosas. Para usar algo del inventario, tócalo y luego toca dónde quieres usarlo.\nHaz clic o pulsa espacio para empezar.",
    en: "Pick a verb (Look or Take) and click on things. To use something from your inventory, tap it and then tap where you want to use it.\nClick or press space to start.",
  }),
  () => go("game")));

scene("end", () => screen(t({ es: "¡Encontraste el cuaderno!", en: "You found the notebook!" }),
  t({ es: "Ahora se puede terminar el invento. Haz clic o pulsa espacio para jugar otra vez.", en: "Now the invention can be finished. Click or press space to play again." }),
  () => go("game")));

go("title");
// #endregion

Retos de diseño

  • Que los puzles sigan la lógica del mundo y no un capricho del diseñador; si la solución es absurda, el jugador termina probando todo con todo.
  • Evitar callejones sin salida, como perder un objeto imprescindible o morir sin aviso, para que el jugador explore sin miedo.
  • Responder a cada intento fallido con un texto útil, para que equivocarse también dé pistas.
  • Dosificar lugares y objetos para que el jugador no se pierda entre demasiadas opciones a la vez.
  • Las aventuras tienen mucho texto. Si los textos están separados del código desde el principio, traducirlas es mucho más fácil.

Historia

  1. 1976
    OrigenColossal Cave Adventure

    Will Crowther lo escribió en 1975 y 1976, y Don Woods lo amplió en 1977. El género toma su nombre de este juego.

    Colossal Cave Adventure (Wikipedia)
  2. 1977
    Zork

    Zork, hecho en el MIT, entiende frases completas en lugar de órdenes de dos palabras. Desde 1980 se vende dividido en tres partes para microordenadores.

    Zork (Wikipedia)
  3. 1980
    Mystery House

    Mystery House, de Roberta y Ken Williams, es la primera aventura gráfica conocida, con un dibujo para cada lugar.

    Mystery House (Wikipedia)
  4. 1984
    King's Quest: Quest for the Crown

    King's Quest pone en pantalla a un personaje animado que caminas entre los elementos del escenario. Es el primer juego de Sierra con protagonista visto en tercera persona.

    King's Quest I (Wikipedia)
  5. 1987
    Maniac Mansion

    Maniac Mansion cambia el teclado por verbos y clics, con el motor SCUMM, y fija el modelo point-and-click de Lucasfilm.

    Maniac Mansion (Wikipedia)
  6. 1990
    The Secret of Monkey Island

    The Secret of Monkey Island hace casi imposible morir, así que el juego se centra en explorar y en el humor.

    The Secret of Monkey Island (Wikipedia)
  7. 1993
    Myst

    Myst, explorado en primera persona sobre imágenes prerrenderizadas, es el juego de PC más vendido durante casi una década e impulsa el CD-ROM.

    Myst (Wikipedia)
  8. 2003
    Samorost

    Samorost, que Jakub Dvorský hizo siendo estudiante, se publica gratis en la web de Amanita Design. Muestra una aventura breve y sin texto que se juega solo con clics.

    Samorost (Wikipedia)
  9. 2012
    The Walking Dead: Season One

    The Walking Dead, de Telltale, deja los puzles en segundo plano y pone en el centro las decisiones de diálogo, cuyas consecuencias pasan de un episodio a otro.

    The Walking Dead (Wikipedia)

Juegos de referencia

1983

The Portopia Serial Murder Case

Yuji Horii (Enix). NEC PC-6001, Famicom

Investigas un asesinato en Kobe junto a tu ayudante Yasu, interrogando a sospechosos y visitando lugares. En la versión de Famicom (1985) el intérprete de texto se cambió por un menú de órdenes, el modelo de las aventuras japonesas y de la novela visual.

1984

King's Quest: Quest for the Crown

Sierra On-Line (Roberta Williams). IBM PCjr, MS-DOS, Apple II, Amiga, Atari ST, Master System

Guías a sir Graham por el reino de cuento de Daventry en busca de tres objetos mágicos. Lo mueves con el teclado entre pantallas animadas, delante y detrás de los elementos del escenario, y escribes órdenes para actuar.

1987

Maniac Mansion

Lucasfilm Games (Ron Gilbert y Gary Winnick). Commodore 64, Apple II, MS-DOS, NES

Controlas a un grupo de adolescentes que entra en la mansión de un científico loco para rescatar a una amiga. En lugar de escribir, eliges un verbo de un menú y haces clic en un objeto, con el motor SCUMM.

1990

The Secret of Monkey Island

Lucasfilm Games (Ron Gilbert, Tim Schafer y Dave Grossman). Amiga, MS-DOS, Atari ST, Macintosh

Guybrush Threepwood quiere ser pirata en un Caribe de humor absurdo. Resuelves puzles de inventario y duelos de insultos con verbos y clics, y casi nunca puedes morir, así que el juego invita a explorar sin miedo.

1993

Myst

Cyan (Rand y Robyn Miller). Macintosh, Windows

Llegas a una isla desierta a través de un libro mágico y la recorres haciendo clic sobre imágenes prerrenderizadas. No hay inventario ni enemigos; el reto es observar el mundo y deducir cómo funcionan sus máquinas.

2001

Phoenix Wright: Ace Attorney

Capcom (Shu Takumi). Game Boy Advance, Nintendo DS, Windows, iOS, Android

Eres un abogado defensor novato. Alternas investigaciones, en las que reúnes pruebas, con juicios en los que interrogas a testigos y señalas la contradicción entre su declaración y una prueba.

2003

Samorost

Amanita Design (Jakub Dvorský). navegador, Windows, macOS, iOS, Android

Un pequeño gnomo debe evitar que una gran nave espacial choque con su planeta. No hay texto ni inventario visible; haces clic por el escenario hasta descubrir qué reacciona y en qué orden.

2012

The Walking Dead: Season One

Telltale Games. Windows, macOS, PlayStation 3, Xbox 360, iOS

Lee Everett protege a la niña Clementine en un apocalipsis zombi. Hay pocos puzles; lo central son las decisiones de diálogo, a veces con tiempo límite, cuyas consecuencias se arrastran de un episodio a otro.

2025

Cheese is the Reason

Studio Laaya. navegador, Windows

Aventura point-and-click corta, hecha en 72 horas para la Ludum Dare 57. Bajas a las profundidades de Ciudad Rata para destapar la cañería de un cliente, hablando con personajes y usando objetos.

Ejercicio

Juega la demo y dibuja en papel qué objeto abre qué (el grafo de dependencias del puzle). Luego añade un puzle nuevo con un objeto más y escribe sus textos en castellano y en una lengua originaria que conozcas. Una aventura corta es una buena forma de contar una historia del lugar donde vives.

Subgéneros

  • Aventura point-and-clickAventura en la que, con el ratón o el dedo, señalas lugares, objetos y personajes de la escena para moverte, examinar, hablar y usar lo que llevas en el inventario. El reto es combinar objetos y pistas de los diálogos para resolver los acertijos que hacen avanzar la historia.
  • Juego de textoJuego que se presenta solo con texto, sin imágenes. Lees lo que ocurre y respondes escribiendo órdenes o eligiendo opciones, así que el mundo y el reto existen sobre todo en tu imaginación.
  • Walking simulatorAventura en la que recorres un lugar a tu ritmo, casi siempre en primera persona, y descubres una historia a través de lo que ves, lees y escuchas. No hay combate ni forma de perder; el interés está en explorar e interpretar.

Fuentes