Linux C贪吃蛇(单链表)

Linux C贪吃蛇(单链表)

功能

  1. Terminal显示游戏区域,蛇,食物,分数。
  2. 蛇只能在限定区域内移动,即碰墙,游戏结束;不允许蛇碰撞自己身体,否则游戏结束,其中包括往蛇头180°方向运动,即蛇头朝右移动时按A使蛇180°转向。
  3. 食物分"✶",“❉”,“☯”,"✡"四种,依次是10-40分,蛇吃到任意食物会变长一格。
  4. 游戏分数达到100之后蛇移动速度会加快,同时每次场上会出现两个食物,吃到任意一个会使场上所有食物位置刷新。
  5. 游戏除wasd控制方向外,q保存退出游戏,目录下产生snake.dat文件,此时重新进入游戏,Load Game选项正常显示,可以读取文件继续游戏;若目录下没有snake.dat文件,Load Game选项显示灰色,不能选择;p暂停游戏,输入任意字符回车后可继续游戏。

实现方式

  1. 蛇移动方式
    在这里插入图片描述
    tail节点_副本作为碰撞及吃食物判断的节点,其它节点从head节点开始赋值为其下一个节点的值,到tail节点为止,tail节点赋值为tail节点_副本的值

  2. 蛇吃到食物后的增长

由tail节点_副本判断到吃到食物后,将tail节点_副本添加到链表尾,成为新的tail节点

流程图

在这里插入图片描述

代码

定义的全局变量,枚举

int gameover = 0;									
int shadow_flag = 0;								//shadow on/off
int food_length = 1;							
int snake_length = 3;								
int scores = 50;									
int speed = 10;										

struct termios oldt, newt;							
int oldf;

typedef enum{UP = -1, DOWN = 1 , LEFT = 2, RIGHT = 4}DIR; //direction
DIR cur_dir = LEFT;

char FOOD_PIC[4][4] = {{"✶"},{"❉"},{"☯"},{"✡"}};		  //food picture

蛇的结构体定义,date内容单独定义成另一个date结构体,方便文件的保存和读取

ypedef struct snake_node_data
{
	int x;
	int y;
	DIR dir;
}Date, *PDate;

typedef struct snake_node
{
	Date date;
	struct snake_node *pnext;
}Snake, *PSnake;

食物结构体类似

typedef struct food_node_data
{
	int x;
	int y;
	int pic;
}FDate, *PFDate;

typedef struct food_node
{
	FDate date;
	struct food_node *pnext;
}Food, *PFood;

初始化蛇的链表,为了美观点,蛇身体*的x坐标用的是奇数

PSnake init(PSnake head, PSnake shadow)
{
	Snake *p, *q;
	int i;
	srand(time(0));
	head = creat_snake_node();				//creat and initialize head node
	head -> date.x = 21;
	head -> date.y = 8;
	head -> date.dir = LEFT;
	q = head;
	for(i = 1 ; i <=2 ; i ++)				//creat and initialize second third node
	{
		p = creat_snake_node();
		q -> pnext = p;
		q = p;
		q -> date.x = 21 - 2 * i;
		q -> date.y = 8;
		q -> date.dir = LEFT;
	}
	q -> pnext = NULL;						
	shadow -> date.x = 0;					//initialize shadow node 
	shadow -> date.y = 0;
	return head;
}

食物产生的位置要避开蛇的位置

PFood mkae_food(PFood food_head, PSnake snake_head)
{
	PFood f_p;
	PSnake s_p;

	f_p = food_head;
	while(f_p != NULL)
	{
		f_p -> date.x = (rand() % 20 + 5) * 2 + 1;     //odd number
		f_p -> date.y = rand() % 16 + 6;
		f_p -> date.pic = rand()%4;
		s_p = snake_head;
		while(s_p != NULL)							//avoid food overlao snake
		{
			if((f_p -> date.x == s_p -> date.x) && (f_p -> date.y == s_p -> date.y))
			break;
			s_p = s_p -> pnext;
		}
		if(s_p == NULL) f_p = f_p -> pnext;	
	}
	return food_head;
}

PFood init_food(PFood food_head, PSnake snake_head)
{

	food_head = creat_food_node();
	food_head = mkae_food(food_head, snake_head);

	return food_head;

}

程序是在terminal上运行,对termios里修改做到不显示所按的键,程序不会等待按键输入, Restorekey()用来恢复设置,不然即使退出程序也会影响到terminal里命令行的使用

void keyinit()
{
	tcgetattr(STDIN_FILENO, &oldt);
	newt = oldt;
	newt.c_lflag &= ~(ICANON | ECHO);
	tcsetattr(STDIN_FILENO, TCSANOW, &newt);
	oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
	fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
}
void Restorekey()
{
	tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
	fcntl(STDIN_FILENO, F_SETFL, oldf);
}

显示游戏区域,光标移动到指定位置后打印

void draw_ground()
{
	int i;
	system("clear");
	for(i = 0 ; i < (WIDTH - 1)* 2 ; i++)
	{
		MOVETO(10 + i, 5);	
		printf("━");
	}
	for(i = 0 ; i < HEIGHT ; i++)
	{
		MOVETO(10, 6 + i);
		printf("┃");
		MOVETO(50, 6 + i);
		printf("┃");
	}
	for(i = 0 ; i < (WIDTH - 1)* 2 ; i++)
	{
		MOVETO(10 + i, 22);	
		printf("━");
	}

	MOVETO(10,5);
	printf("┏");
	MOVETO(50,5);
	printf("┓");
	MOVETO(10,22);
	printf("┗");
	MOVETO(50,22);
	printf("┛");
}

游戏的菜单显示,效果如下,目录下有snake.dat在这里插入图片描述
没有snake.dat就不能选择的load game
在这里插入图片描述
保存,读取文件使用fread,fwrite,前5字节有"Snake"的校准tag

void save(PSnake snake_head, PFood food_head)
{
	FILE *fp;
	int i;
	PSnake s_p;
	PFood f_p;

	s_p = snake_head;
	f_p = food_head;
	if((fp = fopen("snake.dat","w")) == NULL)
	{
		printf("open fail\n");
	}
	else
	{
		fwrite("Snake",5,1,fp);				//save tag
		fwrite(&snake_length, 4, 1, fp);   	//save snake node date
		for(i = 0 ; i < snake_length ; i++)
		{
			fwrite( &(s_p -> date), sizeof(Date),1, fp);
			s_p = s_p -> pnext;
		}

		fwrite(&food_length, 4, 1, fp);     //save food node date
		for(i = 0 ; i < food_length ; i++)
		{
			fwrite( &(f_p -> date), sizeof(FDate),1, fp);
			f_p = f_p -> pnext;
		}
		fwrite(&scores, 4 ,1, fp);			//save scores
		fwrite(&cur_dir, 4, 1, fp);			//save current direction
		fwrite(&speed,4,1,fp);				//save speed
	}
	if(fp != NULL)
	{
		fclose(fp);
		fp = NULL;
	}
}

移动蛇,原理就是上文内容,根据枚举量cur_dir提供方向,然后移动。里面有个shadow的snake_node结构体,是为了保存链表头也就是蛇尾的坐标,因为不将system(“clear”)写入主循环,所以需要单独擦除移动后原来蛇尾坐标上的蛇身体"*"

void move(PSnake snake_head, PSnake shadow, PFood food_head)
{
	Snake snake_head_1;
	PSnake s_p, tail, new_node;
	PFood f_p;

	shadow -> date.x = snake_head -> date.x;  			 //save shadow value
	shadow -> date.y = snake_head -> date.y;

	s_p = snake_head;                 					 //find tail snake node
	while(s_p -> pnext != NULL)
		s_p = s_p -> pnext;
	tail = s_p;

	snake_head_1 = *s_p;               					//snake head copy move  
	if(cur_dir == LEFT || cur_dir == RIGHT)
		snake_head_1.date.x += (cur_dir-3) * 2;
	else
		snake_head_1.date.y += cur_dir;
	snake_head_1.date.dir = cur_dir;

	wall(snake_head_1, snake_head);
	if(gameover == 0)
	{
		f_p = food_head;
		while(f_p != NULL)           				// find snake food collision node
		{
			if( (snake_head_1.date.x == f_p ->date.x) && (snake_head_1.date.y == f_p -> date.y))
				break;
			f_p = f_p ->pnext;
		}
		if (f_p == NULL)             				//no collision node
		{
			s_p = snake_head;             			 //snake body move
			while(s_p -> pnext != NULL)
			{
				s_p -> date.x = s_p -> pnext -> date.x;
				s_p -> date.y = s_p -> pnext -> date.y;
				s_p = s_p -> pnext;
			}

			tail -> date.x = snake_head_1.date.x;      //snake head move
			tail -> date.y = snake_head_1.date.y;
			tail -> date.dir = snake_head_1.date.dir;
		}
		else                             			 //creat new snake node
		{
			shadow_flag = 1;
			new_node = creat_snake_node();
			tail -> pnext = new_node;
			new_node -> pnext = NULL;
			new_node -> date.x = snake_head_1.date.x;      
			new_node -> date.y = snake_head_1.date.y;
			new_node -> date.dir = snake_head_1.date.dir;
			scores += ((f_p -> date.pic) + 1) * 10;
			f_p = food_head;
			while(f_p != NULL)                          //diappear old food node
			{
				MOVETO(f_p -> date.x, f_p -> date.y);
				printf(" ");
				f_p = f_p -> pnext;
			}

			food_head = mkae_food(food_head, snake_head);  //make new food
			snake_length++;
		}

	}
}

碰撞判断wall,碰身体或者墙就结束游戏

void wall(Snake snake_head_1, PSnake snake_head)
{
	PSnake s_p;
												//wall collision
	if(snake_head_1.date.x < 10 || snake_head_1.date.x > 50 || snake_head_1.date.y <= 5 || snake_head_1.date.y >= 22)
		gameover = 1;

	s_p = snake_head -> pnext;
	while(s_p != NULL)							//snake body collision
	{
		if((snake_head_1.date.x == s_p -> date.x) && (snake_head_1.date.y == s_p -> date.y))
			{		
				gameover = 1;
				break;
			}
		s_p = s_p -> pnext;
	}
	
}

显示蛇和食物,switch本来是想做成根据蛇移动方向,蛇头有不同的方向箭头显示,但是不知道是虚拟机的关系还是ubuntu本身的关系,占2光标的字符移动显示会有些残影,所以都改成了"*"

void draw(PSnake snake_head, PSnake shadow, PFood food_head)
{
	PSnake s_p, tail;
	PFood f_p;
	int i;
	s_p = snake_head;						//find tail snake node
	while(s_p -> pnext != NULL)
		s_p = s_p -> pnext;
	tail = s_p;						

	s_p = snake_head;
	while(s_p != NULL)						//draw snake body
	{
		MOVETO(s_p -> date.x, s_p -> date.y);
		printf("*");
		s_p = s_p -> pnext;
	}

	switch(tail -> date.dir)				//draw snake head
	{
		case -1:
				MOVETO(tail -> date.x, tail -> date.y);
				printf("*");
				//printf("↑");
				break;
		case 1:
				MOVETO(tail -> date.x, tail -> date.y);
				printf("*");
				//printf("↓");
				break;
		case 2:
				MOVETO(tail -> date.x, tail -> date.y);
				printf("*");
				//printf("←");
				break;
		case 4:
				MOVETO(tail -> date.x, tail -> date.y);
				printf("*");
				//printf("→");
				break;
	}
	if(gameover == 0 && shadow_flag == 0)			//erase shadow
	{
		if(shadow -> date.x != 0)
		{
			MOVETO(shadow -> date.x, shadow -> date.y);
			printf(" ");
		}
	}
	else shadow_flag = 0;

	f_p = food_head;
	while(f_p != NULL)								//display food
	{
		if(f_p -> date.x != 0){
		MOVETO(f_p -> date.x, f_p -> date.y);
		printf("%s",FOOD_PIC[f_p -> date.pic]);}
		f_p = f_p -> pnext;
	}
	MOVETO(24,4);
	printf("Scores:%d", scores);
}

游戏也会随着分数的变高改变难度,100分之后,场上会多刷一个食物,同时蛇的移动速度会加快,这里switch里只写了100分的等级,可以在下面的case再随意增加食物数量,不过吃到任意食物后,场上所有食物都会刷新改变位置

PFood game_level(PFood food_head)
{
	PFood new_node, f_p;
	f_p = food_head;
	while(f_p -> pnext != NULL)
		f_p = f_p -> pnext;

	switch(scores/100)
	{
		case 1:
				speed = 7;							//speed up
				if(food_length == 1)
				{
					new_node = creat_food_node();	//craet new food node
					new_node -> date.x = 0;
					new_node -> date.y = 0;
					new_node -> date.pic = 0;
					new_node -> pnext = NULL;
					f_p -> pnext = new_node;
					food_length = 2;
				}		
				break;
		case 2:
				speed = 4;
				break;
		case 3:
				speed = 2;
				break;
		default:
				break;
	}
	return food_head;
}

游戏运行截图
在这里插入图片描述
完整代码
https://gitee.com/yysshh/eating_snake_chain_list.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值