Remix of 俄罗斯方块 (Tetris)
by DriftBolt17494 lines12.5 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>俄罗斯方块 (Tetris)</title>
<style>
:root {
--bg: #0f172a;
--panel-bg: #1e293b;
--text: #e6edf3;
--accent: #00ffff;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg);
color: var(--text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
#app {
display: flex;
gap: 25px;
background-color: var(--panel-bg);
padding: 30px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 255, 255, 0.05);
border: 1px solid #334155;
}
.panel {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 130px;
}
h2 {
margin: 0;
font-size: 1.1rem;
color: var(--accent);
letter-spacing: 2px;
text-transform: uppercase;
border-bottom: 1px solid #334155;
padding-bottom: 8px;
}
p {
margin: 0;
font-size: 2.2rem;
font-weight: bold;
font-family: monospace;
text-shadow: 0 0 10px var(--accent);
}
#board-wrapper {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 25px rgba(0, 255, 255, 0.15);
border: 2px solid #334155;
}
canvas {
display: block;
background-color: #0a0f18;
}
#next {
width: 160px !important;
height: 160px !important;
border-radius: 8px;
margin-top: auto;
}
.controls-list {
font-size: 0.95rem;
line-height: 1.8;
color: #a0aec0;
}
ul {
padding-left: 20px;
margin: 0;
}
.overlay-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(15, 23, 42, 0.95);
padding: 25px 45px;
border-radius: 8px;
text-align: center;
font-size: 2rem;
color: var(--accent);
border: 1px solid var(--accent);
z-index: 10;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.4);
display: none;
}
button {
background: transparent;
color: var(--text);
border: 1px solid #4a5568;
padding: 10px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
width: 100%;
}
button:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: var(--accent);
color: var(--accent);
}
</style>
</head>
<body>
<div id="app">
<div class="panel">
<h2>得分 (Score)</h2>
<p id="score">0</p>
<h2>等级 (Level)</h2>
<p id="level">1</p>
<button onclick="resetGame()">重新开始 (Restart)</button>
<div class="controls-list">
<strong>控制说明:</strong>
<ul>
<li>← / → : 移动</li>
<li>⇓ : 加速下落 (Soft Drop)</li>
<li>↑ : 旋转</li>
<li>Space: 暂停 / 继续</li>
</ul>
</div>
</div>
<div id="board-wrapper">
<canvas id="board" width="300" height="600"></canvas>
<div id="pause-text" class="overlay-text">
PAUSED<br><small style="font-size:1rem; margin-top:10px;">按空格继续</small>
</div>
<div id="gameover-text" class="overlay-text">
GAME OVER<br>
最终得分: <span id="final-score">0</span><br>
<small style="font-size:1rem; margin-top:10px;">按 R 键重新开始</small>
</div>
</div>
<div class="panel">
<h2>下一个 (Next)</h2>
<canvas id="next" width="120" height="120"></canvas>
</div>
</div>
<script>
// --- Constants & Setup ---
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
canvas.width = COLS * BLOCK_SIZE;
canvas.height = ROWS * BLOCK_SIZE;
// --- Tetromino Definitions & Colors ---
const shapes = [
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I (Cyan)
[[2,2],[2,2]], // O (Yellow)
[[0,3,0],[3,3,3],[0,0,0]], // T (Purple)
[[0,4,4],[4,4,0],[0,0,0]], // S (Green)
[[5,5,0],[0,5,5],[0,0,0]], // Z (Red)
[[6,0,0],[6,6,6],[0,0,0]], // J (Blue)
[[0,0,7],[7,7,7],[0,0,0]] // L (Orange)
];
const colors = [
null,
'#0ff',
'#ff0',
'#a0f',
'#0f0',
'#f00',
'#00f',
'#f80'
];
// --- Game State Variables ---
let board = [];
let score = 0;
let level = 1;
let linesCleared = 0;
let dropCounter = 0;
let lastTime = 0;
let dropInterval = 800;
let gameRunning = false;
let player = {
pos: { x: 0, y: 0 },
matrix: null,
nextMatrix: null
};
// --- Utility Functions ---
function random(max) {
return Math.floor(Math.random() * max);
}
function rotate(matrix) {
for (let y = 0; y < matrix.length; ++y) {
for (let x = 0; x < y; ++x) {
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
}
}
return matrix.map(row => row.reverse());
}
function updateScore() {
document.getElementById('score').innerText = score;
document.getElementById('level').innerText = level;
}
// --- Core Game Logic ---
function collide(board, player) {
const m = player.matrix;
const o = player.pos;
for (let y = 0; y < m.length; ++y) {
for (let x = 0; x < m[y].length; ++x) {
if (!m[y][x]) continue;
const boardY = o.y + y;
const boardX = o.x + x;
if (boardY >= ROWS) return true; // Bottom boundary
if (boardX < 0 || boardX >= COLS) return true; // Side boundaries
if (boardY >= 0 && board[boardY][boardX] !== 0) return true; // Collision with existing blocks
}
}
return false;
}
function merge(board, player) {
player.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0 && y + player.pos.y >= 0) { // Prevent writing above the board
board[y + player.pos.y][x + player.pos.x] = value;
}
});
});
}
function clearLines() {
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;
}
if (cleared > 0) {
score += [0, 40, 100, 300, 1200][cleared] * level;
linesCleared += cleared;
const newLevel = Math.floor(linesCleared / 10) + 1;
if (newLevel > level) {
level = newLevel;
dropInterval = Math.max(100, 800 - (level - 1) * 50);
}
updateScore();
}
}
function playerMove(offset) {
if (!gameRunning) return;
player.pos.x += offset;
if (collide(board, player)) player.pos.x -= offset;
}
function playerRotate() {
if (!gameRunning) return;
const pos = player.pos.x;
let offset = 1;
rotate(player.matrix);
// Simple Wall Kick logic
while (collide(board, player)) {
player.pos.x += offset;
offset = -(offset + (offset > 0 ? 1 : -1));
if (Math.abs(offset) > player.matrix[0].length) {
rotate(player.matrix); // Undo rotation
player.pos.x = pos;
return;
}
}
}
function playerDrop() {
if (!gameRunning) return;
++player.pos.y;
if (collide(board, player)) {
--player.pos.y;
merge(board, player);
clearLines();
playerReset();
}
dropCounter = 0;
}
function playerReset() {
player.matrix = shapes[random(shapes.length)];
player.pos.y = 0;
player.pos.x = Math.floor(COLS / 2) - Math.floor(player.matrix[0].length / 2);
if (collide(board, player)) {
gameRunning = false;
document.getElementById('final-score').innerText = score;
document.getElementById('gameover-text').style.display = 'block';
}
// Generate the next piece immediately for preview
player.nextMatrix = shapes[random(shapes.length)];
}
function resetGame() {
board = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
score = 0;
level = 1;
linesCleared = 0;
dropCounter = 0;
lastTime = 0;
dropInterval = 800;
document.getElementById('gameover-text').style.display = 'none';
document.getElementById('pause-text').style.display = 'none';
gameRunning = true;
playerReset();
updateScore();
drawNextPiece();
lastTime = performance.now();
requestAnimationFrame(update);
}
function togglePause() {
if (document.getElementById('gameover-text').style.display === 'block') return;
gameRunning = !gameRunning;
document.getElementById('pause-text').style.display = gameRunning ? 'none' : 'block';
if (gameRunning) {
lastTime = performance.now();
requestAnimationFrame(update);
}
}
// --- Rendering Functions ---
function drawBlock(context, pixelX, pixelY, colorIndex, size = BLOCK_SIZE) {
context.fillStyle = colors[colorIndex];
context.fillRect(pixelX * size + 1, pixelY * size + 1, size - 2, size - 2);
// Add basic depth shading for larger blocks
if (size > 15) {
context.fillStyle = 'rgba(255, 255, 255, 0.3)';
context.fillRect(pixelX * size + 1, pixelY * size + 1, size - 2, Math.floor(size / 6));
context.fillStyle = 'rgba(0, 0, 0, 0.4)';
context.fillRect(pixelX * size + 1, (pixelY + 1) * size - Math.ceil(size / 6), size - 2, Math.ceil(size / 6));
}
}
function drawRow(row, y) {
row.forEach((value, x) => {
if (value !== 0) drawBlock(ctx, x, y, value);
});
}
function drawNextPiece() {
const previewSize = 20;
const nextCtx = document.getElementById('next').getContext('2d');
// Clear background
nextCtx.fillStyle = '#1a1a1a';
nextCtx.fillRect(0, 0, nextCtx.canvas.width, nextCtx.canvas.height);
let minCol = 20, maxCol = 0, minRow = 20, maxRow = 0;
player.nextMatrix.forEach((row, y) => row.forEach((val, x) => {
if (val !== 0) {
minRow = Math.min(minRow, y);
maxRow = Math.max(maxRow, y);
minCol = Math.min(minCol, x);
maxCol = Math.max(maxCol, x);
}
}));
const offsetX = Math.floor((nextCtx.canvas.width - (maxCol - minCol + 1) * previewSize) / 2 / previewSize);
const offsetY = Math.floor((nextCtx.canvas.height - (maxRow - minRow + 1) * previewSize) / 2 / previewSize);
player.nextMatrix.forEach((row, y) => row.forEach((val, x) => {
if (val !== 0) drawBlock(nextCtx, x + offsetX, y + offsetY, val, previewSize);
}));
}
function draw() {
// Clear board background
ctx.fillStyle = '#0a0f18';
ctx.fillRect(0, 0, canvas.width, canvas.height);
board.forEach(drawRow);
player.matrix.forEach((row, y) => row.forEach((val, x) => {
if (val !== 0 && y + player.pos.y >= 0) drawBlock(ctx, x + player.pos.x, y + player.pos.y, val);
}));
}
// --- Main Game Loop ---
function update(time = 0) {
if (!gameRunning) return;
const deltaTime = time - lastTime;
lastTime = time;
dropCounter += deltaTime;
if (dropCounter > dropInterval) playerDrop();
draw();
requestAnimationFrame(update);
}
// --- Event Listeners ---
document.addEventListener('keydown', event => {
if (!gameRunning && event.code !== 'KeyR' && event.code !== 'Space') return;
switch (event.code) {
case 'ArrowLeft':
event.preventDefault();
playerMove(-1);
break;
case 'ArrowRight':
event.preventDefault();
playerMove(1);
break;
case 'ArrowDown':
event.preventDefault();
playerDrop();
if (gameRunning) { score += 1; updateScore(); } // Soft drop points
break;
case 'ArrowUp':
event.preventDefault();
playerRotate();
break;
case 'Space':
event.preventDefault();
togglePause();
break;
case 'KeyR':
resetGame();
break;
}
});
// Start the game on page load
window.onload = resetGame;
</script>
</body>
</html>
Game Source: Remix of 俄罗斯方块 (Tetris)
Creator: DriftBolt17
Libraries: none
Complexity: complex (494 lines, 12.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: remix-of-tetris-driftbolt17" to link back to the original. Then publish at arcadelab.ai/publish.