#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>  // 引用 read(),usleep()
#include <termios.h> // 终端 I/O 控制:回显、缓冲、特殊字符处理等
#include <time.h>
#include <signal.h>  // 信号处理

// 游戏区域尺寸
#define WIDTH 10
#define HEIGHT 20
#define BUFFER_HEIGHT 4 // 距离顶部 + 4,方块下落的起始位置

/* 
四维数组:定义俄罗斯方块的 所有形状和旋转状态
第1维 [7]:7种基本方块类型(I, J, L, O, S, T, Z)
第2维 [4]:每种方块的4种旋转状态(0°, 90°, 180°, 270°)
第3维 [4]:每个方块的4个组成块
第4维 [2]:每个块的坐标 (x, y)
*/
const int shapes[7][4][4][2] = {
    // I 形 (shapes[0])
    {
        {{0,0}, {1,0}, {2,0}, {3,0}}, // 旋转0°
        {{0,0}, {0,1}, {0,2}, {0,3}}, // 旋转90°
        {{0,0}, {1,0}, {2,0}, {3,0}}, // 旋转180°
        {{0,0}, {0,1}, {0,2}, {0,3}}  // 旋转270°
    },
    // J 形 (shapes[1])
    {
        {{0,0}, {0,1}, {1,1}, {2,1}},
        {{1,0}, {1,1}, {1,2}, {0,2}},
        {{0,1}, {1,1}, {2,1}, {2,2}},
        {{0,0}, {1,0}, {0,1}, {0,2}}
    },
    // L 形 (shapes[2])
    {
        {{2,0}, {0,1}, {1,1}, {2,1}},
        {{0,0}, {1,0}, {1,1}, {1,2}},
        {{0,1}, {1,1}, {2,1}, {0,2}},
        {{1,0}, {1,1}, {1,2}, {2,2}}
    },
    // O 形 (shapes[3])
    {
        {{0,0}, {1,0}, {0,1}, {1,1}},
        {{0,0}, {1,0}, {0,1}, {1,1}},
        {{0,0}, {1,0}, {0,1}, {1,1}},
        {{0,0}, {1,0}, {0,1}, {1,1}}
    },
    // S 形
    {
        {{1,0}, {2,0}, {0,1}, {1,1}},
        {{0,0}, {0,1}, {1,1}, {1,2}},
        {{1,0}, {2,0}, {0,1}, {1,1}},
        {{0,0}, {0,1}, {1,1}, {1,2}}
    },
    // T 形
    {
        {{1,0}, {0,1}, {1,1}, {2,1}},
        {{1,0}, {1,1}, {2,1}, {1,2}},
        {{0,1}, {1,1}, {2,1}, {1,2}},
        {{1,0}, {0,1}, {1,1}, {1,2}}
    },
    // Z 形
    {
        {{0,0}, {1,0}, {1,1}, {2,1}},
        {{1,0}, {0,1}, {1,1}, {0,2}},
        {{0,0}, {1,0}, {1,1}, {2,1}},
        {{1,0}, {0,1}, {1,1}, {0,2}}
    }
};

// 7中颜色
const char* colors[] = {
    "\033[91m", // 亮红色 - I
    "\033[94m", // 亮蓝色 - J
    "\033[31m", // 红色 - L
    "\033[93m", // 亮黄色 - O
    "\033[92m", // 亮绿色 - S
    "\033[95m", // 亮紫色 - T
    "\033[96m"  // 亮青色 - Z
};

// 游戏状态
int board[HEIGHT + BUFFER_HEIGHT][WIDTH];// 游戏面板的二维数组 board[y][x] != -1 时,该方块已被占用,记录方块形状的编号
int current_piece, next_piece; // 方块形状编号
int current_rotation = 0;      // 旋转方向
int current_x, current_y;      // 坐标
int score = 0;
int level = 1;
int lines_cleared = 0;         // 累计消除的总行数
int game_over = 0;

// 终端设置
struct termios original_termios;

// 清理终端设置
void cleanup_terminal() {
    tcsetattr(STDIN_FILENO, TCSANOW, &original_termios); //ICANON:恢复行缓冲模式,ECHO:重新启用回显
    printf("\033[?25h\033[0m"); //显示光标并重置颜色,\033[?25h 显示光标,\033[?25l 用于隐藏光标,\033[0m 重置所有属性
}

// 初始化终端
void init_terminal() {
    struct termios new_termios;
    tcgetattr(STDIN_FILENO, &original_termios); // 保存原始设置
    // 修改终端设置
    new_termios = original_termios;             // 复制设置
    new_termios.c_lflag &= ~(ICANON | ECHO); // 禁用行缓冲和回显
    new_termios.c_cc[VMIN] = 0;              // 不等待最小字符数
    new_termios.c_cc[VTIME] = 0;             // 无超时
    tcsetattr(STDIN_FILENO, TCSANOW, &new_termios); // 应用新设置
    
    // 注册退出时自动调用,无论程序如何退出(正常退出、Ctrl+C、错误退出),都会自动调用cleanup_terminal()。
    atexit(cleanup_terminal);   // 注册清理函数,把 cleanup_terminal 函数登记到"退出时要执行的函数列表"中,
                                // 当程序退出时,系统自动调用这个列表中的所有函数
    printf("\033[?25l");        // 隐藏光标

    /*
    游戏运行时:光标隐藏,按键立即响应(无需回车),输入不显示
    游戏退出后:光标重新显示,终端恢复正常工作模式,所有颜色和属性重置
    */
}

// 设置光标位置
void set_cursor_pos(int x, int y) {
    printf("\033[%d;%dH", y, x);    //在ANSI转义序列中,行号在前,列号在后,这与通常的(x,y)坐标习惯相反。
}

// 清屏
void clear_screen() {
    printf("\033[2J");
}

// 绘制边框
void draw_border(int x, int y, int width, int height, const char* title) {
    int i, j;
    
    // 绘制标题
    set_cursor_pos(x + 2, y);   // 标题文字:在边框左上角(x + 2)的列位置开始
    printf("\033[1m%s\033[0m", title);
    
    // 绘制上边框
    set_cursor_pos(x, y + 1);
    printf("⚄");
    for (i = 0; i < width; i++) printf("⚄");
    printf("⚄");
    
    // 绘制左右边框
    for (j = 2; j < height + 2; j++) {
        set_cursor_pos(x, y + j);
        printf("▋");
        set_cursor_pos(x + width + 1, y + j);
        printf("▋");
    }
    
    // 绘制下边框
    set_cursor_pos(x, y + height + 2);
    printf("⚄");
    for (i = 0; i < width; i++) printf("⚄");
    printf("⚄");
}

// 初始化游戏:游戏首次启动、玩家按 r 重新开始,需要重置所有游戏状态时,调用此函数
void init_game() {
    int i, j;
    for (i = 0; i < HEIGHT + BUFFER_HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            board[i][j] = -1; // 用 -1 标记所有位置为空
        }
    }
    
    // 初始化随机数生成器,用当前时间作为随机种子,确保每次游戏运行都有不同的随机序列
    // srand() 为 rand()函数设置起始点(种子),time(NULL) 获取当前时间
    srand(time(NULL));

    // 设置初始方块
    current_piece = rand() % 7; // 当前方块 (0-6), 7种不同形状的方块(I, J, L, O, S, T, Z)
    next_piece = rand() % 7;    // 下一个方块 (0-6)

    // 设置方块初始位置
    current_x = WIDTH / 2 - 1;  // 让方块在水平方向大致居中
    current_y = BUFFER_HEIGHT;  // 在缓冲区顶部开始下落,给玩家反应时间

    // 重置游戏状态变量
    score = 0;
    level = 1;
    lines_cleared = 0;  // 消除行数归零
    game_over = 0;      // 游戏运行状态
}

/* 
    检查碰撞,检查指定位置和旋转状态的方块 是否会与 边界 或其他方块 发生碰撞。
    返回 1:发生碰撞,位置无效。       返回 0:没有碰撞,位置有效。
*/
int check_collision(int piece, int rotation, int x, int y) {
    int i;
    for (i = 0; i < 4; i++) {
        int block_x = x + shapes[piece][rotation][i][0]; // 计算实际x坐标
        int block_y = y + shapes[piece][rotation][i][1]; // 计算实际y坐标
        
        // 检查是否:超出左边界,超出右边界,超出下边界
        if (block_x < 0 || block_x >= WIDTH || block_y >= HEIGHT + BUFFER_HEIGHT) {
            return 1;
        }
        // 方块间碰撞检测:只在游戏区域内检查 && 该位置 是否已被其他方块占据
        if (block_y >= 0 && board[block_y][block_x] != -1) {
            return 1;
        }
    }
    return 0;
}

// 锁定方块到游戏区域,标记已被占用
void lock_piece() {
    int i;
    for (i = 0; i < 4; i++) {
        int block_x = current_x + shapes[current_piece][current_rotation][i][0];
        int block_y = current_y + shapes[current_piece][current_rotation][i][1];
        if (block_y >= 0) {
            board[block_y][block_x] = current_piece;  // 记录方块形状的编号,表示该方块已被占用
        }
    }
}

// 清除完整的行
void clear_lines() {
    int i, j, k;
    int lines_to_clear = 0;
    
    for (i = HEIGHT + BUFFER_HEIGHT - 1; i >= 0; i--) 
    {
        int line_full = 1;
        for (j = 0; j < WIDTH; j++) {
            if (board[i][j] == -1) {
                line_full = 0;
                break;
            }
        }
        
        if (line_full) {
            lines_to_clear++;
            // 移动所有行向下
            for (k = i; k > 0; k--) {
                for (j = 0; j < WIDTH; j++) {
                    board[k][j] = board[k-1][j];
                }
            }
            // 清空顶行
            for (j = 0; j < WIDTH; j++) {
                board[0][j] = -1;
            }
            i++; // 重新检查当前行
        }
    }
    
    if (lines_to_clear > 0) 
    {
        lines_cleared += lines_to_clear; // 累计消除的总行数
        
        if(lines_to_clear <=2)
            score += lines_to_clear * 10;
        else if(lines_to_clear == 3)
            score += 50;
        else if(lines_to_clear == 4)
            score += 100;

        // 可以修改提高升级的速度,方便测试
        level = lines_cleared / 5 + 1;  // 每消除5行 升1级,难度渐进
    }
}


// 生成新方块
void new_piece() {
    current_piece = next_piece;
    next_piece = rand() % 7;
    current_x = WIDTH / 2 - 1;
    current_y = BUFFER_HEIGHT;
    current_rotation = 0;
    
    if (check_collision(current_piece, current_rotation, current_x, current_y)) {
        game_over = 1;
    }
}

// 绘制游戏区域
void draw_board() {
    int i, j;
    
    // 绘制游戏区域边框
    draw_border(2, 1, WIDTH * 2, HEIGHT, "俄罗斯方块");
    
    // 绘制游戏区域内容
    for (i = BUFFER_HEIGHT; i < HEIGHT + BUFFER_HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) // 游戏面板的列索引
        {
            // 3:游戏区域 边框左边的偏移量,j * 2:每个游戏格子占2个字符宽度(为了显示方块更美观)方块看起来更方正
            // i - BUFFER_HEIGHT:将游戏面板 行索引 转换为 屏幕行索引(去掉缓冲区偏移)
            // + 3:游戏区域 距离边框上边的偏移量
            set_cursor_pos(3 + j * 2, i - BUFFER_HEIGHT + 3);
            if (board[i][j] != -1) {
                
                // board[i][j] 颜色数组索引,获取方块类型,取值范围:-1~6,空位置 -1,7种不同颜色的方块类型 0-6
                // colors[] 数组建立了类型到颜色的映射关系
                // \033[0m 确保颜色不影响后续输出
                printf("%s🟥\033[0m", colors[board[i][j]]);
            } else {
                printf("  ");
            }
        }
    }
    
    // 绘制当前下落的方块:循环遍历当前方块的4个组成块,计算每个块在游戏区域中的实际位置并绘制。
    for (i = 0; i < 4; i++) // 每个俄罗斯方块由4个小方块组成
    {
        // 计算每个块的实际坐标:current_piece 当前方块类型(0-6),current_rotation 当前旋转状态(0-3)
        int block_x = current_x + shapes[current_piece][current_rotation][i][0];
        int block_y = current_y + shapes[current_piece][current_rotation][i][1];
        // 检查是否在可见区域,只绘制在可见区域内的方块(y坐标 ≥ 缓冲区高度:4)
        if (block_y >= BUFFER_HEIGHT) 
        {
            set_cursor_pos(3 + block_x * 2, block_y - BUFFER_HEIGHT + 3);
            printf("%s🟥\033[0m", colors[current_piece]);
        }
    }
}

// 绘制下一个方块预览
void draw_next_piece() {
    int i;
    
    // 绘制预览区域边框
    draw_border(30, 1, 13, 5, "下一个");
    
    // 清空预览区域
    for (i = 2; i < 7; i++) {
        set_cursor_pos(33, i + 1);
        printf("          ");
    }
    
    // 绘制下一个方块
    for (i = 0; i < 4; i++) {
        int block_x = shapes[next_piece][0][i][0];
        int block_y = shapes[next_piece][0][i][1];
        set_cursor_pos(34 + block_x * 2, block_y + 4);
        printf("%s🟥 \033[0m", colors[next_piece]);
    }
}

// 绘制分数和信息
void draw_info() {
    // 绘制信息区域边框
    draw_border(30, 9, 13, 7, "游戏信息");
    
    set_cursor_pos(31, 11);
    printf("分数: %d", score);
    
    set_cursor_pos(31, 13);
    printf("等级: %d", level);
    
    set_cursor_pos(31, 15);
    printf("行数: %d", lines_cleared);
    
    // 绘制控制说明
    draw_border(50, 1, 17, 13, "控制说明");  // 增加高度容纳所有文字

    set_cursor_pos(51, 3);
    printf("<- -> : 移动");
    set_cursor_pos(51, 4);
    printf("  ^   : 旋转");
    set_cursor_pos(51, 5);
    printf("  v   : 加速");
    set_cursor_pos(51, 6);
    printf("空格  : 直接落下");
    set_cursor_pos(51, 7);
    printf("  P   : 暂停");
    set_cursor_pos(51, 8);
    printf("  Q   : 退出");

    set_cursor_pos(51, 10);
    printf("消除1行 + 10分");
    set_cursor_pos(51, 11);
    printf("消除2行 + 20分");
    set_cursor_pos(51, 12);
    printf("消除3行 + 50分");
    set_cursor_pos(51, 13);
    printf("消除4行 + 100分");
    set_cursor_pos(51, 14);
    printf("消除5行 升级");
    set_cursor_pos(51, 15);
    printf("15级 最快速度");
}

// 绘制游戏结束画面
void draw_game_over() {
    set_cursor_pos(5, 4);
    printf("\033[43m\033[30m游戏结束!\033[0m");
    set_cursor_pos(5, 6);
    printf("\033[43m\033[30m最终分数: %d \033[0m", score);
    set_cursor_pos(5, 8);
    printf("\033[43m\033[30m按 'q' 退出 \033[0m");
    set_cursor_pos(5, 10);
    printf("\033[43m\033[30m按 'r' 重新开始 \033[0m");
}

// 绘制整个游戏界面
void draw_game() 
{
    static int first_draw = 1;
    
    if (first_draw) {
        clear_screen();  // 只在第一次清屏
        first_draw = 0;
    }
    draw_next_piece();
    draw_board();
    draw_info();

    if (game_over) {
        draw_game_over();
    }
    
    // fflush() 确保所有绘制命令 立即显示在屏幕上
    // 强制刷新输出(标准输出stdout: 通常是行缓冲)
    fflush(stdout);
}

/* 
    处理输入:read() 系统调用,非阻塞读取. STDIN_FILENO=0: 用户在键盘的输入,&c:读取数据存储的地址,1:要读取的字节数
    返回值判断 > 0 成功读取到字符(返回读取的字节数),= 0 到达文件末尾(EOF), < 0 读取错误
    为什么不用 getchar() 或 scanf()?阻塞等待,直到用户按回车。
    游戏循环中需要非阻塞检测,不等待输入,实时响应,无需按回车。
*/
void process_input() {
    char c;
    // 读取键盘输入
    while (read(STDIN_FILENO, &c, 1) > 0)
    {
        if (game_over) {
            if (c == 'q' || c == 'Q') {
                clear_screen();
                set_cursor_pos(1,1); // 光标设置在左上角
                exit(0);
            } else if (c == 'r' || c == 'R') {
                init_game();
                return;
            }
        }
        
        if (c == 'q' || c == 'Q') {
            clear_screen();
            set_cursor_pos(1,1);
            exit(0);
        } else if ((c == 'p' || c == 'P') && !game_over) {
            // 暂停功能
            set_cursor_pos(8, 4);
            printf("\033[43m\033[30m游戏暂停\033[0m");
            set_cursor_pos(5, 6);
            printf("\033[43m\033[30m按任意键继续...\033[0m");
            fflush(stdout);
            
            // 等待按键继续
            struct termios old_term, new_term;
            tcgetattr(STDIN_FILENO, &old_term);
            new_term = old_term;
            // 设置为非规范模式,禁用回显
            new_term.c_lflag &= ~(ICANON | ECHO);
            new_term.c_cc[VMIN] = 1;   // 读取1个字符
            new_term.c_cc[VTIME] = 0;  // 无超时
            tcsetattr(STDIN_FILENO, TCSANOW, &new_term);

            // 读取1个字符(不需要清空缓冲区)
            char ch;
            read(STDIN_FILENO, &ch, 1); // 等待玩家 按任意键,恢复游戏
            tcsetattr(STDIN_FILENO, TCSANOW, &old_term); // 恢复原始设置
            
            draw_game();
        } else if (!game_over) {
            if (c == '\033') { // 方向键
                read(STDIN_FILENO, &c, 1); // 读取1个字符
                if (c == '[') {
                    read(STDIN_FILENO, &c, 1); // 读取1个字符
                    switch (c) {
                        case 'A': // 按键上 - 旋转
                            {
                                int new_rotation = (current_rotation + 1) % 4;
                                if (!check_collision(current_piece, new_rotation, current_x, current_y)) {
                                    current_rotation = new_rotation;
                                }
                            }
                            break;
                        case 'B': // 按键下 - 加速下落
                            if (!check_collision(current_piece, current_rotation, current_x, current_y + 1)) {
                                current_y++;
                            }
                            break;
                        case 'C': // 按键右
                            if (!check_collision(current_piece, current_rotation, current_x + 1, current_y)) {
                                current_x++;
                            }
                            break;
                        case 'D': // 按键左
                            if (!check_collision(current_piece, current_rotation, current_x - 1, current_y)) {
                                current_x--;
                            }
                            break;
                    }
                }
            } 
            else if (c == ' ') { // 空格 - 直接落下
                while (!check_collision(current_piece, current_rotation, current_x, current_y + 1)) {
                    current_y++;
                }
                lock_piece();   // 标记方块已被占用
                clear_lines();
                new_piece();
            }
        }
        // 每次输入处理后检查游戏状态并立即更新界面
        if (game_over) {
            draw_game_over();
            fflush(stdout);
        }
    }
}

// 游戏主循环
void game_loop() {
    int frames = 0;   // 帧计数器
    int drop_interval;// 下落速度 随等级增加
    
    while (1) 
	{
        process_input(); // 非阻塞读取键盘输入
    	if(level <= 14) 
			drop_interval = 25 - level*1.5; // 下落速度随等级增加
        else
            drop_interval = 3; // 最快速度限幅
		
        if (!game_over) {
            frames++;
            
            // 等级越低,frames越大,延时更长,自动下落更慢
            if (frames >= drop_interval) {
                frames = 0;
                if (!check_collision(current_piece, current_rotation, current_x, current_y + 1)) {
                    current_y++;
                } else {
                    lock_piece();
                    clear_lines(); // 检查并清除完整行
                    new_piece();   // 生成新方块
                }
            }
            
            draw_game();
        }
        
        usleep(40000); // 40ms
    }
}

int main() 
{
    init_terminal();
    init_game();
    draw_game();
    game_loop();
    
    return 0;
}

DeepSeek 辅助生成,测试完后,我对部分代码以及显示效果,做了优化。

最后的运行效果如图:

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐