c_tetris

第 11 课:最高分系统 🏆

11.1 本课目标

学会实现最高分记录系统,掌握文件 I/O 和数据结构的设计。


11.2 为什么需要最高分系统?

游戏设计角度

最高分系统的作用:

技术学习角度

学到的技能:


11.3 数据结构设计

单条记录

typedef struct {
    char name[32];   // 玩家名字
    int score;       // 分数
    int lines;       // 消除行数
    int level;       // 等级
    time_t date;     // 时间戳
} HighScore;

为什么这些字段?

字段 类型 作用
name char[32] 玩家名字,32 字节足够
score int 分数,用于排序
lines int 消除行数,额外信息
level int 等级,额外信息
date time_t 时间戳,记录达成时间

记录数组

#define MAX_SCORES 10  // 保存前 10 名

HighScore high_scores[MAX_SCORES];

为什么是 10 名?


11.4 保存最高分

完整实现

void save_high_score(const char* name, int score, 
                     int lines, int level) {
    // 1. 创建新记录
    HighScore new_score = {
        .score = score,
        .lines = lines,
        .level = level,
        .date = time(NULL)  // 当前时间
    };
    strncpy(new_score.name, name, 31);
    new_score.name[31] = '\0';  // 确保字符串结束
    
    // 2. 插入到排序数组中
    for (int i = 0; i < MAX_SCORES; i++) {
        if (score > high_scores[i].score) {
            // 3. 后移后面的记录
            for (int j = MAX_SCORES - 1; j > i; j--) {
                high_scores[j] = high_scores[j-1];
            }
            // 4. 插入新记录
            high_scores[i] = new_score;
            break;
        }
    }
    
    // 5. 保存到文件
    FILE* f = fopen("data/highscores.txt", "w");
    if (!f) return;
    
    for (int i = 0; i < MAX_SCORES; i++) {
        if (high_scores[i].score > 0) {
            fprintf(f, "%s %d %d %d %ld\n",
                    high_scores[i].name,
                    high_scores[i].score,
                    high_scores[i].lines,
                    high_scores[i].level,
                    high_scores[i].date);
        }
    }
    
    fclose(f);
}

插入排序详解

初始状态(按分数降序):
[0] Alice   5000
[1] Bob     3200
[2] Carol   1500
[3] Dave    1000
...

新分数:2500

步骤 1:找到插入位置
i=0: 2500 > 5000? 否,继续
i=1: 2500 > 3200? 否,继续
i=2: 2500 > 1500? 是!插入位置是 i=2

步骤 2:后移后面的记录
[9] = [8]
[8] = [7]
...
[3] = [2]

步骤 3:插入新记录
[2] = 新记录 (2500)

结果:
[0] Alice   5000
[1] Bob     3200
[2] You     2500  ← 新记录
[3] Carol   1500
[4] Dave    1000
...

11.5 加载最高分

从文件读取

void load_high_scores(void) {
    FILE* f = fopen("data/highscores.txt", "r");
    if (!f) {
        // 文件不存在,初始化为空
        memset(high_scores, 0, sizeof(high_scores));
        return;
    }
    
    // 读取记录
    for (int i = 0; i < MAX_SCORES; i++) {
        int result = fscanf(f, "%31s %d %d %d %ld",
                           high_scores[i].name,
                           &high_scores[i].score,
                           &high_scores[i].lines,
                           &high_scores[i].level,
                           &high_scores[i].date);
        
        if (result != 5) {
            // 读取失败或文件结束
            // 填充剩余为空记录
            for (int j = i; j < MAX_SCORES; j++) {
                memset(&high_scores[j], 0, sizeof(HighScore));
            }
            break;
        }
    }
    
    fclose(f);
}

fscanf 格式说明

fscanf(f, "%31s %d %d %d %ld", ...)

格式符说明:
%31s  - 读取字符串,最多 31 个字符(留 1 个给\0
%d    - 读取整数(score
%d    - 读取整数(lines
%d    - 读取整数(level
%ld   - 读取长整数(time_t date

返回值:成功读取的字段数
如果返回 5,说明成功读取一条完整记录

11.6 判断是否新高分

bool is_new_high_score(int score) {
    // 如果还没填满,肯定是新高分
    for (int i = 0; i < MAX_SCORES; i++) {
        if (high_scores[i].score == 0) {
            return true;
        }
    }
    
    // 如果分数超过最后一名,是新高分
    return score > high_scores[MAX_SCORES - 1].score;
}

优化:提前判断

// 在游戏结束时,先判断是否可能上榜
if (!is_new_high_score(game->score)) {
    // 不可能上榜,直接显示排行榜
    show_high_scores();
    return;
}

// 可能上榜,显示输入名字界面
// ...

11.7 输入名字界面

完整实现

if (is_new_high_score(game->score)) {
    // 1. 显示输入界面
    clear();
    mvprintw(5, 10, "╔══════════════════════════════╗");
    mvprintw(6, 10, "║    NEW HIGH SCORE!           ║");
    mvprintw(7, 10, "║    Score: %-6d              ║", game->score);
    mvprintw(8, 10, "║    Enter your name:          ║");
    mvprintw(9, 10, "║    >                       < ║");
    mvprintw(10, 10, "╚══════════════════════════════╝");
    refresh();
    
    // 2. 获取用户输入
    echo();          // 显示输入字符
    curs_set(1);     // 显示光标
    move(9, 35);     // 移动到输入位置
    
    char name[32] = {0};
    int i = 0;
    int ch;
    
    while ((ch = getch()) != '\n' && ch != KEY_ENTER && i < 31) {
        if (ch == 127 || ch == KEY_BACKSPACE || ch == 8) {
            // 退格
            if (i > 0) {
                i--;
                name[i] = '\0';
                mvaddch(9, 35 + i, ' ');
                move(9, 35 + i);
            }
        } else if (ch >= 32 && ch <= 126) {
            // 可打印字符
            name[i] = ch;
            i++;
            addch(ch);
        }
        refresh();
    }
    
    name[i] = '\0';
    
    // 3. 默认名字
    if (strlen(name) == 0) {
        strcpy(name, "Player");
    }
    
    // 4. 保存
    save_high_score(name, game->score, 
                    game->lines_cleared, game->level);
    
    // 5. 恢复设置
    noecho();
    curs_set(0);
}

输入处理详解

退格处理:

if (ch == 127 || ch == KEY_BACKSPACE || ch == 8) {
    if (i > 0) {
        i--;                    // 索引减 1
        name[i] = '\0';        // 字符串截断
        mvaddch(9, 35 + i, ' ');// 擦除屏幕上的字符
        move(9, 35 + i);       // 移动光标
    }
}

为什么检查多个退格码?


11.8 显示排行榜

格式化输出

void show_high_scores(void) {
    printf("\n╔══════════════════════════════════════╗\n");
    printf("║         TETRIS HIGH SCORES           ║\n");
    printf("╠══════╤═══════════╤═══════╤═══════════╣\n");
    printf("║ Rank │   Name    │ Score │   Level   ║\n");
    printf("╠══════╪═══════════╪═══════╪═══════════╣\n");
    
    for (int i = 0; i < MAX_SCORES; i++) {
        if (high_scores[i].score > 0) {
            printf("║  %2d  │ %-9s │ %5d │    %2d     ║\n",
                   i + 1,
                   high_scores[i].name,
                   high_scores[i].score,
                   high_scores[i].level);
        } else {
            printf("║  %2d  │           │       │           ║\n", i + 1);
        }
    }
    
    printf("╚══════╧═══════════╧═══════╧═══════════╝\n\n");
}

格式说明符

"%2d"   - 右对齐,2 位宽度
"%-9s"  - 左对齐,9 位宽度(名字)
"%5d"   - 右对齐,5 位宽度(分数)

效果:

╔══════════════════════════════════════╗
║         TETRIS HIGH SCORES           ║
╠══════╤═══════════╤═══════╤═══════════╣
║ Rank │   Name    │ Score │   Level   ║
╠══════╪═══════════╪═══════╪═══════════╣
║   1  │ Alice     │  5000 │     8     ║
║   2  │ Bob       │  3200 │     6     ║
║   3  │ Player    │  1250 │     5     ║
╚══════╧═══════════╧═══════╧═══════════╝

11.9 文件格式

highscores.txt 格式

Alice 5000 42 8 1708934400
Bob 3200 35 6 1708934500
Player 1250 20 5 1708934600

每行字段:

  1. 名字(字符串)
  2. 分数(整数)
  3. 消除行数(整数)
  4. 等级(整数)
  5. 时间戳(Unix 时间)

为什么用空格分隔?


✅ 本课检查清单


📝 小作业

  1. 修改最高分数量为 20 名

  2. 添加日期格式化显示(把时间戳转为可读日期)

  3. 实现删除指定排名的功能


返回目录