文章目录
- 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); // 控制移动速度(微秒级暂停)
}
}
核心逻辑:
- 用nodelay设置非阻塞按键,无按键时input=ERR,蛇保持当前方向自动移动;
- 横坐标增减2是因为ncurses中特殊符号占2列,否则蛇身会错位;
- 移动后先插新蛇头,未吃豆再删尾节点,视觉上就是蛇在移动。
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;
}