Linux C贪吃蛇(单链表)
功能
- Terminal显示游戏区域,蛇,食物,分数。
- 蛇只能在限定区域内移动,即碰墙,游戏结束;不允许蛇碰撞自己身体,否则游戏结束,其中包括往蛇头180°方向运动,即蛇头朝右移动时按A使蛇180°转向。
- 食物分"✶",“❉”,“☯”,"✡"四种,依次是10-40分,蛇吃到任意食物会变长一格。
- 游戏分数达到100之后蛇移动速度会加快,同时每次场上会出现两个食物,吃到任意一个会使场上所有食物位置刷新。
- 游戏除wasd控制方向外,q保存退出游戏,目录下产生snake.dat文件,此时重新进入游戏,Load Game选项正常显示,可以读取文件继续游戏;若目录下没有snake.dat文件,Load Game选项显示灰色,不能选择;p暂停游戏,输入任意字符回车后可继续游戏。
实现方式
-
蛇移动方式
tail节点_副本作为碰撞及吃食物判断的节点,其它节点从head节点开始赋值为其下一个节点的值,到tail节点为止,tail节点赋值为tail节点_副本的值 -
蛇吃到食物后的增长
由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