Ctrl K
ComunidadGuías…

docs/arcade/index.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, viewport-fit=cover"
    />
    <title>Amigo's Arcade - AgenticOS</title>
    <meta
      name="description"
      content="Five small games about building an AI agent. Everything you collect - capabilities, skills, context, memory, MCP servers - lands in one harness that follows you between cabinets."
    />
    <link rel="icon" href="../assets/logo.svg" />
    <meta property="og:title" content="Amigo's Arcade" />
    <meta
      property="og:description"
      content="Five small games about building an AI agent. Collect capabilities, skills, context, memory and MCP servers into one harness."
    />
    <meta property="og:image" content="../assets/social-preview.png" />
    <meta name="twitter:card" content="summary_large_image" />
    <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>
      /* The opening: a sentence wide enough to read, and the harness meter
         directly under it, because "what have I got" is the question this page
         exists to answer. */
      .hub-intro {
        display: grid;
        gap: 18px;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: start;
      }

      @media (max-width: 760px) {
        .hub-intro {
          grid-template-columns: minmax(0, 1fr);
        }
      }

      .hub-lede {
        margin: 0;
        max-width: 62ch;
        font-size: 16px;
        line-height: 1.7;
        color: var(--dim);
        text-wrap: pretty;
      }

      .hub-lede b {
        color: var(--ink);
        font-weight: 500;
      }

      .hub-meter {
        display: grid;
        gap: 8px;
        justify-items: start;
        padding: 14px 16px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 4px;
        min-width: 232px;
      }

      .hub-meter .count {
        font-family: var(--display);
        font-size: 30px;
        line-height: 1;
      }

      .hub-meter .count em {
        font-style: normal;
        color: var(--faint);
        font-size: 19px;
      }

      .hub-meter .label {
        font-family: var(--mono);
        font-size: 10px;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--faint);
      }

      /* Twelve segments, one per part, coloured by kind once it is collected:
         the collection's shape at a glance, without reading a single word. */
      .hub-pips {
        display: flex;
        gap: 2px;
        width: 100%;
      }

      .hub-pips i {
        flex: 1;
        min-width: 4px;
        height: 9px;
        background: var(--line);
        border-radius: 1px;
      }

      .hub-cabinets {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(258px, 1fr));
        gap: 16px;
      }

      .hub-cab {
        display: flex;
        flex-direction: column;
        text-decoration: none;
        color: inherit;
        background: var(--panel);
        border: 1px solid var(--edge);
        border-radius: 5px;
        overflow: hidden;
        transition:
          border-color 140ms,
          transform 140ms,
          box-shadow 140ms;
      }

      .hub-cab:hover,
      .hub-cab:focus-visible {
        border-color: var(--sun);
        transform: translateY(-3px);
        box-shadow: 0 16px 30px -22px rgba(0, 0, 0, 0.9);
      }

      .hub-cab canvas {
        display: block;
        width: 100%;
        height: auto;
        image-rendering: pixelated;
        border-bottom: 1px solid var(--edge);
      }

      .hub-cab .body {
        padding: 14px 16px 16px;
        display: flex;
        flex-direction: column;
        gap: 7px;
        flex: 1;
      }

      .hub-cab h3 {
        font-family: var(--display);
        font-weight: 700;
        font-size: 21px;
        line-height: 1;
        margin: 0;
      }

      .hub-cab p {
        margin: 0;
        font-size: 13.5px;
        color: var(--dim);
        line-height: 1.55;
      }

      .hub-cab .foot {
        margin-top: auto;
        padding-top: 12px;
        display: flex;
        align-items: baseline;
        gap: 10px;
        border-top: 1px solid var(--line);
        font-family: var(--mono);
        font-size: 11px;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--faint);
        font-variant-numeric: tabular-nums;
      }

      .hub-cab .foot em {
        font-style: normal;
        color: var(--sun);
      }

      .hub-cab .foot .play {
        margin-left: auto;
        color: var(--dim);
      }

      .hub-cab:hover .foot .play {
        color: var(--sun);
      }

      .hub-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
        gap: 10px;
      }

      /* A locked slot leads with the thing to go and do, not with the word
         "Locked" repeated eleven times down the page. */
      .hub-slot {
        display: grid;
        gap: 5px;
        padding: 12px 14px 13px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-left: 3px solid var(--line);
        border-radius: 3px;
      }

      .hub-slot .kind {
        display: flex;
        align-items: center;
        gap: 6px;
        font-family: var(--mono);
        font-size: 9.5px;
        letter-spacing: 0.18em;
        text-transform: uppercase;
        color: var(--faint);
      }

      .hub-slot .kind i {
        width: 7px;
        height: 7px;
        background: var(--line);
        display: block;
      }

      .hub-slot .kind .where {
        margin-left: auto;
        letter-spacing: 0.04em;
        text-transform: none;
        color: var(--faint);
      }

      .hub-slot .name {
        font-size: 15px;
        font-weight: 500;
        color: var(--dim);
        line-height: 1.35;
      }

      .hub-slot .meta {
        font-size: 12.5px;
        line-height: 1.5;
        color: var(--faint);
      }

      .hub-slot .perk {
        font-family: var(--mono);
        font-size: 11.5px;
        line-height: 1.5;
        color: var(--faint);
      }

      .hub-slot.is-on {
        background: var(--raised);
      }

      .hub-slot.is-on .name {
        color: var(--ink);
      }

      .hub-slot.is-on .meta {
        color: var(--dim);
      }

      .hub-slot.is-on .perk {
        color: var(--sun);
      }

      .hub-slot.is-on .perk::before {
        content: "+ ";
      }

      .hub-spec-wrap {
        position: relative;
      }

      .hub-spec {
        margin: 0;
        padding: 18px 20px;
        background: #0d0f14;
        border: 1px solid var(--line);
        border-radius: 4px;
        font-family: var(--mono);
        font-size: 12.5px;
        line-height: 1.8;
        color: var(--ink);
        overflow-x: auto;
        white-space: pre;
      }

      .hub-spec .c {
        color: var(--faint);
      }

      .hub-spec .k {
        color: var(--sky);
      }

      .hub-spec .v {
        color: var(--sun);
      }

      .hub-copy {
        position: absolute;
        top: 10px;
        right: 10px;
      }

      .hub-stats {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
        gap: 10px;
      }

      .hub-stats div {
        padding: 12px 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 3px;
      }

      .hub-stats b {
        display: block;
        font-family: var(--mono);
        font-size: 9.5px;
        font-weight: 500;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--faint);
        margin-bottom: 3px;
      }

      .hub-stats span {
        font-family: var(--mono);
        font-size: 21px;
        font-variant-numeric: tabular-nums;
      }

      .hub-foot {
        display: flex;
        gap: 14px;
        flex-wrap: wrap;
        align-items: center;
        border-top: 1px solid var(--line);
        padding-top: 18px;
        font-size: 13px;
        color: var(--faint);
      }

      .hub-foot a {
        color: var(--dim);
        text-decoration: none;
        border-bottom: 1px solid var(--line);
      }

      .hub-foot a:hover,
      .hub-foot a:focus-visible {
        color: var(--sun);
        border-bottom-color: var(--sun);
      }

      .hub-foot .grow {
        margin-left: auto;
      }
    </style>
  </head>
  <body>
    <div class="arc-page">
      <div class="arc-top">
        <img class="arc-mark" src="../assets/amigo.svg" alt="" />
        <h1 class="arc-title">Amigo's Arcade</h1>
        <p class="arc-eyebrow">AgenticOS</p>
      </div>

      <div class="hub-intro">
        <p class="hub-lede">
          Five small games about the same job: giving an agent something it did
          not have before. Every capability, skill, context file, memory and MCP
          server you pick up is written to
          <b>one harness</b> in this browser, and it follows you into the other
          cabinets - a tool earned in the desert is a tool you still have in the
          canyon.
        </p>

        <div class="hub-meter">
          <span class="label">Harness</span>
          <span class="count"
            ><span id="count">0</span
            ><em>&thinsp;/&thinsp;<span id="total">0</span></em></span
          >
          <div class="hub-pips" id="pips" aria-hidden="true"></div>
          <span class="label" id="nextUp"></span>
        </div>
      </div>

      <div class="arc-section">
        <div class="arc-head">
          <h2>Cabinets</h2>
          <p>Keyboard or touch. No install, no account.</p>
        </div>
        <div class="hub-cabinets">
          <a class="hub-cab" href="agents.html">
            <canvas
              width="320"
              height="128"
              data-preview="agents"
              aria-hidden="true"
            ></canvas>
            <div class="body">
              <h3>Just One More Agent</h3>
              <p>
                One comma to fix. A growing army of agents. Equip tools and MCP, pick a provider, and
                cancel meetings before the budget becomes the punchline.
              </p>
              <div class="foot">
                <span data-score="agents"></span
                ><span class="play">Play &rarr;</span>
              </div>
            </div>
          </a>
          <a class="hub-cab" href="context.html">
            <canvas
              width="320"
              height="128"
              data-preview="context"
              aria-hidden="true"
            ></canvas>
            <div class="body">
              <h3>Context Tetris</h3>
              <p>
                Instructions, documents, memories and far too many logs. Stack
                the context, clear rows and keep enough signal to answer.
              </p>
              <div class="foot">
                <span data-score="context"></span
                ><span class="play">Play &rarr;</span>
              </div>
            </div>
          </a>
          <a class="hub-cab" href="runner.html">
            <canvas
              width="320"
              height="128"
              data-preview="runner"
              aria-hidden="true"
            ></canvas>
            <div class="body">
              <h3>Desert Run</h3>
              <p>
                One long shift across the sand. Hop the rate limits, duck the
                confident wrong answers, and mind the thirty-thousand-dollar
                invoice coming the other way.
              </p>
              <div class="foot">
                <span data-score="runner"></span
                ><span class="play">Play &rarr;</span>
              </div>
            </div>
          </a>

          <a class="hub-cab" href="canyon.html">
            <canvas
              width="320"
              height="128"
              data-preview="canyon"
              aria-hidden="true"
            ></canvas>
            <div class="body">
              <h3>Capability Canyon</h3>
              <p>
                Capabilities, document chunks and a typed-output schema are
                scattered across a canyon, and the publish flag refuses a
                half-built agent.
              </p>
              <div class="foot">
                <span data-score="canyon"></span
                ><span class="play">Play &rarr;</span>
              </div>
            </div>
          </a>

          <a class="hub-cab" href="shift.html">
            <canvas
              width="320"
              height="128"
              data-preview="shift"
              aria-hidden="true"
            ></canvas>
            <div class="body">
              <h3>Night Shift</h3>
              <p>
                A quiet control room and a queue that is not. Connect a source,
                clean an export, answer the invoice finance just found - one
                pair of hands, in the right order.
              </p>
              <div class="foot">
                <span data-score="shift"></span
                ><span class="play">Play &rarr;</span>
              </div>
            </div>
          </a>
        </div>
      </div>

      <div class="arc-section">
        <div class="arc-head">
          <h2>The harness</h2>
          <p>
            Each part changes how the games play, not just what the list says.
          </p>
        </div>
        <div class="hub-grid" id="slots"></div>
      </div>

      <div class="arc-section">
        <div class="arc-head">
          <h2>Your agent, as a spec</h2>
          <p>The shape AgenticOS exports for real.</p>
        </div>
        <div class="hub-spec-wrap">
          <pre class="hub-spec" id="spec"></pre>
          <button type="button" class="arc-btn hub-copy" id="copy">Copy</button>
        </div>
      </div>

      <div class="arc-section">
        <div class="arc-head">
          <h2>Shift record</h2>
        </div>
        <div class="hub-stats" id="stats"></div>
      </div>

      <div class="hub-foot">
        <a
          href="https://github.com/vstorm-co/agenticos"
          target="_blank"
          rel="noopener"
          >Star the repository</a
        >
        <a href="../">Read the documentation</a>
        <span class="grow"></span>
        <button type="button" class="arc-btn" id="reset">
          Wipe the harness
        </button>
      </div>
    </div>

    <script src="arcade.js"></script>
    <script>
      (function () {
        "use strict";

        var H = window.Amigo.harness;
        var sprites = window.Amigo.sprites();

        function paintMeter(state) {
          document.getElementById("count").textContent = state.unlocked.length;
          document.getElementById("total").textContent =
            window.Amigo.parts.length;
          var pips = document.getElementById("pips");
          pips.innerHTML = "";
          window.Amigo.parts.forEach(function (part) {
            var pip = document.createElement("i");
            if (state.unlocked.indexOf(part.id) !== -1) {
              pip.style.background = window.Amigo.kinds[part.kind].color;
            }
            pips.appendChild(pip);
          });

          var next = null;
          window.Amigo.parts.forEach(function (part) {
            if (!next && state.unlocked.indexOf(part.id) === -1) next = part;
          });
          document.getElementById("nextUp").textContent = next
            ? "Next: " + next.how.toLowerCase()
            : "Complete";
        }

        function paintSlots(state) {
          var host = document.getElementById("slots");
          host.innerHTML = "";
          window.Amigo.parts.forEach(function (part) {
            var on = state.unlocked.indexOf(part.id) !== -1;
            var kind = window.Amigo.kinds[part.kind];
            var slot = document.createElement("div");
            slot.className = "hub-slot" + (on ? " is-on" : "");
            if (on) slot.style.borderLeftColor = kind.color;
            slot.innerHTML =
              '<span class="kind"><i' +
              (on ? ' style="background:' + kind.color + '"' : "") +
              "></i>" +
              kind.label +
              '<span class="where">' +
              part.where +
              "</span></span>" +
              '<span class="name">' +
              (on ? part.name : part.how) +
              "</span>" +
              '<span class="meta">' +
              (on ? part.blurb : "Not collected yet.") +
              "</span>" +
              '<span class="perk">' +
              part.perk +
              "</span>";
            host.appendChild(slot);
          });
        }

        /* The same collection, printed the way the product prints it. Locked
           parts stay in as comments, because a spec you have not finished is
           exactly what this is. */
        function specText(state) {
          var keys = {
            tool: "capabilities",
            skill: "skills",
            context: "context_files",
            memory: "memory",
            data: "knowledge",
            mcp: "mcp_servers",
            model: "models",
            vstorm: "badges",
          };
          var lines = [
            "# amigo.yaml - exported from the arcade",
            "spec_version: 1",
            "agent:",
            "  name: amigo",
            "  model: claude-opus-5",
            '  instructions: "Answer for the company, not for the model."',
          ];
          Object.keys(keys).forEach(function (kind) {
            lines.push("  " + keys[kind] + ":");
            window.Amigo.parts.forEach(function (part) {
              if (part.kind !== kind) return;
              var slug = part.id.split(".")[1];
              if (state.unlocked.indexOf(part.id) !== -1)
                lines.push("    - " + slug);
              else lines.push("    # - " + slug + "  (not collected yet)");
            });
          });
          return lines.join("\n");
        }

        function paintSpec(state) {
          var html = specText(state)
            .split("\n")
            .map(function (line) {
              if (line.trim().charAt(0) === "#")
                return '<span class="c">' + line + "</span>";
              var m = line.match(/^(\s*)([a-z_]+):(.*)$/);
              if (m) {
                return (
                  m[1] +
                  '<span class="k">' +
                  m[2] +
                  "</span>:" +
                  (m[3] ? '<span class="v">' + m[3] + "</span>" : "")
                );
              }
              return line.replace(/- (.+)$/, '- <span class="v">$1</span>');
            })
            .join("\n");
          document.getElementById("spec").innerHTML = html;
        }

        function paintStats() {
          var s = H.stats();
          var rows = [
            ["Tokens banked", s.lifetimeTokens],
            ["Best run", s.runnerBest],
            ["Longest shift", s.runnerLongest + "s"],
            ["Canyon clears", s.canyonClears],
            ["Shifts worked", s.shiftClears],
            ["Failed runs", s.runnerDeaths],
            ["Commas shipped", s.agentsWins],
            ["Context best", s.contextBest],
            ["Tasks answered", s.contextTasks],
          ];
          document.getElementById("stats").innerHTML = rows
            .map(function (row) {
              return (
                "<div><b>" + row[0] + "</b><span>" + row[1] + "</span></div>"
              );
            })
            .join("");

          document.querySelector('[data-score="agents"]').textContent =
            s.agentsRuns ? "Best " + s.agentsBest + " points" : "Never played";
          document.querySelector('[data-score="context"]').textContent =
            s.contextRuns
              ? "Best " + s.contextBest + " points"
              : "Never played";
          document.querySelector('[data-score="runner"]').innerHTML =
            s.runnerBest
              ? "Best <em>" + s.runnerBest + "</em> tokens"
              : "Never played";
          document.querySelector('[data-score="canyon"]').innerHTML =
            s.canyonClears
              ? "Cleared <em>" + s.canyonClears + "</em>&times;"
              : "Never cleared";
          document.querySelector('[data-score="shift"]').innerHTML =
            s.shiftClears
              ? "Worked <em>" + s.shiftClears + "</em>&times;"
              : "Never worked";
        }

        function paint() {
          var state = H.state();
          paintMeter(state);
          paintSlots(state);
          paintSpec(state);
          paintStats();
        }

        document.getElementById("reset").addEventListener("click", function () {
          if (
            !window.confirm(
              "Wipe the harness? Every collected part goes with it.",
            )
          )
            return;
          H.reset();
          paint();
        });

        document.getElementById("copy").addEventListener("click", function (e) {
          var button = e.currentTarget;
          var done = function (word) {
            button.textContent = word;
            window.setTimeout(function () {
              button.textContent = "Copy";
            }, 1600);
          };
          try {
            navigator.clipboard.writeText(specText(H.state())).then(
              function () {
                done("Copied");
              },
              function () {
                done("Select it");
              },
            );
          } catch (err) {
            done("Select it");
          }
        });

        /* One animated scene per cabinet, drawn from the same sprite and the
           same palette the games use, so a card cannot promise a game that does
           not exist. */
        function preview(canvas, kind) {
          var ctx = canvas.getContext("2d");
          ctx.imageSmoothingEnabled = false;
          var W = canvas.width;
          var H2 = canvas.height;
          var t = 0;

          function amigo(x, y, frame) {
            ctx.fillStyle = "rgba(0,0,0,.3)";
            ctx.fillRect(x + 2, y + 20, 12, 2);
            ctx.drawImage(frame, Math.round(x), Math.round(y));
          }

          function desert() {
            var ground = H2 - 26;
            ctx.fillStyle = "#2a1147";
            ctx.fillRect(0, 0, W, ground * 0.45);
            ctx.fillStyle = "#4b1c47";
            ctx.fillRect(0, ground * 0.45, W, ground * 0.25);
            ctx.fillStyle = "#8a2e43";
            ctx.fillRect(0, ground * 0.7, W, ground * 0.3 + 2);

            for (var s = 0; s < 14; s++) {
              var sx = (s * 53) % W;
              var sy = (s * 31) % (ground - 40);
              if ((Math.floor(t / 7) + s) % 5 === 0) continue;
              ctx.fillStyle = "#f6e7c4";
              ctx.fillRect(sx, sy, 2, 2);
            }

            ctx.fillStyle = "#f2c14e";
            ctx.beginPath();
            ctx.arc(W - 62, ground - 44, 19, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = "rgba(139,46,67,.42)";
            for (var b = -20; b < 20; b += 4)
              ctx.fillRect(W - 81, ground - 44 + b, 38, 2);

            for (var d = 0; d < 4; d++) {
              var dx =
                ((((d * 110 - t * 0.5) % (W + 130)) + W + 130) % (W + 130)) -
                65;
              ctx.fillStyle = "#4c1c3d";
              ctx.fillRect(dx + 6, ground - 17, 60, 17);
              ctx.fillRect(dx, ground - 11, 72, 11);
            }

            ctx.fillStyle = "#c98a4b";
            ctx.fillRect(0, ground, W, H2 - ground);
            ctx.fillStyle = "#a96c37";
            ctx.fillRect(0, ground + 6, W, H2 - ground - 6);
            ctx.fillStyle = "#e0a765";
            var scroll = (t * 2.1) % 22;
            for (var g = -22; g < W + 22; g += 22) {
              ctx.fillRect(Math.round(g - scroll), ground + 2, 8, 2);
              ctx.fillRect(
                Math.round(g - scroll * 1.5 + 11),
                ground + 11,
                4,
                2,
              );
            }

            var cx = W - ((t * 2.1) % (W + 90));
            ctx.fillStyle = "#2e8b57";
            ctx.fillRect(cx, ground - 22, 9, 22);
            ctx.fillRect(cx - 4, ground - 14, 4, 3);
            ctx.fillRect(cx + 9, ground - 18, 4, 3);

            var tx = W - ((t * 2.1 + 150) % (W + 120));
            ctx.fillStyle = "#f2c14e";
            ctx.fillRect(tx, ground - 40 + Math.sin(t * 0.12) * 2, 7, 7);

            var hop = Math.max(0, Math.sin(t * 0.09)) * 22;
            amigo(
              34,
              ground - 19 - hop,
              hop > 1
                ? sprites.air
                : Math.floor(t / 5) % 2
                  ? sprites.step
                  : sprites.stand,
            );
          }

          function canyon() {
            var ground = H2 - 22;
            ctx.fillStyle = "#4aa8d8";
            ctx.fillRect(0, 0, W, H2 * 0.55);
            ctx.fillStyle = "#79c6e2";
            ctx.fillRect(0, H2 * 0.55, W, H2 * 0.45);

            for (var m = 0; m < 4; m++) {
              var mx =
                ((((m * 128 - t * 0.28) % (W + 150)) + W + 150) % (W + 150)) -
                75;
              ctx.fillStyle = "#6e4a7a";
              ctx.fillRect(mx, ground - 58, 52, 58);
              ctx.fillRect(mx - 10, ground - 34, 72, 34);
            }
            for (var c = 0; c < 3; c++) {
              var cx2 =
                ((((c * 168 - t * 0.5) % (W + 200)) + W + 200) % (W + 200)) -
                100;
              ctx.fillStyle = "#8d5470";
              ctx.fillRect(cx2, ground - 38, 66, 38);
            }

            ctx.fillStyle = "#a96c37";
            ctx.fillRect(0, ground, W, H2 - ground);
            ctx.fillStyle = "#c98a4b";
            ctx.fillRect(0, ground, W, 4);
            ctx.fillStyle = "#e0a765";
            ctx.fillRect(0, ground, W, 2);

            ctx.fillStyle = "#4a3b31";
            ctx.fillRect(150, ground - 26, 48, 10);
            ctx.fillStyle = "#2e8b57";
            ctx.fillRect(150, ground - 26, 48, 1);

            var orbY = ground - 52 + Math.sin(t * 0.07) * 3;
            ctx.fillStyle = "rgba(242,193,78,.22)";
            ctx.fillRect(164, orbY - 4, 24, 24);
            ctx.fillStyle = "#1a1f2e";
            ctx.fillRect(168, orbY, 16, 16);
            ctx.fillStyle = "#f2c14e";
            ctx.fillRect(169, orbY + 1, 14, 14);

            var pole = W - 44;
            ctx.fillStyle = "#4a3b31";
            ctx.fillRect(pole, ground - 54, 3, 54);
            ctx.fillStyle = "#2e8b57";
            ctx.fillRect(pole + 3, ground - 52 + Math.sin(t * 0.1) * 2, 20, 12);

            var phase = (t % 150) / 150;
            var hop2 = Math.sin(phase * Math.PI * 4) * 16;
            amigo(
              44 + phase * 70,
              ground - 19 - Math.max(0, hop2),
              hop2 > 1
                ? sprites.air
                : Math.floor(t / 5) % 2
                  ? sprites.step
                  : sprites.stand,
            );
          }

          function control() {
            for (var gx = 0; gx < W; gx += 16) {
              for (var gy = 0; gy < H2; gy += 16) {
                ctx.fillStyle =
                  ((gx + gy) / 16) % 2 === 0 ? "#1b1f28" : "#191d25";
                ctx.fillRect(gx, gy, 16, 16);
                ctx.fillStyle = "rgba(46,139,87,.10)";
                ctx.fillRect(gx, gy + 15, 16, 1);
              }
            }
            ctx.fillStyle = "#2a3140";
            ctx.fillRect(0, 0, W, 10);
            ctx.fillStyle = "#39435a";
            ctx.fillRect(0, 0, W, 3);

            function desk(x, y, color) {
              ctx.fillStyle = "#11141a";
              ctx.fillRect(x, y + 4, 32, 14);
              ctx.fillStyle = "#3a4354";
              ctx.fillRect(x, y - 8, 32, 14);
              ctx.fillStyle = color;
              ctx.fillRect(x + 2, y - 6, 28, 10);
              ctx.fillStyle = "rgba(11,12,15,.55)";
              for (var i = 0; i < 3; i++)
                ctx.fillRect(x + 4, y - 4 + i * 3, 12 + i * 5, 1);
            }
            desk(38, 42, "#2e8b57");
            desk(146, 30, "#4b6bdd");
            desk(244, 62, "#c8412b");

            var leg = (t % 320) / 320;
            var px = 60 + leg * 180;
            var py = 78 - Math.sin(leg * Math.PI) * 26;
            var frame = Math.floor(t / 6) % 2 ? sprites.step : sprites.stand;
            ctx.save();
            if (leg > 0.5) {
              ctx.translate(Math.round(px) + 16, Math.round(py));
              ctx.scale(-1, 1);
              amigo(0, 0, frame);
            } else {
              amigo(px, py, frame);
            }
            ctx.restore();

            ctx.fillStyle = "rgba(11,12,15,.6)";
            ctx.fillRect(Math.round(px) + 3, Math.round(py) - 12, 10, 10);
            ctx.fillStyle = "#2e8b57";
            ctx.fillRect(Math.round(px) + 4, Math.round(py) - 11, 8, 8);
          }

          function officePreview() {
            ctx.fillStyle = "#191522";
            ctx.fillRect(0, 0, W, H2);
            ctx.strokeStyle = "#58425d";
            for (var i = 0; i < 8; i++) {
              var x = 30 + (i % 4) * 76,
                y = 25 + Math.floor(i / 4) * 57;
              ctx.beginPath();
              ctx.moveTo(160, 64);
              ctx.lineTo(x + 8, y + 14);
              ctx.stroke();
              amigo(
                x,
                y,
                Math.floor(t / 20 + i) % 2 ? sprites.stand : sprites.step,
              );
              ctx.fillStyle = "#574353";
              ctx.fillRect(x - 4, y + 19, 37, 6);
              ctx.fillStyle = "#eac05c";
              ctx.fillRect(x + 16, y + 9, 14, 10);
            }
          }
          function contextPreview() {
            ctx.fillStyle = "#101421";
            ctx.fillRect(0, 0, W, H2);
            var shades = ["#54b6ce", "#b587d5", "#687084", "#eac05c"];
            for (var x = 0; x < 16; x++) {
              var height = 1 + ((x * 7) % 4);
              for (var y = 0; y < height; y++) {
                ctx.fillStyle = shades[(x + y) % 4];
                ctx.fillRect(x * 20 + 1, H2 - (y + 1) * 20, 18, 18);
              }
            }
            ctx.fillStyle = "#eac05c";
            var fall = (Math.floor(t / 9) % 4) * 10;
            ctx.fillRect(141, fall, 18, 18);
            ctx.fillRect(121, fall + 20, 18, 18);
            ctx.fillRect(141, fall + 20, 18, 18);
            ctx.fillRect(161, fall + 20, 18, 18);
          }
          function frame() {
            t++;
            if (kind === "runner") desert();
            else if (kind === "canyon") canyon();
            else if (kind === "agents") officePreview();
            else if (kind === "context") contextPreview();
            else control();
            requestAnimationFrame(frame);
          }
          frame();
        }

        /* A player walks into a cabinet, earns three parts and presses Back.
           A bfcache-eligible browser hands this exact document back without
           rerunning a line of it, so the board would still show what it showed
           before the run. The same goes for a second tab writing the record. */
        window.addEventListener("pageshow", function () {
          H.invalidate();
          paint();
        });
        window.addEventListener("storage", function (e) {
          if (e.key && e.key !== H.key) return;
          H.invalidate();
          paint();
        });

        Array.prototype.forEach.call(
          document.querySelectorAll("[data-preview]"),
          function (c) {
            preview(c, c.dataset.preview);
          },
        );

        paint();
      })();
    </script>
  </body>
</html>