
Adventure
You progress by exploring places, talking to characters and solving puzzles with the items you pick up, and your reflexes do not matter. The challenge is understanding how the story's world works.
Also known as: adventure game, graphic adventure
Defining mechanics
- Exploring a world split into places (rooms, screens or descriptions) that you move through at your own pace.
- An inventory, that is, picking up items and using them on the scenery or combining them with each other.
- An interface for giving orders: typing them ("open door") in text adventures, or picking a verb and clicking in point-and-click ones.
- Conversations with characters, often with options, that give clues and move the story forward.
Gameplay loop
You look at a place, imagine a solution ("what if I use this here?"), try it, the world responds and a new place or a new part of the story opens up.
Our demo
Controls. Everything with the mouse or your finger. Pick the verb Look or Take and click on things. To use an item, tap it in the inventory and then tap where you want to use it. The arrows lead to another place.
Open full screenA point-and-click adventure with three places (garden, workshop and study) in which you recover the notebook an inventor left in her locked study. It has two chained inventory puzzles, and you need to solve both to reach the notebook.
Made with Kaplay, in about 199 lines of JavaScript.
How it's made
Setup
The game is created at a fixed size. The keyboard is only used to leave the title screen, because the rest is played with clicks, like point-and-click adventures.
// 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 beginsThe world as data
Each room is a list of objects with a name, a shape, the text shown when you look at it and, where relevant, whether you can take it or which place it leads to. Some texts are functions that change with the state. Adding a place or an item means writing data, not programming.
// 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" } }, ], }, };The puzzles
An inventory puzzle is an "item > target" pair. Using the pole on the tree knocks the key down, and using the key on the door opens it. Since one puzzle sets up the next, they form a chain of dependencies, which is the skeleton of any adventure.
// 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." }; }, };The world state
Everything that changes while you play (where you are, what you carry, what you took and which doors you opened) lives in a single object. That makes it easy to restart the game and, later, to save it.
// 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);Drawing the places
Each object is drawn with simple shapes and covered by an invisible rectangle that receives clicks: the hotspot, or sensitive area. Whenever the state changes, the room is cleared and rebuilt, so what you see always matches the data.
// 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 }]); } }Verbs and inventory
The verbs and inventory slots are at the bottom, and the narration text is at the top. This is the classic point-and-click interface, where you first say what to do and then with what.
// 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")]); }); }What happens on a click
A single click handler decides what you touched, and the interact function resolves the action: combine an item, go through an exit, take or look. The status line previews what you would do ("Use long pole with tree"), a small aid that saves many blind clicks.
// 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();Scenes
The game has three scenes, title, game and end. Taking the notebook leads to the end scene, and from there you can start again.
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");
What we simplified
- There are only two verbs, Look and Take, plus using items. Lucasfilm's classic adventures had a dozen or more.
- There is no protagonist walking around the scene and no conversations with characters; a click acts instantly.
- Places and items are simple shapes, with no illustrations, and the game cannot be saved.
To modify it
- Add a third puzzle, for example a watering can in the workshop that must be filled at a tap in the garden to water a plant that hides something.
- Translate all the text into a third language. Add a new key (for example fr or qu) to every text and make the t function use it when the URL asks for it.
- Add a character you can talk to, who gives a hint when the player has been stuck for a while.
Full code
// 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");
// #endregionDesign challenges
- Making puzzles follow the logic of the world rather than a designer's whim; if the solution is absurd, the player ends up trying everything on everything.
- Avoiding dead ends, such as losing an essential item or dying without warning, so the player explores without fear.
- Answering every failed attempt with useful text, so that getting it wrong also gives clues.
- Pacing places and items so the player does not get lost among too many options at once.
- Adventures have a lot of text. If the text is kept apart from the code from the start, translating them is much easier.
History
- 1976OriginColossal Cave Adventure
Will Crowther wrote it in 1975 and 1976, and Don Woods expanded it in 1977. The genre takes its name from this game.
Colossal Cave Adventure (Wikipedia) - 1977Zork
Zork, made at MIT, understands full sentences instead of two-word commands. From 1980 it is sold split into three parts for microcomputers.
Zork (Wikipedia) - 1980Mystery House
Mystery House, by Roberta and Ken Williams, is the first known graphical adventure, with a drawing for each place.
Mystery House (Wikipedia) - 1984King's Quest: Quest for the Crown
King's Quest puts on screen an animated character that you walk among the scenery. It is Sierra's first game with a protagonist seen in the third person.
King's Quest I (Wikipedia) - 1987Maniac Mansion
Maniac Mansion swaps typing for verbs and clicks, using the SCUMM engine, and sets Lucasfilm's point-and-click model.
Maniac Mansion (Wikipedia) - 1990The Secret of Monkey Island
The Secret of Monkey Island makes dying almost impossible, so the game focuses on exploration and humour.
The Secret of Monkey Island (Wikipedia) - 1993Myst
Myst, explored in the first person over pre-rendered images, is the best-selling PC game for nearly a decade and drives the adoption of CD-ROM.
Myst (Wikipedia) - 2003Samorost
Samorost, which Jakub Dvorský made as a student, is released for free on Amanita Design's website. It shows a short, wordless adventure played with clicks alone.
Samorost (Wikipedia) - 2012The Walking Dead: Season One
Telltale's The Walking Dead pushes puzzles into the background and puts dialogue choices at the centre, with consequences that carry over between episodes.
The Walking Dead (Wikipedia)
Reference games
1976
Colossal Cave Adventure
You explore a cave system in search of treasure by typing one- or two-word commands such as "go north" or "get lamp". It is the first well-known text adventure and gave the adventure genre its name.
1977
Zork
You explore an underground empire looking for treasure by typing full sentences such as "open mailbox". Its parser understood far richer commands than Colossal Cave's, and in 1980 it was split into Zork I, II and III.
1980
Mystery House
You walk through a mansion where the guests keep disappearing; you type commands and each room is shown as a line drawing. It is the first known graphical adventure.
1983
The Portopia Serial Murder Case
You investigate a murder in Kobe with your assistant Yasu, questioning suspects and visiting places. In the Famicom version (1985) the text parser was replaced by a menu of commands, the template for Japanese adventures and the visual novel.
1984
King's Quest: Quest for the Crown
You guide Sir Graham through the fairy-tale kingdom of Daventry in search of three magical items. You walk him with the keyboard across animated screens, in front of and behind the scenery, and type commands to act.
1987
Maniac Mansion
You control a group of teenagers who break into a mad scientist's mansion to rescue a friend. Instead of typing, you pick a verb from a menu and click on an object, using the SCUMM engine.
1990
The Secret of Monkey Island
Guybrush Threepwood wants to become a pirate in an absurdly funny Caribbean. You solve inventory puzzles and insult sword fights with verbs and clicks, and you almost never die, so the game invites fearless exploration.
1993
Myst
You arrive on a deserted island through a magic book and explore it by clicking on pre-rendered images. There is no inventory and no enemies; the challenge is to observe the world and work out how its machines operate.
2001
Phoenix Wright: Ace Attorney
You are a rookie defence attorney. You alternate between investigations, where you gather evidence, and trials, where you cross-examine witnesses and point out where their testimony contradicts a piece of evidence.
2003
Samorost
A tiny gnome must stop a huge spaceship from crashing into his home planet. There is no text and no visible inventory; you click around the scenery until you find what reacts and in which order.
2012
The Walking Dead: Season One
Lee Everett protects a young girl, Clementine, during a zombie apocalypse. There are few puzzles; the core is dialogue choices, sometimes timed, whose consequences carry over from one episode to the next.
2025
Cheese is the Reason
A short point-and-click adventure made in 72 hours for Ludum Dare 57. You go down into the depths of Rat City to unclog a customer's pipe, talking to characters and using items.
Exercise
Play the demo and draw on paper which item unlocks what (the puzzle's dependency graph). Then add a new puzzle with one more item and write its text in Spanish and in an Indigenous language you know. A short adventure is a good way to tell a story about the place where you live.
Subgenres
- Point-and-click adventureAn adventure where you use the mouse or your finger to point at places, objects and characters in the scene to move, examine, talk and use what is in your inventory. The challenge is combining items and dialogue clues to solve the puzzles that move the story forward.
- Text-based gameA game presented in text alone, with no images. You read what happens and respond by typing commands or picking options, so the world and the challenge live mostly in your imagination.
- Walking simulatorAn adventure where you wander a place at your own pace, usually in first person, and uncover a story through what you see, read and hear. There is no combat and no way to lose; the interest lies in exploring and interpreting.