上周, 我们介绍了简单的人工智能, 让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来存储蛇当前吃掉的苹果数量. 坐标px和py存储当前蛇所在的坐标位置. snake和apple是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(); }
蛇向前爬行
我们需要将方向偏移量应用于当前坐标px和py.
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); }
坐标nx和ny仅在[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, turnRight和resetGame.
主游戏循环
将所有这些细节放在一起就完成了游戏的设计. 为了使蛇越来越快地移动, 当蛇每吃一个苹果则减少了延迟, 并将阈值设置为最小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游戏的视频. 非常擅长. 它不断进食, 永不疲劳… 有朋友说, 这是一个假的贪食蛇游戏, 分明是一个男孩爱上了一个女孩, 发生的爱情故事, 哈哈.
想立马玩贪吃蛇游戏?
loading...
上一篇: Microbit 编程: 简易人工智能让电脑玩游戏
下一篇: 清理微信缓存和备份微信聊天记录