C语言贪吃蛇:基于Linux中ncurses库实的贪吃蛇小游戏

文章目录

  • C语言贪吃蛇:基于Linux中ncurses库实的贪吃蛇小游戏
    • 一、Linux系统环境配置与编译
      • [1. 安装ncurses库](#1. 安装ncurses库)
      • [2. 编译命令](#2. 编译命令)
    • 二、核心函数详解(ncurses库+系统调用)
      • [1. ncurses库函数(终端交互与绘图核心)](#1. ncurses库函数(终端交互与绘图核心))
      • [2. 系统调用与标准库函数](#2. 系统调用与标准库函数)
    • 三、菜单交互与功能分发
    • 四、数据结构实现
      • [1. 蛇身存储:双向循环链表](#1. 蛇身存储:双向循环链表)
      • [2. 历史记录存储:自定义动态数组](#2. 历史记录存储:自定义动态数组)
    • 五、游戏核心逻辑
      • [1. 游戏初始化](#1. 游戏初始化)
      • [2. 蛇身移动逻辑](#2. 蛇身移动逻辑)
      • [3. 碰撞检测](#3. 碰撞检测)
      • [4. 豆子生成与吃豆逻辑](#4. 豆子生成与吃豆逻辑)
      • [5. 胜负判断](#5. 胜负判断)
    • 六、辅助功能
      • [1. 设置菜单](#1. 设置菜单)
      • [2. 历史记录菜](#2. 历史记录菜)
      • [3. 历史记录清空](#3. 历史记录清空)
    • 七、资源释放与总结
    • 八、完整代码汇总
      • [1. Mylist.h(双向循环链表头文件)](#1. Mylist.h(双向循环链表头文件))
      • [2. Mylist.c(双向循环链表实现文件)](#2. Mylist.c(双向循环链表实现文件))
      • [3. Myvector.h(自定义动态数组头文件)](#3. Myvector.h(自定义动态数组头文件))
      • [4. Myvector.c(自定义动态数组实现文件)](#4. Myvector.c(自定义动态数组实现文件))
      • [5. Snake.h(游戏核心头文件,含宏定义、结构体声明)](#5. Snake.h(游戏核心头文件,含宏定义、结构体声明))
      • [6. Snake.c(游戏核心实现文件)](#6. Snake.c(游戏核心实现文件))

C语言贪吃蛇:基于Linux中ncurses库实的贪吃蛇小游戏

本项目基于C语言结合ncurses库开发终端版贪吃蛇,可以在Linux系统中运行,无图形库,只有终端效果。文章较长,建议点击目录跳转。

先简单说明用到的技术:核心开发语言为C,终端画面绘制与按键响应通过ncurses库实现,游戏配置与历史记录文件级存储,确保程序关闭后数据不丢失。数据结构选型贴合实际操作需求,蛇身采用双向循环链表存储,历史记录通过自定义动态数组管理,兼顾运行效率与实用性。

效果展示

一、Linux系统环境配置与编译

1. 安装ncurses库

Ubuntu/Debian系:

bash 复制代码
sudo apt-get install libncursesw5-dev

CentOS/RHEL系:

bash 复制代码
sudo yum install ncurses-devel

2. 编译命令

bash 复制代码
gcc Mylist.c Myvector.c Snake.c -o snake -lncursesw

说明:-lncursesw适配宽字符,避免蛇头/豆子符号乱码;取消对应系统命令前的注释即可使用。

二、核心函数详解(ncurses库+系统调用)

以下按功能分类,将项目中涉及的ncurses库函数及系统调用函数做简单介绍

1. ncurses库函数(终端交互与绘图核心)

(1)初始化与清理函数

  • initscr():ncurses初始化函数,用于启动终端图形模式,创建默认标准窗口(stdscr),并初始化ncurses内部数据结构。项目中在Init()函数首步调用,为后续终端绘图与按键响应奠定基础,必须在所有ncurses函数前调用。

  • endwin():ncurses清理函数,用于关闭终端图形模式,恢复终端默认状态(如显示光标、启用按键回显)。项目中在End()函数中调用,避免终端因ncurses占用陷入异常状态,是程序退出前的必要操作。

(2)窗口操作函数

  • newwin(int nlines, int ncols, int begin_y, int begin_x):创建新窗口,参数分别为窗口行数、列数、左上角纵坐标、左上角横坐标。项目中在HistoryScores()函数中创建历史记录窗口(scrresult),实现独立区域的记录展示,与游戏主窗口隔离。

  • delwin(WINDOW *win):销毁指定窗口并释放内存,参数为目标窗口指针。项目中在历史记录查看完毕后调用,销毁scrresult窗口,防止内存泄漏。

  • wclear(WINDOW *win):清空指定窗口内容,参数为目标窗口指针。项目中切换历史记录页码时调用,清除当前页内容后绘制新页,避免视觉残留。

  • box(WINDOW *win, chtype verch, chtype horch):为指定窗口绘制边框,参数分别为目标窗口指针、垂直边框字符、水平边框字符(传0时使用默认边框字符)。项目中为历史记录窗口绘制边框,提升视觉规整度。

  • refresh():刷新标准窗口(stdscr)的缓冲区,将绘制内容同步到终端。项目中菜单切换、标题绘制后调用,确保内容即时显示。

  • wrefresh(WINDOW *win):刷新指定窗口的缓冲区,参数为目标窗口指针。项目中游戏分数更新、历史记录绘制后调用,适配独立窗口的内容同步需求。

(3)绘图与属性控制函数

  • mvprintw(int y, int x, const char *fmt, ...):移动光标到指定位置(y行x列),并在标准窗口打印格式化字符串。项目中绘制菜单标题、选项内容时使用,实现内容的精准定位显示。

  • mvwprintw(WINDOW *win, int y, int x, const char *fmt, ...):移动光标到指定窗口的(y行x列)位置,打印格式化字符串,参数比mvprintw多一个目标窗口指针。项目中在历史记录窗口、游戏分数窗口中打印内容时使用。

  • mvwaddwstr(WINDOW *win, int y, int x, const wchar_t *str):在指定窗口的(y行x列)位置添加宽字符字符串,支持特殊符号显示。项目中绘制蛇头时使用,确保蛇头符号正常渲染。

  • attron(chtype attrs):为标准窗口启用文本属性(如加粗、颜色对),参数为属性标识。项目中绘制菜单标题时,启用COLOR_PAIR(2) | A_BOLD,实现蓝色加粗效果。

  • attroff(chtype attrs):关闭标准窗口的指定文本属性,与attron配套使用,避免属性影响后续内容。

  • wattron(WINDOW *win, chtype attrs):为指定窗口启用文本属性,参数为窗口指针和属性标识。项目中绘制蛇头时,为游戏窗口启用蛇头对应的颜色对。

  • wattroff(WINDOW *win, chtype attrs):关闭指定窗口的文本属性,与wattron配套使用。

(4)按键处理函数

  • getch():从标准窗口读取一个字符(阻塞模式,等待用户按键)。项目中菜单选择时使用,等待用户按上下键切换选项、回车键确认。

  • wgetch(WINDOW *win):从指定窗口读取字符,参数为目标窗口指针。项目中在历史记录窗口、游戏窗口中读取按键,适配独立窗口的交互需求。

  • nodelay(WINDOW *win, bool bf):设置窗口为非阻塞模式(bf为TRUE时启用),此时wgetch/getch会立即返回,无按键时返回ERR。项目中在SnakeMove()函数中启用,实现蛇的自动移动(无按键时保持当前方向)。

  • keypad(WINDOW *win, bool bf):启用/禁用窗口对功能键(方向键、功能键F1-F12)的支持(bf为TRUE时启用)。项目中为菜单窗口、游戏窗口、历史记录窗口启用,确保方向键正常响应。

(5)颜色管理函数

  • start_color():初始化ncurses颜色系统,启用颜色支持。项目中在Init()函数中调用,为后续颜色对配置奠定基础。

  • init_pair(short pair, short fg, short bg):定义颜色对,参数分别为颜色对编号、前景色、背景色。项目中定义3组颜色对,分别用于白字黑底(默认文本)、蓝字黑底(标题)、黄字黑底(豆子)。

  • curs_set(int visibility):设置光标可见性,参数0为隐藏、1为正常、2为高亮。项目中设置为0,隐藏光标提升游戏视觉体验。

2. 系统调用与标准库函数

项目中涉及的系统调用及标准库函数,主要用于文件IO、内存管理、时间控制等。

(1)文件管理函数

  • fopen(const char *pathname, const char *mode):打开指定文件,返回文件指针,参数为文件路径和打开模式(如"w"为写入、"r"为读取)。项目中打开config.txt(配置文件)和gamehestroy.txt(历史记录文件)时使用。
  • fclose(FILE *stream):关闭文件流,释放文件资源,参数为文件指针。项目中写入配置、历史记录后调用,避免文件句柄泄漏。
  • fprintf(FILE *stream, const char *fmt, ...):向文件流写入格式化字符串,参数为文件指针和格式化内容。项目中保存蛇的主题配置、历史记录到文件时使用。

(2)内存管理函数

  • realloc(void *ptr, size_t size):调整已分配内存的大小,参数为原内存指针和新内存大小。项目中动态数组扩容时使用,将数组容量翻倍并迁移数据。

  • free(void *ptr):释放已分配的内存,参数为内存指针。项目中销毁链表、动态数组,以及释放单条历史记录时使用,防止内存泄漏。

(3)时间与延迟函数

  • usleep(useconds_t usec):使进程暂停指定微秒数(1秒=1000000微秒)。项目中控制蛇的移动速度,通过attribute.speed调整暂停时间,数值越小蛇移动越快。

  • time(time_t *t):获取当前系统时间(秒级),返回时间戳。项目中作为srand()的种子,确保每次运行游戏时豆子位置随机。

  • srand(unsigned int seed):初始化随机数生成器,参数为随机种子。项目中结合time(NULL)使用,生成不同的随机数序列,保证豆子位置不重复。

(4)进程控制函数

  • exit(int status):终止当前进程,参数为退出状态码(非0表示异常)。项目中RunOption函数处理异常选项时调用,终止程序并报错。

  • perror(const char *s):打印系统错误信息,参数为自定义错误提示。项目中选项处理异常时使用,辅助定位错误原因。

三、菜单交互与功能分发

这是用户交互的入口,负责绘制菜单、响应上下键选择,再根据选项分发到对应功能(开始游戏/历史记录/设置/退出),核心是解决按键干扰和选中状态切换问题。

效果展示:

核心代码

c 复制代码
// 绘制菜单+读取用户选择,返回选中选项编号
int ShowMenu(int option)
{
    int input = 0;
    attron(COLOR_PAIR(2) | A_BOLD); // 启用2号色对(蓝字黑底)+加粗,绘制标题
    mvprintw(LINES/2 - 8, COLS / 2 - 6, "贪吃蛇大作战"); // 光标居中,打印标题(ncurses坐标:y行x列)
    attroff(COLOR_PAIR(2) | A_BOLD); // 关闭属性,避免影响后续内容

    // 循环绘制4个菜单选项(开始/历史/设置/退出)
    for(int i = 0; i < 4; i++)
    {
        mvprintw(LINES/2 -3 + (i * 2), COLS/2 - 5 , massage1[i]);
    }
    mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "->"); // 绘制选中标记

    // 阻塞读取按键,按回车确认退出循环
    while(input != ENTRE)
    {
        input = getch(); // 读取标准窗口按键
        FROMINPUT(input); // 统一按键(wasd转方向键,过滤无效键)
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "  "); // 清除旧选中标记
        
        if(input == KEY_UP) 
        {
            option = (option == 0 ? 3 : option - 1); // 循环上选(顶部切底部)
        }
        else if(input == KEY_DOWN) 
        {
            option = (option == 3 ? 0 : option + 1); // 循环下选(底部切顶部)
        }
        
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "->"); // 绘制新选中标记
        refresh(); // 刷新标准窗口,即时显示内容
    }
    return option;
}

// 功能分发:根据选中选项执行对应操作
void RunOption(int option)
{
    switch (option)
    {
        case 0: Game(); break; // 0=开始游戏
        case 1: HistoryScores(); break; // 1=查看历史记录
        case 2: Setting(); break; // 2=主题/难度设置
        case 3: End(); break; // 3=退出游戏(释放资源)
        default:
        perror("RunOption"); // 打印异常信息,辅助定位问题
        exit(-1); // 异常终止进程(非0状态码)
    }
}

关键细节

  • 选中标记用->,切换选项时先清旧标记再画新标记,避免视觉残留;

  • 依赖FROMINPUT宏统一按键,不用单独处理wasd和方向键,减少冗余代码;

  • 菜单循环选择逻辑(顶部按上切底部、底部按下切顶部),提升用户体验,避免选到边界后无法操作。

四、数据结构实现

1. 蛇身存储:双向循环链表

蛇身需要频繁执行"头部新增节点"(移动)、"尾部删除节点"(未吃豆时)操作,双向循环链表能实现O(1)增删,且通过head->prev直接获取尾节点,比单向链表更高效。

核心代码(Mylist.h/Mylist.c)

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

// 坐标结构体:存储蛇身节点、豆子的x/y位置(适配ncurses终端坐标)
typedef struct zb{
 int x;
 int y;
} type;

// 链表节点:存坐标+前后指针(双向循环结构,支持O(1)增删)
typedef struct Node{
    type val;
 struct Node* next;
 struct Node* prev;
}Node;

// 链表头:仅存头节点指针(哨兵节点,简化边界判断,避免空指针异常)
typedef struct List{
 struct Node* head;
}List;

// 核心函数声明
List* GetList(); // 创建空链表(初始化哨兵节点)
void PushFront(List* list, type val); // 头插(蛇移动时新增蛇头)
void PopBack(List* list); // 尾删(蛇未吃豆时删除蛇尾)
void Destroy(List* list); // 释放链表所有节点(防内存泄漏)

// 头插实现:复用指定位置插入逻辑,插在哨兵节点之后
void PushFront(List* list, type val)
{
 PushPos(list->head, val); 
}

// 尾删实现:通过head->prev直接获取尾节点(双向循环优势,无需遍历)
void PopBack(List* list)
{
     if(Empty(list)) // 判空保护,避免空链表删节点崩溃
     {
        printf("list is no empty!");
     }
     else
     {
        PopPos(list->head->prev); // 删除尾节点
     }
}

双向循环链表,通过哨兵节点的前驱指针可直接定位尾节点,大幅简化代码逻辑,同时提升蛇身移动时的响应速度。

2. 历史记录存储:自定义动态数组

历史记录数量不固定,静态数组无法满足需求,用动态数组(自定义vector)实现自动扩容,每次容量满时翻倍,平衡扩容频率和内存利用率。

核心代码(Myvector.h/Myvector.c)

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

// 动态数组结构体:适配历史记录动态存储
typedef struct vector
{
    const char** head; // 存储每条历史记录字符串
    int size;          // 当前有效记录数
    int capacity;      // 数组容量(初始4,满后自动扩容)
}vector;

// 核心函数声明
void InitVector(vector* vec); // 初始化数组
void Push(vector* vec, const char* val); // 尾部新增记录
ob_int CheckCapa(vector* vec); // 容量检测与扩容
void DestroyVec(vector* vec); // 释放内存(防泄漏)

// 扩容逻辑:容量满时倍增,平衡效率与内存占用
ob_int CheckCapa(vector* vec)
{
    if(vec->capacity == vec->size)
    {
        vec->capacity *= 2; // 倍增扩容,减少频繁扩容开销
        // realloc调整内存,原数据自动迁移,失败返回NULL
        vec->head = realloc(vec->head, sizeof(const char*)*vec->capacity);
    }
}

// 内存释放:逐元素释放,避免内存泄漏(C语言手动管理核心)
void DestroyVec(vector* vec)
{
    for(int i = 0;i<vec->size;i++)
    {
        free((void*)vec->head[i]); // 释放单条记录内存
        vec->head[i] = NULL; // 置空指针,避免野指针
    }
    free(vec->head); // 释放数组本身内存
    vec->capacity = 0;
    vec->size = 0;
    vec->head = NULL;
}

关键注意点

C语言没有自带动态数组,必须手动管理内存:新增记录前先调用CheckCapa检测容量,退出游戏时调用DestroyVec释放,否则会造成内存泄漏。

五、游戏核心逻辑

1. 游戏初始化

游戏启动时的基础准备工作,包括加载配置文件、读取历史记录、初始化ncurses终端环境,确保后续功能正常运行,避免出现乱码、按键无响应等问题。

核心代码(Snake.c的Init函数)

c 复制代码
void Init()
{ 
    InitAttribute(&attribute); // 加载配置(无配置用默认值)
    InitResult(&allgameresult); // 加载历史记录(无文件则初始化空数组)

    // ncurses环境初始化(缺失会导致绘图/按键异常)
    setlocale(LC_ALL, ""); // 支持中文/特殊符号,避免乱码
    initscr(); // 启动终端图形模式,创建标准窗口stdscr
    cbreak(); // 禁用行缓冲,按键立即响应(无需回车)
    noecho(); // 不回显按键,避免干扰界面
    keypad(stdscr, TRUE); // 启用功能键支持(方向键等)
    curs_set(0); // 隐藏光标,提升视觉体验

    // 初始化颜色系统,定义颜色对(前景色+背景色)
    start_color();
    init_pair(1, COLOR_WHITE, COLOR_BLACK);   // 1号:白字黑底(默认文本)
    init_pair(2, COLOR_BLUE, COLOR_BLACK);    // 2号:蓝字黑底(菜单标题)
    init_pair(3, COLOR_YELLOW, COLOR_BLACK);  // 3号:黄字黑底(豆子)
}

踩坑记录

开发初期遗漏setlocale(LC_ALL, "")配置,导致蛇头、豆子等特殊符号显示乱码;未启用keypad(stdscr, TRUE)时,方向键无法正常响应,这两项均为ncurses环境初始化的必要步骤,不可省略。

2. 蛇身移动逻辑

蛇的移动是核心玩法,本质是"头部新增节点+尾部删除节点"的组合操作,同时限制反向移动(如向上时不能直接向下),支持自动移动和按键变向。

效果展示

核心代码片段(SnakeMove函数)

c 复制代码
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore)
{
    nodelay(scrgame, TRUE); // 非阻塞模式,实现蛇自动移动
    keypad(scrgame, TRUE); // 启用游戏窗口功能键支持

    Node* head = sna->physics->head->next; // 蛇头指针(跳过哨兵节点)
    type val = head->val; // 蛇头初始坐标
    int input = 0;

    while(falg) // falg为真时游戏持续运行
    {
        input = wgetch(scrgame); // 读取游戏窗口按键(非阻塞)
        FROMINPUT(input); // 统一按键逻辑(wasd转方向键)

        // 方向控制:限制反向移动,无按键保持当前方向
        if((input == KEY_UP && sna->dire != SDOWN) || (input == ERR && sna->dire == SUP))
        {
            val.y--; // 上移(ncurses y轴从上到下递增)
            sna->dire = SUP;
        }   
        else if((input == KEY_DOWN && sna->dire != SUP)|| (input == ERR && sna->dire == SDOWN))
        {
            val.y++; // 下移
            sna->dire = SDOWN;
        }
        else if((input == KEY_LEFT && sna->dire != SRIGHT)|| (input == ERR && sna->dire == SLEFT))
        {
            val.x-=2; // 左移(特殊符号占2列,保证对齐)
            sna->dire = SLEFT;
        }
        else if((input == KEY_RIGHT && sna->dire != SLEFT) || (input == ERR && sna->dire == SRIGHT))
        {
            val.x+=2; // 右移
            sna->dire = SRIGHT;
        }

        PushFront(sna->physics, val); // 头部插入新蛇头(移动核心)

        // 未吃豆时删除尾节点,保持蛇身长度
        if(val.x != nap.x || val.y != nap.y)
        {
            mvwprintw(scrgame, sna->physics->head->prev->val.y, sna->physics->head->prev->val.x, "  "); // 清除蛇尾符号
            PopBack(sna->physics); // 删除尾节点
        }

        // 绘制新蛇头(启用对应颜色对)
        wattron(scrgame, COLOR_PAIR(attribute.color.headcol) );
        mvwaddwstr(scrgame, val.y, val.x == -1 ? 0 : val.x, phychar[attribute.color.snahead]);
        wattroff(scrgame, COLOR_PAIR(attribute.color.headcol)); // 关闭颜色属性

        usleep(attribute.speed); // 控制移动速度(微秒级暂停)
    }
}

核心逻辑

  1. 用nodelay设置非阻塞按键,无按键时input=ERR,蛇保持当前方向自动移动;
  2. 横坐标增减2是因为ncurses中特殊符号占2列,否则蛇身会错位;
  3. 移动后先插新蛇头,未吃豆再删尾节点,视觉上就是蛇在移动。

3. 碰撞检测

碰撞检测是游戏结束的关键触发条件,分为撞墙(超出地图边界)和吃自己(蛇头碰到蛇身),用位图优化吃自己的检测效率,避免遍历链表耗时。

核心代码片段(SnakeMove函数)

c 复制代码
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore)
{
    // 位图:标记蛇身位置(1=占用,0=空闲),O(1)碰撞检测
    char bitmap[MAP_LINES3][MAP_COLS3/2 + 1] = {0}; 
    bitmap[head->val.y][head->val.x / 2 + 1] = 1; // 初始化蛇头位置

    while(falg)
    {
        // (省略方向控制和蛇头插入代码)

        // 碰撞检测1:撞墙(超出地图边界,游戏结束)
        if(val.x == -1 || val.x == attribute.mapwidth - 1  || val.y == 0 || val.y == attribute.maplength - 1)
        {
            falg = FALSE; // 游戏结束标志
            input = 2; // 标记撞墙失败
        }

        // 碰撞检测2:吃自己(位图快速判断,无需遍历链表)
        if(val.x != -1 && bitmap[val.y][val.x / 2 + 1] == 1) 
        {
            falg = FALSE;
            input = 1; // 标记吃自己失败
        }

        // 吃豆后更新位图(蛇身变长)
        if(val.x == nap.x && val.y == nap.y)
        {
            bitmap[val.y][val.x / 2 + 1] = 1;
        }
        else // 未吃豆,删除尾节点后更新位图
        {
            bitmap[sna->physics->head->prev->val.y][sna->physics->head->prev->val.x / 2 + 1] = 0;
        }
    }
}

优化思路

最初通过遍历链表判断蛇头是否碰撞自身,蛇身越长,遍历对比的耗时越长,运行效率越低;改用位图优化后,通过坐标下标直接访问判断,检测效率降至O(1),游戏运行流畅度显著提升。

4. 豆子生成与吃豆逻辑

豆子需随机生成,且不能落在蛇身上(否则玩家无法吃到),吃豆后蛇身变长、分数增加,同时重新生成新豆子。

核心代码片段(SnakeMove函数)

c 复制代码
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore)
{
    srand((unsigned)time(NULL)); // 初始化随机数种子(时间戳保证随机性)
    type nap = {head->val.y,head->val.x}; // 豆子初始坐标(避开蛇头)

    // 生成第一个豆子(循环随机,避开蛇身)
    while(bitmap[nap.y][nap.x / 2 + 1] == 1)
    {
        nap.x = rand() % (attribute.mapwidth - 3) ;
        if(nap.x % 2 == 0) nap.x++; // x设为奇数,与蛇身坐标对齐
        nap.y = rand() % (attribute.maplength - 2) + 1;
    }

    // 最大分数=地图有效区域可容纳蛇身最大节点数(胜利条件)
    int maxscore = (attribute.maplength - 2) * (attribute.mapwidth / 2 - 1) - 1; 

    while(falg)
    {
        // (省略移动、碰撞检测代码)

        // 吃豆判断:蛇头与豆子坐标重合
        if(val.x == nap.x && val.y == nap.y)
        {
            sna->score++; // 分数+1
            // 重新生成豆子(避开蛇身)
            while(bitmap[nap.y][nap.x / 2 + 1] == 1)
            {
                nap.x = rand() % (attribute.mapwidth - 3) ;
                if(nap.x % 2 == 0) nap.x++;
                nap.y = rand() % (attribute.maplength - 2) + 1;
            } 
        }

        // 刷新分数显示
        mvwprintw(scrscore, attribute.maplength / 2 - 5, 9, "%d", sna->score);
        wrefresh(scrscore); // 即时更新分数窗口
    }
}

关键细节

豆子x坐标必须设为奇数,因为蛇身坐标是奇数(每次移动增减2),否则会出现蛇头看似碰到豆子却没吃到的错位问题;随机数种子用time(NULL),保证每次运行游戏豆子位置不重复。

5. 胜负判断

游戏结束分两种情况:失败(撞墙/吃自己)和胜利(蛇身占满整个地图,无剩余空间生成豆子),判断后保存对应结果到历史记录。

核心代码片段(SnakeMove函数)

c 复制代码
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore)
{
    // 最大分数=地图可容纳蛇身最大节点数(胜利条件:占满地图)
    int maxscore = (attribute.maplength - 2) * (attribute.mapwidth / 2 - 1) - 1;

    while(falg)
    {
        // (省略移动、碰撞、吃豆代码)

        // 胜利判断:分数达最大值(无空间生成新豆子)
        if(sna->score == maxscore)
        {
            falg = FALSE;
            input = 0; // 标记胜利
        }

        // 游戏结束保存记录(暂停退出不保存)
        if(input != 3)
        {
            char buffer[BUFFERSIZE];
            // 拼接记录:时间+分数+地图大小+速度+结果
            snprintf(buffer, BUFFERSIZE, "%s       %-6d     %s         %s        %s",
                timebuffer, sna->score, massage2[GET_MAPPOS(attribute.maplength)], massage3[GET_SPEEDPOS(attribute.speed)], massage4[input]);  
            Push(&allgameresult, buffer); // 加入历史记录数组
        }
    }
}

地图有效区域(除去边界)的可容纳节点数,就是最大分数,当蛇身长度达到这个数值,说明没有空间生成新豆子,玩家胜利;失败则由碰撞检测触发,通过input标记失败类型,最终同步到历史记录。

六、辅助功能

1. 设置菜单

菜单入口与界面布局:设置菜单通过主菜单"3.设置"选项进入,界面居中显示,包含"主题设置""难度设置""返回主菜单"三个选项,支持上下键切换、回车键确认,操作逻辑与主菜单一致。界面顶部显示"游戏设置"标题,选项下方无额外冗余信息,聚焦核心操作。

调用与交互逻辑:进入设置菜单后,选择对应选项可进入子设置界面(主题/难度),所有修改先缓存至临时变量,仅点击"保存"才同步到全局配置,点击"取消"则放弃修改,有效避免误操作。设置完成后按回车键返回上级菜单,全程支持按键导航,无鼠标交互依赖(适配终端场景)。

主题与难度设置核心功能:支持自定义蛇的颜色/符号(主题)、地图大小/移动速度(难度),用临时变量缓存修改,避免用户改一半取消时,全局配置被篡改。

支持自定义蛇的颜色/符号(主题)、地图大小/移动速度(难度),用临时变量缓存修改,避免用户改一半取消时,全局配置被篡改。

效果展示:

核心代码(含打印逻辑)

c 复制代码
// 1. 绘制设置菜单(含打印代码,返回用户选择项)
int ShowSetMenu()
{
    clear(); // 清空标准窗口,避免残留主菜单内容
    int input = 0;
    int option = 0; // 选中选项下标(0=主题,1=难度,2=返回)
    const char* setMenu[] = {"主题设置", "难度设置", "返回主菜单"};
    int menuLen = sizeof(setMenu)/sizeof(setMenu[0]);

    // 打印菜单标题(居中对齐,启用蓝字加粗属性)
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 5, COLS/2 - 4, "游戏设置"); // 坐标计算:基于终端尺寸居中
    attroff(COLOR_PAIR(2) | A_BOLD);

    // 循环打印菜单选项,初始选中第0项
    for(int i = 0; i < menuLen; i++)
    {
        mvprintw(LINES/2 - 3 + i*2, COLS/2 - 6, setMenu[i]);
    }
    mvprintw(LINES/2 - 3, COLS/2 - 9, "->"); // 打印选中标记

    keypad(stdscr, TRUE); // 启用功能键,支持上下键切换
    // 按键响应与选中标记刷新
    while(input != ENTRE)
    {
        input = getch();
        // 清除旧选中标记(打印空格覆盖)
        mvprintw(LINES/2 - 3 + option*2, COLS/2 - 9, "  ");
        // 上下键切换选项,循环边界
        if(input == KEY_UP)
            option = (option == 0 ? menuLen-1 : option-1);
        else if(input == KEY_DOWN)
            option = (option == menuLen-1 ? 0 : option+1);
        // 打印新选中标记
        mvprintw(LINES/2 - 3 + option*2, COLS/2 - 9, "->");
        refresh(); // 刷新窗口,使打印内容即时生效
    }
    return option;
}

// 2. 主题设置(含打印提示、选择项打印逻辑)
int ThemeSet(GameAtt* tmp)
{
    clear();
    int input = 0;
    int option = 0;
    // 蛇头符号选项(宽字符,适配ncurses宽字符渲染)
    const wchar_t* headChar[] = {L"■", L"●", L"◆"};
    // 主题选项描述
    const char* themeDesc[] = {"主题1(黄头+■)", "主题2(蓝头+●)", "主题3(红头+◆)", "保存", "取消"};
    int themeLen = sizeof(themeDesc)/sizeof(themeDesc[0]);

    // 打印主题设置标题和说明
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 6, COLS/2 - 5, "主题设置");
    attroff(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 4, COLS/2 - 15, "上下键选择,回车键确认,实时预览蛇头样式");

    while(1)
    {
        // 打印主题选项和当前预览
        for(int i = 0; i < themeLen; i++)
        {
            mvprintw(LINES/2 - 2 + i*2, COLS/2 - 8, themeDesc[i]);
        }
        // 打印选中标记
        mvprintw(LINES/2 - 2 + option*2, COLS/2 - 11, "->");
        // 打印蛇头预览(启用对应颜色对,宽字符专用打印函数)
        wattron(stdscr, COLOR_PAIR(tmp->color.headcol));
        mvwaddwstr(stdscr, LINES/2 + 5, COLS/2 - 1, headChar[tmp->color.snahead]);
        wattroff(stdscr, COLOR_PAIR(tmp->color.headcol));

        input = getch();
        // 清除旧标记
        mvprintw(LINES/2 - 2 + option*2, COLS/2 - 11, "  ");
        // 上下键切换选项,边界控制
        if(input == KEY_UP)
            option = (option == 0 ? themeLen-1 : option-1);
        else if(input == KEY_DOWN)
            option = (option == themeLen-1 ? 0 : option+1);
        // 选择主题时,实时更新临时配置(预览生效)
        else if(input == ENTRE)
        {
            if(option < 3)
            {
                tmp->color.snahead = option;
                tmp->color.headcol = option + 2; // 颜色对编号对应主题
                clear(); // 清空窗口,重新打印预览
                continue;
            }
            else if(option == 3) return 1; // 保存,返回主设置菜单
            else return 2; // 取消,返回主设置菜单
        }
        refresh();
    }
}

// 3. 难度设置(含打印地图/速度选项、数值预览)
int GameGradeSet(GameAtt* tmp)
{
    clear();
    int input = 0;
    int option = 0;
    // 难度选项(对应地图大小、速度)
    const char* gradeDesc[] = {"简单(小地图+慢速度)", "中等(中地图+中速度)", "困难(大地图+快速度)", "保存", "取消"};
    int gradeLen = sizeof(gradeDesc)/sizeof(gradeDesc[0]);

    // 打印难度设置标题和说明
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 6, COLS/2 - 5, "难度设置");
    attroff(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 4, COLS/2 - 20, "选择难度后实时更新配置,保存后生效");

    while(1)
    {
        // 打印难度选项
        for(int i = 0; i < gradeLen; i++)
        {
            mvprintw(LINES/2 - 2 + i*2, COLS/2 - 12, gradeDesc[i]);
        }
        // 打印选中标记
        mvprintw(LINES/2 - 2 + option*2, COLS/2 - 15, "->");
        // 打印当前配置预览(地图大小、速度数值)
        mvprintw(LINES/2 + 5, COLS/2 - 15, "当前预览:地图(%dx%d) 速度(%d微秒)",
                 tmp->mapwidth, tmp->maplength, tmp->speed);

        input = getch();
        // 清除旧标记
        mvprintw(LINES/2 - 2 + option*2, COLS/2 - 15, "  ");
        // 上下键切换选项
        if(input == KEY_UP)
            option = (option == 0 ? gradeLen-1 : option-1);
        else if(input == KEY_DOWN)
            option = (option == gradeLen-1 ? 0 : option+1);
        // 选择难度时,更新临时配置
        else if(input == ENTRE)
        {
            if(option < 3)
            {
                // 按难度赋值:简单(40x20, 300000)、中等(60x30, 200000)、困难(80x40, 100000)
                tmp->mapwidth = 40 + option*20;
                tmp->maplength = 20 + option*10;
                tmp->speed = 300000 - option*100000;
                clear();
                continue;
            }
            else if(option == 3) return 1; // 保存
            else return 0; // 取消
        }
        refresh();
    }
}

// 4. 设置入口:临时变量缓存修改,避免篡改全局配置(防误操作)
void Setting()
{
    clear(); // 清空标准窗口
    refresh(); // 刷新生效
    int option = 0;
    GameAtt tmp = attribute; // 拷贝全局配置到临时变量
    do
    {
        option = ShowSetMenu(); // 调用菜单打印函数,返回选择项
        if(option == 0)                    
        {
            while(2 == ThemeSet(&tmp)){}; // 主题设置(返回2继续设置)
        }
        else if(option == 1)
        {
            while(0 == GameGradeSet(&tmp)){}; // 难度设置(返回0继续设置)
        }
    }while(option != 2); // 选2(退出)结束设置
}

打印代码核心说明

  • 坐标计算逻辑:通过 LINES/2(终端总行数/2)、COLS/2(终端总列数/2)实现菜单居中,选项间距设为2行(i*2),避免视觉拥挤。
  • 视觉反馈控制:选中标记用 -> 表示,切换选项前先打印空格覆盖旧标记,再绘制新标记,避免残留;标题启用 COLOR_PAIR(2) | A_BOLD 实现蓝字加粗,与普通选项区分。
  • 宽字符打印:蛇头符号为宽字符,需用 mvwaddwstr函数(而非 mvprintw),搭配 setlocale(LC_ALL, "") 确保渲染正常,避免乱码。
  • 实时预览打印:主题、难度选择时,即时更新临时配置并重新打印预览内容,让用户直观看到设置效果,提升交互体验。
c 复制代码
// 设置入口:临时变量缓存修改,避免篡改全局配置(防误操作)
void Setting()
{
    clear(); // 清空标准窗口
    refresh(); // 刷新生效
    int option = 0;
    GameAtt tmp = attribute; // 拷贝全局配置到临时变量
    do
    {
        option = ShowSetMenu(); // 绘制设置菜单,返回选择项
        if(option == 0)                     
        {
            while(2 == ThemeSet(&tmp)){}; // 主题设置(返回2继续设置)
        }
        else if(option == 1)
        {
            while(0 == GameGradeSet(&tmp)){}; // 难度设置(返回0继续设置)
        }
    }while(option != 2); // 选2(退出)结束设置
}

// 难度设置核心逻辑:取消重置/保存同步
if(opt == 1) // 取消:临时变量恢复全局配置原值
{
    tmp->maplength = attribute.maplength;
    tmp->mapwidth = attribute.mapwidth;
    tmp->speed = attribute.speed;
}
else if(opt == 2) // 保存:临时变量同步到全局
{
    attribute.maplength = tmp->maplength;
    attribute.mapwidth = tmp->mapwidth;
    attribute.speed = tmp->speed;
}

设计亮点

用临时变量tmp隔离修改,用户在设置界面的操作不会直接影响全局配置,只有点击"保存"才同步,避免误操作导致的配置混乱,提升用户体验。

2. 历史记录菜

菜单入口与界面布局:历史记录菜单通过主菜单"2.历史记录"选项进入,界面为居中弹窗式窗口(独立于主窗口),窗口带默认边框,顶部显示"历史记录"标题,中间区域展示分页记录(含时间、分数、地图大小、速度、结果),底部无额外提示,按指定按键完成翻页、清空、退出操作。

调用与交互逻辑:进入菜单后自动加载本地历史记录并显示第一页,支持三大核心操作:上下键切换页码(页码范围适配记录总数,无数据时提示空记录)、退格键触发清空(含二次确认,防误删)、Q/q键退出菜单并销毁窗口,返回主界面。所有操作实时反馈(如清空后显示"清除成功",暂停2秒供查看)。

加载本地历史记录文件,支持分页展示(每页显示固定条数),上下键切换页码,按Q退出查看,满足用户回溯游戏记录的需求。

加载本地历史记录文件,支持分页展示(每页显示固定条数),上下键切换页码,按Q退出查看,满足用户回溯游戏记录的需求。

效果展示:

核心代码(含打印逻辑)

c 复制代码
// 1. 历史记录主函数(含窗口绘制、记录打印、提示打印)
void HistoryScores()
{
    // 创建历史记录窗口(居中显示,与游戏窗口隔离)
    WINDOW* scrresult = newwin(RESULT_LINES, RESULT_COLS, (LINES-RESULT_LINES)/2, (COLS-RESULT_COLS)/2);
    box(scrresult, 0, 0); // 绘制窗口边框(默认字符,提升规整度)
    keypad(scrresult, TRUE); // 启用功能键(上下键翻页、退格键清空)
    int maxPerPage = RESULT_LINES - 3; // 每页最大记录数(扣除标题、边框占用行)
    int currPage = 0; // 当前页码(从0开始)
    int input = 0;
    int totalPage = (allgameresult.size + maxPerPage - 1) / maxPerPage; // 总页数计算

    // 打印窗口标题(居中,蓝字加粗)
    wattron(scrresult, COLOR_PAIR(2) | A_BOLD);
    mvwprintw(scrresult, 0, RESULT_COLS/2 - 5, "历史记录");
    wattroff(scrresult, COLOR_PAIR(2) | A_BOLD);

    do
    {
        wclear(scrresult); // 清空窗口,避免翻页残留
        box(scrresult, 0, 0); // 重绘边框(清空后边框消失,需重新绘制)
        // 重新打印标题
        wattron(scrresult, COLOR_PAIR(2) | A_BOLD);
        mvwprintw(scrresult, 0, RESULT_COLS/2 - 5, "历史记录");
        wattroff(scrresult, COLOR_PAIR(2) | A_BOLD);

        // 无历史记录时,打印提示信息
        if(allgameresult.size == 0)
        {
            mvwprintw(scrresult, RESULT_LINES/2, RESULT_COLS/2 - 7, "暂无历史记录");
            mvwprintw(scrresult, RESULT_LINES/2 + 2, RESULT_COLS/2 - 10, "按Q/q返回主菜单");
        }
        else
        {
            // 打印记录表头(时间、分数、地图、速度、结果)
            mvwprintw(scrresult, 1, 3, "时间                分数      地图大小      速度        结果");
            // 打印分隔线(用横线填充,视觉区分表头与内容)
            mvwprintw(scrresult, 2, 3, "------------------------------------------------------------");
            // 打印当前页记录(从动态数组中读取,避免越界)
            int startIdx = currPage * maxPerPage;
            for(int i = 0; i < maxPerPage; i++)
            {
                if(startIdx + i < allgameresult.size)
                {
                    mvwprintw(scrresult, 3 + i, 3, allgameresult.head[startIdx + i]);
                }
            }
            // 打印页码信息(当前页/总页数)
            mvwprintw(scrresult, RESULT_LINES - 1, RESULT_COLS/2 - 8, "第%d/%d页  上下键翻页", currPage + 1, totalPage);
            // 打印操作提示
            mvwprintw(scrresult, RESULT_LINES - 1, 3, "退格键清空记录  Q/q返回");
        }

        input = wgetch(scrresult); // 读取窗口按键
        // 页码切换逻辑
        if(input == KEY_UP && currPage > 0)
            currPage--; // 上一页(页码>0可用)
        else if(input == KEY_DOWN && (currPage + 1) < totalPage)
            currPage++; // 下一页(未到最后一页可用)
        // 清空记录逻辑(含二次确认打印)
        else if(input == KEY_BACKSPACE && allgameresult.size > 0)
            ClearHistory(scrresult); // 调用清空函数,含提示打印

    } while (input != 'q' && input != 'Q'); // Q/q退出查看

    delwin(scrresult); // 销毁窗口,释放内存
    refresh(); // 刷新标准窗口,恢复主界面
}

// 2. 清空历史记录(含二次确认提示、结果提示打印)
void ClearHistory(WINDOW* scrresult)
{
    // 打印二次确认提示(居中显示,覆盖部分记录区域)
    mvwprintw(scrresult, RESULT_LINES/2 - 2, RESULT_COLS/2 - 15, "清除后将无法恢复,是否确认清除?");
    mvwprintw(scrresult, RESULT_LINES/2, RESULT_COLS/2 - 13, "Y:确认清除  N:取消清除");
    wrefresh(scrresult); // 刷新窗口,显示提示

    int input = 0;
    while(1)
    {
        input = wgetch(scrresult);
        if(input == 'y' || input == 'Y')
        {
            // 清空内存记录
            DestroyVec(&allgameresult);
            InitVector(&allgameresult);
            // 打印清除成功提示
            wclear(scrresult);
            box(scrresult, 0, 0);
            mvwprintw(scrresult, RESULT_LINES/2, RESULT_COLS/2 - 4, "清除成功");
            wrefresh(scrresult);
            usleep(2000000); // 暂停2秒,让用户查看提示
            break;
        }
        else if(input == 'n' || input == 'N')
        {
            // 取消清除,刷新窗口恢复记录显示
            wrefresh(scrresult);
            break;
        }
    }
}

打印代码核心说明

  • 窗口隔离打印:通过 newwin 创建独立窗口,所有打印操作(mvwprintw)均针对该窗口,避免影响主界面,符合终端多窗口交互规范。
  • 边界场景打印:无记录时打印友好提示,避免空白窗口;页码计算时用"进一法"确保最后一页记录完整,同时打印页码和操作提示,降低用户学习成本。
  • 交互提示打印:清空记录时打印二次确认提示,清除成功后暂停2秒显示结果,避免误操作;提示信息均居中或靠左对齐,与记录内容分区,视觉清晰。
  • 格式统一打印:表头与记录内容对齐,用横线分隔表头,每条记录按"时间-分数-地图-速度-结果"固定格式打印,提升可读性,与文件存储格式保持一致。
c 复制代码
void HistoryScores()
{
    // 创建历史记录窗口(居中显示,与游戏窗口隔离)
    WINDOW* scrresult = newwin(RESULT_LINES, RESULT_COLS, (LINES-RESULT_LINES)/2, (COLS-RESULT_COLS)/2);
    box(scrresult, 0, 0); // 绘制边框(默认字符)
    keypad(scrresult, TRUE); // 启用功能键(上下键翻页)
    int max = RESULT_LINES - 3; // 每页最大记录数(扣除标题/边框)
    int pos = 0; // 当前页码(从0开始)
    int input = 0;

    do
    {
        wclear(scrresult); // 清空窗口,避免翻页残留
        box(scrresult, 0, 0); // 重绘边框(清空后消失)
        mvwprintw(scrresult, 0, RESULT_COLS/2 - 5, "历史记录"); // 居中打印标题

        // 打印当前页记录(避免数组越界)
        type1* tmp = allgameresult.head + pos * max;
        for(int i = 0; i < max; i++)
        {
            if(i + pos * max < allgameresult.size)
            {
                mvwprintw(scrresult, 2 + i, 3, tmp[i]);  
            }
        }

        input = wgetch(scrresult); // 读取窗口按键
        if(input == KEY_UP && pos != 0) pos--; // 上一页(页码>0可用)
        else if(input == KEY_DOWN && (pos+1) * max < allgameresult.size) pos++; // 下一页(有数据可用)
    } while (input != 'q' && input != 'Q'); // Q/q退出查看

    delwin(scrresult); // 销毁窗口,释放内存
    refresh(); // 刷新标准窗口,恢复主界面
}

实现逻辑

按窗口高度计算每页最大记录数,通过pos变量控制页码,切换页码时重新绘制当前页记录,避免全局刷新导致的闪屏;按Q退出后释放窗口资源,防止内存泄漏。

3. 历史记录清空

支持一键清空所有历史记录,添加确认提示(防止误操作),清空后同步更新本地文件,确保记录彻底删除。

效果展示:

核心代码片段(HistoryScores函数)

c 复制代码
if(input == KEY_BACKSPACE) // 退格键触发清空记录
{
    // 确认提示(防误删,清除后不可恢复)
    mvwprintw(scrresult, RESULT_LINES / 2 -2 , RESULT_COLS / 2 - 15,"清除后将无法恢复,是否确认清除");
    mvwprintw(scrresult, RESULT_LINES / 2  , RESULT_COLS  / 2 - 13,"y: 确认清除  n: 取消清除");  
    do
    {
        input = wgetch(scrresult); // 读取确认按键
        if(input == 'y' || input == 'Y')
        {
            DestroyVec(&allgameresult); // 清空内存记录
            InitVector(&allgameresult); // 重新初始化空数组
            mvwprintw(scrresult, RESULT_LINES / 2  , RESULT_COLS  / 2 - 4,"清除成功");  
            wrefresh(scrresult); // 显示提示
            sleep(2); // 暂停2秒,让用户查看提示
            break;
        }
    }while(input != 'n' && input != 'N'); // N/n取消清除
}

关键注意点

清空记录后不仅要释放内存中的动态数组,还要在退出游戏时同步更新gamehestroy.txt(通过End函数实现),否则下次启动游戏会重新加载旧记录。

七、资源释放与总结

游戏退出时的收尾工作,保存当前配置(主题、难度)和历史记录到本地文件,释放所有动态内存和ncurses窗口资源,避免内存泄漏和资源残留。

核心代码

c 复制代码
void End()
{
    // 保存主题配置(w模式:覆盖写入,无文件则创建)
    FILE* conf = fopen("config.txt","w");
    fprintf(conf,"蛇头颜色:%d, 蛇头符号序号:%d\n", attribute.color.headcol,attribute.color.snahead);
    fprintf(conf,"蛇身颜色:%d, 蛇身符号序号:%d\n", attribute.color.phycol,attribute.color.snaphy);
    fclose(conf); // 关闭文件流,释放资源

    // 保存历史记录(w模式:覆盖写入)
    FILE* res = fopen("gamehestroy.txt","w");
    for(int i = 0; i < allgameresult.size; i++)
    {
        fprintf(res,"%s\n",allgameresult.head[i]);  
    }
    fclose(res); // 关闭文件流
    
    DestroyVec(&allgameresult); // 释放记录数组内存
    endwin(); // 关闭ncurses,恢复终端默认状态(必做)
}

开发提醒

必须调用endwin()关闭ncurses环境,否则终端会陷入异常状态(如光标不显示、按键无回显),需重启终端才能恢复;文件操作后要及时fclose,避免文件句柄泄漏。

八、完整代码汇总

gitee连接:https://gitee.com/fan-cc-fan/linux-learning-hub/tree/master/RetorSnaker

1. Mylist.h(双向循环链表头文件)

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

// 坐标结构体:存储蛇身节点、豆子的x/y位置(适配ncurses终端坐标)
typedef struct zb{
 int x;
 int y;
} type;

// 链表节点:存坐标+前后指针(双向循环结构,支持O(1)增删)
typedef struct Node{
    type val;
 struct Node* next;
 struct Node* prev;
}Node;

// 链表头:仅存头节点指针(哨兵节点,简化边界判断,避免空指针异常)
typedef struct List{
 struct Node* head;
}List;

// 核心函数声明
List* GetList(); // 创建空链表(初始化哨兵节点)
void PushFront(List* list, type val); // 头插(蛇移动时新增蛇头)
void PopBack(List* list); // 尾删(蛇未吃豆时删除蛇尾)
void Destroy(List* list); // 释放链表所有节点(防内存泄漏)
void PushPos(Node* pos, type val); // 指定位置插入节点(内部复用)
void PopPos(Node* pos); // 删除指定节点(内部复用)
int Empty(List* list); // 判空函数(内部复用)

2. Mylist.c(双向循环链表实现文件)

c 复制代码
#include"Mylist.h"

// 创建空链表,初始化哨兵节点
List* GetList()
{
    List* list = (List*)malloc(sizeof(List));
    Node* sentinel = (Node*)malloc(sizeof(Node));
    sentinel->next = sentinel;
    sentinel->prev = sentinel;
    list->head = sentinel;
    return list;
}

// 指定位置插入节点(插在pos节点之后)
void PushPos(Node* pos, type val)
{
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->val = val;
    newNode->next = pos->next;
    newNode->prev = pos;
    pos->next->prev = newNode;
    pos->next = newNode;
}

// 头插实现:复用指定位置插入逻辑,插在哨兵节点之后
void PushFront(List* list, type val)
{
 PushPos(list->head, val); 
}

// 删除指定节点
void PopPos(Node* pos)
{
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
    pos = NULL;
}

// 判空函数
int Empty(List* list)
{
    return list->head->next == list->head;
}

// 尾删实现:通过head->prev直接获取尾节点(双向循环优势,无需遍历)
void PopBack(List* list)
{
     if(Empty(list)) // 判空保护,避免空链表删节点崩溃
     {
        printf("list is empty!");
     }
     else
     {
        PopPos(list->head->prev); // 删除尾节点
     }
}

// 释放链表所有节点(含哨兵节点)
void Destroy(List* list)
{
    Node* cur = list->head->next;
    while(cur != list->head)
    {
        Node* tmp = cur;
        cur = cur->next;
        free(tmp);
    }
    free(list->head);
    free(list);
    list = NULL;
}

3. Myvector.h(自定义动态数组头文件)

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

// 动态数组结构体:适配历史记录动态存储
typedef struct vector
{
    const char** head; // 存储每条历史记录字符串
    int size;          // 当前有效记录数
    int capacity;      // 数组容量(初始4,满后自动扩容)
}vector;

// 核心函数声明
void InitVector(vector* vec); // 初始化数组
void Push(vector* vec, const char* val); // 尾部新增记录
int CheckCapa(vector* vec); // 容量检测与扩容(返回0成功,-1失败)
void DestroyVec(vector* vec); // 释放内存(防泄漏)

4. Myvector.c(自定义动态数组实现文件)

c 复制代码
#include"Myvector.h"

// 初始化数组:容量初始为4,size为0
void InitVector(vector* vec)
{
    vec->capacity = 4;
    vec->size = 0;
    vec->head = (const char**)malloc(sizeof(const char*)*vec->capacity);
    if(vec->head == NULL)
    {
        perror("InitVector malloc failed");
        exit(-1);
    }
}

// 扩容逻辑:容量满时倍增,平衡效率与内存占用
int CheckCapa(vector* vec)
{
    if(vec->capacity == vec->size)
    {
        vec->capacity *= 2; // 倍增扩容,减少频繁扩容开销
        // realloc调整内存,原数据自动迁移,失败返回-1
        const char** tmp = (const char**)realloc(vec->head, sizeof(const char*)*vec->capacity);
        if(tmp == NULL)
        {
            perror("CheckCapa realloc failed");
            return -1;
        }
        vec->head = tmp;
    }
    return 0;
}

// 尾部新增记录:先检测容量,再拷贝字符串存入
void Push(vector* vec, const char* val)
{
    if(CheckCapa(vec) == -1)
    {
        return;
    }
    // 拷贝字符串,避免原地址失效导致野指针
    char* tmp = (char*)malloc(sizeof(char)*(strlen(val)+1));
    strcpy(tmp, val);
    vec->head[vec->size++] = tmp;
}

// 内存释放:逐元素释放,避免内存泄漏(C语言手动管理核心)
void DestroyVec(vector* vec)
{
    for(int i = 0;i<vec->size;i++)
    {
        free((void*)vec->head[i]); // 释放单条记录内存
        vec->head[i] = NULL; // 置空指针,避免野指针
    }
    free(vec->head); // 释放数组本身内存
    vec->capacity = 0;
    vec->size = 0;
    vec->head = NULL;
}

5. Snake.h(游戏核心头文件,含宏定义、结构体声明)

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<ncurses.h>
#include<time.h>
#include<string.h>
#include"Mylist.h"
#include"Myvector.h"

// 宏定义
#define ENTRE 10 // 回车键ASCII值
#define BUFFERSIZE 128 // 字符串缓冲区大小
#define MAP_LINES3 20 // 默认地图行数
#define MAP_COLS3 40 // 默认地图列数
#define RESULT_LINES 15 // 历史记录窗口行数
#define RESULT_COLS 60 // 历史记录窗口列数

// 方向枚举
typedef enum{
    SUP,    // 上
    SDOWN,  // 下
    SLEFT,  // 左
    SRIGHT  // 右
}Direction;

// 颜色配置结构体
typedef struct{
    int headcol; // 蛇头颜色对编号
    int phycol;  // 蛇身颜色对编号
    int snahead; // 蛇头符号序号
    int snaphy;  // 蛇身符号序号
}ColorAttr;

// 游戏属性结构体
typedef struct{
    int maplength; // 地图行数
    int mapwidth;  // 地图列数
    int speed;     // 蛇移动速度(微秒)
    ColorAttr color; // 颜色配置
}GameAtt;

// 蛇结构体
typedef struct{
    List* physics; // 蛇身链表
    Direction dire; // 移动方向
    int score;     // 当前分数
}Snake;

// 全局变量声明
extern GameAtt attribute; // 游戏属性
extern vector allgameresult; // 历史记录数组
extern const wchar_t* phychar[]; // 蛇身符号数组
extern const char* massage1[]; // 菜单选项数组
extern const char* massage2[]; // 地图大小描述数组
extern const char* massage3[]; // 速度描述数组
extern const char* massage4[]; // 游戏结果描述数组
extern int falg; // 游戏运行标志
extern type nap; // 豆子坐标

// 函数声明
void Init(); // 游戏初始化
void End(); // 游戏资源释放
void Game(); // 游戏主函数
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore); // 蛇移动逻辑
int ShowMenu(int option); // 绘制菜单并返回选择项
void RunOption(int option); // 功能分发
void Setting(); // 主题与难度设置
int ShowSetMenu(); // 绘制设置菜单
int ThemeSet(GameAtt* tmp); // 主题设置
int GameGradeSet(GameAtt* tmp); // 难度设置
void HistoryScores(); // 历史记录查看与管理
void InitAttribute(GameAtt* attr); // 初始化游戏属性
void InitResult(vector* vec); // 初始化历史记录

6. Snake.c(游戏核心实现文件)

c 复制代码
#include"Snake.h"

// 全局变量定义
GameAtt attribute;
vector allgameresult;
const wchar_t* phychar[] = {L"■", L"●", L"▲"}; // 蛇头符号数组
const char* massage1[] = {"开始游戏", "历史记录", "设置", "退出游戏"};
const char* massage2[] = {"小地图(10x20)", "中地图(15x30)", "大地图(20x40)"};
const char* massage3[] = {"慢速", "中速", "快速"};
const char* massage4[] = {"胜利", "吃自己失败", "撞墙失败"};
int falg;
type nap;

// 初始化游戏属性(加载配置或默认值)
void InitAttribute(GameAtt* attr)
{
    FILE* conf = fopen("config.txt", "r");
    if(conf != NULL)
    {
        fscanf(conf, "蛇头颜色:%d, 蛇头符号序号:%d\n", &attr->color.headcol, &attr->color.snahead);
        fscanf(conf, "蛇身颜色:%d, 蛇身符号序号:%d\n", &attr->color.phycol, &attr->color.snaphy);
        fclose(conf);
    }
    else
    {
        // 默认配置
        attr->color.headcol = 2;
        attr->color.snahead = 0;
        attr->color.phycol = 1;
        attr->color.snaphy = 1;
    }
    attr->maplength = MAP_LINES3;
    attr->mapwidth = MAP_COLS3;
    attr->speed = 300000; // 默认中速(300毫秒)
}

// 初始化历史记录(加载文件或初始化空数组)
void InitResult(vector* vec)
{
    InitVector(vec);
    FILE* res = fopen("gamehestroy.txt", "r");
    if(res != NULL)
    {
        char buf[BUFFERSIZE];
        while(fgets(buf, BUFFERSIZE, res) != NULL)
        {
            buf[strcspn(buf, "\n")] = '\0'; // 去除换行符
            Push(vec, buf);
        }
        fclose(res);
    }
}

// 游戏初始化
void Init()
{ 
    InitAttribute(&attribute); // 加载配置(无配置用默认值)
    InitResult(&allgameresult); // 加载历史记录(无文件则初始化空数组)

    // ncurses环境初始化(缺失会导致绘图/按键异常)
    setlocale(LC_ALL, ""); // 支持中文/特殊符号,避免乱码
    initscr(); // 启动终端图形模式,创建标准窗口stdscr
    cbreak(); // 禁用行缓冲,按键立即响应(无需回车)
    noecho(); // 不回显按键,避免干扰界面
    keypad(stdscr, TRUE); // 启用功能键支持(方向键等)
    curs_set(0); // 隐藏光标,提升视觉体验

    // 初始化颜色系统,定义颜色对(前景色+背景色)
    start_color();
    init_pair(1, COLOR_WHITE, COLOR_BLACK);   // 1号:白字黑底(默认文本)
    init_pair(2, COLOR_BLUE, COLOR_BLACK);    // 2号:蓝字黑底(菜单标题)
    init_pair(3, COLOR_YELLOW, COLOR_BLACK);  // 3号:黄字黑底(豆子)
}

// 游戏资源释放
void End()
{
    // 保存主题配置(w模式:覆盖写入,无文件则创建)
    FILE* conf = fopen("config.txt","w");
    fprintf(conf,"蛇头颜色:%d, 蛇头符号序号:%d\n", attribute.color.headcol,attribute.color.snahead);
    fprintf(conf,"蛇身颜色:%d, 蛇身符号序号:%d\n", attribute.color.phycol,attribute.color.snaphy);
    fclose(conf); // 关闭文件流,释放资源

    // 保存历史记录(w模式:覆盖写入)
    FILE* res = fopen("gamehestroy.txt","w");
    for(int i = 0; i < allgameresult.size; i++)
    {
        fprintf(res,"%s\n",allgameresult.head[i]);  
    }
    fclose(res); // 关闭文件流
    
    DestroyVec(&allgameresult); // 释放记录数组内存
    endwin(); // 关闭ncurses,恢复终端默认状态(必做)
    exit(0); // 正常退出进程
}

// 绘制菜单并返回选择项
int ShowMenu(int option)
{
    int input = 0;
    attron(COLOR_PAIR(2) | A_BOLD); // 启用2号色对(蓝字黑底)+加粗,绘制标题
    mvprintw(LINES/2 - 8, COLS / 2 - 6, "贪吃蛇大作战"); // 光标居中,打印标题(ncurses坐标:y行x列)
    attroff(COLOR_PAIR(2) | A_BOLD); // 关闭属性,避免影响后续内容

    // 循环绘制4个菜单选项(开始/历史/设置/退出)
    for(int i = 0; i < 4; i++)
    {
        mvprintw(LINES/2 -3 + (i * 2), COLS/2 - 5 , massage1[i]);
    }
    mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "->"); // 绘制选中标记

    // 阻塞读取按键,按回车确认退出循环
    while(input != ENTRE)
    {
        input = getch(); // 读取标准窗口按键
        // FROMINPUT宏:统一wasd与方向键(此处补充宏定义逻辑)
        if(input == 'w' || input == 'W') input = KEY_UP;
        else if(input == 's' || input == 'S') input = KEY_DOWN;
        else if(input == 'a' || input == 'A') input = KEY_LEFT;
        else if(input == 'd' || input == 'D') input = KEY_RIGHT;
        
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "  "); // 清除旧选中标记
        
        if(input == KEY_UP) 
        {
            option = (option == 0 ? 3 : option - 1); // 循环上选(顶部切底部)
        }
        else if(input == KEY_DOWN) 
        {
            option = (option == 3 ? 0 : option + 1); // 循环下选(底部切顶部)
        }
        
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8 , "->"); // 绘制新选中标记
        refresh(); // 刷新标准窗口,即时显示内容
    }
    return option;
}

// 绘制设置菜单
int ShowSetMenu()
{
    clear();
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 8, COLS/2 - 4, "游戏设置");
    attroff(COLOR_PAIR(2) | A_BOLD);

    const char* setMenu[] = {"主题设置", "难度设置", "返回主菜单"};
    int option = 0;
    int input = 0;

    for(int i = 0; i < 3; i++)
    {
        mvprintw(LINES/2 -3 + (i * 2), COLS/2 - 5, setMenu[i]);
    }
    mvprintw(LINES/2 -3, COLS/2 - 8, "->");
    refresh();

    while(input != ENTRE)
    {
        input = getch();
        if(input == 'w' || input == 'W') input = KEY_UP;
        else if(input == 's' || input == 'S') input = KEY_DOWN;
        
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8, "  ");
        if(input == KEY_UP) option = (option == 0 ? 2 : option - 1);
        else if(input == KEY_DOWN) option = (option == 2 ? 0 : option + 1);
        
        mvprintw(LINES/2 -3 + (option * 2), COLS/2 - 8, "->");
        refresh();
    }
    return option;
}

// 主题设置
int ThemeSet(GameAtt* tmp)
{
    clear();
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 8, COLS/2 - 4, "主题设置");
    attroff(COLOR_PAIR(2) | A_BOLD);

    const char* headSym[] = {"■", "●", "▲"};
    const char* headCol[] = {"蓝色", "白色", "黄色"};
    int hSym = tmp->color.snahead;
    int hCol = tmp->color.headcol - 1;
    int input = 0;

    while(1)
    {
        clear();
        attron(COLOR_PAIR(2) | A_BOLD);
        mvprintw(LINES/2 - 8, COLS/2 - 4, "主题设置");
        attroff(COLOR_PAIR(2) | A_BOLD);

        mvprintw(LINES/2 - 4, COLS/2 - 10, "蛇头符号:%s", headSym[hSym]);
        mvprintw(LINES/2 - 2, COLS/2 - 10, "蛇头颜色:%s", headCol[hCol]);
        mvprintw(LINES/2 + 2, COLS/2 - 15, "←→切换选项  空格确认  ESC返回");

        input = getch();
        if(input == KEY_LEFT || input == KEY_RIGHT)
        {
            if(input == KEY_LEFT)
            {
                hSym = (hSym == 0 ? 2 : hSym - 1);
                hCol = (hCol == 0 ? 2 : hCol - 1);
            }
            else
            {
                hSym = (hSym == 2 ? 0 : hSym + 1);
                hCol = (hCol == 2 ? 0 : hCol + 1);
            }
        }
        else if(input == ' ')
        {
            tmp->color.snahead = hSym;
            tmp->color.headcol = hCol + 1;
            return 1; // 确认保存
        }
        else if(input == 27)
        {
            return 2; // 取消返回
        }
        refresh();
    }
}

// 难度设置
int GameGradeSet(GameAtt* tmp)
{
    clear();
    attron(COLOR_PAIR(2) | A_BOLD);
    mvprintw(LINES/2 - 8, COLS/2 - 4, "难度设置");
    attroff(COLOR_PAIR(2) | A_BOLD);

    int grade = 1; // 0=简单,1=中等,2=困难
    int input = 0;

    while(1)
    {
        clear();
        attron(COLOR_PAIR(2) | A_BOLD);
        mvprintw(LINES/2 - 8, COLS/2 - 4, "难度设置");
        attroff(COLOR_PAIR(2) | A_BOLD);

        mvprintw(LINES/2 - 4, COLS/2 - 10, "当前难度:%s", massage3[grade]);
        mvprintw(LINES/2 - 2, COLS/2 - 10, "地图大小:%s", massage2[grade]);
        mvprintw(LINES/2 + 2, COLS/2 - 15, "←→切换  空格确认  ESC返回");

        input = getch();
        if(input == KEY_LEFT) grade = (grade == 0 ? 2 : grade - 1);
        else if(input == KEY_RIGHT) grade = (grade == 2 ? 0 : grade + 1);
        else if(input == ' ')
        {
            // 根据难度设置地图和速度
            tmp->maplength = 10 + grade * 5;
            tmp->mapwidth = 20 + grade * 10;
            tmp->speed = 500000 - grade * 100000;
            return 1; // 确认保存
        }
        else if(input == 27)
        {
            return 0; // 取消返回
        }
        refresh();
    }
}

// 主题与难度设置入口
void Setting()
{
    clear();
    refresh();
    int option = 0;
    GameAtt tmp = attribute; // 拷贝全局配置到临时变量
    do
    {
        option = ShowSetMenu(); // 绘制设置菜单,返回选择项
        if(option == 0)                     
        {
            while(2 == ThemeSet(&tmp)){}; // 主题设置(返回2继续设置)
        }
        else if(option == 1)
        {
            while(0 == GameGradeSet(&tmp)){}; // 难度设置(返回0继续设置)
        }
    }while(option != 2); // 选2(退出)结束设置

    // 同步临时变量到全局配置
    attribute = tmp;
}

// 历史记录查看与管理
void HistoryScores()
{
    // 创建历史记录窗口(居中显示,与游戏窗口隔离)
    WINDOW* scrresult = newwin(RESULT_LINES, RESULT_COLS, (LINES-RESULT_LINES)/2, (COLS-RESULT_COLS)/2);
    box(scrresult, 0, 0); // 绘制边框(默认字符)
    keypad(scrresult, TRUE); // 启用功能键(上下键翻页)
    int max = RESULT_LINES - 3; // 每页最大记录数(扣除标题/边框)
    int pos = 0; // 当前页码(从0开始)
    int input = 0;

    do
    {
        wclear(scrresult); // 清空窗口,避免翻页残留
        box(scrresult, 0, 0); // 重绘边框(清空后消失)
        mvwprintw(scrresult, 0, RESULT_COLS/2 - 5, "历史记录"); // 居中打印标题
        mvwprintw(scrresult, RESULT_LINES-1, 2, "上下键翻页  Q退出  退格键清空");

        // 打印当前页记录(避免数组越界)
        for(int i = 0; i < max; i++)
        {
            int idx = i + pos * max;
            if(idx < allgameresult.size)
            {
                mvwprintw(scrresult, 2 + i, 3, "%d. %s", idx+1, allgameresult.head[idx]);  
            }
        }

        input = wgetch(scrresult); // 读取窗口按键
        if(input == KEY_UP && pos != 0) pos--; // 上一页(页码>0可用)
        else if(input == KEY_DOWN && (pos+1) * max < allgameresult.size) pos++; // 下一页(有数据可用)
        else if(input == KEY_BACKSPACE) // 退格键触发清空记录
        {
            // 确认提示(防误删,清除后不可恢复)
            mvwprintw(scrresult, RESULT_LINES / 2 -2 , RESULT_COLS / 2 - 15,"清除后将无法恢复,是否确认清除");
            mvwprintw(scrresult, RESULT_LINES / 2  , RESULT_COLS  / 2 - 13,"y: 确认清除  n: 取消清除");  
            do
            {
                input = wgetch(scrresult); // 读取确认按键
                if(input == 'y' || input == 'Y')
                {
                    DestroyVec(&allgameresult); // 清空内存记录
                    InitVector(&allgameresult); // 重新初始化空数组
                    mvwprintw(scrresult, RESULT_LINES / 2  , RESULT_COLS  / 2 - 4,"清除成功");  
                    wrefresh(scrresult); // 显示提示
                    sleep(2); // 暂停2秒,让用户查看提示
                    pos = 0;
                    break;
                }
            }while(input != 'n' && input != 'N'); // N/n取消清除
        }
    } while (input != 'q' && input != 'Q'); // Q/q退出查看

    delwin(scrresult); // 销毁窗口,释放内存
    refresh(); // 刷新标准窗口,恢复主界面
}

// 蛇移动逻辑
void SnakeMove(Snake* sna, WINDOW* scrgame, WINDOW* scrscore)
{
    nodelay(scrgame, TRUE); // 非阻塞模式,实现蛇自动移动
    keypad(scrgame, TRUE); // 启用游戏窗口功能键支持

    Node* head = sna->physics->head->next; // 蛇头指针(跳过哨兵节点)
    type val = head->val; // 蛇头初始坐标
    int input = 0;

    // 位图:标记蛇身位置(1=占用,0=空闲),O(1)碰撞检测
    char bitmap[MAP_LINES3][MAP_COLS3/2 + 1] = {0}; 
    bitmap[head->val.y][head->val.x / 2 + 1] = 1; // 初始化蛇头位置

    srand((unsigned)time(NULL)); // 初始化随机数种子(时间戳保证随机性)
    nap = (type){head->val.x + 4, head->val.y}; // 豆子初始坐标(避开蛇头)

    // 生成第一个豆子(循环随机,避开蛇身)
    while(bitmap[nap.y][nap.x / 2 + 1] == 1)
    {
        nap.x = rand() % (attribute.mapwidth - 3) ;
        if(nap.x % 2 == 0) nap.x++; // x设为奇数,与蛇身坐标对齐
        nap.y = rand() % (attribute.maplength - 2) + 1;
    }
    // 绘制第一个豆子
    wattron(scrgame, COLOR_PAIR(3));
    mvwprintw(scrgame, nap.y, nap.x, "●");
    wattroff(scrgame, COLOR_PAIR(3));

    // 最大分数=地图有效区域可容纳蛇身最大节点数(胜利条件)
    int maxscore = (attribute.maplength - 2) * (attribute.mapwidth / 2 - 1) - 1; 
    char timebuffer[32];
    time_t now;

    while(falg)
    {
        input = wgetch(scrgame); // 读取游戏窗口按键(非阻塞)
        // 统一wasd与方向键
        if(input == 'w' || input == 'W') input = KEY_UP;
        else if(input == 's' || input == 'S') input = KEY_DOWN;
        else if(input == 'a' || input == 'A') input = KEY_LEFT;
        else if(input == 'd' || input == 'D') input = KEY_RIGHT;

        // 方向控制:限制反向移动,无按键保持当前方向
        if((input == KEY_UP && sna->dire != SDOWN) || (input == ERR && sna->dire == SUP))
        {
            val.y--; // 上移(ncurses y轴从上到下递增)
            sna->dire = SUP;
        }   
        else if((input == KEY_DOWN && sna->dire != SUP)|| (input == ERR && sna->dire == SDOWN))
        {
            val.y++; // 下移
            sna->dire = SDOWN;
        }
        else if((input == KEY_LEFT && sna->dire != SRIGHT)|| (input == ERR && sna->dire == SLEFT))
        {
            val.x-=2; // 左移(特殊符号占2列,保证对齐)
            sna->dire = SLEFT;
        }
        else if((input == KEY_RIGHT && sna->dire != SLEFT) || (input == ERR && sna->dire == SRIGHT))
        {
            val.x+=2; // 右移
            sna->dire = SRIGHT;
        }

        // 碰撞检测1:撞墙(超出地图边界,游戏结束)
        if(val.x == -1 || val.x == attribute.mapwidth - 1  || val.y == 0 || val.y == attribute.maplength - 1)
        {
            falg = FALSE; // 游戏结束标志
            input = 2; // 标记撞墙失败
        }

        // 碰撞检测2:吃自己(位图快速判断,无需遍历链表)
        if(val.x != -1 && bitmap[val.y][val.x / 2 + 1] == 1) 
        {
            falg = FALSE;
            input = 1; // 标记吃自己失败
        }

        PushFront(sna->physics, val); // 头部插入新蛇头(移动核心)
        bitmap[val.y][val.x / 2 + 1] = 1; // 更新位图

        // 吃豆判断:蛇头与豆子坐标重合
        if(val.x == nap.x && val.y == nap.y)
        {
            sna->score++; // 分数+1
            // 重新生成豆子(避开蛇身)
            while(bitmap[nap.y][nap.x / 2 + 1] == 1)
            {
                nap.x = rand() % (attribute.mapwidth - 3) ;
                if(nap.x % 2 == 0) nap.x++;
                nap.y = rand() % (attribute.maplength - 2) + 1;
            } 
            // 绘制新豆子
            wattron(scrgame, COLOR_PAIR(3));
            mvwprintw(scrgame, nap.y, nap.x, "●");
            wattroff(scrgame, COLOR_PAIR(3));
        }
        else // 未吃豆时删除尾节点,保持蛇身长度
        {
            mvwprintw(scrgame, sna->physics->head->prev->val.y, sna->physics->head->prev->val.x, "  "); // 清除蛇尾符号
            bitmap[sna->physics->head->prev->val.y][sna->physics->head->prev->val.x / 2 + 1] = 0; // 更新位图
            PopBack(sna->physics); // 删除尾节点
        }

        // 胜利判断:分数达最大值(无空间生成新豆子)
        if(sna->score == maxscore)
        {
            falg = FALSE;
            input = 0; // 标记胜利
        }

        // 绘制新蛇头(启用对应颜色对)
        wattron(scrgame, COLOR_PAIR(attribute.color.headcol) );
        mvwaddwstr(scrgame, val.y, val.x == -1 ? 0 : val.x, phychar[attribute.color.snahead]);
        wattroff(scrgame, COLOR_PAIR(attribute.color.headcol)); // 关闭颜色属性

        // 刷新分数显示
        mvwprintw(scrscore, attribute.maplength / 2 - 5, 9, "%d", sna->score);
        wrefresh(scrgame);
        wrefresh(scrscore); // 即时更新分数窗口

        usleep(attribute.speed); // 控制移动速度(微秒级暂停)
    }

    // 游戏结束保存记录(暂停退出不保存)
    if(input != 3)
    {
        time(&now);
        strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", localtime(&now));
        char buffer[BUFFERSIZE];
        // 拼接记录:时间+分数+地图大小+速度+结果
        snprintf(buffer, BUFFERSIZE, "%s       %-6d     %s         %s        %s",
            timebuffer, sna->score, massage2[GET_MAPPOS(attribute.maplength)], massage3[GET_SPEEDPOS(attribute.speed)], massage4[input]);  
        Push(&allgameresult, buffer); // 加入历史记录数组
    }

    // 游戏结束提示
    wclear(scrgame);
    mvwprintw(scrgame, attribute.maplength/2 - 2, attribute.mapwidth/2 - 8, "游戏结束!%s", massage4[input]);
    mvwprintw(scrgame, attribute.maplength/2, attribute.mapwidth/2 - 10, "最终分数:%d  按任意键返回", sna->score);
    wrefresh(scrgame);
    nodelay(scrgame, FALSE);
    wgetch(scrgame);
}

// 游戏主函数
void Game()
{
    clear();
    falg = TRUE;

    // 创建游戏窗口和分数窗口
    WINDOW* scrgame = newwin(attribute.maplength, attribute.mapwidth, (LINES-attribute.maplength)/2, (COLS-attribute.mapwidth)/2);
    WINDOW* scrscore = newwin(attribute.maplength, 20, (LINES-attribute.maplength)/2, (COLS-attribute.mapwidth)/2 + attribute.mapwidth + 5);
    box(scrgame, 0, 0);
    box(scrscore, 0, 0);
    mvwprintw(scrscore, attribute.maplength/2 - 5, 2, "当前分数:");

    // 初始化蛇
    Snake sna;
    sna.physics = GetList();
    sna.dire = SRIGHT;
    sna.score = 0;
    // 初始化蛇身(3个节点)
    type initPos[] = {{3, 1}, {5, 1}, {7, 1}};
    for(int i = 0; i < 3; i++)
    {
        PushFront(sna.physics, initPos[i]);
    }

    // 绘制初始蛇身
    Node* cur = sna.physics->head->next;
    wattron(scrgame, COLOR_PAIR(attribute.color.headcol));
    mvwaddwstr(scrgame, cur->val.y, cur->val.x, phychar[attribute.color.snahead]);
    wattroff(scrgame, COLOR_PAIR(attribute.color.headcol));
    cur = cur->next;
    while(cur != sna.physics->head)
    {
        wattron(scrgame, COLOR_PAIR(attribute.color.phycol));
        mvwaddwstr(scrgame, cur->val.y, cur->val.x, phychar[attribute.color.snaphy]);
        wattroff(scrgame, COLOR_PAIR(attribute.color.phycol));
        cur = cur->next;
    }

    wrefresh(scrgame);
    wrefresh(scrscore);

    // 启动蛇移动逻辑
    SnakeMove(&sna, scrgame, scrscore);

    // 释放资源
    Destroy(sna.physics);
    delwin(scrgame);
    delwin(scrscore);
}

// 功能分发:根据选中选项执行对应操作
void RunOption(int option)
{
    switch (option)
    {
        case 0: Game(); break; // 0=开始游戏
        case 1: HistoryScores(); break; // 1=查看历史记录
        case 2: Setting(); break; // 2=主题/难度设置
        case 3: End(); break; // 3=退出游戏(释放资源)
        default:
        perror("RunOption"); // 打印异常信息,辅助定位问题
        exit(-1); // 异常终止进程(非0状态码)
    }
}

// 主函数
int main()
{
    Init(); // 初始化游戏
    int option = 0;
    while(1)
    {
        clear();
        option = ShowMenu(option); // 显示菜单并获取选择
        RunOption(option); // 执行对应功能
    }
    return 0;
}
相关推荐
invicinble1 小时前
对于后端要和linux打交道要掌握的点
linux·运维·python
_Johnny_1 小时前
ubuntu将磁盘剩余空间自动分配指南
linux·运维·ubuntu
fie88891 小时前
基于MATLAB的时变Copula实现方案
开发语言·matlab
冬奇Lab1 小时前
【Kotlin系列12】函数式编程在Kotlin中的实践:从Lambda到函数组合的优雅之旅
android·开发语言·kotlin
leiming61 小时前
linux 进程学习之信号
linux·运维·学习
若风的雨1 小时前
linux Page Table 和 TLB 操作总结
linux
AlenTech2 小时前
如何解决Ubuntu中使用系统pip报错的问题,error: externally-managed-environment
linux·ubuntu·pip
写代码的【黑咖啡】2 小时前
Python中的Msgpack:高效二进制序列化库
开发语言·python
Jaxson Lin2 小时前
Java编程进阶:线程基础与实现方式全解析
java·开发语言