贪吃蛇代码实现

一.基本信息

实现目标:使用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~56
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)//一个墙体占两个字节
	{
		wprintf(L"%lc", WALL);
	}
	//y = 26, x: 0~56
	SetPos(0,26);
	for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节
	{
		wprintf(L"%lc", WALL);
	}
	//x = 0  ,   y: 1~26
	for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//x = 56, y: 1~26
	for (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;//食物的分值为10
	ps->score = 0;
	ps->sleep_time = 200;//单位是毫秒
	ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{
	setlocale(LC_ALL, "");
	//1.创建食物坐标
	//食物坐标范围x:2~54,y:1~25
	int 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~56
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)//一个墙体占两个字节
	{
		wprintf(L"%lc", WALL);
	}
	//y = 26, x: 0~56
	SetPos(0,26);
	for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节
	{
		wprintf(L"%lc", WALL);
	}
	//x = 0  ,   y: 1~26
	for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//x = 56, y: 1~26
	for (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;//食物的分值为10
	ps->score = 0;
	ps->sleep_time = 200;//单位是毫秒
	ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{
	setlocale(LC_ALL, "");
	//1.创建食物坐标
	//食物坐标范围x:2~54,y:1~25
	int 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轴坐标-1
		break;
	case DOWN://下
		pcur->x = ps->psnake->x;
		pcur->y = ps->psnake->y + 1;
		break;
	case LEFT://左
		pcur->x = ps->psnake->x - 2;//x轴坐标要-2
		pcur->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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Macs Fan Control Pro for Mac:全面优化Mac风扇控制软件

Macs Fan Control Pro for Mac是一款专为苹果电脑用户设计的风扇控制软件&#xff0c;旨在通过精确的风扇速度调节&#xff0c;全面优化Mac的散热性能&#xff0c;确保系统始终运行在最佳状态。 Macs Fan Control Pro for Mac中文版下载 该软件具备实时监控功能&#xff0c;能够…

HarmonyOS ArkUI实战开发-NAPI异步编程

笔者在前 5 小节里讲述了在 OpenHarmony 上通过 NAPI 的方式实现了 JS 调用 C的能力&#xff0c;但是这些实现都是同步的&#xff0c;本节笔者简单介绍一下 NAPI 的异步实现。 约定编程规范 ArkUI 开发框架对外提供的 API 命名是需遵守一定规范的&#xff0c;以 ohos.display…

初学python记录:力扣39. 组合总和

题目&#xff1a; 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限…

【STM32】嵌入式实验二 GPIO 实验:数码管

实验内容&#xff1a; 编写程序&#xff0c;在数码管上显示自己的学号。 数码管相关电路&#xff1a; PA7对应的应该是段码&#xff0c;上面的图写错了。 注意&#xff1a;选中数码管是低电平选中&#xff1b;并且用74HC595模块驱动输出的段码&#xff0c; 这个模块的学习可以…

echarts折线图默认不显示数据圆点,鼠标划上之后折线图才显示圆点

只需要设置showSymbol为false就可以了&#xff0c;表示只在 tooltip hover 的时候显示。 代码如下&#xff1a; option {tooltip: {trigger: axis},xAxis: {type: category,data: [Mon, Tue, Wed, Thu, Fri, Sat, Sun]},yAxis: {type: value},series: [{data: [150, 230, 224…

【智能算法】寄生捕食算法(PPA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2020年&#xff0c;AAA Mohamed等人受到自然界乌鸦-布谷鸟-猫寄生系统启发&#xff0c;提出了寄生捕食算法&#xff08;Parasitism – Predation Algorithm, PPA&#xff09;。 2.算法原理 2.1算法…

HTML使用jQuery实现两个点击按钮,分别控制改文本字体颜色和字体大小

jQuery 简介 jQuery 是一个广泛使用的 JavaScript 库&#xff0c;旨在简化对 HTML 文档的操作、事件处理、动画效果和 AJAX 等操作。 案例源码 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name&q…

【C++】学习笔记——类和对象_4

文章目录 二、类和对象13.运算符重载赋值运算符重载 14. 日期类的实现Date.h头文件Date.cpp源文件test.cpp源文件 未完待续 二、类和对象 13.运算符重载 赋值运算符重载 我们之前学了一个拷贝构造函数&#xff0c;本质上就是创建一个对象&#xff0c;该对象初始化为一个已经…

go-cqhttp 机器人使用教程

API | go-cqhttp 帮助中心 参考 | go-cqhttp 帮助中心 机器人下载 发送消息 http://127.0.0.1:5700/send_msg?message_typeprivate&user_id911412667&message你好呀 检查端口是否打开 netstat -ano | findstr :5701 发送的请求 软件的dopost的解析 Overridepro…

Learn ComputeShader 02 Multiple kernels

前面已经成功创建了第一个compute shader&#xff0c;并且使用它替换掉quad的材质的纹理&#xff0c;现在我们将要在计算着色器中创建多个kernel。 首先调整上次的计算着色器&#xff0c;让它显示为红色。 然后再次创建一个kernel&#xff0c;显示为黄色。 结果应该是这样的…

【算法刷题】手撕LRU算法(原理、图解、核心思想)

文章目录 1.LRU算法1.1相关概念1.2图解举例1.3基于HashMap和双向链表实现1.3.1核心思想1.3.2代码解读1.3.3全部代码 1.LRU算法 1.1相关概念 LRU&#xff08;Least Recently Used&#xff0c;最近最久未使用算法&#xff09;&#xff1a; 定义&#xff1a;根据页面调入内存后的…

【Vue3】$subscribe订阅与反应

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

大模型实战—用户反馈分析

大模型实战—用户反馈概要提取 前面我们已经本地部署了大模型,正好公司有一个业务,可以用来练练手,业务背景是这样的,我们的产品上有一个用户反馈的功能,里面积累了有史以来用户对产品的反馈,公司现在想要分析一下用户目前对产品的评价,以及用户关注的点是什么。关于之…

OpenHarmony开发实例:【 待办事项TodoList】

简介 TodoList应用是基于OpenHarmony SDK开发的安装在润和HiSpark Taurus AI Camera(Hi3516d)开发板标准系统上的应用&#xff1b;应用主要功能是以列表的形式&#xff0c;展示需要完成的日程&#xff1b;通过本demo可以学习到 JS UI 框架List使用&#xff1b; 运行效果 样例…

vector的底层与使用

前言&#xff1a;vector是顺序表&#xff08;本质也是数组&#xff09; 文档参考网站&#xff1a;https://legacy.cplusplus.com/reference/vector/vector/vector/ //底层代码 #include<assert.h> #include<iostream> #include<vector> #include<string&g…

实现Spring底层机制(阶段1—编写自己的Spring容器,扫描包,得到bean的Class对象)

环境搭建抛出问题 1.环境搭建 1.创建maven项目 2.导入依赖 <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://maven.ap…

隐藏表头和最高层级的复选框

隐藏表头和最高层级的复选框 <!-- 表格 --><el-tableref"tableRef"v-loading"tableLoading"default-expand-allclass"flex-1 !h-auto"row-key"regionId":header-cell-class-name"selectionClass":row-class-name&q…

20240422,C++文件操作

停电一天之后&#xff0c;今天还有什么理由不学习呜呜……还是没怎么学习 一&#xff0c;文件操作 文件操作可以将数据持久化&#xff0c;对文件操作时须包含头文件<fstream> 两种文件类型&#xff1a;文本文件&#xff1a;文件以文本的ASCII码形式存储&#xff1b;二进…

算法导论 总结索引 | 第三部分 第十一章:散列表

1、动态集合结构&#xff0c;它至少要支持 INSERT、SEARCH 和 DELETE字典操作 散列表 是实现字典操作的 一种有效的数据结构。尽管 最坏情况下&#xff0c;散列表中 查找一个元素的时间 与链表中 查找的时间相同&#xff0c;达到了 Θ(n)。在实际应用中&#xff0c;散列表的性…

GLID: Pre-training a Generalist Encoder-Decoder Vision Model

1 研究目的 现在存在的问题是&#xff1a; 目前&#xff0c;尽管自监督预训练方法&#xff08;如Masked Autoencoder&#xff09;在迁移学习中取得了成功&#xff0c;但对于不同的下游任务&#xff0c;仍需要附加任务特定的子架构&#xff0c;这些特定于任务的子架构很复杂&am…