贪吃蛇代码实现

一.基本信息

实现目标:使用C语言在Windows环境的控制台中实现贪吃蛇游戏

游戏运行

  1. 地图绘制
  2. 基本玩法
  3. 提示信息
  4. 游戏的开始与结束

基本玩法

  1. 通过上下左右键控制蛇的移动
  2. 蛇可以加速减速
  3. 吃掉食物可以得分并增加蛇的长度
  4. 可以自动暂停

游戏结束

  1. 撞墙
  2. 撞到自己
  3. 主动选择退出

涉及到的知识点:

C语言函数,枚举,结构体,单链表,动态内存管理,预处理指令,Win32API

Windows控制面板的设置(VS2022):

效果:

(ps:system函数可以用来执行系统命令,头文件<stdlib.h> )

 回想一下贪吃蛇的游戏面板,其中蛇应该出现在界面中央,而食物的位置是随机的,这就涉及以下几个问题

1.屏幕光标的坐标设置。

2.如何去操控光标呢?又如何去设置光标的坐标呢?

3.游戏进行过程中并不会看到光标,也就是说,要隐藏光标

4.如果通过按键的输入来控制蛇的移动

至于如何解决这些问题,就要用到Win32API的知识了,下面我就来简单介绍一下。

二.Win32API:

WIN32 API简介:

       Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。

控制台上的屏幕坐标COORD

COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。

COORD原型说明:

typedef struct  _COORD  
{
SHORT X; 
SHORT Y;
}COORD;
COORD pos ={10,15};

2.WIN32API函数

2.1.GetStdHandle函数

作用:

GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。(句柄类似于玩游戏时操控的手柄)

函数原型声明:

HANDLE GetStdHandle(DWORD nStdHandle);

参数: 

对于我们今天要实现的贪吃蛇代码来说,只需用到第二个参数就可以了

返回值:

如果该函数成功,则返回值为指定设备的句柄,(或为由先前对 SetStdHandle 的调用设置的重定向句柄。)

如果函数失败,则返回值为 INVALID_HANDLE_VALUE。

实例:

 HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄

2.2. CONSOLE_CURSOR_INFO

这是windows自带的一个结构体(可以直接拿来用),包含了控制台光标的信息

原型声明:

typedef struct  _CONSOLE_CURSOR_INFO  
{DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO;

• dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。

• bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。

实例:

 CONSOLE_CURSOR_INFO CursorInfo;CursorInfo.bVisible = false;

2.3 GetConsoleCursorInfo 函数

作用:

检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。

语法:

BOOL WINAPI GetConsoleCursorInfo(HANDLE               hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

参数含义:

hConsoleOutput 
控制台屏幕缓冲区的句柄。

lpConsoleCursorInfo
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息

返回值:

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

实例:

 HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息

2.4 SetConsoleCursorInfo 函数

作用:

设置制定控制台光标大小和可见性

BOOL WINAPI SetConsoleCursorInfo(HANDLE              hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

参数

hConsoleOutput 
控制台屏幕缓冲区的句柄。

lpConsoleCursorInfo [in]
指向 结构的指针CONSOLE_CURSOR_INFO,该结构为控制台屏幕缓冲区的光标提供新的规范。

返回值

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

 实例:

    HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息CursorInfo.bVisible = false;SetConsoleCursorInfo(houtput,&CursorInfo);//设置控制台光标的状态

2.5 SetConsoleCursorPosition 函数

作用:

设置指定控制台屏幕缓冲区中的光标位置。

语法:

BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD  dwCursorPosition
);

参数含义:

hConsoleOutput 
控制台屏幕缓冲区的句柄。 

dwCursorPosition
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。

 实例:

	COORD pos = { 10,15 };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄SetConsoleCursorPosition(houtput, pos);//设置光标位置

2.6 GetAsyncKeyState 函数 

作用:

确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState 后是否按下了该键。

语法:

SHORT GetAsyncKeyState(int vKey
);

参数:

vkey:虚拟键码

以下是需要用到的虚拟键码

VK_ESCAPE0x1BESC 键
VK_SPACE0x20空格键
VK_LEFT0x25LEFT ARROW 键
VK_UP0x26UP ARROW 键
VK_RIGHT0x27RIGHT ARROW 键
VK_DOWN0x28

DOWN ARROW 键

VK_F30x72F3 键
VK_F40x73F4 键

返回值:

GetAsyncKeyState 的返回值是short类型,在上一次调用函数GetAsyncKeyState 后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位位置为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.

总结:

GetStdHandle 函数——获得设备的控制手柄

GetConsoleCursorInfo 函数——获得光标信息

SetConsoleCursorInfo 函数——隐藏光标

SetConsoleCursorPosition 函数——设置光标位置

GetAsyncKeyState 函数 ——检测按键情况

这些函数本质上和库函数无甚区别,知道这些函数的返回值类型和参数后,就可以直接使用了。

三.贪吃蛇游戏的设计与分析

1.布置地图

1.1 控制台窗口的坐标:

横向为x轴,从左到右依次增长,纵向为y轴,从上到下,依次增长

坐标原点(0,0)在最左上角

x轴和y轴的单位长度不同,y轴的单位长度要大于x轴

1.2 地图元素:

墙体:□

蛇身:●

食物:★

以上字符,我采用宽字符的方式打印

普通字符占一个字节,宽字符占两个字节,汉字就是宽字符

 1.3.宽字符的打印

背景介绍: C语言在不断适应国际化的过程中,加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

1.3.1setlocale函数

C语言的标准输出是英文模式,即所有字符都只占一个字节,这显然无法满足所有国家(例如中国)的输出要求,因为光汉字数量就多打10万个,更别提字符了,所以C语言提供了本地模式。

setlocale函数用于修改当前地区,可以针对一个内容修改,也可针对不同内容修改。

原型声明:
char* setlocale (int category, const char* locale);

setlocale 的第一个参数如果是LC_ALL,就会影响所有的内容。C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。在任意程序执行开始,都会隐藏式执行调用: 

setlocale(LC_ALL, "C");//标准模式

参数:

category

Portion of the locale affected. It is one of the following constant values defined as macros in <clocale>:

valuePortion of the locale affected
LC_ALLThe entire locale.
LC_COLLATEAffects the behavior of strcoll and strxfrm.
LC_CTYPEAffects character handling functions (all functions of <cctype>, except isdigit and isxdigit), and the multibyte and wide character functions.
LC_MONETARYAffects monetary formatting information returned by localeconv.
LC_NUMERICAffects the decimal-point character in formatted input/output operations and string formatting functions, as well as non-monetary information returned by localeconv.
LC_TIMEAffects the behavior of strftime.

locale

C string containing the name of a C locale. These are system specific, but at least the two following locales must exist:

locale namedescription
"C"Minimal "C" locale
""Environment's default locale

If the value of this parameter is NULL, the function does not make any changes to the current locale, but the name of the current locale is still returned by the function.

而我们只需用到 LC_ALL和""

实例:
setlocale(LC_ALL, "");//切换到本地环境
1.3.2.打印函数

宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应wprintf() 的占位符为 %ls 

演示:

#include<stdio.h>
#include<locale.h>
int main()
{setlocale(LC_ALL, "");//切换到本地环境wchar_t ch1 = L'□';wchar_t ch2 = L'吃';wprintf(L"%lc\n", ch1);printf("%c\n", 'a');wprintf(L"%lc\n", ch2);return 0;
}

效果:

普通字符和宽字符的打印宽度:

1.4.地图坐标

实现一个27行(y轴),58列(x轴)的棋盘(读者可以自行修改)

设置光标位置
//设置光标位置
void SetPos(short x, short y)
{COORD pos = { x,y };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(houtput, pos);
}
墙体坐标:

y = 0  ,   x: 0~56

y = 26,   x: 0~56

x = 0  ,   y: 1~26

x = 56,   y: 1~26

代码逻辑:

打印墙体的两种情况:

横行:先设置光标位置,再通过循环依次打印剩下的墙体

纵列:在循环体里设置光标位置,再打印墙体

函数代码:
#define WALL L'□'
//创建地图
void CreatMap()
{setlocale(LC_ALL, "");//设置为本地模式int i = 0;//一个墙体占两个字节,//y = 0 , x: 0~56SetPos(0, 0);for (i = 0; i < 58; i += 2)//一个墙体占两个字节{wprintf(L"%lc", WALL);}//y = 26, x: 0~56SetPos(0,26);for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节{wprintf(L"%lc", WALL);}//x = 0  ,   y: 1~26for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节{SetPos(0, i);wprintf(L"%lc", WALL);}//x = 56, y: 1~26for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}

但是我们测试代码时会出现这种情况:

最下面一行的墙体被遮挡了,那时因为循环结束后,光标在(0,26)这个位置

在代码末尾手动将光标坐标置为(27,0)就可以了

2.数据结构体设计 

2.1蛇身和食物链表

结构体中应包含蛇的位置坐标和指向下一个位置的指针

代码:

//贪吃蛇蛇身的节点
typedef struct SnakeNode
{int x;//x轴坐标int y;//y轴坐标struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;

2.2贪吃蛇游戏的结构体

回想一下贪吃蛇游戏,有哪些元素呢?

蛇,食物,食物的分数,蛇走的方向,当下的得分,蛇的速度,游戏状态(死亡or正常),食物的分值

那结构体的元素应包含:

1.指向蛇和食物的指针

2.蛇头的方向:上下左右,可以用枚举体

3.当下的得分

4.游戏状态,也可以用枚举

5.蛇的速度

6.食物的分值

这里就出现了一个问题了,蛇的速度如何设置呢?这里介绍一个方法,通过调节程序休眠的时间(即蛇每走一步,程序就暂停一段时间)间隔,来控制速度

代码:

//蛇头移动的方向
enum DIRECTION
{UP,//上DOWN,//下LEFT,//左RIGHT//右
};
//游戏状态
enum GAME_STATUS
{NORMAL,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己了END_NORMAL//主动退出
};
//贪吃蛇
typedef struct Snake
{SnakeNode* psnake;//指向蛇的指针SnakeNode* pfood;//指向食物的指针enum DIRECTION dir;//蛇头走的方向enum GAME_STATUS status;//当前游戏的状态int food_score;//食物的分数int score;//当下的得分int sleep_time;//程序休眠时间
}Snake;

3.游戏流程设计

3.1.游戏开始

3.1.1设置窗口大小和名字

3.1.2隐藏光标

3.1.2创建欢迎界面

3.1.3创建地图,蛇,食物

3.2 游戏运行

3.2.1打印提示信息

3.2.2蛇的移动和速度

3.2.3食物的分值(蛇速度越快,食物分值越高)

3.2.4计算当下的得分

3.2.5判断当下的游戏状态(蛇是否死亡)

3.2.6蛇是否吃到食物了(吃到食物,蛇身加长)

3.2.7通过按键情况移动蛇身(蛇头移动方向的下一个位置是蛇身的新节点,尾结点删除)

3.3游戏结束

3.3.1给出结束原因

3.3.2销毁蛇身

3.3.3给出提示信息,要不要继续玩

四.游戏主逻辑的实现

1.游戏开始GameStart:完成游戏的初始化

2.游戏进行GameRun:完成游戏逻辑的实现

3.游戏结束GameEnd:销毁蛇身,释放内存

1.GameStart

1.1初始化游戏数据

1.1.1创建蛇身链表

创建蛇身节点,(蛇的坐标默认是不变的,大概位于控制台控制台屏幕中央)

1.1.2设置贪吃蛇游戏的数据(即给Snake结构体的各个变量赋值)

蛇头方向,游戏状态,当下的得分,食物的分值,以及默认程序休眠时间

1.1.3食物链表

由于食物的创建涉及到创建节点和设置随机坐标两方面,所以将其单独封装成一个函数

设置坐标,申请节点,打印食物

食物的坐标可以完全随机吗?显然不是,它是有一定限制的

食物坐标的位置是有范围限定

1.不可与蛇身重叠

2.不能在墙体之外

3.食物的x轴坐标必须为2的倍数

为什么呢?

蛇身占两个字节,它往前挪动一步也是两个字节。

第二个问题就来了,为什么不能一个字节一个字节的移动呢?

因为蛇身的下一个位置,就是新的链表节点所在,所以食物的坐标x必须为2的倍数,这样蛇才能吃到食物

代码
#define BODY L'●'  
#define FOOD L'★'#define POS_X 24
#define POS_Y 5//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{setlocale(LC_ALL, "");//本地化//初始化蛇身int i = 0;SnakeNode* pcur = NULL;for(i = 0;i<5;i++){pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间if (pcur == NULL){perror("InitSnake():malloc");//打印错误信息exit(1);//非正常退出}//设置蛇的位置坐标pcur->x = POS_X+i*2;//一个蛇身占两个字节pcur->y = POS_Y;pcur->next = NULL;		if (ps->psnake == NULL)//蛇身为空{ps->psnake = pcur;}else//头插法{pcur->next = ps->psnake;ps->psnake = pcur;}}pcur = ps->psnake;//pcur指向蛇头//打印蛇身while (pcur){//设置光标位置SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//初始化其他数据ps->dir = RIGHT;//初始状态向右走ps->food_score = 10;//食物的分值为10ps->score = 0;ps->sleep_time = 200;//单位是毫秒ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{setlocale(LC_ALL, "");//1.创建食物坐标//食物坐标范围x:2~54,y:1~25int x = 0;int y = 0;//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:do{x = rand() % 52 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);SnakeNode* pcur = ps->psnake;while (pcur)//食物不能和蛇身重合{if (x == ps->psnake->x && y == ps->psnake->y){goto again;//重新生成坐标}pcur = pcur->next;}//2.创建食物节点SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));if (pf == NULL){perror("CreatFood():malloc");//打印错误信息exit(1);}//3.给食物节点的变量赋值pf->x = x;pf->y = y;pf->next = NULL;ps->pfood = pf;//pfood指针指向新创建的节点//4.打印食物SetPos(x, y);wprintf(L"%lc", FOOD);
}

效果:

1.2.创建地图

该函数在前面已经实现了,即CreatMap函数

1.3打印欢迎界面

首先,语句的出现位置一定是在界面中央,即要先设置光标位置

其次,提示游戏规则

代码
//欢迎界面
void WelComeToGame()
{//1.打印第一个界面SetPos(40, 15);//设置光标位置printf("%s", "欢迎来到贪吃蛇小游戏!");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");//程序暂停system("cls");//清理屏幕信息//2.打印提示游戏规则的界面SetPos(25, 12);//设置光标位置printf("用 ↑  . ↓  . ←  . → 分别控制蛇的移动,  F3为加速,F4为减速\n");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");system("cls");
}

效果:

     

1.4.在游戏开始界面右边的空白处打印提示信息

打印游戏规则提示语句,但是我们发现,食物的分值和得分是会随着游戏的进行而变化的,所以不妨把这两个信息放在GameRun函数中去实现。

代码:
//打印提示信息
void PrintInfo(Snake* ps)
{SetPos(64, 11);printf("↑ 上 .↓ 下 .←  左 .→ 右\n");SetPos(64, 12);printf(" F3为加速,F4为减速\n");SetPos(64, 14);printf("不能穿墙,不能咬到自己");//SetPos(64, 15);//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);SetPos(64,17);printf("按ECS退出,按空格暂停");
}

GameStart函数代码:

//游戏开始
void GameStart()
{//欢迎界面WelComeToGame();srand((unsigned int)time(NULL));//初始化贪吃蛇数据Snake snake = { 0 };InitSnake(&snake);//创建食物CreatFood(&snake);//创建地图CreatMap();//打印提示信息PrintInfo(&snake);
}

2.GameRun

2.1接收按键信息

       在前面有介绍,如果 GetAsyncKeyState 函数返回值的16位short数据中,最低位为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1.

        那么,如何检测最低位呢?将返回值按位与1,即可得到最低位。

        游戏过程中,游戏的有效按键有8个,(上下左右,加速减速,退出暂停),那就需要考虑八种情况。如果每一种情况都写一遍判断语句,太过麻烦,不妨将其分装成一个宏。

#define KEY_PRESS(VK) (GetAsyncKeyState(VK)&1)?1:0
//最低位为1表达式为真,宏为1,反之,宏为0
2.1.1.改变蛇头的方向

       在贪吃蛇的结构体中,我们写了一个枚举变量dir,用来表示蛇头的方向。蛇每向前走一步,我们就判断一次玩家的按键情况,然后修改枚举变量的值。

       这时候问题来了,假设记录的上一个蛇头方向是向前,那么蛇能往后走吗?显然是不能的(不然不就咬到自己了嘛),所以在代码中还要进行if语句判断。

2.1.2.改变蛇的运动速度

按F3,F4键后,蛇会加速,减速,相应地,食物的分值也会增加和上升,假设每减速一次,休眠时间增加30ms,食物的分值减少2分,那么程序休眠时间最多增加到320s,因为食物的分值必须大于0。为了保持一致性,我将休眠时间的下限设置为80ms(当然读者也可以不设置该下限,但至少休眠时间必须保证大于0)

2.1.3.游戏暂停或退出

玩家按了ESC键后,整个游戏退出,按空格键,游戏暂停,这里就要写一个函数——暂停函数,当玩家再次按了空格键后,暂停状态结束。

2.2蛇移动函数

2.2.1创建新的节点
2.2.2根据蛇头的位置和方向确定下一个节点的坐标

蛇头方向向上或向下:x轴坐标不变,y轴坐标-1,+1

向左或向右:y轴坐标不变,x轴坐标-2,+2,

然后链接新节点

2.2.3.根据蛇有无吃到食物,来决定尾结点是否保留

没吃到食物,尾结点释放;吃到食物,尾结点保留,相当于蛇身的长度增加一节,将其写成不同的两个函数

在两个函数中,都要对新创建的蛇身进行打印,不同的是未吃到食物的函数中,需要在原来蛇尾的地方打印两个空格,用来清理蛇身。且在遍历蛇身的过程中,只遍历到倒数第二个节点,并释放尾结点;吃到食物的函数中,要对得分进行修改,并销毁原食物节点,创建新的食物

2.3.4判断游戏的状态

撞墙否,咬到自己否,并据此设置贪吃蛇结构体中的游戏状态变量

3.GameEnd

打印游戏结束的原因:正常退出or撞墙or咬到自己

在测试文件中,还可以加上提示语句:是否再来一局

到这里,代码的整体逻辑基本就写完了,但还有一些善后的工作,例如光标还未隐藏,控制台的窗口名字还未设置,不妨将其加到Gamestart函数中去。

	system("mode con cols=100 lines=30");//设置控制面板大小system("title 贪吃蛇");//更改终端窗口的名字HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息CursorInfo.bVisible = false;SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标的状态

附录:

snake.h文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>#define WALL L'□'
#define BODY L'●'  
#define FOOD L'★'#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&1)?1:0)
//最低位为1表达式为真,宏为1,反之,宏为0//蛇头移动的方向
enum DIRECTION
{UP,//上DOWN,//下LEFT,//左RIGHT//右
};
//游戏状态
enum GAME_STATUS
{NORMAL,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己了END_NORMAL//主动退出
};
//贪吃蛇蛇身的节点
typedef struct SnakeNode
{int x;//x轴坐标int y;//y轴坐标struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;//贪吃蛇
typedef struct Snake
{SnakeNode* psnake;//指向蛇的指针SnakeNode* pfood;//指向食物的指针enum DIRECTION dir;//蛇头走的方向enum GAME_STATUS status;//当前游戏的状态int food_score;//食物的分数int score;//当下的得分int sleep_time;//程序休眠时间
}Snake;//设置光标位置
void SetPos(short x,short y);
//创建地图
void CreatMap();//游戏开始函数
void GameStart(Snake* ps);//初始化贪吃蛇数据
void InitSnake(Snake* ps);//创建食物
void CreatFood(Snake* ps);//欢迎界面
void WelComeToGame();//打印提示信息
void PrintInfo(Snake* ps);//游戏进行函数
void GameRun(Snake* ps);//暂停
void Pause();//蛇移动
void SnakeMove(Snake* ps);//判断下一个节点是否是食物
int NextIsFood(Snake* ps);//吃掉食物
void EatFood(Snake* ps);
//未吃掉食物
void NoFood(Snake* ps);//是否撞墙
int KillBYWall(Snake* ps);
//是否咬到自己
int KillBYSelf(Snake* ps);//游戏结束
void GameEnd(Snake* ps);

snake.c文件

#include"snake.h"
//设置光标位置
void SetPos(short x, short y)
{COORD pos = { x,y };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(houtput, pos);
}
//创建地图
void CreatMap()
{setlocale(LC_ALL, "");//设置为本地模式int i = 0;//一个墙体占两个字节,//y = 0 , x: 0~56SetPos(0, 0);for (i = 0; i < 58; i += 2)//一个墙体占两个字节{wprintf(L"%lc", WALL);}//y = 26, x: 0~56SetPos(0,26);for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节{wprintf(L"%lc", WALL);}//x = 0  ,   y: 1~26for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节{SetPos(0, i);wprintf(L"%lc", WALL);}//x = 56, y: 1~26for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}SetPos(0, 27);
}//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{setlocale(LC_ALL, "");//本地化//初始化蛇身int i = 0;SnakeNode* pcur = NULL;for(i = 0;i<5;i++){pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间if (pcur == NULL){perror("InitSnake():malloc");//打印错误信息exit(1);//非正常退出}//设置蛇的位置坐标pcur->x = POS_X+i*2;//一个蛇身占两个字节pcur->y = POS_Y;pcur->next = NULL;if (ps->psnake == NULL)//蛇身为空{ps->psnake = pcur;}else//头插法{pcur->next = ps->psnake;ps->psnake = pcur;}}pcur = ps->psnake;//pcur指向蛇头//打印蛇身while (pcur){//设置光标位置SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//初始化其他数据ps->dir = RIGHT;//初始状态向右走ps->food_score = 10;//食物的分值为10ps->score = 0;ps->sleep_time = 200;//单位是毫秒ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{setlocale(LC_ALL, "");//1.创建食物坐标//食物坐标范围x:2~54,y:1~25int x = 0;int y = 0;//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:do{x = rand() % 52 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);SnakeNode* pcur = ps->psnake;while (pcur)//食物不能和蛇身重合{if (x == ps->psnake->x && y == ps->psnake->y){goto again;//重新生成坐标}pcur = pcur->next;}//2.创建食物节点SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));if (pf == NULL){perror("CreatFood():malloc");//打印错误信息exit(1);}//3.给食物节点的变量赋值pf->x = x;pf->y = y;pf->next = NULL;ps->pfood = pf;//pfood指针指向新创建的节点//4.打印食物SetPos(x, y);wprintf(L"%lc", FOOD);
}//欢迎界面
void WelComeToGame()
{//1.打印第一个界面SetPos(40, 15);//设置光标位置printf("%s", "欢迎来到贪吃蛇小游戏!");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");//程序暂停system("cls");//清理屏幕信息//2.打印提示游戏规则的界面SetPos(25, 12);//设置光标位置printf("用 ↑  . ↓  . ←  . → 分别控制蛇的移动,  F3为加速,F4为减速\n");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");system("cls");
}
//打印提示信息
void PrintInfo(Snake* ps)
{SetPos(64, 11);printf("↑ 上 .↓ 下 .←  左 .→ 右\n");SetPos(64, 12);printf(" F3为加速,F4为减速\n");SetPos(64, 14);printf("不能穿墙,不能咬到自己");//SetPos(64, 15);//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);SetPos(64,17);printf("按ESC退出,按空格暂停");SetPos(0, 27);
}
//游戏开始
void GameStart(Snake* ps)
{system("mode con cols=100 lines=30");//设置控制面板大小system("title 贪吃蛇");//更改终端窗口的名字HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息CursorInfo.bVisible = false;SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标的状态//欢迎界面WelComeToGame();srand((unsigned int)time(NULL));//初始化贪吃蛇数据InitSnake(ps);//创建食物CreatFood(ps);//创建地图CreatMap();//打印提示信息PrintInfo(ps);
}
//暂停
void Pause()
{while (1){Sleep(300);//休眠if (KEY_PRESS(VK_SPACE))//再按了一次暂停键,暂停结束{break;}}
}//判断下一个节点是否是食物
int NextIsFood(Snake* ps)
{//蛇头的下一个位置坐标与食物坐标重合return (ps->psnake->x == ps->pfood->x && ps->psnake->y == ps->pfood->y);//表达式为真,返回1,为假,返回0
}
//吃掉食物
void EatFood(Snake* ps)
{setlocale(LC_ALL, "");SnakeNode* pcur = ps->psnake;//打印蛇身while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->score += ps->food_score;free(ps->pfood);CreatFood(ps);
}
//未吃掉食物
void NoFood(Snake* ps)
{setlocale(LC_ALL, "");SnakeNode* pcur = ps->psnake;//打印蛇身while (pcur->next->next)//找到倒数第二个节点{SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//清理屏幕上原来的蛇尾SetPos(pcur->next->x, pcur->next->y);printf("  ");//在原来蛇尾的位置打印两个空格free(pcur->next);pcur->next = NULL;
}//蛇移动
void SnakeMove(Snake* ps)
{//1.创建新的节点SnakeNode* pcur = (SnakeNode*)malloc(sizeof(SnakeNode));if (pcur == NULL){perror("SnakeMove():malloc");exit(1);}//根据蛇头的位置和方向确定下一个节点的坐标switch(ps->dir){case UP://上pcur->x = ps->psnake->x;//x轴坐标不变pcur->y = ps->psnake->y - 1;//y轴坐标-1break;case DOWN://下pcur->x = ps->psnake->x;pcur->y = ps->psnake->y + 1;break;case LEFT://左pcur->x = ps->psnake->x - 2;//x轴坐标要-2pcur->y = ps->psnake->y;break;case RIGHT://右pcur->x = ps->psnake->x + 2;pcur->y = ps->psnake->y;break;}//3.将新节点与蛇身链接起来pcur->next = ps->psnake ;ps->psnake = pcur;if (NextIsFood(ps)){EatFood(ps);}else{NoFood(ps);}
}//是否撞墙
int KillBYWall(Snake* ps)
{return (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26);//表达式为真,返回1,为假,返回0
}
//是否咬到自己
int KillBYSelf(Snake* ps)
{//判断蛇头是否和蛇身重叠SnakeNode* pcur = ps->psnake->next;while (pcur){if (ps->psnake->x == pcur->x && ps->psnake->y == pcur->y){return 1;}pcur = pcur->next;}return 0;//没有重合,返回0
}
//游戏进行函数
void GameRun(Snake* ps)
{do{//打印提示信息SetPos(64, 15);printf("食物分值:%d,得分:%d", ps->food_score, ps->score);if (KEY_PRESS(VK_UP) && ps->dir != DOWN)//按键:上{ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP) //下{ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) //左{ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) //右{ps->dir = RIGHT;}else if (KEY_PRESS(VK_SPACE))//按了空格{Pause();}else if (KEY_PRESS(VK_ESCAPE))//ESC键{ps->status = END_NORMAL;break;}else if (KEY_PRESS(VK_F3))//加速键{if(ps->sleep_time>=80)//速度上限{ps->sleep_time -= 30;ps->food_score += 2;}}else if (KEY_PRESS(VK_F4))// 减速键{if (ps->sleep_time <= 320)//速度下限{ps->sleep_time += 30;ps->food_score -= 2;//食物的分数不能小于等于0}}Sleep(ps->sleep_time);SnakeMove(ps);//蛇移动if (KillBYWall(ps)){ps->status = KILL_BY_WALL;}else if (KillBYSelf(ps)){ps->status = KILL_BY_SELF;}} while (ps->status == NORMAL);
}//游戏结束
void GameEnd(Snake* ps)
{//1.打印游戏退出信息if (ps->status == END_NORMAL){SetPos(24,12);printf("您已退出,游戏结束!");}else if (ps->status == KILL_BY_SELF){SetPos(24, 12);printf("咬到自己了,游戏结束!");}else if (ps->status == KILL_BY_WALL){SetPos(24, 12);printf("撞墙了,游戏结束!");}//释放蛇身的节点SnakeNode* pcur = ps->psnake;while (pcur){SnakeNode* del = pcur;pcur = pcur->next;free(del);}ps->psnake = NULL;//释放食物节点free(ps->pfood);ps->pfood = NULL;
}

test.c文件

#include"snake.h"int main()
{char ch;do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(24, 15);printf("是否再来一局?(Y/N):");scanf("%c", &ch);} while (ch == 'Y' || ch == 'y');SetPos(0, 27);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/1934.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录算法训练营第三十四天|1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

1005.K次取反后最大化的数组和 思路&#xff1a; 贪心的思路&#xff0c;局部最优&#xff1a;让绝对值大的负数变为正数&#xff0c;当前数值达到最大&#xff0c;整体最优&#xff1a;整个数组和达到最大。 局部最优可以推出全局最优。 那么如果将负数都转变为正数了&…

SAP DMS后台配置(4)

当DMS和CS文档服务器关联的时候,除了需要BASIS进行相关的配置以外,我们DMS也要进行相关的系统配置 路径:SPRO—跨应用组件—文档管理—常规数据—存储系统设置—维护还原系统 路径:SPRO—跨应用组件—文档管理—常规数据—存储系统设置—维护还原类别 当DMS和CS文档服…

HarmonyOS开发实例:【图片编辑应用】

介绍 本篇Codelab通过动态设置元素样式的方式&#xff0c;实现几种常见的图片操作&#xff0c;包括裁剪、旋转、缩放和镜像。效果如图所示&#xff1a; 相关概念 [image组件]&#xff1a;图片组件&#xff0c;用来渲染展示图片。[div组件]&#xff1a;基础容器组件&#xff0…

VMware虚拟机安装CentOS详细教程

前言 最近小白发现&#xff0c;有很多小伙伴在学习过程中都需要配置CentOS的系统&#xff0c;因此决定更新一篇在VMware虚拟机上安装CentOS的教程。 小白最近也去看了一下各种云的服务商出租的服务器&#xff0c;其中可选的系统中也有包括CentOS&#xff0c;所以在VMware配置…

用python做傅里叶变换和系统辨识

一、原始信号 1、理想数据 &#xff08;1&#xff09;系统参数 参数类型数值J0.5 k g ∗ m 2 kg*m^2 kg∗m2K0.2b5 &#xff08;2&#xff09;激励曲线 import matplotlib.pyplot as plt import numpy as np# 生成数据 x np.linspace(0, 10, 1000) # 生成0到10之间的100…

Midjourney与waifu2x双剑合璧:完美打造超高清动漫图像

在追求完美的动漫图像时&#xff0c;质量和分辨率是两个关键因素。Midjourney&#xff08;一个神秘而强大的AI图像生成工具&#xff09;与waifu2x&#xff08;一个专门用于放大动漫风格图像的AI工具&#xff09;的结合使得创造超高清的动漫图像变得触手可及。本文将引导您如何使…

【C++】---STL之vector详解

【C】---STL之vector详解 一、vector的介绍&#xff1a;二、vector的成员函数&#xff1a;1、vector类的构造函数2、vector的元素访问符3、vector的迭代器4、vector的模版5、vector的拷贝构造6、vector的容量&#xff08;1&#xff09;vector的增容机制&#xff08;2&#xff0…

Spring的过滤器、拦截器、切面区别及案例分析

Spring的过滤器、拦截器、切面 三者的区别&#xff0c;以及对应案例分析 一、三者的实现方式 1.1 过滤器 xxxFilter 过滤器的配置比较简单&#xff0c;直接实现Filter接口即可&#xff0c;也可以通过WebFilter注解实现对特定URL的拦截&#xff0c;Filter接口中定义了三个方法…

告别数据丢失,轻松掌握文件自动备份秘籍

在这个数字化高速发展的时代&#xff0c;我们的工作和生活都离不开电脑&#xff0c;而电脑中存储的文件和数据更是至关重要。然而&#xff0c;数据丢失的风险无处不在&#xff0c;可能因为硬件故障、软件崩溃、病毒攻击等原因而导致重要文件丢失。因此&#xff0c;文件自动备份…

Abaqus三维晶体塑性Voronoi泰森多边形晶格建模插件

插件介绍 AbyssFish Voronoi2D&3D 3D V3.0 插件可对Abaqus内已进行网格划分的部件&#xff08;Part&#xff09;生成Voronoi泰森多边形区块。插件可对任意形状的二维或三维部件、任意特征&#xff08;实体或壳&#xff09;、任意单元形状进行指派Voronoi晶格&#xff0c;可…

【STM32F4】按键开关

上一章&#xff0c;我们介绍了STM32F4的IO口作为输出的使用&#xff0c;这一章&#xff0c;将向大家介绍如何使用按键作为输入使用。 &#xff08;一&#xff09;硬件连接 根据正点原子的stm32f4阿波罗开发板&#xff0c;可以看见 按键KEY0连接在PH3上、 KEY1连接在PH2上、 …

SQLite的DBSTAT 虚拟表(三十六)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite运行时可加载扩展(三十五&#xff09; 下一篇&#xff1a;SQLite—系列文章目录 1. 概述 DBSTAT 虚拟表是一个只读的同名虚拟表&#xff0c;返回 有关用于存储内容的磁盘空间量的信息 的 SQLite 数据库。 示例用例…

FPGA - ZYNQ 基于Axi_Lite的PS和PL交互

前言 在FPGA - ZYNQ 基于EMIO的PS和PL交互中介绍了ZYNQ 中PS端和PL端交互的开发流程&#xff0c;接下来构建基于基于Axi_Lite的PS和PL交互。 开发流程 Axi_Lite从机 在FPGA - AXI4_Lite&#xff08;实现用户端与axi4_lite之间的交互逻辑&#xff09;中&#xff0c;详解介绍…

性能工具之 JMeter 自定义 Java Sampler 支持国密 SM2 算法

文章目录 一、前言二、加密接口1、什么是SM22、被测接口加密逻辑 三、准备工作四、JMeter 扩展实现步骤1&#xff1a;准备开发环境步骤2&#xff1a;了解实现方法步骤3&#xff1a;runTest 方法步骤4&#xff1a;getDefaultParameters 方法步骤5&#xff1a;setupTest 方法 五、…

HTX迪拜之夜盛大举行:共筑开放、互联的Web3生态系统

4月18日&#xff0c;由HTX、HTX DAO主办&#xff0c;去中心化AI云游戏协议DeepLink赞助的HTX迪拜之夜主题活动“领航者相聚&#xff0c;引领币圈新风向”在迪拜盛大举行。通过在全球第二大加密中心-迪拜的频繁亮相&#xff0c;HTX正积极塑造自己作为行业领导者的形象&#xff0…

Mysql学习一

目录 1.启动数据库&#xff1a; 2.命令行连接到MySQL&#xff08;winr输入cmd&#xff09; 3.MySQL的三重结构&#xff1a; 4.SQL语句分类&#xff1a; 1.启动数据库&#xff1a; winr——输入services.msc进入本地服务 2.命令行连接到MySQL&#xff08;winr输入cmd&#x…

109. Python的turtle库简介

109. Python的turtle库简介 【目录】 文章目录 109. Python的turtle库简介1. 什么是turtle库&#xff1f;2. 用turtle库绘制一个爱心图案3. 库的导入方法3.1 直接导入整个库3.2 从库中导入特定的函数或类3.3 导入库中的所有内容3.4 为导入的库设置别名3.5 为导入的函数或变量设…

阿里巴巴Java开发规范——编程规约(3)

# 阿里巴巴Java开发规范——编程规约&#xff08;3&#xff09; 编程规约 &#xff08;四&#xff09; OOP规约 1.【强制】构造方法里面禁止加入任何业务逻辑&#xff0c;如果有初始化逻辑&#xff0c;请放在 init 方法中 这条编程规范的目的是为了保持代码的清晰性、可读性…

AOP

代理模式 提出问题 现有缺陷 假设我们有一个计算类&#xff0c;里面有加减乘除四个方法&#xff0c;现在我们要为这四个方法添加日志&#xff0c;即在方法执行的前后分别输出一句话&#xff0c;这时我们会发现如下缺陷&#xff1a; 1.对核心业务有干扰。核心业务是加减乘除…

货拉拉0-1数据指标体系构建与应用

目录 一、背景 二、指标体系搭建 2.1 指标设计 2.2 指标体系搭建 2.3 指标维度拆解 三、指标标准化建设 四、指标元数据管理 五、指标应用&未来规划 原文大佬介绍的这篇指标体系构建有借鉴意义&#xff0c;现摘抄下来用作沉淀学习。如有侵权请告知~ 一、背景 指标…