vibe-coding-tetris / index.html
keeperballon's picture
Update index.html
c5bccdd verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Tetris</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #6c5ce7;
--secondary-color: #a29bfe;
--accent-color: #fd79a8;
--dark-color: #2d3436;
--light-color: #f9f9f9;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
--border-radius: 10px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #dfe6e9, #b2bec3);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: var(--dark-color);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 1000px;
width: 100%;
background-color: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 20px;
}
@media (min-width: 768px) {
.container {
flex-direction: row;
justify-content: center;
gap: 40px;
padding: 40px;
}
}
h1 {
font-size: 2.5rem;
margin-bottom: 20px;
color: var(--primary-color);
text-align: center;
width: 100%;
}
.game-container {
position: relative;
}
.tetris-board {
border: 2px solid var(--primary-color);
border-radius: var(--border-radius);
background-color: #fff;
box-shadow: var(--shadow);
display: grid;
grid-template-rows: repeat(20, 1fr);
grid-template-columns: repeat(10, 1fr);
gap: 1px;
width: 300px;
height: 600px;
overflow: hidden;
}
.cell {
background-color: #f5f5f5;
border-radius: 2px;
}
.side-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.next-piece-container, .score-container, .level-container, .controls-container {
background-color: white;
border-radius: var(--border-radius);
padding: 15px;
box-shadow: var(--shadow);
width: 200px;
}
.next-piece-container h2, .score-container h2, .level-container h2, .controls-container h2 {
color: var(--primary-color);
font-size: 1.2rem;
margin-bottom: 10px;
text-align: center;
}
.next-piece-preview {
height: 100px;
display: grid;
grid-template-rows: repeat(4, 1fr);
grid-template-columns: repeat(4, 1fr);
gap: 1px;
margin: 0 auto;
}
.control-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: 10px 15px;
font-family: 'Poppins', sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: block;
width: 100%;
margin-bottom: 10px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
}
.control-btn:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
}
.control-btn:active {
transform: translateY(0);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}
.score, .level {
font-size: 1.5rem;
font-weight: 600;
color: var(--accent-color);
text-align: center;
}
.game-over-modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: var(--border-radius);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.game-over-modal.active {
opacity: 1;
pointer-events: all;
}
.game-over-modal h2 {
color: white;
font-size: 2rem;
margin-bottom: 20px;
}
.game-over-modal p {
color: white;
font-size: 1.2rem;
margin-bottom: 30px;
}
.keyboard-controls {
margin-top: 15px;
font-size: 0.9rem;
color: #666;
}
.keyboard-controls p {
margin: 5px 0;
}
.tetromino {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.I {
background-color: #00cec9;
}
.J {
background-color: #0984e3;
}
.L {
background-color: #e17055;
}
.O {
background-color: #fdcb6e;
}
.S {
background-color: #00b894;
}
.T {
background-color: #6c5ce7;
}
.Z {
background-color: #d63031;
}
@keyframes flash {
0%, 100% {
filter: brightness(1);
}
50% {
filter: brightness(1.5);
}
}
.flash-row {
animation: flash 0.2s 3;
}
@media (max-width: 767px) {
.tetris-board {
width: 280px;
height: 560px;
}
.side-panel {
margin-top: 20px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.next-piece-container, .score-container, .level-container, .controls-container {
width: calc(50% - 10px);
min-width: 130px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="game-container">
<h1>Modern Tetris</h1>
<div class="tetris-board" id="tetris-board"></div>
<div class="game-over-modal" id="game-over-modal">
<h2>Game Over</h2>
<p>Your score: <span id="final-score">0</span></p>
<button class="control-btn" id="restart-btn">Play Again</button>
</div>
</div>
<div class="side-panel">
<div class="next-piece-container">
<h2>Next Piece</h2>
<div class="next-piece-preview" id="next-piece-preview"></div>
</div>
<div class="score-container">
<h2>Score</h2>
<div class="score" id="score">0</div>
</div>
<div class="level-container">
<h2>Level</h2>
<div class="level" id="level">1</div>
</div>
<div class="controls-container">
<h2>Controls</h2>
<button class="control-btn" id="start-btn">Start / Pause</button>
<div class="keyboard-controls">
<p>← → : Move left/right</p>
<p>↓ : Soft drop</p>
<p>↑ : Rotate</p>
<p>Space : Hard drop</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game constants
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
const PREVIEW_SIZE = 4;
// Game elements
const tetrisBoard = document.getElementById('tetris-board');
const nextPiecePreview = document.getElementById('next-piece-preview');
const scoreElement = document.getElementById('score');
const levelElement = document.getElementById('level');
const startBtn = document.getElementById('start-btn');
const gameOverModal = document.getElementById('game-over-modal');
const finalScoreElement = document.getElementById('final-score');
const restartBtn = document.getElementById('restart-btn');
// Game state
let gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0));
let score = 0;
let level = 1;
let linesCleared = 0;
let currentPiece = null;
let nextPiece = null;
let gameInterval = null;
let isPaused = false;
let isGameOver = false;
// Tetrominoes
const tetrominoes = {
I: {
shape: [
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
color: 'I'
},
J: {
shape: [
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
color: 'J'
},
L: {
shape: [
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
],
color: 'L'
},
O: {
shape: [
[1, 1],
[1, 1]
],
color: 'O'
},
S: {
shape: [
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
color: 'S'
},
T: {
shape: [
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
color: 'T'
},
Z: {
shape: [
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
color: 'Z'
}
};
// Initialize game board
function initBoard() {
tetrisBoard.innerHTML = '';
nextPiecePreview.innerHTML = '';
// Create game board cells
for (let row = 0; row < BOARD_HEIGHT; row++) {
for (let col = 0; col < BOARD_WIDTH; col++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = row;
cell.dataset.col = col;
tetrisBoard.appendChild(cell);
}
}
// Create next piece preview cells
for (let row = 0; row < PREVIEW_SIZE; row++) {
for (let col = 0; col < PREVIEW_SIZE; col++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = row;
cell.dataset.col = col;
nextPiecePreview.appendChild(cell);
}
}
}
// Generate random tetromino
function getRandomTetromino() {
const tetrominoKeys = Object.keys(tetrominoes);
const randomKey = tetrominoKeys[Math.floor(Math.random() * tetrominoKeys.length)];
const tetromino = { ...tetrominoes[randomKey] };
return {
...tetromino,
row: 0,
col: Math.floor((BOARD_WIDTH - tetromino.shape[0].length) / 2)
};
}
// Draw tetromino on the board
function drawTetromino() {
clearBoard();
// Draw settled pieces
for (let row = 0; row < BOARD_HEIGHT; row++) {
for (let col = 0; col < BOARD_WIDTH; col++) {
if (gameBoard[row][col]) {
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
if (cell) {
cell.classList.add('tetromino', gameBoard[row][col]);
}
}
}
}
// Draw current piece
if (currentPiece) {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col]) {
const boardRow = currentPiece.row + row;
const boardCol = currentPiece.col + col;
if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) {
const cell = document.querySelector(`.cell[data-row="${boardRow}"][data-col="${boardCol}"]`);
if (cell) {
cell.classList.add('tetromino', currentPiece.color);
}
}
}
}
}
}
}
// Draw next piece in preview
function drawNextPiece() {
// Clear the preview
const previewCells = nextPiecePreview.querySelectorAll('.cell');
previewCells.forEach(cell => {
cell.className = 'cell';
});
if (!nextPiece) return;
// Center the piece in the preview
const offsetRow = Math.floor((PREVIEW_SIZE - nextPiece.shape.length) / 2);
const offsetCol = Math.floor((PREVIEW_SIZE - nextPiece.shape[0].length) / 2);
for (let row = 0; row < nextPiece.shape.length; row++) {
for (let col = 0; col < nextPiece.shape[row].length; col++) {
if (nextPiece.shape[row][col]) {
const previewRow = offsetRow + row;
const previewCol = offsetCol + col;
if (previewRow >= 0 && previewRow < PREVIEW_SIZE && previewCol >= 0 && previewCol < PREVIEW_SIZE) {
const cell = nextPiecePreview.querySelector(`.cell[data-row="${previewRow}"][data-col="${previewCol}"]`);
if (cell) {
cell.classList.add('tetromino', nextPiece.color);
}
}
}
}
}
}
// Clear the visual board (not the data)
function clearBoard() {
const cells = tetrisBoard.querySelectorAll('.cell');
cells.forEach(cell => {
cell.className = 'cell';
});
}
// Check if a move is valid
function isValidMove(piece, rowOffset = 0, colOffset = 0) {
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
const newRow = piece.row + row + rowOffset;
const newCol = piece.col + col + colOffset;
// Check if out of bounds or colliding with settled pieces
if (
newRow < 0 ||
newRow >= BOARD_HEIGHT ||
newCol < 0 ||
newCol >= BOARD_WIDTH ||
(newRow >= 0 && gameBoard[newRow][newCol])
) {
return false;
}
}
}
}
return true;
}
// Check and clear completed lines
function checkLines() {
let linesComplete = 0;
let flashRows = [];
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
if (gameBoard[row].every(cell => cell !== 0)) {
linesComplete++;
flashRows.push(row);
}
}
if (linesComplete > 0) {
// Flash the lines that will be cleared
flashRows.forEach(row => {
for (let col = 0; col < BOARD_WIDTH; col++) {
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
if (cell) {
cell.classList.add('flash-row');
}
}
});
// Wait for animation to complete then clear lines
setTimeout(() => {
for (let row of flashRows) {
// Remove the row
gameBoard.splice(row, 1);
// Add a new empty row at the top
gameBoard.unshift(Array(BOARD_WIDTH).fill(0));
}
// Update score and level
updateScore(linesComplete);
linesCleared += linesComplete;
if (linesCleared >= level * 10) {
level++;
levelElement.textContent = level;
updateGameSpeed();
}
drawTetromino();
}, 600);
}
}
// Calculate score based on lines cleared
function updateScore(lines) {
const linePoints = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines
const points = linePoints[lines] * level;
score += points;
scoreElement.textContent = score;
}
// Update game speed based on level
function updateGameSpeed() {
if (gameInterval) {
clearInterval(gameInterval);
}
const speed = Math.max(100, 1000 - (level - 1) * 100); // Decrease interval as level increases
gameInterval = setInterval(moveDown, speed);
}
// Move the current piece down
function moveDown() {
if (isPaused || isGameOver || !currentPiece) return;
if (isValidMove(currentPiece, 1, 0)) {
currentPiece.row++;
drawTetromino();
} else {
// Lock piece in place
lockPiece();
// Check for completed lines
checkLines();
// Generate new piece
spawnPiece();
}
}
// Move the current piece left
function moveLeft() {
if (isPaused || isGameOver || !currentPiece) return;
if (isValidMove(currentPiece, 0, -1)) {
currentPiece.col--;
drawTetromino();
}
}
// Move the current piece right
function moveRight() {
if (isPaused || isGameOver || !currentPiece) return;
if (isValidMove(currentPiece, 0, 1)) {
currentPiece.col++;
drawTetromino();
}
}
// Rotate the current piece
function rotatePiece() {
if (isPaused || isGameOver || !currentPiece) return;
// Create a copy of the current piece
const rotatedPiece = { ...currentPiece };
// Create a new rotated shape matrix
const N = rotatedPiece.shape.length;
const rotatedShape = Array(N).fill().map(() => Array(N).fill(0));
// Perform the rotation (90 degrees clockwise)
for (let row = 0; row < N; row++) {
for (let col = 0; col < N; col++) {
rotatedShape[col][N - 1 - row] = rotatedPiece.shape[row][col];
}
}
rotatedPiece.shape = rotatedShape;
// Check if the rotation is valid
if (isValidMove(rotatedPiece)) {
currentPiece.shape = rotatedShape;
drawTetromino();
}
}
// Hard drop the current piece
function hardDrop() {
if (isPaused || isGameOver || !currentPiece) return;
while (isValidMove(currentPiece, 1, 0)) {
currentPiece.row++;
}
drawTetromino();
lockPiece();
checkLines();
spawnPiece();
}
// Lock the current piece in place
function lockPiece() {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col]) {
const boardRow = currentPiece.row + row;
const boardCol = currentPiece.col + col;
if (boardRow >= 0 && boardRow < BOARD_HEIGHT && boardCol >= 0 && boardCol < BOARD_WIDTH) {
gameBoard[boardRow][boardCol] = currentPiece.color;
}
}
}
}
}
// Spawn a new piece
function spawnPiece() {
currentPiece = nextPiece || getRandomTetromino();
nextPiece = getRandomTetromino();
drawNextPiece();
// Check if game is over (collision on spawn)
if (!isValidMove(currentPiece)) {
gameOver();
}
drawTetromino();
}
// Game over
function gameOver() {
isGameOver = true;
clearInterval(gameInterval);
finalScoreElement.textContent = score;
gameOverModal.classList.add('active');
}
// Start or pause the game
function toggleGame() {
if (isGameOver) {
resetGame();
return;
}
if (isPaused) {
// Resume game
isPaused = false;
startBtn.textContent = 'Pause';
updateGameSpeed();
} else {
// Pause game
isPaused = true;
startBtn.textContent = 'Resume';
clearInterval(gameInterval);
}
}
// Reset the game
function resetGame() {
// Reset game state
gameBoard = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0));
score = 0;
level = 1;
linesCleared = 0;
isPaused = false;
isGameOver = false;
// Reset UI
scoreElement.textContent = score;
levelElement.textContent = level;
startBtn.textContent = 'Pause';
gameOverModal.classList.remove('active');
// Start new game
spawnPiece();
updateGameSpeed();
}
// Handle keyboard controls
function handleKeyPress(e) {
if (isGameOver) return;
switch(e.key) {
case 'ArrowLeft':
moveLeft();
break;
case 'ArrowRight':
moveRight();
break;
case 'ArrowDown':
moveDown();
break;
case 'ArrowUp':
rotatePiece();
break;
case ' ':
hardDrop();
break;
case 'p':
case 'P':
toggleGame();
break;
}
}
// Initialize the game
function init() {
initBoard();
spawnPiece();
// Event listeners
document.addEventListener('keydown', handleKeyPress);
startBtn.addEventListener('click', toggleGame);
restartBtn.addEventListener('click', resetGame);
// Start the game paused
isPaused = true;
startBtn.textContent = 'Start';
}
// Start the game
init();
});
</script>
</body>
</html>