C语言游戏实现——贪吃蛇

思路讲解

**

贪吃蛇游戏是要求我们要操控一条蛇,在游戏规定的空间之内,进行吃食物,吃到一个就增加蛇身的长度,并且游戏得分加1,如果吃到自己,和碰到墙就算死亡,同时可以增加蛇的速度和减慢蛇的速度,相对应的得分也会增加或减少,这就是游戏规则

**
我们可以创建一个结构体变量来存放游戏的相关信息

//贪吃蛇
struct snake_information
{
	snakenode* psnake;
	snakenode* pfood;
	int sleeptime;
	int socre;
	int food_socre;
	enum GAEM_STATE state;
	enum DRECTION snake_dir;
};

用一个snkaenode来存放蛇的节点的信息

struct snakenode
{
	int x;
	int y;
	struct snakenode* nodenext;
};
typedef struct snakenode snakenode;

用一个枚举体来存放当前游戏的状态

enum GAEM_STATE
{
	NORMAL = 1,
	KILL_BY_SEIF,
	KILL_BY_WALL,
	END_NORMAL,
	OK
};

还有存放蛇当前的方向

enum DRECTION
{
	UP = 1,
	DOWM,
	RIGHT,
	LEFT

};

这就是思路

游戏说明

1.首先我们要在开头列出开始游戏欢迎界面。
2.在开头列出相关操作按键的说明。
3.本贪吃蛇游戏设计的按键为:上下左右方向键作为蛇的移动方向F3和F4作为蛇的移动速度的加快和减慢,空格键是暂停 ESC是退出如果玩完一局要继续玩游戏就按1,不玩就按2.
有了以上的思路,我们大体可以分为几个部分来写,首先就是游戏的欢迎界面,其次就是地图的创建,和蛇身的创建,然后就是对蛇的控制和移动,还有对吃到食物和没有吃到食物的判断,还有撞墙没有,最后对于得分的控制。
在写代码之前我们要先来了解几个win32API函数

什么是WIN32API

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

GetStdHandle函数

GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。总的来说就是这个函数要获取控制台的权限,然后要配合其他函数调用使用我们只有获得了操控控制台的权限,我们才能设置控制台的大小,还有光标坐标的改变等。
他的原型是这样的

HANDLE GetStdHandle(DWORD nStdHandle);

这是他的参数
在这里插入图片描述
实例

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

这里我们创建了一个名为HANDLE的结构体指针变量,用来存放GetStdHandle这个函数的返回值

GetConsoleCursorInfo 函数

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息,说的通俗一点就是用来检查控制台里面的光标信息,在这里插入图片描述
这个就是光标
在这里插入图片描述
有了这两个函数的了解我们就可以写一个可以写一个获取控制台光标的信息的函数了

HANDLE hOutput = NULL;
//先创建一个HANDLE类型的结构体指针变量,因为GetStdHandle的返回类型是HANDLE
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取控制台的权限

CONSOLE_CURSOR_INFO CursorInfo;//创建一个CONSOLE_CURSOR_INFO的结构体指针

GetConsoleCursorInfo(hOutput, &CursorInfo);

//调用GetConsoleCursorInfo函数来获取控制台光标大小,和可见信息

CONSOLE_CURSOR_INFO函数

这个结构体,包含有关控制台光标的信息

typedef struct _CONSOLE_CURSOR_INFO {
  DWORD dwSize;
  BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

在这里插入图片描述
我们可以用这行代码隐藏光标信息,为什么要隐藏光标那是由于在运行程序的时候那个光标会在那里一直跳动,不美观

CursorInfo.bVisible = false; //隐藏控制台光标

SetConsoleCursorPosition 函数

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置,也就是说这个函数我们可以设置我们的光标在控制台显示的任意位置。
他的语法是这样的

BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

实例

# include<stdio.h>
# include<Windows.h>
int main()
{
	COORD pos = { 20, 34 };
	HANDLE hOutput = NULL;

	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

	SetConsoleCursorPosition(hOutput, pos);
	printf("112233");
}

在这里插入图片描述
有了这些了解,我们可以单独封装一个函数用来设置光标位置,为以后的贪吃蛇做准备。
我们还需要了解一个函数,我们想一想我们要在键盘上控制贪吃蛇的移动和加速减速,还有控制游戏的相关信息,这时候就需要用一个这个函数

getAsyncKeyState 函数

这个函数是可以获取按键情况,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果要检测一个按键是否被按过,可以检测这个函数的返回值的最低为是不是为1可以用按位与来检测,可以写一个宏函数来判断,因为简单不用在去封装一个函数来占用较大的系统空间

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

这个函数的返回类型与1进行按位与如果是按过了,函数的返回的最低为就是1,与1进行&操作就返回的是1,1为真这个宏函数就返回1,反之为假。(VK)就是按键的虚拟值这个链接就是一些键盘的16进制的虚拟值

本地化设置

为什么要进行本地化设置,因为不同的国家之间有差异
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
在标准中,依赖地区的部分有以下⼏项:
• 数字量的格式
• 货币量的格式
• 字符集
• ⽇期和时间的表⽰形式
这里要用到setlocale函数,他的原型是这样的
在这里插入图片描述
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和" “(本地模式)。在任意程序执⾏开始,都会隐藏式执⾏调⽤:当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。当程序运⾏起来后想改变地区,就只能显⽰调setlocale函数。⽤” "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。这里我们设置本地化的、只是为了打印宽字符汉字

有了以上的API函数的了解下面我们正式进入贪吃蛇游戏的讲解

游戏开始前准备工作

游戏开始菜单

我们先从最简单的游戏开始菜单讲起,这里就要用到我们的API函数用来定位光标的位置和控制台窗口的大小设置

//设置从哪开始打印
void set_pos(short x, short y)
{
	HANDLE handoutput = NULL;
	handoutput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	SetConsoleCursorPosition(handoutput, pos);
}
void welcome_to_game()
{
	set_pos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	set_pos(42, 20);
	system("pause");
	system("cls");
	set_pos(25, 14);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
	set_pos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");
	set_pos(42, 20);
	system("pause");
	system("cls");
}

wprintf就是用来打印宽字符的格式是

wprintf(L"%l//要打印的内容");

这里我们首先封装了一个设置光标位置的函数,用来在控制台的任意位置来打印所需要的信息,这个welcome_to_game函数将会被gamestart函数给调用
下面这个函数就是我们游戏开始时的准备工作下面会一一讲解

void startgame(snakeinfo* 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);
	welcome_to_game();
	init_wall();
	init_snake(ps);
	create_food(ps);
}

这里我们这里要关注这个代码

system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

这个就是设置我们控制台的大小的函数,这个函数在windows.h里面这里我们可以设置我们的控制台的长和宽也就是x和y坐标,但这里的设置都是有讲究的我们要设置宽字符的打印,宽字符占两个字符,那么就需要吧X和Y坐标都设置偶数,然后把控制台的名字改成贪吃蛇在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里就是我们设置好展示到屏幕上的内容。

地图的创建

接下来就创建地图了,我们的地图需要创建成这个样子
在这里插入图片描述
那该怎么创建呢,下面是代码

void init_wall()
{
	//打印上边框
	set_pos(0, 0);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	set_pos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 0; i <= 25; i++)
	{
		set_pos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 0; i <= 25; i++)
	{
		set_pos(56, i);
		wprintf(L"%lc", WALL);
	}
}

这里的WALL我们用了一个宏定义来代替方块

#define WALL L'□'

这里我们行和列的创建也是有讲究的我们的x轴必须为偶数y轴可以为奇数,也可以为偶数,这是因为我们打印的是宽字符x要占两个坐标y只占一个坐标
z在这里插入图片描述
可以看看这张图。这样墙体的创建就完成了。

蛇身的创建

蛇身的创建我们可以用单链表来实现,如果不知道单链表可以看看这篇博客单链表详解
可以看看这张图
在这里插入图片描述
这是代码

void init_snake(snakeinfo* ps)
{//头插入

	snakenode* pcur = NULL;
	//五个节点
	for (int i = 0; i < 5; i++)
	{
		pcur = (snakenode*)malloc(sizeof(snakenode));
		if (pcur == NULL)
		{
			perror("init_snake();malloc");
			exit(1);
		}
		pcur->nodenext = NULL;
		pcur->x = SNAKEBODY_X + 2 * i;//x必须是2的倍数
		pcur->y = SNAKEBODY_Y;//这里的x,和y我们宏定义成24,5

		//空链表
		if (ps->psnake == NULL)
		{
			ps->psnake = pcur;
		}
		//非空链表
		else
		{
			pcur->nodenext = ps->psnake;
			ps->psnake = pcur;
		}
	}
	pcur = ps->psnake;
	while (pcur != NULL)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);//BODY宏定义成#define BODY L'●'
		pcur = pcur->nodenext;
	}
	ps->food_socre = 10;//设置游戏食物初始分数
	ps->socre = 0;//设置当前得分
	ps->state = OK;//设置当前游戏状态
	ps->snake_dir = RIGHT;//设置蛇的运行方向
	ps->sleeptime = 200;//设置游戏的运行速度
}

这样我们就创建出蛇身了,他的原理就是通过创建一个链表存放,X和Y在控制台上的坐标,通过遍历链表设置坐标然后在打印出来
在这里插入图片描述
有了坐标我们就需要创建食物了

创建食物

下面是代码

void create_food(snakeinfo* ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % (55 - 2 + 1) + 2;
		y = rand() % (25 - 1 + 1) + 1;
	} while (x % 2 != 0);
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		if (x == pcur->x && y == pcur->y)
		{
			goto again;
		}
		pcur = pcur->nodenext;
	}
	snakenode* food = (snakenode*)malloc(sizeof(snakenode));
	if (food == NULL)
	{
		perror("food();malloc");
		exit(1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->nodenext = NULL;
	set_pos(x, y);
	wprintf(L"%lc", FOOD);
	ps->pfood = food;
}

这里我们创建食物也用了链表来创建,首先我们要在地图上随机生成食物,他只能在我们的地图范围内,还有不能生成在我们蛇的身上所以我们用了一个do,while和goto,agian语句来防止食物随机生成在蛇的身上,然后在把食物的信息存放在snakeinfo结构体里面。
在这里插入图片描述

这里我们的游戏开始准备工作就做完了。下面就要让游戏跑起来了

运行游戏

下面是游戏运行的整体代码,会一一讲解

void game_run(snakeinfo* ps)
{
	PrintHelpInfo();

	do
	{
		set_pos(64, 10);
		printf("总分数:%d\n", ps->socre);
		set_pos(64, 11);
		printf("当前食物的分数:%2d\n", ps->food_socre);
		if (KEY_PRESS(VK_UP) && ps->snake_dir != DOWM)
		{
			ps->snake_dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->snake_dir != UP)
		{
			ps->snake_dir = DOWM;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->snake_dir != RIGHT)
		{
			ps->snake_dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->snake_dir != LEFT)
		{
			ps->snake_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->state = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->sleeptime > 80)
			{
				ps->sleeptime = ps->sleeptime - 30;
				ps->food_socre = ps->food_socre + 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->food_socre > 2)
			{
				ps->sleeptime = ps->sleeptime + 30;
				ps->food_socre = ps->food_socre - 2;
			}
		}
		snake_move(ps);
		Sleep(ps->sleeptime);
	} while (ps->state == OK);
}

打印帮助信息

先来看看这个函数 PrintHelpInfo();

void PrintHelpInfo()
{
	set_pos(60, 10);
	wprintf(L"%ls", L"不能穿墙,不能吃自己");
	set_pos(60, 15);
	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动方向");
	set_pos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	set_pos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");

}

这里就是各种提示,他会显示在游戏运行时的右边
在这里插入图片描述

游戏运行时按键检测

这里就是检查我们游戏运行的按键情况,如果当前按下的键是向上键,并且当前的蛇的运行状态不是向下的,那么就可以改变蛇的走向,如果方向是向下的,按上键这个是不合法的,左右键也同理,当前是左不能往右,是右不能往左

	if (KEY_PRESS(VK_UP) && ps->snake_dir != DOWM)
		{
			ps->snake_dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->snake_dir != UP)
		{
			ps->snake_dir = DOWM;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->snake_dir != RIGHT)
		{
			ps->snake_dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->snake_dir != LEFT)
		{
			ps->snake_dir = RIGHT;
		}
		
		

暂停游戏

当我们按下空格键时游戏的状态变成暂停,这里我们用了一个死循环来控制暂停,如果我们要继续进行游戏就在按一下空格键就行了,就打破死循环

void Pause()
{
	while (1)
	{
		Sleep(500);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

加速减速判断

这里我们设置的是按F3和F4来控制游戏的加速减速,我们通过sleep函数的休眠时间来控制蛇的快慢,我们初始的时候sleep设置的是200毫秒,这里设定的最快不能超过80毫秒每按一次减去30毫秒分数加2,
减速也同样的道理,只是用分数来判断,最低不能低于2分每次加30毫秒。

else if (KEY_PRESS(VK_F3))
{
	//加速
	if (ps->sleeptime > 80)
	{
		ps->sleeptime = ps->sleeptime - 30;
		ps->food_socre = ps->food_socre + 2;
	}
}
else if (KEY_PRESS(VK_F4))
{
	//减速
	if (ps->food_socre > 2)
	{
		ps->sleeptime = ps->sleeptime + 30;
		ps->food_socre = ps->food_socre - 2;
	}
}

蛇的移动

蛇的移动都在这个函数里面

snake_move(ps);

下面是代码

void snake_move(snakeinfo* ps)
{
	//设置蛇即将的走向
	snakenode* nextnode = (snakenode*)malloc(sizeof(snakenode));
	if (nextnode == NULL)
	{
		perror("snake_move();;malloc");
		exit(1);
	}

	switch (ps->snake_dir)
	{
	case UP:
		nextnode->x = ps->psnake->x;
		nextnode->y = ps->psnake->y - 1;
		break;
	case DOWM:
		nextnode->x = ps->psnake->x;
		nextnode->y = ps->psnake->y + 1;
		break;
	case LEFT:
		nextnode->x = ps->psnake->x - 2;
		nextnode->y = ps->psnake->y;
		break;
	case RIGHT:
		nextnode->x = ps->psnake->x + 2;
		nextnode->y = ps->psnake->y;
		break;
	}

	if (next_is_food(nextnode, ps))
	{
		eat_food(nextnode, ps);
	}
	else
	{
		no_food(nextnode, ps);
	}
	kill_by_wall(ps);
	kill_by_self(ps);
}

这里我们通过创建新的节点来设置蛇的走向然后用一个switch语句来判断当前蛇的方向每走一次判断下一个节点是食物还不是食物,或者撞墙,撞身体死了,X为什么每次要走2歌因为是宽字符。

下一个节点是食物

这里要结合两个函数来看,一个是吃食物,另一个是判断下一个节点是不是食物
判断下一个节点是不是食物

int next_is_food(snakenode* pn, snakeinfo* ps)
{
	if (ps->pfood->x == pn->x && ps->pfood->y == pn->y)
		return 1;
	else
		return 0;
}

这里就是通过蛇头来判断当前节点是不是食物,如果是就返回1,不是就返回0,通过查重坐标来判断。
吃食物

void eat_food(snakenode* pn, snakeinfo* ps)
{
	ps->pfood->nodenext = ps->psnake;
	ps->psnake = ps->pfood;
	free(pn);
	pn = NULL;
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->nodenext;
	}
	ps->socre = ps->socre + ps->food_socre;
	create_food(ps);
}

这里就是把食物节点变成蛇头节点,把食物拿来头插入,让食物节点变成蛇头节点,然后在把pn申请的空间给释放了,这里我们开头是申请了空间的来成为蛇的下一个节点,因为这里有食物,食物已经创建了一个节点,所以那个节点我们不需要了就要释放,然后就是遍历链表循环打印蛇身出现在控制台上,最后就是加分了,然后在创建一次食物。

下一个节点不是食物

void no_food(snakenode* pn, snakeinfo* ps)
{//采用头插法
	pn->nodenext = ps->psnake;
	ps->psnake = pn;
	snakenode* pcur = ps->psnake;
	while (pcur->nodenext->nodenext != NULL)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->nodenext;
	}
	set_pos(pcur->nodenext->x, pcur->nodenext->y);
	printf("  ");
	free(pcur->nodenext);
	pcur->nodenext = NULL;
}

这里我们就要把我们开头申请的节点用上了,也是采用头插法,让新节点变成头,然后在循环打印出来为什么while里面要这样写pcur->nodenext->nodenext != NULL可以看看这张图
在这里插入图片描述
我们往前面走了最后一个空间肯定要消除掉所以要打印空字符如果不打印上一次蛇尾就会留下印子,像这样,我们需要打印空格完在释放空间节点
在这里插入图片描述

撞墙

void kill_by_wall(snakeinfo* ps)
{
	if (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26)
	{
		ps->state = KILL_BY_WALL;
	}
}

这里就很简单,就只需要判断蛇头有没有和墙的任何一个坐标重合

自己杀死自己

void kill_by_self(snakeinfo* ps)
{
	snakenode* pcur = ps->psnake->nodenext;
	while (pcur != NULL)
	{
		if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y)
		{
			ps->state = KILL_BY_SEIF;
			break;
		}
		pcur = pcur->nodenext;
	}
}

这里相当于我们创建了一个pcur来遍历除开蛇头的剩下的蛇身节点,pcur就像一个探针,一直在蛇身上走,如果头节点与这个pcur相撞,那么就判定自己咬到自己了,然后就把游戏状态改为自己杀死自己,退出游戏。

# 结束游戏

这里就是结束游戏,根据返回的状态来判断游戏是以什么样的情况结束,最后就是释放节点,创建一个临时节点。

void gameend(snakeinfo* ps)
{
	set_pos(26, 12);
	switch (ps->state)
	{
	case NORMAL:
		printf("正常退出游戏\n");
		break;
	case KILL_BY_SEIF:
		printf("碰到自己身体了\n");
		break;
	case KILL_BY_WALL :
		printf("碰到墙体了\n");
		break;
	}
	snakenode* temp = NULL;
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		temp = pcur;
		pcur = pcur->nodenext;
		free(temp);
	}
	set_pos(21, 25);

}

主函数

这里就游戏运行的主要逻辑。

void test()
{
	int ch = 0;
	do
	{
		fflush(stdin);
		snakeinfo snake = { 0 };
		startgame(&snake);
		game_run(&snake);
		gameend(&snake);
		set_pos(10, 10);
		wprintf(L"再来一局:1/2?注意:只能按1次1或者2\n");
		set_pos(15, 16);
		scanf("%d", &ch);
		system("cls");
	} while(ch == 1);
	set_pos(0, 27);
}
int main()
{
	srand((unsigned int)time(NULL));//rand函数要用

	setlocale(LC_ALL, "");
	test();
	return 0;
}

源代码

#define _CRT_SECURE_NO_WARNINGS 1
# include<stdio.h>
# include<locale.h>
# include<assert.h>
# include<stdlib.h>
# include<time.h>
# include<windows.h>
# include<stdbool.h>
# define  SNAKEBODY_X 24
# define  SNAKEBODY_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
enum DRECTION
{
	UP = 1,
	DOWM,
	RIGHT,
	LEFT

};
enum GAEM_STATE
{
	NORMAL = 1,
	KILL_BY_SEIF,
	KILL_BY_WALL,
	END_NORMAL,
	OK
};
struct snakenode
{
	int x;
	int y;
	struct snakenode* nodenext;
};
typedef struct snakenode snakenode;
//贪吃蛇
struct snake_information
{
	snakenode* psnake;
	snakenode* pfood;
	int sleeptime;
	int socre;
	int food_socre;
	enum GAEM_STATE state;
	enum DRECTION snake_dir;
};
typedef struct snake_information snakeinfo;
void set_pos(short x, short y);//
void welcome_to_game();//
void startgame(snakeinfo* ps);//
void init_snake(snakeinfo* ps);//
void init_wall();//
void create_food(snakeinfo* ps);//
void eat_food(snakenode* pn, snakeinfo* ps);//
void snake_move(snakeinfo* ps);//
void no_food(snakenode* pn, snakeinfo* ps);//
int next_is_food(snakenode* pn, snakeinfo* ps);//
void kill_by_wall(snakeinfo* ps);//
void kill_by_self(snakeinfo* ps);//
void game_run(snakeinfo* ps);//
void gameend(snakeinfo* ps);
#define _CRT_SECURE_NO_WARNINGS 1
#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

//设置从哪开始打印
void set_pos(short x, short y)
{
	HANDLE handoutput = NULL;
	handoutput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	SetConsoleCursorPosition(handoutput, pos);
}

void init_wall()
{
	//打印上边框
	set_pos(0, 0);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	set_pos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	for (int i = 0; i <= 25; i++)
	{
		set_pos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 0; i <= 25; i++)
	{
		set_pos(56, i);
		wprintf(L"%lc", WALL);
	}
}
void welcome_to_game()
{
	set_pos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	set_pos(42, 20);
	system("pause");
	system("cls");
	set_pos(25, 14);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
	set_pos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");
	set_pos(42, 20);
	system("pause");
	system("cls");
}
void init_snake(snakeinfo* ps)
{//头插入

	snakenode* pcur = NULL;
	//五个节点
	for (int i = 0; i < 5; i++)
	{
		pcur = (snakenode*)malloc(sizeof(snakenode));
		if (pcur == NULL)
		{
			perror("init_snake();malloc");
			exit(1);
		}
		pcur->nodenext = NULL;
		pcur->x = SNAKEBODY_X + 2 * i;
		pcur->y = SNAKEBODY_Y;

		//空链表
		if (ps->psnake == NULL)
		{
			ps->psnake = pcur;
		}
		//非空链表
		else
		{
			pcur->nodenext = ps->psnake;
			ps->psnake = pcur;
		}
	}
	pcur = ps->psnake;
	while (pcur != NULL)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->nodenext;
	}
	ps->food_socre = 10;
	ps->socre = 0;
	ps->state = OK;
	ps->snake_dir = RIGHT;
	ps->sleeptime = 200;
}
void create_food(snakeinfo* ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % (55 - 2 + 1) + 2;
		y = rand() % (25 - 1 + 1) + 1;
	} while (x % 2 != 0);
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		if (x == pcur->x && y == pcur->y)
		{
			goto again;
		}
		pcur = pcur->nodenext;
	}
	snakenode* pfood = (snakenode*)malloc(sizeof(snakenode));
	if (pfood == NULL)
	{
		perror("food();malloc");
		exit(1);
	}
	pfood->x = x;
	pfood->y = y;
	pfood->nodenext = NULL;
	set_pos(x, y);
	wprintf(L"%lc", FOOD);
	ps->pfood = pfood;
}

void startgame(snakeinfo* 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);
	welcome_to_game();
	init_wall();
	init_snake(ps);
	create_food(ps);
}
void PrintHelpInfo()
{
	set_pos(60, 10);
	wprintf(L"%ls", L"不能穿墙,不能吃自己");
	set_pos(60, 15);
	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动方向");
	set_pos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	set_pos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");

}
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}
int next_is_food(snakenode* pn, snakeinfo* ps)
{
	if (ps->pfood->x == pn->x && ps->pfood->y == pn->y)
		return 1;
	else
		return 0;
}
void eat_food(snakenode* pn, snakeinfo* ps)
{
	ps->pfood->nodenext = ps->psnake;
	ps->psnake = ps->pfood;
	free(pn);
	pn = NULL;
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->nodenext;
	}
	ps->socre = ps->socre + ps->food_socre;
	create_food(ps);
}
void no_food(snakenode* pn, snakeinfo* ps)
{//采用头插法
	pn->nodenext = ps->psnake;
	ps->psnake = pn;
	snakenode* pcur = ps->psnake;
	while (pcur->nodenext->nodenext != NULL)
	{
		set_pos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->nodenext;
	}
	set_pos(pcur->nodenext->x, pcur->nodenext->y);
	//printf("  ");
	free(pcur->nodenext);
	pcur->nodenext = NULL;
}
void kill_by_wall(snakeinfo* ps)
{
	if (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26)
	{
		ps->state = KILL_BY_WALL;
	}
}
void kill_by_self(snakeinfo* ps)
{
	snakenode* pcur = ps->psnake->nodenext;
	while (pcur != NULL)
	{
		if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y)
		{
			ps->state = KILL_BY_SEIF;
			break;
		}
		pcur = pcur->nodenext;
	}
}
void snake_move(snakeinfo* ps)
{
	//设置蛇即将的走向
	snakenode* nextnode = (snakenode*)malloc(sizeof(snakenode));
	if (nextnode == NULL)
	{
		perror("snake_move();;malloc");
		exit(1);
	}

	switch (ps->snake_dir)
	{
	case UP:
		nextnode->x = ps->psnake->x;
		nextnode->y = ps->psnake->y - 1;
		break;
	case DOWM:
		nextnode->x = ps->psnake->x;
		nextnode->y = ps->psnake->y + 1;
		break;
	case LEFT:
		nextnode->x = ps->psnake->x - 2;
		nextnode->y = ps->psnake->y;
		break;
	case RIGHT:
		nextnode->x = ps->psnake->x + 2;
		nextnode->y = ps->psnake->y;
		break;
	}

	if (next_is_food(nextnode, ps))
	{
		eat_food(nextnode, ps);
	}
	else
	{
		no_food(nextnode, ps);
	}
	kill_by_wall(ps);
	kill_by_self(ps);
}
void game_run(snakeinfo* ps)
{
	PrintHelpInfo();

	do
	{
		set_pos(64, 10);
		printf("总分数:%d\n", ps->socre);
		set_pos(64, 11);
		printf("当前食物的分数:%2d\n", ps->food_socre);
		if (KEY_PRESS(VK_UP) && ps->snake_dir != DOWM)
		{
			ps->snake_dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->snake_dir != UP)
		{
			ps->snake_dir = DOWM;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->snake_dir != RIGHT)
		{
			ps->snake_dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->snake_dir != LEFT)
		{
			ps->snake_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->state = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->sleeptime > 80)
			{
				ps->sleeptime = ps->sleeptime - 30;
				ps->food_socre = ps->food_socre + 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->food_socre > 2)
			{
				ps->sleeptime = ps->sleeptime + 30;
				ps->food_socre = ps->food_socre - 2;
			}
		}
		snake_move(ps);
		Sleep(ps->sleeptime);
	} while (ps->state == OK);
}
void gameend(snakeinfo* ps)
{
	set_pos(26, 12);
	switch (ps->state)
	{
	case NORMAL:
		printf("正常退出游戏\n");
		break;
	case KILL_BY_SEIF:
		printf("碰到自己身体了\n");
		break;
	case KILL_BY_WALL :
		printf("碰到墙体了\n");
		break;
	}
	snakenode* temp = NULL;
	snakenode* pcur = ps->psnake;
	while (pcur)
	{
		temp = pcur;
		pcur = pcur->nodenext;
		free(temp);
	}
	set_pos(21, 25);

}

void test()
{
	int ch = 0;
	do
	{
		fflush(stdin);
		snakeinfo snake = { 0 };
		startgame(&snake);
		game_run(&snake);
		gameend(&snake);
		set_pos(10, 10);
		wprintf(L"再来一局:1/2?注意:只能按1次1或者2\n");
		set_pos(15, 16);
		scanf("%d", &ch);
		system("cls");
	} while(ch == 1);
	set_pos(0, 27);
}
int main()
{
	srand((unsigned int)time(NULL));

	setlocale(LC_ALL, "");
	test();
	return 0;
}

  • 15
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值