← back to Model Arena
contrarian FIX FIRST: gate retry to existing competitors (kills metered spend-injection), serialize metered per-model + double-fire guard, stuck-run watchdog + corrupt-record heal, gpt-5.1 id, winner breakout layout + crown tooltip + honest cost label
fbf19ba16e13c96003e55c664ae39b2a2a587ec2 · 2026-07-22 22:38:38 -0700 · Steve
Files touched
A 5x/sweep-2-contrarian-fixes.mdM data/artifacts/0ac1af2eaed4/gemma3-12b.htmlM data/artifacts/0ac1af2eaed4/hermes3-8b.htmlA data/artifacts/0ac1af2eaed4/kimi.htmlA data/artifacts/0ac1af2eaed4/qwen3-14b.htmlA data/artifacts/74df3b61e7ca/grok.htmlA data/artifacts/966d54f224fd/gemma3-12b.htmlA data/artifacts/966d54f224fd/grok.htmlA data/artifacts/966d54f224fd/hermes3-8b.htmlA data/artifacts/966d54f224fd/kimi.htmlM data/challenges.jsonA data/costlog.jsonlM public/index.htmlM server.js
Diff
commit fbf19ba16e13c96003e55c664ae39b2a2a587ec2
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 22:38:38 2026 -0700
contrarian FIX FIRST: gate retry to existing competitors (kills metered spend-injection), serialize metered per-model + double-fire guard, stuck-run watchdog + corrupt-record heal, gpt-5.1 id, winner breakout layout + crown tooltip + honest cost label
---
5x/sweep-2-contrarian-fixes.md | 15 +
data/artifacts/0ac1af2eaed4/gemma3-12b.html | 159 ++++----
data/artifacts/0ac1af2eaed4/hermes3-8b.html | 122 +++---
data/artifacts/0ac1af2eaed4/kimi.html | 57 +++
data/artifacts/0ac1af2eaed4/qwen3-14b.html | 13 +
data/artifacts/74df3b61e7ca/grok.html | 591 ++++++++++++++++++++++++++++
data/artifacts/966d54f224fd/gemma3-12b.html | 184 +++++++++
data/artifacts/966d54f224fd/grok.html | 256 ++++++++++++
data/artifacts/966d54f224fd/hermes3-8b.html | 284 +++++++++++++
data/artifacts/966d54f224fd/kimi.html | 135 +++++++
data/challenges.json | 204 +++++++++-
data/costlog.jsonl | 4 +
public/index.html | 13 +-
server.js | 58 ++-
14 files changed, 1937 insertions(+), 158 deletions(-)
diff --git a/5x/sweep-2-contrarian-fixes.md b/5x/sweep-2-contrarian-fixes.md
new file mode 100644
index 0000000..dd2a8d7
--- /dev/null
+++ b/5x/sweep-2-contrarian-fixes.md
@@ -0,0 +1,15 @@
+# post-/contrarian FIX FIRST pass — 2026-07-22
+
+Panel verdict: FIX FIRST (4× FIX FIRST, 1× KILL-as-scoped/Strategist).
+
+## Blockers fixed (this pass)
+1. **Metered spend-injection (Engineer #1, live $-vector):** `/retry/:model` now 400s if the model isn't already a competitor in that battle. Was: anyone with the shared cred could inject paid models onto every challenge and loop. Verified: retry/claude→400, retry/hermes3-8b→200.
+2. **Metered double-spend race (Engineer #2):** metered generations now serialize per-model via onKey('metered:'+id); runModel has a double-fire guard (no-op if already running/queued). Two rapid retries queue instead of both charging.
+3. **Zombie/stuck runs (User/Skeptic/Engineer #3):** startup sentinel now also heals corrupt records (finished_at<started_at); a 30s watchdog fails runs stuck past 660s running / 20min queued so the UI shows an honest error instead of an infinite poll. Verified: all prior zombie runs healed to error.
+4. **gpt-5.3 hallucinated id (Skeptic/Strategist):** the live "neon city" battle proved gpt-5.3 404s; changed to gpt-5.1 (verified against OpenAI /models), env-overridable OPENAI_MODEL.
+5. **Winner not a visual event (Designer):** `.pane.winner` breaks the grid (grid-column:1/-1, order:-1, taller iframe, one-time gold flash) — champion on top, full width.
+6. **Crown unexplained (User):** Crown button now has a title tooltip explaining it records a ledger vote.
+7. **Dishonest cost estimate (Skeptic):** relabeled "Estimated battle cost" → "Rough ceiling (flat per-model guess; actual metered by real tokens)".
+
+## Deferred to Steve (Strategist dissent — a product-scope call, not an autonomous fix)
+The arena's challenges are game/toy demos; the Strategist argues the win-rate ledger only becomes a real "moat" if challenges are drawn from Steve's actual production workloads (product-description gen, vendor-PDF JSON extraction, inquiry classification) with rubric scores, echoing ollama-model-eval. That's a direction change — surfaced for Steve, not applied.
diff --git a/data/artifacts/0ac1af2eaed4/gemma3-12b.html b/data/artifacts/0ac1af2eaed4/gemma3-12b.html
index 395d342..4ef3802 100644
--- a/data/artifacts/0ac1af2eaed4/gemma3-12b.html
+++ b/data/artifacts/0ac1af2eaed4/gemma3-12b.html
@@ -1,117 +1,134 @@
<!DOCTYPE html>
-<html>
-<head>
+<html><head>
<title>Fireworks Simulator</title>
<style>
body { margin: 0; overflow: hidden; background-color: black }
canvas { display: block }
</style>
-</head>
-<body>
+</head><body>
<canvas id="fireworksCanvas"></canvas>
<script>
const canvas = document.getElementById('fireworksCanvas');
const ctx = canvas.getContext('2d');
+
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let fireworks = [];
-const numFireworks = 30;
+const numFireworks = 10; // Adjust for density
class Firework {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- this.radius = Math.random() * 2 + 1;
- this.color = `hsl(${Math.random() * 360}, 100%, ${Math.random() * 50 + 50}%)`;
- this.particles = [];
- this.angle = Math.random() * 2 * Math.PI;
- this.speed = Math.random() * 5 + 10;
+ constructor() {
+ this.x = Math.random() * canvas.width;
+ this.y = canvas.height;
+ this.angle = Math.random() * Math.PI * 2;
+ this.speed = Math.random() * 5 + 3;
this.gravity = 0.1;
- this.damping = 0.95;
+ this.friction = 0.98;
+ this.particles = [];
+ this.lifespan = 100;
}
launch() {
- for (let i = 0; i < 30; i++) {
- const angle = this.angle + Math.random() * 0.4 - 0.2 * Math.PI;
- this.particles.push({
- x: this.x + Math.cos(angle) * this.radius,
- y: this.y + Math.sin(angle) * this.radius,
- vx: Math.cos(angle) * this.speed,
- vy: Math.sin(angle) * this.speed,
- life: 1 - Math.random() * 0.5,
- color: this.color
- });
- }
+ this.velocity = { x: Math.cos(this.angle) * this.speed, y: -Math.sin(this.angle) * this.speed };
}
update() {
- if (this.particles.length > 0) {
- for (let i = this.particles.length - 1; i >= 0; i--) {
- const particle = this.particles[i];
- particle.vx *= this.damping;
- particle.vy -= this.gravity * particle.life;
- particle.x += particle.vx;
- particle.y += particle.vy;
-
- if (particle.life <= 0 || particle.y > canvas.height) {
- this.particles.splice(i, 1);
- }
- }
+ if (!this.launched) return;
+ this.velocity.x *= this.friction;
+ this.velocity.y += this.gravity;
+ this.x += this.velocity.x;
+ this.y += this.velocity.y;
+ this.lifespan--;
+
+ if (this.lifespan <= 0) {
+ this.explode();
+ return true; // Signal to remove the firework
}
+ return false;
}
- draw() {
- ctx.fillStyle = this.color;
- for (const particle of this.particles) {
- ctx.beginPath();
- ctx.arc(particle.x, particle.y, Math.max(1, particle.life * 2), 0, 2 * Math.PI);
- ctx.fill();
+ explode() {
+ const particleCount = Math.floor(Math.random() * 30 + 30);
+ for (let i = 0; i < particleCount; i++) {
+ const angle = Math.random() * Math.PI * 2;
+ const speed = Math.random() * 2 + 1;
+ this.particles.push({ x: this.x, y: this.y, angle, speed });
}
}
-}
-function addFirework() {
- fireworks.push(new Firework(Math.random() * canvas.width, -50));
-}
+ draw() {
+ if (!this.launched) return;
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, 5, 0, Math.PI * 2);
+ ctx.fillStyle = 'white';
+ ctx.fill();
+ }
-canvas.addEventListener('click', () => {
- addFirework();
-});
+ drawParticles() {
+ for (let i = 0; i < this.particles.length; i++) {
+ const particle = this.particles[i];
+ particle.angle += Math.random()*0.05 - 0.025; // slight random drift
+ particle.speed *= 0.98;
-let autoShow = true; // Auto-show mode
-setInterval(() => {
- if (autoShow) {
- addFirework();
- }
-}, 500); // Adjust interval for frequency
+ ctx.beginPath();
+ ctx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
+ ctx.fill();
+
+ particle.x += Math.cos(particle.angle) * particle.speed;
+ particle.y += -Math.sin(particle.angle) * particle.speed;
+ }
+ }
+}
+function init() {
+ for (let i = 0; i < numFireworks; i++) {
+ fireworks.push(new Firework());
+ }
+}
-function animate() {
+function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
- for (let i = fireworks.length - 1; i >= 0; i--) {
- fireworks[i].launch();
- fireworks[i].update();
- fireworks[i].draw();
-
- if(fireworks[i].particles.length === 0) {
- fireworks.splice(i, 1);
+ for (let i = 0; i < fireworks.length; i++) {
+ const firework = fireworks[i];
+ if (firework.update()) {
+ fireworks.splice(i, 1); // Remove expired/exploded fireworks
+ i--;
+ continue;
}
+
+ firework.draw();
+ firework.drawParticles();
}
- requestAnimationFrame(animate);
+ requestAnimationFrame(draw);
}
-animate();
-window.addEventListener('resize', () => {
- canvas.width = window.innerWidth;
- canvas.height = window.innerHeight;
+canvas.addEventListener('click', () => {
+ const newFirework = new Firework();
+ newFirework.launch();
+ fireworks.push(newFirework);
});
+
+
+function autoShow() {
+ setInterval(() => {
+ const newFirework = new Firework();
+ newFirework.launch();
+ fireworks.push(newFirework);
+ }, 1500); // Adjust interval as needed
+}
+
+init();
+draw();
+autoShow();
+
+
</script>
-</body>
-</html>
\ No newline at end of file
+</body></html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/hermes3-8b.html b/data/artifacts/0ac1af2eaed4/hermes3-8b.html
index 9645581..ad9ab80 100644
--- a/data/artifacts/0ac1af2eaed4/hermes3-8b.html
+++ b/data/artifacts/0ac1af2eaed4/hermes3-8b.html
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Interactive Fireworks Simulator</title>
+ <title>Fireworks Simulator</title>
<style>
body {
margin: 0;
@@ -11,97 +11,99 @@
touch-action: none;
cursor: crosshair;
}
- canvas {
+ #canvas {
display: block;
}
</style>
</head>
<body>
-<canvas></canvas>
+<canvas id="canvas" width="400" height="300"></canvas>
<script>
- const canvas = document.querySelector('canvas');
+ const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
- // Set canvas size
- canvas.width = window.innerWidth * 0.9;
- canvas.height = window.innerHeight * 0.8;
-
- let particles = [];
let showMode = false;
- class Particle {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- this.size = Math.random() * 3 + 1;
- this.speedX = (Math.random() - 0.5) * 4;
- this.speedY = (Math.random() - 0.5) * 4;
- }
-
- draw() {
- ctx.beginPath();
- ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
- ctx.fillStyle = `rgba(255, ${Math.max(0, 255 - this.size)}, 0, 0.75)`;
- ctx.fill();
-
- if (this.size > 1) {
- ctx.strokeStyle = `rgba(255, ${Math.max(0, 255 - this.size)}, 0, 0.5)`;
- ctx.lineWidth = Math.floor(this.size / 2);
- ctx.strokeRect(this.x - (this.size / 2), this.y - (this.size / 2), this.size, this.size);
- }
- }
-
- update() {
- this.x += this.speedX;
- this.y += this.speedY;
+ function getRandomValue(min, max) {
+ return Math.random() * (max - min) + min;
+ }
- if (this.x + this.size > canvas.width || this.x - this.size < 0)
- this.speedX = -this.speedX;
-
- if (this.y + this.size > canvas.height || this.y - this.size < 0)
- this.speedY = -this.speedY;
- }
+ function Particle(x, y) {
+ this.x = x;
+ this.y = y;
+ this.size = getRandomValue(10, 15);
+ this.speedX = getRandomValue(-5, 5);
+ this.speedY = -getRandomValue(1, 5);
+ this.color = 'rgba(255, 255, 255, 0.8)';
}
+ Particle.prototype.draw = function() {
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
+ ctx.fillStyle = this.color;
+ ctx.fill();
+ ctx.stroke();
+ };
+
function createExplosion(x, y) {
+ let particles = [];
for (let i = 0; i < 50; i++) {
particles.push(new Particle(x, y));
}
+ return particles;
}
- function loop() {
+ function drawParticles(particles) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
+ for (let i = 0; i < particles.length; i++) {
+ let particle = particles[i];
+ particle.draw();
+ particle.y += particle.speedY;
+ particle.x += particle.speedX;
- if (!showMode) {
- particles.forEach((particle, index, array) => {
- particle.update();
- particle.draw();
+ if (
+ particle.x + particle.size > canvas.width ||
+ particle.x - particle.size < 0 ||
+ particle.y + particle.size > canvas.height ||
+ particle.y - particle.size < 0
+ ) {
+ particles.splice(i, 1);
+ i--;
+ }
+ }
+ }
- if (particle.y + particle.size < 0 || particle.y - particle.size > canvas.height ||
- particle.x + particle.size < 0 || particle.x - particle.size > canvas.width) {
- array.splice(index, 1);
- }
- });
+ function loop() {
+ let particles = [];
+
+ if (!showMode) {
+ for (let i = 0; i < 5; i++) {
+ let x = getRandomValue(100, canvas.width - 100);
+ let y = canvas.height;
+ particles.push(...createExplosion(x, y));
+ }
} else {
- particles.forEach(particle => {
- particle.size -= Math.random() * 2;
- if (particle.size <= 0) {
- particles.splice(particles.indexOf(particle), 1);
- }
- particle.draw();
- });
+ for (let i = 0; i < 10; i++) {
+ let x = getRandomValue(50, canvas.width - 50);
+ let y = getRandomValue(50, canvas.height - 100);
+ particles.push(...createExplosion(x, y));
+ }
}
- requestAnimationFrame(loop);
+ drawParticles(particles);
+
+ if (!showMode) {
+ requestAnimationFrame(loop);
+ } else {
+ setTimeout(loop, 1000 / 60); // 60 FPS
+ }
}
canvas.addEventListener('click', () => {
showMode = !showMode;
- createExplosion(canvas.width / 2, canvas.height / 2);
});
loop();
-
</script>
</body>
</html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/kimi.html b/data/artifacts/0ac1af2eaed4/kimi.html
new file mode 100644
index 0000000..3e082ba
--- /dev/null
+++ b/data/artifacts/0ac1af2eaed4/kimi.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+body{margin:0;overflow:hidden;background:#000}
+canvas{display:block}
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<script>
+const c=document.getElementById('c'),x=c.getContext('2d');
+let w=c.width=innerWidth,h=c.height=innerHeight;
+onresize=()=>{w=c.width=innerWidth;h=c.height=innerHeight};
+const R=[],P=[];
+class Particle{
+constructor(x,y,c){
+this.x=x;this.y=y;
+const a=Math.random()*6.28,v=Math.random()*6;
+this.vx=Math.cos(a)*v;this.vy=Math.sin(a)*v;
+this.l=1;this.c=c;this.g=0.08;
+}
+update(){
+this.vy+=this.g;this.x+=this.vx;this.y+=this.vy;this.l-=0.015;
+x.globalAlpha=this.l;x.fillStyle=this.c;x.beginPath();x.arc(this.x,this.y,2.5,0,7);x.fill();
+}
+}
+class Rocket{
+constructor(tx,ty){
+this.x=Math.random()*w;this.y=h;this.tx=tx;this.ty=ty;
+const dx=tx-this.x,dy=ty-h,d=Math.sqrt(dx*dx+dy*dy);
+this.vx=dx/d*12;this.vy=dy/d*12;this.dead=0;
+this.c=`hsl(${Math.random()*360},100%,60%)`;
+}
+update(){
+this.x+=this.vx;this.y+=this.vy;
+if(Math.hypot(this.x-this.tx,this.y-this.ty)<10||this.vy>0){
+this.dead=1;
+for(let i=0;i<40;i++)P.push(new Particle(this.x,this.y,this.c));
+}
+x.fillStyle=this.c;x.beginPath();x.arc(this.x,this.y,3,0,7);x.fill();
+}
+}
+function loop(){
+x.globalCompositeOperation='source-over';
+x.fillStyle='rgba(0,0,0,0.15)';x.fillRect(0,0,w,h);
+x.globalCompositeOperation='lighter';
+for(let i=R.length-1;i>=0;i--){R[i].update();if(R[i].dead)R.splice(i,1)}
+for(let i=P.length-1;i>=0;i--){P[i].update();if(P[i].l<=0)P.splice(i,1)}
+requestAnimationFrame(loop);
+}
+c.onclick=e=>R.push(new Rocket(e.clientX,e.clientY));
+setInterval(()=>R.push(new Rocket(Math.random()*w,Math.random()*h*0.6)),600);
+loop();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/qwen3-14b.html b/data/artifacts/0ac1af2eaed4/qwen3-14b.html
new file mode 100644
index 0000000..32e4664
--- /dev/null
+++ b/data/artifacts/0ac1af2eaed4/qwen3-14b.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>canvas{display:block;background:#000;}</style>
+</head>
+<body>
+<canvas></canvas>
+<script>
+const canvas=document.querySelector("canvas"),ctx=canvas.getContext("2d"),rockets=[],particles=[],gravity=0.5;
+function addRocket(x,y){rockets.push({x,y,vx:0,vy:-5,exploded:false,trail:[]});}
+function explodeRocket(r){for(let i=0;i<50;i++){const a=Math.random()*Math.PI*2,s=Math.random()*5+3;particles.push({x:r.x,y:r.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-2,life:100,color:`hsl(${Math.random()*360},100%,70%)`,trail:[{x:r.x,y:r.y}]})}r.exploded=true;}
+function update(){for(let i=rockets.length-1;i>=0;i--){let r=rockets[i];if(!r.exploded){r.vy+=gravity;r.x+=r.vx;r.y+=r.vy;r.trail.push({x:r.x,y:r.y});if(r.trail.length>10)r.trail.shift();if(r.y<0)explodeRocket(r)}}for(let i=particles.length-1;i>=0;i--){let p=particles[i];p.vy+=gravity;p.x+=p.vx;p.y+=p.vy;p.life--;p.trail.push({x:p.x,y:p.y});if(p.trail.length>10)p.trail.shift();if(p.life<=0)particles.splice(i,1);}}
+function draw(){ctx.fillStyle="rgba(0,0,0,0.2)";ctx.fillRect(0,0,canvas.width,canvas.height);for(let r of rockets){if(!r.exploded){ctx.beginPath();ctx.arc(r.x,r.y,4,0,Math.PI*2);ctx.fillStyle="white";ctx.fill();ctx.beginPath();for(let j=0;j<r.trail.length;j++){let p=r.trail[j];ctx.moveTo(p.x,p.y);ctx
\ No newline at end of file
diff --git a/data/artifacts/74df3b61e7ca/grok.html b/data/artifacts/74df3b61e7ca/grok.html
new file mode 100644
index 0000000..e5f702b
--- /dev/null
+++ b/data/artifacts/74df3b61e7ca/grok.html
@@ -0,0 +1,591 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Synthwave Visualizer</title>
+<style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ html, body {
+ width: 100%; height: 100%;
+ overflow: hidden;
+ background: #0a0015;
+ font-family: "Segoe UI", system-ui, sans-serif;
+ }
+ canvas {
+ display: block;
+ width: 100%; height: 100%;
+ position: absolute; inset: 0;
+ }
+ #ui {
+ position: absolute;
+ bottom: 28px; left: 50%;
+ transform: translateX(-50%);
+ z-index: 10;
+ display: flex;
+ gap: 14px;
+ align-items: center;
+ }
+ button {
+ background: linear-gradient(180deg, #ff2a6d 0%, #d4004f 100%);
+ color: #fff;
+ border: 2px solid #ff6b9d;
+ border-radius: 4px;
+ padding: 12px 36px;
+ font-size: 14px;
+ font-weight: 700;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ cursor: pointer;
+ box-shadow:
+ 0 0 20px rgba(255, 42, 109, 0.5),
+ inset 0 1px 0 rgba(255,255,255,0.25);
+ transition: transform 0.1s, box-shadow 0.15s;
+ }
+ button:hover {
+ box-shadow:
+ 0 0 30px rgba(255, 42, 109, 0.8),
+ inset 0 1px 0 rgba(255,255,255,0.3);
+ }
+ button:active { transform: scale(0.96); }
+ button.playing {
+ background: linear-gradient(180deg, #05d9e8 0%, #018a94 100%);
+ border-color: #5ef0ff;
+ box-shadow:
+ 0 0 20px rgba(5, 217, 232, 0.5),
+ inset 0 1px 0 rgba(255,255,255,0.25);
+ }
+ #title {
+ position: absolute;
+ top: 22px; left: 50%;
+ transform: translateX(-50%);
+ z-index: 10;
+ color: #ff2a6d;
+ font-size: clamp(14px, 3vw, 22px);
+ font-weight: 800;
+ letter-spacing: 0.45em;
+ text-transform: uppercase;
+ text-shadow:
+ 0 0 10px rgba(255,42,109,0.8),
+ 0 0 40px rgba(255,42,109,0.4),
+ 2px 2px 0 #05d9e8;
+ pointer-events: none;
+ white-space: nowrap;
+ }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div id="title">SYNTHWAVE</div>
+<div id="ui">
+ <button id="btn" type="button">PLAY</button>
+</div>
+<script>
+(function () {
+ const canvas = document.getElementById("c");
+ const ctx = canvas.getContext("2d");
+ const btn = document.getElementById("btn");
+
+ let W, H, dpr;
+ function resize() {
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
+ W = window.innerWidth;
+ H = window.innerHeight;
+ canvas.width = W * dpr;
+ canvas.height = H * dpr;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ }
+ window.addEventListener("resize", resize);
+ resize();
+
+ // ─── Audio ───────────────────────────────────────────────
+ let audioCtx = null;
+ let masterGain, analyser;
+ let playing = false;
+ let schedTimer = null;
+ let nextNoteTime = 0;
+ let step = 0;
+ const lookahead = 0.08;
+ const scheduleAhead = 0.25;
+ const bpm = 108;
+ const stepDur = (60 / bpm) / 4; // 16th notes
+
+ // Bass pattern (A minor synthwave)
+ const bassNotes = [
+ 55.00, 0, 55.00, 0, 55.00, 0, 65.41, 0,
+ 73.42, 0, 73.42, 0, 65.41, 0, 55.00, 0,
+ 49.00, 0, 49.00, 0, 49.00, 0, 55.00, 0,
+ 58.27, 0, 58.27, 0, 55.00, 0, 49.00, 0
+ ];
+
+ // Lead arpeggio
+ const leadNotes = [
+ 220.00, 261.63, 329.63, 392.00,
+ 329.63, 261.63, 220.00, 261.63,
+ 246.94, 293.66, 369.99, 440.00,
+ 369.99, 293.66, 246.94, 293.66,
+ 196.00, 246.94, 293.66, 369.99,
+ 293.66, 246.94, 196.00, 246.94,
+ 220.00, 277.18, 329.63, 415.30,
+ 329.63, 277.18, 220.00, 277.18
+ ];
+
+ // Pad chords (root frequencies for layered saws)
+ const padChords = [
+ [110, 164.81, 220], // Am
+ [98, 146.83, 196], // G
+ [87.31, 130.81, 174.61], // F
+ [98, 146.83, 196] // G
+ ];
+
+ function initAudio() {
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ masterGain = audioCtx.createGain();
+ masterGain.gain.value = 0.35;
+ analyser = audioCtx.createAnalyser();
+ analyser.fftSize = 256;
+ analyser.smoothingTimeConstant = 0.78;
+ masterGain.connect(analyser);
+ analyser.connect(audioCtx.destination);
+ }
+
+ function playOsc(type, freq, t0, dur, gainVal, detune) {
+ const osc = audioCtx.createOscillator();
+ const g = audioCtx.createGain();
+ const f = audioCtx.createBiquadFilter();
+ osc.type = type;
+ osc.frequency.setValueAtTime(freq, t0);
+ if (detune) osc.detune.setValueAtTime(detune, t0);
+ f.type = "lowpass";
+ f.frequency.setValueAtTime(1800, t0);
+ f.Q.value = 4;
+ g.gain.setValueAtTime(0.0001, t0);
+ g.gain.exponentialRampToValueAtTime(gainVal, t0 + 0.02);
+ g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+ osc.connect(f);
+ f.connect(g);
+ g.connect(masterGain);
+ osc.start(t0);
+ osc.stop(t0 + dur + 0.05);
+ }
+
+ function playNoiseHit(t0, dur, gainVal) {
+ const len = Math.max(1, Math.floor(audioCtx.sampleRate * dur));
+ const buf = audioCtx.createBuffer(1, len, audioCtx.sampleRate);
+ const data = buf.getChannelData(0);
+ for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
+ const src = audioCtx.createBufferSource();
+ src.buffer = buf;
+ const g = audioCtx.createGain();
+ const f = audioCtx.createBiquadFilter();
+ f.type = "highpass";
+ f.frequency.value = 4000;
+ g.gain.setValueAtTime(gainVal, t0);
+ g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+ src.connect(f);
+ f.connect(g);
+ g.connect(masterGain);
+ src.start(t0);
+ src.stop(t0 + dur + 0.02);
+ }
+
+ function playKick(t0) {
+ const osc = audioCtx.createOscillator();
+ const g = audioCtx.createGain();
+ osc.type = "sine";
+ osc.frequency.setValueAtTime(140, t0);
+ osc.frequency.exponentialRampToValueAtTime(40, t0 + 0.12);
+ g.gain.setValueAtTime(0.9, t0);
+ g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.28);
+ osc.connect(g);
+ g.connect(masterGain);
+ osc.start(t0);
+ osc.stop(t0 + 0.3);
+ }
+
+ function scheduleNote(s, t) {
+ const i = s % 32;
+ const beat = s % 16;
+
+ // Kick on 1 and 3
+ if (beat === 0 || beat === 8) playKick(t);
+ // Snare-ish noise on 2 and 4
+ if (beat === 4 || beat === 12) playNoiseHit(t, 0.1, 0.18);
+ // Hi-hat every other 16th
+ if (s % 2 === 0) playNoiseHit(t, 0.035, 0.05);
+
+ // Bass
+ const bn = bassNotes[i];
+ if (bn > 0) {
+ playOsc("sawtooth", bn, t, stepDur * 1.6, 0.22, 0);
+ playOsc("square", bn * 0.5, t, stepDur * 1.6, 0.1, -8);
+ }
+
+ // Lead every step with slight rests
+ const ln = leadNotes[i];
+ if (i % 4 !== 3) {
+ playOsc("square", ln, t, stepDur * 0.9, 0.07, 0);
+ playOsc("square", ln, t, stepDur * 0.9, 0.04, 7);
+ }
+
+ // Pad on bar starts
+ if (i % 8 === 0) {
+ const chord = padChords[Math.floor(i / 8) % 4];
+ const padDur = stepDur * 8.5;
+ chord.forEach((f, idx) => {
+ playOsc("sawtooth", f, t, padDur, 0.04, idx * 3);
+ playOsc("sawtooth", f * 2, t, padDur, 0.02, -idx * 2);
+ });
+ }
+ }
+
+ function scheduler() {
+ if (!playing) return;
+ const now = audioCtx.currentTime;
+ while (nextNoteTime < now + scheduleAhead) {
+ scheduleNote(step, nextNoteTime);
+ nextNoteTime += stepDur;
+ step++;
+ }
+ schedTimer = setTimeout(scheduler, lookahead * 1000);
+ }
+
+ function start() {
+ if (!audioCtx) initAudio();
+ if (audioCtx.state === "suspended") audioCtx.resume();
+ playing = true;
+ step = 0;
+ nextNoteTime = audioCtx.currentTime + 0.08;
+ scheduler();
+ btn.textContent = "STOP";
+ btn.classList.add("playing");
+ }
+
+ function stop() {
+ playing = false;
+ if (schedTimer) { clearTimeout(schedTimer); schedTimer = null; }
+ btn.textContent = "PLAY";
+ btn.classList.remove("playing");
+ }
+
+ btn.addEventListener("click", () => {
+ if (playing) stop(); else start();
+ });
+
+ // ─── Visuals ─────────────────────────────────────────────
+ const freqData = new Uint8Array(128);
+ let time = 0;
+ let smoothBass = 0;
+ let smoothMid = 0;
+ let smoothHi = 0;
+ let pulse = 0;
+
+ function getEnergy() {
+ if (!analyser || !playing) {
+ smoothBass *= 0.96;
+ smoothMid *= 0.96;
+ smoothHi *= 0.96;
+ pulse *= 0.95;
+ return;
+ }
+ analyser.getByteFrequencyData(freqData);
+ const n = freqData.length;
+ let bass = 0, mid = 0, hi = 0;
+ const bEnd = Math.floor(n * 0.12);
+ const mEnd = Math.floor(n * 0.45);
+ for (let i = 0; i < bEnd; i++) bass += freqData[i];
+ for (let i = bEnd; i < mEnd; i++) mid += freqData[i];
+ for (let i = mEnd; i < n; i++) hi += freqData[i];
+ bass = (bass / (bEnd * 255)) || 0;
+ mid = (mid / ((mEnd - bEnd) * 255)) || 0;
+ hi = (hi / ((n - mEnd) * 255)) || 0;
+ smoothBass += (bass - smoothBass) * 0.35;
+ smoothMid += (mid - smoothMid) * 0.25;
+ smoothHi += (hi - smoothHi) * 0.2;
+ pulse += (bass * 1.4 - pulse) * 0.4;
+ }
+
+ function drawSky() {
+ const g = ctx.createLinearGradient(0, 0, 0, H * 0.62);
+ g.addColorStop(0, "#0b001a");
+ g.addColorStop(0.35, "#1a0533");
+ g.addColorStop(0.55, "#3b0a54");
+ g.addColorStop(0.75, "#6b1d5c");
+ g.addColorStop(0.9, "#c44b2b");
+ g.addColorStop(1, "#ff6b35");
+ ctx.fillStyle = g;
+ ctx.fillRect(0, 0, W, H);
+ }
+
+ function drawStars() {
+ const seed = 42;
+ function rand(i) {
+ const x = Math.sin(i * 127.1 + seed * 311.7) * 43758.5453;
+ return x - Math.floor(x);
+ }
+ const count = 80;
+ for (let i = 0; i < count; i++) {
+ const x = rand(i) * W;
+ const y = rand(i + 100) * H * 0.5;
+ const tw = 0.4 + 0.6 * Math.sin(time * 2 + i);
+ const a = (0.3 + rand(i + 200) * 0.7) * tw * (0.5 + smoothHi);
+ ctx.fillStyle = `rgba(255,255,255,${a})`;
+ const sz = 1 + rand(i + 300) * 1.5;
+ ctx.fillRect(x, y, sz, sz);
+ }
+ }
+
+ function drawSun() {
+ const cx = W * 0.5;
+ const horizon = H * 0.55;
+ const baseR = Math.min(W, H) * 0.16;
+ const r = baseR * (1 + pulse * 0.08);
+
+ // Glow
+ const glow = ctx.createRadialGradient(cx, horizon - r * 0.15, r * 0.2, cx, horizon - r * 0.15, r * 2.2);
+ glow.addColorStop(0, `rgba(255,100,50,${0.35 + pulse * 0.25})`);
+ glow.addColorStop(0.4, `rgba(255,50,100,${0.12 + pulse * 0.1})`);
+ glow.addColorStop(1, "rgba(80,0,80,0)");
+ ctx.fillStyle = glow;
+ ctx.beginPath();
+ ctx.arc(cx, horizon - r * 0.15, r * 2.2, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Sun body
+ const sg = ctx.createLinearGradient(cx, horizon - r * 1.3, cx, horizon + r * 0.4);
+ sg.addColorStop(0, "#fff5a0");
+ sg.addColorStop(0.25, "#ffcc33");
+ sg.addColorStop(0.5, "#ff6b35");
+ sg.addColorStop(0.75, "#ff2a6d");
+ sg.addColorStop(1, "#c41e6a");
+ ctx.save();
+ ctx.beginPath();
+ ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+ ctx.clip();
+ ctx.fillStyle = sg;
+ ctx.fillRect(cx - r - 2, horizon - r * 1.4, r * 2 + 4, r * 2.5);
+
+ // Horizontal scan lines on sun
+ const lines = 14;
+ for (let i = 0; i < lines; i++) {
+ const t = i / lines;
+ const yOff = (t - 0.15) * r * 2;
+ const y = horizon - r * 0.1 + yOff;
+ const thickness = 1.5 + t * 5 + pulse * 2;
+ const gap = 3 + t * 6;
+ if (t > 0.28) {
+ ctx.fillStyle = `rgba(10,0,25,${0.55 + t * 0.35})`;
+ ctx.fillRect(cx - r - 2, y, r * 2 + 4, thickness);
+ // skip gap already by spacing
+ i; // spacing controlled by loop density
+ }
+ }
+ // Better scan line approach
+ ctx.restore();
+
+ // Redraw sun with proper lines
+ ctx.save();
+ ctx.beginPath();
+ ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+ ctx.clip();
+ ctx.fillStyle = sg;
+ ctx.fillRect(cx - r - 2, horizon - r * 1.4, r * 2 + 4, r * 2.5);
+
+ let y = horizon - r * 0.1 - r * 0.15;
+ let spacing = 4;
+ while (y < horizon - r * 0.1 + r) {
+ const rel = (y - (horizon - r * 0.1)) / r; // -1 to 1
+ if (rel > -0.2) {
+ const th = 2 + Math.max(0, rel) * 7;
+ ctx.fillStyle = `rgba(8,0,20,${0.5 + Math.max(0, rel) * 0.4})`;
+ ctx.fillRect(cx - r, y, r * 2, th);
+ spacing = 5 + Math.max(0, rel) * 10;
+ }
+ y += spacing;
+ }
+ ctx.restore();
+
+ // Rim light
+ ctx.strokeStyle = `rgba(255,200,100,${0.4 + pulse * 0.3})`;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+ ctx.stroke();
+ }
+
+ function drawMountains() {
+ const horizon = H * 0.55;
+ // Back range
+ ctx.beginPath();
+ ctx.moveTo(0, horizon);
+ const peaks = [
+ [0.0, 0], [0.1, 0.08], [0.18, 0.03], [0.28, 0.14],
+ [0.38, 0.05], [0.48, 0.12], [0.55, 0.04], [0.65, 0.16],
+ [0.75, 0.06], [0.85, 0.11], [0.95, 0.03], [1.0, 0.07]
+ ];
+ peaks.forEach(([px, py]) => {
+ ctx.lineTo(W * px, horizon - H * py * (0.9 + pulse * 0.1));
+ });
+ ctx.lineTo(W, horizon);
+ ctx.closePath();
+ const mg = ctx.createLinearGradient(0, horizon - H * 0.18, 0, horizon);
+ mg.addColorStop(0, "#2a0845");
+ mg.addColorStop(1, "#120020");
+ ctx.fillStyle = mg;
+ ctx.fill();
+
+ // Front silhouette
+ ctx.beginPath();
+ ctx.moveTo(0, horizon);
+ const peaks2 = [
+ [0, 0], [0.08, 0.05], [0.15, 0.02], [0.25, 0.09],
+ [0.4, 0.03], [0.5, 0.07], [0.6, 0.02], [0.72, 0.1],
+ [0.82, 0.04], [0.92, 0.06], [1, 0.02]
+ ];
+ peaks2.forEach(([px, py]) => {
+ ctx.lineTo(W * px, horizon - H * py * 0.7);
+ });
+ ctx.lineTo(W, horizon);
+ ctx.closePath();
+ ctx.fillStyle = "#0d0018";
+ ctx.fill();
+ }
+
+ function drawGrid() {
+ const horizon = H * 0.55;
+ const vanishX = W * 0.5;
+ const bassBoost = 1 + pulse * 0.35;
+ const scroll = (time * 0.35) % 1;
+
+ // Ground fill
+ const gg = ctx.createLinearGradient(0, horizon, 0, H);
+ gg.addColorStop(0, "#120028");
+ gg.addColorStop(1, "#050010");
+ ctx.fillStyle = gg;
+ ctx.fillRect(0, horizon, W, H - horizon);
+
+ // Horizontal lines (perspective)
+ ctx.lineWidth = 1.5;
+ const rows = 24;
+ for (let i = 0; i <= rows; i++) {
+ const t = (i + scroll) / rows;
+ if (t <= 0 || t > 1) continue;
+ // Ease for perspective
+ const p = t * t;
+ const y = horizon + p * (H - horizon) * bassBoost * 0.98;
+ if (y > H) continue;
+ const alpha = 0.15 + t * 0.65 + pulse * 0.2;
+ const r = Math.floor(255 * (0.2 + smoothMid * 0.3));
+ const g = Math.floor(50 + 150 * smoothHi);
+ const b = Math.floor(200 + 55 * pulse);
+ ctx.strokeStyle = `rgba(${Math.min(255, r + 100)}, ${Math.min(255, g)}, ${b}, ${alpha})`;
+ ctx.beginPath();
+ ctx.moveTo(0, y);
+ ctx.lineTo(W, y);
+ ctx.stroke();
+ }
+
+ // Vertical lines converging to vanishing point
+ const cols = 18;
+ for (let i = -cols; i <= cols; i++) {
+ if (i === 0) continue;
+ const xBottom = vanishX + i * (W / cols) * 1.4;
+ const alpha = 0.25 + pulse * 0.2;
+ ctx.strokeStyle = `rgba(255, 40, 120, ${alpha})`;
+ ctx.lineWidth = 1.2;
+ ctx.beginPath();
+ ctx.moveTo(vanishX, horizon);
+ ctx.lineTo(xBottom, H + 10);
+ ctx.stroke();
+ }
+
+ // Center glow line
+ ctx.strokeStyle = `rgba(5, 217, 232, ${0.35 + pulse * 0.4})`;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(vanishX, horizon);
+ ctx.lineTo(vanishX, H);
+ ctx.stroke();
+
+ // Horizon neon line
+ const hg = ctx.createLinearGradient(0, horizon, W, horizon);
+ hg.addColorStop(0, "rgba(255,42,109,0)");
+ hg.addColorStop(0.5, `rgba(255,42,109,${0.85 + pulse * 0.15})`);
+ hg.addColorStop(1, "rgba(255,42,109,0)");
+ ctx.strokeStyle = hg;
+ ctx.lineWidth = 2 + pulse * 3;
+ ctx.beginPath();
+ ctx.moveTo(0, horizon);
+ ctx.lineTo(W, horizon);
+ ctx.stroke();
+
+ // Cyan accent on horizon
+ ctx.strokeStyle = `rgba(5,217,232,${0.4 + pulse * 0.3})`;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(W * 0.2, horizon);
+ ctx.lineTo(W * 0.8, horizon);
+ ctx.stroke();
+ }
+
+ function drawSpectrum() {
+ if (!analyser) return;
+ const horizon = H * 0.55;
+ const bars = 48;
+ const dataStep = Math.floor(freqData.length / bars);
+ const barW = W / bars;
+ for (let i = 0; i < bars; i++) {
+ let sum = 0;
+ for (let j = 0; j < dataStep; j++) sum += freqData[i * dataStep + j] || 0;
+ const v = (sum / (dataStep * 255)) || 0;
+ const h = v * H * 0.12 * (1 + pulse * 0.3);
+ if (h < 1) continue;
+ const x = i * barW;
+ const grad = ctx.createLinearGradient(0, horizon - h, 0, horizon);
+ grad.addColorStop(0, "rgba(5,217,232,0.7)");
+ grad.addColorStop(0.5, "rgba(255,42,109,0.5)");
+ grad.addColorStop(1, "rgba(255,42,109,0)");
+ ctx.fillStyle = grad;
+ ctx.fillRect(x + 1, horizon - h, barW - 2, h);
+ }
+ }
+
+ function drawVignette() {
+ const vg = ctx.createRadialGradient(W / 2, H / 2, H * 0.25, W / 2, H / 2, H * 0.85);
+ vg.addColorStop(0, "rgba(0,0,0,0)");
+ vg.addColorStop(1, "rgba(0,0,0,0.55)");
+ ctx.fillStyle = vg;
+ ctx.fillRect(0, 0, W, H);
+ }
+
+ function drawScanlines() {
+ ctx.fillStyle = "rgba(0,0,0,0.07)";
+ for (let y = 0; y < H; y += 3) {
+ ctx.fillRect(0, y, W, 1);
+ }
+ }
+
+ let last = performance.now();
+ function frame(now) {
+ const dt = Math.min(0.05, (now - last) / 1000);
+ last = now;
+ time += dt;
+
+ getEnergy();
+
+ drawSky();
+ drawStars();
+ drawSun();
+ drawMountains();
+ drawSpectrum();
+ drawGrid();
+ drawVignette();
+ drawScanlines();
+
+ requestAnimationFrame(frame);
+ }
+ requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/gemma3-12b.html b/data/artifacts/966d54f224fd/gemma3-12b.html
new file mode 100644
index 0000000..6419fbc
--- /dev/null
+++ b/data/artifacts/966d54f224fd/gemma3-12b.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tron Game</title>
+<style>
+body { margin: 0; overflow: hidden; background-color: black; }
+canvas { display: block; }
+</style>
+</head>
+<body>
+
+<canvas id="tronCanvas"></canvas>
+
+<script>
+const canvas = document.getElementById('tronCanvas');
+const ctx = canvas.getContext('2d');
+
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+let playerX = 50;
+let playerY = 50;
+let speedX = 2;
+let speedY = 0;
+let trail = [];
+let enemyX = canvas.width - 50;
+let enemyY = 50;
+let enemySpeedX = -2;
+let enemySpeedY = 0;
+let enemyTrail = [];
+let score = 0;
+
+function drawPlayer() {
+ ctx.fillStyle = 'red';
+ ctx.fillRect(playerX, playerY, 10, 10);
+}
+
+function drawEnemy() {
+ ctx.fillStyle = 'blue';
+ ctx.fillRect(enemyX, enemyY, 10, 10);
+}
+
+function updatePlayer() {
+ playerX += speedX;
+ playerY += speedY;
+
+ if (playerX < 0) playerX = canvas.width;
+ if (playerX > canvas.width) playerX = 0;
+ if (playerY < 0) playerY = canvas.height;
+ if (playerY > canvas.height) playerY = 0;
+}
+
+function updateEnemy() {
+ enemyX += enemySpeedX;
+ enemyY += enemySpeedY;
+
+ if(enemyX < 0) enemyX = canvas.width;
+ if(enemyX > canvas.width) enemyX = 0;
+ if (enemyY < 0) enemyY = canvas.height;
+ if(enemyY > canvas.height) enemyY = 0;
+}
+
+function drawTrail() {
+ ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
+ for (let i = 0; i < trail.length; i++) {
+ ctx.fillRect(trail[i].x, trail[i].y, 10, 10);
+ }
+}
+
+function drawEnemyTrail() {
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.3)';
+ for (let i = 0; i < enemyTrail.length; i++) {
+ ctx.fillRect(enemyTrail[i].x, enemyTrail[i].y, 10, 10);
+ }
+}
+
+function collision() {
+ // Player - Enemy Collision
+ if (playerX === enemyX && playerY === enemyY) {
+ alert("Game Over! Score: " + score);
+ reset();
+ }
+
+ //Player - Trail Collision
+ for(let i = 0; i < trail.length; i++){
+ if (playerX === trail[i].x && playerY === trail[i].y){
+ alert("Game Over! Score: " + score);
+ reset();
+ }
+ }
+
+ //Enemy - Trail Collision
+ for(let i = 0; i < enemyTrail.length; i++){
+ if (enemyX === enemyTrail[i].x && enemyY === enemyTrail[i].y){
+ alert("Game Over! Score: " + score);
+ reset();
+ }
+ }
+}
+
+function reset() {
+ playerX = 50;
+ playerY = 50;
+ speedX = 2;
+ speedY = 0;
+ trail = [];
+ enemyX = canvas.width - 50;
+ enemyY = 50;
+ enemySpeedX = -2;
+ enemySpeedY = 0;
+ enemyTrail = [];
+ score = 0;
+}
+
+function gameLoop() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ drawPlayer();
+ drawEnemy();
+ drawTrail();
+ drawEnemyTrail();
+
+ collision();
+
+ updatePlayer();
+ updateEnemy();
+
+ trail.push({ x: playerX, y: playerY });
+ if (trail.length > 20) {
+ trail.shift();
+ }
+
+ enemyTrail.push({x: enemyX, y: enemyY});
+ if(enemyTrail.length > 20){
+ enemyTrail.shift();
+ }
+
+ requestAnimationFrame(gameLoop);
+}
+
+
+document.addEventListener('keydown', function(event) {
+ switch (event.key) {
+ case 'ArrowUp':
+ speedY = -2;
+ break;
+ case 'ArrowDown':
+ speedY = 2;
+ break;
+ case 'ArrowLeft':
+ speedX = -2;
+ break;
+ case 'ArrowRight':
+ speedX = 2;
+ break;
+ }
+});
+
+document.addEventListener('keyup', function(event) {
+ if (event.key === 'ArrowUp') speedY = 0;
+ else if (event.key === 'ArrowDown') speedY = 0;
+ else if (event.key === 'ArrowLeft') speedX = 0;
+ else if (event.key === 'ArrowRight') speedX = 0;
+});
+
+//AI enemy movement - simple chase
+function updateEnemyAI(){
+ enemySpeedX = playerX - enemyX;
+ enemySpeedY = playerY - enemyY;
+
+ let distance = Math.sqrt(Math.pow(playerX - enemyX, 2) + Math.pow(playerY - enemyY, 2));
+ if (distance > 50){
+ enemySpeedX /= 2;
+ enemySpeedY /= 2;
+ }
+
+ updateEnemy();
+}
+
+gameLoop();
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/grok.html b/data/artifacts/966d54f224fd/grok.html
new file mode 100644
index 0000000..ff9c772
--- /dev/null
+++ b/data/artifacts/966d54f224fd/grok.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Tron Light Cycles</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{background:#000;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:'Segoe UI',system-ui,sans-serif;overflow:hidden;color:#0ff}
+#wrap{position:relative;display:flex;flex-direction:column;align-items:center;gap:12px}
+canvas{border:2px solid #0ff;box-shadow:0 0 20px #0ff4,inset 0 0 40px #0ff1;background:#050510;image-rendering:pixelated;cursor:none}
+#hud{display:flex;gap:32px;font-size:14px;letter-spacing:2px;text-transform:uppercase;text-shadow:0 0 8px #0ff}
+#hud span{color:#fff}
+#overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;background:rgba(0,0,0,.85);z-index:10;gap:16px}
+#overlay h1{font-size:48px;letter-spacing:8px;text-shadow:0 0 20px #0ff,0 0 40px #0ff;color:#0ff}
+#overlay p{font-size:16px;color:#8ff;letter-spacing:2px;max-width:400px;text-align:center;line-height:1.6}
+#overlay .btn{margin-top:12px;padding:12px 36px;border:2px solid #0ff;background:transparent;color:#0ff;font-size:18px;letter-spacing:4px;cursor:pointer;text-transform:uppercase;transition:all .2s;font-family:inherit}
+#overlay .btn:hover{background:#0ff;color:#000;box-shadow:0 0 30px #0ff}
+#scores{display:flex;gap:40px;font-size:13px;letter-spacing:1px}
+.p1{color:#0ff;text-shadow:0 0 6px #0ff}
+.p2{color:#f0f;text-shadow:0 0 6px #f0f}
+.hidden{display:none!important}
+</style>
+</head>
+<body>
+<div id="wrap">
+<div id="hud">
+<div class="p1">YOU <span id="s1">0</span></div>
+<div class="p2">CPU <span id="s2">0</span></div>
+</div>
+<canvas id="c" width="640" height="480"></canvas>
+<div id="overlay">
+<h1>TRON</h1>
+<p>Light Cycles — survive longer than the CPU.<br>Arrow keys or WASD to steer.<br>Don't hit walls or trails.</p>
+<button class="btn" id="startBtn">Start</button>
+</div>
+</div>
+<script>
+const canvas=document.getElementById('c');
+const ctx=canvas.getContext('2d');
+const overlay=document.getElementById('overlay');
+const startBtn=document.getElementById('startBtn');
+const s1El=document.getElementById('s1');
+const s2El=document.getElementById('s2');
+
+const W=640,H=480,CELL=8;
+const COLS=W/CELL,ROWS=H/CELL;
+const DIRS={UP:[0,-1],DOWN:[0,1],LEFT:[-1,0],RIGHT:[1,0]};
+const OPP={UP:'DOWN',DOWN:'UP',LEFT:'RIGHT',RIGHT:'LEFT'};
+
+let grid,players,running,frame,scores=[0,0],keys={},animId;
+
+function resetGrid(){
+ grid=new Array(COLS*ROWS).fill(0);
+}
+
+function idx(x,y){return y*COLS+x;}
+
+function makePlayer(x,y,dir,color,glow,isAI){
+ return{x,y,dir,nextdir:dir,color,glow,alive:true,trail:[],isAI,path:[]};
+}
+
+function initGame(){
+ resetGrid();
+ players=[
+ makePlayer(Math.floor(COLS*0.25),Math.floor(ROWS/2),'RIGHT','#00ffff','#00ffff88',false),
+ makePlayer(Math.floor(COLS*0.75),Math.floor(ROWS/2),'LEFT','#ff00ff','#ff00ff88',true)
+ ];
+ running=true;
+ frame=0;
+ keys={};
+}
+
+function drawGridBg(){
+ ctx.fillStyle='#050510';
+ ctx.fillRect(0,0,W,H);
+ ctx.strokeStyle='#0a0a20';
+ ctx.lineWidth=1;
+ for(let x=0;x<=W;x+=CELL){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
+ for(let y=0;y<=H;y+=CELL){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
+}
+
+function drawTrail(p){
+ if(p.trail.length<1)return;
+ ctx.shadowBlur=12;
+ ctx.shadowColor=p.color;
+ ctx.strokeStyle=p.color;
+ ctx.lineWidth=CELL-2;
+ ctx.lineCap='square';
+ ctx.lineJoin='miter';
+ ctx.beginPath();
+ const t=p.trail;
+ ctx.moveTo(t[0][0]*CELL+CELL/2,t[0][1]*CELL+CELL/2);
+ for(let i=1;i<t.length;i++){
+ ctx.lineTo(t[i][0]*CELL+CELL/2,t[i][1]*CELL+CELL/2);
+ }
+ ctx.lineTo(p.x*CELL+CELL/2,p.y*CELL+CELL/2);
+ ctx.stroke();
+ ctx.shadowBlur=0;
+ // head glow
+ ctx.fillStyle=p.color;
+ ctx.shadowBlur=18;
+ ctx.shadowColor=p.color;
+ ctx.fillRect(p.x*CELL+1,p.y*CELL+1,CELL-2,CELL-2);
+ ctx.shadowBlur=0;
+}
+
+function valid(x,y){
+ return x>=0&&x<COLS&&y>=0&&y<ROWS&&grid[idx(x,y)]===0;
+}
+
+function aiThink(p){
+ const dirs=['UP','DOWN','LEFT','RIGHT'];
+ const cur=p.dir;
+ const candidates=[];
+ for(const d of dirs){
+ if(d===OPP[cur])continue;
+ const [dx,dy]=DIRS[d];
+ const nx=p.x+dx,ny=p.y+dy;
+ if(valid(nx,ny)){
+ // score by open space ahead
+ let open=0,cx=nx,cy=ny;
+ for(let i=0;i<12;i++){
+ cx+=dx;cy+=dy;
+ if(!valid(cx,cy))break;
+ open++;
+ }
+ // look sideways a bit
+ const side=d==='UP'||d==='DOWN'?['LEFT','RIGHT']:['UP','DOWN'];
+ for(const s of side){
+ const [sx,sy]=DIRS[s];
+ if(valid(nx+sx,ny+sy))open+=0.5;
+ }
+ // prefer not turning if straight is good
+ if(d===cur)open+=1.5;
+ candidates.push({d,open});
+ }
+ }
+ if(candidates.length===0)return;
+ candidates.sort((a,b)=>b.open-a.open);
+ // slight randomness among top
+ const top=candidates.filter(c=>c.open>=candidates[0].open-1);
+ p.nextdir=top[Math.floor(Math.random()*top.length)].d;
+}
+
+function step(){
+ if(!running)return;
+ frame++;
+ // AI
+ for(const p of players){
+ if(p.alive&&p.isAI&&frame%2===0)aiThink(p);
+ }
+ // apply next dir
+ for(const p of players){
+ if(!p.alive)continue;
+ if(p.nextdir!==OPP[p.dir])p.dir=p.nextdir;
+ }
+ // move
+ for(const p of players){
+ if(!p.alive)continue;
+ p.trail.push([p.x,p.y]);
+ grid[idx(p.x,p.y)]=1;
+ const [dx,dy]=DIRS[p.dir];
+ p.x+=dx;p.y+=dy;
+ }
+ // collisions (after all moved — simultaneous)
+ const dead=[];
+ for(let i=0;i<players.length;i++){
+ const p=players[i];
+ if(!p.alive)continue;
+ if(p.x<0||p.x>=COLS||p.y<0||p.y>=ROWS){dead.push(i);continue;}
+ // hit trail or head-on
+ if(grid[idx(p.x,p.y)]!==0){dead.push(i);continue;}
+ for(let j=0;j<players.length;j++){
+ if(i===j)continue;
+ const o=players[j];
+ if(o.alive&&o.x===p.x&&o.y===p.y){dead.push(i);break;}
+ }
+ }
+ for(const i of dead)players[i].alive=false;
+ // mark new heads
+ for(const p of players){
+ if(p.alive)grid[idx(p.x,p.y)]=1;
+ }
+ // check end
+ const alive=players.filter(p=>p.alive);
+ if(alive.length<=1){
+ running=false;
+ if(alive.length===1){
+ if(!alive[0].isAI){scores[0]++;s1El.textContent=scores[0];showEnd('YOU WIN');}
+ else{scores[1]++;s2El.textContent=scores[1];showEnd('CPU WINS');}
+ }else{
+ showEnd('DRAW');
+ }
+ }
+}
+
+function showEnd(msg){
+ overlay.classList.remove('hidden');
+ overlay.querySelector('h1').textContent=msg;
+ overlay.querySelector('p').textContent='Press Start or Space for another round';
+ startBtn.textContent='Again';
+}
+
+function render(){
+ drawGridBg();
+ for(const p of players)drawTrail(p);
+}
+
+function loop(){
+ step();
+ render();
+ if(running)animId=requestAnimationFrame(()=>{
+ // speed control ~12 moves/sec
+ setTimeout(loop,1000/14);
+ });
+}
+
+function start(){
+ cancelAnimationFrame(animId);
+ overlay.classList.add('hidden');
+ initGame();
+ render();
+ loop();
+}
+
+startBtn.addEventListener('click',start);
+
+document.addEventListener('keydown',e=>{
+ const k=e.key.toLowerCase();
+ if(k===' '||k==='enter'){
+ if(!running)start();
+ e.preventDefault();
+ return;
+ }
+ if(!running||!players[0].alive)return;
+ const p=players[0];
+ let nd=null;
+ if(k==='arrowup'||k==='w')nd='UP';
+ else if(k==='arrowdown'||k==='s')nd='DOWN';
+ else if(k==='arrowleft'||k==='a')nd='LEFT';
+ else if(k==='arrowright'||k==='d')nd='RIGHT';
+ if(nd&&nd!==OPP[p.dir]){p.nextdir=nd;e.preventDefault();}
+});
+
+// initial splash render
+resetGrid();
+players=[];
+drawGridBg();
+ctx.fillStyle='#0ff';
+ctx.font='bold 20px monospace';
+ctx.textAlign='center';
+ctx.shadowBlur=10;ctx.shadowColor='#0ff';
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/hermes3-8b.html b/data/artifacts/966d54f224fd/hermes3-8b.html
new file mode 100644
index 0000000..8c7aeb4
--- /dev/null
+++ b/data/artifacts/966d54f224fd/hermes3-8b.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Tron Game</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ font-family: Arial, sans-serif;
+ }
+
+ canvas {
+ display: block;
+ background-color: #000;
+ }
+
+ #score {
+ color: white;
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ z-index: 1;
+ }
+
+ .circle {
+ position: absolute;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background-color: #fff;
+ z-index: 2;
+ }
+ </style>
+</head>
+<body>
+ <div id="score">Player 1: 0 - Player 2: 0</div>
+ <canvas id="gameCanvas" width="800" height="400"></canvas>
+
+ <script>
+ const canvas = document.getElementById('gameCanvas');
+ const ctx = canvas.getContext('2d');
+
+ let player1Position = { x: 5, y: 200 };
+ let player2Position = { x: 795, y: 200 };
+ let playersPositionsHistory = [{}, {}];
+ let score = [0, 0];
+
+ function drawPlayers() {
+ ctx.fillStyle = 'yellow';
+ ctx.beginPath();
+ ctx.arc(player1Position.x, player1Position.y, 2.5, 0, 2 * Math.PI);
+ ctx.fill();
+
+ ctx.fillStyle = 'blue';
+ ctx.beginPath();
+ ctx.arc(player2Position.x, player2Position.y, 2.5, 0, 2 * Math.PI);
+ ctx.fill();
+ }
+
+ function drawLines() {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'white';
+
+ for (let i = -10; i <= canvas.height + 10; i++) {
+ ctx.beginPath();
+ ctx.moveTo(0, i);
+ ctx.lineTo(canvas.width, i);
+ ctx.stroke();
+ }
+
+ for (let i = -10; i <= canvas.width + 10; i += 20) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, canvas.height);
+ ctx.stroke();
+ }
+ }
+
+ function updatePlayerPosition(playerIndex, dx, dy) {
+ playersPositionsHistory[playerIndex][`x${dx > 0 ? '++' : '--'}y${dy > 0 ? '++' : '--'}`] = true;
+ playerPositionsHistory[playerIndex].x += dx;
+ playerPositionsHistory[playerIndex].y += dy;
+
+ if (playersPositionsHistory[playerIndex].x < 0 || playersPositionsHistory[playerIndex].x > canvas.width ||
+ playersPositionsHistory[playerIndex].y < 0 || playersPositionsHistory[playerIndex].y > canvas.height) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkCollision(playerIndex, dx, dy) {
+ let collided = false;
+
+ for (let i = 0; i < playerIndex !== 0 ? 1 : 2; i++) {
+ if (i === playerIndex) continue;
+
+ const otherPlayerHistory = playersPositionsHistory[i];
+ const currentOtherPlayerPosition = { x: otherPlayerHistory.x, y: otherPlayerHistory.y };
+
+ for (const [key, value] of Object.entries(otherPlayerHistory)) {
+ if (!value) continue;
+
+ switch (key) {
+ case 'x++y++':
+ if (updatePlayerPosition(playerIndex, dx > 0 ? 1 : -1, dy > 0 ? 1 : -1) &&
+ updatePlayerPosition(i, value.x - currentOtherPlayerPosition.x, value.y - currentOtherPlayerPosition.y)) {
+ collided = true;
+ break;
+ }
+ break;
+ case 'x--y++':
+ if (updatePlayerPosition(playerIndex, dx < 0 ? 1 : -1, dy > 0 ? 1 : -1) &&
+ updatePlayerPosition(i, value.x + currentOtherPlayerPosition.x, value.y - currentOtherPlayerPosition.y)) {
+ collided = true;
+ break;
+ }
+ break;
+ case 'x++y--':
+ if (updatePlayerPosition(playerIndex, dx > 0 ? 1 : -1, dy < 0 ? 1 : -1) &&
+ updatePlayerPosition(i, value.x - currentOtherPlayerPosition.x, value.y + currentOtherPlayerPosition.y)) {
+ collided = true;
+ break;
+ }
+ break;
+ case 'x--y--':
+ if (updatePlayerPosition(playerIndex, dx < 0 ? 1 : -1, dy < 0 ? 1 : -1) &&
+ updatePlayerPosition(i, value.x + currentOtherPlayerPosition.x, value.y + currentOtherPlayerPosition.y)) {
+ collided = true;
+ break;
+ }
+ break;
+ }
+ }
+
+ if (collided) return true;
+ }
+
+ return false;
+ }
+
+ function movePlayers(event) {
+ const dx = event.keyCode === 37 || event.keyCode === 65 ? -1 : event.keyCode === 39 || event.keyCode === 68 ? 1 : 0;
+ const dy = event.keyCode === 87 || event.keyCode === 83 ? 1 : event.keyCode === 81 || event.keyCode === 69 ? -1 : 0;
+
+ if (dx !== 0 && !checkCollision(0, dx, dy)) {
+ player1Position.x += 10 * dx;
+ player1Position.y += 10 * dy;
+ }
+
+ if (dy !== 0 && !checkCollision(1, dx, dy)) {
+ player2Position.x += 10 * dx;
+ player2Position.y += 10 * dy;
+ }
+ }
+
+ document.addEventListener('keydown', movePlayers);
+
+ function gameLoop() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ drawPlayers();
+ drawLines();
+
+ for (let i = 0; i < playersPositionsHistory.length; i++) {
+ const playerHistory = playersPositionsHistory[i];
+ let playerIndex = i === 0 ? 1 : 0;
+
+ for (const [key, value] of Object.entries(playerHistory)) {
+ if (!value) continue;
+
+ switch (key) {
+ case 'x++y++':
+ player1Position.x = parseInt(key.split('x++y')[0]) + 10 * (playerIndex === 0 ? 1 : -1);
+ player1Position.y = parseInt(key.split('y++')[1]) + 10 * (playerIndex === 0 ? 1 : -1);
+ break;
+ case 'x--y++':
+ player2Position.x = parseInt(key.split('x--y')[0]) + 10 * (playerIndex === 0 ? -1 : 1);
+ player2Position.y = parseInt(key.split('y++')[1]) + 10 * (playerIndex === 0 ? 1 : -1);
+ break;
+ case 'x++y--':
+ player1Position.x = parseInt(key.split('x++y')[0]) + 10 * (playerIndex === 0 ? 1 : -1);
+ player1Position.y = parseInt(key.split('y--')[1]) + 10 * (playerIndex === 0 ? -1 : 1);
+ break;
+ case 'x--y--':
+ player2Position.x = parseInt(key.split('x--y')[0]) + 10 * (playerIndex === 0 ? -1 : 1);
+ player2Position.y = parseInt(key.split('y--')[1]) + 10 * (playerIndex === 0 ? -1 : 1);
+ }
+ }
+ }
+
+ setTimeout(gameLoop, 100);
+ }
+
+ gameLoop();
+
+ setInterval(() => {
+ if (
+ canvas.width <= Math.min(player1Position.x, player2Position.x) ||
+ player1Position.x >= canvas.width + 10 ||
+ player1Position.y >= canvas.height + 10 ||
+ player2Position.x <= -10 ||
+ player2Position.y >= canvas.height + 10
+ ) {
+ score = [score[1], score[0]];
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ return;
+ }
+
+ const player1LastPositionsHistory = playersPositionsHistory[0];
+ const player2LastPositionsHistory = playersPositionsHistory[1];
+
+ for (const [key, value] of Object.entries(player1LastPositionsHistory)) {
+ if (!value) continue;
+
+ switch (key) {
+ case 'x++y++':
+ if (player1Position.x <= canvas.width && player1Position.y <= canvas.height &&
+ !player2PositionsHistory[1].hasOwnProperty(key)) {
+ score[0]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x--y++':
+ if (player1Position.x >= -10 && player1Position.y <= canvas.height &&
+ !player2PositionsHistory[1].hasOwnProperty(key)) {
+ score[0]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x++y--':
+ if (player1Position.x <= canvas.width && player1Position.y >= -10 &&
+ !player2PositionsHistory[1].hasOwnProperty(key)) {
+ score[0]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x--y--':
+ if (player1Position.x >= -10 && player1Position.y >= -10 &&
+ !player2PositionsHistory[1].hasOwnProperty(key)) {
+ score[0]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ }
+ }
+
+ for (const [key, value] of Object.entries(player2LastPositionsHistory)) {
+ if (!value) continue;
+
+ switch (key) {
+ case 'x++y++':
+ if (player2Position.x >= -10 && player2Position.y <= canvas.height &&
+ !player1PositionsHistory[0].hasOwnProperty(key)) {
+ score[1]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x--y++':
+ if (player2Position.x <= canvas.width && player2Position.y <= canvas.height &&
+ !player1PositionsHistory[0].hasOwnProperty(key)) {
+ score[1]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x++y--':
+ if (player2Position.x >= -10 && player2Position.y >= -10 &&
+ !player1PositionsHistory[0].hasOwnProperty(key)) {
+ score[1]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ break;
+ case 'x--y--':
+ if (player2Position.x <= canvas.width && player2Position.y >= -10 &&
+ !player1PositionsHistory[0].hasOwnProperty(key)) {
+ score[1]++;
+ document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+ }
+ }
+ }
+ }, 500);
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/kimi.html b/data/artifacts/966d54f224fd/kimi.html
new file mode 100644
index 0000000..a5fb067
--- /dev/null
+++ b/data/artifacts/966d54f224fd/kimi.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Tron Light Cycles</title>
+<style>
+body { margin: 0; overflow: hidden; background: #050505; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Courier New', monospace; }
+canvas { box-shadow: 0 0 30px #00f3ff, inset 0 0 30px rgba(0,243,255,0.1); border: 2px solid #00f3ff; background: #000; }
+#ui { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: #00f3ff; text-align: center; text-shadow: 0 0 10px #00f3ff; pointer-events: none; font-size: 14px; letter-spacing: 2px; }
+#ui span { display: block; margin-top: 5px; font-size: 12px; opacity: 0.8; }
+</style>
+</head>
+<body>
+<div id="ui">TRON LIGHT CYCLES<span>ARROWS to STEER | R to RESET</span></div>
+<canvas id="c"></canvas>
+<script>
+const canvas = document.getElementById('c');
+const ctx = canvas.getContext('2d');
+const W = 100, H = 60, CELL = 10;
+canvas.width = W * CELL;
+canvas.height = H * CELL;
+
+let grid, player, ai, gameOver, frame;
+
+class Bike {
+ constructor(x, y, dir, color, isAI) {
+ this.x = x; this.y = y; this.dir = dir; this.color = color; this.isAI = isAI; this.alive = true;
+ }
+
+ aiMove() {
+ const opp = {0:2, 2:0, 1:3, 3:1};
+ const dx = [0, 1, 0, -1], dy = [-1, 0, 1, 0];
+ let best = this.dir, bestScore = -1;
+
+ for (let d = 0; d < 4; d++) {
+ if (d === opp[this.dir]) continue;
+ let nx = this.x + dx[d], ny = this.y + dy[d];
+ if (nx < 0 || nx >= W || ny < 0 || ny >= H || grid[nx][ny]) continue;
+
+ let score = 0, cx = nx, cy = ny;
+ for (let i = 0; i < 6; i++) {
+ if (cx < 0 || cx >= W || cy < 0 || cy >= H || grid[cx][cy]) break;
+ score++; cx += dx[d]; cy += dy[d];
+ }
+ if (score > bestScore) { bestScore = score; best = d; }
+ }
+ this.dir = best;
+ }
+}
+
+function reset() {
+ grid = Array(W).fill(0).map(() => Array(H).fill(false));
+ player = new Bike(20, 30, 1, '#00f3ff', false);
+ ai = new Bike(80, 30, 3, '#ff0055', true);
+ gameOver = false; frame = 0;
+}
+
+function update() {
+ if (gameOver) return;
+
+ if (frame % 3 === 0) {
+ const dx = [0, 1, 0, -1], dy = [-1, 0, 1, 0];
+
+ if (ai.alive) ai.aiMove();
+
+ let pNext = {x: player.x + dx[player.dir], y: player.y + dy[player.dir]};
+ let aNext = {x: ai.x + dx[ai.dir], y: ai.y + dy[ai.dir]};
+
+ let pDead = pNext.x < 0 || pNext.x >= W || pNext.y < 0 || pNext.y >= H || grid[pNext.x][pNext.y];
+ let aDead = aNext.x < 0 || aNext.x >= W || aNext.y < 0 || aNext.y >= H || grid[aNext.x][aNext.y];
+
+ if (pNext.x === aNext.x && pNext.y === aNext.y) pDead = aDead = true;
+ if (pNext.x === ai.x && pNext.y === ai.y) pDead = true;
+ if (aNext.x === player.x && aNext.y === player.y) aDead = true;
+
+ if (!pDead) {
+ grid[player.x][player.y] = true;
+ player.x = pNext.x; player.y = pNext.y;
+ } else player.alive = false;
+
+ if (!aDead) {
+ grid[ai.x][ai.y] = true;
+ ai.x = aNext.x; ai.y = aNext.y;
+ } else ai.alive = false;
+
+ if (!player.alive || !ai.alive) gameOver = true;
+ }
+ frame++;
+}
+
+function draw() {
+ ctx.fillStyle = 'rgba(0,0,0,0.3)';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ ctx.fillStyle = '#111';
+ for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) if (grid[x][y]) ctx.fillRect(x*CELL, y*CELL, CELL-1, CELL-1);
+
+ ctx.shadowBlur = 15;
+ if (player.alive) {
+ ctx.fillStyle = player.color; ctx.shadowColor = player.color;
+ ctx.fillRect(player.x*CELL, player.y*CELL, CELL-1, CELL-1);
+ }
+ if (ai.alive) {
+ ctx.fillStyle = ai.color; ctx.shadowColor = ai.color;
+ ctx.fillRect(ai.x*CELL, ai.y*CELL, CELL-1, CELL-1);
+ }
+ ctx.shadowBlur = 0;
+
+ if (gameOver) {
+ ctx.fillStyle = 'white';
+ ctx.font = 'bold 40px monospace';
+ ctx.textAlign = 'center';
+ let msg = !player.alive && !ai.alive ? 'DRAW' : (player.alive ? 'YOU WIN' : 'AI WINS');
+ ctx.fillText(msg, canvas.width/2, canvas.height/2);
+ ctx.font = '20px monospace';
+ ctx.fillStyle = '#888';
+ ctx.fillText('Press R', canvas.width/2, canvas.height/2 + 40);
+ }
+}
+
+function loop() { update(); draw(); requestAnimationFrame(loop); }
+
+document.addEventListener('keydown', e => {
+ if (gameOver && (e.key === 'r' || e.key === 'R')) { reset(); return; }
+ const opp = {0:2, 2:0, 1:3, 3:1};
+ const map = {'ArrowUp':0, 'ArrowRight':1, 'ArrowDown':2, 'ArrowLeft':3};
+ if (map.hasOwnProperty(e.key) && player.dir !== opp[map[e.key]]) player.dir = map[e.key];
+ e.preventDefault();
+});
+
+reset();
+loop();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/challenges.json b/data/challenges.json
index 48b7803..c1f04ff 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -8,34 +8,212 @@
"runs": [
{
"model": "hermes3-8b",
- "status": "error",
- "error": "server restarted mid-run",
- "seconds": 12,
+ "status": "done",
+ "error": null,
+ "seconds": 18,
"cost": 0,
- "started_at": "2026-07-23T05:25:58.638Z",
- "bytes": 3086,
- "finished_at": "2026-07-23T05:25:51.776Z"
+ "started_at": "2026-07-23T05:38:07.592Z",
+ "bytes": 2904,
+ "finished_at": "2026-07-23T05:38:25.720Z"
},
{
"model": "qwen25-7b",
"status": "error",
- "error": "server restarted mid-run",
+ "error": "timeout after 600s",
"seconds": 600,
"cost": null,
- "started_at": "2026-07-23T05:25:10.008Z",
- "finished_at": "2026-07-23T05:12:46.664Z"
+ "started_at": "2026-07-23T05:26:27.484Z",
+ "finished_at": "2026-07-23T05:36:27.494Z"
},
{
"model": "gemma3-12b",
"status": "done",
"error": null,
- "seconds": 25,
+ "seconds": 28,
+ "cost": 0,
+ "started_at": "2026-07-23T05:36:16.579Z",
+ "bytes": 3117,
+ "finished_at": "2026-07-23T05:36:44.762Z"
+ },
+ {
+ "model": "qwen3-14b",
+ "status": "done",
+ "error": null,
+ "seconds": 138,
"cost": 0,
- "started_at": "2026-07-23T05:25:14.115Z",
- "bytes": 2760,
- "finished_at": "2026-07-23T05:25:39.475Z"
+ "started_at": "2026-07-23T05:32:56.078Z",
+ "bytes": 1295,
+ "finished_at": "2026-07-23T05:35:14.449Z"
+ },
+ {
+ "model": "kimi",
+ "status": "done",
+ "error": null,
+ "seconds": 65,
+ "cost": 0.0067,
+ "started_at": "2026-07-23T05:35:30.432Z",
+ "bytes": 1682,
+ "finished_at": "2026-07-23T05:36:35.854Z"
}
],
"voted_at": "2026-07-23T05:15:22.408Z"
+ },
+ {
+ "id": "966d54f224fd",
+ "title": "neon city",
+ "prompt": "playable html game like tron",
+ "created_at": "2026-07-23T05:29:23.102Z",
+ "winner": null,
+ "runs": [
+ {
+ "model": "qwen3-14b",
+ "status": "error",
+ "error": "no HTML document in model output (0 chars)",
+ "seconds": 142,
+ "cost": null,
+ "started_at": "2026-07-23T05:29:23.133Z",
+ "finished_at": "2026-07-23T05:31:44.783Z"
+ },
+ {
+ "model": "gemma3-12b",
+ "status": "done",
+ "error": null,
+ "seconds": 35,
+ "cost": 0,
+ "started_at": "2026-07-23T05:31:44.784Z",
+ "bytes": 3946,
+ "finished_at": "2026-07-23T05:32:19.763Z"
+ },
+ {
+ "model": "hermes3-8b",
+ "status": "done",
+ "error": null,
+ "seconds": 36,
+ "cost": 0,
+ "started_at": "2026-07-23T05:32:19.764Z",
+ "bytes": 12191,
+ "finished_at": "2026-07-23T05:32:56.077Z"
+ },
+ {
+ "model": "qwen25-7b",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:36:27.495Z"
+ },
+ {
+ "model": "claude",
+ "status": "error",
+ "error": "anthropic error: {\"type\":\"invalid_request_error\",\"message\":\"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.\"}",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:29:23.105Z",
+ "finished_at": "2026-07-23T05:29:23.403Z"
+ },
+ {
+ "model": "kimi",
+ "status": "done",
+ "error": null,
+ "seconds": 185,
+ "cost": 0.0162,
+ "started_at": "2026-07-23T05:29:23.128Z",
+ "bytes": 4559,
+ "finished_at": "2026-07-23T05:32:28.001Z"
+ },
+ {
+ "model": "gpt",
+ "status": "error",
+ "error": "openai error: {\"message\":\"The model `gpt-5.3` does not exist or you do not have access to it.\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"model_not_found\"}",
+ "seconds": 1,
+ "cost": null,
+ "started_at": "2026-07-23T05:29:23.130Z",
+ "finished_at": "2026-07-23T05:29:24.073Z"
+ },
+ {
+ "model": "grok",
+ "status": "done",
+ "error": null,
+ "seconds": 36,
+ "cost": 0.0443,
+ "started_at": "2026-07-23T05:29:23.131Z",
+ "bytes": 7691,
+ "finished_at": "2026-07-23T05:29:58.821Z"
+ }
+ ]
+ },
+ {
+ "id": "74df3b61e7ca",
+ "title": "Synthwave Visualizer",
+ "prompt": "Build a synthwave music visualizer using WebAudio oscillators (no external audio files): a retro sun over an animated perspective grid that pulses to generated music, with play/stop controls.",
+ "created_at": "2026-07-23T05:36:38.158Z",
+ "winner": null,
+ "runs": [
+ {
+ "model": "qwen3-14b",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:36:44.763Z"
+ },
+ {
+ "model": "gemma3-12b",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null
+ },
+ {
+ "model": "hermes3-8b",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null
+ },
+ {
+ "model": "qwen25-7b",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null
+ },
+ {
+ "model": "claude",
+ "status": "error",
+ "error": "anthropic error: {\"type\":\"invalid_request_error\",\"message\":\"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.\"}",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:36:38.160Z",
+ "finished_at": "2026-07-23T05:36:38.403Z"
+ },
+ {
+ "model": "kimi",
+ "status": "error",
+ "error": "server restarted mid-run",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:36:38.162Z"
+ },
+ {
+ "model": "gpt",
+ "status": "error",
+ "error": "openai error: {\"message\":\"The model `gpt-5.3` does not exist or you do not have access to it.\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"model_not_found\"}",
+ "seconds": 0,
+ "cost": null,
+ "started_at": "2026-07-23T05:36:38.162Z",
+ "finished_at": "2026-07-23T05:36:38.452Z"
+ },
+ {
+ "model": "grok",
+ "status": "done",
+ "error": null,
+ "seconds": 58,
+ "cost": 0.0997,
+ "started_at": "2026-07-23T05:36:38.163Z",
+ "bytes": 17543,
+ "finished_at": "2026-07-23T05:37:35.718Z"
+ }
+ ]
}
]
\ No newline at end of file
diff --git a/data/costlog.jsonl b/data/costlog.jsonl
new file mode 100644
index 0000000..819ee84
--- /dev/null
+++ b/data/costlog.jsonl
@@ -0,0 +1,4 @@
+{"ts":"2026-07-23T05:29:58.819Z","provider":"xai","model":"grok-4.5","task":"model-arena","input_tokens":292,"output_tokens":2893,"cost_usd":0.044271}
+{"ts":"2026-07-23T05:32:27.999Z","provider":"moonshot","model":"kimi-k2.5","task":"model-arena","input_tokens":90,"output_tokens":6449,"cost_usd":0.016176}
+{"ts":"2026-07-23T05:36:35.853Z","provider":"moonshot","model":"kimi-k2.5","task":"model-arena","input_tokens":127,"output_tokens":2649,"cost_usd":0.006699}
+{"ts":"2026-07-23T05:37:35.716Z","provider":"xai","model":"grok-4.5","task":"model-arena","input_tokens":322,"output_tokens":6581,"cost_usd":0.099681}
diff --git a/public/index.html b/public/index.html
index c656550..b49b794 100644
--- a/public/index.html
+++ b/public/index.html
@@ -68,7 +68,12 @@ input[type=range]{accent-color:var(--neon);width:130px}
#detail .meta{color:var(--dim);font-size:12px;margin-bottom:14px;white-space:pre-wrap}
.arena{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:16px}
.pane{background:var(--panel);border:1px solid var(--line)}
-.pane.winner{border-color:var(--gold);box-shadow:0 0 18px rgba(255,199,46,.25)}
+/* the crowned pane breaks out of the equal grid and takes the top, full width —
+ the fight has a champion, not a slightly-yellower column */
+.pane.winner{grid-column:1/-1;order:-1;border-color:var(--gold);box-shadow:0 0 26px rgba(255,199,46,.35);animation:crowned .9s ease-out 1}
+@keyframes crowned{0%{box-shadow:0 0 60px rgba(255,199,46,.9)}100%{box-shadow:0 0 26px rgba(255,199,46,.35)}}
+.pane.winner iframe{height:560px}
+.pane.winner .bar{background:linear-gradient(90deg,rgba(255,199,46,.14),transparent)}
.pane .bar{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid var(--line);font-size:12px}
.pane .bar .name{color:#fff;font-weight:700}
.pane .bar .stat{color:var(--dim);font-size:11px}
@@ -224,7 +229,9 @@ async function loadModels(){
function picked(){ return [...document.querySelectorAll('#model-picks input:checked')].map(i=>i.value); }
function updateEst(){
const est = picked().reduce((s,id)=>{ const m=models.find(x=>x.id===id); return s+(m?m.estCost:0); },0);
- $('#est').textContent = 'Estimated battle cost: ' + (est ? '~$'+est.toFixed(2)+' (metered models selected)' : '$0 (all local)');
+ $('#est').textContent = est
+ ? 'Rough ceiling: ~$' + est.toFixed(2) + ' (flat per-model guess; actual is metered by real tokens and logged after each run)'
+ : 'Cost: $0 (all local)';
}
$('#f-go').onclick = async ()=>{
const title=$('#f-title').value.trim(), prompt=$('#f-prompt').value.trim(), ids=picked();
@@ -277,7 +284,7 @@ async function renderDetail(id, statusOnly){
<span class="name">${mLabel(run.model)}</span>
<span class="stat">${run.status==='done'?stats.join(' · '):run.status}</span>
<span class="right">
- ${run.status==='done'&&c.winner!==run.model?`<button class="btn" data-win="${run.model}">👑 Crown</button>`:''}
+ ${run.status==='done'&&c.winner!==run.model?`<button class="btn" data-win="${run.model}" title="Crown this model the winner of this battle — records a vote into the real-world win-rate ledger. One winner per battle; you can re-crown.">👑 Crown</button>`:''}
${c.winner===run.model?'<span class="chip winner">WINNER</span>':''}
${(run.status==='error'||run.status==='done')?`<button class="btn pink" data-retry="${run.model}">↻</button>`:''}
</span>
diff --git a/server.js b/server.js
index 8ceb105..1b6e3e0 100644
--- a/server.js
+++ b/server.js
@@ -35,7 +35,7 @@ const MODELS = [
{ id: 'qwen25-7b', label: 'Qwen2.5 7B (Mac1)', kind: 'local', host: 'http://192.168.1.133:11434', model: 'qwen2.5:7b', estCost: 0 },
{ id: 'claude', label: 'Claude Fable 5', kind: 'metered', provider: 'anthropic', model: 'claude-fable-5', envKey: 'ANTHROPIC_API_KEY', estCost: 0.15 },
{ id: 'kimi', label: 'Kimi K2.5', kind: 'metered', provider: 'moonshot', model: 'kimi-k2.5', envKey: 'MOONSHOT_API_KEY', estCost: 0.03 },
- { id: 'gpt', label: 'GPT-5.3', kind: 'metered', provider: 'openai', model: 'gpt-5.3', envKey: 'OPENAI_API_KEY', estCost: 0.10 },
+ { id: 'gpt', label: 'GPT-5.1', kind: 'metered', provider: 'openai', model: process.env.OPENAI_MODEL || 'gpt-5.1', envKey: 'OPENAI_API_KEY', estCost: 0.10 },
{ id: 'grok', label: 'Grok 4.5', kind: 'metered', provider: 'xai', model: process.env.GROK_MODEL || 'grok-4.5', envKey: 'XAI_API_KEY', estCost: 0.10 },
];
@@ -47,8 +47,15 @@ function saveChallenges(list) {
fs.renameSync(tmp, CH_FILE);
}
let challenges = loadChallenges();
-// anything left "running" from a previous process died with it
-for (const c of challenges) for (const r of c.runs) if (r.status === 'running' || r.status === 'queued') { r.status = 'error'; r.error = 'server restarted mid-run'; }
+// anything left "running"/"queued" from a previous process died with it; also
+// heal corrupt records (finished before started) that a mid-write restart left behind
+for (const c of challenges) for (const r of c.runs) {
+ const corrupt = r.started_at && r.finished_at && r.finished_at < r.started_at;
+ if (r.status === 'running' || r.status === 'queued' || (r.status !== 'done' && corrupt)) {
+ r.status = 'error'; r.error = 'server restarted mid-run';
+ if (r.seconds == null) r.seconds = 0;
+ }
+}
saveChallenges(challenges);
function appendJsonl(file, obj) { try { fs.appendFileSync(file, JSON.stringify(obj) + '\n'); } catch {} }
@@ -92,12 +99,13 @@ function httpJson(url, opts, body, timeoutMs) {
});
}
-// serialize local generations per host so ollama doesn't thrash model loads
-const hostQueues = {};
-function onHost(host, fn) {
- const prev = hostQueues[host] || Promise.resolve();
+// serialize local generations per host (ollama model-load thrash) AND metered
+// generations per model (so two rapid retries queue instead of double-spending)
+const queues = {};
+function onKey(key, fn) {
+ const prev = queues[key] || Promise.resolve();
const next = prev.then(fn, fn);
- hostQueues[host] = next.catch(() => {});
+ queues[key] = next.catch(() => {});
return next;
}
@@ -152,7 +160,10 @@ function runModel(challenge, modelId) {
const m = MODELS.find(x => x.id === modelId);
const run = challenge.runs.find(r => r.model === modelId);
if (!m || !run) return;
- run.status = 'queued'; run.error = null; saveChallenges(challenges);
+ // double-fire guard: never start a model that's already in flight (blocks the
+ // metered double-spend race from two rapid retries on the same run)
+ if (run.status === 'running' || run.status === 'queued') return;
+ run.status = 'queued'; run.error = null; run.started_at = null; run.finished_at = null; saveChallenges(challenges);
const prompt = ARENA_PROMPT(challenge.prompt);
const exec = async () => {
run.status = 'running'; run.started_at = new Date().toISOString(); saveChallenges(challenges);
@@ -173,9 +184,30 @@ function runModel(challenge, modelId) {
run.finished_at = new Date().toISOString();
saveChallenges(challenges);
};
- if (m.kind === 'local') onHost(m.host, exec); else exec();
+ if (m.kind === 'local') onKey(m.host, exec); else onKey('metered:' + m.id, exec);
}
+// stuck-run watchdog — a wedged Ollama host (e.g. Mac1) can leave a run
+// 'running'/'queued' far past its budget; the UI would poll it forever. Every
+// 30s, fail any run stuck well past its expected ceiling so it surfaces as an
+// honest error instead of an infinite spinner.
+const STUCK_RUNNING_MS = 660000; // local timeout is 600s; grace 60s
+const STUCK_QUEUED_MS = 1200000; // 20 min waiting to even start = host is dead
+setInterval(() => {
+ const now = Date.now();
+ let dirty = false;
+ for (const c of challenges) for (const r of c.runs) {
+ if (r.status === 'running' && r.started_at && now - Date.parse(r.started_at) > STUCK_RUNNING_MS) {
+ r.status = 'error'; r.error = 'timed out (watchdog) — model host unresponsive'; r.finished_at = new Date().toISOString(); dirty = true;
+ } else if (r.status === 'queued' && r.started_at == null) {
+ // stamp a queue-entry time so we can age it out; reuse challenge age as the floor
+ const floor = Date.parse(c.created_at || new Date().toISOString());
+ if (now - floor > STUCK_QUEUED_MS) { r.status = 'error'; r.error = 'never started — model host busy/down'; r.seconds = 0; r.finished_at = new Date().toISOString(); dirty = true; }
+ }
+ }
+ if (dirty) saveChallenges(challenges);
+}, 30000).unref();
+
// ---------- ledger ----------
function buildLedger() {
const stats = {};
@@ -269,7 +301,11 @@ const server = http.createServer(async (req, res) => {
if ((m = p.match(/^\/api\/challenges\/([a-f0-9]+)\/retry\/([\w-]+)$/)) && req.method === 'POST') {
const c = challenges.find(x => x.id === m[1]);
if (!c) return send(res, 404, { error: 'not found' });
- if (!c.runs.some(r => r.model === m[2])) c.runs.push({ model: m[2], status: 'queued', error: null, seconds: null, cost: null });
+ // retry ONLY re-runs a model already in this battle. Adding an arbitrary
+ // (metered) model via retry was a spend-injection vector — anyone with the
+ // shared cred could loop paid models onto every challenge. New competitors
+ // are added at battle-creation time, not here.
+ if (!c.runs.some(r => r.model === m[2])) return send(res, 400, { error: 'model not in this battle — retry only re-runs an existing competitor' });
runModel(c, m[2]);
return send(res, 200, c);
}
← c838657 Kimi K2.5 is now a pre-checked default arena participant (+
·
back to Model Arena
·
auto-save: 2026-07-22T22:47:10 (5 files) — data/challenges.j 45826ed →