← back to Model Arena
data/artifacts/04692f72ef1d/gpt.html
611 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tetris Single File</title>
<style>
body {
background:#111;
color:#eee;
font-family:Arial,Helvetica,sans-serif;
display:flex;
justify-content:center;
align-items:flex-start;
margin:0;
padding:10px;
}
#game-container {
display:flex;
gap:15px;
}
#side-panel {
display:flex;
flex-direction:column;
gap:10px;
font-size:14px;
}
canvas {
background:#000;
border:2px solid #444;
image-rendering:pixelated;
}
#info {
border:1px solid #444;
padding:6px 8px;
background:#181818;
}
.label {
color:#aaa;
}
.value {
font-weight:bold;
color:#fff;
}
#controls {
font-size:12px;
color:#ccc;
}
button {
padding:4px 8px;
background:#333;
border:1px solid #666;
color:#eee;
cursor:pointer;
}
button:hover {
background:#444;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game" width="200" height="400"></canvas>
<div id="side-panel">
<canvas id="next" width="80" height="80"></canvas>
<div id="info">
<div><span class="label">Score:</span> <span class="value" id="score">0</span></div>
<div><span class="label">Lines:</span> <span class="value" id="lines">0</span></div>
<div><span class="label">Level:</span> <span class="value" id="level">1</span></div>
</div>
<div id="controls">
<div><b>Controls</b></div>
<div>← → : Move</div>
<div>↑ : Rotate</div>
<div>↓ : Soft drop</div>
<div>Space : Hard drop</div>
<div>P : Pause/Resume</div>
<div>R : Restart</div>
</div>
<button id="restartBtn">Restart</button>
</div>
</div>
<script>
(function() {
const COLS = 10;
const ROWS = 20;
const BLOCK = 20;
const CANVAS_W = COLS*BLOCK;
const CANVAS_H = ROWS*BLOCK;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const nextCanvas = document.getElementById('next');
const nctx = nextCanvas.getContext('2d');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const scoreEl = document.getElementById('score');
const linesEl = document.getElementById('lines');
const levelEl = document.getElementById('level');
const restartBtn = document.getElementById('restartBtn');
const COLORS = {
0: "#000000",
I: "#00f0f0",
J: "#0000f0",
L: "#f0a000",
O: "#f0f000",
S: "#00f000",
T: "#a000f0",
Z: "#f00000"
};
// Tetromino shapes with rotation states (4x4 matrix each)
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]
]
],
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]
]
],
O: [
[
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0]
],
[
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0]
],
[
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0]
],
[
[0,1,1,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,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]
]
],
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]
]
],
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]
]
]
};
const TETROMINOES = Object.keys(SHAPES);
let grid, current, nextPiece, dropStart, dropInterval;
let score, lines, level;
let gameOver, paused;
let requestId;
function resetGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
const row = [];
for (let c = 0; c < COLS; c++) {
row.push(0);
}
grid.push(row);
}
}
function randomPiece() {
const type = TETROMINOES[Math.floor(Math.random() * TETROMINOES.length)];
return {
type,
rotation: 0,
x: 3,
y: -2,
shape: SHAPES[type][0]
};
}
function rotatePiece(p, dir=1) {
const len = SHAPES[p.type].length;
const newRot = (p.rotation + dir + len) % len;
return {
...p,
rotation: newRot,
shape: SHAPES[p.type][newRot]
};
}
function collide(p, offX = 0, offY = 0) {
const m = p.shape;
for (let r = 0; r < m.length; r++) {
for (let c = 0; c < m[r].length; c++) {
if (!m[r][c]) continue;
const nx = p.x + c + offX;
const ny = p.y + r + offY;
if (ny >= ROWS || nx < 0 || nx >= COLS) return true;
if (ny >= 0 && grid[ny][nx]) return true;
}
}
return false;
}
function merge(p) {
const m = p.shape;
for (let r = 0; r < m.length; r++) {
for (let c = 0; c < m[r].length; c++) {
if (!m[r][c]) continue;
const nx = p.x + c;
const ny = p.y + r;
if (ny >= 0 && ny < ROWS && nx >= 0 && nx < COLS) {
grid[ny][nx] = p.type;
}
}
}
}
function clearLines() {
let cleared = 0;
outer: for (let r = ROWS - 1; r >= 0; r--) {
for (let c = 0; c < COLS; c++) {
if (!grid[r][c]) continue outer;
}
grid.splice(r, 1);
grid.unshift(new Array(COLS).fill(0));
cleared++;
r++;
}
if (cleared > 0) {
lines += cleared;
const lineScores = [0, 40, 100, 300, 1200];
score += lineScores[cleared] * level;
const newLevel = 1 + Math.floor(lines / 10);
if (newLevel !== level) {
level = newLevel;
updateDropInterval();
}
updateHUD();
}
}
function updateHUD() {
scoreEl.textContent = score;
linesEl.textContent = lines;
levelEl.textContent = level;
}
function drawBlock(x, y, color, context = ctx, size = BLOCK) {
context.fillStyle = color;
context.fillRect(x * size, y * size, size, size);
context.strokeStyle = "#222";
context.lineWidth = 1;
context.strokeRect(x * size, y * size, size, size);
}
function drawGrid() {
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cell = grid[r][c];
if (cell) {
drawBlock(c, r, COLORS[cell]);
}
}
}
}
function drawPiece(p, ghost=false) {
const m = p.shape;
let gy = p.y;
if (ghost) {
while (!collide(p, 0, gy - p.y + 1)) gy++;
}
for (let r = 0; r < m.length; r++) {
for (let c = 0; c < m[r].length; c++) {
if (!m[r][c]) continue;
const nx = p.x + c;
const ny = (ghost ? gy : p.y) + r;
if (ny >= 0) {
const baseColor = COLORS[p.type];
let color = baseColor;
if (ghost) {
// simple ghost effect: darker
color = shadeColor(baseColor, -50);
}
drawBlock(nx, ny, color);
}
}
}
}
function shadeColor(col, amt) {
const num = parseInt(col.slice(1), 16);
let r = (num >> 16) + amt;
let g = ((num >> 8) & 0x00FF) + amt;
let b = (num & 0x0000FF) + amt;
r = Math.max(Math.min(255, r), 0);
g = Math.max(Math.min(255, g), 0);
b = Math.max(Math.min(255, b), 0);
return "#" + (r<<16 | g<<8 | b).toString(16).padStart(6,'0');
}
function drawNext() {
nctx.fillStyle = "#000";
nctx.fillRect(0,0,nextCanvas.width,nextCanvas.height);
const p = nextPiece;
const m = SHAPES[p.type][0];
const size = 16;
const cols = m[0].length;
const rows = m.length;
const offsetX = Math.floor((nextCanvas.width/size - cols)/2);
const offsetY = Math.floor((nextCanvas.height/size - rows)/2);
for (let r = 0; r < m.length; r++) {
for (let c = 0; c < m[r].length; c++) {
if (!m[r][c]) continue;
drawBlock(offsetX + c, offsetY + r, COLORS[p.type], nctx, size);
}
}
}
function draw() {
drawGrid();
if (current) {
drawPiece(current, true);
drawPiece(current, false);
}
if (gameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, CANVAS_H/2 - 40, CANVAS_W, 80);
ctx.fillStyle = "#fff";
ctx.font = "bold 20px Arial";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", CANVAS_W/2, CANVAS_H/2 - 5);
ctx.font = "14px Arial";
ctx.fillText("Press R to restart", CANVAS_W/2, CANVAS_H/2 + 18);
} else if (paused) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, CANVAS_H/2 - 30, CANVAS_W, 60);
ctx.fillStyle = "#fff";
ctx.font = "bold 18px Arial";
ctx.textAlign = "center";
ctx.fillText("PAUSED", CANVAS_W/2, CANVAS_H/2);
}
}
function updateDropInterval() {
// Simple speed curve: min 80ms
dropInterval = Math.max(80, 800 - (level-1)*70);
}
function newPiece() {
current = nextPiece || randomPiece();
nextPiece = randomPiece();
drawNext();
dropStart = performance.now();
if (collide(current, 0, 0)) {
gameOver = true;
}
}
function hardDrop() {
if (gameOver || paused) return;
while (!collide(current, 0, 1)) {
current.y++;
score += 1;
}
lockPiece();
}
function lockPiece() {
merge(current);
clearLines();
newPiece();
}
function move(dx, dy) {
if (gameOver || paused) return;
if (!collide(current, dx, dy)) {
current.x += dx;
current.y += dy;
} else if (dy === 1) {
lockPiece();
}
}
function rotateCurrent() {
if (gameOver || paused) return;
const rotated = rotatePiece(current, 1);
// basic wall kicks: try shifts -1, +1, -2, +2
const kicks = [0, -1, 1, -2, 2];
for (let k of kicks) {
if (!collide(rotated, k, 0)) {
rotated.x += k;
current = rotated;
return;
}
}
}
function togglePause() {
if (gameOver) return;
paused = !paused;
if (!paused) {
dropStart = performance.now();
loop();
} else {
if (requestId) cancelAnimationFrame(requestId);
draw();
}
}
function restart() {
resetGrid();
score = 0;
lines = 0;
level = 1;
updateDropInterval();
updateHUD();
gameOver = false;
paused = false;
nextPiece = randomPiece();
newPiece();
dropStart = performance.now();
loop();
}
function loop(timestamp) {
if (gameOver || paused) {
draw();
return;
}
if (!dropStart) dropStart = timestamp;
const delta = timestamp - dropStart;
if (delta > dropInterval) {
move(0,1);
dropStart = timestamp;
}
draw();
requestId = requestAnimationFrame(loop);
}
document.addEventListener('keydown', function(e) {
switch (e.key) {
case "ArrowLeft":
e.preventDefault(); move(-1,0); break;
case "ArrowRight":
e.preventDefault(); move(1,0); break;
case "ArrowDown":
e.preventDefault(); move(0,1); score += 1; updateHUD(); break;
case "ArrowUp":
e.preventDefault(); rotateCurrent(); break;
case " ":
e.preventDefault(); hardDrop(); updateHUD(); break;
case "p":
case "P":
e.preventDefault(); togglePause(); break;
case "r":
case "R":
e.preventDefault(); restart(); break;
}
});
restartBtn.addEventListener('click', function() {
restart();
});
// Init
resetGrid();
score = 0;
lines = 0;
level = 1;
updateDropInterval();
updateHUD();
nextPiece = randomPiece();
newPiece();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>