Tetris · seed-2.0-mini
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
Got it, let's tackle this. The user wants a full Tetris game in a single index.html file, no external resources. First, I need to outline all the parts:
1. HTML structure: a canvas for the main game, a canvas for the next piece preview, some score display, controls maybe? Wait, keep it clean, use keyboard controls (arrow keys, space for rotate, maybe down for soft drop).
2. CSS: style the containers, center everything, make it look decent, responsive? Maybe, but keep it simple.
3. JS:
- Define the tetromino shapes, colors, rotations. The seven standard ones: I, O, T, L, J, S, Z.
- Game board dimensions: usually 10 columns wide, 20 rows tall.
- Next piece preview: usually 4x4 grid.
- Game state: current piece, next piece, board state, score, level, speed (drop interval decreasing as level increases).
- Collision detection: check if a piece is out of bounds or overlapping with locked pieces.
- Rotation: rotate the piece, check for wall kicks? Wait, basic rotation first, maybe simple rotation without wall kicks first, but maybe add that later? No, start with basic, but make rotation work.
- Line clearing: check each row, clear full lines, update score, add lines to the level, increase speed.
- Spawning new pieces: when current piece locks, spawn next piece, check for game over (if new piece collides immediately).
- Keyboard input handlers: left/right move, rotate, soft drop, hard drop?
- Game loop: use requestAnimationFrame, track time since last drop, move piece down when interval passes.
Wait, first, let's structure the HTML: a wrapper div with the game area, next piece preview, score display. Let's set the main canvas to 300px wide (10 cols * 30px each) and 600px tall (20 rows *30px). The preview canvas can be 120x120 (4*30). Then a score div, level div, etc.
CSS: style the body to have a dark background, center everything, use a nice font like sans-serif. The game container should be flex, gap between main canvas and preview/score.
Now the JS part:
First, define the tetrominoes. Each shape is an array of rotation states, each state is an array of [x,y] coordinates relative to the origin (usually the top-left of the piece's bounding box). Wait, or the origin is the pivot point. For example, the I piece: its rotations are [[0,1], [1,1], [2,1], [3,1]] for horizontal, then [[1,0], [1,1], [1,2], [1,3]] for vertical, etc. Also, each has a color: I is cyan, O is yellow, T is purple, L is orange, J is blue, S is green, Z is red.
Then the game board: a 2D array, 20 rows, 10 columns, initialized to null (empty) or the color when filled.
Current piece: position (x, y) which is the top-left of the piece's bounding box? Wait no, better to have the pivot point. Wait, maybe for each piece, we have a rotation index, and the offset from the pivot. Alternatively, track the piece's origin (x, y) which is the top-left corner of the bounding box of the current rotation.
Wait, let's make it simple: each tetromino is defined as an array of blocks, each block is [dx, dy] relative to the piece's origin (the top-left of the piece's bounding box). Wait no, actually, for rotation, the pivot is important. For most pieces, the pivot is the center, except I and O? Wait O is 2x2, so its pivot is the top-left? No, O doesn't rotate, right, because it's a square. Oh right, O's shape is the same no matter rotation.
Wait let's list all seven tetrominoes properly:
const TETROMINOES = [
// I
{
shapes: [
[[0, 1], [1, 1], [2, 1], [3, 1]], // horizontal
[[1, 0], [1, 1], [1, 2], [1, 3]] // vertical
],
color: '#00ffff'
},
// O
{
shapes: [[[0,0], [0,1], [1,0], [1,1]]],
color: '#ffff00'
},
// T
{
shapes: [
[[0,1], [1,0], [1,1], [2,1]], // down
[[1,0], [1,1], [1,2], [2,1]], // right
[[0,1], [1,1], [2,1], [1,2]], // up
[[1,0], [1,1], [1,2], [0,1]] // left
],
color: '#aa00ff'
},
// L
{
shapes: [
[[0,1], [1,1], [2,1], [2,0]], // down
[[1,0], [1,1], [1,2], [2,2]], // right
[[0,1], [1,1], [2,1], [0,2]], // up
[[1,0], [1,1], [1,2], [0,0]] // left
],
color: '#ff8800'
},
// J
{
shapes: [
[[0,1], [1,1], [2,1], [0,0]], // down
[[1,0], [1,1], [1,2], [2,0]], // right
[[0,1], [1,1], [2,1], [2,2]], // up
[[1,0], [1,1], [1,2], [0,2]] // left
],
color: '#0000ff'
},
// S
{
shapes: [
[[1,0], [2,0], [0,1], [1,1]], // horizontal
[[1,0], [1,1], [2,1], [2,2]] // vertical
],
color: '#00ff00'
},
// Z
{
shapes: [
[[0,0], [1,0], [1,1], [2,1]], // horizontal
[[2,0], [1,1], [2,1], [1,2]] // vertical
],
color: '#ff0000'
}
];
Yes, that's correct. Each shape has multiple rotation states, except O which has one.
Then, the game board: COLS = 10, ROWS = 20. Each cell is null or a color string.
Next, the current piece: we need to track its type, rotation index, x position (column offset from left of board), y position (row offset from top of board). Wait, x is the column of the pivot? Or wait, let's take the origin as the top-left of the bounding box of the current shape. For example, the I piece horizontal shape has a bounding box of 4 columns wide, 1 row tall. So x is the leftmost column of the piece, y is the topmost row. So when we spawn the I piece, x is COLS/2 - 2 (since 4 columns wide, center at 5, so 5-2=3, so left edge at 3, spans 3,4,5,6). Wait yes, COLS is 10, so columns 0-9, so center is 4.5, so for 4 wide, start at 4-2=2? Wait 2,3,4,5: that's 4 columns, centered. Oh right, 10 columns, so middle is index 4 and 5? Wait no, 0 to 9, so 5 is the 6th column, so center is between 4 and5. So for a 4-wide piece, x should be 3, so the pieces are at columns 3,4,5,6: that's centered.
Wait let's calculate spawn x: for a given shape, find the minimum dx across all blocks, then subtract that from the spawn position? No, easier to just spawn the piece at the top center, adjusted for the shape's width. Alternatively, for all pieces, spawn at x = Math.floor(COLS/2) - 2 for 4-wide pieces, but O is 2x2, so Math.floor(COLS/2)-1, which would be 5-1=4, so x=4, y=0. Wait yes, that works for most pieces.
Then, the game loop: we need to track the time since the last drop, so we can move the piece down at regular intervals. The initial drop speed can be, say, 1000ms per line, and each level increases the speed: level 1 is 1000ms, level 2 900, etc., down to a minimum like 100ms.
Score: clearing 1 line gives 100 * level, 2 lines 300, 3 lines 500, 4 lines (tetris) 800 * level? That's the standard Tetris score system.
Next piece preview: a separate canvas that draws the next piece, scaled appropriately.
Keyboard controls:
- Left arrow: move piece left
- Right arrow: move piece right
- Down arrow: soft drop (move down one row, add a small score, like 1 point per soft drop)
- Up arrow or space: rotate the piece
- Space maybe hard drop: move the piece all the way down immediately, add score equal to the number of lines dropped.
Collision detection: for each block of the current piece, check if its position (x + dx, y + dy) is outside the board (x <0, x >= COLS, y >= ROWS) or if the board at that position is already filled (not null). Also, when moving down, check if y + dy +1 >= ROWS or board[y+dy+1][x+dx] is not null.
Line clearing: after a piece locks, iterate through each row, check if all columns in the row are filled. If so, remove that row, add all rows above it down by one, increment the line count, update score and level.
Game over condition: when spawning a new piece, it collides immediately (meaning the top of the board is filled with locked pieces).
Now, let's put all this together into a single index.html file.
Wait, first, the HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title>
<style>
/* CSS here */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Arial', sans-serif;
}
body {
background-color: #1a1a1a;
color: white;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
gap: 20px;
}
.game-container {
display: flex;
gap: 40px;
align-items: flex-start;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.preview-container {
background-color: #2d2d2d;
padding: 10px;
border-radius: 8px;
}
.preview-container h3 {
text-align: center;
margin-bottom: 10px;
}
#nextPieceCanvas {
background-color: #1a1a1a;
border-radius: 4px;
}
#gameCanvas {
background-color: #2d2d2d;
border-radius: 4px;
}
.score-board {
background-color: #2d2d2d;
padding: 15px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 10px;
}
.score-board div {
font-size: 18px;
}
.controls {
background-color: #2d2d2d;
padding: 15px;
border-radius: 8px;
font-size: 14px;
}
</style>
</head>
<body>
<h1>Tetris</h1>
<div class="game-container">
<canvas id="gameCanvas" width="300" height="600"></canvas>
<div class="sidebar">
<div class="preview-container">
<h3>Next Piece</h3>
<canvas id="nextPieceCanvas" width="120" height="120"></canvas>
</div>
<div class="score-board">
<div>Score: <span id="score">0</span></div>
<div>Lines: <span id="lines">0</span></div>
<div>Level: <span id="level">1</span></div>
</div>
<div class="controls">
<p>Controls:</p>
<p>←→: Move</p>
<p>↓: Soft Drop</p>
<p>Space/↑: Rotate</p>
</div>
</div>
</div>
<script>
// JS code here
</script>
</body>
</html>
Now the JS part:
First, get the canvas elements and contexts:
const gameCanvas = document.getElementById('gameCanvas');
const gameCtx = gameCanvas.getContext('2d');
const nextCanvas = document.getElementById('nextPieceCanvas');
const nextCtx = nextCanvas.getContext('2d');
Then define the constants:
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30; // 30px per block, matches canvas size 30*10=300, 30*20=600
const NEXT_BLOCK_SIZE = 30; // same size, preview is 4x4 = 120px
Then the tetrominoes as before:
const TETROMINOES = [
// I
{
shapes: [
[[0, 1], [1, 1], [2, 1], [3, 1]],
[[1, 0], [1, 1], [1, 2], [1, 3]]
],
color: '#00ffff'
},
// O
{
shapes: [[[0,0], [0,1], [1,0], [1,1]]],
color: '#ffff00'
},
// T
{
shapes: [
[[0,1], [1,0], [1,1], [2,1]],
[[1,0], [1,1], [1,2], [2,1]],
[[0,1], [1,1], [2,1], [1,2]],
[[1,0], [1,1], [1,2], [0,1]]
],
color: '#aa00ff'
},
// L
{
shapes: [
[[0,1], [1,1], [2,1], [2,0]],
[[1,0], [1,1], [1,2], [2,2]],
[[0,1], [1,1], [2,1], [0,2]],
[[1,0], [1,1], [1,2], [0,0]]
],
color: '#ff8800'
},
// J
{
shapes: [
[[0,1], [1,1], [2,1], [0,0]],
[[1,0], [1,1], [1,2], [2,0]],
[[0,1], [1,1], [2,1], [2,2]],
[[1,0], [1,1], [1,2], [0,2]]
],
color: '#0000ff'
},
// S
{
shapes: [
[[1,0], [2,0], [0,1], [1,1]],
[[1,0], [1,1], [2,1], [2,2]]
],
color: '#00ff00'
},
// Z
{
shapes: [
[[0,0], [1,0], [1,1], [2,1]],
[[2,0], [1,1], [2,1], [1,2]]
],
color: '#ff0000'
}
];
Then game state variables:
let board = [];
let currentPiece = null;
let nextPiece = null;
let score = 0;
let linesCleared = 0;
let level = 1;
let dropTime = 0;
let dropInterval = 1000; // ms per drop
let lastTime = 0;
Initialize the board:
function initBoard() {
for (let r = 0; r < ROWS; r++) {
board[r] = [];
for (let c = 0; c < COLS; c++) {
board[r][c] = null;
}
}
}
Spawn a new piece: pick a random tetromino, set its rotation to 0, spawn at the top center.
function spawnPiece() {
const tetromino = TETROMINOES[Math.floor(Math.random() * TETROMINOES.length)];
const shape = tetromino.shapes[0];
// Calculate spawn x: center the piece
let minX = Math.min(...shape.map(b => b[0]));
let maxX = Math.max(...shape.map(b => b[0]));
let spawnX = Math.floor(COLS / 2) - Math.floor((maxX - minX + 1)/2);
return {
tetromino,
rotation: 0,
x: spawnX,
y: 0
};
}
Wait, that's better than hardcoding, because it adjusts for the shape's width. For example, the I piece has maxX - minX +1 = 4, so (4)/2 is 2, so spawnX is 5 -2 =3, which is correct.
Collision detection function: check if a piece at (x, y) with rotation r collides with the board or walls.
function checkCollision(piece, offsetX = 0, offsetY = 0) {
const shape = piece.tetromino.shapes[piece.rotation % piece.tetromino.shapes.length];
for (const [dx, dy] of shape) {
const x = piece.x + dx + offsetX;
const y = piece.y + dy + offsetY;
// Check out of bounds
if (x < 0 || x >= COLS || y >= ROWS) return true;
// Check if y is above the board (allowed)
if (y < 0) continue;
// Check if board cell is filled
if (board[y][x] !== null) return true;
}
return false;
}
Wait, if y is negative, that's okay, because the piece can be partially above the board when spawning.
Rotate a piece: increment the rotation index, then check for collision. If collides, maybe adjust x position (wall kick)? But let's keep it simple first: if rotated piece collides, don't rotate. Wait, but standard Tetris has wall kicks, but for simplicity, maybe just don't rotate if it collides, but let's add a small wall kick: try shifting left, right, up if collision happens. Alternatively, just skip rotation if collision. Let's do basic first:
function rotatePiece() {
const oldRotation = currentPiece.rotation;
currentPiece.rotation = (currentPiece.rotation + 1) % currentPiece.tetromino.shapes.length;
// If collision, revert rotation
if (checkCollision(currentPiece)) {
currentPiece.rotation = oldRotation;
// Try wall kicks: shift left, right, up
// Try shift left by 1
currentPiece.x--;
if (!checkCollision(currentPiece)) return;
// Try shift right by 1
currentPiece.x += 2;
if (!checkCollision(currentPiece)) return;
// Revert x
currentPiece.x--;
// Try shift up by 1
currentPiece.y--;
if (!checkCollision(currentPiece)) return;
// Revert everything
currentPiece.y++;
currentPiece.rotation = oldRotation;
}
}
That's a simple wall kick: try left, right, up if rotation collides.
Move the piece left/right:
function movePiece(dirX) {
if (!checkCollision(currentPiece, dirX, 0)) {
currentPiece.x += dirX;
}
}
Soft drop: move down one row, add 1 to score.
function softDrop() {
if (!checkCollision(currentPiece, 0, 1)) {
currentPiece.y += 1;
score += 1;
updateScoreDisplay();
} else {
lockPiece();
}
}
Hard drop: move the piece all the way down, add score equal to the number of lines dropped.
function hardDrop() {
let dropAmount = 0;
while (!checkCollision(currentPiece, 0, dropAmount +1)) {
dropAmount++;
}
currentPiece.y += dropAmount;
score += dropAmount * 2;
updateScoreDisplay();
lockPiece();
}
Lock a piece: add its blocks to the board, then check for line clears, spawn new piece, check game over.
function lockPiece() {
const shape = currentPiece.tetromino.shapes[currentPiece.rotation];
const color = currentPiece.tetromino.color;
for (const [dx, dy] of shape) {
const x = currentPiece.x + dx;
const y = currentPiece.y + dy;
if (y >= 0) { // only add to board if not above the board
board[y][x] = color;
}
}
// Check for line clears
clearLines();
// Spawn next piece
currentPiece = nextPiece;
nextPiece = spawnPiece();
// Check game over: if new piece collides immediately
if (checkCollision(currentPiece)) {
alert(`Game Over! Final Score: ${score}`);
resetGame();
}
}
Clear completed lines:
function clearLines() {
let lines = 0;
// Iterate from bottom to top
for (let r = ROWS -1; r >=0; r--) {
if (board[r].every(cell => cell !== null)) {
// Remove this line, add all lines above down by one
board.splice(r, 1);
// Add a new empty line at the top
board.unshift(new Array(COLS).fill(null));
lines++;
// Since we removed a line, we need to check the same row again (now has the line from above)
r++;
}
}
if (lines > 0) {
linesCleared += lines;
// Update level: every 10 lines, level up
level = Math.floor(linesCleared /10) +1;
// Update drop interval: decrease by 50ms per level, min 100ms
dropInterval = Math.max(1000 - (level -1)*50, 100);
// Calculate score: https://tetris.fandom.com/wiki/Scoring
const lineScores = [0, 100, 300, 500, 800];
score += lineScores[lines] * level;
updateScoreDisplay();
}
}
Update the score, lines, level displays:
function updateScoreDisplay() {
document.getElementById('score').textContent = score;
document.getElementById('lines').textContent = linesCleared;
document.getElementById('level').textContent = level;
}
Draw the board:
function drawBoard() {
for (let r =0; r < ROWS; r++) {
for (let c=0; c < COLS; c++) {
const color = board[r][c];
gameCtx.fillStyle = color || '#2d2d2d';
gameCtx.fillRect(c * BLOCK_SIZE, r * BLOCK_SIZE, BLOCK_SIZE -1, BLOCK_SIZE -1); // -1 for gap between blocks
gameCtx.strokeStyle = '#444';
gameCtx.strokeRect(c * BLOCK_SIZE, r * BLOCK_SIZE, BLOCK_SIZE -1, BLOCK_SIZE -1);
}
}
}
Draw the current piece:
function drawCurrentPiece() {
const shape = currentPiece.tetromino.shapes[currentPiece.rotation];
const color = currentPiece.tetromino.color;
for (const [dx, dy] of shape) {
const x = (currentPiece.x + dx) * BLOCK_SIZE;
const y = (currentPiece.y + dy) * BLOCK_SIZE;
gameCtx.fillStyle = color;
gameCtx.fillRect(x, y, BLOCK_SIZE -1, BLOCK_SIZE -1);
gameCtx.strokeStyle = '#fff';
gameCtx.strokeRect(x, y, BLOCK_SIZE -1, BLOCK_SIZE -1);
}
}
Draw the next piece in the preview canvas:
function drawNextPiece() {
nextCtx.clearRect(0,0, nextCanvas.width, nextCanvas.height);
if (!nextPiece) return;
const shape = nextPiece.tetromino.shapes[0]; // use first rotation for preview
const color = nextPiece.tetromino.color;
// Calculate offset to center the preview
const minX = Math.min(...shape.map(b => b[0]));
const maxX = Math.max(...shape.map(b => b[0]));
const minY = Math.min(...shape.map(b => b[1]));
const maxY = Math.max(...shape.map(b => b[1]));
const offsetX = (nextCanvas.width - (maxX - minX +1)*NEXT_BLOCK_SIZE)/2 - minX*NEXT_BLOCK_SIZE;
const offsetY = (nextCanvas.height - (maxY - minY +1)*NEXT_BLOCK_SIZE)/2 - minY*NEXT_BLOCK_SIZE;
for (const [dx, dy] of shape) {
const x = dx * NEXT_BLOCK_SIZE + offsetX;
const y = dy * NEXT_BLOCK_SIZE + offsetY;
nextCtx.fillStyle = color;
nextCtx.fillRect(x, y, NEXT_BLOCK_SIZE -1, NEXT_BLOCK_SIZE -1);
nextCtx.strokeStyle = '#fff';
nextCtx.strokeRect(x, y, NEXT_BLOCK_SIZE -1, NEXT_BLOCK_SIZE -1);
}
}
Main draw function:
function drawGame() {
gameCtx.clearRect(0,0, gameCanvas.width, gameCanvas.height);
drawBoard();
drawCurrentPiece();
drawNextPiece();
}
Game loop:
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
if (deltaTime > dropInterval) {
softDrop();
lastTime = timestamp;
}
drawGame();
requestAnimationFrame(gameLoop);
}
Reset the game:
function resetGame() {
initBoard();
score =0;
linesCleared=0;
level=1;
dropInterval=1000;
currentPiece = spawnPiece();
nextPiece = spawnPiece();
updateScoreDisplay();
lastTime=0;
}
Keyboard event handler:
function handleKeyDown(e) {
switch(e.key) {
case 'ArrowLeft':
movePiece(-1);
break;
case 'ArrowRight':
movePiece(1);
break;
case 'ArrowDown':
softDrop();
break;
case 'ArrowUp':
case ' ':
e.preventDefault(); // prevent space from scrolling
rotatePiece();
break;
case 'Space':
e.preventDefault();
hardDrop();
break;
}
}
Now, add event listeners, initialize the game:
document.addEventListener('keydown', handleKeyDown);
resetGame();
requestAnimationFrame(gameLoop);
Wait, wait, in the handleKeyDown, 'Space' is a separate case? No, ' ' is Space, so the case 'ArrowUp': case ' ': would cover both, right? Because in JS, you can list multiple cases like that. Oh right, I had case 'ArrowUp': case ' ':, then prevent default, then rotatePiece. Then case 'Space' is redundant, but actually, e.key for space is ' ', so that's okay. Also, when you press space, it does both rotate and hard drop? Oh no, wait no, I had:
Wait the code:
case 'ArrowUp':
case ' ':
e.preventDefault(); // prevent space from scrolling
rotatePiece();
break;
case 'Space':
e.preventDefault();
hardDrop();
break;
Wait no, that's wrong, because 'Space' is the same as ' ', so that case will never be hit. Oh right, e.key for space bar is ' ', not 'Space'. So I should remove the 'Space' case, and just have ' ', and for hard drop, maybe use another key? Or wait, hard drop is usually space held? No, wait standard Tetris: up arrow or X rotate, space hard drop. Oh right, so let's adjust: let's make space for hard drop, and up arrow or Z for rotate. Wait let's fix that:
Change the handleKeyDown:
case 'ArrowLeft':
movePiece(-1);
break;
case 'ArrowRight':
movePiece(1);
break;
case 'ArrowDown':
softDrop();
break;
case 'ArrowUp':
case 'z':
case 'Z':
e.preventDefault();
rotatePiece();
break;
case ' ':
e.preventDefault();
hardDrop();
break;
That's better, because space is hard drop, up arrow or Z rotate.
Also, in the resetGame function, we call initBoard(), which initializes the board array correctly.
Wait let's test for possible issues: when the piece is at the bottom, lockPiece() is called, which adds the pieces to the board, then clears lines, then spawns new currentPiece from nextPiece, then spawns new nextPiece. That's correct.
Another thing: the drawBoard function uses BLOCK_SIZE-1 for the rect, so there's a small gap between blocks, which looks nicer.
Also, the next piece preview: we calculate the offset to center the piece, which is good.
Now, let's put all this JS code into the script tag.
Wait also, when we lock the piece, if y is negative, we skip adding to the board? But that's only possible if the piece is spawning, but when locking, the piece has already fallen into place, so y should be >=0 except when spawning, but lockPiece is called when the piece can't move down further, so y+dy +1 >= ROWS, so y+dy >= ROWS-1, so y is okay. Wait no, the [dx, dy] are relative to the piece's x and y, so currentPiece.y + dy could be negative? No, because when spawning, the piece's y is 0, and dy is at least 0 for most shapes? Wait no, for example, the T piece's first shape has dy values 0,1,1,1. So currentPiece.y is 0, so dy can be 0 or 1, so y+dy is 0 or 1, which is okay. Wait some shapes have dy negative? No, all the shapes we defined have dy >=0, because the first shape of each tetromino is the "default" orientation with the topmost row at dy=0. Oh right, yes, all the shapes we defined have dy starting from 0, so the minimum dy is 0, so currentPiece.y + dy >= currentPiece.y >=0, so the check if (y >=0) is redundant, but it's okay to leave it in.
Another thing: when we splice the board when clearing lines, we have to make sure that we don't go out of bounds. Since we iterate from r=ROWS-1 down to 0, and when we splice r, we add 1 to r to check the same row again, which is correct because after splicing, the row r now has the row that was above it.
Now, let's test the score calculation: lineScores[lines] is 0 for 0 lines, 100 for 1, 300 for 2, 500 for3, 800 for4, multiplied by level, that's correct per the TTC scoring system.
Now, let's put all the code together into the index.html file.
Wait also, in the drawGame function, we first clear the game canvas, then draw the board, then the current piece, then the next piece. That's correct.
Another thing: the initial call to resetGame() sets up currentPiece and nextPiece, which is correct.
Now, let's write the full index.html file, combining all the parts.
Wait let's check for any missing parts:
- Did we handle the case where the current piece is null? No, because resetGame() sets currentPiece to spawnPiece(), so it's never null.
- The nextPiece is set in resetGame() as well, so drawNextPiece() works.
Yes.
Now, let's write the full code:
Wait let's combine all the CSS and JS into the single file. Let's make sure there are no syntax errors.
Wait in the initBoard function:
function initBoard() {
for (let r = 0; r < ROWS; r++) {
board[r] = [];
for (let c = 0; c < COLS; c++) {
board[r][c] = null;
}
}
}
Yes, that's correct.
In the spawnPiece function:
const minX = Math.min(...shape.map(b => b[0]));
const maxX = Math.max(...shape.map(b => b[0]));
let spawnX = Math.floor(COLS / 2) - Math.floor((maxX - minX + 1)/2);
Wait for the O piece, shape is [[0,0], [0,1], [1,0], [1,1]], so minX=0, maxX=1, so (1-0+1)/2 = 1, so Math.floor(1) is 1, so spawnX = 5 -1=4, which is correct, since O piece is at x=4, y=0, so blocks at (4,0), (4,1), (5,0), (5,1), which is centered.
Yes.
Another test: I piece, minX=0, maxX=3, so (3-0+1)/2=2, spawnX=5-2=3, which is correct, blocks at 3,4,5,6.
Perfect.
Now, let's write the full index.html file:/tmp/ws/index.html (11022 bytes)Successfully wrote 11022 bytes to /tmp/ws/index.html
thinking
Great, the file was written successfully. Now let's verify that it's a complete, self-contained Tetris game. Let's check: - All CSS is in a <style> tag, no external resources. - All JS is in a <script> tag, no external imports. - It has all the required features: seven tetrominoes, rotation, line clears, increasing speed, next-piece preview, score, level, controls. - The game runs fully sandboxed, no network requests. The user asked for a playable Tetris on a canvas with all those features, and this file delivers. Now I should confirm that the file is ready, and maybe mention how to run it: open index.html in a web browser.
Perfect! I've created a complete, self-contained Tetris game in `index.html` with all the requested features: 1. **All 7 standard tetrominoes** with correct colors and rotation 2. **Rotations** with simple wall kick detection 3. **Line clearing** with standard TTC scoring 4. **Increasing difficulty**: speed increases every level (every 10 lines cleared) 5. **Next-piece preview** panel 6. **Score, line count, and level tracking** 7. Full keyboard controls: - Arrow keys: move left/right, soft drop - Space: hard drop - Up/Z: rotate pieces To play the game: 1. Open `/tmp/ws/index.html` in any modern web browser 2. Use the keyboard controls to play! The game is fully sandboxed, no external resources or network requests required.
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 falling pieces, next-piece preview, score tracking, and responsive controls, but no lines cleared yet.
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.