Microbit 游戏编程: 不会吃胖的贪食蛇 (自带人工智能)


上周, 我们介绍了简单的人工智能, 让Microbit玩接苹果游戏 (Microbit 编程: 简易人工智能让电脑玩游戏). 这周, 我们将设计一个简易的贪食蛇游戏, 并且让电脑有AI自己能玩(对的, 让我们看它玩).

经典的贪食蛇游戏在手机洛基亚时代得到了推广, 当时还不是智能手机, 在黑白的像素点的小屏幕上贪食蛇游戏是最适合不过的了. 不过, 贪食蛇游戏一般会让你能按4个方向键, 很直觉的操作, 而这周, 我们将设计一个简易的贪食蛇游戏, 主要有下面两点:

  • 我们这次的贪食蛇光吃不长胖, 也就是身体不会变长.
  • 由于Microbit只有两个键 A 和 B, 我们需要将操作改成 A 向左, B向右. 方向是相对于当前蛇的行走方向.

这么设计一来, 这游戏还是相当锻炼脑子开发智力的, 因为人需要时刻的去站在蛇的角度去想向左还是向右转.

这个Microbit游戏同样需要用到两个精灵对象: 蛇和苹果.

定义游戏中用到的全局变量

我们需要定放几个变量:

1
2
3
4
5
6
7
let direction = 0;
let dxOffset = [[1, 0], [0, 1], [-1, 0], [0, -1]];
let score = 0;
let px = 0;
let py = 0;
let snake = game.createSprite(px, py);
let apple = game.createSprite(2, 2);
let direction = 0;
let dxOffset = [[1, 0], [0, 1], [-1, 0], [0, -1]];
let score = 0;
let px = 0;
let py = 0;
let snake = game.createSprite(px, py);
let apple = game.createSprite(2, 2);

方向 direction 变量存放的是一个0到3的整数, 也就是4个方向:

  • 0: 右
  • 1: 下
  • 2: 左
  • 3: 上

方向被定义为顺时针方向(但是, 您也可以将其设为逆时针方向). 要改变方向(例如向左转), 我们需要知道当前的方向偏移量, 该方向偏移量是一个包含四组坐标偏移量的数组-X和Y.

例如, 当direction = 0时, 这表示向右. dxOffset[direction]取得值为数组 [1, 0], 也就是下一个像素点是 X + 1和Y + 0, 即其在右边的像素点.

我们定义Score来存储蛇当前吃掉的苹果数量. 坐标pxpy存储当前蛇所在的坐标位置. snakeapple是Microbit LED上的两个Sprite精灵对象.

放置下一个苹果

当蛇吃了一个苹果时, 我们需要放置下一个苹果. 我们可以生成两个0到4(含)之间的随机整数. 但是, 我们需要确保不要将苹果放在蛇身上. 我们可以使用以下Javascript函数来执行此操作:

1
2
3
4
5
6
7
8
9
10
function placeNextApple() {
    let x = Math.randomRange(0, 4);
    let y = Math.randomRange(0, 4);
    while (x == snake.x() && y == snake.y()) {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    }
    apple.goTo(x, y);
    apple.setBrightness(100);
}
function placeNextApple() {
    let x = Math.randomRange(0, 4);
    let y = Math.randomRange(0, 4);
    while (x == snake.x() && y == snake.y()) {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    }
    apple.goTo(x, y);
    apple.setBrightness(100);
}

在这里, 当我们在x和y中生成随机数时, 我们使用Sprite.goTo()方法让苹果移动到该位置. 为了区分蛇和苹果像素, 我们将苹果像素的亮度更改为100(从0到255, 其中255最亮).

正如您在上面看到的, 我们首先生成x和y, 如果它刚好在蛇上时使用while循环. 有一些代码重复, 生成x和y作为随机整数的代码出现两次. 我们可以使用do-while循环来重写上述逻辑, 使代码更简单明了了.

1
2
3
4
5
6
7
8
9
function placeNextApple() {
    let x, y;
    do {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    } while ((x == snake.x()) && (y == snake.y()))
    apple.goTo(x, y);
    apple.setBrightness(100);
}
function placeNextApple() {
    let x, y;
    do {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    } while ((x == snake.x()) && (y == snake.y()))
    apple.goTo(x, y);
    apple.setBrightness(100);
}

在生成apple对象后, 我们可以立马调用 placeNextApple 函数来使苹果出现在一个随机的位置上.

蛇的三种动作

作为蛇, 它在当前状态下可以做三件事: 什么都不做(向前移动), 向左转或向右转. 要向右转, 我们需要更改方向变量 direction -将其移到它旁边 (下一个) – 这是顺时针方向.

1
2
3
function turnRight() {
    direction = (direction + 1) % 4;
}
function turnRight() {
    direction = (direction + 1) % 4;
}

模运算符确保它不断循环. 例如, 当direction = 3(向上时)时, 其右方向为0, 即右边. 同样, 这是使蛇向左转的Javascript函数.

1
2
3
function turnLeft() {
    direction = (direction + 3) % 4;
}
function turnLeft() {
    direction = (direction + 3) % 4;
}

游戏功能: 游戏结束和重置游戏

这是结束游戏时需要用到的两个辅助函数(当蛇移出LED屏幕时)

1
2
3
4
5
6
function gameOver() {
    game.setScore(score);
    game.pause();
    basic.pause(1000);
    game.gameOver();
}
function gameOver() {
    game.setScore(score);
    game.pause();
    basic.pause(1000);
    game.gameOver();
}

重新游戏需要重置设置分数等.

1
2
3
4
5
6
7
8
9
10
function resetGame() {
    game.setScore(0);
    score = 0;
    direction = 0;    
    px = 0;
    py = 0;
    snake.goTo(px, py);
    placeNextApple();
    game.resume();
}
function resetGame() {
    game.setScore(0);
    score = 0;
    direction = 0;    
    px = 0;
    py = 0;
    snake.goTo(px, py);
    placeNextApple();
    game.resume();
}

蛇向前爬行

我们需要将方向偏移量应用于当前坐标pxpy.

1
2
3
4
5
6
7
8
function moveForward() {
    let dx = dxOffset[direction];
    px += dx[0];
    py += dx[1];
    if (!validPixelCoordinate(px, py)) {
        gameOver();
    }
}
function moveForward() {
    let dx = dxOffset[direction];
    px += dx[0];
    py += dx[1];
    if (!validPixelCoordinate(px, py)) {
        gameOver();
    }
}

我们使用一个函数来检查一个位置坐标是否有效(是否在LED之内):

1
2
3
function validPixelCoordinate(nx: Number, ny: Number): boolean {
    return (nx >= 0 && nx <= 4 && ny >= 0 && ny <= 4);
}
function validPixelCoordinate(nx: Number, ny: Number): boolean {
    return (nx >= 0 && nx <= 4 && ny >= 0 && ny <= 4);
}

坐标nxny仅在[0, 4]范围内时有效.

游戏控制

这似乎是最简单的部分. A左转, B右转. AB一起按则重置游戏.

1
2
3
4
5
6
7
8
9
10
11
input.onButtonPressed(Button.A, function () {
    turnLeft();
})
 
input.onButtonPressed(Button.B, function () {
    turnRight();
})
 
input.onButtonPressed(Button.AB, function () {
    resetGame();
})
input.onButtonPressed(Button.A, function () {
    turnLeft();
})

input.onButtonPressed(Button.B, function () {
    turnRight();
})

input.onButtonPressed(Button.AB, function () {
    resetGame();
})

这里用到了上面定义了这三个函数: turnLeft, turnRightresetGame.

主游戏循环

将所有这些细节放在一起就完成了游戏的设计. 为了使越来越快地移动, 当蛇每吃一个苹果则减少了延迟, 并将阈值设置为最小100毫秒.

1
2
3
4
5
6
7
8
9
10
11
12
13
basic.forever(function () {
    if (game.isGameOver()) {
        return;
    }
    let delay = Math.max(100, 1000 - score * 50);
    basic.pause(delay);
    moveForward();
    snake.goTo(px, py);
    if (snake.isTouching(apple)) {
        score++;
        placeNextApple();
    }
})
basic.forever(function () {
    if (game.isGameOver()) {
        return;
    }
    let delay = Math.max(100, 1000 - score * 50);
    basic.pause(delay);
    moveForward();
    snake.goTo(px, py);
    if (snake.isTouching(apple)) {
        score++;
        placeNextApple();
    }
})

如果蛇击中(吃掉)苹果(通过isTouching方法来判断碰撞检测), 我们将增加分数并放置下一个苹果.

贪食蛇游戏(版本1)和Microbit模拟器: https://makecode.microbit.org/_2eE1EseWyFs5

具有人工智能的贪食蛇

我们让计算机在Microbit上玩这个蛇游戏. 电脑永远不会犯错或累. 蛇很容易做出AI策略. 它只需要计算三个动作的成本: 什么都不做, 左转或右转.

成本函数可以是苹果和蛇的下一个位置的距离. 可以计算X和Y的偏移量.

1
let dist = Math.abs(nextX - apple.x()) + Math.abs(nextY - apple.y());
let dist = Math.abs(nextX - apple.x()) + Math.abs(nextY - apple.y());

我们还需要确保下一个像素有效. 如果无效, 则费用将被简单地设置为非常大的数字, 例如 9999, 这样这种方案就不会被采取. 策略是采取最小的成本的行动. 在游戏的任何时刻, 贪食蛇至少有一项动作是有效的, 例如 当蛇在拐角处时, 只有一个方向是有效的(其余两个方向则是会让游戏结束的)

让我们计算三个方向的成本并选择最小的那一个方向 (这也就是贪心算法).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function letComputerPlay() {
    let x = snake.x();
    let y = snake.y();
 
    // 如果不改变方向 向前移动的成本
    let dx = dxOffset[direction];
    let nx1 = x + dx[0];
    let ny1 = y + dx[1];
    let dist1 = 9999;
    if (validPixelCoordinate(nx1, ny1)) {
        dist1 = Math.abs(nx1 - apple.x()) + Math.abs(ny1 - apple.y());
    }
 
    // 向右移动后的成本
    let dx1 = (direction + 1) % 4;
    dx = dxOffset[dx1];
    let nx2 = x + dx[0];
    let ny2 = y + dx[1];
    let dist2 = 9999;
    if (validPixelCoordinate(nx2, ny2)) {
        dist2 = Math.abs(nx2 - apple.x()) + Math.abs(ny2 - apple.y());
    }
 
    // 向左移动后的成本
    let dx2 = (direction + 3) % 4;
    dx = dxOffset[dx2];
    let nx3 = x + dx[0];
    let ny3 = y + dx[1];
    let dist3 = 9999;
    if (validPixelCoordinate(nx3, ny3)) {
        dist3 = Math.abs(nx3 - apple.x()) + Math.abs(ny3 - apple.y());
    }
 
    if (dist1 <= dist2 && dist1 <= dist3) {
        // 什么都不做让蛇向前移动就是当前最好的选择
        return;
    } else if (dist2 <= dist1 && dist2 <= dist3) {
        turnRight();
    } else if (dist3 <= dist1 && dist3 <= dist2) {
        turnLeft();
    }
}
function letComputerPlay() {
    let x = snake.x();
    let y = snake.y();

    // 如果不改变方向 向前移动的成本
    let dx = dxOffset[direction];
    let nx1 = x + dx[0];
    let ny1 = y + dx[1];
    let dist1 = 9999;
    if (validPixelCoordinate(nx1, ny1)) {
        dist1 = Math.abs(nx1 - apple.x()) + Math.abs(ny1 - apple.y());
    }

    // 向右移动后的成本
    let dx1 = (direction + 1) % 4;
    dx = dxOffset[dx1];
    let nx2 = x + dx[0];
    let ny2 = y + dx[1];
    let dist2 = 9999;
    if (validPixelCoordinate(nx2, ny2)) {
        dist2 = Math.abs(nx2 - apple.x()) + Math.abs(ny2 - apple.y());
    }

    // 向左移动后的成本
    let dx2 = (direction + 3) % 4;
    dx = dxOffset[dx2];
    let nx3 = x + dx[0];
    let ny3 = y + dx[1];
    let dist3 = 9999;
    if (validPixelCoordinate(nx3, ny3)) {
        dist3 = Math.abs(nx3 - apple.x()) + Math.abs(ny3 - apple.y());
    }

    if (dist1 <= dist2 && dist1 <= dist3) {
        // 什么都不做让蛇向前移动就是当前最好的选择
        return;
    } else if (dist2 <= dist1 && dist2 <= dist3) {
        turnRight();
    } else if (dist3 <= dist1 && dist3 <= dist2) {
        turnLeft();
    }
}

然后, 我们只需要在主游戏循环调用letComputerPlay方法即可.

带有AI的Microbit模拟器的贪食蛇游戏: https://makecode.microbit.org/_7CJeJrEMv3Ts

下一周, 我们将设计吃了会长胖的贪食蛇游戏, 将会非常有意思也会非常有挑战!

以下是Microbit玩Snake游戏的视频. 非常擅长. 它不断进食, 永不疲劳… 有朋友说, 这是一个假的贪食蛇游戏, 分明是一个男孩爱上了一个女孩, 发生的爱情故事, 哈哈.

想立马玩贪吃蛇游戏?

英文: Microbit Programming: The Development of a Snake Eating Apple Game and AI (Version 1 – Snake Does Not Grow)

GD Star Rating
loading...
本文一共 1547 个汉字, 你数一下对不对.
Microbit 游戏编程: 不会吃胖的贪食蛇 (自带人工智能). (AMP 移动加速版本)
上一篇: Microbit 编程: 简易人工智能让电脑玩游戏
下一篇: 清理微信缓存和备份微信聊天记录

扫描二维码,分享本文到微信朋友圈
52bf57f12f5ec11fae6b4439a6f5341e Microbit 游戏编程: 不会吃胖的贪食蛇 (自带人工智能) Microbit 编程 技术 游戏 程序设计 计算机

评论