简单的手工移动的贪吃蛇模型来自于我之前的一篇博客,为了实现简单的AI功能,我只是在它的基础上添加了相关的AI部分代码而已。但就算如此,也有许多东西需要考虑。
AI贪吃蛇通俗点来说就是让蛇自己实现***自动寻路功能***。从这一点来说,首先应该想到的就是有关图的搜索算法了,这个我们下面会详细描述。现在我们先考虑最简单的一种情况,即***使用某种办法,让蛇自己先动起来。***
那么使用什么办法呢?要想让蛇自己寻找食物去吃,那么首先应该知道食物的坐标,这个不难办到(我们假设每次只会出现一个食物),然后就需要一条从蛇本身到食物的一条路径,蛇只要沿着这条路径走,就能吃到食物了。先不要去管这种方法的可行性有多高,最起码它能让蛇自己动起来,这已经是一个很好的开头了,不是吗?
那么怎么去选择这条路径呢?我们知道,两点之间的路径可以有好多种,其中直线最短,显然我们希望蛇能沿着最短路径吃到食物,但直线显然是不现实的,它无法沿着直线走。那我们可以退而求其次,选择一条***最简单方便的路径***,这条路径就是所谓的***曼哈顿距离***。它实际上很简单,在具体的编程中也很容易操作。
红、蓝与黄线都表示曼哈顿距离,它们都拥有一样的长度;而绿线则表示欧几里得距离。
我们可以选择***红线***这种最简单的曼哈顿距离,每次我们记录下路径,然后沿着这条路径去走,直到吃到食物。在游戏初期,蛇身的长度很短时,这种方法是有一定作用的,可以让蛇坚持一段时间。但一旦蛇身变长,情形就显然有所不同了。
***贪吃蛇每时每刻所面临的情形不是静态的一成不变的,而是随着它的移动不断发生变化的,前一刻所做出的安全可行的决策,在后一刻未必是安全可行的,也就是说,对于路径的规划应该是动态的,蛇每走一步,我们都应该重新审视蛇当前所面临的处境,重新寻找出一条可行的路径。***
基于这种考虑,我们可以想到使用***BFS和DFS***来动态解决蛇的自动寻路问题。因为***BFS***找到的一定是最短路径,因此,我们优先考虑使用***BFS***。但后面我们就会意识到,事情有时并非绝对,并不是所有的情况下都需要走最短的路径。
现在我们要思考几个问题:
- 每次只要找到最短路径,就要沿着该路径去吃食物吗?
- 吃到食物后,蛇有没有可能陷入绝境?
- 如果无法找到蛇到食物的最短路径怎么办?
- 。。。
这样想来,有些问题确实还挺复杂的。针对上述问题,我们先给出一个初步的算法。
if (能找到吃食物的最短路径)
去吃食物
else
向安全的地方随便走一步
显然上述算法也是有缺陷的,它并没有解决我们上面提出的问题。也就是说,***它只满足于现在能吃到食物,而不去考虑这种行为是否会将自己置身于危险的境地。但它解决了一个问题,如果无法找到路径,只能***wander***一步,这种看似没有道理的***闲逛,在实际中却是工作的很好。这种方案具有一定的随机性,并非完美无缺。但在绝大多数情况下它是可行的。
蛇在去吃食物时,显然不能只顾当前,而应该考虑的长远一点。如果仔细观察蛇身的移动,我们会发现,每次蛇身移动过后,蛇尾总是会空出一个位置,而这个位置一定是安全的,也是我们想要的。也就是说,每当去吃食物时,先判断一下能否找到从蛇身到蛇尾的路径,然后再作出决策。
至于从蛇身到蛇尾的路径应该是什么样的,显然此时我们理所当然的认为应该是最短路径。但之后我们就会发现,这种方法如果运气好的话,可以让蛇运行相当长的一段时间,但它有时却会让蛇陷入到***无限循环***中。
if (能找到吃食物的最短路径)
if (吃到食物后能找到蛇尾)
去吃食物
else
if (能找到蛇头到蛇尾的最短路径)
跟着蛇尾走
else
wander一步
else
if (能找到蛇头到蛇尾的最短路径)
跟着蛇尾走
else
wander一步
这时的算法稍微有点复杂,但其实也不难,因为逻辑简单清楚。这里可能会出现问题的地方就是***wander***这种策略了。
我们可以试想一下这种情况:
当蛇要走下一步时发现,它能找到吃食物的路径,但吃过后却无法找到自己的尾巴,然而却能找到从自己蛇头到蛇尾的路径。基于我们的算法策略,它认为现在去吃食物不安全,所以它决定跟着蛇尾走,但这种做法是有缺陷的,很有可能它会一直陷入这种怪圈中,因为下一步可能一直是不安全的,这样就会让它自己陷入无限循环之中,即一直追着自己的尾巴走。
对于应该如何解决这种困境,我的初步想法是,***在寻找到蛇尾的路径时,不能使用BFS,而应该使用DFS朝相反方向找一条相对来说较远的路径。***这种反其道而行之的做法却是合理的,当蛇无法找到可行路径时,它就应该为自己之后的行动留有余地。
这种方法应该有一定概率跑完全图,但我还没有去实现它,我目前实现了只使用***BFS***的那个版本。
我们上面讨论了相关算法策略,但在具体编程实现时仍有许多需要仔细思考的东西,下面我会给出相应的C代码,并作出一定的解释。
#define DELAY 50 /* 设置延时 */
/* 蛇的活动地图的大小 */
#define ROW (LINES - 3)
#define COL (COLS - 25)
typedef struct snake { /* 蛇身节点 */
int sx;
int sy;
struct snake *next;
} Snake;
typedef struct qnode { /* 队列节点 */
int x;
int y;
struct qnode *pre; /* 用于回溯路径 */
struct qnode *next;
} Queue;
struct smap {
int vis[30][120]; /* 标记是否访问过某个点 */
} Smap;
#define isok(x, y) ((x) > 2 && (x) <= ROW && (y) > 3 && (y) <= COL)
Snake *head, *tail; /* 蛇头、蛇尾 */
Queue *front, *rear; /* 队头,队尾 */
int fx, fy; /* 食物坐标 */
int nx, ny; /* 蛇下一步要走的坐标 */
int dx[] = {0, 2, 0, -2};
int dy[] = {2, 0, -2, 0};
int foundpath; /* 是否找到路径 */
int findtail; /* 是否在寻找蛇尾 */
int main(void)
{
init();
signal(SIGALRM, display_snake);
swait();
endwin();
exit(0);
}
void swait(void)
{
int c;
/* 输入'q',则退出游戏 */
while ((c = getch()) != 'q')
set_ticker(DELAY);
}
***display_snake()***函数便是整个AI部分的核心控制函数了,它完全符合我们上面所描述的算法,只是简单的用C语言翻译过来而已。
/* initgame函数:游戏初始化 */
void init(void)
{
initscr(); /* 初始化curses */
start_color(); /* 初始化颜色表 */
set_color(); /* 设置颜色 */
box(stdscr, ACS_VLINE, ACS_HLINE); /* 绘制一个同物理终端大小相同的窗口 */
noecho(); /* 关闭键入字符的回显 */
cbreak(); /* 字符一键入,直接传递给程序 */
curs_set(0); /* 隐藏光标 */
draw_map();
creat_snake();
creat_food();
refresh();
}
/* display_snake函数:游戏的主要控制逻辑 */
void display_snake(int signo)
{
if (bfs(head->sx, head->sy, fx, fy)) { /* 能找到食物 */
int tx = nx;
int ty = ny;
findtail = 1;
if (bfs(nx, ny, tail->sx, tail->sy)) /* 吃到食物后能找到蛇尾 */
add_snake(tx, ty); /* 去吃食物 */
else {
if (bfs(head->sx, head->sy, tail->sx, tail->sy))
add_snake(nx, ny); /* 跟着蛇尾走 */
else
around(); /* 随便逛逛 */
}
} else {
findtail = 1;
if (bfs(head->sx, head->sy, tail->sx, tail->sy))
add_snake(nx, ny);
else
around();
}
findtail = 0;
if (is_eat_food())
creat_food();
else
del_snake();
refresh();
}
***around()***函数只是在蛇头周围找出一处安全的位置去走。至于有关蛇身移动的细节,可以参考我之前的那篇博客。
/* around函数:随便逛逛 */
void around(void)
{
findtail = 0;
for (int i = 0; i < 4; i++) {
int tx = head->sx + dx[i];
int ty = head->sy + dy[i];
if (isok(tx, ty) && !is_crash_snake(tx, ty)) {
add_snake(tx, ty);
break;
}
}
}
/* creat_snake函数:初始化蛇身 */
void creat_snake(void)
{
assert(head = tail = malloc(sizeof(Snake)));
head->next = NULL;
srand(clock()); /* 以当前挂钟时间作随机种子数 */
while ((head->sx = rand() % (ROW - 2) + 3) % 2 == 0)
;
while ((head->sy = rand() % (COL - 3) + 4) % 2 != 0)
;
attron(COLOR_PAIR(1));
mvaddch(head->sx, head->sy, ' ');
attroff(COLOR_PAIR(1));
}
/* creat_food函数:设置食物 */
void creat_food(void)
{
srand(clock());
while ((fx = rand() % (ROW - 2) + 3) % 2 == 0)
;
while ((fy = rand() % (COL - 3) + 4) % 2 != 0)
;
if (is_crash_snake(fx, fy)) /* 食物不能覆盖蛇身 */
creat_food();
attron(COLOR_PAIR(2));
mvaddch(fx, fy, ' ');
attroff(COLOR_PAIR(2));
}
/* add_snake函数:在蛇头增加2个节点 */
void add_snake(int x, int y)
{
Snake *p, *q;
assert(p = malloc(sizeof(Snake)));
assert(q = malloc(sizeof(Snake)));
head->next = p;
p->next = q;
q->next = NULL;
attron(COLOR_PAIR(1));
p->sx = (head->sx + x) / 2;
p->sy = (head->sy + y) / 2;
mvaddch(p->sx, p->sy, ' ');
q->sx = x;
q->sy = y;
mvaddch(q->sx, q->sy, ' ');
attroff(COLOR_PAIR(1));
head = q;
}
/* del_snake函数:在蛇尾删除2个节点 */
void del_snake(void)
{
Snake *tmp;
mvaddch(tail->sx, tail->sy, ' ');
mvaddch(tail->next->sx, tail->next->sy, ' ');
tmp = tail->next->next;
free(tail->next);
free(tail);
tail = tmp;
}
/* is_eat_food函数:判断是否吃到食物 */
int is_eat_food(void)
{
return head->sx == fx && head->sy == fy;
}
***is_crash_snake()***函数需要注意的一点是,***当正在寻找到蛇尾的路径时,蛇尾本身将不能视作蛇身的一部分。***即在判断是否撞到蛇自己时,应该忽略蛇尾。
/* bfs函数:寻找从(sx, sy)到(rx, ry)的最短路径 */
int bfs(int sx, int sy, int rx, int ry)
{
memset(Smap.vis, 0, sizeof(Smap.vis));
initqueue(sx, sy, NULL);
Smap.vis[sx][sy] = 1; /* 标记为已访问 */
foundpath = 0; /* 未找到路径 */
while (front) {
if (front->x == rx && front->y == ry) { /* 找到了食物 */
backpath(front); /* 回溯路径 */
foundpath = 1; /* 已找到路径 */
break;
}
for (int i = 0; i < 4; i++) {
int tx = front->x + dx[i];
int ty = front->y + dy[i];
if (!Smap.vis[tx][ty] && isok(tx, ty) && !is_crash_snake(tx, ty)) {
enqueue(tx, ty, front);
Smap.vis[tx][ty] = 1;
}
}
front = front->next;
}
clean(); /* 清空队列 */
return foundpath;
}
/* backpath函数:回溯路径,找到蛇下一步需要走的坐标 */
void backpath(Queue *p)
{
Queue *q;
while (p->pre) {
q = p;
p = p->pre;
}
nx = q->x;
ny = q->y;
}
/* is_crash_snake函数:是否撞到了蛇自己 */
int is_crash_snake(int x, int y)
{
Snake *p = tail;
if (findtail)
while (p) {
if (p != tail && x == p->sx && y == p->sy)
return 1;
p = p->next;
}
else
while (p) {
if (x == p->sx && y == p->sy)
return 1;
p = p->next;
}
return 0;
}
下面是队列的几个基本操作。队列主要用于***BFS***中,用于记录路径。
/* initqueue函数:初始化队列 */
void initqueue(int x, int y, Queue *pre)
{
Queue *p;
assert(p = malloc(sizeof(Queue)));
p->x = x;
p->y = y;
p->pre = pre;
front = rear = p;
p->next = NULL;
}
/* enqueue函数:入队操作 */
void enqueue(int x, int y, Queue *pre)
{
Queue *p;
assert(p = malloc(sizeof(Queue)));
p->x = x;
p->y = y;
p->pre = pre;
rear->next = p;
rear = p;
p->next = NULL;
}
/* clean函数:销毁队列 */
void clean(void)
{
Queue *tmp;
while (rear) {
tmp = rear->pre;
free(rear);
rear = tmp;
}
}
最后是一些画图和设置函数,可以根据自己的需要去调整。
/* draw_map函数:绘制游戏地图 */
void draw_map(void)
{
int i;
attron(COLOR_PAIR(3));
for (i = 3; i < COLS - 2; i += 2) {
mvaddch(2, i, ' ');
mvaddch(LINES - 2, i, ' ');
}
for (i = 3; i < LINES - 1; i += 2) {
mvaddch(i, 3, ' ');
mvaddch(i, COL + 1, ' ');
mvaddch(i, COLS - 4, ' ');
}
attroff(COLOR_PAIR(3));
}
/* set_color函数:设置颜色属性 */
void set_color(void)
{
init_pair(1, COLOR_GREEN, COLOR_GREEN);
init_pair(2, COLOR_RED, COLOR_RED);
init_pair(3, COLOR_WHITE, COLOR_WHITE);
}
/* set_ticker函数:设置间隔计时器(ms) */
int set_ticker(int n_msecs)
{
struct itimerval new_timeset;
long n_sec, n_usecs;
n_sec = n_msecs / 1000;
n_usecs = (n_msecs % 1000) * 1000L;
new_timeset.it_interval.tv_sec = n_sec; /* 设置初始间隔 */
new_timeset.it_interval.tv_usec = n_usecs;
new_timeset.it_value.tv_sec = n_sec; /* 设置重复间隔 */
new_timeset.it_value.tv_usec = n_usecs;
return setitimer(ITIMER_REAL, &new_timeset, NULL);
}
最后要说明的一点是,不管是***BFS***还是***DFS***,都是***盲目式***搜索算法,一般而言,它们都不是高效的,而使用***启发式***搜索算法显然更好一些,如大名鼎鼎的***A****算法。我呢,希望自己以后能有时间用***A*算法去实现一下***AI贪吃蛇。