
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 completaUna 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
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 beginsEl 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" } }, ], }, };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." }; }, };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);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 }]); } }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")]); }); }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();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");
// #endregionRetos 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
- 1976OrigenColossal 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) - 1977Zork
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) - 1980Mystery House
Mystery House, de Roberta y Ken Williams, es la primera aventura gráfica conocida, con un dibujo para cada lugar.
Mystery House (Wikipedia) - 1984King'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) - 1987Maniac 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) - 1990The 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) - 1993Myst
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) - 2003Samorost
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) - 2012The 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
1976
Colossal Cave Adventure
Exploras una red de cuevas en busca de tesoros escribiendo órdenes de una o dos palabras, como «go north» o «get lamp». Es la primera aventura de texto conocida y dio nombre al género de aventura.
1977
Zork
Exploras un imperio subterráneo en busca de tesoros escribiendo frases completas como «abre el buzón». Su intérprete entendía órdenes mucho más ricas que las de Colossal Cave, y en 1980 se dividió en Zork I, II y III.
1980
Mystery House
Recorres una mansión donde van desapareciendo los invitados; escribes órdenes y cada habitación se muestra con un dibujo de líneas. Es la primera aventura gráfica conocida.
1983
The Portopia Serial Murder Case
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
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
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
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
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
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
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
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
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.