docs/arcade/canyon.html
Rama main · réplica de lectura del repositorio original.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Capability Canyon - Amigo's Arcade</title>
<meta
name="description"
content="Three capabilities are scattered across a canyon, and the publish flag refuses a half-built agent."
/>
<link rel="icon" href="../assets/logo.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400;700&family=Inter:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="arcade.css" />
<style>
.arc-screen {
background: #4aa8d8;
}
.arc-screen canvas {
aspect-ratio: 30 / 16;
}
.hearts {
display: flex;
gap: 4px;
margin-top: 3px;
}
.heart {
width: 12px;
height: 11px;
background: var(--poncho);
clip-path: polygon(50% 100%, 0 38%, 0 14%, 25% 0, 50% 18%, 75% 0, 100% 14%, 100% 38%);
}
.heart.is-gone {
background: rgba(11, 12, 15, 0.45);
}
.caps {
display: flex;
gap: 6px;
margin-top: 3px;
}
.cap {
display: flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
border-radius: 99px;
background: rgba(11, 12, 15, 0.5);
border: 1px solid rgba(245, 244, 239, 0.2);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--dim);
}
.cap i {
display: block;
width: 7px;
height: 7px;
background: rgba(245, 244, 239, 0.25);
}
.cap.is-on {
color: var(--ink);
border-color: var(--sun);
}
.cap.is-on i {
background: var(--sun);
}
/* The playbook perk: a strip of the whole canyon with the three orbs and
Amigo on it, so a player who has it can plan a route. */
.map {
position: absolute;
left: 12px;
right: 12px;
bottom: 12px;
height: 6px;
z-index: 4;
background: rgba(11, 12, 15, 0.6);
border: 1px solid var(--edge);
}
.map i {
position: absolute;
top: -1px;
width: 3px;
height: 6px;
background: var(--sun);
}
.map i.is-got {
background: var(--sage);
}
.map b {
position: absolute;
top: -3px;
width: 2px;
height: 10px;
background: var(--poncho);
}
@media (max-width: 640px) {
.hearts {
gap: 3px;
margin-top: 2px;
}
.heart {
width: 9px;
height: 8px;
}
.caps {
gap: 3px;
margin-top: 2px;
}
.cap {
gap: 3px;
padding: 1px 5px;
font-size: 8px;
letter-spacing: 0.04em;
}
.cap i {
width: 5px;
height: 5px;
}
}
</style>
</head>
<body>
<div class="arc-page is-game">
<div class="arc-top">
<img class="arc-mark" src="../assets/amigo.svg" alt="" />
<h1 class="arc-title">Capability Canyon</h1>
<a class="arc-back" href="./" aria-label="Back to the arcade">←<span class="label"> the arcade</span></a>
<p class="arc-eyebrow">Nothing publishes half-built</p>
<button type="button" class="arc-fs" id="sound">Sound on</button>
<button type="button" class="arc-fs" id="fs">Fullscreen</button>
</div>
<div class="arc-stage">
<div class="arc-screen" id="screen">
<canvas
id="game"
width="480"
height="256"
aria-label="A side-scrolling canyon: Amigo collects three capabilities and reaches the publish flag"
></canvas>
<div class="arc-hud" id="hud" hidden>
<div><b>Lives</b><div class="hearts" id="hearts"></div></div>
<div><b>Tokens</b><span class="arc-value" id="tokens">0</span></div>
<div><b>Docs</b><span class="arc-value" id="docs">0/3</span></div>
<div class="arc-spacer">
<b>Spec</b>
<div class="caps">
<div class="cap" id="cap-search"><i></i>Search</div>
<div class="cap" id="cap-knowledge"><i></i>Knowledge</div>
<div class="cap" id="cap-mcp"><i></i>MCP</div>
</div>
</div>
</div>
<div class="map" id="map" hidden></div>
<div class="arc-overlay" id="overlay">
<h2>Capability Canyon</h2>
<p>
Three capabilities are scattered across the canyon. Pick up all three, then reach the
flag - the publish gate refuses a half-built agent, exactly like the real one.
</p>
<p class="arc-hint" id="perkLine"></p>
<div class="arc-actions">
<button type="button" class="arc-btn is-primary" data-action="retry">Start</button>
</div>
<p class="arc-hint">Space to start · <a href="./">back to the arcade</a></p>
</div>
</div>
</div>
<p class="arc-rotate">Turn the phone sideways for a bigger picture</p>
<div class="arc-legend">
<div><strong><kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd></strong> Walk. Momentum carries.</div>
<div><strong><kbd>Space</kbd> / <kbd>↑</kbd></strong> Jump. Hold it longer to go higher.</div>
<div><strong>Land on a drift</strong> Stomping a hallucination is the only way past it.</div>
<div><strong>The flag</strong> Opens with Search, Knowledge and MCP in hand. Not before.</div>
<div><strong><kbd>P</kbd> <kbd>R</kbd> <kbd>F</kbd></strong> Pause, restart, fullscreen.</div>
</div>
</div>
<script src="arcade.js"></script>
<script>
(function () {
"use strict";
var H = window.Amigo.harness;
var FRAMES = window.Amigo.sprites();
/* Twenty-column chunks. Rows are padded rather than aligned by hand, so
a short row is a row of sky and never a row that shifts the geometry.
# rock = ledge ^ spike o token
E drift 1/2/3 capability F publish flag */
var CW = 20;
var ROWS = 16;
var CHUNKS = [
[
"", "", "", "", "", "", "", "",
" ooo",
"", "", "", "",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "", "", "", "",
" oooo",
"",
" ========",
" E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "", "",
" oo oo",
"", "", "", "", "",
"####### #########",
"####### #########",
"####### #########",
],
[
"", "", "", "",
" 1",
" =====",
" d",
" ===",
" o",
" ===",
" o",
" ===",
" E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "", "", "", "",
" ooooo",
"",
" ==========",
" ^^^ B ^^^",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "",
" o",
"",
"==== ==========",
"",
" =====",
" ===",
" 2",
" ===== ^^^^^^ k",
"####################",
"####################",
"####################",
],
[
"", "", "", "",
" d",
" ========",
"",
" =====",
"",
" =====",
"",
" ===",
" E",
"####################",
"####################",
"####################",
],
[
"", "", "", "",
" 3",
" ======",
"",
" ====",
"",
" ===",
"",
" ===",
" ^^^^ E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "",
" p",
" ======",
"",
" =====",
"",
" =====",
" ^^^^^^^ E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "",
"====",
"",
" =====",
"",
" ====",
"",
" n",
" ===== ^^^^^^",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "",
" d",
"",
" =======",
"",
" ooooo",
" ============",
" E E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "",
" oooooo",
"", "", "", "",
" ==== ====",
" E",
"####################",
"####################",
"####################",
],
[
"", "", "", "", "", "", "", "",
"",
" F",
"", "",
" oo",
"####################",
"####################",
"####################",
],
];
var grid = [];
var spawns = { tokens: [], drifts: [], caps: [], extras: [], bills: [], flag: null };
for (var r = 0; r < ROWS; r++) grid.push([]);
CHUNKS.forEach(function (chunk, ci) {
for (var y = 0; y < ROWS; y++) {
var row = (chunk[y] || "").padEnd(CW, " ");
for (var x = 0; x < CW; x++) {
var gx = ci * CW + x;
var ch = row[x];
var tile = " ";
if (ch === "#" || ch === "=" || ch === "^") tile = ch;
else if (ch === "o") spawns.tokens.push({ x: gx * 16 + 5, y: y * 16 + 5 });
else if (ch === "E") spawns.drifts.push({ x: gx * 16, y: y * 16 });
else if (ch === "1")
spawns.caps.push({ id: "search", label: "Web search", x: gx * 16, y: y * 16 });
else if (ch === "2")
spawns.caps.push({ id: "knowledge", label: "Knowledge base", x: gx * 16, y: y * 16 });
else if (ch === "3")
spawns.caps.push({ id: "mcp", label: "MCP tools", x: gx * 16, y: y * 16 });
else if (ch === "d")
spawns.extras.push({
kind: "doc",
id: null,
label: "Document chunk",
x: gx * 16 + 2,
y: y * 16 + 2,
});
else if (ch === "p")
spawns.extras.push({
kind: "schema",
id: "skill.structured",
label: "Structured output",
x: gx * 16,
y: y * 16,
});
else if (ch === "n")
spawns.extras.push({
kind: "notion",
id: "mcp.notion",
label: "Notion MCP",
x: gx * 16,
y: y * 16,
});
else if (ch === "k")
spawns.extras.push({
kind: "chip",
id: "model.deepseek",
label: "DeepSeek",
x: gx * 16,
y: y * 16,
});
else if (ch === "B")
spawns.bills.push({ x: gx * 16, y: y * 16 - 14, w: 28, h: 30 });
else if (ch === "F") spawns.flag = { x: gx * 16, y: y * 16 };
grid[y][gx] = tile;
}
}
});
var COLS = CHUNKS.length * CW;
var LEVEL_W = COLS * 16;
var LEVEL_H = ROWS * 16;
function solidAt(gx, gy) {
if (gx < 0 || gx >= COLS || gy < 0 || gy >= ROWS) return false;
var t = grid[gy][gx];
return t === "#" || t === "=";
}
function spikeAt(gx, gy) {
return gx >= 0 && gx < COLS && gy >= 0 && gy < ROWS && grid[gy][gx] === "^";
}
var cv = document.getElementById("game");
/* Upright on a phone, the canyon trades width for height: the camera
already clamps to whatever the view is, so a narrower field shows
twenty columns instead of thirty and the picture stops sitting in a
letterbox. The level itself is sixteen rows, so height cannot grow. */
if (
window.matchMedia &&
window.matchMedia("(pointer: coarse)").matches &&
window.innerHeight > window.innerWidth
) {
cv.width = 320;
}
var ctx = cv.getContext("2d");
ctx.imageSmoothingEnabled = false;
var W = cv.width;
var HGT = cv.height;
var screenEl = document.getElementById("screen");
var overlay = document.getElementById("overlay");
var hud = document.getElementById("hud");
var heartsEl = document.getElementById("hearts");
var tokensEl = document.getElementById("tokens");
var docsEl = document.getElementById("docs");
var mapEl = document.getElementById("map");
var capEls = {
search: document.getElementById("cap-search"),
knowledge: document.getElementById("cap-knowledge"),
mcp: document.getElementById("cap-mcp"),
};
var P = { w: 10, h: 18 };
var game = null;
function perks() {
return {
doubleJump: H.has("tool.browser"),
freeHit: H.has("skill.retry"),
hearts: H.has("memory.long_term") ? 4 : 3,
keepCheckpoint: H.has("memory.session"),
magnet: H.has("tool.web_search"),
playbook: H.has("context.playbook") || H.has("data.vectors"),
slowDrifts: H.has("skill.structured"),
bonusDoc: H.has("mcp.gdrive"),
};
}
var lastCheckpoint = 40;
function reset(fromCheckpoint) {
var perk = perks();
game = {
mode: "playing",
t: 0,
perk: perk,
x: fromCheckpoint || 40,
y: 13 * 16 - P.h,
vx: 0,
vy: 0,
face: 1,
grounded: false,
coyote: 0,
jumpHeld: 0,
jumps: 0,
invuln: 0,
usedFreeHit: false,
hearts: perk.hearts,
tokens: 0,
checkpoint: fromCheckpoint || 40,
caps: { search: false, knowledge: false, mcp: false },
camera: 0,
items: spawns.tokens.map(function (t) {
return { x: t.x, y: t.y, got: false };
}),
caplings: spawns.caps.map(function (c) {
return { id: c.id, label: c.label, x: c.x, y: c.y, got: false };
}),
drifts: spawns.drifts.map(function (d) {
return { x: d.x, y: d.y, w: 13, h: 12, vx: perk.slowDrifts ? -0.28 : -0.45, dead: 0, home: d.x };
}),
extras: spawns.extras
.map(function (e) {
return { kind: e.kind, id: e.id, label: e.label, x: e.x, y: e.y, got: false };
})
.concat(
/* The Drive connection is worth one more chunk of source
material, sitting where the first ledge is. */
perk.bonusDoc
? [{ kind: "doc", id: null, label: "Document chunk", x: 432, y: 160, got: false }]
: []
),
bills: spawns.bills.map(function (b) {
return { x: b.x, y: b.y, w: b.w, h: b.h };
}),
docs: 0,
pops: [],
};
paintHud();
mapEl.hidden = !perk.playbook;
}
function paintHud() {
heartsEl.innerHTML = "";
for (var i = 0; i < game.perk.hearts; i++) {
var h = document.createElement("div");
h.className = "heart" + (i < game.hearts ? "" : " is-gone");
heartsEl.appendChild(h);
}
tokensEl.textContent = game.tokens;
docsEl.textContent = game.docs + "/3";
Object.keys(capEls).forEach(function (id) {
capEls[id].classList.toggle("is-on", game.caps[id]);
});
if (game.perk.playbook) paintMap();
}
function paintMap() {
var marks = game.caplings
.map(function (c) {
return (
'<i class="' +
(c.got ? "is-got" : "") +
'" style="left:' +
((c.x / LEVEL_W) * 100).toFixed(2) +
'%"></i>'
);
})
.join("");
mapEl.innerHTML =
marks + '<b style="left:' + ((game.x / LEVEL_W) * 100).toFixed(2) + '%"></b>';
}
function showPerks() {
var p = perks();
var active = [];
if (p.doubleJump) active.push("Browser control gives a second jump");
if (p.freeHit) active.push("Retry policy covers one hit");
if (p.hearts > 3) active.push("Long-term memory adds a heart");
if (p.keepCheckpoint) active.push("Session memory keeps your checkpoint");
if (p.magnet) active.push("Web search pulls tokens in");
if (p.playbook) active.push("The map shows the canyon");
if (p.slowDrifts) active.push("Structured output slows the drifts");
if (p.bonusDoc) active.push("Drive adds a fourth document");
document.getElementById("perkLine").textContent = active.length
? "Harness active: " + active.join(" · ")
: "Harness empty - the desert run and the night shift both feed this.";
}
function start() {
reset(game && game.perk.keepCheckpoint ? lastCheckpoint : 40);
overlay.hidden = true;
hud.hidden = false;
}
function finish(kind) {
game.mode = kind;
window.Amigo.sound.play(kind === "won" ? "win" : "fail");
var have = Object.values(game.caps).filter(Boolean).length;
var earned = [];
H.record({
canyonBestTokens: { best: game.tokens },
canyonDepth: { best: Math.floor(game.x / (CW * 16)) },
lifetimeTokens: { add: game.tokens },
});
if (kind === "won") {
H.record({ canyonClears: { add: 1 } });
earned.push(H.unlock("tool.browser"));
lastCheckpoint = 40;
}
if (game.tokens >= 40) earned.push(H.unlock("skill.compaction"));
if (game.docs >= 3) earned.push(H.unlock("data.vectors"));
if (game.caps.knowledge) earned.push(H.unlock("memory.session"));
H.played("canyon");
var got = earned.filter(Boolean);
var note = got.length
? "Harness upgraded: " +
got
.map(function (part) {
return part.name;
})
.join(", ") +
"."
: kind === "won"
? "An agent is a spec, and a spec publishes only once it is complete."
: "The canyon keeps what you collected. The harness does too.";
overlay.innerHTML = window.Amigo.continueScreen({
won: kind === "won",
title: kind === "won" ? "Published v1" : "Run ended",
reason: kind === "won" ? "" : "Out of lives",
note: note,
stats: [
["Tokens", game.tokens],
["Capabilities", have + " of 3"],
["Clears", H.stats().canyonClears],
],
retryLabel: kind === "won" ? "Run it again" : "Try again",
});
overlay.hidden = false;
window.Amigo.wireContinue(overlay, start);
if (got.length) window.Amigo.unlockToast(screenEl, got[0]);
}
function note(msg) {
window.Amigo.toast(screenEl, msg);
}
var keys = {};
var LEFT = ["ArrowLeft", "KeyA"];
var RIGHT = ["ArrowRight", "KeyD"];
var JUMP = ["Space", "ArrowUp", "KeyW"];
function held(list) {
return list.some(function (k) {
return keys[k];
});
}
document.addEventListener("keydown", function (e) {
if (LEFT.concat(RIGHT, JUMP, ["ArrowDown"]).indexOf(e.code) !== -1) e.preventDefault();
if (JUMP.indexOf(e.code) !== -1) {
if (game && game.mode !== "playing") {
start();
return;
}
if (!e.repeat) tryJump();
}
keys[e.code] = true;
});
document.addEventListener("keyup", function (e) {
keys[e.code] = false;
if (JUMP.indexOf(e.code) !== -1 && game) game.jumpHeld = 0;
});
cv.addEventListener("pointerdown", function (e) {
if (game && game.mode !== "playing") {
start();
return;
}
var r = cv.getBoundingClientRect();
var rel = (e.clientX - r.left) / r.width;
if (rel > 0.62) {
keys.Space = true;
tryJump();
} else if (rel < 0.3) keys.ArrowLeft = true;
else keys.ArrowRight = true;
});
document.addEventListener("pointerup", function () {
keys.Space = false;
keys.ArrowLeft = false;
keys.ArrowRight = false;
if (game) game.jumpHeld = 0;
});
function tryJump() {
if (!game || game.mode !== "playing") return;
var limit = game.perk.doubleJump ? 2 : 1;
if (game.coyote > 0 && game.jumps === 0) {
game.vy = -4.6;
game.coyote = 0;
game.jumps = 1;
game.jumpHeld = 1;
game.grounded = false;
window.Amigo.sound.play("jump");
} else if (game.jumps < limit) {
game.jumps++;
game.vy = -4.1;
game.jumpHeld = 1;
window.Amigo.sound.play("jump2");
for (var i = 0; i < 5; i++) {
game.pops.push({
x: game.x + 5,
y: game.y + P.h,
vx: Math.random() * 2 - 1,
vy: Math.random() * 1.2,
life: 14,
tint: "245,244,239",
});
}
}
}
function hurt(knock, reason) {
if (game.invuln > 0) return;
if (game.perk.freeHit && !game.usedFreeHit) {
game.usedFreeHit = true;
game.invuln = 90;
note("Retry policy: that one was retried, not counted.");
return;
}
game.hearts--;
game.invuln = 80;
game.vy = -3.4;
window.Amigo.sound.play("hurt");
game.vx = knock * 2.4;
paintHud();
if (reason) note(reason);
if (game.hearts <= 0) finish("lost");
}
function respawn() {
game.hearts--;
paintHud();
if (game.hearts <= 0) {
finish("lost");
return;
}
game.x = game.checkpoint;
game.y = 0;
game.vx = 0;
game.vy = 0;
game.invuln = 70;
note("Back to the last checkpoint.");
}
function overlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function moveX(dx) {
game.x += dx;
var top = Math.floor(game.y / 16);
var bot = Math.floor((game.y + P.h - 1) / 16);
var col;
var gy;
if (dx > 0) {
col = Math.floor((game.x + P.w) / 16);
for (gy = top; gy <= bot; gy++)
if (solidAt(col, gy)) {
game.x = col * 16 - P.w - 0.01;
game.vx = 0;
}
} else if (dx < 0) {
col = Math.floor(game.x / 16);
for (gy = top; gy <= bot; gy++)
if (solidAt(col, gy)) {
game.x = (col + 1) * 16 + 0.01;
game.vx = 0;
}
}
game.x = Math.max(0, Math.min(LEVEL_W - P.w, game.x));
}
function moveY(dy) {
game.y += dy;
game.grounded = false;
var left = Math.floor(game.x / 16);
var right = Math.floor((game.x + P.w - 1) / 16);
var row;
var gx;
if (dy > 0) {
row = Math.floor((game.y + P.h) / 16);
for (gx = left; gx <= right; gx++)
if (solidAt(gx, row)) {
game.y = row * 16 - P.h;
game.vy = 0;
game.grounded = true;
game.jumps = 0;
}
} else if (dy < 0) {
row = Math.floor(game.y / 16);
for (gx = left; gx <= right; gx++)
if (solidAt(gx, row)) {
game.y = (row + 1) * 16;
game.vy = 0;
game.jumpHeld = 99;
}
}
}
function touchingSpike() {
var left = Math.floor((game.x + 2) / 16);
var right = Math.floor((game.x + P.w - 2) / 16);
var row = Math.floor((game.y + P.h - 1) / 16);
return spikeAt(left, row) || spikeAt(right, row);
}
function update() {
if (!game || game.mode !== "playing") return;
if (pauseCtl.on) return;
game.t++;
if (game.invuln > 0) game.invuln--;
var accel = game.grounded ? 0.55 : 0.34;
if (held(LEFT) && !held(RIGHT)) {
game.vx -= accel;
game.face = -1;
} else if (held(RIGHT) && !held(LEFT)) {
game.vx += accel;
game.face = 1;
} else game.vx *= game.grounded ? 0.76 : 0.94;
game.vx = Math.max(-2.6, Math.min(2.6, game.vx));
if (game.grounded) game.coyote = 7;
else if (game.coyote > 0) game.coyote--;
else if (game.jumps === 0) game.jumps = 1;
if (held(JUMP) && game.jumpHeld > 0 && game.jumpHeld < 12 && game.vy < 0) {
game.jumpHeld++;
game.vy -= 0.26;
}
game.vy = Math.min(7.5, game.vy + 0.32);
moveX(game.vx);
moveY(game.vy);
if (game.y > LEVEL_H + 40) {
respawn();
return;
}
if (touchingSpike() && game.invuln === 0) {
hurt(-game.face, "Spikes: a hard failure, not a retry.");
if (game.mode !== "playing") return;
}
if (game.grounded && game.x > game.checkpoint + 40) {
game.checkpoint = game.x;
lastCheckpoint = game.x;
}
for (var i = 0; i < game.drifts.length; i++) {
var d = game.drifts[i];
if (d.dead) {
d.dead--;
continue;
}
if (Math.abs(d.x - game.x) > W) continue;
d.x += d.vx;
var ahead = Math.floor((d.x + (d.vx > 0 ? d.w + 1 : -1)) / 16);
var footY = Math.floor((d.y + d.h + 1) / 16);
var midY = Math.floor((d.y + d.h / 2) / 16);
if (solidAt(ahead, midY) || !solidAt(ahead, footY)) d.vx *= -1;
var nextFoot = Math.floor((d.y + d.h + 2.2) / 16);
if (
solidAt(Math.floor((d.x + 2) / 16), nextFoot) ||
solidAt(Math.floor((d.x + d.w - 2) / 16), nextFoot)
) {
d.y = nextFoot * 16 - d.h;
} else d.y += 2.2;
if (overlap(game.x, game.y, P.w, P.h, d.x, d.y, d.w, d.h)) {
var falling = game.vy > 1 && game.y + P.h - game.vy <= d.y + 4;
if (falling) {
d.dead = 22;
game.vy = -4.2;
window.Amigo.sound.play("stomp");
game.tokens += 2;
paintHud();
for (var s = 0; s < 8; s++) {
game.pops.push({
x: d.x + 6,
y: d.y + 5,
vx: Math.random() * 3 - 1.5,
vy: -Math.random() * 2.2,
life: 24,
tint: "245,244,239",
});
}
} else hurt(game.x < d.x ? -1 : 1, "");
if (game.mode !== "playing") return;
}
}
var reach = game.perk.magnet ? 40 : 0;
for (var k = 0; k < game.items.length; k++) {
var it = game.items[k];
if (it.got) continue;
if (reach) {
var dx = game.x + 5 - it.x;
var dy = game.y + 9 - it.y;
var dist = Math.hypot(dx, dy);
if (dist < reach && dist > 1) {
it.x += (dx / dist) * 1.8;
it.y += (dy / dist) * 1.8;
}
}
if (overlap(game.x, game.y, P.w, P.h, it.x - 2, it.y - 2, 10, 10)) {
it.got = true;
game.tokens++;
window.Amigo.sound.play("token");
paintHud();
}
}
for (var x0 = 0; x0 < game.extras.length; x0++) {
var ex = game.extras[x0];
if (ex.got) continue;
if (overlap(game.x, game.y, P.w, P.h, ex.x, ex.y, 16, 16)) {
ex.got = true;
if (ex.kind === "doc") {
game.docs++;
game.tokens += 3;
paintHud();
window.Amigo.sound.play("pickup");
note("Document chunk " + game.docs + " of 3 - embedded and searchable.");
} else {
var got = H.unlock(ex.id);
if (got) window.Amigo.unlockToast(screenEl, got);
else {
window.Amigo.sound.play("pickup");
note(ex.label + " - already in the harness.");
}
game.tokens += 5;
paintHud();
}
for (var fx = 0; fx < 12; fx++) {
game.pops.push({
x: ex.x + 8,
y: ex.y + 8,
vx: Math.random() * 4 - 2,
vy: -Math.random() * 3,
life: 28,
tint: "245,244,239",
});
}
}
}
for (var bi = 0; bi < game.bills.length; bi++) {
var bill = game.bills[bi];
if (overlap(game.x, game.y, P.w, P.h, bill.x, bill.y, bill.w, bill.h)) {
if (game.invuln === 0) {
window.Amigo.sound.play("bill");
hurt(game.x < bill.x ? -1 : 1, "Invoice: $30,000 of tokens, nobody approved it.");
if (game.mode !== "playing") return;
}
}
}
for (var c = 0; c < game.caplings.length; c++) {
var cap = game.caplings[c];
if (cap.got) continue;
if (overlap(game.x, game.y, P.w, P.h, cap.x, cap.y, 16, 16)) {
cap.got = true;
game.caps[cap.id] = true;
paintHud();
note(cap.label + " added to the spec.");
for (var q = 0; q < 14; q++) {
game.pops.push({
x: cap.x + 8,
y: cap.y + 8,
vx: Math.random() * 4 - 2,
vy: -Math.random() * 3,
life: 30,
tint: "242,193,78",
});
}
}
}
for (var p = 0; p < game.pops.length; p++) {
var pop = game.pops[p];
pop.x += pop.vx;
pop.y += pop.vy;
pop.vy += 0.16;
pop.life--;
}
game.pops = game.pops.filter(function (pop) {
return pop.life > 0;
});
if (
spawns.flag &&
overlap(game.x, game.y, P.w, P.h, spawns.flag.x + 4, spawns.flag.y, 10, 66)
) {
var have = Object.values(game.caps).filter(Boolean).length;
if (have === 3) {
finish("won");
return;
}
if (game.t % 40 === 0)
note("Publish refused: " + (3 - have) + " capability still missing.");
}
var target = Math.max(0, Math.min(LEVEL_W - W, game.x - W * 0.38));
game.camera += (target - game.camera) * 0.12;
if (game.perk.playbook && game.t % 8 === 0) paintMap();
}
function mesas(offset, color, height, spacing) {
ctx.fillStyle = color;
var base = HGT - 30;
for (var i = -1; i < 8; i++) {
var x = i * (spacing + 90) - (offset % (spacing + 90));
ctx.fillRect(x, base - height, spacing, height);
ctx.fillRect(x - 14, base - height * 0.55, spacing + 28, height);
}
}
function drawToken(it) {
var bob = Math.sin(game.t * 0.11 + it.x * 0.07) * 1.6;
var x = Math.round(it.x);
var y = Math.round(it.y + bob);
ctx.fillStyle = "#f2c14e";
ctx.fillRect(x, y, 7, 7);
ctx.fillStyle = "#c99327";
ctx.fillRect(x, y + 5, 7, 2);
ctx.fillRect(x + 5, y, 2, 7);
ctx.fillStyle = "#fff3cf";
ctx.fillRect(x + 1, y + 1, 2, 2);
}
function drawCap(c) {
var bob = Math.sin(game.t * 0.08) * 2;
var x = Math.round(c.x);
var y = Math.round(c.y + bob);
var glow = 0.22 + Math.sin(game.t * 0.12) * 0.12;
ctx.fillStyle = "rgba(242,193,78," + glow + ")";
ctx.fillRect(x - 4, y - 4, 24, 24);
ctx.fillStyle = "#1a1f2e";
ctx.fillRect(x, y, 16, 16);
ctx.fillStyle = "#f2c14e";
ctx.fillRect(x + 1, y + 1, 14, 14);
ctx.fillStyle = "#1a1f2e";
if (c.id === "search") {
ctx.fillRect(x + 4, y + 3, 6, 1);
ctx.fillRect(x + 3, y + 4, 1, 5);
ctx.fillRect(x + 10, y + 4, 1, 5);
ctx.fillRect(x + 4, y + 9, 6, 1);
ctx.fillRect(x + 10, y + 10, 3, 3);
} else if (c.id === "knowledge") {
ctx.fillRect(x + 3, y + 4, 10, 2);
ctx.fillRect(x + 3, y + 8, 10, 2);
ctx.fillRect(x + 3, y + 12, 10, 1);
} else {
ctx.fillRect(x + 5, y + 2, 2, 4);
ctx.fillRect(x + 9, y + 2, 2, 4);
ctx.fillRect(x + 3, y + 6, 10, 4);
ctx.fillRect(x + 7, y + 10, 2, 4);
}
}
function drawExtra(e) {
var bob = Math.round(Math.sin(game.t * 0.08 + e.x * 0.03) * 2);
var x = Math.round(e.x);
var y = Math.round(e.y) + bob;
if (e.kind === "doc") {
ctx.fillStyle = "rgba(212,118,58,.2)";
ctx.fillRect(x - 2, y - 2, 18, 20);
ctx.fillStyle = "#f5f4ef";
ctx.fillRect(x + 1, y, 12, 16);
ctx.fillStyle = "#d4763a";
ctx.fillRect(x + 1, y, 12, 2);
ctx.fillStyle = "#8e95a1";
ctx.fillRect(x + 3, y + 5, 8, 1);
ctx.fillRect(x + 3, y + 8, 8, 1);
ctx.fillRect(x + 3, y + 11, 5, 1);
ctx.fillStyle = "#c2c6cd";
ctx.fillRect(x + 9, y + 12, 4, 4);
return;
}
if (e.kind === "schema") {
/* Pydantic AI: a typed model, drawn as the braces that wrap one. */
ctx.fillStyle = "rgba(229,56,127,.22)";
ctx.fillRect(x - 3, y - 3, 22, 22);
ctx.fillStyle = "#7a1338";
ctx.fillRect(x, y, 16, 16);
ctx.fillStyle = "#e5387f";
ctx.fillRect(x + 1, y + 1, 14, 14);
ctx.fillStyle = "#fff0f6";
ctx.fillRect(x + 4, y + 3, 2, 1);
ctx.fillRect(x + 3, y + 4, 1, 3);
ctx.fillRect(x + 2, y + 7, 1, 2);
ctx.fillRect(x + 3, y + 9, 1, 3);
ctx.fillRect(x + 4, y + 12, 2, 1);
ctx.fillRect(x + 10, y + 3, 2, 1);
ctx.fillRect(x + 12, y + 4, 1, 3);
ctx.fillRect(x + 13, y + 7, 1, 2);
ctx.fillRect(x + 12, y + 9, 1, 3);
ctx.fillRect(x + 10, y + 12, 2, 1);
return;
}
if (e.kind === "notion") {
ctx.fillStyle = "rgba(245,244,239,.18)";
ctx.fillRect(x - 3, y - 3, 22, 22);
ctx.fillStyle = "#1a1f2e";
ctx.fillRect(x, y, 16, 16);
ctx.fillStyle = "#f5f4ef";
ctx.fillRect(x + 2, y + 2, 12, 12);
ctx.fillStyle = "#1a1f2e";
ctx.fillRect(x + 4, y + 4, 2, 8);
ctx.fillRect(x + 10, y + 4, 2, 8);
ctx.fillRect(x + 6, y + 6, 2, 2);
ctx.fillRect(x + 8, y + 8, 2, 2);
return;
}
ctx.fillStyle = "rgba(77,107,254,.22)";
ctx.fillRect(x - 3, y - 3, 22, 22);
ctx.fillStyle = "#2b3f9e";
ctx.fillRect(x, y, 16, 16);
ctx.fillStyle = "#4d6bfe";
ctx.fillRect(x + 1, y + 1, 14, 14);
ctx.fillStyle = "#eef1ff";
ctx.fillRect(x + 4, y + 6, 8, 4);
ctx.fillRect(x + 10, y + 4, 3, 3);
ctx.fillRect(x + 3, y + 10, 3, 2);
ctx.fillStyle = "#2b3f9e";
for (var pin = 0; pin < 3; pin++) {
ctx.fillRect(x - 2, y + 3 + pin * 5, 2, 2);
ctx.fillRect(x + 16, y + 3 + pin * 5, 2, 2);
}
}
function drawBill(b) {
var x = Math.round(b.x);
var y = Math.round(b.y);
ctx.fillStyle = "#f5f4ef";
ctx.fillRect(x, y, b.w, b.h);
ctx.fillStyle = "#d8d5cb";
ctx.fillRect(x, y + b.h - 4, b.w, 4);
ctx.fillStyle = "#8e95a1";
ctx.fillRect(x + 3, y + 4, 16, 1);
ctx.fillRect(x + 3, y + 7, 11, 1);
ctx.fillRect(x + 3, y + 10, 14, 1);
ctx.fillStyle = "#c8412b";
ctx.fillRect(x + 2, y + 14, b.w - 4, 11);
ctx.fillStyle = "#fff4ec";
ctx.fillRect(x + 5, y + 17, 1, 5);
ctx.fillRect(x + 4, y + 18, 3, 1);
ctx.fillRect(x + 4, y + 20, 3, 1);
ctx.fillRect(x + 9, y + 17, 3, 5);
ctx.fillRect(x + 14, y + 17, 3, 5);
ctx.fillRect(x + 19, y + 17, 3, 5);
}
function drawDrift(d) {
if (d.dead) {
ctx.fillStyle = "#f5f4ef";
ctx.fillRect(Math.round(d.x), Math.round(d.y + 8), d.w, 4);
return;
}
var x = Math.round(d.x);
var y = Math.round(d.y);
var squish = Math.round(Math.sin(game.t * 0.16 + d.home) * 1);
ctx.fillStyle = "#f5f4ef";
ctx.fillRect(x, y + squish, d.w, d.h - squish);
ctx.fillRect(x + 3, y + d.h, 4, 3);
ctx.fillStyle = "#1a1f2e";
var look = d.vx < 0 ? 0 : 3;
ctx.fillRect(x + 3 + look, y + 3, 2, 3);
ctx.fillRect(x + 8 + Math.round(look * 0.3), y + 3, 2, 3);
ctx.fillStyle = "#c8412b";
ctx.fillRect(x + 4, y + 8, 6, 1);
}
function drawFlag(f) {
var x = f.x + 6;
var top = f.y;
ctx.fillStyle = "#4a3b31";
ctx.fillRect(x, top, 3, 64);
var ready = Object.values(game.caps).filter(Boolean).length === 3;
var wave = Math.round(Math.sin(game.t * 0.1) * 2);
ctx.fillStyle = ready ? "#2e8b57" : "#8e95a1";
ctx.fillRect(x + 3, top + 2 + wave, 22, 13);
ctx.fillStyle = ready ? "#f2c14e" : "#5c626d";
ctx.fillRect(x + 6, top + 6 + wave, 16, 2);
ctx.fillRect(x + 6, top + 9 + wave, 10, 2);
ctx.fillStyle = "#4a3b31";
ctx.fillRect(x - 5, top + 60, 13, 4);
}
function drawAmigo() {
if (game.invuln > 0 && Math.floor(game.t / 4) % 2 === 0) return;
var frame = FRAMES.stand;
if (!game.grounded) frame = FRAMES.air;
else if (Math.abs(game.vx) > 0.35 && Math.floor(game.t / 5) % 2 === 0) frame = FRAMES.step;
ctx.save();
ctx.translate(Math.round(game.x - 3), Math.round(game.y - 1));
if (game.face < 0) {
ctx.translate(16, 0);
ctx.scale(-1, 1);
}
ctx.drawImage(frame, 0, 0);
ctx.restore();
}
function draw() {
if (!game) return;
var cam = Math.round(game.camera);
ctx.fillStyle = "#4aa8d8";
ctx.fillRect(0, 0, W, HGT * 0.55);
ctx.fillStyle = "#79c6e2";
ctx.fillRect(0, HGT * 0.55, W, HGT * 0.45);
mesas(cam * 0.25, "#6e4a7a", 118, 70);
mesas(cam * 0.5, "#8d5470", 78, 110);
ctx.fillStyle = "rgba(255,255,255,.72)";
for (var i = 0; i < 6; i++) {
var x = (((i * 190 - cam * 0.15) % (W + 240)) + W + 240) % (W + 240) - 120;
var y = 22 + (i % 3) * 26;
ctx.fillRect(x, y, 34, 7);
ctx.fillRect(x + 7, y - 6, 20, 6);
ctx.fillRect(x - 6, y + 4, 46, 5);
}
ctx.save();
ctx.translate(-cam, 0);
var from = Math.max(0, Math.floor(cam / 16) - 1);
var to = Math.min(COLS - 1, Math.ceil((cam + W) / 16));
for (var gy = 0; gy < ROWS; gy++) {
for (var gx = from; gx <= to; gx++) {
var t = grid[gy][gx];
if (t === " ") continue;
var px = gx * 16;
var py = gy * 16;
if (t === "#") {
ctx.fillStyle = "#a96c37";
ctx.fillRect(px, py, 16, 16);
var open = !solidAt(gx, gy - 1);
if (open) {
ctx.fillStyle = "#c98a4b";
ctx.fillRect(px, py, 16, 4);
ctx.fillStyle = "#e0a765";
ctx.fillRect(px, py, 16, 2);
}
ctx.fillStyle = "#8a5327";
ctx.fillRect(px, py + 10, 16, 2);
ctx.fillRect(px + 6, py + 5, 2, 4);
} else if (t === "=") {
ctx.fillStyle = "#4a3b31";
ctx.fillRect(px, py, 16, 12);
ctx.fillStyle = "#6b5647";
ctx.fillRect(px, py, 16, 3);
ctx.fillStyle = "#2e8b57";
ctx.fillRect(px, py, 16, 1);
} else if (t === "^") {
ctx.fillStyle = "#cfd3da";
for (var sp = 0; sp < 4; sp++) {
ctx.fillRect(px + sp * 4 + 1, py + 6, 2, 10);
ctx.fillRect(px + sp * 4, py + 10, 4, 6);
}
ctx.fillStyle = "#8e95a1";
ctx.fillRect(px, py + 14, 16, 2);
}
}
}
if (spawns.flag) drawFlag(spawns.flag);
game.caplings.forEach(function (c) {
if (!c.got) drawCap(c);
});
game.items.forEach(function (it) {
if (!it.got) drawToken(it);
});
game.extras.forEach(function (e) {
if (!e.got) drawExtra(e);
});
game.bills.forEach(drawBill);
game.drifts.forEach(drawDrift);
game.pops.forEach(function (pop) {
ctx.fillStyle = "rgba(" + pop.tint + "," + pop.life / 30 + ")";
ctx.fillRect(Math.round(pop.x), Math.round(pop.y), 3, 3);
});
drawAmigo();
ctx.restore();
}
reset(40);
game.mode = "ready";
hud.hidden = true;
mapEl.hidden = true;
showPerks();
window.Amigo.wireContinue(overlay, start);
var pauseCtl = window.Amigo.pauser(
screenEl,
function () {
return game && game.mode === "playing";
},
start,
function () {
keys = {};
if (game) game.jumpHeld = 0;
}
);
window.Amigo.fit(screenEl, cv);
window.Amigo.fullscreen(document.querySelector(".arc-page"), document.getElementById("fs"));
window.Amigo.soundButton(document.getElementById("sound"));
window.Amigo.touchpad(document.querySelector(".arc-page"), "canyon");
window.Amigo.loop(update, draw);
})();
</script>
</body>
</html>