设计简单的贪吃蛇AI

前言

接着上一篇设计简单的贪吃蛇。本博客的目标是写出一个通过几率比较大的贪吃蛇AI。

代码重构

由于设计AI什么的代码我自己不太会用C写出较为易写的代码,这篇博客的源代码采用C++实现,重构了上一篇简单的贪吃蛇的C代码。
新重构后的代码有4个部分:Game类表示地图和一小部分逻辑,Snake类表示蛇和一小部分逻辑,SnakeAI类表示贪吃蛇AI,剩下的一些函数是关于操作系统兼容的代码。

Game类

Game类包含地图和游戏逻辑。有下列函数:
地图相关:
is_food_cell表示是否是食物格子;is_obstacle表示是否是障碍物;is_snake_cell表示是否是蛇;is_valid_cell表示格子是否可以走;get_cell_type表示获取格子的类型,是空白还是障碍还是蛇等;set_cell_type表示修改格子,为原来的updateMap函数;output函数输出地图到控制台;scale表示地图面积;width函数表示地图宽度;height函数表示地图高度。
游戏逻辑相关:
is_win表示是否通关;is_game_over表示是否游戏结束;score表示游戏分数;set_game_over表示设置游戏结束;set_score表示修改游戏分数。

Snake类

Snake类包含蛇的相关逻辑,有下列函数:
body表示获取蛇的所有部分的坐标;head表示获取蛇头坐标;tail表示获取蛇尾坐标;length表示获取蛇长;move表示蛇向某个方向移动一格。

SnakeAI类

SnakeAI类是本篇博客要介绍的内容,由下列函数:
build表示初始化哈密尔顿回路;build_path表示通过移动方向序列推导出移动过程各位置坐标;decide_next表示下一步移动的决策;distance表示两个点在哈密尔顿回路中的距离;find_maximum_path表示寻找地图中两点的最长路径;find_minimum_path表示寻找地图中两点的最短路径。

其他的一下函数

操作系统相关的函数请参考上一篇博客。

智能蛇

首先实现一个贪吃蛇的AI,我们可以采取什么样的方式呢?我们第一想到的就是求出蛇到食物的最短路径然后直接走过去,如果我们尝试这么做的结果就是蛇很快就死了,因为蛇自己很容易缠在一起然后蛇头与食物就被分到两个分量内了,当然遇到这种情况可以游荡直到蛇头和食物间连通。
但是我们观察了一些网上贪吃蛇AI的gif图后发现,如果我们让蛇一直贴着墙(障碍物,蛇身体)走,那么蛇本身就基本不会圈出好几片不连通的区域,这样我们不管食物蛇也会自己走到食物上的。

求最长路

当然这就需要我们写一个求哈密尔顿回路(为什么请参考下一小节决策)的算法。首先最短路算法很容易实现,一个简单的广度优先搜索(BFS)即可实现。当我们有了一条最短路后,我们就可以通过调整最短路来达到最长路。

怎么个调整呢,比如从(1,5)开始,到(1,1)(坐标的第一个数字表示第几行,第二个数字表示第几列)的最短路是S,A,A,A,A,A,W(S表示向下走,A表示向左走,D表示向右走,W表示向上走),那么第一个S我们就可以调整成D,S,A,如果S的右边的格子和右下方的格子都是空白格子的话。如果地图是6*6的,我们,现在调整后我们先走到右边界,再走回左边界,再回到(1,1)。我们再扩展一次,我们发现第一个A(紧跟在D,S后的那个A)可以调整成S,A,W,然后这个新的第一个A又可以调整成S,A,W,不断地调整后路径就变成L型了,还是贴着墙走。然后再扩展我们找到第一个在拐角的W,我们发现可以调整成A,W,D。。。以此类推,我们发现这样就可以将最短路径扩展成一个最长路,具体的实现请参见find_maximum_path。请读者在草稿纸上多试几次模拟上述过程以便理解。

注意到我们做出来的这个路径加上从(1,1)直接走到(1,5)的路径就成为一个环,而且是完全覆盖整个地图的,我们知道这样的环叫做哈密尔顿回路。当然我们没有必要从(1,5)开始,我们一开始的路径可以是(1,1)到(1,1)的。

决策

我们之前求出这样的最长路,是一个先绕一个方向,后再绕另一个方向绕圈的一个路径,事实上如果一条蛇沿着这个哈密尔顿回路一直走下去,那么游戏必赢(想想为什么?)。好吧其实理解起来也不会很困难吧,蛇身各个格子都一定在哈密尔顿回路上,因此蛇沿着这条路走,实际上全地图每个格子都会路过,那么食物格子必经过,又蛇各个格子都在回路上,因此蛇一定不会吃到自己。到这里实际上我们到此整体的思路就结束了。

为了加快程序,我们可以在最开始蛇比较短的时候走最短路径加快程序的速度(毕竟沿着哈密尔顿回路走的速度太慢了,每次吃食物最坏情况就是遍历完整个图,蛇步数最坏 O((nm)2) )。

源代码

编译时请开启C++11。
原来是写在好几个文件里的,为了方便大家编译测试,这里就把所有代码合并在一起了。。

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <queue>
#include <climits>
#include <stdexcept>
#include <algorithm>

#if defined(WIN32) || defined(_WIN32)
#include <windows.h>
#include <conio.h>

// 设置光标位置到(x, y)
void setCursor(int x, int y) {
    COORD c;
    c.X = x;
    c.Y = y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), c);
}

// 清空屏幕,以便重新打印地图
void clearScreen() {
    system("cls");
}

void sleepProgram(int ms) {
    Sleep(ms);
}

char getKey() {
    if (_kbhit()) return _getch();
    else return -1;
}
#else
#include <termio.h>
#include <unistd.h>
#include <fcntl.h>

// 设置光标位置到(x, y)
void setCursor(int x, int y) {
    printf("\033[%d;%dH", y + 1, x + 1);
}

// 清空屏幕,以便重新打印地图
void clearScreen() {
    printf("\033[2J");
}

// Linux下实现Windows的getch函数
int _getch() {
    struct termios tm, tm_old;
    int fd = 0, ch;

    if (tcgetattr(fd, &tm) < 0) {//保存现在的终端设置
        return -1;
    }

    tm_old = tm;
    cfmakeraw(&tm);//更改终端设置为原始模式,该模式下所有的输入数据以字节为单位被处理
    if (tcsetattr(fd, TCSANOW, &tm) < 0) {//设置上更改之后的设置
        return -1;
    }

    ch = getchar();
    if (tcsetattr(fd, TCSANOW, &tm_old) < 0) {//更改设置为最初的样子
        return -1;
    }

    return ch;
}

char getKey() {
    fcntl(0, F_SETFL, O_NONBLOCK);
    return _getch();
}

void sleepProgram(int ms) {
    usleep(ms * 1000);
}
#endif

void putCharAt(char newChar, int x, int y) {
    setCursor(x, y);
    putchar(newChar);
}

// 坐标位移, 下标:左0,右1,上2,下3
// 表示从屏幕左上角为原点,向下为x轴,向右为y轴。
// dx 表示方向为下标i的时候的x轴位移
const int dx[] = { 0, 0, -1, 1 };
// dy 表示方向为下标i的时候的y轴位移
const int dy[] = { -1, 1, 0, 0 };
const char ESCAPE = 27; // ESC 按键的ASCII码

                        // 随机一个[0,n)的数字
int randomN(int n) {
    return (int)(rand() * 1.0 / RAND_MAX * n);
}

template<typename T>
T **new_array(int row, int col) {
    T **res = new T*[row];
    for (int i = 0; i < row; ++i)
        res[i] = new T[col];
    return res;
}

template<typename T>
void delete_array(T **array, int row, int col) {
    for (int i = 0; i < row; ++i)
        delete[] array[i];
    delete[] array;
}

enum Direction {
    WEST = 0, EAST, NORTH, SOUTH
};

Direction negative(Direction d) {
    switch (d) {
    case WEST: return EAST;
    case EAST: return WEST;
    case NORTH: return SOUTH;
    case SOUTH: return NORTH;
    default: throw std::invalid_argument("Unrecognized direction " + d);
    }
}

struct Point {
    int x, y;

    Point(int _x, int _y) : x(_x), y(_y) {}

    Point to(Direction d) {
        return Point(x + dx[d], y + dy[d]);
    }

    Point from(Direction d) {
        return to(negative(d));
    }

    Point to(Point dir) {
        return Point(x + dir.x, y + dir.y);
    }

    bool operator==(const Point &b) const {
        return x == b.x && y == b.y;
    }

    bool operator!=(const Point &b) const {
        return x != b.x || y != b.y;
    }
};

enum MapCell {
    FOOD_CELL = '$', // 表示地图中的食物格子
    SNAKE_HEAD = 'H', // 表示地图中蛇的头
    SNAKE_BODY = 'X', // 表示地图中蛇的身体
    EMPTY_CELL = ' ', // 表示地图中的空格子
    BORDER_CELL = '*' // 表示地图的边界
};

class Game {
    int _width, _height;
    MapCell **map;
    std::vector<Point> empty;
    Point food; // 食物位置

    int gameOver = 0, win = 0, _score = 0; // 游戏是否结束,是否胜利

public:

    Game(int w, int h) : _height(h), _width(w), food(0, 0) {
        int i, j;
        map = new_array<MapCell>(h + 2, w + 2);
        for (i = 0; i <= h + 1; ++i) {
            for (j = 0; j <= w + 1; ++j) {
                if (i == 0 || j == 0 || j == w + 1 || i == h + 1)
                    map[i][j] = BORDER_CELL;
                else {
                    map[i][j] = EMPTY_CELL;

                    // 记录空格子
                    empty.push_back(Point(i, j));
                }
            }
        }
    }

    ~Game() {
        delete_array(map, _height + 2, _width + 2);
    }

    int width() { return _width; }
    int height() { return _height; }
    int scale() { return _width * _height; }
    int score() { return _score; }
    bool is_game_over() { return gameOver; }
    void set_game_over() { gameOver = true; }
    bool is_win() { return win; }

    // 判断(x,y)是否是食物格子
    bool is_food_cell(const Point &p) {
        return p.x == food.x && p.y == food.y;
    }

    // 判断(x,y)是否界外
    bool is_out_of_bound(const Point &p) {
        return p.x < 1 || p.x > _height || p.y < 1 || p.y > _width;
    }

    bool is_obstacle(const Point &p) {
        return map[p.x][p.y] == BORDER_CELL;
    }

    // 判断(x,y)是不是蛇
    bool is_snake_cell(const Point &p) {
        return map[p.x][p.y] == SNAKE_BODY || map[p.x][p.y] == SNAKE_HEAD;
    }

    bool is_valid_cell(const Point &p) {
        return !is_out_of_bound(p) && !is_obstacle(p) && !is_snake_cell(p);
    }

    // 更新地图,顺便更新屏幕
    void set_cell_type(const Point &p, MapCell newChar) {
        int x = p.x, y = p.y;
        // 维护空格子
        // 如果少了一个空格子
        if (map[x][y] == EMPTY_CELL && newChar != EMPTY_CELL) {
            for (auto it = empty.begin(); it != empty.end(); ++it)
                if (*it == p) {
                    empty.erase(it);
                    break;
                }
        }
        else if (map[x][y] != EMPTY_CELL && newChar == EMPTY_CELL) {
            // 如果多一个空格子
            empty.push_back(Point(x, y));
        }

        map[x][y] = newChar;

        // 更新屏幕
        putCharAt(newChar, y, x);
    }

    // 输出地图
    void output() {
        for (int i = 0; i <= _height + 1; ++i) {
            for (int j = 0; j <= _width + 1; ++j)
                putchar(map[i][j]);
            putchar('\n');
        }
    }

    // 在地图中生成一个食物格子
    void generate_food() {
        // 不断地随机位置直到找到一个空格子
        food = empty[randomN(empty.size())];

        // 更新地图的(foodX, foodY)。
        set_cell_type(food, FOOD_CELL);
    }

    Point food_cell() { return food; }

    bool set_score(int new_score) {
        _score = new_score;
        if (_score >= scale()) {
            win = true;
            return true;
        }
        return false;
    }

    MapCell get_cell_type(const Point &p) {
        return map[p.x][p.y];
    }
};

class Snake {
    std::deque<Point> snake;
public:
    Game * game;

    Snake(Game *g, int initialLength) : game(g) {
        if (initialLength > g->width())
            throw std::out_of_range("Snake length > map width");

        for (int i = 1; i <= initialLength; ++i) {
            snake.push_back(Point(1, initialLength - i + 1));
            if (i > 1)
                game->set_cell_type(snake.back(), SNAKE_BODY);
        }
        game->set_cell_type(snake.front(), SNAKE_HEAD);
    }

    void move(Direction dir) {
        // 将要到的格子
        Point np = snake.front().to(dir);

        // 如果不是食物格子
        if (!game->is_food_cell(np)) {
            // 如果下一步出界或走到了蛇的身体,则游戏结束
            if (!game->is_valid_cell(np)) {
                game->set_game_over();
                return;
            }

            // 否则蛇前进一格
            game->set_cell_type(snake.back(), EMPTY_CELL);
            snake.pop_back();
        }
        else {
            // 如果蛇占满了地图,说明游戏完成,结束
            if (game->set_score(snake.size() + 1)) {
                return;
            }
            else { // 否则继续生成食物
                game->generate_food();
            }
        }

        // 如果蛇不止1格,将原来的H置为X
        if (snake.size() > 1)
            game->set_cell_type(snake.front(), SNAKE_BODY);
        // 更新蛇的头
        game->set_cell_type(np, SNAKE_HEAD);
        snake.push_front(np);
    }

    int length() { return snake.size(); }
    Point head() { return snake.front(); }
    Point tail() { return snake.back(); }
    const std::deque<Point> &body() { return snake; }
};

class SnakeAI {
    Game *game;
    Snake *snake;

    struct Node {
        int idx, dis;
        Direction fromDir;
        bool vis;
    } **nodes;
    Direction dir;

    std::deque<Direction> find_minimum_path_to(const Point &goal) {
        std::deque<Direction> path;
        MapCell original = game->get_cell_type(goal);
        game->set_cell_type(goal, EMPTY_CELL);
        find_minimum_path(snake->head(), goal, path);
        game->set_cell_type(goal, original); // restore
        return path;
    }

    std::deque<Direction> find_maximum_path_to(const Point &goal) {
        std::deque<Direction> path;
        MapCell original = game->get_cell_type(goal);
        game->set_cell_type(goal, EMPTY_CELL);
        find_maximum_path(snake->head(), goal, path);
        game->set_cell_type(goal, original); // restore
        return path;
    }

    int distance(int from, int to, int size) {
        return from < to ? to - from : to + size - from;
    }

    Node &node(const Point &p) { return nodes[p.x][p.y]; }

    void find_minimum_path(const Point &src, const Point &dst, std::deque<Direction> &path) {
        int row = game->height(), col = game->width();

        for (int i = 1; i <= row; ++i)
            for (int j = 1; j <= col; ++j)
                nodes[i][j].dis = INT_MAX;

        path.clear();
        node(src).dis = 0;

        std::queue<Point> q;
        q.push(src);

        // bfs
        while (!q.empty()) {
            Point u = q.front();
            q.pop();

            if (u == dst) {
                build_path(src, dst, path);
                break;
            }

            Direction dirs[] = { EAST, WEST, NORTH, SOUTH };
            std::random_shuffle(dirs, dirs + 4);

            Direction best = u == src ? dir : node(u).fromDir;
            for (int i = 0; i < 4; ++i) {
                Point v = u.to(dirs[i]);
                if (game->is_valid_cell(v) && best == dirs[i]) {
                    std::swap(dirs[0], dirs[i]);
                    break;
                }
            }

            for (int i = 0; i < 4; ++i) {
                Point v = u.to(dirs[i]);
                if (game->is_valid_cell(v) && node(v).dis == INT_MAX) {
                    node(v).fromDir = dirs[i];
                    node(v).dis = node(u).dis + 1;
                    q.push(v);
                }
            }
        }
    }

    void find_maximum_path(const Point &from, const Point &to, std::deque<Direction> &path) {
        find_minimum_path(from, to, path);

        for (int i = 1; i <= game->height(); ++i)
            for (int j = 1; j <= game->width(); ++j)
                nodes[i][j].vis = false;

        Point u = from;
        node(u).vis = true;
        for (const Direction &d : path) {
            u = u.to(d);
            node(u).vis = true;
        }

        for (auto it = path.begin(); it != path.end(); ) {
            if (it == path.begin())
                u = from;
            bool extended = false;
            Direction dir = *it, d;
            Point v = u.to(dir);
            switch (dir) {
            case NORTH: case SOUTH: d = WEST; break; // vertical to horizontal
            case WEST: case EAST: d = NORTH; break; // horizontal to vertical
            }

            for (int k = 0; k < 2; ++k, d = negative(d)) { // Try d first, try ~d later.
                Point cur = u.to(d), next = v.to(d);
                if (game->is_valid_cell(cur) && game->is_valid_cell(next) &&
                    !node(cur).vis && !node(next).vis) {
                    node(cur).vis = node(next).vis = true;
                    it = path.erase(it);
                    it = path.insert(it, negative(d));
                    it = path.insert(it, dir);
                    it = path.insert(it, d);
                    it = path.begin();
                    extended = true;
                    break;
                }
            }

            if (!extended) {
                ++it; u = v;
            }
        }

    }

    void build_path(const Point &from, const Point &to, std::deque<Direction> &path) {
        Point now = to;
        while (now != from) {
            Point parent = now.from(node(now).fromDir);
            path.push_front(node(now).fromDir);
            now = parent;
        }
    }

    void build() {
        std::deque<Direction> maxPath = find_maximum_path_to(snake->tail());

        int x = 0;
        for (auto it = snake->body().crbegin(); it != snake->body().crend(); ++it)
            node(*it).idx = x++;

        int size = game->scale();
        Point u = snake->head();
        for (const Direction &d : maxPath) {
            Point v = u.to(d);
            node(v).idx = (node(u).idx + 1) % size;
            u = v;
        }
    }

public:
    SnakeAI(Game *g, Snake *s) : game(g), snake(s) {
        nodes = new_array<Node>(g->height() + 2, g->width() + 2);

        build();
    }

    ~SnakeAI() {
        delete_array(nodes, game->height() + 2, game->width() + 2);
    }

    void decide_next() {
        if (game->is_game_over())
            return;

        int size = game->scale();
        Point head = snake->head(), tail = snake->tail();
        int headIndex = node(head).idx;
        int tailIndex = node(tail).idx;

        // Try to take shortcuts when the snake is not long enough
        if (snake->length() < game->scale() * 3 / 4) {
            std::deque<Direction> minPath = find_minimum_path_to(game->food_cell());

            if (!minPath.empty()) {
                Direction nextDir = minPath.front();
                Point nextPos = head.to(nextDir);
                int nextIndex = node(nextPos).idx;
                int foodIndex = node(game->food_cell()).idx;
                headIndex = distance(tailIndex, headIndex, size);
                nextIndex = distance(tailIndex, nextIndex, size);
                foodIndex = distance(tailIndex, foodIndex, size);

                if (nextIndex > headIndex && nextIndex <= foodIndex) {
                    dir = nextDir;
                    return;
                }
            }
        }

        // Move along the hamilton cycle
        headIndex = node(head).idx;
        for (Direction d = WEST; d <= SOUTH; d = (Direction)(d + 1)) {
            Point adj = head.to(d);
            if (!game->is_valid_cell(adj))
                continue;
            if (node(adj).idx == (headIndex + 1) % size)
                dir = d;
        }
    }

    Direction get_direction() {
        return dir;
    }
};

int main() {
    int n, m;
    Direction d = WEST;
    char ch;

    // 初始化随机种子
    srand(time(0));

    // 输入游戏地图的规模
    do {
        std::cout << "Please enter the size of map, height first, width second: ";
        std::cin >> n >> m;

        if (n < 2 || m < 2 || n > 254 || m > 254) {
            std::cout << "Your input is not valid, 5 <= n <= 254, 5 <= m <= 254\n";
        }
        else {
            break;
        }
    } while (1);

    // 初始化地图、蛇
    Game *game = new Game(m, n);
    Snake *snake = new Snake(game, 5);
    game->generate_food();
    SnakeAI *ai = new SnakeAI(game, snake);

    // 输出最开始的地图
    clearScreen();
    game->output();

    bool autogo = true;
    bool enableAI = true;

    while (!game->is_game_over()) {
        if (autogo) {
            if (enableAI) {
                sleepProgram(30);
                ai->decide_next();
                switch (ai->get_direction()) {
                case WEST: ch = 'A'; break;
                case EAST: ch = 'D'; break;
                case NORTH: ch = 'W'; break;
                case SOUTH: ch = 'S'; break;
                }
            }
            else {
                sleepProgram(500);
                char newch = getKey();
                if (newch != -1)
                    ch = newch;
            }
        }
        else
            ch = _getch();

        if (ch >= 'a' && ch <= 'z') {
            ch = ch - 'a' + 'A';
        }

        // 判断当前的按键
        switch (ch) {
            // 如果是方向按键,记录方向
        case 'A': d = WEST; break;
        case 'D': d = EAST; break;
        case 'W': d = NORTH; break;
        case 'S': d = SOUTH; break;
            // 如果是ESC,则退出游戏。
        case ESCAPE: return 0;
            // 如果是不正确的按键,则跳过重试
        default: continue;
        }
        snake->move(d);

        setCursor(0, n + 2);

        // 如果游戏结束且失败,输出最终分数
        if (game->is_game_over()) {
            printf("Game Over! Your final score is %d.\n", game->score());
            break;
        }

        // 如果游戏结束且胜利
        if (game->is_win()) {
            printf("Congraulations!\n");
            break;
        }
    }

    delete ai;
    delete snake;
    delete game;

    return 0;
}

猜你喜欢

转载自blog.csdn.net/huanghongxun/article/details/78885872