c_tetris

第 7 课:消行逻辑 🧹

7.1 本课目标

理解消行检测的原理,学会如何消除满行并计算分数。


7.2 什么是消行?

当游戏板的某一行被方块完全填满时:

消除前:              消除后:
┌────────┐          ┌────────┐
│        │          │        │  ← 空行
│ ████   │  ← 满行  │ ████   │  ← 上面的行下落
│███████ │          │███████ │
│████████│          │████████│
└────────┘          └────────┘
   ↑                     ↑
 第 19 行(满)消除      第 19 行变空

消行后发生什么:

  1. 满行被消除
  2. 上面的所有行下落一格
  3. 玩家获得分数
  4. 可能升级(每消除 10 行升 1 级)

7.3 为什么消行很重要?

游戏核心机制

消行是俄罗斯方块的核心玩法

玩家目标:
1. 放置方块
2. 形成满行
3. 消除得分
4. 避免堆满

如果没有消行:
- 方块会一直堆积
- 游戏很快结束
- 没有分数,没有乐趣

策略深度

单行 vs 多行:

策略 1:单行消除(简单)
┌────────┐
│        │
│ ████   │  ← 消除 1 行 = 40 分
│███████ │
└────────┘

策略 2:四行消除(俄罗斯方块!)
┌────────┐
│ ████   │  ← 等待 I 方块
│ ████   │  ← 一次消除 4 行
│ ████   │  ← 1200 分!
│ ████   │
└────────┘

高手会:


7.4 消行算法详解

算法步骤

步骤 1:从底部向上检查每一行
步骤 2:判断该行是否全满
步骤 3:如果满行,消除并下落上面的行
步骤 4:统计消除行数,计算分数
步骤 5:返回消除行数

代码实现

// board.c
int board_clear_lines(Board* board) {
    int cleared = 0;  // 统计消除行数
    
    // 步骤 1:从底部向上检查
    for (int y = BOARD_HEIGHT - 1; y >= 0; y--) {
        bool full = true;
        
        // 步骤 2:检查该行是否全满
        for (int x = 0; x < BOARD_WIDTH; x++) {
            if (!board->cells[y][x]) {
                full = false;  // 有空格,不是满行
                break;
            }
        }
        
        if (full) {
            cleared++;
            
            // 步骤 3:消除该行,上面的行下落
            for (int yy = y; yy > 0; yy--) {
                memcpy(board->cells[yy], 
                       board->cells[yy-1], 
                       sizeof(board->cells[0]));
            }
            
            // 清空顶行
            memset(board->cells[0], 0, sizeof(board->cells[0]));
            
            // 重要:重新检查当前行(因为上面的行下落了)
            y++;
        }
    }
    
    return cleared;
}

7.5 代码详解

为什么从底部向上检查?

for (int y = BOARD_HEIGHT - 1; y >= 0; y--)
//     ↑ 从最底部开始

原因:

对比:

❌ 从顶部向下检查:
行 0: 满行 → 消除,行 1-19 下落
     ↓
行 1: 现在是原来的行 2(跳过了原来的行 1!)
     ↓
结果:漏检!

✅ 从底部向上检查:
行 19: 满行 → 消除,行 0-18 下落
     ↓
行 18: 现在是原来的行 17(正确!)
     ↓
结果:全部检查到!

为什么需要 y++

if (full) {
    // ... 消除逻辑
    y++;  // 重要!重新检查当前行
}

原因图解:

初始状态:
行 19: ████ ████  ← 满行,消除
行 18: ████ ████  ← 满行
行 17: ████ ████  ← 满行

消除行 19 后,行 18 下落到行 19:
行 19: ████ ████  ← 原来的行 18(现在是满行!)
行 18: ████ ████  ← 原来的行 17
行 17: 空行       ← 新下落的空行

如果不 y++:
- 下一次循环 y=18
- 漏掉了新的行 19(可能也是满行)

如果 y++:
- y++ 后,循环 y--,还是检查 y=19
- 正确检查新的行 19

memcpy 的作用

memcpy(board->cells[yy], 
       board->cells[yy-1], 
       sizeof(board->cells[0]));

这行代码做什么?

把上一行(yy-1)的整行数据复制到当前行(yy)

内存布局:
cells[yy-1]: [0][1][2][3][4][5][6][7][8][9]  ← 复制这 10 个 int
    ↓
cells[yy]:   [0][1][2][3][4][5][6][7][8][9]  ← 粘贴到这里

效果:整行下落

为什么用 memcpy?

// 方案 1:逐个元素复制(慢)
for (int x = 0; x < BOARD_WIDTH; x++) {
    board->cells[yy][x] = board->cells[yy-1][x];
}

// 方案 2:memcpy(快)
memcpy(board->cells[yy], 
       board->cells[yy-1], 
       sizeof(board->cells[0]));

// memcpy 是底层内存复制,速度更快

7.6 计分系统

计分规则

// score.c
#define SCORE_SINGLE  40    // 1 行
#define SCORE_DOUBLE  100   // 2 行
#define SCORE_TRIPLE  300   // 3 行
#define SCORE_TETRIS  1200  // 4 行(俄罗斯方块!)

int score_calculate_lines(int lines, int level) {
    int base_scores[] = {0, 40, 100, 300, 1200};
    
    if (lines >= 1 && lines <= 4) {
        return base_scores[lines] * (level + 1);
    }
    return 0;
}

为什么分数是这样设计的?

鼓励多行消除:

等级 0 时:

4 次单行消除:40 × 4 = 160 分
1 次四行消除:1200 分

1200 ÷ 160 = 7.5 倍!

所以高手会追求一次消除 4 行,而不是慢慢消单行

等级加成:

同样消除 4 行:

等级 0: 1200 × (0+1) = 1200 分
等级 5: 1200 × (5+1) = 7200 分
等级 10: 1200 × (10+1) = 13200 分

等级越高,分数越多,鼓励挑战高难度

7.7 消行在游戏循环中的位置

// game.c
void game_update(Game* game) {
    game->current.y++;
    
    if (board_check_collision(&game->board, &game->current)) {
        game->current.y--;
        
        // 1. 锁定方块
        board_lock_tetromino(&game->board, &game->current);
        
        // 2. 检查并消除行
        int lines = board_clear_lines(&game->board);
        
        // 3. 如果有消除,更新分数
        if (lines > 0) {
            score_add(&game->score, 
                     score_calculate_lines(lines, game->level));
            game->lines_cleared += lines;
            
            // 4. 检查升级
            if (game->lines_to_level <= 0) {
                game->level++;
                game->lines_to_level = 10;
                game->drop_interval = game->drop_interval * 90 / 100;
            }
        }
        
        // 5. 生成新方块
        game->current = game->next;
        tetromino_init(&game->next, random_tetromino());
    }
}

执行顺序很重要:

  1. 先锁定方块(把方块固定到游戏板)
  2. 再检查消行(可能有行被填满了)
  3. 然后计分和升级
  4. 最后生成新方块

7.8 消行动画(扩展)

简单闪烁效果

// render.c
void render_line_clear_animation(int lines[]) {
    // 闪烁 3 次
    for (int i = 0; i < 3; i++) {
        // 显示消除的行(白色)
        for (int l = 0; l < lines_count; l++) {
            int y = lines[l];
            for (int x = 0; x < BOARD_WIDTH; x++) {
                attron(COLOR_PAIR(8));
                mvaddch(y, x * 2, "██");
                attroff(COLOR_PAIR(8));
            }
        }
        refresh();
        napms(50);
        
        // 隐藏(黑色)
        for (int l = 0; l < lines_count; l++) {
            int y = lines[l];
            for (int x = 0; x < BOARD_WIDTH; x++) {
                mvaddstr(y, x * 2, "  ");
            }
        }
        refresh();
        napms(50);
    }
}

7.9 调试技巧

显示消除行数

int lines = board_clear_lines(&game->board);
if (lines > 0) {
    printf("消除了 %d 行!得分:%d\n", 
           lines, 
           score_calculate_lines(lines, game->level));
}

可视化游戏板

void print_board(Board* board) {
    for (int y = 0; y < BOARD_HEIGHT; y++) {
        printf("|");
        for (int x = 0; x < BOARD_WIDTH; x++) {
            if (board->cells[y][x]) {
                printf("█");
            } else {
                printf(" ");
            }
        }
        printf("|\n");
    }
}

✅ 本课检查清单


📝 小作业

  1. 修改消行逻辑,从顶部向下检查,观察会出现什么问题

  2. 如果不使用 memcpy,改用 for 循环逐个元素复制,代码怎么写?

  3. 实现一个简单的消行动画(闪烁效果)


下一课:UI 界面