俄罗斯方块终端游戏实现 —— C语言系统编程与终端控制

俄罗斯方块终端游戏实现 ------ C语言系统编程与终端控制

本项目是一个使用 C语言 实现的简易 俄罗斯方块(Tetris)终端游戏,完整涵盖了 系统调用、终端控制、随机数、二维数组地图、碰撞检测、图形绘制、非阻塞输入、光标控制、清屏与重绘、方块旋转与锁定、行消除与计分机制 等多个核心知识点。


📌 项目依赖头文件与宏定义

复制代码
#include <fcntl.h>   // 用于 fcntl() 函数,实现非阻塞输入检测
#include <stdio.h>   // 标准输入输出,printf, putchar 等
#include <stdlib.h>  // rand(), srand() 生成随机方块
#include <termios.h> // 终端属性控制,实现 getch() 与 kbhit()
#include <time.h>    // time() 为随机数播种
#include <unistd.h>  // usleep() 实现游戏帧率控制

#define H 22  // 游戏地图高度(行数),含边界
#define W 12  // 游戏地图宽度(列数),含边界

✅ 理想结果:程序编译无警告,头文件正确包含,宏定义生效,地图尺寸为 22 行 × 12 列。


🧱 全局变量与方块结构体定义

复制代码
int map[H][W];  // 二维地图数组,1=边界,2=固定方块,0=空白
int score = 0;  // 玩家得分,每消除一行 +100 分

// 当前方块结构体:位置(x,y)、类型(0-6)、旋转状态(0-3)
struct Block
{
  int x;
  int y;
  int type;      // 0-6 对应 7 种方块类型
  int rotation;  // 0-3 对应 4 种旋转状态
};
struct Block currentBlock;  // 当前活动方块实例

✅ 理想结果:全局变量初始化成功,currentBlock 可在函数中被正确赋值和读取。


🔲 七种方块的 4×4×4 三维旋转矩阵定义

复制代码
// 7种方块,每种4种旋转,每种旋转为4×4矩阵(实际使用中心2×4或3×3区域)
int BLOCKS[7][4][4][4] = {
    // I 方块 (长条形) ------ 4种旋转状态相同或镜像
    {{{0, 0, 0, 0}, {1, 1, 1, 1}, {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}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}}},

    // O 方块 (正方形) ------ 4种旋转完全相同
    {{{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}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}}},

    // T 方块 (T 字形) ------ 4种旋转状态
    {{{0, 0, 0, 0}, {0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},

    // S 方块 (S 形) ------ 2种独特旋转,重复两次
    {{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
     {{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}},

    // Z 方块 (Z 形) ------ 2种独特旋转,重复两次
    {{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},

    // L 方块 (L 形) ------ 4种旋转状态
    {{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 0, 0, 0}, {1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},

    // J 方块 (J 形) ------ 4种旋转状态
    {{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
     {{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
     {{0, 0, 0, 0}, {0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},
     {{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}}};

✅ 理想结果:所有方块形状与旋转状态定义正确,游戏中可随机生成并正确旋转。


🗺️ 地图初始化函数 ------ initMap()

复制代码
void initMap()  // 初始化游戏地图,设置边界
{
  int x, y;
  for (y = 0; y < H; ++y)
  {
    for (x = 0; x < W; ++x)
    {
      if (x == 0 || x == W - 1 || y == H - 1)  // 左右边界和底边界
      {
        map[y][x] = 1;  // 边界用 1 标记,绘制为红色
      }
      else
      {
        map[y][x] = 0;  // 内部区域初始为空白
      }
    }
  }
}

✅ 理想结果:地图四周和底部显示红色边界,内部为空白,为游戏提供碰撞检测基础。


🎲 创建新方块 ------ createNewBlock()

复制代码
void createNewBlock()
{
  currentBlock.type = rand() % 7;      // 随机选择 0~6 类型
  currentBlock.rotation = rand() % 4;  // 随机选择 0~3 旋转状态

  // 新方块出生在顶部中央(x 偏移 -2 以居中 4×4 方块)
  currentBlock.x = W / 2 - 2;
  currentBlock.y = 0;
}

✅ 理想结果:每次调用生成一个随机类型和旋转的新方块,出现在地图顶部中央。


🚧 碰撞检测 ------ checkCollision()

复制代码
int checkCollision(int x, int y, int type, int rotation)
// 检测指定位置、类型、旋转的方块是否与边界或固定块碰撞
{
  int i, j;  // 遍历 4×4 方块矩阵
  for (i = 0; i < 4; ++i)
  {
    for (j = 0; j < 4; ++j)
    {
      if (BLOCKS[type][rotation][i][j] != 0)  // 如果该位置是方块实体
      {
        int nx = x + j;  // 计算在地图中的实际 x 坐标
        int ny = y + i;  // 计算在地图中的实际 y 坐标

        // 检测是否越界:左、右、下、上边界(上边界允许负值,但地图不绘制)
        if (nx < 0 || nx >= W || ny >= H || ny < 0)
        {
          return 1;  // 发生碰撞
        }

        // 检测是否与已固定的方块(map[][] == 2)碰撞
        if (ny >= 0 && map[ny][nx] != 0)  // 注意:只检测地图内区域
        {
          return 1;
        }
      }
    }
  }
  return 0;  // 无碰撞
}

✅ 理想结果:方块移动或旋转前调用,若返回 1 则禁止操作,防止穿墙或重叠。


🔒 锁定方块 ------ lockBlock()

复制代码
void lockBlock()
// 将当前活动方块写入地图,变为固定块(值为 2)
{
  int i, j;
  for (i = 0; i < 4; ++i)
  {
    for (j = 0; j < 4; ++j)
    {
      if (BLOCKS[currentBlock.type][currentBlock.rotation][i][j] != 0)
      {
        int nx = currentBlock.x + j;
        int ny = currentBlock.y + i;
        // 确保坐标在地图范围内再写入
        if (ny >= 0 && ny < H && nx >= 0 && nx < W)
        {
          map[ny][nx] = 2;  // 固定方块标记为 2
        }
      }
    }
  }
}

✅ 理想结果:方块落地后变为地图的一部分,不可再移动,后续方块与其碰撞。


🧹 行消除与下移 ------ checkLineClear()

复制代码
void checkLineClear()
{
  int y = H - 2;  // 从倒数第二行开始检查(最后一行是边界)
  int lines_cleared = 0;

  while (y >= 0)  // 从底向上逐行检查
  {                   
    int is_full = 1;  // 假设当前行是满的

    // 检查游戏区域(x=1 到 W-2)是否全为固定块(值为2)
    for (int x = 1; x < W - 1; x++)
    {
      if (map[y][x] == 0)  // 存在空白
      {
        is_full = 0;
        break;
      }
    }

    if (is_full)
    {
      ++lines_cleared;

      // 将当前行及以上所有行向下移动一行
      for (int y_tmp = y; y_tmp > 0; y_tmp--)
      {
        for (int x = 0; x < W; x++)
        {
          map[y_tmp][x] = map[y_tmp - 1][x];  // 下移
        }
      }
      // 清空最顶行
      for (int x = 0; x < W; x++)
      {
        map[0][x] = 0;
      }
      // 注意:y 不自减,因为下移后原 y 行是新内容,需重新检查
    }
    else
    {
      --y;  // 当前行不满,检查上一行
    }
  }

  // 更新分数:每消除一行 +100 分
  score += lines_cleared * 100;
}

✅ 理想结果:满行被消除,上方行下移,顶部清空,分数按行数增加。


🖱️ 终端光标控制函数

复制代码
void gotoxy(int x, int y)
// 使用 VT100 转义序列定位光标到指定位置(y行, x列)
{
  printf("\033[%d;%dH", y + 1, x * 2 + 1);  // 行号+1,列号*2+1(因每个方块占2字符宽)
  fflush(stdout);  // 强制刷新输出缓冲区
}

void hideCursor()
// 隐藏终端光标,提升游戏沉浸感
{
  printf("\033[?25l");  // VT100 隐藏光标指令
  fflush(stdout);
}

void showCursor()
// 游戏结束后显示光标
{
  printf("\033[?25h");  // VT100 显示光标指令
  fflush(stdout);
}

✅ 理想结果:光标可精确定位到指定位置绘制方块,游戏运行时光标隐藏,退出后恢复。


⌨️ 终端非阻塞输入函数 ------ getch() 与 kbhit()

复制代码
int getch(void)
// 从终端读取一个字符,不回显(非缓冲、非回显模式)
{
  struct termios oldt, newt;
  int ch;
  tcgetattr(STDIN_FILENO, &oldt);          // 获取当前终端属性
  newt = oldt;
  newt.c_lflag &= ~(ICANON | ECHO);        // 关闭缓冲与回显
  tcsetattr(STDIN_FILENO, TCSANOW, &newt); // 应用新设置
  ch = getchar();                          // 读取字符
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // 恢复原设置
  return ch;
}

int kbhit(void)
// 检测是否有按键按下,非阻塞(立即返回)
{
  struct termios oldt, newt;
  int ch;
  int oldf;

  tcgetattr(STDIN_FILENO, &oldt);
  newt = oldt;
  newt.c_lflag &= ~(ICANON | ECHO);
  tcsetattr(STDIN_FILENO, TCSANOW, &newt);
  oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
  fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); // 设置非阻塞读取

  ch = getchar();  // 尝试读取

  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
  fcntl(STDIN_FILENO, F_SETFL, oldf);  // 恢复原设置

  if (ch != EOF)
  {
    ungetc(ch, stdin);  // 将字符放回输入流,供后续 getch() 使用
    return 1;           // 有按键
  }
  return 0;             // 无按键
}

✅ 理想结果:kbhit() 立即返回是否有按键,getch() 读取按键值,支持方向键(ESC [ D/C/A)。


🖼️ 地图绘制函数 ------ drawMap()

复制代码
void drawMap()
{
  printf("\033[2J");  // VT100 清屏指令
  gotoxy(0, 0);       // 光标归位到左上角

  int y = 0, x = 0;
  for (y = 0; y < H; ++y)
  {
    for (x = 0; x < W; ++x)
    {
      // 如果当前格子属于活动方块范围
      if (x >= currentBlock.x && x < currentBlock.x + 4 && 
          y >= currentBlock.y && y < currentBlock.y + 4)
      {
        int rel_x = x - currentBlock.x;
        int rel_y = y - currentBlock.y;

        // 如果方块矩阵中该位置为实体,则绘制活动方块
        if (BLOCKS[currentBlock.type][currentBlock.rotation][rel_y][rel_x])
        {
          printf("[]");
          continue;
        }
      }

      // 绘制地图内容:边界(红色)、固定块(白色)、空白(空格)
      if (map[y][x] == 1)
      {
        printf("\033[1;31m[]\033[0m");  // 红色边界
      }
      else if (map[y][x] == 2)
      {
        printf("[]");  // 固定方块
      }
      else
      {
        printf("  ");  // 空白
      }
    }
    putchar('\n');  // 换行
  }

  // 在右侧显示当前分数
  gotoxy(W + 1, 5);
  printf("Score: %d", score);
  fflush(stdout);  // 强制输出
}

✅ 理想结果:每帧清屏重绘,活动方块叠加在地图上,边界红色,分数实时显示。


🎮 主游戏循环 ------ main()

复制代码
int main(void)
{
  srand(time(NULL));  // 用当前时间初始化随机数种子
  initMap();          // 初始化带边界的地图
  createNewBlock();   // 创建第一个活动方块
  hideCursor();       // 隐藏终端光标

  while (1)  // 无限游戏循环
  {
    if (kbhit())  // 检测是否有按键
    {
      char key = getch();  // 读取按键
      if (key == 27)       // 如果是 ESC 键(方向键序列开头)
      {
        getch();         // 读取 '['
        key = getch();   // 读取方向字符
        if (key == 'D')  // 左方向键
        {
          // 检测左移是否碰撞,无碰撞则移动
          if (checkCollision(currentBlock.x - 1, currentBlock.y, 
                             currentBlock.type, currentBlock.rotation) == 0)
          {
            --currentBlock.x;
          }
        }
        else if (key == 'C')  // 右方向键
        {
          if (checkCollision(currentBlock.x + 1, currentBlock.y, 
                             currentBlock.type, currentBlock.rotation) == 0)
          {
            ++currentBlock.x;
          }
        }
        else if (key == 'A')  // 上方向键(旋转)
        {
          int new_rotation = (currentBlock.rotation + 1) % 4;  // 循环旋转
          if (checkCollision(currentBlock.x, currentBlock.y, 
                             currentBlock.type, new_rotation) == 0)
          {
            currentBlock.rotation = new_rotation;
          }
        }
      }
    }

    // 尝试下落一格
    if (checkCollision(currentBlock.x, currentBlock.y + 1, 
                       currentBlock.type, currentBlock.rotation) == 0)
    {
      ++currentBlock.y;  // 无碰撞,下落
    }
    else  // 下落发生碰撞
    {
      lockBlock();       // 锁定当前方块到地图
      checkLineClear();  // 检查并消除满行
      createNewBlock();  // 生成新方块
    }

    drawMap();       // 绘制当前帧
    usleep(500000);  // 延迟 0.5 秒(500毫秒),控制下落速度
  }

  showCursor();  // 理论上不会执行(因 while(1)),但保留用于规范
  return 0;
}

✅ 理想结果:游戏持续运行,方块自动下落,支持左右移动和旋转,落地后锁定,消除行并计分,新方块生成,帧率稳定。


🧩 知识点归纳

类别 知识点 对应函数/代码段

系统编程 终端属性控制、非阻塞输入 getch(), kbhit(), termios.h, fcntl.h

图形界面 VT100 转义序列、光标定位、清屏、颜色 gotoxy(), hideCursor(), \033[2J, \033[1;31m

游戏逻辑 碰撞检测、方块锁定、行消除、计分 checkCollision(), lockBlock(), checkLineClear()

数据结构 三维数组存储方块形态、二维地图 BLOCKS[7][4][4][4], map[H][W]

控制流程 主循环、条件移动、状态更新 main() 中 while 循环与条件分支

随机机制 随机生成方块类型与旋转 srand(), rand() % 7, rand() % 4

时间控制 控制帧率与下落速度 usleep(500000)


🏁 运行环境与理想结果

  • 操作系统:Linux / macOS(支持 VT100 终端)
  • 编译命令:gcc -o tetris main.c
  • 运行命令:./tetris
  • 控制键:
    • ←:左移(ESC [ D)
    • →:右移(ESC [ C)
    • ↑:旋转(ESC [ A)
    • ESC:退出(未实现,需 Ctrl+C 强制终止)
  • 视觉效果:
    • 红色边界框
    • 白色活动与固定方块 "[]"
    • 实时分数显示
    • 方块自动下落,支持操作
    • 消行后上方方块下移,分数增加

✅ 最终理想结果:游戏流畅运行,玩家可通过方向键控制方块,消除行得分,无崩溃或逻辑错误。

相关推荐
青草地溪水旁2 小时前
C/C++ 标准库中的 `strspn` 函数
c语言·c++
诸葛务农3 小时前
光电对抗:多模/复合制导中算法和软件平台
算法
Starshime3 小时前
【C语言】变量和常量
c语言·开发语言
晨非辰3 小时前
#C语言——刷题攻略:牛客编程入门训练(十):攻克 循环控制(二),轻松拿捏!
c语言·开发语言·经验分享·学习·visual studio
Swift社区3 小时前
LeetCode 378 - 有序矩阵中第 K 小的元素
算法·leetcode·矩阵
墨染点香3 小时前
LeetCode 刷题【73. 矩阵置零】
算法·leetcode·矩阵
tqs_123453 小时前
redis zset score的计算
java·算法
_Coin_-3 小时前
算法训练营DAY60 第十一章:图论part11
算法·图论