🎮ArcadeLab

Remix of 俄罗斯方块 (Tetris)

by DriftBolt17
494 lines12.5 KB
▶ Play
<!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>&#8592; / &#8594; : 移动</li>
        <li>&#8659; : 加速下落 (Soft Drop)</li>
        <li>&#8593; : 旋转</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.