Tetris · gpt-5-mini
Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.
This one-shot run produced no runnable artifact.
agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 2 messages
Build a playable Tetris on a canvas with all seven tetrominoes, rotation, line clears, increasing speed, a next-piece preview, and score.
thinking
**Planning Tetris Implementation** I need to create a single index.html file with a complete implementation of Tetris, using no external resources. It should include all seven tetrominoes, basic rotation, and simple wall kicks. The game will have line clears, increasing speed, a next-piece preview, and a scoring system. I'll implement keyboard controls for movement and include features like pause and restart. It’s essential to ensure everything is functional and works smoothly using canvas and requestAnimationFrame or setInterval.**Designing Tetris Mechanics** I'm planning to implement the Tetris game using shapes defined as arrays for their rotation states with coordinates. The grid will be 10 wide and 20 tall, plus 2 hidden rows. I’ll follow the standard scoring system: singles grant 100 points times the level, doubles 300, triples 500, and tetris lines 800. Levels will rise every 10 lines, increasing the drop speed. For the next-piece preview, I’ll use the same canvas, creating a side panel for the preview, score, and controls.**Mapping Keyboard Controls** For the keyboard controls, I have the Left Arrow for moving left, Right Arrow for moving right, Up Arrow for clockwise rotation, Z for counterclockwise rotation, Down for soft drops, Space for hard drops, and P to pause the game. I’m thinking of implementing rotation with simple wall kicks, colliding and adjusting offsets. Each tetromino will be represented as an array of four rotation states. The canvas dimensions will be 450 pixels wide and 600 pixels tall, with block sizes of 30 pixels.**Setting Up the Game Loop** I’m planning the game loop to use a drop timer that accumulates deltaTime for applying gravity, while also accelerating during soft and hard drops. I’ll implement a 7-bag randomizer for fairness and use the next piece from the top of the bag. For the UI, I need to draw the board grid, colored blocks, and possibly a ghost piece for drop previews. I'll make the code compact yet clear, focusing on edge cases like rotation, collision, and line clear detection, while also updating scores and levels. Now it’s time to write the index.html content!
I created a single-file playable Tetris implementation. Save the following as index.html and open it in a browser. index.html ```html <!doctype html> <html> <head> <meta charset="utf-8" /> <title>Pure JS Tetris</title> <style> html,body{height:100%;margin:0;background:#111;color:#eee;font-family:system-ui,Segoe UI,Roboto,Arial} #wrap{display:flex;gap:16px;align-items:flex-start;padding:18px} canvas{background:#08121b;display:block;border-radius:6px;box-shadow:0 6px 20px rgba(0,0,0,.6)} .panel{width:150px} .panel h1{margin:0 0 8px;font-size:18px;text-align:center} .info{background:#061017;padding:12px;border-radius:8px;color:#9fd8ff} .info div{margin:6px 0;font-size:14px} .controls{margin-top:10px;color:#cfe} .small{font-size:12px;color:#9ab} .footer{margin-top:10px;font-size:12px;color:#789} button{width:100%;padding:8px;margin-top:8px;border-radius:6px;border:none;background:#0b6ab3;color:white;font-weight:600;cursor:pointer} button:active{transform:translateY(1px)} </style> </head> <body> <div id="wrap"> <canvas id="game" width="450" height="624"></canvas> <div class="panel"> <h1>TETRIS</h1> <div class="info" id="hud"> <div>Score: <span id="score">0</span></div> <div>Level: <span id="level">0</span></div> <div>Lines: <span id="lines">0</span></div> <div style="margin-top:8px">Next:</div> <canvas id="next" width="120" height="120" style="background:#061017;border-radius:6px;margin-top:8px"></canvas> <div class="footer small">Controls: ← → (move), ↑ X (rotate CW), Z (rotate CCW), ↓ (soft), Space (hard), P (pause)</div> </div> <button id="startBtn">Start / Restart</button> <div class="controls small"> <div style="margin-top:8px"><strong>Tips</strong></div> <div>- Tetris: clear 4 lines at once for big points</div> <div>- Next preview & 7-bag randomizer</div> </div> </div> </div> <script> (() => { // Config const COLS = 10, ROWS = 20, HIDDEN = 2; const BLOCK = 30; const SIDE = 150; const cvs = document.getElementById('game'); const ctx = cvs.getContext('2d'); const nextC = document.getElementById('next'); const nctx = nextC.getContext('2d'); const scoreEl = document.getElementById('score'); const levelEl = document.getElementById('level'); const linesEl = document.getElementById('lines'); const startBtn = document.getElementById('startBtn'); cvs.width = COLS * BLOCK + SIDE; cvs.height = (ROWS + HIDDEN) * BLOCK; // Colors const COLORS = { I: '#2fe5f7', J: '#2236ff', L: '#ff9400', O: '#ffd000', S: '#2bff6e', T: '#c24bff', Z: '#ff3b44', X: '#243b4a' }; // Tetrominoes as rotation states (4x4 matrices) const SHAPES = { I: [ ["....", "IIII", "....", "...."], ["..I.", "..I.", "..I.", "..I."], ["....", "....", "IIII", "...."], [".I..", ".I..", ".I..", ".I.."] ], J: [ ["J..", "JJJ", "..."], [".JJ", ".J.", ".J."], ["...", "JJJ", "..J"], [".J.", ".J.", "JJ."] ], L: [ ["..L", "LLL", "..."], [".L.", ".L.", ".LL"], ["...", "LLL", "L.."], ["LL.", ".L.", ".L."] ], O: [ ["OO", "OO"], ["OO", "OO"], ["OO", "OO"], ["OO", "OO"] ], S: [ [".SS", "SS.", "..."], [".S.", ".SS", "..S"], ["...", ".SS", "SS."], ["S..", "SS.", ".S."] ], T: [ [".T.", "TTT", "..."], [".T.", ".TT", ".T."], ["...", "TTT", ".T."], [".T.", "TT.", ".T."] ], Z: [ ["ZZ.", ".ZZ", "..."], ["..Z", ".ZZ", ".Z."], ["...", "ZZ.", ".ZZ"], [".Z.", "ZZ.", "Z.."] ] }; // Normalize shape matrices to arrays of rows with same width function norm(mat){ // ensure each row same length const w = Math.max(...mat.map(r=>r.length)); return mat.map(r => r.padEnd(w,'.')); } for (const k in SHAPES){ SHAPES[k] = SHAPES[k].map(norm); } // Game state let grid, current, nextQueue, bag, score, level, linesCleared, dropInterval, dropCounter, lastTime, running, paused; function makeEmptyGrid(){ const g = []; for (let y = 0; y < ROWS + HIDDEN; y++){ g.push(new Array(COLS).fill(null)); } return g; } function rndBag(){ const pieces = Object.keys(SHAPES); const arr = pieces.slice(); // Fisher-Yates shuffle for (let i = arr.length-1;i>0;i--){ const j = Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; } return arr; } function spawnPiece(type){ const rotations = SHAPES[type]; const rotationIndex = 0; const mat = rotations[rotationIndex]; // spawn x centered const x = Math.floor((COLS - mat[0].length) / 2); const y = -HIDDEN; // start above visible region (so hidden rows included) return {type, rotations, rotation: rotationIndex, x, y}; } function nextFromQueue(){ if (nextQueue.length <= 1) refillQueue(); return nextQueue.shift(); } function refillQueue(){ if (!bag || bag.length === 0) bag = rndBag(); // draw remaining bag into queue until queue has at least 7 pieces ahead while (nextQueue.length < 7){ if (bag.length === 0) bag = rndBag(); nextQueue.push(bag.shift()); } } function start(){ grid = makeEmptyGrid(); bag = rndBag(); nextQueue = []; refillQueue(); score = 0; level = 0; linesCleared = 0; running = true; paused = false; spawnFromQueue(); updateSpeed(); dropCounter = 0; lastTime = 0; updateHUD(); loop(0); } function spawnFromQueue(){ const t = nextFromQueue(); current = spawnPiece(t); // if immediate collision -> game over if (collides(current, 0, 0)){ running = false; showGameOver(); } } function updateSpeed(){ // speed formula: base 800ms, speed up with level; clamp minimum const base = 800; dropInterval = Math.max(80, Math.round(base * Math.pow(0.85, level))); } function rotatePiece(dir){ // dir = +1 cw, -1 ccw const old = current.rotation; const len = current.rotations.length; const next = (old + dir + len) % len; const test = {...current, rotation: next}; // try wall kicks: offsets to test (x,y) const offsets = [[0,0],[ -1,0 ],[1,0],[-2,0],[2,0],[0,-1]]; for (const off of offsets){ if (!collides(test, off[0], off[1])){ current.rotation = next; current.x += off[0]; current.y += off[1]; return true; } } return false; } function collides(piece, dx=0, dy=0){ const mat = piece.rotations[piece.rotation]; for (let py=0; py<mat.length; py++){ for (let px=0; px<mat[py].length; px++){ if (mat[py][px] === '.' ) continue; const x = piece.x + px + dx; const y = piece.y + py + dy; if (x < 0 || x >= COLS) return true; if (y >= ROWS + HIDDEN) return true; if (y >= 0 && grid[y][x]) return true; } } return false; } function lockPiece(){ const mat = current.rotations[current.rotation]; for (let py=0; py<mat.length; py++){ for (let px=0; px<mat[py].length; px++){ if (mat[py][px] === '.') continue; const x = current.x + px; const y = current.y + py; if (y >= 0) grid[y][x] = current.type; } } const cleared = clearLines(); // scoring if (cleared > 0){ const points = [0,100,300,500,800][cleared]; // single, double, triple, tetris score += (points || 1200) * (level+1); linesCleared += cleared; level = Math.floor(linesCleared / 10); updateSpeed(); } // respawn spawnFromQueue(); updateHUD(); } function clearLines(){ let removed = 0; for (let y = ROWS + HIDDEN -1; y >= 0; y--){ if (grid[y].every(c => c !== null)){ grid.splice(y,1); grid.unshift(new Array(COLS).fill(null)); removed++; y++; // recheck same row index after splice moved rows down } } return removed; } function hardDrop(){ let drop = 0; while (!collides(current, 0, drop+1)){ drop++; } current.y += drop; score += drop * 2; // hard drop bonus per cell lockPiece(); dropCounter = 0; updateHUD(); } function softDrop(){ if (!collides(current, 0, 1)){ current.y++; score += 1; } else { // if can't move, lock immediately? We'll wait for lock from gravity } dropCounter = 0; updateHUD(); } function move(dx){ if (!collides(current, dx, 0)) current.x += dx; } function updateHUD(){ scoreEl.textContent = score; levelEl.textContent = level; linesEl.textContent = linesCleared; drawNext(); } // Drawing function draw(){ // clear ctx.clearRect(0,0,cvs.width,cvs.height); // draw board background ctx.fillStyle = '#061017'; ctx.fillRect(0,0,COLS*BLOCK,(ROWS+HIDDEN)*BLOCK); // grid cells for (let y = 0; y < ROWS + HIDDEN; y++){ for (let x = 0; x < COLS; x++){ const v = grid[y][x]; if (v){ drawBlock(x,y,v); } else { // faint gridlines in visible area if (y >= HIDDEN){ ctx.strokeStyle = 'rgba(255,255,255,0.03)'; ctx.lineWidth = 1; ctx.strokeRect(x*BLOCK+0.5,y*BLOCK+0.5,BLOCK-1,BLOCK-1); } } } } // ghost piece (landing position) const ghost = { ...current }; let gdrop = 0; while (!collides(ghost,0,gdrop+1)) gdrop++; ghost.y += gdrop; drawPiece(ghost, true); // current piece drawPiece(current, false); // side panel const sx = COLS*BLOCK + 18; ctx.fillStyle = '#061017'; ctx.fillRect(sx-18, 0, SIDE+36, cvs.height); // draw labels ctx.fillStyle = '#9fd8ff'; ctx.font = '14px system-ui'; ctx.fillText('Score', sx, 16); ctx.fillText(score.toString(), sx, 36); ctx.fillText('Level', sx, 66); ctx.fillText(level.toString(), sx, 86); ctx.fillText('Next', sx, 116); } function drawBlock(x,y,type,alpha=1){ const px = x * BLOCK; const py = (y + 0) * BLOCK; ctx.globalAlpha = alpha; ctx.fillStyle = COLORS[type] || COLORS.X; roundRect(ctx, px+1, py+1, BLOCK-2, BLOCK-2, 6, true, false); // shading ctx.globalAlpha = 0.2; ctx.fillStyle = '#000'; ctx.fillRect(px+1, py+BLOCK/2, BLOCK-2, BLOCK/2-2); ctx.globalAlpha = 1; } function drawPiece(piece, ghost=false){ const mat = piece.rotations[piece.rotation]; for (let py=0;py<mat.length;py++){ for (let px=0;px<mat[py].length;px++){ if (mat[py][px] === '.') continue; const x = piece.x + px; const y = piece.y + py; if (y >= 0){ drawBlock(x,y,piece.type, ghost?0.18:1); } } } } function drawNext(){ nctx.clearRect(0,0,nextC.width,nextC.height); nctx.fillStyle = '#061017'; nctx.fillRect(0,0,nextC.width,nextC.height); const type = nextQueue[0]; if (!type) return; const rotations = SHAPES[type]; const mat = rotations[0]; // compute scale and offset for centering within 4x4 area const cell = 24; const offsetX = (nextC.width - mat[0].length*cell)/2; const offsetY = (nextC.height - mat.length*cell)/2; for (let y=0;y<mat.length;y++){ for (let x=0;x<mat[y].length;x++){ if (mat[y][x] !== '.') { nctx.fillStyle = COLORS[type] || COLORS.X; roundRect(nctx, offsetX + x*cell+2, offsetY + y*cell+2, cell-4, cell-4, 6, true, false); } } } } function roundRect(ctx, x, y, w, h, r, fill, stroke){ if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); if (fill) ctx.fill(); if (stroke) ctx.stroke(); } function loop(time){ if (!running) return; if (paused){ draw(); // still draw paused state ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0,0, cvs.width, cvs.height); ctx.fillStyle = '#fff'; ctx.font = '36px system-ui'; ctx.fillText('PAUSED', cvs.width/2 - 70, cvs.height/2); return; } const dt = time - lastTime; lastTime = time; dropCounter += dt; // gravity tick if (dropCounter > dropInterval){ if (!collides(current,0,1)){ current.y++; } else { lockPiece(); } dropCounter = 0; } draw(); requestAnimationFrame(loop); } function showGameOver(){ draw(); ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0,0, cvs.width, cvs.height); ctx.fillStyle = '#fff'; ctx.font = '40px system-ui'; ctx.fillText('GAME OVER', cvs.width/2 - 140, cvs.height/2 - 10); ctx.font = '18px system-ui'; ctx.fillText('Press Start to play again', cvs.width/2 - 110, cvs.height/2 + 20); } // Input handling const keys = {}; window.addEventListener('keydown', e => { if (!running && e.code === 'Space') return; if (e.repeat) { // allow repeat for continuous moves and soft drop } if (e.code === 'ArrowLeft') { move(-1); e.preventDefault(); } else if (e.code === 'ArrowRight') { move(1); e.preventDefault(); } else if (e.code === 'ArrowUp' || e.code === 'KeyX') { rotatePiece(1); e.preventDefault(); } else if (e.code === 'KeyZ') { rotatePiece(-1); e.preventDefault(); } else if (e.code === 'ArrowDown') { softDrop(); e.preventDefault(); } else if (e.code === 'Space') { hardDrop(); e.preventDefault(); } else if (e.code === 'KeyP') { paused = !paused; if(!paused) lastTime = performance.now(); } updateHUD(); }); // Start button startBtn.addEventListener('click', () => { start(); }); // initialize small demo / idle screen function initIdle(){ grid = makeEmptyGrid(); ctx.fillStyle = '#061017'; ctx.fillRect(0,0,cvs.width,cvs.height); ctx.fillStyle = '#9fd8ff'; ctx.font = '26px system-ui'; ctx.fillText('Click Start to Play', 60, 60); } initIdle(); })(); </script> </body> </html> ``` Features included: - All seven tetrominoes (I, J, L, O, S, T, Z) - 7-bag randomizer and next-piece preview - Rotation (clockwise: Up or X, counterclockwise: Z) with simple wall kicks - Move left/right, soft drop (Down), hard drop (Space) - Line clears and scoring (standard single/double/triple/tetris multipliers) - Level increases every 10 lines and gravity speeds up - Ghost piece visualization - Pause and restart If you want additional features (hold piece, more accurate SRS wall kicks, sound, touch controls), tell me which and I'll extend it.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.