使用 javascript 实现俄罗斯方块

0. 操作流程图

1. 介绍

俄罗斯方块(俄语:Русская клеточная игра или Русская клеточная игра в квадрате)是一款经典的益智类游戏,其规则简单,操作简单,具有很高的趣味性。 俄罗斯方块是一款2D的益智游戏,玩家控制一个方块在一个方格内自由移动,通过控制方块的旋转、下落、以及移动,来消除周围的方块,并在此过程中获得分数。

游戏的基本规则如下:

  • 方块由四个小方块组成,每个小方块都有四个角落,四条边,以及一个中心点。
  • 方块可以旋转,但不能翻转。
  • 方块只能在空白的格子上移动。
  • 方块移动到底部时,方块会固定在底部,并开始下落。
  • 方块下落时,如果它与其他方块发生碰撞,则游戏结束。
  • 方块下落到底部时,它会消除周围的方块,并获得分数。
  • 方块消除一行时,游戏结束。

本文将用javascript实现俄罗斯方块游戏。

2. 预览效果

3. DOM 结构

html 复制代码
<div>
  <div id="tetris-score">Score: 0</div>
  <div>
    <button onclick="startGame()">Start Game</button>
  </div>
  <div id="tetris-container">
    <div id="tetris-board"></div>
    <div id="tetris-preview">
      <div id="next-piece"></div>
    </div>
  </div>
</div>

4. CSS 基础样式

css 复制代码
  #tetris-container {
    display: flex;
    margin-top: 20px;
    position: relative;
  }
  #tetris-board {
    width: 300px;
    height: 600px;
    border: 2px solid #000;
    position: relative;
    background-color: #f0f0f0;
  }
  #tetris-preview{
    width: 120px;
    height: 120px;
    margin-left: 20px;
    border: 2px solid #000;
    position: relative;
    background-color: #f0f0f0;
  }
  .rui-piece-cell{
    box-sizing: border-box;
    border: 1px solid #aaa;
  }
  #tetris-score {
    font-size: 20px;
    font-weight: bold;
    text-align: center;
  }
  button {
    display: block;
    margin: 0 auto;
    padding: 10px 20px;
    font-size: 18px;
    font-weight: bold;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }

  button:hover {
    background-color: #45a049;
  }

5. 全局变量声明

javascript 复制代码
  const BOARD_WIDTH = 10; // 俄罗斯方块板宽
  const BOARD_HEIGHT = 20; // 俄罗斯方块板高
  const PREVIEW_WIDTH = 6; // 俄罗斯方块预览宽
  let timer = null; // 定时器
  let board = Array.from({length: BOARD_HEIGHT}, () => Array(BOARD_WIDTH).fill(0)); // 俄罗斯方块板
  let currentPiece; // 当前俄罗斯方块
  let nextPiece; // 下一个俄罗斯方块
  let score = 0; // 得分

  const TETROMINOES = [
    [[1,1,1,1]], // I
    [[1,1],[1,1]], // O
    [[0,1,0],[1,1,1]], // T 向上
    [[1,1,1],[0,1,0]], // T 向下
    [[1,0,0],[1,1,1]], // L
    [[0,0,1],[1,1,1]], // J
    [[0,1,1],[1,1,0]], // S
    [[1,1,0],[0,1,1]]  // Z
  ];

6. 创建当前俄罗斯方块和下一个俄罗斯方块

  1. 如果没有下一个俄罗斯方块,则创建一个,说明游戏刚开始,下一个俄罗斯方块还未创建。
  2. 随机选择一个俄罗斯方块,计算俄罗斯方块出现的X坐标位置。
  3. 创建下一个俄罗斯方块,并对下一个俄罗斯方块进行预告绘制。
  4. 返回当前俄罗斯方块。
javascript 复制代码
  function createRandomPiece() {
    // 随机选择一个俄罗斯方块
    const randomIndex = Math.floor(Math.random() * TETROMINOES.length);
    // 计算俄罗斯方块出现的X坐标位置
    const randomX = Math.floor((BOARD_WIDTH - TETROMINOES[randomIndex][0].length) / 2);
    return {
      shape: TETROMINOES[randomIndex],
      x: randomX,
      y: 0
    };
  }
  function createPiece() {
    // 如果没有下一个俄罗斯方块,则创建一个
    let piece = nextPiece || createRandomPiece();
    // 随机选择下一个俄罗斯方块
    nextPiece = createRandomPiece();
    // 绘制下一个俄罗斯方块
    drawNextPiece(nextPiece);
    // 返回当前俄罗斯方块
    return piece;
  }

7. 绘制预告俄罗斯方块

javascript 复制代码
  function drawNextPiece(piece) {
    // 获取展示预告俄罗斯方块的盒子
    const nextPieceElement = document.getElementById('next-piece');
    // 清空展示预告俄罗斯方块的盒子
    nextPieceElement.innerHTML = '';
    // 获取俄罗斯方块的宽和高
    nextPieceWidth = piece.shape[0].length;
    nextPieceHeight = piece.shape.length;
    // 计算预告俄罗斯方块的左上角坐标
    let left = (PREVIEW_WIDTH - nextPieceWidth) / 2 * 20;
    let top = (PREVIEW_WIDTH - nextPieceHeight) / 2 * 20;
    // 绘制预告俄罗斯方块
    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 cell = document.createElement('div');
          cell.style.width = '20px';
          cell.style.height = '20px';
          cell.style.position = 'absolute';
          cell.style.left = `${col * 20 + left}px`;
          cell.style.top = `${row * 20 + top}px`;
          cell.style.backgroundColor = '#000';
          cell.classList.add('rui-piece-cell');
          nextPieceElement.appendChild(cell);
        }
      }
    }
  }

8. 绘制俄罗斯方块板

javascript 复制代码
  function draw() {
    // 获取俄罗斯方块板的盒子
    const boardElement = document.getElementById('tetris-board');
    // 清空俄罗斯方块板的盒子
    boardElement.innerHTML = '';
    // 获取当前俄罗斯方块的形状、X坐标、Y坐标
    let { shape, x:i, y:j } = currentPiece;
    // 绘制俄罗斯方块板
    for (let y = 0; y < BOARD_HEIGHT; y++) {
      for (let x = 0; x < BOARD_WIDTH; x++) {
        // 判断当前坐标是否是俄罗斯方块的一部分
        let isPiece = false;
        for (let row = 0; row < currentPiece.shape.length; row++) {
          for (let col = 0; col < currentPiece.shape[row].length; col++) {
            if (currentPiece.shape[row][col] && 
                x === currentPiece.x + col && 
                y === currentPiece.y + row) {
              isPiece = true;
            }
          }
        }
        // 创建一个俄罗斯方块板的单元格
        const cell = document.createElement('div');
        // 设置单元格的样式
        const cell = document.createElement('div');
        cell.style.position = 'absolute';
        cell.style.width = '30px';
        cell.style.height = '30px';
        cell.style.left = `${x * 30}px`;
        cell.style.top = `${y * 30}px`;
        cell.style.backgroundColor = isPiece ? '#f00' : (board[y][x] ? '#000' : '#f0f0f0');
        cell.classList.add('rui-piece-cell');
        boardElement.appendChild(cell);
      }
    }

    // 绘制得分
    document.getElementById('tetris-score').innerText = `Score: ${score}`;
  }

9. 移动俄罗斯方块

javascript 复制代码
  function movePiece(dx, dy) {
    // 如果当前俄罗斯方块可以移动,则移动当前俄罗斯方块
    if (!collision(currentPiece.shape, currentPiece.x + dx, currentPiece.y + dy)) {
      currentPiece.x += dx;
      currentPiece.y += dy;
    }
  }

10. 旋转俄罗斯方块

javascript 复制代码
  function rotatePiece() {
    // 旋转当前俄罗斯方块
    const rotated = currentPiece.shape[0].map((_, i) => 
      currentPiece.shape.map(row => row[i]).reverse()
    );
    // 如果旋转后的俄罗斯方块未发生碰撞,则旋转当前俄罗斯方块
    if (!collision(rotated, currentPiece.x, currentPiece.y)) {
      currentPiece.shape = rotated;
    }
  }

11. 俄罗斯方块是否碰撞

javascript 复制代码
  function collision(shape, x, y) {
    // 判断当前俄罗斯方块是否与俄罗斯方块板发生碰撞
    for (let row = 0; row < shape.length; row++) {
      for (let col = 0; col < shape[row].length; col++) {
        // 判断当前俄罗斯方块的某一格是否与俄罗斯方块板的某一格发生碰撞
        if (shape[row][col] &&
            (board[y + row] && board[y + row][x + col]) !== 0) {
          return true;
        }
      }
    }
    return false;
  }

12. 俄罗斯方块下落

javascript 复制代码
  function placePiece() {
    // 放置当前俄罗斯方块
    for (let row = 0; row < currentPiece.shape.length; row++) {
      for (let col = 0; col < currentPiece.shape[row].length; col++) {
        if (currentPiece.shape[row][col]) {
          board[currentPiece.y + row][currentPiece.x + col] = 1;
        }
      }
    }
    // 清除满行
    clearLines();
    // 创建下一个俄罗斯方块
    currentPiece = createPiece();
    // 如果当前俄罗斯方块与俄罗斯方块板发生碰撞,则游戏结束
    if (collision(currentPiece.shape, currentPiece.x, currentPiece.y)) {
      gameOver();
    }
  }

13. 清除满行

javascript 复制代码
  function clearLines() {
    // 清除满行
    let linesCleared = 0;
    // 遍历俄罗斯方块板,从上到下遍历每一行
    for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
      if (board[y].every(cell => cell !== 0)) {
        board.splice(y, 1);
        board.unshift(Array(BOARD_WIDTH).fill(0));
        linesCleared++;
        y++;
      }
    }
    // 增加分数
    score += linesCleared * 100;
  }

14. 游戏结束

javascript 复制代码
  function gameOver() {
    alert(`Game Over! Your score: ${score}`);
    // 清空俄罗斯方块板
    board = Array.from({length: BOARD_HEIGHT}, () => Array(BOARD_WIDTH).fill(0));
    // 重置得分
    score = 0;
    // 创建新的俄罗斯方块
    currentPiece = createPiece();
  }

15. 开始游戏

javascript 复制代码
  function gameLoop() {
    draw();
    // 判断当前俄罗斯方块是否可以下落
    if (!collision(currentPiece.shape, currentPiece.x, currentPiece.y + 1)) {
      currentPiece.y++;
    } else {
      placePiece();
    }
    // 开启下一轮游戏
    timer = setTimeout(gameLoop, 1000);
  }

  function startGame() {
    // 判断游戏是否已经开始,开始就清除定时器
    if(timer) {
      clearTimeout(timer); 
    }
    currentPiece = createPiece();
    draw();
    gameLoop();
  }
相关推荐
Zuckjet5 分钟前
从零到百万:Notion如何用CRDT征服离线协作的终极挑战?
前端
ikonan10 分钟前
译:Chrome DevTools 实用技巧和窍门清单
前端·javascript
Juchecar10 分钟前
Vue3 v-if、v-show、v-for 详解及示例
前端·vue.js
ccc101814 分钟前
通过学长的分享,我学到了
前端
编辑胜编程14 分钟前
记录MCP开发表单
前端
可爱生存报告14 分钟前
vue3 vite quill-image-resize-module打包报错 Cannot set properties of undefined
前端·vite
__lll_14 分钟前
前端性能优化:Vue + Vite 全链路性能提升与打包体积压缩指南
前端·性能优化
weJee15 分钟前
pnpm原理
前端·前端工程化
小高00716 分钟前
⚡️ Vue 3.5 正式发布:10× 响应式性能、SSR 水合黑科技、告别 .value!
前端·javascript·vue.js
乡村中医17 分钟前
🔥如何在函数式编程中使用设计模式-单例模式
前端·代码规范