Tetris · nex-n2-pro
Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.
transcript · 4 messages
Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.
thinking
We need respond by building single-page web app in index.html current dir. Need inspect cwd maybe empty. Need create self-contained index.html with canvas Tetris playable, seven tetrominoes, rotation, line clears, increasing speed, next-piece preview, score. Need no external. Need maybe use bash pwd/ls? Could write directly. Need ensure code works. Need maybe include instructions. Need maybe create index.html. Let's craft robust Tetris.
Need consider user specifically "Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score." We can create file. Need maybe not overdo final. We need maybe use write. Let's inspect maybe not needed but can write. Need maybe maybe current directory /tmp/ws. We'll write index.html. Need ensure fully sandboxed. Need include CSS, JS. Canvas main and next preview. Keyboard controls: arrows/A D move, up/W/X rotate, down soft drop, space hard drop, P pause, R restart. Score: 100/300/500/800 lines times level, soft/hard drop. Increasing speed by level. Tetrominoes all 7: I,J,L,O,S,T,Z. Rotation with wall kicks maybe simple. Need game loop.
Need maybe include touch buttons? User didn't ask but playable. Could include on-screen buttons? Maybe canvas only. Need "playable Tetris on a canvas" maybe keyboard playable. Could include click/touch buttons too maybe nice. But no external.
Need build robust. Let's design.
HTML structure:
<!doctype html><html lang="en"><head><meta charset=utf-8><meta name=viewport...><title>Tetris</title><style> ... responsive flex; body dark; .wrap; canvas; .side panel; .next canvas; .stats; .controls; buttons. </style></head><body> <div class="app"> <h1>Canvas Tetris</h1> <div class="game"> <canvas id="board" width="300" height="600"></canvas> <aside> <canvas id="next" width="120" height="120"></canvas> <div id=score>... etc </div> <button id=startBtn>New Game</button> <button id=pauseBtn>Pause</button> <div class=controls>...</div> </aside> </div></div>
<script>
(() => {
'use strict';
const COLS=10, ROWS=20, BLOCK=30;
const canvas=document.getElementById('board'), ctx=...
const nextCanvas...
const COLORS = { I:'#00e5ff', J:'#2f6bff', L:'#ff9f1c', O:'#ffd500', S:'#00d26a', T:'#b967ff', Z:'#ff4d6d' };
const SHAPES = { I: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], ... } But rotation matrices.
Could use matrix shapes of 4x4. Need rotation. Spawn x = Math.floor(COLS/2) - Math.ceil(matrix[0].length/2). For I shape 4x4 x=3. For O 2x2 x=4. OK.
Need collision(board,piece): for each y,x if cell then nx=piece.x+x, ny=piece.y+y. if nx<0 || >=COLS || ny>=ROWS || (ny>=0 && board[ny][nx]) return true.
merge: if ny<0 game over. board[ny][nx]=piece.type.
clear lines: for y=ROWS-1 downto 0 if every board[y].every(Boolean): splice; unshift(Array(COLS).fill(0)); y++; count++.
Need next bag randomizer: 7-bag. pieces queue. Need next piece preview. Could maintain bag=[]; function nextType(){ if bag empty fill 7 types shuffle; return bag.shift(); } spawn uses nextType. We need have current and next. At game start current = createPiece(nextType()); nextType=nextType(); drawNext. On piece lock spawn nextPiece.
Rotation: rotate matrix clockwise. For each type. For O no change maybe. Use rotateMatrix(matrix): matrix[0].map((_, i)=>matrix.map(row=>row[i]).reverse()); For 4x4. After rotate, wall kicks. Need handle I and O maybe. Use kicks = [[0,0],[1,0],[-1,0],[2,0],[-2,0],[0,-1],[0,-2],[1,-1],[-1,-1]]. For each offset, if not collision then apply. For O maybe still okay. Maybe rotate can allow. Use piece.matrix=rotated; piece.x+=kx; piece.y+=ky.
Need hard drop: while !collision drop; score += dist*2; merge; afterLock; Need soft drop: move down if possible score +=1 else lock? Usually if down pressed and collision, lock. Need implement.
Need drop interval based on level: Math.max(80, 1000 - (level-1)*80) or exponential. Level increases every 10 lines. Score line clear: [0,100,300,500,800][count]*level. Soft/hard optional.
Need game loop: let dropCounter=0,lastTime=0,dropInterval=1000, paused=false, gameOver=false, running=false. requestAnimationFrame(update). update(time=0){ const delta=time-lastTime; lastTime=time; if (!paused && !gameOver) { dropCounter += delta; if (dropCounter > dropInterval) { playerDrop(false); } } draw(); requestAnimationFrame(update);}
playerDrop(manual=false): if (!move(0,1)) { lockPiece(); } dropCounter=0. For automatic, if lock then spawn. For manual, maybe score +=1 if move success. Need if lock from manual. Good.
lockPiece: merge; clear lines; update score; spawn next; if collision spawn -> gameOver. Need spawn: current = nextPiece; nextPiece = createPiece(nextType()); update next preview.
Potential issue if lock after merge and clear, spawn could gameover if collision. Good.
Need start/restart: reset board; bag=[]; current=createPiece(nextType()); nextPiece=createPiece(nextType()); score=0 lines=0 level=1; dropCounter=0; gameOver=false; paused=false; running=true; update UI. Maybe hide overlay? Draw overlay.
Need draw: ctx.clear; draw board with grid; draw ghost? Not required but can. Need draw current and next. Need canvas CSS scales? Use width 300 height 600. For retina maybe maybe set canvas internal size? Could use CSS image-rendering pixelated. Need maybe responsive. Could implement device pixel ratio. But if canvas CSS width = 300px and internal 300, OK. But high DPI maybe. Could set canvas.width = COLS*BLOCK = 300, height=600. CSS width: min(80vw, 300px). For next canvas width 120 height 120.
Need drawBlock(ctx,x,y,size,color): fill with rounded? Simpler rectangles. Canvas main: for each cell drawBlock(x*BLOCK,y*BLOCK,BLOCK,color). Need maybe draw grid lines. Next preview: draw centered. For matrix 4x4, compute minX/maxX/minY/maxY of filled cells. size = 24? previewCanvas width 120. draw cells in 4x4 grid centered: startX=(canvas.width - shapeWidth*previewBlock)/2 - minX*previewBlock; startY=(canvas.height - shapeHeight*previewBlock)/2 - minY*previewBlock. But for I shape width=4, startY maybe 24 if h=1, OK. For O h=2.
Need draw overlay: if not running? maybe title "Press New Game"; if paused; if game over. On initial board empty and overlay.
Need keyboard handling:
document.addEventListener('keydown', e => {
const keys = ['ArrowLeft','ArrowRight','ArrowDown','ArrowUp','Space','KeyA','KeyD','KeyS','KeyW','KeyX','KeyP','KeyR']; if includes e.preventDefault();
if (e.repeat && e.code === 'ArrowDown') maybe still allow? But if e.repeat, repeated soft drop will lock rapidly. Could allow with throttling? Simpler: if e.repeat return for certain keys except maybe down? But arrow down repeat can soft drop many. Fine. But if e.repeat and key hard drop could multiple. Need avoid: if (e.repeat && e.code !== 'ArrowDown') return.
if (!running && e.code === 'Enter') startGame;
if (gameOver && (e.code === 'KeyR'||e.code==='Enter')) startGame;
if (paused && e.code !== 'KeyP') return? Maybe arrow won't.
switch e.code:
ArrowLeft/KeyA move(-1,0)
ArrowRight/KeyD
ArrowDown/KeyS playerDrop(true)
ArrowUp/KeyW/KeyX rotate(1)
KeyZ rotate(-1)
Space hardDrop
KeyP togglePause
KeyR startGame
});
Need if paused and P toggles. If game over and R. Need prevent default for space etc.
On-screen buttons:
<div class=touch-controls>
<button data-action=left>←</button> etc
</div>
Need click/touch pointerdown. Use pointerdown to call actions. But repeated movement? Could use click. For mobile playable, maybe use pointerdown. Need not necessary. But nice. We can include buttons. Need handle no external.
Need maybe "Canvas" only? There is next preview canvas. The main board canvas. Good.
Need maybe all code self-contained. Need create index.html. Let's write.
Need ensure no syntax errors. Let's draft carefully.
Detailed code:
const board = Array.from({length: ROWS}, () => Array(COLS).fill(0));
let current = null;
let nextPiece = null;
let bag = [];
let score=0, lines=0, level=1, dropCounter=0, lastTime=0, paused=false, gameOver=false, started=false;
function cloneMatrix(m){ return m.map(row => row.slice()); }
function createPiece(type) { return { type, matrix: cloneMatrix(SHAPES[type]), x:0,y:0};}
function resetPiecePosition(piece){ piece.x = Math.floor(COLS / 2) - Math.ceil(piece.matrix[0].length / 2); piece.y = piece.type === 'I' ? -1 : 0; } Hmm for I 4x4 y=-1, top row empty so cells at row1. For other 2x2 y=0. For T 3x3 y=0. Good.
function randomType() { if (!bag.length) bag = shuffle([...Object.keys(SHAPES)]); return bag.pop(); }
function shuffle(arr){ for i=arr.length-1...; return arr; }
function spawnPiece(){ current = nextPiece || createPiece(randomType()); resetPiecePosition(current); nextPiece = createPiece(randomType()); updateNext(); if (collides(current)) { gameOver=true; started=false? maybe running remains true? } }
At start: resetGame calls current = createPiece(randomType()), reset; nextPiece=createPiece(randomType()), spawn? Need avoid gameover if initial. We can:
function newGame() { board fill; bag=[]; score=...; paused=false; gameOver=false; started=true; current=createPiece(randomType()); resetPiecePosition(current); nextPiece=createPiece(randomType()); updateNext(); dropCounter=0; lastTime=0; updateUI(); }
At lock: spawnPiece calls current=nextPiece, reset, nextPiece=createPiece(randomType()), updateNext(); if collides current gameOver=true; started=false? Hmm if gameOver, overlay. If started=false then "Press New Game". But maybe started remains true, overlay game over. Use started true always after new game.
Need updateUI:
scoreEl.textContent = score.toLocaleString();
linesEl.textContent = lines;
levelEl.textContent = level;
speedEl.textContent = `${dropInterval()} ms`;
pauseBtn.textContent = paused ? 'Resume' : 'Pause';
statusEl.textContent = gameOver ? 'Game over' : paused ? 'Paused' : 'Playing';
Maybe speed dynamic.
Need dropInterval: return Math.max(90, 1000 * Math.pow(0.86, level - 1)); maybe at level 10 221ms, level 20 49ms. Max 80. Or formula 1000 - (level-1)*75. Use max 80.
Need line clear:
function sweepLines() {
let cleared=0;
outer: for (let y=ROWS-1; y>=0; y--) {
for (let x=0; x<COLS; x++) {
if (!board[y][x]) continue outer;
}
const row = board.splice(y,1)[0].fill(0);
board.unshift(row);
cleared++;
y++;
}
if (cleared >0) { lines += cleared; const oldLevel=level; level = Math.floor(lines/10)+1; const points = [0,100,300,500,800][cleared] * level; score += points; maybe if level > oldLevel score += (level-oldLevel)*500; updateUI(); }
}
Need note if level changes after score? Scoring uses new level maybe okay. Could use oldLevel. Tetris scoring: points * level. Use oldLevel maybe. Then update. We'll do oldLevel.
Need merge:
function mergePiece(){ current.matrix.forEach((row,y)=> row.forEach((value,x)=> { if(value) { const by=current.y+y; if(by<0) { gameOver=true; } else board[by][current.x+x]=current.type; } })) }
If spawn collision after merge? gameover.
Need playerMove(dx,dy): if !collides -> x+=dx; y+=dy; return true; else false.
playerDrop:
function playerDrop(manual=false) {
if (gameOver) return;
if (move(0,1)) { if (manual) score += 1; updateUI(); }
else { lockPiece(); }
dropCounter=0;
}
Potential if manual down at bottom, lock. Good.
Hard drop:
let distance=0; while (!collides(...)) { current.y++; distance++; } current.y--; distance--; score += distance*2; lockPiece(); dropCounter=0; updateUI();
Need if current starts colliding? distance=0.
Need rotate:
function rotatePiece(dir=1) {
if (gameOver || paused) return;
const originalMatrix=cloneMatrix(current.matrix);
const rotated = rotateMatrix(originalMatrix, dir);
const originalX=current.x, originalY=current.y;
current.matrix=rotated;
const kicks = current.type === 'O' ? [[0,0]] : [[0,0],[1,0],[-1,0],[2,0],[-2,0],[0,-1],[0,-2],[0,1],[1,-1],[-1,-1],[2,-1],[-2,-1]];
for (const [kx,ky] of kicks) { current.x=originalX+kx; current.y=originalY+ky; if (!collides(current)) { return; } }
current.matrix=originalMatrix; current.x=originalX; current.y=originalY;
}
rotateMatrix:
function rotateMatrix(matrix, dir=1) {
const N=matrix.length;
const rotated = matrix[0].map((_, i) => matrix.map(row => row[i]));
if (dir > 0) return rotated.map(row => row.reverse());
return rotated.reverse(); // For 4x4? Let's verify. For CCW, transpose then reverse rows? Matrix [[a,b],[c,d]], transpose [[a,c],[b,d]], reverse rows -> [[b,d],[a,c]]? Actually CCW should [[b,d],[a,c]]. Yes. For clockwise: transpose [[a,c],[b,d]], reverse each row -> [[c,a],[d,b]] yes. Good.
}
But for non-square? SHAPES all square. Good.
Need drawBoard:
function drawCell(ctx, px, py, size, color, alpha=1) {
ctx.globalAlpha=alpha;
ctx.fillStyle=color; ctx.fillRect(px+1,py+1,size-2,size-2);
ctx.fillStyle='rgba(255,255,255,.25)'; ctx.fillRect(px+1,py+1,size-2,Math.max(2,size*0.12));
ctx.fillStyle='rgba(0,0,0,.25)'; ctx.fillRect(px+1,py+size-Math.max(2,size*0.12)-1,size-2,Math.max(2,size*0.12));
ctx.globalAlpha=1;
}
Need if size small maybe Math.max(2,...).
draw:
ctx.fillStyle='#08111f'; ctx.fillRect(0,0,canvas.width,canvas.height);
draw board cells.
Draw grid: ctx.strokeStyle='rgba(255,255,255,0.06)'; for x=0..COLS etc. But board background maybe.
Ghost: if current and !gameOver:
const ghost={...current, y: current.y}; while(!collides(ghost)) ghost.y++; ghost.y--; drawMatrix(ghost, .18)
Then current.
drawMatrix(piece, alpha): for cells.
Need maybe draw only current if started. If no current, draw grid.
drawNext:
ctx.clear; fill; if nextPiece drawMatrixInPreview.
Need maybe draw preview grid.
Need overlay:
function drawOverlay(title, subtitle) { ctx.save(); ctx.fillStyle='rgba(2,6,14,.72)'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.textAlign='center'; ctx.fillStyle='#fff'; ctx.font='bold 30px system-ui'; ctx.fillText(title, canvas.width/2, canvas.height/2-10); ctx.font='16px system-ui'; ctx.fillStyle='#b8c7d9'; ctx.fillText(subtitle, canvas.width/2, canvas.height/2+22); ctx.restore();}
In draw: if !started drawOverlay('Tetris','Press New Game or Enter'); if paused drawOverlay('Paused','Press P or Resume'); if gameOver drawOverlay('Game Over','Press R or New Game');
Need if paused and gameOver? gameOver priority.
Need draw next preview even before game? nextPiece null; draw "Next".
Need maybe "All seven tetrominoes" shapes:
I: [
[0,0,0,0],
[1,1,1,1],
[0,0,0,0],
[0,0,0,0]
],
J: [
[1,0,0],
[1,1,1],
[0,0,0]
],
L: [
[0,0,1],
[1,1,1],
[0,0,0]
],
O: [
[1,1],
[1,1]
],
S: [
[0,1,1],
[1,1,0],
[0,0,0]
],
T: [
[0,1,0],
[1,1,1],
[0,0,0]
],
Z: [
[1,1,0],
[0,1,1],
[0,0,0]
]
Need collision handles matrix length.
Need maybe "increasing speed" visible. Level every 10 lines.
Need maybe "score" visible.
Need maybe "line clears" visible.
Need maybe "next-piece preview" visible.
Need maybe "playable Tetris on a canvas" maybe main board canvas. Good.
Need maybe no external resources. Good.
Need maybe "current directory /tmp/ws". Use write.
Need maybe include no markdown final? We can say created index.html. But final should be concise. Need maybe not include code? User asked build. We can just say done. But maybe final maybe mention controls. Need ensure file present. We can maybe run ls. But after write maybe no need. However maybe use bash ls to verify. Let's write file.
/tmp/ws/index.html (21612 bytes)Successfully wrote 21612 bytes to /tmp/ws/index.html
thinking
Created `/tmp/ws/index.html`. It’s a self-contained, no-network Canvas Tetris app with: - all seven tetrominoes - rotation with wall kicks - line clears - increasing speed by level - next-piece preview - score, lines, and level display - keyboard and touch/click controls
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
EVALUATION
Functional Tetris with next-piece preview, score, and pieces moving/rotating in response to keyboard input; click/drag/wheel had no effect as 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.