oneshotlm
Compare all models on this prompt →

2048 · grok-4.6

Build a playable 2048 game with arrow-key tile sliding and merging, smooth animations, a score, and win and lose states.

⛨ sandboxed Open in new tab ↗
transcript · 12 messages
user

Build a playable 2048 game with arrow-key tile sliding and merging, smooth animations, a score, and win and lose states.

assistant
thinking
The user wants me to build a complete 2048 game as a single self-contained HTML file. Let me create a polished, playable version with:

I'll build a complete, self-contained 2048 game in `index.html` with sliding, merges, score, and win/lose states.

write /tmp/ws/index.html (21820 bytes)
toolResult

Successfully wrote 21820 bytes to /tmp/ws/index.html

assistant
thinking
The game is written. Let me verify the file exists and think through whether the logic is correct. I should review the merge animation path carefully - there might be bugs.
I see the issue with the render function deleting merged source tiles. Let me fix that by updating the render logic to preserve merged tiles during the animation phase.

Merge slides were getting removed mid-animation. I'll keep those tiles on the board until the move finishes.

edit {"path":"/tmp/ws/index.html","edits":[{"oldText":" function render({ spawnIds, mergeIds } = {}) {\n const live = new Set();\n
toolResult

Successfully replaced 1 block(s) in /tmp/ws/index.html.

assistant
thinking
The render function already has keepIds support now. The move function should pass keepIds when rendering during animation. Let me check the current move function to see if it needs updating.
read {"path":"/tmp/ws/index.html","offset":380,"limit":120}
toolResult

el.style.transition = ""; } } function render({ spawnIds, mergeIds, keepIds } = {}) { const live = new Set(keepIds || []); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const t = board[r][c]; if (!t) continue; live.add(t.id); let el = tileEls.get(t.id); if (!el) { el = document.createElement("div"); el.className = "tile"; tileEls.set(t.id, el); tilesEl.appendChild(el); setTileTransform(el, t.fromR != null ? t.fromR : r, t.fromC != null ? t.fromC : c, { animate: false }); } el.dataset.v = tileClass(t.value); el.textContent = t.value; setTileTransform(el, r, c); el.classList.remove("spawn", "merge"); if (spawnIds && spawnIds.has(t.id)) el.classList.add("spawn"); if (mergeIds && mergeIds.has(t.id)) el.classList.add("merge"); } } for (const [id, el] of tileEls) { if (!live.has(id)) { el.remove(); tileEls.delete(id); } } } function bumpScore(delta) { if (!delta) return; score += delta; scoreEl.textContent = score; if (score > best) { best = score; bestEl.textContent = best; try { localStorage.setItem("2048-best", String(best)); } catch (_) {} } const pop = document.createElement("div"); pop.className = "score-pop"; pop.textContent = "+" + delta; scoreEl.parentElement.appendChild(pop); setTimeout(() => pop.remove(), 700); } function slideLine(line) { const tiles = line.filter(Boolean); const out = []; let gained = 0; let moved = false; let i = 0; while (i < tiles.length) { const cur = tiles[i]; const nxt = tiles[i + 1]; if (nxt && nxt.value === cur.value) { const merged = { id: nextId++, value: cur.value * 2, justMerged: true, fromIds: [cur.id, nxt.id], fromR: cur.r, fromC: cur.c }; gained += merged.value; out.push(merged); i += 2; moved = true; } else { out.push(cur); i += 1; } } while (out.length < SIZE) out.push(null); for (let k = 0; k < SIZE; k++) { const a = line[k] ? line[k].id : null; const b = out[k] ? out[k].id : null; if (a !== b) moved = true; } return { line: out, gained, moved }; } function rotateCW(b) { const n = emptyBoard(); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) n[c][SIZE - 1 - r] = b[r][c]; } return n; } function rotateCCW(b) { const n = emptyBoard(); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) n[SIZE - 1 - c][r] = b[r][c]; } return n; } function move(dir) { if (locked) return; // Normalize so we always slide left. // 0 left, 1 up, 2 right, 3 down let working = board.map(row => row.slice()); if (dir === 1) working = rotateCCW(working); else if (dir === 2) working = working.map(row => row.slice().reverse()); else if (dir === 3) working = rotateCW(working); let anyMoved = false; let gained = 0; const next = emptyBoard(); const mergeOrigins = []; for (let r = 0; r < SIZE; r++) { const result = slideLine(working[r]); next[r] = result.line; [252 more lines in file. Use offset=500 to continue.]

assistant
thinking
The file looks good. The keepIds change is in place. The move function already keeps departing tiles separately. Let me also update the render() call during animation to pass keepIds so departing tile...
read {"path":"/tmp/ws/index.html","offset":500,"limit":160}
toolResult

gained += result.gained; if (result.moved) anyMoved = true; } if (!anyMoved) return; if (dir === 1) working = rotateCW(next); else if (dir === 2) working = next.map(row => row.slice().reverse()); else if (dir === 3) working = rotateCCW(next); else working = next; // Prepare animation origins for merged tiles by keeping old tiles // visually until they reach the merge cell, then replace. const oldById = new Map(); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const t = board[r][c]; if (t) oldById.set(t.id, { r, c, value: t.value, id: t.id }); } } // Temporarily place source tiles at destination for slide, then swap to merged. const animBoard = emptyBoard(); const departing = []; for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const t = working[r][c]; if (!t) continue; if (t.justMerged && t.fromIds) { t.fromIds.forEach((id, idx) => { const src = oldById.get(id); departing.push({ id, value: src.value, fromR: src.r, fromC: src.c, toR: r, toC: c }); }); animBoard[r][c] = null; } else { const src = oldById.get(t.id); t.fromR = src ? src.r : r; t.fromC = src ? src.c : c; animBoard[r][c] = t; } } } locked = true; // Render departing tiles + unmerged movers const tempIds = []; departing.forEach(d => { let el = tileEls.get(d.id); if (!el) { el = document.createElement("div"); el.className = "tile"; tileEls.set(d.id, el); tilesEl.appendChild(el); } el.dataset.v = tileClass(d.value); el.textContent = d.value; el.classList.remove("spawn", "merge"); setTileTransform(el, d.fromR, d.fromC, { animate: false }); tempIds.push(d.id); }); board = animBoard; // Keep departing tiles alive during slide tempIds.forEach(id => { // park them conceptually so render won't delete if we skip render of them }); // Move unmerged tiles render(); // Slide departing tiles departing.forEach(d => { const el = tileEls.get(d.id); if (el) setTileTransform(el, d.toR, d.toC); }); setTimeout(() => { tempIds.forEach(id => { const el = tileEls.get(id); if (el) el.remove(); tileEls.delete(id); }); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const t = working[r][c]; if (t) { t.r = r; t.c = c; t.fromR = r; t.fromC = c; } } } board = working; spawn(); const spawnIds = new Set(); const mergeIds = new Set(); for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const t = board[r][c]; if (!t) continue; if (t.justSpawned) spawnIds.add(t.id); if (t.justMerged) mergeIds.add(t.id); t.justSpawned = false; t.justMerged = false; } } render({ spawnIds, mergeIds }); bumpScore(gained); locked = false; checkEnd(); }, ANIM_MS); } function canMove() { if (emptyCells().length) return true; for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const v = board[r][c].value; if (c + 1 < SIZE && board[r][c + 1].value === v) return true; if (r + 1 < SIZE && board[r + 1][c].value === v) return true; } } return false; } function has2048() { for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { if (board[r][c] && board[r][c].value >= 2048) return true; } } return false; } function showOverlay(kind) { if (kind === "win") { overlayTitle.textContent = "You win!"; overlayMsg.textContent = "You reached 2048."; keepGoingBtn.style.display = ""; } else { overlayTitle.textContent = "Game over"; overlayMsg.textContent = "No more moves left."; keepGoingBtn.style.display = "none"; } overlay.classList.add("show"); } function hideOverlay() { overlay.classList.remove("show"); } [92 more lines in file. Use offset=660 to continue.]

assistant
thinking
I need to fix the render call to keep departing tiles. Also I should update tile r,c positions after rotations so fromR/fromC stay correct... actually looking at the code, they store r,c on tiles when...

Keeping departing tiles visible during the slide, then swapping in the merged tile.

edit {"path":"/tmp/ws/index.html","edits":[{"oldText":" board = animBoard;\n // Keep departing tiles alive during slide\n te
toolResult

Successfully replaced 1 block(s) in /tmp/ws/index.html.

assistant
thinking
The game looks complete. Let me also fix a potential issue - when keepPlaying is true after win, checkEnd shouldn't show win again. Looking at the code:

`index.html` is a self-contained 2048 game. Open it in a browser to play. - **Move:** arrow keys, WASD, or swipe - **Merge:** matching tiles slide together; score ticks up with a `+n` pop - **Win / lose:** overlay at 2048 (Keep going or Try again) and when the board is stuck - **Best score** is stored in `localStorage` - Sliding, spawn, and merge animations; no network or extra files

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modelgrok-4.6
vendorxAI · Grok
harnesspi-agent
generated2026-08-13

EVALUATION

4/5 working

2048 clone with tile grid, score, and functioning arrow-key and WASD tile movement/merging; click/drag/wheel/enter do nothing (expected).

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.0%
on click0.0%
on drag0.0%
on wheel0.0%
on enter + space0.0%
on arrow keys3.6%
on w/a/s/d3.6%
frame spread25.8 / 255
console errors0
js errors none

METRICS

runtime99.6s
tokens in50k
tokens out8.2k
cached32k
cost / run$0.1012