Tessarune
Dungeon Mechanisms · pressure_plate.js

感圧板

32×32 · JavaScript 247 行

32×32 の床の罠で、4 枚の切石を組んだ枠の受け口に、枠より 2px 高い暗いスレートの板が収まり、紋(輪の中に algiz の字)が彫られています。up(8 コマ)は紋の溝がかすかに脈打ちます。press(6 コマの一回再生)は板が沈んで枠と面一になり、すき間から土ぼこりが噴き、紋が光の柱になって光ります。down(8 コマ)は紋が光り続け、輪を光が回り、光の粒が昇ります。release(6 コマの一回再生)は板が跳ね上がって戻ります。down-violet は光を紫に差し替えたものです。

感圧板 — 描画結果6×
このプログラムで描画(変種「press」) · 32×32 · 6 コマ
Dungeon Mechanisms動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

pressure_plate.js · 1–10
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants://   up       loop 8f     — plate raised, the rune's grooves faintly pulse//   press    one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0//   down     loop 8f     — plate flush, rune glowing steadily, a glint runs round the ring, motes rise//   release  one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0//   down-violet          — the glow ramp swapped to violet

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

pressure_plate.js · 236–247
submit(Plate, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 0.8, loop: true },  anchor: { x: 16, y: GROUND },  variants: [    { name: "up", args: ["up"] },    { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } },    { name: "down", args: ["down"] },    { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } },    { name: "down-violet", args: ["down"], repalette: swap("glow", "v") },  ],});
プログラム全体を表示 (247 行)
pressure_plate.js · 247 行
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants://   up       loop 8f     — plate raised, the rune's grooves faintly pulse//   press    one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0//   down     loop 8f     — plate flush, rune glowing steadily, a glint runs round the ring, motes rise//   release  one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0//   down-violet          — the glow ramp swapped to violetconst W = 32, GROUND = 29;const FR = { x0: 1, x1: 30, y0: 6, y1: 26, face: 2 };   // frame top face rows y0..y1, front face below itconst OP = { x0: 5, x1: 26, y0: 9, y1: 23 };            // socket opening on the frame's top faceconst PL = { x0: 6, x1: 25, y0: 10, y1: 23 };           // plate footprint (flush position)const RY = 2;                                              // rune row offset inside the plate's top face// the rune: o = ring, a = diamond, * = core   (18 x 9 grid; symmetric about the plate centre)const RUNE = [  "......oooooo......",  "....oo......oo....",  "...o....aa....o...",  "..o....a**a....o..",  "o.o...a****a...o.o",  "..o....a**a....o..",  "...o....aa....o...",  "....oo......oo....",  "......oooooo......",];const GY = 0;                                              // grid row offset of RUNE[0]const RING = [];                                           // ring pixels in angular order, for the glint{  RUNE.forEach((row, y) => [...row].forEach((c, x) => { if (c === "o") RING.push([x, y, Math.atan2((y - 4) * 1.6, x - 8.5)]); }));  RING.sort((a, b) => a[2] - b[2]);}// ---- one frame into a key buffer: {part, key}function build(h, rune) {  const B = new Array(W * W).fill(null);  const set = (x, y, part, key) => { if (x >= 0 && y >= 0 && x < W && y < W) B[y * W + x] = { part, key }; };  // frame: a ring of dressed floor stones, top face then a 2px front face  for (let y = FR.y0; y <= FR.y1; y++) for (let x = FR.x0; x <= FR.x1; x++) {    if ((x === FR.x0 || x === FR.x1) && y === FR.y0) continue;                 // chamfered back corners    if (x >= OP.x0 && x <= OP.x1 && y >= OP.y0 && y <= OP.y1) {      // the socket: its back wall (faces the camera) is a dim row, the rest a black gap      set(x, y, "socket", y === OP.y0 ? "n0" : "ink");      continue;    }    let k = "n2";    if (y === FR.y0 || x === FR.x0 || (y === FR.y0 + 1 && (x === FR.x0 + 1))) k = "n3";   // outer north / west edges lit    if (x === FR.x1 && y > FR.y0) k = "n1";                                               // outer east edge    if (y === OP.y1 + 1 && x >= OP.x0 && x <= OP.x1) k = "n3";                           // front rim's inner lip faces north    if (x === OP.x1 + 1 && y >= OP.y0 && y <= OP.y1) k = "n3";                           // right rim's inner lip faces west    if (y === OP.y0 - 1 && x >= OP.x0 - 1 && x <= OP.x1) k = "n1";                       // back rim's inner lip faces south    if (x === OP.x0 - 1 && y >= OP.y0 - 1 && y <= OP.y1) k = "n1";                       // left rim's inner lip faces east    set(x, y, "frame", k);  }  // block joints across the rims (dark joint, the next stone's lit edge beside it)  const jointV = (x, y0, y1) => { for (let y = y0; y <= y1; y++) { set(x, y, "frame", "n1"); if (B[y * W + x + 1] && B[y * W + x + 1].key === "n2") set(x + 1, y, "frame", "n3"); } };  const jointH = (y, x0, x1) => { for (let x = x0; x <= x1; x++) { set(x, y, "frame", "n1"); if (B[(y + 1) * W + x] && B[(y + 1) * W + x].key === "n2") set(x, y + 1, "frame", "n3"); } };  jointV(11, FR.y0 + 1, OP.y0 - 2); jointV(21, FR.y0 + 1, OP.y0 - 2);  jointV(9, OP.y1 + 2, FR.y1); jointV(18, OP.y1 + 2, FR.y1); jointV(25, OP.y1 + 2, FR.y1);  jointH(15, FR.x0 + 1, OP.x0 - 2); jointH(18, OP.x1 + 2, FR.x1 - 1);  // chips (countable marks)  set(3, 21, "frame", "n1"); set(4, 21, "frame", "n3");  set(28, 11, "frame", "n1"); set(28, 12, "frame", "n3");  set(14, 25, "frame", "n1"); set(15, 25, "frame", "n1");  // frame front face  for (let i = 1; i <= FR.face; i++) for (let x = FR.x0; x <= FR.x1; x++) {    if ((x === FR.x0 || x === FR.x1) && i === FR.face) continue;                 // chamfered front corners    let k = i === 1 ? "n1" : "n0";    if (x === FR.x0 + (i === FR.face ? 1 : 0)) k = i === 1 ? "n2" : "n1";    if (x === 9 || x === 18 || x === 25) k = "ink";                              // the joints run down the face    set(x, FR.y1 + i, "frame", k);  }  // plate: top face shifted up by h, front face h rows tall (inside the socket, so its sides never show)  const top = PL.y0 - h, bot = PL.y1 - h;  for (let y = top; y <= bot; y++) for (let x = PL.x0; x <= PL.x1; x++) {    let k = "n3";    if (x === PL.x1) k = "n2";    if (x === PL.x0 || y === top) k = "n4";    if (y === top && x === PL.x1) k = "n3";    // carved corner brackets one pixel in    const cx = x - PL.x0 <= 3 ? x - PL.x0 : PL.x1 - x <= 3 ? PL.x1 - x : -1;    const cy = y - top <= 3 ? y - top : bot - y <= 3 ? bot - y : -1;    if (cx >= 1 && cy >= 1 && ((cx === 1 && cy <= 3) || (cy === 1 && cx <= 3))) k = "n2";    set(x, y, "plate", k);  }  for (let i = 1; i <= h; i++) for (let x = PL.x0; x <= PL.x1; x++) {    set(x, bot + i, "plate", i === 1 ? (x === PL.x0 ? "n2" : "n1") : x === PL.x0 ? "n1" : "n0");  }  // rune grooves, then the groove's lower wall: lit stone when dark, a dim glow when the rune is lit  const RW = RUNE[0].length, RH = RUNE.length;  const isG = (x, y) => y >= 0 && y < RH && x >= 0 && x < RW && RUNE[y][x] !== ".";  const inside = (x, y) => Math.abs(x - 8.5) + Math.abs(y - 4) * 1.6 < 5.5;   // the diamond and its surround  for (let y = 0; y < RH; y++) for (let x = 0; x < RW; x++) {    const px = PL.x0 + 1 + x, py = top + RY + GY + y;    const c = RUNE[y][x];    if (c !== ".") { set(px, py, "rune", rune.key(c, x, y)); continue; }    const adj = isG(x - 1, y) || isG(x + 1, y) || isG(x, y - 1) || isG(x, y + 1);    if (rune.halo === "all" && adj) set(px, py, "rune", rune.haloKey);    else if (isG(x, y - 1) && !inside(x, y)) {      if (rune.halo) set(px, py, "rune", rune.haloKey); else set(px, py, "plate", "n4");    }  }  return B;}const PARTS = ["frame", "socket", "plate", "rune"];function draw(g, B) {  g.outlined("ink", () => {    for (const part of PARTS) g.part(part, () => g.field({ x: [0, W - 1], y: [0, W - 1] }, (x, y) => {      const b = B[y * W + x];      return b && b.part === part ? b.key : null;    }));  });}// ---- rune looksconst lvKey = (lv) => ["glow0", "glow1", "glow2", "glow3"][Math.max(0, Math.min(3, lv))];// a look = levels for ring / diamond / core; halo: null (dark grooves), "low" (dim glow on the grooves' lower wall)// or "all" (a solid glow around every groove, for the flash)function look(ring, dia, core, halo, haloLv, glint) {  return {    key: (c, x, y) => (c === "o" && glint && glint(x, y) ? "glow3" : lvKey(c === "o" ? ring : c === "a" ? dia : core)),    halo: halo || null, haloKey: lvKey(haloLv || 0),  };}function runeUp(t) {  // faint pulse: dark grooves; the core warms first, then the diamond  const p = 0.5 - 0.5 * Math.cos(TAU * t);  return look(0, p > 0.6 ? 1 : 0, p > 0.85 ? 2 : p > 0.3 ? 1 : 0);}function runeDown(t) {  // steady: ring and diamond glow2, core white-hot, ticks glow1; a glint runs round the ring  const i = Math.floor(t * RING.length) % RING.length;  const on = new Set([i, (i + 1) % RING.length].map((k) => RING[k][0] + "," + RING[k][1]));  return look(2, 2, 3, "low", 1, (x, y) => on.has(x + "," + y));}// ---- effectsfunction pillar(g, top, amount) {  // a shaft of light straight up out of the rune: solid at the flash, thinning into dither as it fades  if (amount <= 0) return;  g.detached("flare", () => {    const base = top + RY + 4;    const cols = [[13, 0.5, "glow2"], [14, 0.8, "glow2"], [15, 1, "glow3"], [16, 1, "glow3"], [17, 0.8, "glow2"], [18, 0.5, "glow2"]];    for (const [x, s, k0] of cols) {      const len = Math.round(amount * s * (base + 1));      for (let k = 0; k < len; k++) {        const y = base - k;        if (y < 0) break;        const u = k / len;        if ((amount < 0.9 || u > 0.55 || s < 0.6) && !chk(x, y)) continue;   // the fringe is always dithered        g.pixel(x, y, u > 0.8 ? "glow1" : u > 0.55 && k0 === "glow3" ? "glow2" : k0);      }    }    // sparks flung sideways from the flash    if (amount > 0.5) for (const [dx, dy] of [[-6, -4], [7, -3], [-9, 1], [10, 2]]) {      const x = 15.5 + dx * (amount > 0.9 ? 1 : 1.4), y = base - 3 + dy * (amount > 0.9 ? 1 : 1.4) - (amount > 0.9 ? 0 : 2);      g.pixel(Math.round(x), Math.round(y), amount > 0.9 ? "glow3" : "glow2");    }  });}// dust: [x, y, dx, dy] — puffs squirting out of the side gaps and over the front rimconst DUST = [[3, 13, -1, -0.6], [4, 20, -1, 0.2], [28, 13, 1, -0.6], [27, 20, 1, 0.2], [10, 25, -0.8, 0.3], [21, 25, 0.8, 0.3]];function dust(g, k) {  if (k <= 0 || k > 3) return;  g.detached("dust", () => {    for (const [x0, y0, dx, dy] of DUST) {      const x = Math.max(1, Math.min(30, Math.round(x0 + dx * (k - 1) * 1.3))), y = Math.round(y0 + dy * (k - 1) * 1.5 - (k - 1) * 0.7);      const s = dx < 0 ? -1 : 1;      if (k === 1) {                 // a round puff: lit crown, grey body, shaded underside        g.pixel(x, y - 1, "n5"); g.pixel(x + s, y - 1, "n4"); g.pixel(x, y, "n4"); g.pixel(x + s, y, "n3");      } else if (k === 2) {          // it spreads out and thins        g.pixel(x - 1, y, "n4"); g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3"); g.pixel(x + s, y - 1, "n4");      } else {                       // last wisps        g.pixel(x, y - 1, "n3"); if (x + 2 * s >= 0 && x + 2 * s <= 31) g.pixel(x + 2 * s, y, "n3");      }    }  });}function motes(g, top, t) {  g.detached("motes", () => {    for (let k = 0; k < 3; k++) {      const p = (t + k / 3) % 1;      const x = [12, 16, 19][k] + Math.round(Math.sin(TAU * (p + k * 0.37)));      const y = top + RY + 3 - Math.round(p * 11);      if (p < 0.8) g.pixel(x, y, p < 0.4 ? "glow3" : "glow2");    }  });}// ---- statesconst PRESS = [  // h, rune, pillar, dust stage  [2, () => runeUp(0), 0, 0],  [1, () => look(1, 1, 2, "low", 0), 0, 0],  [0, () => look(3, 3, 3, "all", 2), 1, 1],    // impact: the rune flares white, dust squirts from the gaps  [0, () => look(3, 3, 3, "all", 1), 0.7, 2],  [0, () => look(2, 3, 3, "low", 1), 0.35, 3],  [0, () => runeDown(0), 0, 0],                // == down, frame 0];const RELEASE = [  [0, () => runeDown(0)],                      // == down, frame 0  [0, () => look(1, 2, 2, "low", 0)],  [1, () => look(1, 1, 1)],  [3, () => look(0, 0, 1)],             // springs up a pixel past rest  [2, () => runeUp(0)],  [2, () => runeUp(0)],                        // == up, frame 0];const Plate = sprite(pal, (g, t, state) => {  contactShadow(g, 16, GROUND, 15, 2);  if (state === "up") { draw(g, build(2, runeUp(t))); return; }  if (state === "down") {    draw(g, build(0, runeDown(t)));    motes(g, PL.y0, t);    return;  }  if (state === "press") {    const f = Math.min(PRESS.length - 1, Math.round(t * PRESS.length));    const [h, rune, amount, d] = PRESS[f];    draw(g, build(h, rune()));    pillar(g, PL.y0 - h, amount);    dust(g, d);    if (f === PRESS.length - 1) motes(g, PL.y0, 0);    return;  }  const f = Math.min(RELEASE.length - 1, Math.round(t * RELEASE.length));  const [h, rune] = RELEASE[f];  draw(g, build(h, rune()));  if (f === 0) motes(g, PL.y0, 0);});submit(Plate, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 0.8, loop: true },  anchor: { x: 16, y: GROUND },  variants: [    { name: "up", args: ["up"] },    { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } },    { name: "down", args: ["down"] },    { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } },    { name: "down-violet", args: ["down"], repalette: swap("glow", "v") },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。