【C语言】贪吃蛇 详解

该项目需要的技术要点

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

由于篇幅限制 和 使知识模块化,

若想了解 使用到的 Win32API 的知识:请点击跳转:【Win32API】贪吃蛇会使用到的 Win32API

目录

1. 贪吃蛇游戏设计与分析

1.0 贪吃蛇页面大纲

1.1 地图

1.1.1 控制台窗口的坐标知识

1.1.2 宽字符:

1.1.3 地图坐标

1.2 蛇身和食物

1.3 数据结构设计

1.4 整个游戏流程设计

2. 核⼼逻辑实现分析

2.1 游戏主逻辑 

2.2 游戏开始

2.3 游戏运行

2.4 游戏结束

3.总代码概览

Snake.c

Snake.h

 test.c



准备工作:创建三个文件

1. 贪吃蛇游戏设计与分析

1.0 贪吃蛇页面大纲

 

 我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?

1.1 地图

这里不得不讲一下控制台窗口的一些知识

1.1.1 控制台窗口的坐标知识

控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。

1.1.2 宽字符:

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符 ● ,打印食物使用宽字符★或图中的那个
普通的字符是占一个字节的,这类宽字符是占用 2 个字节。

宽字符的来源
过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入和 <locale.h> 头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
(1) <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

(2) setlocale函数

因为各个地区的代码语言环境不同:需要转换到 本地环境, 才能支持宽字符(如汉字)的输出

用 "" 作为第 2 个参数,(注意:就是一对双引号,中间没有空格) ,调用 setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
#include<locale.h> // 包含头文件
setlocale(LC_ALL, "" );//切换到本地环境:中文环境

(3)宽字符 的 打印格式
宽字符要 在单引号前面加上 L,表示宽字符,宽字符的打印用wprintf,对应wprintf()的占位符为%lc;

假设 A 为一个窄字符,B 为 宽字符

窄字符:printf("%c", A)
 
宽字符:wprintf(L"%lc", B);
从输出的结果来看,我们发现一个普通字符占一个字符的位置
但是打印一个宽字符,占用 2 个字符的位置,那么我们如果
要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

下图,边框上的墙体就是 一种宽字符:□ ,宽度占 2 个字符位

1个坐标可以放1个正常字符,即窄字符

2个坐标可以存放1个宽字符 

1.1.3 地图坐标

我们假设实现一个棋盘 27 行, 58 列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,因为 宽字符的原因:最好 是    2 * 行数  = 列数
如下:
void CreateMap()
{
	// 上:29个宽格子
	SetPos(0, 0);// 从 (0, 0)开始
	for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
	{
		wprintf(L"%c", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	// 打印 左右  坐标需要跟着不断变化 :因为 默认打印是 横着打印的
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

1.2 蛇身和食物

初始化状态,假设蛇的长度是 5 ,蛇身的每个节点是●,在固定的一个坐标处,比如 (24,5) 处开始出现蛇,连续 5 个节点。 注意: 蛇的每个节点的 x 坐标必须是 2 的倍数, 即偶数,否则撞墙时可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是 2 的倍数)坐标不能和蛇的身体重合,然后打印★。

void InitSnake(pSnake ps)
{
	// 创建5个节点: 
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake:malloc");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		// 头插法:
		// 没有节点下
		if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
		{
			ps->pSnake = cur;
		}
		// 已经有节点  :原本不是有  ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
		else
		{
			cur->next = ps->pSnake;
			ps->pSnake = cur;
		}
	}
	//打印蛇的身体
	// 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
	// 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
	cur = ps->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	// 初始化贪吃蛇的其他信息
	ps->dir = RIGHT;  // 初始默认向右
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200; // 休眠 200 ms
	ps->state = OK;
}

1.3 数据结构设计

在游戏运行的过程中,蛇每次吃一个⻝物,蛇的身体就会变⻓一节,如果我们使用 链表 存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,
所以蛇节点结构如下:
// 蛇身节点
// 蛇身用链表来维护:一个蛇身节点就是一个链表节点
// 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
// 创建一个指向蛇身节点的结构体指针

要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇:

//贪吃蛇
// 整条蛇
// 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头
	pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样
	enum DIRECTION dir;//蛇头的方向默认是向右
	enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型)
	int Score;//当前获得分数
	int FoodWeight;//默认每个食物 10 分
	int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度)
}Snake, * pSnake;  // pSnake 是 指向贪吃蛇的指针: 下面就用上了

蛇的方向:可以一一列举,使用枚举


//蛇走的方向
enum DIRECTION
{
	UP = 1, // 上
	DOWN, // 下
	LEFT, // 左
	RIGHT // 右
};

游戏状态:可以一一列举,使用枚举

/蛇的状态:  维护游戏状态的 枚举类型
enum GAME_STATE
{
	OK,//正常运行
	ESC, // 按 ESC 键退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到自己
	//END_NOMAL//正常结束
};

1.4 整个游戏流程设计

2. 核⼼逻辑实现分析

2.1 游戏主逻辑 

GameStart(&snake);   游戏开始
GameRun(&snake);    游戏过程
GameEnd(&snake);    游戏结束

#include"snake.h"

void test()
{
	int ch = 0;
	do
	{
		// 创建一条贪吃蛇
		Snake snake = { 0 };
		// 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
		GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
		GameRun(&snake); // 游戏过程
		GameEnd(&snake);//善后工作
		SetPos(20, 15);
		printf("再来一局?(y/n)");
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');

}
int main()
{
	//适配本地中文环境
	setlocale(LC_ALL, "");

	//贪吃蛇游戏的设置
	test();
	return 0;
}

2.2 游戏开始

    一、打印欢迎信息
    WelcomeToGame();
    二、绘制地图
    CreateMap(); 
    三、初始化蛇
    InitSnake(ps); 
    四、创建食物
    CreateFood(ps);

// 游戏前准备
void GameStart(pSnake ps)
{
	// 一、设置控制台的信息:窗口大小、窗口名
	// 设置控制台窗口的长宽:设置控制台窗口的大小
	// 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆  列为 100, 行 30 
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");// 设置cmd窗口名称 

	// 二、隐藏光标:不想光标在一直闪
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏光标操作: 6.5.1节 那里有示例
	CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
	GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
	//先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
	cursorinfo.bVisible = false;  //隐藏控制台光标
	SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
	
	//一、打印欢迎信息
	WelcomeToGame();
	//二、绘制地图
	CreateMap(); 
	//三、初始化蛇
	InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
	//四、创建食物
	CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
}

游戏前准备  GameStart(pSnake ps)   的 四个子函数

先封装 设置坐标函数

//封装一个设置光标位置的函数 SetPos:
void SetPos(int x, int y)
{
	//获取标准输出设备的句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}

 一、打印欢迎信息


void WelcomeToGame()
{
	//一、 欢迎页面
	// 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30 
	SetPos(38, 13);  // 坐标的设置 看感觉来吧 
	printf("欢迎来到贪吃蛇小游戏\n");
	// 打印完上面这句后:会有一句:请按任意键继续. . .     位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标
	SetPos(38, 16);  // 请按任意键继续. . .     的位置

	system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . .    然后打印另一张
	// 清空屏幕信息:要打印下一张页面
	system("cls");

	//二、功能介绍页面
	SetPos(29, 8);
	printf("1、用  ↑   ↓   ←    →  来控制蛇的移动  \n");
	SetPos(29, 10);
	printf("2、F3 是 加速,F4 是 减速     O(∩_∩)O\n");
	SetPos(29, 12);
	printf("3、加速可以获得更高的分数    (づ ̄ 3 ̄)づ\n");
	SetPos(38, 16);  // 请按任意键继续. . .     的位置
	system("pause"); //一张页面打印完成后 暂停一下
	// 清空屏幕信息:要打印下一张页面
	system("cls");
}

 

 二、绘制地图


void CreateMap()
{
	// 上:29个宽格子
	SetPos(0, 0);// 从 (0, 0)开始
	for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
	{
		wprintf(L"%c", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	// 打印 左右  坐标需要跟着不断变化 :因为 默认打印是 横着打印的
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

三、初始化 蛇

// 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要
// 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
// 创建5个节点,然后将每个节点存放在链表中进⾏管理。
void InitSnake(pSnake ps)
{
	// 创建5个节点: 
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake:malloc");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		// 头插法:
		// 没有节点下
		if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
		{
			ps->pSnake = cur;
		}
		// 已经有节点  :原本不是有  ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
		else
		{
			cur->next = ps->pSnake;
			ps->pSnake = cur;
		}
	}
	//打印蛇的身体
	// 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
	// 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
	cur = ps->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	// 初始化贪吃蛇的其他信息
	ps->dir = RIGHT;  // 初始默认向右
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200; // 休眠 200 ms
	ps->state = OK;
}

 四、创建食物


// 思路:
// 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模
// 2、x 的范围:2 ~ 54    ---> 生成 0 ~ 52   + 2    ---> rand()%53  + 2    因为 x 是占 2 个窄字符的, x  的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖
// 3、y 的范围:1 ~ 25    ---> 生成 0 ~ 24   + 2    ---> rand()%25 + 1
// 创建食物注意事项
// 1.食物是随机出现的,坐标就是随机的
// 2.坐标必须在墙内
// 3.坐标不能在蛇的身体上
void CreateFood(pSnake ps)
{
	// 1.食物是随机出现的,坐标就是随机的
	// 2.坐标必须在墙内
	int x = 0;
	int y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);  // 这一步啥意思呀:42min 左右

	// 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比
	//判断食物是否在蛇身上
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物: 一个新节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//  (pSnakeNode) 和 (SnakeNode)  千万别混淆!!!
	if (pFood == NULL)
	{
		perror("CreateFood():malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点
	// 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动

	//打印食物
	SetPos(x, y);
	wprintf(L"%c", FOOD);
	//getchar(); // 注意 :这个别忘了关闭
}

2.3 游戏运行

游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

确定了蛇的方向和速度,蛇就可以移动了。

 

// 游戏运行过程
void GameRun(pSnake ps)
{
	// 一、打印界面右侧的帮助提示信息
	PrintHelpInfo();
	// 游戏一般 是 do while循环:总之都需要先让游戏先运行一次

	do
	{
		//当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
		SetPos(65, 4);
		//[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
		// SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),  FOREGROUND_RED | BACKGROUND_INTENSITY);
		CONSOLE_FONT_INFOEX cfi;
		cfi.cbSize = sizeof cfi;
		cfi.dwFontSize.X = 0; //字宽
		cfi.dwFontSize.Y = 20;//字高
		cfi.FontWeight = FW_NORMAL;//粗细
		SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);

		printf("总分:%5d", ps->Score);
		SetPos(65, 6 );
		printf("食物的分值:%02d", ps->FoodWeight);

		// 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
		// 上下左右,ESC,空格,F3,F4
		// 不能同时 方向 等于 相反方向
		if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于  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_ESCAPE))
		{
			ps->state = ESC; // 将状态该成 退出状态
			break;
		}
		else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
		{
			//游戏要暂定
			// 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
			Pause();//暂定和回复暂定
		}
		else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
		{
			if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
		{
			if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		// 三 四 步 别写反
		// 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
		// 蛇身移动较为复杂,封装成一个函数便于分析
		SnakeMove(ps);
		// 四、睡眠一下
		Sleep(ps->SleepTime);

	} while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束

}

游戏运行过程函数:GameRun(pSnake ps) 的 几个子函数

一、PrintHelpInfo():打印界面右侧的帮助提示信息

二、Pause():游戏暂停键

三、SnakeMove(ps):蛇身移动(较为复杂)

一、PrintHelpInfo():打印界面右侧的帮助提示信息

void PrintHelpInfo()
{
	// 设置位置
	SetPos(65, 8);
	printf("温馨提示\n");
	SetPos(65, 10);
	printf("1、不能穿墙\n");
	SetPos(65, 11);
	printf("2、不能咬到自己\n");
	SetPos(65, 12);
	printf("3、按   ESC   退出游戏\n");
	SetPos(65, 13);
	printf("4、按   空格  暂停游戏\n");
	SetPos(65, 15);
	printf("操作回顾\n");
	SetPos(65, 17);
	printf("5、用 ↑↓ ← → 来控制蛇的移动  \n");
	SetPos(65, 18);
	printf("6、F3 加速,F4 减速\n");
	SetPos(65, 19);
	printf("7、加速可以获得更高的分数\n");
	SetPos(65, 22);
	printf("版权 @时差\n");
	// getchar(); // 注意 :这个别忘了关闭
}

二、Pause():游戏暂停键

void Pause()//游戏暂停
{
	// tip :  死循环的 sleep  , 再循环中 判断 再次点击空格键 即可回复
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

三、SnakeMove(ps):蛇身移动(较为复杂)

分成几个子函数:

  1. NextIsFood(ps, pNext):判断蛇头 下一步处 是否是 食物
  2. EatFood(ps, pNext); 是食物就吃掉
  3. NotEatFood(ps, pNext); 不是食物就正常走一步
  4. KillByWall(ps);// 检测撞墙
  5. KillBySelf(ps);// 是否撞到自己

// 蛇身 移动 
// 移动的本质: 没吃食物,则长度不变,将  方向节点 链接到 蛇头  上,free 掉 尾节点
void SnakeMove(pSnake ps)
{
	// 根据按键:调整移动方向
	// 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min 
	// 取名:方向节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc:");
		return;
	}
	pNext->next = NULL;// 指向空就好

	// 利用 方向节点,
	// pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头 
	// 通过 改变 方向节点  的坐标位置,引导蛇头的移动
	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}
	//下一个坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就吃掉:将  方向节点 链接到 蛇身上,将 食物指针 free 释放掉
		EatFood(ps, pNext);
	}
	else
	{
		//不是食物就正常一步: 没吃食物,则长度不变,将  方向节点 链接到 蛇头  上,free 掉 尾节点
		NotEatFood(ps, pNext);
	}
	// getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了

	//检测撞墙
	KillByWall(ps);
	//是否撞到自己
	KillBySelf(ps);
}

 1、NextIsFood(ps, pNext):判断蛇头 下一步处 是否是 食物

// 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	// 两者坐标重合 就是了
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物
	else return 0;
}

 2、EatFood(ps, pNext); 是食物就吃掉

//下一步处 是食物就吃掉:将  方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉
void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点
	ps->pSnake = pNext; // 更新头指针

	// 打印蛇身:每次更新一轮,就打印一次
	// PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本
	//打印蛇
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}

	// 分数变化:
	ps->Score += ps->FoodWeight;

	// 释放 旧食物节点:吃掉了 就不存在了
	free(ps->pFood);
	//创建新食物;
	CreateFood(ps);
}

3、 NotEatFood(ps, pNext); 不是食物就正常走一步

void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	// 头插法:链接方向指针,相当于移动了一步
	pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点
	ps->pSnake = pNext;// 更新头指针

	// 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法
	pSnakeNode cur = ps->pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	// 将尾节点的位置  打印成 空白字符:不能 还打印蛇身
	// 先 打印成 空白,后再释放:别急着释放
	SetPos(cur->next->x, cur->next->y);
	printf("  ");// 注意 空格是 两个

	free(cur->next);
	cur->next = NULL;//易错

	// 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点
	// 打印蛇身:每次更新一轮,就打印一次
	 // PrintSnake(ps);
}

4、 KillByWall(ps);// 检测撞墙

void KillByWall(pSnake ps)
{
	if (ps->pSnake->x == 0 ||
		ps->pSnake->x == 56 ||
		ps->pSnake->y == 0 ||
		ps->pSnake->y == 26)
	{
		ps->state = KILL_BY_WALL;
	}
}

5、KillBySelf(ps);// 是否撞到自己

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->pSnake->next;
	while (cur)
	{
		if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
		{
			ps->state = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

2.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->state)
	{
	case ESC:
		printf("主动退出游戏\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,撞到自己,游戏结束\n");
		break;
	}

	//释放
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}

	free(ps->pFood);
	ps = NULL;

}

3.总代码概览

Snake.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"snake.h"

//封装一个设置光标位置的函数 SetPos:
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}

// 打印欢迎信息
void WelcomeToGame()
{
	//一、 欢迎页面
	// 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30 
	SetPos(38, 13);  // 坐标的设置 看感觉来吧 
	printf("欢迎来到贪吃蛇小游戏\n");
	// 打印完上面这句后:会有一句:请按任意键继续. . .     位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标
	SetPos(38, 16);  // 请按任意键继续. . .     的位置

	system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . .    然后打印另一张
	// 清空屏幕信息:要打印下一张页面
	system("cls");

	//二、功能介绍页面
	SetPos(29, 8);
	printf("1、用  ↑   ↓   ←    →  来控制蛇的移动  \n");
	SetPos(29, 10);
	printf("2、F3 是 加速,F4 是 减速     O(∩_∩)O\n");
	SetPos(29, 12);
	printf("3、加速可以获得更高的分数    (づ ̄ 3 ̄)づ\n");
	SetPos(38, 16);  // 请按任意键继续. . .     的位置
	system("pause"); //一张页面打印完成后 暂停一下
	// 清空屏幕信息:要打印下一张页面
	system("cls");
}


// 绘制地图
void CreateMap()
{
	// 上:29个宽格子
	SetPos(0, 0);// 从 (0, 0)开始
	for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
	{
		wprintf(L"%c", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	// 打印 左右  坐标需要跟着不断变化 :因为 默认打印是 横着打印的
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

// 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要
// 初始化 蛇
// 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
// 创建5个节点,然后将每个节点存放在链表中进⾏管理。
void InitSnake(pSnake ps)
{
	// 创建5个节点: 
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake:malloc");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		// 头插法:
		// 没有节点下
		if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
		{
			ps->pSnake = cur;
		}
		// 已经有节点  :原本不是有  ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
		else
		{
			cur->next = ps->pSnake;
			ps->pSnake = cur;
		}
	}
	//打印蛇的身体
	// 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
	// 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
	cur = ps->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	// 初始化贪吃蛇的其他信息
	ps->dir = RIGHT;  // 初始默认向右
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200; // 休眠 200 ms
	ps->state = OK;
}

// 创建食物
// 思路:
// 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模
// 2、x 的范围:2 ~ 54    ---> 生成 0 ~ 52   + 2    ---> rand()%53  + 2    因为 x 是占 2 个窄字符的, x  的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖
// 3、y 的范围:1 ~ 25    ---> 生成 0 ~ 24   + 2    ---> rand()%25 + 1
// 创建食物注意事项
// 1.食物是随机出现的,坐标就是随机的
// 2.坐标必须在墙内
// 3.坐标不能在蛇的身体上
void CreateFood(pSnake ps)
{
	// 1.食物是随机出现的,坐标就是随机的
	// 2.坐标必须在墙内
	int x = 0;
	int y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);  // 这一步啥意思呀:42min 左右

	// 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比
	//判断食物是否在蛇身上
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物: 一个新节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//  (pSnakeNode) 和 (SnakeNode)  千万别混淆!!!
	if (pFood == NULL)
	{
		perror("CreateFood():malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点
	// 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动

	//打印食物
	SetPos(x, y);
	wprintf(L"%c", FOOD);
	//getchar(); // 注意 :这个别忘了关闭
}



// 游戏前准备
void GameStart(pSnake ps)
{
	// 一、设置控制台的信息:窗口大小、窗口名
	// 设置控制台窗口的长宽:设置控制台窗口的大小
	// 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆  列为 100, 行 30 
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");// 设置cmd窗口名称 

	// 二、隐藏光标:不想光标在一直闪
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏光标操作: 6.5.1节 那里有示例
	CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
	GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
	//先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
	cursorinfo.bVisible = false;  //隐藏控制台光标
	SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
	
	//打印欢迎信息
	WelcomeToGame();
	//绘制地图
	CreateMap(); 
	//初始化蛇
	InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
	//创建食物
	CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
}


void PrintHelpInfo()
{
	// 设置位置
	SetPos(65, 8);
	printf("温馨提示\n");
	SetPos(65, 10);
	printf("1、不能穿墙\n");
	SetPos(65, 11);
	printf("2、不能咬到自己\n");
	SetPos(65, 12);
	printf("3、按   ESC   退出游戏\n");
	SetPos(65, 13);
	printf("4、按   空格  暂停游戏\n");
	SetPos(65, 15);
	printf("操作回顾\n");
	SetPos(65, 17);
	printf("5、用 ↑↓ ← → 来控制蛇的移动  \n");
	SetPos(65, 18);
	printf("6、F3 加速,F4 减速\n");
	SetPos(65, 19);
	printf("7、加速可以获得更高的分数\n");
	SetPos(65, 22);
	printf("版权 @时差\n");
	// getchar(); // 注意 :这个别忘了关闭
}

// 按 空格  :暂定和回复暂定
void Pause()//游戏暂停
{
	// tip :  死循环的 sleep  , 再循环中 判断 再次点击空格键 即可回复
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

// 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	// 两者坐标重合 就是了
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物
	else return 0;
}

//下一步处 是食物就吃掉:将  方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉
void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点
	ps->pSnake = pNext; // 更新头指针

	// 打印蛇身:每次更新一轮,就打印一次
	// PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本
	//打印蛇
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}

	// 分数变化:
	ps->Score += ps->FoodWeight;

	// 释放 旧食物节点:吃掉了 就不存在了
	free(ps->pFood);
	//创建新食物;
	CreateFood(ps);
}

void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	// 头插法:链接方向指针,相当于移动了一步
	pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点
	ps->pSnake = pNext;// 更新头指针

	// 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法
	pSnakeNode cur = ps->pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	// 将尾节点的位置  打印成 空白字符:不能 还打印蛇身
	// 先 打印成 空白,后再释放:别急着释放
	SetPos(cur->next->x, cur->next->y);
	printf("  ");// 注意 空格是 两个

	free(cur->next);
	cur->next = NULL;//易错

	// 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点
	// 打印蛇身:每次更新一轮,就打印一次
	 // PrintSnake(ps);
}

void KillByWall(pSnake ps)
{
	if (ps->pSnake->x == 0 ||
		ps->pSnake->x == 56 ||
		ps->pSnake->y == 0 ||
		ps->pSnake->y == 26)
	{
		ps->state = KILL_BY_WALL;
	}
}

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->pSnake->next;
	while (cur)
	{
		if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
		{
			ps->state = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

// 蛇身 移动 
// 移动的本质: 没吃食物,则长度不变,将  方向节点 链接到 蛇头  上,free 掉 尾节点
void SnakeMove(pSnake ps)
{
	// 根据按键:调整移动方向
	// 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min 
	// 取名:方向节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc:");
		return;
	}
	pNext->next = NULL;// 指向空就好

	// 利用 方向节点,
	// pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头 
	// 通过 改变 方向节点  的坐标位置,引导蛇头的移动
	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}
	//下一个坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就吃掉:将  方向节点 链接到 蛇身上,将 食物指针 free 释放掉
		EatFood(ps, pNext);
	}
	else
	{
		//不是食物就正常一步: 没吃食物,则长度不变,将  方向节点 链接到 蛇头  上,free 掉 尾节点
		NotEatFood(ps, pNext);
	}
	// getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了

	//检测撞墙
	KillByWall(ps);
	//是否撞到自己
	KillBySelf(ps);
}

// 游戏运行过程
void GameRun(pSnake ps)
{
	// 一、打印界面右侧的帮助提示信息
	PrintHelpInfo();
	// 游戏一般 是 do while循环:总之都需要先让游戏先运行一次

	do
	{
		//当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
		SetPos(65, 4);
		//[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
		// SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),  FOREGROUND_RED | BACKGROUND_INTENSITY);
		CONSOLE_FONT_INFOEX cfi;
		cfi.cbSize = sizeof cfi;
		cfi.dwFontSize.X = 0; //字宽
		cfi.dwFontSize.Y = 20;//字高
		cfi.FontWeight = FW_NORMAL;//粗细
		SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);

		printf("总分:%5d", ps->Score);
		SetPos(65, 6 );
		printf("食物的分值:%02d", ps->FoodWeight);

		// 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
		// 上下左右,ESC,空格,F3,F4
		// 不能同时 方向 等于 相反方向
		if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于  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_ESCAPE))
		{
			ps->state = ESC; // 将状态该成 退出状态
			break;
		}
		else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
		{
			//游戏要暂定
			// 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
			Pause();//暂定和回复暂定
		}
		else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
		{
			if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
		{
			if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		// 三 四 步 别写反
		// 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
		// 蛇身移动较为复杂,封装成一个函数便于分析
		SnakeMove(ps);
		// 四、睡眠一下
		Sleep(ps->SleepTime);

	} while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束

}

void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->state)
	{
	case ESC:
		printf("主动退出游戏\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,撞到自己,游戏结束\n");
		break;
	}

	//释放
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}

	free(ps->pFood);
	ps = NULL;

}

Snake.h

#pragma once
#include<locale.h>
#include<stdlib.h>
#include<stdio.h>
#include<windows.h>
#include<stdbool.h>
#define WALL L'□' // 因为 每次打印都要写一次 L什么什么的  还不如直接定义一个宏
#define BODY L'●' // 打印是 蛇身
#define FOOD L'※' // 打印食物
//Đʘ◨ↇↀↈ:不行的
// D●
// 蛇 默认的起始坐标位置:要确定 起始的蛇身 每个 节点的 坐标

#define POS_X 24//蛇初始坐标
#define POS_Y 5

#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
// 枚举类型不熟的可以去翻笔记
// enum  就是 集合版的 define


//蛇走的方向
enum DIRECTION
{
	UP = 1, // 上
	DOWN, // 下
	LEFT, // 左
	RIGHT // 右
};

//蛇的状态:  维护游戏状态的 枚举类型
enum GAME_STATE
{
	OK,//正常运行
	ESC, // 按 ESC 键退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到自己
	//END_NOMAL//正常结束
};

// 蛇身节点
// 蛇身用链表来维护:一个蛇身节点就是一个链表节点
// 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
// 创建一个指向蛇身节点的结构体指针


//贪吃蛇
// 整条蛇
// 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头
	pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样
	enum DIRECTION dir;//蛇头的方向默认是向右
	enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型)
	int Score;//当前获得分数
	int FoodWeight;//默认每个食物 10 分
	int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度)
}Snake, * pSnake;  // pSnake 是 指向贪吃蛇的指针: 下面就用上了,其实就是 Snake*

//定位控制台光标位置
void SetPos(int x, int y);

//游戏开始前的准备
void GameStart(pSnake ps);

//欢迎界面
void WelcomeToGame();//打印欢迎信息

//绘制地图
void CreateMap();//创建地图

//初始化蛇
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);

//游戏运行的整个逻辑
void GameRun(pSnake ps);

//打印帮助信息
void PrintHelpInfo();

//蛇移动的函数- 每次走一步
void SnakeMove(pSnake ps);

//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);

//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);

//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);

//检测撞墙
void KillByWall(pSnake ps);

//撞到自己
void KillBySelf(pSnake ps);

//游戏结束资源释放
void GameEnd(pSnake ps);

 test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"

void test()
{
	int ch = 0;
	do
	{
		// 创建一条贪吃蛇
		Snake snake = { 0 };
		// 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
		GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
		GameRun(&snake); // 游戏过程
		GameEnd(&snake);//善后工作
		SetPos(20, 15);
		printf("再来一局?(y/n)");
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');

}
int main()
{
	//适配本地中文环境
	setlocale(LC_ALL, "");

	//贪吃蛇游戏的设置
	test();
	return 0;
}

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

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

相关文章

《数字孪生城市建设指引报告(2023年)》指引智慧城市行动方向

2023年12月27日&#xff0c;中国信息通信研究院&#xff08;简称“中国信通院”&#xff09;产业与规划研究所、中国互联网协会数字孪生技术应用工作委员会和苏州工业园区数字孪生创新坊联合发布《数字孪生城市建设指引报告&#xff08;2023年&#xff09;》。该报告提出了三大…

掌握CSS网格函数fit-content()的妙用

CSS网格布局是一种强大的布局系统&#xff0c;它提供了灵活的网格化设计能力。其中&#xff0c;fit-content()函数是一项重要的功能&#xff0c;它可以帮助我们在网格容器中自动调整网格项的尺寸。本文将详细讲解fit-content()函数的使用方法及其常见应用场景&#xff0c;助你掌…

探索PostgreSQL:从基础到实践(简单实例)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 下载前言一、PostgreSQL是什么&#xff1f;二、使用步骤1.引入库2.读入数据 总结 下载 点击下载提取码888999 前言 在当今的大数据时代&#xff0c;数据库作为信…

《Redis核心技术与实战》学习笔记1——基本架构:一个键值数据库包含什么?

基本架构&#xff1a;一个键值数据库包含什么&#xff1f; 文章目录 基本架构&#xff1a;一个键值数据库包含什么&#xff1f;可以存哪些数据&#xff1f;可以对数据做什么操作&#xff1f;采用什么访问模式&#xff1f;如何定位键值对的位置&#xff1f;不同操作的具体逻辑是…

【力扣】两数相加,模拟+递归

两数相加原题地址 方法一&#xff1a;模拟 注意到链表的方向是从低位到高位&#xff0c;而做“竖式相加”也是低位到高位。 1 2 3 4 5 ----------- 1 6 8 所以可以用同样的方法来模拟。如果不考虑进位&#xff0c;只需要取出对应位的2个数相加&#xff0c;再尾插到新的…

【flutter】报错 cmdline-tools component is missing

在flutterSDK目录下&#xff0c;双击flutter_console.bat&#xff0c;调出命令行。 输入flutter doctor&#xff0c;如果第三个诊断为[x]&#xff0c;报cmdline-tools component is missing错&#xff08;我这已经修改好了&#xff0c;所以是勾了&#xff09;&#xff0c;那就可…

爬虫(三)

1.JS逆向实战破解X-Bogus值 X-Bogus:以DFS开头&#xff0c;总长28位 答案是X-Bogus,因为会把负载里面所有的值打包生成X-Boogus 1.1 找X-Bogus加密位置&#xff08;请求堆栈&#xff09; 1.1.1 绝招加高级断点&#xff08;日志断点&#xff09; 日志断点看有没有X-B值 日志…

【wu-lazy-cloud-network】Java自动化内网穿透

项目介绍 wu-lazy-cloud-network 是一款基于&#xff08;wu-framework-parent&#xff09;孵化出的项目&#xff0c;内部使用Lazy ORM操作数据库&#xff0c;主要功能是网络穿透&#xff0c;对于没有公网IP的服务进行公网IP映射 使用环境JDK17 Spring Boot 3.0.2 功能 1.内网…

Hadoop-IDEA开发平台搭建

1.安装下载Hadoop文件 1&#xff09;hadoop-3.3.5 将下载的文件保存到英文路径下&#xff0c;名称一定要短。否则容易出问题&#xff1b; 2&#xff09;解压下载下来的文件&#xff0c;配置环境变量 3&#xff09;我的电脑-属性-高级设置-环境变量 4.详细配置文件如下&#…

神经网络的权重是什么?

请参考这个视频https://www.bilibili.com/video/BV18P4y1j7uH/?spm_id_from333.788&vd_source1a3cc412e515de9bdf104d2101ecc26a左边是拟合的函数&#xff0c;右边是均方和误差&#xff0c;也就是把左边的拟合函数隐射到了右边&#xff0c;右边是真实值与预测值之间的均方…

[Linux] 网络编程套接字

目录 预备知识 网络字节序 网络字节序和主机字节序转换的库函数 socket编程接口 socket常见API sockaddr结构 套接字的种类 预备知识 1.在IP数据包头部中&#xff0c;有两个IP地址&#xff0c;分别叫做源IP地址和目的IP地址。 2.端口号&#xff1a;是传输层协议的内容…

Springboot集成jasypt实现配置文件加密

Jasypt它提供了单密钥对称加密和非对称加密两种加密方式。 单密钥对称加密&#xff1a;一个密钥加盐&#xff0c;可以同时用作内容的加密和解密依据&#xff1b; 非对称加密&#xff1a;使用公钥和私钥两个密钥&#xff0c;才可以对内容加密和解密&#xff1b; 我们以单密钥对称…

性能评测|虚拟化和裸金属 K8s 哪个性能更好?

本文重点 整体而言&#xff0c;SKS&#xff08;虚拟机 Kubernetes&#xff09;可以达到裸金属 Kubernetes 性能的 82% – 96%&#xff0c;满足绝大部分场景下生产容器应用的性能需求。更多虚拟化与裸金属 Kubernetes 架构、特性、适用场景与性能对比&#xff0c;欢迎阅读文末电…

mac检查CPU温度和风扇速度软件:Macs Fan Control Pro 1.5.17中文版

Macs Fan Control Pro for Mac是一款专业的电脑风扇控制工具&#xff0c;旨在帮助Mac用户有效控制电脑的风扇速度&#xff0c;提高电脑的运行效率和稳定性。 软件下载&#xff1a;Macs Fan Control Pro 1.5.17中文版 该软件支持多种风扇控制模式和预设方案&#xff0c;用户可以…

数据结构——B/顺序表和链表

&#x1f308;个人主页&#xff1a;慢了半拍 &#x1f525; 创作专栏&#xff1a;《史上最强算法分析》 | 《无味生》 |《史上最强C语言讲解》 | 《史上最强C练习解析》 &#x1f3c6;我的格言&#xff1a;一切只是时间问题。 ​ 1.线性表 线性表&#xff08;linear list&…

一文搞懂电容!

2.电容 1.品牌 国外&#xff1a;村田 muRata、松下 PANASONIC、三星 SAMSUNG、太诱 TAIYO YUDEN、TDK、威世 VISHAY、等等。 国内&#xff1a;国巨 YAGEO(中国台湾)、风华 FH、宇阳科技 EYANG、信昌电陶 PSA、三环 C 2.电容的主要作用 滤波、旁路、去耦、隔直&#xff08;…

亚信安慧AntDB构建繁荣生态的数据库管理系统

亚信安慧AntDB是一款数据库管理系统&#xff0c;它采用全球影响力大、社区繁荣、开放度高、生态增长迅速的PG内核。这款系统具有卓越的性能和稳定性&#xff0c;在全球范围内备受用户青睐。与此同时&#xff0c;AntDB的社区也是充满活力的&#xff0c;用户可以在社区中交流经验…

前端页面禁止debugger调试并跳转空白页面----文心一言官网实现方式

技术点&#xff1a;setInterval定时器Object.defineProperty 背景&#xff1a; 某天打开文心一言想看看接口返回结构是怎样的&#xff0c;熟练的打开浏览器开发者工具查看网络请求。 发现出现了以下debugger断点 这难不倒我&#xff0c;去掉断点调试&#xff0c;继续下一步不…

Stable Diffusion 模型下载:RealCartoon-Anime - V10

文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 这个检查点是从 RealCartoon3D 检查点分支出来的。它的目标是产生更多的“动漫”风格&#xff0c;因为我喜欢动漫。:)我知道有很多人做得很好&#xff08;比如aniw…

Kafka 使用手册

kafka3.0 文章目录 kafka3.01. 什么是kafka&#xff1f;2. kafka基础架构3. kafka集群搭建4. kafka命令行操作主题命令行【topic】生产者命令行【producer】消费者命令行【consumer】 5. kafka生产者生产者消息发送流程Producer 发送原理普通的异步发送带回调函数的异步发送同步…