d1-fishpond--openai-gpt-6-astra

Build a single-file HTML game: a koi pond where fish gently follow the mouse cursor. One self-contained .html file (inline CSS/JS, no external dependencies, no build step). The fish should wander on their own and steer toward the cursor when it moves, with smooth motion and a calm, considered visual style. Deliver the complete file with no placeholders. Accompany the code with a brief explanation of your design choices.

ModelGPT-6 Astra (OpenAI) openai/gpt-6-astra
Taskd1-fishpond · web-app · javascript · tests out of scope
Harnessopenrouter-chat-completions openrouter-chat-2026-09-22 at temperature 0.3
Generated2026-09-22T14:10:26.805Z (generated)
LicenseMIT · demo-eligible
ContaminationNot previously public · No viral exposure recorded

Prompt

Build a single-file HTML game: a koi pond where fish gently follow the mouse cursor. One self-contained .html file (inline CSS/JS, no external dependencies, no build step). The fish should wander on their own and steer toward the cursor when it moves, with smooth motion and a calm, considered visual style. Deliver the complete file with no placeholders. Accompany the code with a brief explanation of your design choices. ## Output contract First, write a brief explanation of your design choices — plain prose, no code fences. Then emit exactly one fenced code block per file, and no other text between or after the blocks: - The FIRST line of every block is that file's marker line: - TypeScript or JavaScript: `// FILE: <path>` - Python: `# FILE: <path>` - A single-file web app is one file named `index.html`, so its marker line is `// FILE: index.html`. - Everything after the marker line is that file complete, from its first line to its last — no diffs, no omissions, no placeholders. - Tag each fence with the file's language, for example ```html, ```typescript, ```python.

Artifact

Form: single-file

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#173e40">
<title>Stillwater — a little koi pond</title>
<style>
  :root { color-scheme: dark; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
  * { box-sizing: border-box; }
  body { margin: 0; background: #102d30; color: #f0ebda; overflow: hidden; }
  main { position: relative; height: 100vh; height: 100svh; min-height: 320px; isolation: isolate; }
  canvas { display: block; width: 100%; height: 100%; touch-action: none; }
  header { position: absolute; top: clamp(24px, 5vw, 60px); left: clamp(24px, 5vw, 64px); pointer-events: none; }
  .eyebrow { margin: 0 0 12px; font-size: 10px; letter-spacing: .3em; text-transform: uppercase; opacity: .65; }
  h1 { font-family: Georgia, "Times New Roman", serif; font-size: clamp(34px, 6vw,  sixty); font-size: clamp(34px, 5vw, 58px); font-weight: 400; letter-spacing: -.035em; margin: 0; }
  .subtitle { margin: 12px 0 0; font-size: 13px; font-weight: 300; color: #baccc3; }
  .seal { position: absolute; top: clamp(24px, 5vw, 60px); right: clamp(24px, 5vw, 64px); writing-mode: vertical-rl; font-size: 10px; letter-spacing: .24em; opacity: .5; pointer-events: none; }
  footer { position: absolute; bottom: 28px; left: clamp(24px, 5vw, 64px); right: clamp(24px, 5vw, 64px); display: flex; justify-content: space-between; align-items: center; gap: 20px; }
  .hint { font-size: 11px; letter-spacing: .06em; line-height: 1.8; color: #c0d0c5; pointer-events: none; }
  .hint span { display: block; opacity: .55; font-size: 10px; }
  .controls { display: flex; gap: 8px; }
  button { appearance: none; background: #173e407a; border: 1px solid #c6dbca40; border-radius: 30px; padding: 11px 17px; color: #e1e9dc; font: inherit; font-size: 11px; letter-spacing: .06em; cursor: pointer; backdrop-filter: blur(10px); transition: background .2s, border-color .2s; }
  button:hover { background: #41645f99; border-color: #c6dbca80; }
  button:focus-visible { outline: 2px solid #e7d49c; outline-offset: 4px; }
  @media (max-width: 520px) {
    footer { bottom: 22px; align-items: flex-end; gap: 12px; }
    .hint { max-width: 140px; font-size: 10px; }
    .hint span { display: none; }
    button { padding: 10px 13px; }
    .seal { display: none; }
  }
  @media (prefers-reduced-motion: reduce) { button { transition: none; } }
</style>
</head>
<body>
<main aria-label="Stillwater, an interactive koi pond">
  <canvas id="pond" role="img" aria-label="Koi wander through a jade pond. Move your pointer to guide them, or tap to feed them. Use the buttons to feed or pause the pond."></canvas>
  <header>
    <p class="eyebrow">A small moment of quiet</p>
    <h1>Stillwater</h1>
    <p class="subtitle">Nothing to hurry. Nowhere to be.</p>
  </header>
  <div class="seal" aria-hidden="true">THE POND / No. 01</div>
  <footer>
    <div class="hint">Move slowly. They’ll follow.<span>Click or touch the water to feed.</span></div>
    <div class="controls">
      <button id="feed" type="button" aria-label="Add fish food to the center of the pond">Feed koi</button>
      <button id="pause" type="button" aria-pressed="false" aria-label="Pause pond animation">Pause</button>
    </div>
  </footer>
</main>
<script>
(() => {
  "use strict";
  const canvas = document.getElementById("pond");
  const ctx = canvas.getContext("2d");
  const backdrop = document.createElement("canvas");
  const bg = backdrop.getContext("2d");
  const pauseButton = document.getElementById("pause");
  const motionQuery = matchMedia("(prefers-reduced-motion: reduce)");
  const TAU = Math.PI * 2;
  const rand = (a, b) => a + Math.random() * (b - a);
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  let width = 0, height = 0, dpr = 1, time = 0, last = 0;
  let paused = false, reduced = motionQuery.matches, rippleClock = 0;
  const pointer = { x: 0, y: 0, moved: -100, inside: false };
  const fish = [], food = [], ripples = [];
  const palettes = [
    ["#eee3c6", "#c45c3d", "#293d37"],
    ["#e9d9b7", "#d17445", "#dfb879"],
    ["#e8e3d2", "#293f3c", "#ac4e36"],
    ["#dcae59", "#b07832", "#f0d08a"],
    ["#e5e7d9", "#849e96", "#ccd5c3"],
    ["#e4d6b7", "#b94d37", "#233c38"],
    ["#cc8650", "#f1dcb9", "#883f2c"],
    ["#e5e2cf", "#c7633e", "#b25236"]
  ];

  function ellipse(c, x, y, rx, ry, rotation, color) {
    c.fillStyle = color;
    c.beginPath();
    c.ellipse(x, y, rx, ry, rotation, 0, TAU);
    c.fill();
  }

  // Repaint the still pond floor only when the viewport changes.
  function paintBackdrop() {
    bg.setTransform(dpr, 0, 0, dpr, 0, 0);
    const water = bg.createLinearGradient(0, 0, width, height);
    water.addColorStop(0, "#193d40");
    water.addColorStop(.5, "#285951");
    water.addColorStop(1, "#123a3c");
    bg.fillStyle = water;
    bg.fillRect(0, 0, width, height);

    let seed = 418;
    const random = () => {
      seed = (seed * 16807) % 2147483647;
      return (seed - 1) / 2147483646;
    };
    for (let i = 0; i < 130; i++) {
      const x = random() * width, y = random() * height;
      const r = 4 + random() * 29;
      ellipse(bg, x, y, r, r * .65, random() * TAU, "rgba(8,32,31,.09)");
      ellipse(bg, x - 1, y - 2, r * .86, r * .5, 0, "rgba(131,156,125,.035)");
    }
    for (let i = 0; i < 900; i++) {
      bg.fillStyle = random() > .5 ? "#d4ddbd08" : "#001e260a";
      bg.fillRect(random() * width, random() * height, 1, 1);
    }
    const glow = bg.createRadialGradient(width * .55, height * .4, 0, width * .55, height * .4, Math.max(width, height) * .65);
    glow.addColorStop(0, "#99b98b13");
    glow.addColorStop(1, "#061e2d55");
    bg.fillStyle = glow;
    bg.fillRect(0, 0, width, height);

    const size = clamp(width / 1000, .6, 1.15);
    const pads = [
      [width - 52 * size, height * .29, 39, -.3],
      [width - 117 * size, height * .29 + 29 * size, 31, .8],
      [width - 59 * size, height * .29 + 70 * size, 27, 2.2],
      [34 * size, height * .77, 38, .2],
      [83 * size, height * .77 + 40 * size, 25, 1.5]
    ];
    for (const [x, y, radius, angle] of pads) {
      const r = radius * size;
      bg.save();
      bg.translate(x, y);
      bg.rotate(angle);
      ellipse(bg, 4, 8, r * 1.05, r * .85, 0, "#092e3277");
      bg.scale(1, .82);
      const leaf = bg.createLinearGradient(-r, -r, r, r);
      leaf.addColorStop(0, "#688664");
      leaf.addColorStop(1, "#355e50");
      bg.fillStyle = leaf;
      bg.beginPath();
      bg.moveTo(0, 0);
      bg.arc(0, 0, r, .18, TAU - .18);
      bg.closePath();
      bg.fill();
      bg.strokeStyle = "#adbf851c";
      bg.lineWidth = 1;
      for (let j = 1; j < 9; j++) {
        const a = j * TAU / 9;
        bg.beginPath();
        bg.moveTo(-2, 0);
        bg.quadraticCurveTo(Math.cos(a + .2) * r * .55, Math.sin(a + .2) * r * .55, Math.cos(a) * r * .9, Math.sin(a) * r * .9);
        bg.stroke();
      }
      bg.restore();
    }
  }

  function resize() {
    const oldW = width, oldH = height;
    width = canvas.clientWidth;
    height = canvas.clientHeight;
    dpr = Math.min(devicePixelRatio || 1, 2);
    canvas.width = backdrop.width = Math.round(width * dpr);
    canvas.height = backdrop.height = Math.round(height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    if (oldW && oldH) {
      for (const f of fish) {
        f.x *= width / oldW;
        f.y *= height / oldH;
      }
      for (const p of food) {
        p.x *= width / oldW;
        p.y *= height / oldH;
      }
    }
    paintBackdrop();
    render();
  }

  function addRipple(x, y, strength = 1) {
    ripples.push({ x, y, age: 0, strength });
    if (ripples.length > 35) ripples.shift();
  }

  function feedAt(x, y) {
    if (paused) return;
    for (let i = 0; i < 7; i++) {
      if (food.length >= 42) food.shift();
      food.push({
        x: clamp(x + rand(-22, 22), 15, width - 15),
        y: clamp(y + rand(-22, 22), 15, height - 15),
        age: 0,
        seed: rand(0, TAU)
      });
    }
    addRipple(x, y, 1.1);
  }

  function togglePause() {
    paused = !paused;
    pauseButton.textContent = paused ? "Resume" : "Pause";
    pauseButton.setAttribute("aria-pressed", String(paused));
    pauseButton.setAttribute("aria-label", paused ? "Resume pond animation" : "Pause pond animation");
  }

  function point(event) {
    const bounds = canvas.getBoundingClientRect();
    pointer.x = event.clientX - bounds.left;
    pointer.y = event.clientY - bounds.top;
    pointer.moved = time;
    pointer.inside = true;
  }

  canvas.addEventListener("pointermove", event => {
    point(event);
    if (!paused && time - rippleClock > .45) {
      addRipple(pointer.x, pointer.y, .35);
      rippleClock = time;
    }
  });
  canvas.addEventListener("pointerdown", event => {
    point(event);
    feedAt(pointer.x, pointer.y);
  });
  canvas.addEventListener("pointerleave", () => { pointer.inside = false; });
  canvas.addEventListener("pointerup", event => {
    if (event.pointerType !== "mouse") pointer.inside = false;
  });
  canvas.addEventListener("pointercancel", () => { pointer.inside = false; });
  document.getElementById("feed").addEventListener("click", () => feedAt(width * .5, height * .55));
  pauseButton.addEventListener("click", togglePause);
  window.addEventListener("keydown", event => {
    if (event.code === "Space" && event.target === document.body) {
      event.preventDefault();
      togglePause();
    }
  });
  motionQuery.addEventListener("change", event => { reduced = event.matches; });
  window.addEventListener("resize", resize);

  function update(dt) {
    time += dt;
    const scale = clamp(Math.min(width, height) / 680, .63, 1);
    for (const f of fish) {
      f.size = f.baseSize * scale;
      let desiredX = Math.cos(f.angle) * .65 + Math.cos(time * .23 + f.seed) * .65;
      let desiredY = Math.sin(f.angle) * .65 + Math.sin(time * .19 + f.seed * 2) * .65;
      let target = null, best = Infinity, appetite = false;
      for (const p of food) {
        const distance = Math.hypot(p.x - f.x, p.y - f.y);
        if (distance < best) { best = distance; target = p; appetite = true; }
      }
      const interest = pointer.inside ? clamp(1 - (time - pointer.moved) / 5, 0, 1) : 0;
      if (!target && interest > 0) target = pointer;
      let pace = 1;
      if (target) {
        const dx = target.x - f.x, dy = target.y - f.y;
        const distance = Math.hypot(dx, dy) || 1;
        const pull = appetite ? 2.3 : interest * 1.65;
        const comfort = appetite ? 12 : 64 * scale;
        const approach = clamp((distance - comfort) / 100, -.35, 1);
        desiredX += dx / distance * pull * approach;
        desiredY += dy / distance * pull * approach;
        pace = appetite ? 1.25 : .8 + .3 * clamp(distance / 200, 0, 1);
      }
      // A modest personal space keeps the school from collapsing into a point.
      for (const other of fish) {
        if (other === f) continue;
        const dx = f.x - other.x, dy = f.y - other.y;
        const distance = Math.hypot(dx, dy) || 1;
        const radius = 62 * scale;
        if (distance < radius) {
          desiredX += dx / distance * (1 - distance / radius) * 2.6;
          desiredY += dy / distance * (1 - distance / radius) * 2.6;
        }
      }
      const margin = 100 * scale;
      desiredX += Math.max(0, 1 - f.x / margin) * 4 - Math.max(0, 1 - (width - f.x) / margin) * 4;
      desiredY += Math.max(0, 1 - f.y / margin) * 4 - Math.max(0, 1 - (height - f.y) / margin) * 4;
      const desiredAngle = Math.atan2(desiredY, desiredX);
      const turn = Math.atan2(Math.sin(desiredAngle - f.angle), Math.cos(desiredAngle - f.angle));
      f.angle += clamp(turn, -1.25 * dt, 1.25 * dt);
      const speed = f.speed * scale * pace * (reduced ? .45 : 1);
      f.x = clamp(f.x + Math.cos(f.angle) * speed * dt, 10, width - 10);
      f.y = clamp(f.y + Math.sin(f.angle) * speed * dt, 10, height - 10);
      f.phase += dt * (reduced ? 1.5 : 3.2) * pace;
      const noseX = f.x + Math.cos(f.angle) * f.size * .42;
      const noseY = f.y + Math.sin(f.angle) * f.size * .42;
      for (let i = food.length - 1; i >= 0; i--) {
        if (Math.hypot(food[i].x - noseX, food[i].y - noseY) < 15 * scale) {
          addRipple(food[i].x, food[i].y, .3);
          food.splice(i, 1);
        }
      }
    }
    for (let i = food.length - 1; i >= 0; i--) {
      const p = food[i];
      p.age += dt;
      p.x += Math.sin(time * .5 + p.seed) * dt * .8;
      if (p.age > 24) food.splice(i, 1);
    }
    for (let i = ripples.length - 1; i >= 0; i--) {
      ripples[i].age += dt;
      if (ripples[i].age > 3.5) ripples.splice(i, 1);
    }
  }

  function drawFish(f) {
    const length = f.size;
    const spine = t => Math.sin(f.phase - t * 4.2) * length * .073 * t * t;
    const body = new Path2D();
    const steps = 24;
    for (let side = 1; side >= -1; side -= 2) {
      for (let i = 0; i <= steps; i++) {
        const t = side === 1 ? i / steps : 1 - i / steps;
        const breadth = Math.pow(Math.max(0, Math.sin(Math.PI * t)), .65) * length * .145 * (1 - t * .48);
        const x = -t * length, y = spine(t) + side * breadth;
        if (side === 1 && i === 0) body.moveTo(x, y);
        else body.lineTo(x, y);
      }
    }
    body.closePath();
    ctx.save();
    ctx.translate(f.x + 7, f.y + 12);
    ctx.rotate(f.angle);
    ctx.translate(length * .43, 0);
    ctx.fillStyle = "#062b3048";
    ctx.fill(body);
    ctx.restore();

    ctx.save();
    ctx.translate(f.x, f.y);
    ctx.rotate(f.angle);
    ctx.translate(length * .43, 0);

    // Translucent paired pectoral fins.
    for (const side of [-1, 1]) {
      ctx.fillStyle = f.colors[0] + "65";
      ctx.beginPath();
      ctx.moveTo(-length * .31, side * length * .08);
      ctx.quadraticCurveTo(-length * .40, side * length * (.30 + Math.sin(f.phase) * .025), -length * .58, side * length * .24);
      ctx.quadraticCurveTo(-length * .52, side * length * .12, -length * .43, side * length * .07);
      ctx.fill();
    }
    const tailY = spine(1);
    ctx.fillStyle = f.colors[0] + "ad";
    ctx.beginPath();
    ctx.moveTo(-length * .92, spine(.92));
    ctx.quadraticCurveTo(-length * 1.08, tailY - length * .03, -length * 1.18, tailY - length * .16);
    ctx.quadraticCurveTo(-length * 1.22, tailY, -length * 1.13, tailY + length * .04);
    ctx.lineTo(-length * 1.19, tailY + length * .16);
    ctx.quadraticCurveTo(-length * 1.01, tailY + length * .10, -length * .92, spine(.92));
    ctx.fill();

    ctx.fillStyle = f.colors[0];
    ctx.fill(body);
    ctx.save();
    ctx.clip(body);
    for (const patch of f.patches) {
      ellipse(ctx, -length * patch.t, spine(patch.t) + patch.y * length,
        patch.rx * length, patch.ry * length, patch.angle, f.colors[patch.color]);
    }
    const volume = ctx.createLinearGradient(0, -length * .15, 0, length * .16);
    volume.addColorStop(0, "#123f3940");
    volume.addColorStop(.4, "#ffffff12");
    volume.addColorStop(.6, "#fff9d522");
    volume.addColorStop(1, "#163c3f60");
    ctx.fillStyle = volume;
    ctx.fillRect(-length, -length * .3, length, length * .6);
    ctx.strokeStyle = "#fff9df38";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(-length * .29, -length * .012);
    ctx.quadraticCurveTo(-length * .55, spine(.55) - 2, -length * .79, spine(.79));
    ctx.stroke();
    ctx.restore();

    for (const side of [-1, 1]) {
      ellipse(ctx, -length * .12, side * length * .054, length * .018, length * .013, 0, "#293c35");
      ellipse(ctx, -length * .116, side * length * .054 - .4, length * .005, length * .004, 0, "#f7e7c4");
    }
    ctx.restore();
  }

  function render() {
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(backdrop, 0, 0, width, height);

    // Long, barely visible caustic lines move above the pond floor.
    ctx.lineWidth = 1;
    for (let row = 0; row < 9; row++) {
      ctx.beginPath();
      for (let x = -20; x <= width + 20; x += 20) {
        const y = height * (row + .5) / 9 +
          Math.sin(x * .006 + time * .12 + row * 1.7) * 18 +
          Math.sin(x * .012 - time * .08 + row) * 9;
        if (x === -20) ctx.moveTo(x, y); else ctx.lineTo(x, y);
      }
      ctx.strokeStyle = "#c6dec009";
      ctx.stroke();
    }
    for (const f of fish) drawFish(f);
    for (const p of food) {
      const alpha = clamp((24 - p.age) / 3, 0, 1);
      ctx.globalAlpha = alpha;
      ellipse(ctx, p.x + 1, p.y + 2, 2.7, 2, 0, "#0f323f70");
      ellipse(ctx, p.x, p.y, 2, 1.5, p.seed, "#d6b574");
    }
    ctx.globalAlpha = 1;
    for (const r of ripples) {
      const fade = Math.pow(1 - r.age / 3.5, 2) * r.strength;
      for (let ring = 0; ring < 2; ring++) {
        const radius = 4 + r.age * (reduced ? 13 : 23) - ring * 10;
        if (radius <= 0) continue;
        ctx.strokeStyle = `rgba(206,228,202,${fade * .22})`;
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.ellipse(r.x, r.y, radius, radius * .82, 0, 0, TAU);
        ctx.stroke();
      }
    }
    for (let i = 0; i < 24; i++) {
      const x = ((i * 173.71 + Math.sin(time * .1 + i) * 12) % width + width) % width;
      const y = ((i * 97.33 + time * (reduced ? .3 : 1.2)) % height + height) % height;
      ellipse(ctx, x, y, 1.2, .8, 0, "#d9e9bf27");
    }
  }

  resize();
  palettes.forEach((colors, i) => {
    const angle = i / palettes.length * TAU;
    fish.push({
      x: width * .5 + Math.cos(angle) * width * .24,
      y: height * .53 + Math.sin(angle) * height * .23,
      angle: angle + Math.PI / 2,
      baseSize: rand(73, 108),
      size: 80,
      speed: rand(22, 33),
      phase: rand(0, TAU),
      seed: rand(0, 20),
      colors,
      patches: Array.from({ length: 7 }, (_, j) => ({
        t: rand(.12, .87),
        y: rand(-.075, .075),
        rx: rand(.045, .13),
        ry: rand(.055, .13),
        angle: rand(-1, 1),
        color: j % 3 === 0 ? 2 : 1
      }))
    });
  });
  update(0);
  render();

  document.addEventListener("visibilitychange", () => { last = 0; });
  function frame(now) {
    const dt = last ? Math.min((now - last) / 1000, .04) : 0;
    last = now;
    if (!paused && !document.hidden) {
      update(dt);
      render();
    }
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Explanation

The pond uses a muted jade palette, softly layered water, and individually patterned koi with gently undulating bodies. Fish wander, avoid crowding, and gradually turn toward a moving cursor; clicking or tapping adds food and ripples. The canvas adapts to high-density displays, supports touch, and includes pause controls and reduced-motion handling.

Judge detail

No raw judge output is published for this item yet. When it is, it lands under results/raw/ and appears here verbatim; the rubric and protocol are already documented on the Methodology page.