在控制台实现贪吃蛇

在控制台实现贪吃蛇

  • 前备知识
    • Win32API
      • COORD这个结构体的声明如下:
      • GetStdHandle 函数
      • GetConsoleCursorInfo 函数
      • SetConsoleCursorInfo 函数
    • SetConsoleCursorPosition 函数
      • getAsyncKeyState 函数
    • 控制台窗口的大小以及字符打印介绍
      • 控制台中的坐标
      • 宽字符及本地化介绍
      • setlocale 函数介绍:
      • 宽字符打印
  • 贪吃蛇的实现
    • 贪吃蛇结构的定义
    • GameStart函数
      • Welcomeshow函数(欢迎界面)
      • Mapshow函数(地图打印)
      • Initsnack初始化
        • CreatFood函数(创建食物)
      • 贪吃蛇和食物的打印
    • GameRun函数
      • snackmove函数(贪吃蛇移动)
        • next_is_food函数
      • kill函数(判断蛇的状态)
    • GameEnd函数
  • 完整的代码如下
  • 测试结果如下:

本文通过C语言在Windows环境下的控制台实现贪吃蛇小游戏,实现的基本功能包括地图的绘制,蛇的移动(这个过程到底是吃到食物还是没有吃到食物),以及贪吃蛇是否撞墙,或撞到自身,通过贪吃蛇是否吃到食物来计算当前的得分,还将实现加速减速的功能以及暂停游戏的功能

前备知识

这部分主要介绍实现贪吃蛇小游戏中所使用的Win32API。需要注意使用这些API需要包含头文件Windows.h

Win32API

由于本游戏是在Windows系统下的控制台中进行的,所以需要用到一些win32API,Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启
视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。

COORD这个结构体的声明如下:

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;

在这里插入图片描述
可通过一下的形式给COORD类型的变量进行赋值

COORD pos={15,20}

GetStdHandle 函数

GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。检索指定标准设备的句柄(标准输入、标准输出或标准错误)。

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);

在这里插入图片描述
对于我们实现的这个贪吃蛇来说,我们需要的输出设备,在贪吃蛇中使用的方式如下:

HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorInfo 函数

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

BOOL WINAPI GetConsoleCursorInfo(_In_  HANDLE  hConsoleOutput, _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

使用示例:

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得输出设备的句柄
CONSOLE_CURSOR_INFO curserinfo;
GetConsoleCursorInfo(houtput,&curserinfo)//从输出中句柄中获得光标的信息,将信息存放在curserinfo中

CONSOLE_CURSOR_INFO 结构体形式如下:
在这里插入图片描述
dwSize
由游标填充的字符单元的百分比。 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。
bVisible
游标的可见性。 如果游标可见,则此成员为 TRUE。

SetConsoleCursorInfo 函数

为指定的控制台屏幕缓冲区设置光标的大小和可见性。
在这里插入图片描述
示例:

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

SetConsoleCursorPosition 函数

设置指定控制台屏幕缓冲区中的光标位置。
在这里插入图片描述

COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);

由于在贪吃蛇中,我们需要在屏幕上不断打印,所以我们需要不断的调整光标位置,因此我们根据SetConsoleCursorPosition 函数写一个调整屏幕光标位置的函数。

void setpos(short x,short y)
{
	//首先需要获得输出设备的句柄
	HANDLE houtpot=GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos={x,y};
	//设置光标位置
	SetConsoleCursorPosition(houtpot,pos);
}

getAsyncKeyState 函数

GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
在贪吃蛇中我们定义一个宏来确定一个键是否被按下,VK表示的是虚拟键值

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

控制台窗口的大小以及字符打印介绍

可以使用cmd命令来设置控制台窗口的长宽:比如设计控制台窗口的大小30行,100列,可以在控制台窗口输入以下指令:

mode con cols=100 lines=30

通过一下指令设置控制台窗口的名字:

title snack

以上两个控制台命令可在控制台窗口执行,也可在C语言中通过system函数来执行

#include<stdio.h>
int main()
{
	system("mode con cols=100 lines=30");
	system("title snack");
	getchar();
	return 0;
}

pic center
往system函数传入上面的指令,可以实现窗口大小的调整以及控制台名字的改变。加入getchar这个的函数的目的是为了让程序停下来,以使得能够看见当前的效果,不然程序结束窗口的名字还是会变成原来的名字。

控制台中的坐标

COORD是Windows API中定义的一个结构体,这个结构体表示在控制台屏幕缓冲区上的坐标,坐标系(0,0)表示缓冲区顶部左侧单元格。
![pic center](https://img-blog.csdnimg.cn/direct/10cc7a690eae4ca8a3ad79abdb5c7721.png在这里插入图片描述

宽字符及本地化介绍

打印蛇使用宽字符■,打印食物使用宽字符◆,打印墙体使用宽字符□,普通的字符是占⼀个字节的,这类宽字符是占用2个字节。为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
#pic center
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部
分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下⾯的⼀个宏,
指定⼀个类项:
• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的行为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。

setlocale 函数介绍:

#pic centersetlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。 setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和" "(本地模式)。在C语言程序执行开始前都会执行下面语句:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常⽅式执行。
当程序运行起来后想改变地区,就只能显示调⽤setlocale函数。用" "作为第2个参数,调用setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

setlocale(LC_ALL, "");//切换到本地环境

宽字符打印

如果想在控制台屏幕上打印宽字符,首先需要进行本地化设置,在宽字符前需要加上L,不然C语言会将其当成窄字符处理,宽字符的打印用wprintf函数,打印单个宽字符所用的占位符为**%lc**,打印宽字符串所用的占位符是**%ls**。
示例:

wprintf(L"%ls","欢迎来到贪吃蛇小游戏\n");

⼀个普通字符占⼀个字符的位置但是打印⼀个汉字字符,占用2个字符的位置(在屏幕上就是行占用2个字符,也就是2个坐标)。屏幕上打印的蛇,食物以及相应的提示信息都是宽字符,所以我们需要计算一下这个控制台屏幕上的坐标。本文介绍的贪吃蛇小游戏使用的是58列,27行。由于坐标是从0开始算的,所以x的坐标范围为[0,57],y的坐标范围为[0,26]。我们通过在这个范围的边界四周设置墙体,所以对于食物x坐标范围为[2,54],y坐标为[1,25]。食物随机创建的位置不能和蛇身重合也不能出现在墙上。由于蛇身的打印或食物的打印都是用宽字符,所以蛇身和食物的横坐标x需要是2的倍数,否则显示会出现问题。

贪吃蛇的实现

贪吃蛇的实现分为三个部分,其大逻辑是游戏的开始,游戏的运行以及游戏的结束。在游戏的开始需要对游戏进行初始化之类的,这包括窗口大小的调整,窗口的名字,以及控制台屏幕光标的隐藏,欢迎界面,贪吃蛇地图的绘制,蛇和食物创建及初始化。
在游戏运行中,需要判断是否按过相应的控制键(在这个小游戏的设置中我们使用↑.↓.←.→.来控制蛇的移动,F3为加速,F4为减速),并且加速将能获得更高的分数,减速获得的分数更少。在游戏的结束,需要对贪吃蛇小游戏中的参数进行处理,如释放动态开辟的蛇的节点,以及食物等。

贪吃蛇结构的定义

贪吃蛇的实现是用链表来实现的,其结构如下:

//定义一个蛇的节点,食物的节点也和这是一样的
typedef struct snacknode
{
	short x;
	short y;
	struct snacknode* next;
}snacknode;

贪吃蛇小游戏需要定义一个枚举类型来表示当前游戏的状态,如下:

typedef enum state
{
	//正常进行
	Normal,
	//撞墙
	Kill_by_wall,
	//撞到自己
	Kill_by_self,
	//正常退出
	Normal_exit
}state;

通过这几种状态来表示当前游戏的状态。
对于贪吃蛇的移动的方向也需要进行定义,我们也采用枚举来进行定义,如下:

//蛇移动的方向
typedef enum direction
{
	//向上移动
	Up,
	//向下移动
	Down,
	//向左移动
	Left,
	//向右移动
	Right
}direction;

光有上面还是不行,如果进行传参的话参数量太多,所以我们将这个贪吃蛇小游戏的参数都放到一个结构体中,如下:

//定义贪吃蛇整个数据,其中包括蛇头,食物,游戏状态,食物的分数,当前的得分,蛇移动的方向,加速减速(这个其实就是睡眠时间)
typedef struct snack
{
	snacknode* snackhead;//蛇头
	snacknode* food;//食物
	state snack_state;//游戏状态
	int food_score;//当前食物的分数
	int total_score;//当前的得分
	int sleep_time;//通过sleep_time的长短来控制蛇的移动速度
	direction dir;//蛇移动的方向
}snack;

GameStart函数

这个是游戏开始函数,需要对对蛇的参数进行初始化以及打印相应的提示信息。
在游戏开始的时候,我们需要将控制台窗口的名字改为snack,并且调整窗口大小为30行100列,为了更好的显示我们还需要对控制台窗口的光标进行隐藏。这些完成之后需要打印欢迎界面,以及打印贪吃蛇小游戏的地图,并且初始化蛇的状态,打印蛇以及食物。

//游戏开始
void GameStart(snack* s)
{
	//设置窗口大小,设置窗口名字
	system("mode con cols=100 lines=30");
	system("title snack");
	//获取控制台的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO curserinfo;
	//通过句柄获取控制台中的光标
	GetConsoleCursorInfo(houtput, &curserinfo);//从句柄中获取鼠标的信息,将这个信息存储到curseinfo中
	curserinfo.bVisible = false;
	//将光标隐藏
	SetConsoleCursorInfo(houtput, &curserinfo);
	//打印欢迎界面
	Welcomeshow();
	//打印地图
	Mapshow();
	//初始化蛇
	Initsnack(s);
	//打印蛇和食物
	Showsnack(s);
	Showfood(s);
}

Welcomeshow函数(欢迎界面)

欢迎界面的打印,需要将光标设置到合适的位置,其代码如下:

//打印欢迎界面
void Welcomeshow()
{
	setpos(40, 15);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
	setpos(25, 15);
	wprintf(L"用↑.↓.←.→.来控制蛇的移动,F3为加速,F4为减速\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
	setpos(40, 15);
	wprintf(L"加速将能获得更高的分数\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
}

Mapshow函数(地图打印)

这个函数的实现也需要将光标的位置放到合适的位置,由于我们实现的是27行,58列的地图,而地图的的字符也是用宽字符□打印的(占两个字节),所以打印上和下的时候每次i都加2需要注意边界条件。虽说是27行,58列的地图但对于打印上,其x坐标为0到56,y坐标为0。对于打印下需要注意x坐标为0到56,y坐标为26。对于打印左,x坐标为0,y坐标为1到25(为什么不是0到26呢?这是由于前面打印上和下的时候已经打印过了)。对于打印右,x坐标为56,y坐标为0到25(为什么不是0到26呢?这是由于前面打印上和下的时候已经打印过了)。

#define snackprint L"■"//蛇打印的样式
#define foodprint L"◆"//食物打印的样式
#define wallprint L'□'//墙体打印的样式
//打印地图,创建27行,58列的地图
void Mapshow()
{
	//打印上
	int i = 0;
	for (i = 0; i < 58; i+=2)
	{
		setpos(i, 0);
		wprintf(L"%lc", wallprint);
	}
	//打印下
	for (i = 0; i < 58; i += 2)
	{
		setpos(i, 26);
		wprintf(L"%lc", wallprint);
	}
	//打印左
	for (i = 1; i < 26; i++)
	{
		setpos(0, i);
		wprintf(L"%lc", wallprint);
	}
	//打印右
	for (i = 1; i < 26; i++)
	{
		setpos(56, i);
		wprintf(L"%lc", wallprint);
	}
	setpos(40,27);
}

Initsnack初始化

贪吃蛇节点的底层是链表,我们在最开始的时候创建5个节点表示贪吃蛇的初试长度,贪吃蛇的最开始的方向是朝右行驶的,在这个初试化的时候还需要创建食物(食物的位置不能和挡枪蛇的节点重叠只能出现在墙体内)

#define POS_X 24//蛇初试的x坐标
#define POS_Y 5//蛇初试的y坐标
//初始化蛇以及状态,最开始蛇身为5
void Initsnack(snack* s)
{
	s->snackhead = NULL;
	//创建蛇的链表
	for (int i = 0; i < 5; i++)
	{
		snacknode* newnode = (snacknode*)malloc(sizeof(snacknode));//创建蛇节点
		if (newnode == NULL)
		{
			perror("Initsnack->malloc");
			return;
		}
		newnode->next = s->snackhead;
		s->snackhead = newnode;
		newnode->x = POS_X + i * 2;
		newnode->y = POS_Y;
	}
	//创建食物
	CreatFood(s);
	//初始化状态
	s->dir = Right;
	s->sleep_time = 300;
	s->snack_state = Normal;
	s->food_score = 10;
}
CreatFood函数(创建食物)
void CreatFood(snack* s)
{
	snacknode* food = (snacknode*)malloc(sizeof(snacknode));
	if (food == NULL)
	{
		perror("CreatFood->malloc");
		return;
	}
	short x;
	short y;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//此时x的坐标满足是2的倍数
	//现在还需要满足生成的食物不是蛇的身体或蛇头
	snacknode* cur = s->snackhead;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			//表示与蛇身重叠了,跳到前面生成随机坐标,继续生成
			goto again;
		}
		cur = cur->next;
	}
	//此时表示食物生成成功
	food->x = x;
	food->y = y;
	s->food = food;
}

贪吃蛇和食物的打印

//打印蛇
void Showsnack(snack* s)
{
	snacknode* cur = s->snackhead;
	//打印蛇
	while (cur)
	{
		setpos(cur->x, cur->y);
		wprintf(snackprint);
		cur = cur->next;
	}
}
//打印食物
void Showfood(snack* s)
{
	snacknode* cur = s->food;
	setpos(cur->x, cur->y);
	wprintf(foodprint);
}

GameRun函数

这个函数主要是完成游戏运行的逻辑,首先需要打印相应的提示信息,然后需要检测是否有相应的功能键按下,如果有相应的键按下需要修改贪吃蛇相应的参数。由于程序运行的很快,为了能够看到效果所以需要用Sleep这个函数来对其进行休眠来体现视觉效果,同时这个Sleep函数也间接性地控制了贪吃蛇移动的速度。然后便是蛇的移动的函数了。最后就是kill函数来判断蛇是否还是正常的状态,其实可以将这个函数理解为每次蛇走完看是否是正常的状态。代码如下:

//游戏运行
void GameRun(snack* s)
{
	do
	{
		Printhelpinfo(s);
		//检测是否有相应的功能键按下
		if (KEY_PRESS(VK_UP) && s->dir != Down)
		{
			s->dir = Up;
		}
		else if (KEY_PRESS(VK_DOWN) && s->dir != Up)
		{
			s->dir = Down;
		}
		else if (KEY_PRESS(VK_LEFT) && s->dir != Right)
		{
			s->dir = Left;
		}
		else if (KEY_PRESS(VK_RIGHT) && s->dir != Left)
		{
			s->dir = Right;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			stop();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			s->snack_state = Normal_exit;
			break;
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (s->sleep_time > 100)
			{
				s->sleep_time -= 50;
				s->food_score += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (s->food_score > 2)
			{
				s->sleep_time += 50;
				s->food_score -= 2;
			}
		}
		Sleep(s->sleep_time);
		//蛇的移动
		snackmove(s);
		//判断蛇是否撞墙或撞到自己
		kill(s);
	} while (s->snack_state == Normal);//正常模式就继续
}

snackmove函数(贪吃蛇移动)

贪吃蛇的移动其实就是根据当前贪吃蛇移动方向,算出贪吃蛇蛇移动到下一个节点的位置,不管下一个位置是不是食物都将下一个节点的结构体snacknode给创建出来,然后将下一个节点的坐标给这个节点,如果下一个位置是食物则将该位置变为新的蛇头。如果不是食物依然将这个位置变为蛇头,但最后一个位置不打印并且将最后一个位置的节点给释放点。代码如下:

//蛇的移动
void snackmove(snack* s)
{
	snacknode* next = (snacknode*)malloc(sizeof(snacknode));
	if (next == NULL)
	{
		perror("snackmove->malloc");
		return;
	}
	if (s->dir == Up)
	{
		next->x = s->snackhead->x;
		next->y = s->snackhead->y - 1;
	}
	else if (s->dir == Down)
	{
		next->x = s->snackhead->x;
		next->y = s->snackhead->y + 1;
	}
	else if (s->dir == Left)
	{
		next->x = s->snackhead->x-2;
		next->y = s->snackhead->y;
	}
	else if (s->dir == Right)
	{
		next->x = s->snackhead->x+2;
		next->y = s->snackhead->y;
	}
	//蛇的下一个位置是食物
	if (next_is_food(s,next))
	{
		next->next = s->snackhead;
		s->snackhead = next;
		snacknode* snacktail = s->snackhead;
		while (snacktail!= NULL)
		{
			setpos(snacktail->x, snacktail->y);
			wprintf(snackprint);
			snacktail = snacktail->next;
		}
		free(s->food);//将食物节点释放掉
		s->food = NULL;
		s->total_score += s->food_score;
		CreatFood(s);//创建新食物
		Showfood(s);
	}
	//蛇的下一个位置不是食物
	else
	{
		next->next = s->snackhead;
		s->snackhead = next;
		snacknode* snacktailpre = s->snackhead;
		while (snacktailpre->next->next != NULL)
		{
			setpos(snacktailpre->x, snacktailpre->y);
			wprintf(snackprint);
			snacktailpre = snacktailpre->next;
		}
		setpos(snacktailpre->x, snacktailpre->y);
		wprintf(snackprint);
		snacknode* del = snacktailpre->next;//释放蛇的尾节点
		snacktailpre->next = NULL;
		setpos(del->x, del->y);
		printf("  ");
		free(del);
		del = NULL;
	}
}
next_is_food函数
//判断下一个节点是否是食物,蛇移动的下一个节点为食物就返回true,否则返回false
bool next_is_food(snack* s, snacknode* next)
{
	if (s->food->x == next->x && s->food->y == next->y)
	{
		return true;
	}
	return false;
}

kill函数(判断蛇的状态)

//撞到自己或撞到墙
void kill(snack* s)
{
	//首先判断是否撞到自己
	snacknode* head = s->snackhead;
	snacknode* cur = s->snackhead->next;
	while (cur)
	{
		if (cur->x == head->x && cur->y == head->y)
		{
			//将状态设置成Kill_by_self
			s->snack_state = Kill_by_self;
			return;
		}
		cur = cur->next;
	}
	if (head->x == 0 || head->x == 56 || head->y == 0 || head->y == 26)
	{
		s->snack_state = Kill_by_wall;
	}
}

GameEnd函数

该函数是用来处理游戏结束之后的善后工作的,如判断贪吃蛇是什么情况导致游戏结束的,并打印相应的提示信息。并且将开辟的动态内存给释放掉。
代码如下:

//游戏结束
void GameEnd(snack* s)
{
	//此时表示不是正常模式,已经退出游戏了
	if (s->snack_state == Normal_exit)
	{
		setpos(20, 10);
		wprintf(L"正常退出\n");
	}
	else if (s->snack_state == Kill_by_self)
	{
		setpos(20, 10);
		wprintf(L"很遗憾你咬到自己了\n");
	}
	else if (s->snack_state == Kill_by_wall)
	{
		setpos(20, 10);
		wprintf(L"很遗憾你撞到墙了\n");
	}
	//将动态内存给释放
	snacknode* cur = s->snackhead;
	while (cur)
	{
		snacknode* del = cur;
		cur = cur->next;
		free(del);
	}
	s->snackhead = NULL;
	//食物的那个内存也需要释放掉
	free(s->food);
	s->food = NULL;
}

完整的代码如下

SnackGame.h文件代码如下:

#pragma once
#include<stdio.h>
#include<windows.h>
#include<assert.h>
#include<stdlib.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
//#include"vld.h"

//定义按键是否被按下的宏,如果按下就是1否则就是0
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

#define POS_X 24//蛇初试的x坐标
#define POS_Y 5//蛇初试的y坐标

#define snackprint L"■"//蛇打印的样式
#define foodprint L"◆"//食物打印的样式
#define wallprint L'□'//墙体打印的样式


//定义一个蛇的节点
typedef struct snacknode
{
	short x;
	short y;
	struct snacknode* next;
}snacknode;

typedef enum state
{
	//正常进行
	Normal,
	//撞墙
	Kill_by_wall,
	//撞到自己
	Kill_by_self,
	//正常退出
	Normal_exit
}state;

//蛇移动的方向
typedef enum direction
{
	//向上移动
	Up,
	//向下移动
	Down,
	//向左移动
	Left,
	//向右移动
	Right
}direction;


//定义贪吃蛇整个数据,其中包括蛇头,食物,游戏状态,食物的分数,当前的得分,蛇移动的方向,加速减速(这个其实就是睡眠时间)
typedef struct snack
{
	snacknode* snackhead;
	snacknode* food;
	state snack_state;
	int food_score;
	int total_score;
	int sleep_time;
	direction dir;
}snack;

//设置坐标
void setpos(short x, short y);

//打印欢迎界面
void Welcomeshow();

//打印地图,创建27行,58列的地图
void Mapshow();

//初始化蛇,最开始蛇身为5
void Initsnack(snack* s);

//创建食物
void CreatFood(snack* s);

//打印蛇和食物
void Showsnack(snack* s);

//打印食物
void Showfood(snack* s);

//打印帮助信息
Printhelpinfo(snack* s);

//暂停游戏
void stop();

//蛇的移动
void snackmove(snack* s);

//判断下一个节点是否是食物
bool next_is_food(snack* s, snacknode* next);

//撞到自己或撞到墙
void kill(snack* s);

test.c代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SnackGame.h"
//游戏开始
void GameStart(snack* s)
{
	//设置窗口大小,设置窗口名字
	system("mode con cols=100 lines=30");
	system("title snack");
	//获取控制台的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO curserinfo;
	//通过句柄获取控制台中的光标
	GetConsoleCursorInfo(houtput, &curserinfo);//从句柄中获取鼠标的信息,将这个信息存储到curseinfo中
	curserinfo.bVisible = false;
	//将光标隐藏
	SetConsoleCursorInfo(houtput, &curserinfo);
	//打印欢迎界面
	Welcomeshow();
	//打印地图
	Mapshow();
	//初始化蛇
	Initsnack(s);
	//打印蛇和食物
	Showsnack(s);
	Showfood(s);
}

//游戏运行
void GameRun(snack* s)
{
	//setpos(40, 27);
	//system("pause");
	do
	{
		Printhelpinfo(s);
		//检测是否有相应的功能键按下
		if (KEY_PRESS(VK_UP) && s->dir != Down)
		{
			s->dir = Up;
		}
		else if (KEY_PRESS(VK_DOWN) && s->dir != Up)
		{
			s->dir = Down;
		}
		else if (KEY_PRESS(VK_LEFT) && s->dir != Right)
		{
			s->dir = Left;
		}
		else if (KEY_PRESS(VK_RIGHT) && s->dir != Left)
		{
			s->dir = Right;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			stop();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			s->snack_state = Normal_exit;
			break;
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (s->sleep_time > 100)
			{
				s->sleep_time -= 50;
				s->food_score += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (s->food_score > 2)
			{
				s->sleep_time += 50;
				s->food_score -= 2;
			}
		}
		Sleep(s->sleep_time);
		//蛇的移动
		snackmove(s);
		//判断蛇是否撞墙或撞到自己
		kill(s);
	} while (s->snack_state == Normal);//正常模式就继续
}

//游戏结束
void GameEnd(snack* s)
{
	//此时表示不是正常模式,已经退出游戏了
	if (s->snack_state == Normal_exit)
	{
		setpos(20, 10);
		wprintf(L"正常退出\n");
	}
	else if (s->snack_state == Kill_by_self)
	{
		setpos(20, 10);
		wprintf(L"很遗憾你咬到自己了\n");
	}
	else if (s->snack_state == Kill_by_wall)
	{
		setpos(20, 10);
		wprintf(L"很遗憾你撞到墙了\n");
	}
	//将动态内存给释放
	snacknode* cur = s->snackhead;
	while (cur)
	{
		snacknode* del = cur;
		cur = cur->next;
		free(del);
	}
	s->snackhead = NULL;
	//食物的那个内存也需要释放掉
	free(s->food);
	s->food = NULL;
}


void Gametest()
{
	srand((unsigned int)time(NULL));
	char input = 'y';
	do
	{
		if (input == 'y' || input == 'Y')
		{
			system("cls");
			//创建蛇
			snack s = { 0 };
			//游戏开始,进行相应的初始化
			GameStart(&s);
			//游戏运行
			GameRun(&s);
			// 游戏结束,进行后续的处理
			GameEnd(&s);
			setpos(0, 27);
			wprintf(L"是否要再玩Y/N:");
			setpos(15, 27);
			input = getchar();
			while (getchar() != '\n');
		}
		else if(input != 'y' || input != 'Y'|| input == 'n' || input == 'N')
		{
			setpos(0, 27);
			setpos(0, 27);
			wprintf(L"输入错误请重新输入Y/N:");
			setpos(25, 27);
			input = getchar();
			while(getchar()!='\n');
		}
	} while (input != 'n' && input != 'N');
}

int main()
{
	setlocale(LC_ALL, "");//切换到本地环境
	Gametest();
	return 0;
}

SnackGame.c文件代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SnackGame.h"

//设置光标的坐标
void setpos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;
	//首先获取控制台中的句柄
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置光标的位置pos
	SetConsoleCursorPosition(houtput, pos);
}

//打印欢迎界面
void Welcomeshow()
{
	setpos(40, 15);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
	setpos(25, 15);
	wprintf(L"用↑.↓.←.→.来控制蛇的移动,F3为加速,F4为减速\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
	setpos(40, 15);
	wprintf(L"加速将能获得更高的分数\n");
	setpos(43, 25);//用来控制 请按任意键继续. . .这个字符打印的位置
	system("pause");
	system("cls");
}

//打印地图,创建27行,58列的地图
void Mapshow()
{
	//打印上
	int i = 0;
	for (i = 0; i < 58; i+=2)
	{
		setpos(i, 0);
		wprintf(L"%lc", wallprint);
	}
	//打印下
	for (i = 0; i < 58; i += 2)
	{
		setpos(i, 26);
		wprintf(L"%lc", wallprint);
	}
	//打印左
	for (i = 1; i < 26; i++)
	{
		setpos(0, i);
		wprintf(L"%lc", wallprint);
	}
	//打印右
	for (i = 1; i < 26; i++)
	{
		setpos(56, i);
		wprintf(L"%lc", wallprint);
	}
	setpos(40,27);
}

//创建食物
void CreatFood(snack* s)
{
	snacknode* food = (snacknode*)malloc(sizeof(snacknode));
	if (food == NULL)
	{
		perror("CreatFood->malloc");
		return;
	}
	short x;
	short y;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//此时x的坐标满足是2的倍数
	//现在还需要满足生成的食物不是蛇的身体或蛇头
	snacknode* cur = s->snackhead;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			//跳到前面生成随机坐标,继续生成
			goto again;
		}
		cur = cur->next;
	}
	//此时表示食物生成成功
	food->x = x;
	food->y = y;
	s->food = food;
}

//初始化蛇以及状态,最开始蛇身为5
void Initsnack(snack* s)
{
	s->snackhead = NULL;
	//创建蛇的链表
	for (int i = 0; i < 5; i++)
	{
		snacknode* newnode = (snacknode*)malloc(sizeof(snacknode));//创建蛇节点
		if (newnode == NULL)
		{
			perror("Initsnack->malloc");
			return;
		}
		newnode->next = s->snackhead;
		s->snackhead = newnode;
		newnode->x = POS_X + i * 2;
		newnode->y = POS_Y;
	}
	//创建食物
	CreatFood(s);
	//初始化状态
	s->dir = Right;
	s->sleep_time = 300;
	s->snack_state = Normal;
	s->food_score = 10;
}

//打印蛇
void Showsnack(snack* s)
{
	snacknode* cur = s->snackhead;
	//打印蛇
	while (cur)
	{
		setpos(cur->x, cur->y);
		wprintf(snackprint);
		cur = cur->next;
	}
}
//打印食物
void Showfood(snack* s)
{
	snacknode* cur = s->food;
	setpos(cur->x, cur->y);
	wprintf(foodprint);
}

//打印帮助信息
Printhelpinfo(snack* s)
{
	setpos(64, 10);
	wprintf(L"得分:");
	printf("%3d", s->total_score);
	wprintf(L",每个食物得分:");
	printf("%3d", s->food_score);
	wprintf(L"分\n");
	setpos(64, 15);
	wprintf(L"不能穿墙,不能咬到自己");
	setpos(64, 16);
	wprintf(L"用↑.↓.←.→分别控制蛇的移动\n");
	setpos(64, 17);
	wprintf(L"F3为加速,F4为减速\n");
	setpos(64, 18);
	wprintf(L"速度越快食物分数越高\n");
	setpos(64, 19);
	wprintf(L"速度越慢食物分数越低\n");
	setpos(64, 20);
	wprintf(L"ESC:退出游戏.,space暂停游戏\n");
}

//暂停游戏
void stop()
{
	while (1)
	{
		Sleep(500);
		if (KEY_PRESS(VK_SPACE))
			break;
	}
}

//判断下一个节点是否是食物,蛇移动的下一个节点为食物就返回true,否则返回false
bool next_is_food(snack* s, snacknode* next)
{
	if (s->food->x == next->x && s->food->y == next->y)
	{
		return true;
	}
	return false;
}

//蛇的移动
void snackmove(snack* s)
{
	snacknode* next = (snacknode*)malloc(sizeof(snacknode));
	if (next == NULL)
	{
		perror("snackmove->malloc");
		return;
	}
	if (s->dir == Up)
	{
		next->x = s->snackhead->x;
		next->y = s->snackhead->y - 1;
	}
	else if (s->dir == Down)
	{
		next->x = s->snackhead->x;
		next->y = s->snackhead->y + 1;
	}
	else if (s->dir == Left)
	{
		next->x = s->snackhead->x-2;
		next->y = s->snackhead->y;
	}
	else if (s->dir == Right)
	{
		next->x = s->snackhead->x+2;
		next->y = s->snackhead->y;
	}
	//蛇的下一个位置是食物
	if (next_is_food(s,next))
	{
		next->next = s->snackhead;
		s->snackhead = next;
		snacknode* snacktail = s->snackhead;
		while (snacktail!= NULL)
		{
			setpos(snacktail->x, snacktail->y);
			wprintf(snackprint);
			snacktail = snacktail->next;
		}
		free(s->food);
		s->food = NULL;
		s->total_score += s->food_score;
		CreatFood(s);//创建新食物
		Showfood(s);
	}
	//蛇的下一个位置不是食物
	else
	{
		next->next = s->snackhead;
		s->snackhead = next;
		snacknode* snacktailpre = s->snackhead;
		while (snacktailpre->next->next != NULL)
		{
			setpos(snacktailpre->x, snacktailpre->y);
			wprintf(snackprint);
			snacktailpre = snacktailpre->next;
		}
		setpos(snacktailpre->x, snacktailpre->y);
		wprintf(snackprint);
		snacknode* del = snacktailpre->next;
		snacktailpre->next = NULL;
		setpos(del->x, del->y);
		printf("  ");
		free(del);
		del = NULL;
	}
}

//撞到自己或撞到墙
void kill(snack* s)
{
	//首先判断是否撞到自己
	snacknode* head = s->snackhead;
	snacknode* cur = s->snackhead->next;
	while (cur)
	{
		if (cur->x == head->x && cur->y == head->y)
		{
			//将状态设置成Kill_by_self
			s->snack_state = Kill_by_self;
			return;
		}
		cur = cur->next;
	}
	if (head->x == 0 || head->x == 56 || head->y == 0 || head->y == 26)
	{
		s->snack_state = Kill_by_wall;
	}
}

测试结果如下:

在这里插入图片描述
#pic_center
#pic_center
#pic_ceenter
#pic_center
#pic_center
本文介绍了贪吃蛇小游戏的简易实现,主要是通过链表来对其进行处理的。感谢大家的观看,如有错误不足之处欢迎大家批评指正!!!

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

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

相关文章

多线程情况下IBMMQ报文丢失原因分析

背景 最近工作中&#xff0c;使用IBMMQ&#xff0c;重启服务时有偶发性的报文丢失情况&#xff0c;应用从队列中获取到了消息&#xff0c;但是线程停止没有处理。 分析 消息处理线程流程&#xff1a; 判断线程状态是否可用&#xff0c;如果不可用直接返回。使用MQQueue.get…

Seurat -- Introduction to scRNA-seq integration 跟随学习记录

文章目录 数据是如何转换的原始ifnb数据对象Splits object后的数据对象数据对象构建完成后的标准流程Normalization后的数据对象scale 后的数据对象 不同的样本进行整合JoinLayers干了什么 数据是如何转换的 seurat object 中assays R N A l a y e r s RNAlayers RNAlayersco…

卡尔曼滤波器(一):卡尔曼滤波器简介

观看MATLAB技术讲座笔记&#xff0c;该技术讲座视频来自bilibili账号&#xff1a;MATLAB中国。 一、什么是卡尔曼滤波器 卡尔曼滤波器是一种优化估计算法&#xff0c;是一种设计最优状态观测器的方法&#xff0c;其功能为&#xff1a; 估算只能被间接测量的变量&#xff1b;通…

​漏电继电器JHOK-ZBLφ150mm 0.03-3A 0.2-2S导轨安装JOSEF约瑟

系列型号&#xff1a; JHOK-ZBL多档切换式漏电&#xff08;剩余&#xff09;继电器&#xff08;导轨&#xff09; JHOK-ZBL1多档切换式漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBL2多档切换式漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBM多档切换式漏电&#xff08;…

深入理解分布式事务① ---->分布式事务基础(四大特性、五大类型、本地事务、MySQL并发事务问题、MySQL事务隔离级别命令设置)详解

目录 深入理解分布式事务① ---->分布式事务基础&#xff08;四大特性、五大类型、本地事务、MySQL并发事务问题、MySQL事务隔离级别命令设置&#xff09;详解事务的基本概念1、什么是事务&#xff1f;2、事务的四大特性2-1&#xff1a;原子性&#xff08;Atomic&#xff09…

STM32点灯大师(中断法)

一、使用CubeMX配置 新增加了RCC进行配置 二、代码 需要重写虚函数&#xff0c;给自己引用

Python打怪升级(4)

在计算机领域常常有说"合法"和"非法"指的是:是否合理&#xff0c;是否有效&#xff0c;并不是指触犯了法律。 random.randint(begin,end) 详细讲解一下这个random是指模板&#xff0c;也就是别人写好的代码直接来用&#xff0c;在Python当中&#xff0c;…

《R语言与农业数据统计分析及建模》学习——ggplot2绘图基础

一、农业科研数据可视化常用图形及用途 1、数据可视化的重要性 通过可视化&#xff0c;我们可以更直观地理解和分析数据的特征和趋势。 2、常用图表类型及其概述 散点图&#xff1a;用于展示两个变量之间的关系&#xff0c;可用于观察数据的分布、趋势和异常值。 折线图&…

网络安全之CSRFSSRF漏洞(上篇)(技术进阶)

目录 一&#xff0c;CSRF篇 二&#xff0c;认识什么是CSRF 三&#xff0c;实现CSRF攻击的前提 四&#xff0c;实战演练 【1】案例1 【2】案例2 【3】案例3 【4】案例4&#xff08;metinfo&#xff09; 一&#xff0c;CSRF篇 二&#xff0c;认识什么是CSRF CSRF&#x…

YesPMP众包平台最新项目

YesPMP一站式互联网众包平台&#xff0c;最新外包项目&#xff0c;有感兴趣的用户可进入平台参与竞标。 &#xff08;竞标后由项目方直接与服务商联系&#xff0c;双方直接对接&#xff09; 1.查看项目&#xff1a;个人技术-YesPMP平台 2.查看项目&#xff1…

【003_音频开发_基础篇_Linux进程通信(20种你了解几种?)】

003_音频开发_基础篇_Linux进程通信&#xff08;20种你了解几种&#xff1f;) 文章目录 003_音频开发_基础篇_Linux进程通信&#xff08;20种你了解几种&#xff1f;)创作背景Linux 进程通信类型fork() 函数fork() 输出 2 次fork() 输出 8 次fork() 返回值fork() 创建子进程 方…

zkVM选型要点

1. 引言 当选择ZK工具&#xff0c;来做可验证链下计算来扩容区块链时&#xff0c;需考虑&#xff1a; 1&#xff09;为何应选择zkVM&#xff1f;2&#xff09;zkVM有哪些基本功能&#xff1f;3&#xff09;哪些zkVM可提供这些基本功能&#xff1f; 2. 为何应选择zkVM&#x…

OpenCV——图像分块局部阈值二值化

目录 一、算法原理1、算法概述2、参考文献 二、代码实现三、结果展示 OpenCV——图像分块局部阈值二值化由CSDN点云侠原创&#xff0c;爬虫自重。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫。 一、算法原理 1、算法概述 针对目前局部阈值二值…

消息队列 Kafka 入门篇(二) -- 安装启动与可视化工具

一、Windows 10 环境安装 1、下载与解压 首先&#xff0c;访问Apache Kafka的官方下载地址&#xff1a; https://kafka.apache.org/downloads 在本教程中&#xff0c;我们将使用kafka_2.13-2.8.1版本作为示例。下载完成后&#xff0c;解压到您的工作目录的合适位置&#xff…

目标检测——YOLOv6算法解读

论文&#xff1a;YOLOv6: A Single-Stage Object Detection Framework for Industrial Applications (2022.9.7) 作者&#xff1a;Chuyi Li, Lulu Li, Hongliang Jiang, Kaiheng Weng, Yifei Geng, Liang Li, Zaidan Ke, Qingyuan Li, Meng Cheng, Weiqiang Nie, Yiduo Li, Bo …

企业商业活动如何获得央级媒体的采访报道?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 企业想要获得央级媒体的采访报道&#xff0c;确实需要精心策划和准备&#xff1a; 一、如何巧妙给媒体报选题 精准定位&#xff1a;首先要明确企业的核心价值、创新点或行业影响力&…

【C++】手撕list(list的模拟实现)

目录 01.节点 02.迭代器 迭代器运算符重载 03.list类 &#xff08;1&#xff09;构造与析构 &#xff08;2&#xff09;迭代器相关 &#xff08;3&#xff09;容量相关 &#xff08;4&#xff09;访问操作 &#xff08;5&#xff09;插入删除 我们在学习数据结构的时候…

StartAI智能绘图软件出现“缺少Python运行库”怎么办?

StartAI做为一款国产AI界的新秀&#xff0c;是一款贴合AIGC新手的智能绘图软件。新手安装遇见“缺少Python运行库”怎么办”&#xff1f;小编一招搞定~ 解决方法&#xff1a;手动下载【resource文件】&#xff0c;将文件添加到安装目录下。 点击链接进行手动下载噢~ 确保 Star…

图像处理之模板匹配(C++)

图像处理之模板匹配&#xff08;C&#xff09; 文章目录 图像处理之模板匹配&#xff08;C&#xff09;前言一、基于灰度的模板匹配1.原理2.代码实现3.结果展示 总结 前言 模板匹配的算法包括基于灰度的匹配、基于特征的匹配、基于组件的匹配、基于相关性的匹配以及局部变形匹…

Spring-IOC之组件扫描

版本 Spring Framework 6.0.9​ 1. 前言 通过自动扫描&#xff0c;Spring 会自动从扫描指定的包及其子包下的所有类&#xff0c;并根据类上的特定注解将该类装配到容器中&#xff0c;而无需在 XML 配置文件或 Java 配置类中逐一声明每一个 Bean。 支持的注解 Spring 支持一系…
最新文章