简单AI贪吃蛇

简单的手工移动的贪吃蛇模型来自于我之前的一篇博客,为了实现简单的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贪吃蛇

猜你喜欢

转载自blog.csdn.net/qq_41145192/article/details/82634753