一条贪吃蛇的自我修养——从手动皮皮蛇到智能皮皮蛇

时间:2022-06-09 03:09:22

不知道大家当初选择学习C语言是出于什么目的,我的目的比较简单粗暴,就是为了以后能自己设计游戏。然!鹅!学了之后才发现一个简单的贪吃蛇也能让我累死累活,不过经历了我的仔细琢磨研究,总算是完成了贪吃蛇的代码,以及后续的升级版——智能蛇(伪)(说伪是因为我用的算法做出来的蛇比较傻)。
闲话不多说,让我们先来看看一个最基础的皮皮蛇是如何构建的吧。
首先我们规定蛇的活动范围为10x10,蛇头为“H”,蛇身为“X”,食物为“$”,阻碍物为“*”。
我们的第一个目标是做一条能接收指令自己动的蛇;
让我们先书写它的伪代码

 输出字符矩阵
WHILE not 游戏结束 DO
ch=等待输入
CASE ch DO
‘A’:左前进一步,break
‘D’:右前进一步,break
‘W’:上前进一步,break
‘S’:下前进一步,break
END CASE
输出字符矩阵
END WHILE
输出 Game Over!!!

然后我们按照伪代码先写好总控代码:

int main()
{
char choice;
printmap();
while (running) {
scanf("%c", &choice);
move(choice);
printmap();
}
return 0;
}

这里运用了自顶向下的思路,写完总体我们再分别书写各个函数,条理就会显得比较清晰,最后的成品如下:

#include <stdio.h>
#include<stdlib.h>
#include<time.h>
char map[12][13] = {
"************",
"*XXXXH *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"************"
};
int bodyx[5] = { 1,1,1,1 };
int bodyy[5] = { 4,3,2,1 };
int headx = 1, heady = 5;
int running = 1, len = 5;
void printmap() {
system("cls");
for (int i = 0; i < 12; i++)
printf("%s\n", map[i]);
}
void gameover() {
printf("Game over!\n");
running = 0;
}
void move(char ctrl) {
int i = 0;
int prex = headx, prey = heady;
switch (ctrl) {
case 'A':case 'a':
heady--;
break;
case 'W':case 'w':
headx--;
break;
case 'D':case 'd':
heady++;
break;
case 'S':case 's':
headx++;
break;
default:
return;
}
if (map[headx][heady] != ' ')
gameover();
else {
map[headx][heady] = 'H';
map[prex][prey] = map[bodyx[i]][bodyy[i]];
for (i; i < len - 2; i++) {
map[bodyx[i]][bodyy[i]] = map[bodyx[i + 1]][bodyy[i + 1]];
}
map[bodyx[i]][bodyy[i]] = ' ';
for (i = len - 3; i >= 0; i--) {
bodyx[i + 1] = bodyx[i];
bodyy[i + 1] = bodyy[i];
}
bodyx[0] = prex;
bodyy[0] = prey;
}
}
int main()
{
char choice;
printmap();
while (running) {
scanf("%c", &choice);
move(choice);
printmap();
}
return 0;
}

完成了会动的蛇,我们当然不能忘了贪吃蛇的精髓,吃了食物身体会变长。
有了第一条代码做基础,如果你能理解第一条代码中move函数的意图,那么完成这一步将会变得十分轻松。
我们只需增加一个food函数,这要用到随机数,因为食物的put是在随机位置的。
那么,我们就有:

void food() {
foodx = rand() % 10 + 1;
foody = rand() % 10 + 1;
while (map[foodx][foody] != ' ') {
foodx = rand() % 10 + 1;
foody = rand() % 10 + 1;
}
map[foodx][foody] = '$';
}

然后我们还要考虑吃了食物的蛇身体会变长,我们对move函数稍加改进:

void move(char ctrl) {
int i = 0;
int prex = headx, prey = heady;
switch (ctrl) {
case 'A':case 'a':
heady--;
break;
case 'W':case 'w':
headx--;
break;
case 'D':case 'd':
heady++;
break;
case 'S':case 's':
headx++;
break;
default:
return;
}
if (map[headx][heady] != ' '&&map[headx][heady] != '$')
gameover();
else if (map[headx][heady] == ' ') {
map[headx][heady] = 'H';
map[prex][prey] = map[bodyx[i]][bodyy[i]];
for (i; i < len - 2; i++) {
map[bodyx[i]][bodyy[i]] = map[bodyx[i + 1]][bodyy[i + 1]];
}
map[bodyx[i]][bodyy[i]] = ' ';
for (i = len - 3; i >= 0; i--) {
bodyx[i + 1] = bodyx[i];
bodyy[i + 1] = bodyy[i];
}
bodyx[0] = prex;
bodyy[0] = prey;
}
else {
map[headx][heady] = 'H';
map[prex][prey] = map[bodyx[i]][bodyy[i]];
for (i; i < len - 2; i++) {
map[bodyx[i]][bodyy[i]] = map[bodyx[i + 1]][bodyy[i + 1]];
}
map[bodyx[i]][bodyy[i]] = 'X';
len++;
for (i = len - 3; i >= 0; i--) {
bodyx[i + 1] = bodyx[i];
bodyy[i + 1] = bodyy[i];
}
bodyx[0] = prex;
bodyy[0] = prey;
food();
}
}

完成这些,再把food函数放到main里的正确位置,一个会吃的蛇就大功告成了。

然后我们进入第三步,升华这只皮皮蛇的蛇生!
我们还是先来写个伪代码,关于让他自动寻路的算法的伪代码:

     Hx,Hy: 头的位置
Fx,Fy:食物的位置
function whereGoNext(Hx,Hy,Fx,Fy) {
用数组movable[3]={“a”,”d”,”w”,”s”} 记录可走的方向
用数组distance[3]={0,0,0,0} 记录离食物的距离
分别计算蛇头周边四个位置到食物的距离。H头的位置,F食物位置
例如:假设输入”a” 则distance[0] = |Fx – (Hx-1)| + |Fy – Hy|
如果 Hx-1,Hy 位置不是Blank,则 distance[0] = 9999
选择distance中存最小距离的下标p,注意最小距离不能是9999
返回 movable[p]
}

我们依然只需要写一个函数,再将它插入到main函数中正确的位置即可
以下是我写的wheregonext函数:

char wheregonext(int hx, int hy, int fx, int fy) {
int p = 0, min = 100;
char movable[4] = { 'a','w','d','s' };
int distance[4] = { 0 };
distance[0] = abs(fx - hx) + abs(fy - (hy - 1));
if (distance[0] <= min && (map[hx][hy - 1] == ' ' || map[hx][hy - 1] == '$')) {
min = distance[0];
p = 0;
}
else
min = min;
distance[1] = abs(fx - (hx - 1)) + abs(fy - hy);
if (distance[1] <= min && (map[hx - 1][hy] == ' ' || map[hx - 1][hy] == '$')) {
min = distance[1];
p = 1;
}
else
min = min;
distance[2] = abs(fx - hx) + abs(fy - (hy + 1));
if (distance[2] <= min && (map[hx][hy + 1] == ' ' || map[hx][hy + 1] == '$')) {
min = distance[2];
p = 2;
}
else
min = min;
distance[3] = abs(fx - (hx + 1)) + abs(fy - hy);
if (distance[3] <= min && (map[hx + 1][hy] == ' ' || map[hx + 1][hy] == '$')) {
min = distance[3];
p = 3;
}
else
min = min;
return movable[p];
}

此时的主函数稍微修改一下:

#include <stdio.h>
#include<stdlib.h>
#include<time.h>
#include<math.h>
#include<windows.h>
int main()
{
char choice;
srand(time(NULL));
food();
printmap();
while (running) {
Sleep(40);
choice = wheregonext(headx, heady, foodx, foody);
move(choice);
printmap();
}
return 0;
}

这是在Windows下书写的智能蛇,当我们改用Linux环境时,就要进行一些小改动。
比如头文件windows.h在linux中不存在,我们可以使用unist.h,还要注意的是,两个头文件中的sleep函数用法不一样;再比如清屏,Windows中用system(“cls”)即可,而Linux中可以使用VT100终端中的printf(“\033[2J”)实现清屏。关于VT100终端标准,有兴趣的可以自行浏览http://www.cnblogs.com/zengjfgit/p/4373564.html
在Linux中,完成智能贪吃蛇,我们还要实现kbhit函数,这对于我们初学者来说难度太大,所以这里给好了代码,我们只需将自己在Windows下的智能蛇代码融入即可(maybe):

#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <termios.h>
#include <unistd.h>

static struct termios ori_attr, cur_attr;

static __inline
int tty_reset(void)
{
if (tcsetattr(STDIN_FILENO, TCSANOW, &ori_attr) != 0)
return -1;

return 0;
}


static __inline
int tty_set(void)
{

if ( tcgetattr(STDIN_FILENO, &ori_attr) )
return -1;

memcpy(&cur_attr, &ori_attr, sizeof(cur_attr) );
cur_attr.c_lflag &= ~ICANON;
// cur_attr.c_lflag |= ECHO;
cur_attr.c_lflag &= ~ECHO;
cur_attr.c_cc[VMIN] = 1;
cur_attr.c_cc[VTIME] = 0;

if (tcsetattr(STDIN_FILENO, TCSANOW, &cur_attr) != 0)
return -1;

return 0;
}

static __inline
int kbhit(void)
{

fd_set rfds;
struct timeval tv;
int retval;

/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 0;
tv.tv_usec = 0;

retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */

if (retval == -1) {
perror("select()");
return 0;
} else if (retval)
return 1;
/* FD_ISSET(0, &rfds) will be true. */
else
return 0;
return 0;
}

//将你的 snake 代码放在这里

int main()
{
//设置终端进入非缓冲状态
int tty_set_flag;
tty_set_flag = tty_set();

//将你的 snake 代码放在这里
printf("pressed `q` to quit!\n");
while(1) {

if( kbhit() ) {
const int key = getchar();
printf("%c pressed\n", key);
if(key == 'q')
break;
} else {
;// fprintf(stderr, "<no key detected>\n");
}
}

//恢复终端设置
if(tty_set_flag == 0)
tty_reset();
return 0;
}

最后,一个能在Linux系统下自行活动的蛇就完成了,当然,我们这里用的算法是比较傻的,能拿多少分完全随缘,就像我的一次游戏体验:
一条贪吃蛇的自我修养——从手动皮皮蛇到智能皮皮蛇
(我觉得这水平我还是可以碾压的)
更高级的算法还是留给有兴趣的的人去挑战吧。