在控制台实现贪吃蛇

news2024/10/7 16:26:12

在控制台实现贪吃蛇

  • 前备知识
    • 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.coloradmin.cn/o/1618784.html

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

相关文章

的记忆:pandas(实在会忘记,就看作是一个 Excel 表格,或者是 SQL 表,或者是字典的字典。)

pandas 是一个开源的 Python 数据分析库&#xff0c;它提供了快速、灵活和富有表现力的数据结构&#xff0c;旨在使“关系”或“标记”数据的“快速分析、清洗和转换”变得既简单又直观。pandas 非常适合于数据清洗和转换、数据分析和建模等任务。以下是 pandas 的基本概念和主…

使用 Flutter 打造引人入胜的休闲游戏体验

作者 / Zoey Fan 去年&#xff0c;Flutter 休闲游戏工具包进行了一次重大更新。近期在旧金山举办的游戏开发者大会 (GDC) 上&#xff0c;Flutter 首次亮相。GDC 是游戏行业的顶级专业盛会&#xff0c;致力于帮助游戏开发者不断提升开发技能。欢迎您继续阅读&#xff0c;了解开发…

Redis(四) 主从、哨兵、集群环境搭建

结合前三期 Redis(一) Redis简介(Redis(一) Redis简介-CSDN博客) Redis(二) 可编程性(Redis(二) 可编程性-CSDN博客) Redis(三) 事务与发布订阅(Redis(三) 事务与发布订阅-CSDN博客) 目录 1.0 Redis主从 1.1 Redis 主从结构的基本原理和工作方式 1.2 Redis 主从结构的好处 …

经典案例|使用Supabase解决可视化大屏项目的常见问题

敏博科技专业致力于应急管理行业&#xff0c;提供以物联网技术和感知预警算法模型为核心的先进产品和解决方案。应急管理行业的业务非常繁多和复杂&#xff0c;很多时候都需要在短时间内交付出稳定高效的业务系统。如下两张图某市的安全生产监测预警系统 MemFire Cloud应用开…

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

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

Maven的下载和环境变量的配置

1下载 maven官网https://maven.apache.org/index.html点击Download 这个是Windows的下载版本&#xff0c;一般是最新的版本&#xff0c; 老的版本下载 以下是对应的老版本下载链接 下载好后是一个压缩包的形式 点击他进行解压到想放的文件夹中&#xff0c;&#xff08;一般不…

设计模式-00 设计模式简介之几大原则

设计模式-00 设计模式简介之几大原则 本专栏主要分析自己学习设计模式相关的浅解&#xff0c;并运用modern cpp 来是实现&#xff0c;描述相关设计模式。 通过编写代码&#xff0c;深入理解设计模式精髓&#xff0c;并且很好的帮助自己掌握设计模式&#xff0c;顺便巩固自己的c…

【神经网络基础辨析】什么是神经网络的主干(backbone)、颈部(neck)和头部(head)网络

在神经网络中&#xff0c;通常将网络分为三个部分&#xff1a;骨干网络&#xff08;Backbone&#xff09;、颈部网络&#xff08;Neck&#xff09;、和头部网络&#xff08;Head&#xff09;。 骨干网络&#xff08;Backbone&#xff09; 骨干网络通常是神经网络的主要部分&a…

探索设计模式的魅力:主从模式与AI大模型的结合-开启机器学习新纪元

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索主从模式与AI大模型之旅✨ &#x1f31f;Hey, tech enthusiasts! 你是否还在追…

【嵌入式AI部署神经网络】STM32CubeIDE上部署神经网络之指纹识别(Pytorch)——篇一|环境搭建与模型初步部署篇

前言&#xff1a;本篇主要讲解搭建所需环境&#xff0c;以及基于pytorch框架在stm32cubeide上部署神经网络&#xff0c;部署神经网络到STM32单片机&#xff0c;本篇实现初步部署模型&#xff0c;没有加入训练集与验证集&#xff0c;将在第二篇加入。篇二详细讲解STM32CubeIDE上…

PHP之内置web服务器

1. 前言 PHP从5.4开始&#xff0c;就提供了一个内置的web服务器。 这个主要是用来做本地的开发测试用的&#xff0c;不能用于线上环境。 将PHP的安装路径配置到电脑的系统环境变量Path中&#xff0c;下图是win7&#xff0c;win10中会看的更清楚 2. 进入项目目录&#xff0c;执…

OpenHarmony南向开发案例:【 智能家居中控】

应用场景简介 智能家居。 今天打造的这一款全新智能家庭控制系统&#xff0c;凸显应用在智能控制和用户体验的特点&#xff0c;开创国内智能家居系统体验新局面。新的系统主要应用在鸿蒙生态。 工程版本 系统版本/API版本&#xff1a;OpenHarmony SDK API 8IDE版本&#xf…

unity cinemachine相机 (案例 跟随角色移动)

安装相机包 打开包管理工具 在 unity registry 搜索cinemachine 会在maincamera中生成一个组件cinemachineBrain 只能通过虚拟相机操控 主相机 虚拟相机的参数 案例 1.固定相机效果 位置 在固定的地方 默认的模式 2.相机跟随人物效果 焦距设置 20 跟随设置 把playere…

【LeetCode热题100】【多维动态规划】不同路径

题目链接&#xff1a;62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记…

Django模型继承之Meta继承

在Django模型继承中&#xff0c;当一个抽象基类被设计完成后&#xff0c;它会将该基类中定义的Meta内部类以属性的形式提供给子类。另外&#xff0c;如果子类未定义自己的Meta类&#xff0c;那么它就会默认继承抽象基类的Meta类。 关于Meta类的继承&#xff0c;大致总结如下&a…

Ubuntu20.04安装redis5.0.7

redis下载命令&#xff1a; wget https://download.redis.io/releases/redis-5.0.7.tar.gz 解压到 opt目录下 tar -zxvf redis-5.0.7.tar.gz -C /opt apt install -y gcc # 安装gccapt install make # 安装make 后面执行make一直报错 make报错后清除&#xff1a; make …

机器学习(XgBoost)预测顶和底

之前的文章中&#xff0c;我们对中证1000指数进行了顶和底的标注。这一篇我们将利用这份标注数据&#xff0c;实现机器学习预测顶和底&#xff0c;并探讨一些机器学习的原理。 我们选取的特征非常简单–上影线和WR&#xff08;William’s R&#xff09;的一个变种。选取这两个…

环境配置——Windows平台配置VScode运行环境为远程服务器或虚拟机

1. 远程机需要先安装SSH服务&#xff0c;命令如下 sudo apt install openssh-server 2. 安装好后需要开启SSH服务&#xff1a; sudo service sshd start 3. 查看SSH服务是否有被开启&#xff1a; sudo systemctl status sshd.service 4. 本地Windows需要生成密钥将公钥放…

毕业撒花 流感服务小程序的设计与实现

目录 1.1 总体页面设计 1.1.1 用户首页 1.1.2 新闻页面 1.1.3 我的页面 1.1.5 管理员登陆页面 1.1.6 管理员首页 1.2 用户模块 1.2.1 体检预约功能 1.2.2 体检报告功能 1.2.4 流感数据可视化功能 1.2.5 知识科普功能 1.2.6 疾病判断功能 1.2.7 出示个人就诊码功能 …

(五)AB测试及两个案例 学习简要笔记 #统计学 #CDA学习打卡

目录 一. AB测试简介 1&#xff09;假设检验的一般步骤 2&#xff09;基于假设检验的AB测试步骤 二. 案例1&#xff1a;使用基于均值的假设检验进行AB测试 1&#xff09;原始数据 2&#xff09;提出原假设H0和备择假设H1 3&#xff09;使用均值之差的t检验&#xff0c;计…