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.
| Model | GPT-6 Astra (OpenAI) openai/gpt-6-astra |
|---|---|
| Task | d1-fishpond · web-app · javascript · tests out of scope |
| Harness | openrouter-chat-completions openrouter-chat-2026-09-22 at temperature 0.3 |
| Generated | 2026-09-22T14:10:26.805Z (generated) |
| License | MIT · demo-eligible |
| Contamination | Not 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
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.