oneshotlm
Compare all models on this prompt →

Tetris · minimax-m2.7

Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.

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

Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.

assistant
thinking
The user wants me to build a playable Tetris game. Let me first create a specification document, then implement the game.

Key features needed:
1. All seven tetrominoes (I, O, T, S, Z, J, L)
2. Rotation
3. Line clears
4. Increasing speed
5. Next-piece preview
6. Score display

Let me create the SPEC.md first, then build the game.
write /workspace/SPEC.md (3648 bytes)
toolResult

Successfully wrote 3648 bytes to /workspace/SPEC.md

assistant
thinking
Now I'll implement the Tetris game according to the spec. I need to create a single HTML file with all the CSS and JavaScript inline.
write /workspace/index.html (21100 bytes)
toolResult

Successfully wrote 21100 bytes to /workspace/index.html

assistant
thinking
Now I need to verify the implementation:
1. Check the file exists
2. Verify external resources (Google Fonts)
3. Use Playwright to test it in a browser

Let me check the external resource and then test with Playwright.
bash {"command":"curl -sI \"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap\" | head -1"}
toolResult

/bin/bash: line 1: curl: command not found

assistant
thinking
curl is not available. Let me try with wget or check if there's another way.
bash {"command":"wget -q --spider \"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap\" && echo \"Font URL is val
toolResult

/bin/bash: line 1: wget: command not found Command exited with code 127

assistant
thinking
Neither curl nor wget is available. Let me try to test with Playwright directly to verify the page loads without errors.
bash {"command":"npx playwright install chromium 2>/dev/null || npm install -g playwright && npx playwright install chromium","timeout":120}
toolResult

npm error code ENOENT npm error syscall mkdir npm error path /home/node/.npm npm error errno ENOENT npm error enoent Invalid response body while trying to fetch https://registry.npmjs.org/playwright: ENOENT: no such file or directory, mkdir '/home/node/.npm' npm error enoent This is related to npm not being able to find a file. npm error enoent npm error Log files were not written due to an error writing to the directory: /home/node/.npm/_logs npm error You can rerun the command with `--loglevel=verbose` to see the logs in your terminal Command exited with code 1

assistant
thinking
Let me check what's available for testing.
bash {"command":"which node && node --version"}
toolResult

/usr/local/bin/node v24.18.0

assistant
thinking
Node is available. Let me try installing playwright differently or check if it's already installed.
bash {"command":"npm list playwright 2>/dev/null || npm list -g playwright 2>/dev/null || ls /usr/local/lib/node_modules/ | grep -i play"}
toolResult

/workspace `-- (empty) /usr/local/lib `-- (empty) Command exited with code 1

assistant
thinking
Playwright doesn't seem to be installed. Let me try installing it locally in the workspace directory.
bash {"command":"cd /workspace && npm init -y && npm install playwright --prefer-offline 2>&1 | tail -20","timeout":180}
toolResult

Wrote to /workspace/package.json: { "name": "workspace", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs" } npm error code ENOENT npm error syscall mkdir npm error path /home/node/.npm npm error errno ENOENT npm error enoent Invalid response body while trying to fetch https://registry.npmjs.org/playwright: ENOENT: no such file or directory, mkdir '/home/node/.npm' npm error enoent This is related to npm not being able to find a file. npm error enoent npm error Log files were not written due to an error writing to the directory: /home/node/.npm/_logs npm error You can rerun the command with `--loglevel=verbose` to see the logs in your terminal

assistant
thinking
There's a filesystem issue with npm. Let me check if the HTML file syntax is correct by doing a basic check. Since I can't use Playwright, I'll verify the code by reading it and ensuring there are no obvious errors.
bash {"command":"ls -la /workspace/index.html && wc -l /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 21100 Jul 10 00:30 /workspace/index.html 629 /workspace/index.html

assistant
thinking
The file was created successfully. Let me verify the implementation against the spec by reading through the key parts of the code.
read {"path":"/workspace/index.html","offset":1,"limit":50}
toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tetris</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { min-height: 100vh; display: flex; justify-content: center; align-items: center; background: #0f0f23; background-image: radial-gradient(ellipse at 50% 0%, rgba(0, 217, 255, 0.1) 0%, transparent 50%), linear-gradient(180deg, #0f0f23 0%, #1a1a2e 100%); font-family: 'JetBrains Mono', monospace; overflow: hidden; } .game-container { display: flex; gap: 20px; padding: 30px; background: rgba(22, 33, 62, 0.6); border-radius: 16px; border: 1px solid rgba(0, 217, 255, 0.2); box-shadow: 0 0 60px rgba(0, 217, 255, 0.1), inset 0 0 60px rgba(0, 0, 0, 0.3); } .board-wrapper { position: relative; } #gameCanvas { border: 2px solid #00d9ff; border-radius: 4px; box-shadow: 0 0 20px rgba(0, 217, 255, 0.3), [580 more lines in file. Use offset=51 to continue.]

assistant
thinking
The HTML structure looks correct. Let me verify the key game logic parts are properly implemented.
read {"path":"/workspace/index.html","offset":200,"limit":100}
toolResult

</div> <div class="info-box"> <label>Lines</label> <div class="value" id="linesDisplay">0</div> </div> <div class="controls"> <span>Arrow Keys</span> Move<br> <span>Up / X</span> Rotate CW<br> <span>Z</span> Rotate CCW<br> <span>Space</span> Hard Drop<br> <span>P</span> Pause </div> </div> </div> <script> const COLS = 10; const ROWS = 20; const CELL_SIZE = 30; const COLORS = { I: '#00d9ff', O: '#ffd700', T: '#9b59b6', S: '#2ecc71', Z: '#e74c3c', J: '#3498db', L: '#e67e22' }; const SHAPES = { I: [ [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], [[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]], [[0,0,0,0], [0,0,0,0], [1,1,1,1], [0,0,0,0]], [[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]] ], O: [[[1,1],[1,1]]], T: [ [[0,1,0], [1,1,1], [0,0,0]], [[0,1,0], [0,1,1], [0,1,0]], [[0,0,0], [1,1,1], [0,1,0]], [[0,1,0], [1,1,0], [0,1,0]] ], S: [ [[0,1,1], [1,1,0], [0,0,0]], [[0,1,0], [0,1,1], [0,0,1]], [[0,0,0], [0,1,1], [1,1,0]], [[1,0,0], [1,1,0], [0,1,0]] ], Z: [ [[1,1,0], [0,1,1], [0,0,0]], [[0,0,1], [0,1,1], [0,1,0]], [[0,0,0], [1,1,0], [0,1,1]], [[0,1,0], [1,1,0], [1,0,0]] ], J: [ [[1,0,0], [1,1,1], [0,0,0]], [[0,1,1], [0,1,0], [0,1,0]], [[0,0,0], [1,1,1], [0,0,1]], [[0,1,0], [0,1,0], [1,1,0]] ], L: [ [[0,0,1], [1,1,1], [0,0,0]], [[0,1,0], [0,1,0], [0,1,1]], [[0,0,0], [1,1,1], [1,0,0]], [[1,1,0], [0,1,0], [0,1,0]] ] }; const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const nextCanvas = document.getElementById('nextCanvas'); const nextCtx = nextCanvas.getContext('2d'); const scoreDisplay = document.getElementById('scoreDisplay'); const levelDisplay = document.getElementById('levelDisplay'); const linesDisplay = document.getElementById('linesDisplay'); const pauseOverlay = document.getElementById('pauseOverlay'); const gameOverOverlay = document.getElementById('gameOverOverlay'); const finalScore = document.getElementById('finalScore'); const levelUpEl = document.getElementById('levelUp'); let board = []; let currentPiece = null; let nextPiece = null; let score = 0; let level = 1; let lines = 0; let gameOver = false; let paused = false; let dropInterval = null; let lastDropTime = 0; let animatingLines = []; let animationStart = 0; const PIECE_TYPES = ['I', 'O', 'T', 'S', 'Z', 'J', 'L']; function createBoard() { board = []; for (let r = 0; r < ROWS; r++) { [331 more lines in file. Use offset=300 to continue.]

assistant
thinking
The code looks well-structured. All seven tetrominoes are defined with their rotations. Let me verify the rest of the game logic.
read {"path":"/workspace/index.html","offset":300,"limit":150}
toolResult

board[r] = []; for (let c = 0; c < COLS; c++) { board[r][c] = null; } } } function randomPiece() { const type = PIECE_TYPES[Math.floor(Math.random() * PIECE_TYPES.length)]; return { type, shape: SHAPES[type], rotation: 0, x: Math.floor((COLS - SHAPES[type][0][0].length) / 2), y: 0 }; } function getShape(piece) { return piece.shape[piece.rotation % piece.shape.length]; } function collides(piece, offsetX = 0, offsetY = 0, newRotation = null) { const shape = newRotation !== null ? piece.shape[newRotation % piece.shape.length] : getShape(piece); for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (shape[r][c]) { const newX = piece.x + c + offsetX; const newY = piece.y + r + offsetY; if (newX < 0 || newX >= COLS || newY >= ROWS) return true; if (newY >= 0 && board[newY][newX]) return true; } } } return false; } function lockPiece() { const shape = getShape(currentPiece); for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (shape[r][c]) { const y = currentPiece.y + r; const x = currentPiece.x + c; if (y < 0) { endGame(); return; } board[y][x] = currentPiece.type; } } } checkLines(); spawnPiece(); } function checkLines() { const fullLines = []; for (let r = ROWS - 1; r >= 0; r--) { if (board[r].every(cell => cell !== null)) { fullLines.push(r); } } if (fullLines.length > 0) { animatingLines = fullLines; animationStart = performance.now(); setTimeout(() => { for (const row of fullLines.sort((a, b) => a - b)) { board.splice(row, 1); board.unshift(Array(COLS).fill(null)); } const cleared = fullLines.length; lines += cleared; const points = [0, 100, 300, 500, 800][cleared] * level; score += points; const newLevel = Math.floor(lines / 10) + 1; if (newLevel > level) { level = newLevel; updateDropSpeed(); showLevelUp(); } updateDisplay(); animatingLines = []; }, 200); } } function showLevelUp() { levelUpEl.classList.remove('show'); void levelUpEl.offsetWidth; levelUpEl.classList.add('show'); } function getDropSpeed() { return Math.max(100, 1000 * Math.pow(0.85, level - 1)); } function updateDropSpeed() { if (dropInterval) clearInterval(dropInterval); dropInterval = setInterval(drop, getDropSpeed()); } function drop() { if (!currentPiece || gameOver || paused) return; if (!collides(currentPiece, 0, 1)) { currentPiece.y++; } else { lockPiece(); } } function hardDrop() { if (!currentPiece || gameOver || paused) return; let dropped = 0; while (!collides(currentPiece, 0, 1)) { currentPiece.y++; dropped++; } score += dropped * 2; updateDisplay(); lockPiece(); } function move(dx) { if (!currentPiece || gameOver || paused) return; if (!collides(currentPiece, dx, 0)) { currentPiece.x += dx; } } function rotate(dir) { if (!currentPiece || gameOver || paused || currentPiece.type === 'O') return; const newRotation = (currentPiece.rotation + dir + currentPiece.shape.length) % currentPiece.shape.length; if (!collides(currentPiece, 0, 0, newRotation)) { currentPiece.rotation = newRotation; } else if (!collides(currentPiece, -dir, 0, newRotation)) { currentPiece.x -= dir; currentPiece.rotation = newRotation; } else if (!collides(currentPiece, dir * 2, 0, newRotation)) { currentPiece.x += dir * 2; currentPiece.rotation = newRotation; } } function getGhostY() { if (!currentPiece) return 0; let ghostY = currentPiece.y; while (!collides(currentPiece, 0, ghostY - currentPiece.y + 1)) { ghostY++; } [181 more lines in file. Use offset=450 to continue.]

assistant
thinking
The game logic looks complete with all the required features:
1. All 7 tetrominoes (I, O, T, S, Z, J, L) ✓
2. Rotation with wall kicks ✓
3. Line clears with scoring ✓
4. Increasing speed ✓
5. Next piece preview ✓
6. Score/level/lines display ✓

Let me verify the rest of the rendering and event handling code.
read {"path":"/workspace/index.html","offset":450,"limit":100}
toolResult

return ghostY; } function spawnPiece() { currentPiece = nextPiece || randomPiece(); nextPiece = randomPiece(); if (collides(currentPiece)) { endGame(); } } function endGame() { gameOver = true; finalScore.textContent = score; gameOverOverlay.classList.add('active'); } function resetGame() { createBoard(); score = 0; level = 1; lines = 0; gameOver = false; paused = false; animatingLines = []; updateDisplay(); spawnPiece(); gameOverOverlay.classList.remove('active'); pauseOverlay.classList.remove('active'); updateDropSpeed(); } function togglePause() { if (gameOver) return; paused = !paused; pauseOverlay.classList.toggle('active', paused); } function updateDisplay() { scoreDisplay.textContent = score; levelDisplay.textContent = level; linesDisplay.textContent = lines; } function drawCell(context, x, y, type, alpha = 1) { const color = COLORS[type]; context.fillStyle = color; context.globalAlpha = alpha; context.fillRect(x * CELL_SIZE + 1, y * CELL_SIZE + 1, CELL_SIZE - 2, CELL_SIZE - 2); context.fillStyle = 'rgba(255,255,255,0.3)'; context.fillRect(x * CELL_SIZE + 2, y * CELL_SIZE + 2, CELL_SIZE - 6, 3); context.fillStyle = 'rgba(0,0,0,0.3)'; context.fillRect(x * CELL_SIZE + 2, y * CELL_SIZE + CELL_SIZE - 5, CELL_SIZE - 6, 3); context.globalAlpha = 1; } function drawBoard() { ctx.fillStyle = '#16213e'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#1f3460'; ctx.lineWidth = 1; for (let r = 0; r <= ROWS; r++) { ctx.beginPath(); ctx.moveTo(0, r * CELL_SIZE); ctx.lineTo(canvas.width, r * CELL_SIZE); ctx.stroke(); } for (let c = 0; c <= COLS; c++) { ctx.beginPath(); ctx.moveTo(c * CELL_SIZE, 0); ctx.lineTo(c * CELL_SIZE, canvas.height); ctx.stroke(); } for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { if (board[r][c]) { const isAnimating = animatingLines.includes(r); drawCell(ctx, c, r, board[r][c], isAnimating ? 0.5 + 0.5 * Math.sin((performance.now() - animationStart) / 30) : 1); } } } } function drawPiece() { if (!currentPiece) return; const shape = getShape(currentPiece); const ghostY = getGhostY(); for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (shape[r][c]) { const drawY = ghostY + r; if (drawY >= 0) { drawCell(ctx, currentPiece.x + c, drawY, currentPiece.type, 0.25); } } } } for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (shape[r][c]) { [81 more lines in file. Use offset=550 to continue.]

assistant
thinking
Let me see the rest of the code to make sure the event handlers are correct.
read {"path":"/workspace/index.html","offset":550,"limit":80}
toolResult

const drawY = currentPiece.y + r; if (drawY >= 0) { drawCell(ctx, currentPiece.x + c, drawY, currentPiece.type); } } } } } function drawNextPiece() { nextCtx.fillStyle = 'rgba(15, 15, 35, 0.6)'; nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height); if (!nextPiece) return; const shape = nextPiece.shape[0]; const offsetX = (nextCanvas.width - shape[0].length * 22) / 2; const offsetY = (nextCanvas.height - shape.length * 22) / 2; for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (shape[r][c]) { const x = offsetX / 22 + c; const y = offsetY / 22 + r; const color = COLORS[nextPiece.type]; nextCtx.fillStyle = color; nextCtx.fillRect(offsetX + c * 22 + 1, offsetY + r * 22 + 1, 20, 20); nextCtx.fillStyle = 'rgba(255,255,255,0.3)'; nextCtx.fillRect(offsetX + c * 22 + 2, offsetY + r * 22 + 2, 16, 3); } } } } function render() { drawBoard(); drawPiece(); drawNextPiece(); requestAnimationFrame(render); } document.addEventListener('keydown', (e) => { if (gameOver) { if (e.key === 'r' || e.key === 'R') resetGame(); return; } switch (e.key) { case 'ArrowLeft': move(-1); break; case 'ArrowRight': move(1); break; case 'ArrowDown': if (!paused && currentPiece && !collides(currentPiece, 0, 1)) { currentPiece.y++; score += 1; updateDisplay(); } break; case 'ArrowUp': case 'x': case 'X': rotate(1); break; case 'z': case 'Z': rotate(-1); break; case ' ': e.preventDefault(); hardDrop(); break; case 'p': case 'P': togglePause(); break; } }); createBoard(); spawnPiece(); updateDropSpeed(); render(); </script> </body> </html> [1 more lines in file. Use offset=630 to continue.]

assistant
thinking
The implementation is complete. Let me verify it against the spec:

**SPEC.md Verification:**

1. **All 7 tetrominoes** ✓ - I, O, T, S, Z, J, L with all rotation states defined
2. **Rotation** ✓ - Clockwise (Up/X) and counter-clockwise (Z), with wall kicks
3. **Line clears** ✓ - Line detection, removal animation, and scoring
4. **Increasing speed** ✓ - Level up every 10 lines, speed formula `1000 * 0.85^(level-1)`
5. **Next-piece preview** ✓ - Rendered in sidebar canvas
6. **Score** ✓ - Line clear scoring (100/300/500/800 × level), soft drop (1 pt), hard drop (2 pts)
7. **Visual effects** ✓ - Ghost piece, line clear animation, 3D cell effect
8. **Controls** ✓ - All specified keys implemented
9. **Game states** ✓ - Playing, Paused, Game Over with proper overlays

The implementation matches the spec completely. The file is ready and self-contained.

The Tetris game is complete. Here's what's implemented: **Features:** - All 7 tetrominoes (I, O, T, S, Z, J, L) with proper rotations - Ghost piece showing where the block will land - Line clears with flash animation - Score system: Single (100×level), Double (300×level), Triple (500×level), Tetris (800×level) - Soft drop (1 pt/cell) and hard drop (2 pts/cell) - Next piece preview in sidebar - Speed increases 15% per level (level up every 10 lines) - Level up notification animation **Controls:** | Key | Action | |-----|--------| | ← → | Move | | ↓ | Soft drop | | ↑ / X | Rotate CW | | Z | Rotate CCW | | Space | Hard drop | | P | Pause | | R | Restart (after game over) | **Files created:** - `/workspace/SPEC.md` - Specification document - `/workspace/index.html` - Self-contained game (21KB) Open `index.html` in a browser to play.

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

modelminimax-m2.7
vendorMiniMax
harnesspi-agent
generated2026-07-10

EVALUATION

4/5 working

Functional Tetris with piece drop, rotation, next preview, score/level/lines tracking, and responsive keyboard controls.

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.7%
on click0.8%
on drag1.0%
on wheel1.6%
on enter + space2.0%
on arrow keys1.2%
on w/a/s/d0.9%
frame spread13.6 / 255
console errors1
js errors none

METRICS

runtime101.5s
tokens in26k
tokens out8.0k
cached108k
cost / run$0.0139