原版贪吃蛇
自动寻路之深度优先搜索DFS
我试了一下BFS广度优先没有太大提升,还是有局限性,所以我就直接搞A*算法,它会有更好的权重评估。
1、移动策略
移动策略没什么变化,和上次一样
1、查找当前蛇到达食物的路线
if(有到达食物的路线){
临时计算到达食物后的蛇
if(到达食物后的蛇可以到达当前蛇尾) return 到达食物路线的下一步
}
2、如果上面未返回,查找蛇能到达当前蛇尾的下一步空位
if(有到达蛇尾的空位) return 距离食物最远的下一步空位
3、如果上面都未返回,那就是你的寻路算法有问题,按理来说上面的步骤是应该能返回的,最后兜底返回一个有效空位
2、直接代码上解析
我没有用以前的一维数组,改成了二维数组存位置,并封装了类,大致步骤没什么变化
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>贪吃蛇</title>
</head>
<body style="height: 90vh; display: flex;flex-direction: column; justify-content: center; align-items: center;">
<canvas id="canvas" width="400" height="400" style="background-color: black">对不起,您的浏览器不支持canvas</canvas>
<button id="btn">暂停</button>
<button id="btn1">开始</button>
<script>
class SnakeGame {
constructor(canvas, gZise = 20) {
this.ctx = canvas.getContext('2d');//画布
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
//例如,20X20,从0到399表示box里[0~19]*[0~19]的所有节点,每20px一个节点
this.gZise = gZise;//方形地图边长,一维数组表示每个方格位置
this.mLen = gZise * gZise;//总格数
this.snake = [[0, 0]]; // 初始位置
this.dirList = [[0, -1], [-1, 0], [0, 1], [1, 0]]; // 方向数组 左上右下
this.direction = this.dirList[2]; // 初始方向:向右
this.food = this.generateFood(); //生成食物
this.stop = false;//暂停
this.watchDirKey(); //键盘监听
this.draw(this.food, "yellow"); //画出食物
this.draw(this.snake[0], "blueviolet"); //画出蛇头
}
//根据位置画色块
draw(pos, color) {
if (!pos) return;
this.ctx.fillStyle = color;
this.ctx.fillRect(pos[0] * 20 + 1, pos[1] * 20 + 1, 18, 18);
}
//监听用户按键
watchDirKey() {
document.onkeydown = (evt) => { //监听方向
this.direction = this.dirList[(evt || event).keyCode - 37];
};
}
//生成食物
generateFood() {
if (this.snake.length == this.mLen) return; //已满,不生成食物
let food;
do {
food = [Math.floor(Math.random() * this.gZise), Math.floor(Math.random() * this.gZise)];
} while (this.snake.some(pos => this.equal(pos, food)));
return food;
}
//位置是否合法
isValid(pos, snake = this.snake) {
if (this.equal(snake[snake.length - 1], pos)) return true; //可以移到蛇尾位置
const [x, y] = pos;
return (
x >= 0 && x < this.gZise &&
y >= 0 && y < this.gZise &&
!snake.some(it => this.equal(it, pos))
);
}
//两点是否相等
equal(m, n) {
return m && n && m[0] === n[0] && m[1] === n[1];
}
//移动
move(pos = this.snake[0] + this.direction) {
//有传位置就使用,没有就按当前方向移动
if (!pos) pos = [this.snake[0][0] + this.direction[0], this.snake[0][1] + this.direction[1]];
if (!this.isValid(pos)) {
this.stop = true;
return alert("游戏结束!得分:" + this.snake.length);
}
this.draw(this.snake[0], "lime"); //覆盖原来的蛇头
this.snake.unshift(pos); //添加蛇头
if (this.equal(pos, this.food)) {
//如果吃到食物时,产生一个蛇身以外的随机的点,不会去掉蛇尾
this.food = this.generateFood();
this.draw(this.food, "yellow"); //画出食物
} else {
//没有吃到食物时正常移动,蛇尾出队列
this.draw(this.snake.pop(), "black"); //覆盖原来的蛇尾
}
this.draw(pos, "blueviolet"); //画出新蛇头
}
//玩家控制
playerMove() {
this.move();
this.stop || setTimeout(this.playerMove.bind(this), 400); //移动速度
}
//曼哈顿距离
hDist(m, n) {
return Math.abs(m[0] - n[0]) + Math.abs(m[1] - n[1]);
}
//获取周围位置
getNeighbors(pos) {
return this.dirList.map(dir => [pos[0] + dir[0], pos[1] + dir[1]]);
}
//移动一步(f是食物位置),返回新蛇数组
moveStep(sn, pos, f) {
let tmp = [pos, ...sn];
if (!this.equal(pos, f)) tmp.pop();
return tmp;
}
//获取start到current的路线
reconstructPath(cameFrom, current, start) {
const totalPath = [current];
while (cameFrom.has(current.toString())) {
current = cameFrom.get(current.toString());
if (current.toString() == start.toString()) return totalPath;
totalPath.unshift(current);
}
return totalPath;
}
//A*算法,到达goal的最优路线
aStarSearch(sn, goal, food) {
const openMap = new Map(); // 开放集合,存储待评估的节点
const cameFrom = new Map(); // 路径映射,记录每个节点的前驱节点
const gScore = new Map(); // 从起点到当前节点的实际成本(实际路线长度)
const fScore = new Map(); // 从起点到目标的估计成本(实际路线长度+估计剩余长度)
openMap.set(sn[0].toString(), sn);
gScore.set(sn[0].toString(), 0); // 起点的gScore为0
fScore.set(sn[0].toString(), this.hDist(sn[0], goal)); // 起点的 fScore 为启发式估计
while (openMap.size > 0) {
// 获取 fScore 最小的节点(最近的点)
let curStr = Array.from(openMap.keys()).reduce((a, b) => fScore.get(a) < fScore.get(b) ? a : b);
let curSn = openMap.get(curStr);
let current = curStr.split(',').map(Number);
openMap.delete(curStr); // 从开放集合中移除当前节点
if (this.equal(current, goal)) { // 如果到达目标
let path = this.reconstructPath(cameFrom, current, sn[0]); // 重建路径并返回
//如果食物存在,本次搜索是移向食物路线,即goal==food
if (food) {
//从吃到食物状态开始搜索,是否能到达当前蛇尾
let tmp = [...path].reverse();
tmp = [...tmp, ...sn].splice(0, sn.length + 1);
if (this.aStarSearch(tmp, tmp[tmp.length - 1])) return path[0]; //若能到达蛇尾,返回路线第一步
//若不能到达蛇尾,当前路线行不通,删除当前位置(目标位置)的记录,继续搜索其他路线
cameFrom.delete(curStr);
gScore.delete(curStr);
fScore.delete(curStr);
continue;
} else {
//如果食物不存在,即搜索的是蛇尾路线,返回结果
return path[0];
}
}
for (const neighbor of this.getNeighbors(current)) { // 遍历当前节点的邻居
if (!this.isValid(neighbor, curSn)) continue; // 如果邻居无效,跳过
const nStr = neighbor.toString();
const curNeiGScore = gScore.get(curStr) + 1; // 计算当前 gScore(路线长度加一)
const prevGScore = gScore.get(nStr) || Infinity; //上次此位置的gScore(为空时,设置为Infinity以便比较后赋值)
if (curNeiGScore < prevGScore) { //当前位置距离小于上次此位置(更优的位置)
cameFrom.set(nStr, current); // 记录前驱节点
gScore.set(nStr, curNeiGScore); // 更新 gScore
fScore.set(nStr, curNeiGScore + this.hDist(neighbor, goal)); // 更新 fScore
openMap.set(nStr, this.moveStep(curSn, neighbor)); // 将邻居添加到开放集合
}
}
}
}
//查找最优下一步
findNext(sn, food) {
if (food === undefined) return sn[sn.length - 1]; //已满,衔尾蛇
let lns0 = this.getNeighbors(sn[0]).filter(v => this.isValid(v, sn));//蛇头周围点
if (lns0.length == 0) return sn[0] - sn[1] + sn[0];//当前方向下一位置
if (lns0.length == 1) return lns0[0]; //只有一个空位
//查找食物路线
let f_next = this.aStarSearch(sn, food, food);
if (f_next) return f_next;
//查找食物路线失败,移向蛇尾
lns0.sort((a, b) => this.hDist(b, food) - this.hDist(a, food)); //从离食物最远的位置找起
for (const neighbor of lns0) {
let tmp = this.moveStep(sn, neighbor, food);
let tail_next = this.aStarSearch(tmp, tmp[tmp.length - 1]);
if (tail_next) return neighbor;
}
return lns0[0];
}
// 自动寻路控制
autoMove() {
this.move(this.findNext(this.snake, this.food));
this.stop || setTimeout(this.autoMove.bind(this), 50); //移动速度
}
}
</script>
<script>
var game;
document.getElementById('btn').onclick = () => {
console.log(game.snake, game.food);
game.stop = !game.stop;
game.stop || game.autoMove();
}
document.getElementById('btn1').onclick = () => {
game = new SnakeGame(document.getElementById('canvas'));
game.autoMove();
}
</script>
</body>
</html>
3、算法评估
- 1、算法的核心就是:搜索算法中通过评估权重来决定最优位置,以及移动策略。
- 2、这次的算法测试中偶尔能够铺满地图,不能100%通关,测试一次太花时间了。
- 3、因为是最短路径,所以经常斜向移动,导致后期地图上会留下很多单独的空位,如果食物生成在这些位置,就会花费很长时间去绕路,还很有可能根本吃不到。
- 4、如果有大佬有更优策略,欢迎留言!!!
4、无敌赖皮策略
除了老老实实搞寻路算法,还有一个不需要算法就能通关的策略,就是让蛇移动在固定路线上,循环走过地图的每个位置。
所以,最后我就再加了一个巡航路线,可以节省后期移动时间,我就只展示一下附加的一些代码。
class SnakeGame {
constructor(canvas, gZise = 20) {
this.ctx = canvas.getContext('2d');//画布
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
//例如,20X20,从0到399表示box里[0~19]*[0~19]的所有节点,每20px一个节点
this.gZise = gZise;//方形地图边长,一维数组表示每个方格位置
this.mLen = gZise * gZise;//总格数
this.snake = [[0, 0]]; // 初始位置
this.cruisePath = this.getCruisePath(gZise); //生成固定巡航路线(记录每个位置要移动的下一位置)
this.dirList = [[0, -1], [-1, 0], [0, 1], [1, 0]]; // 方向数组 左上右下
this.direction = this.dirList[2]; // 初始方向:向右
this.food = this.generateFood(); //生成食物
this.stop = false;//暂停
this.watchDirKey();
this.draw(this.food, "yellow"); //画出食物
this.draw(this.snake[0], "yellow"); //画出蛇头
}
//生成巡航路线
getCruisePath(size) {
let total = size * size;
let path = new Map(); //记录当前位置的下一位置
let cur = [0, 0], curStr; //从原点开始
let dir = [1, 0];//初始方向,向右
while (path.size < total) {
curStr = cur.toString();
let x = cur[0] + dir[0], y = cur[1] + dir[1];
if (x == size) {
//向右越界
x = cur[0], y = cur[1] + 1; //向下一格
dir = [-1, 0]; //方向改为向左
} else if (x == 0) {
//留出最左边为安全线
if (dir[0] == -1 && dir[1] == 0) {
//方向向左
if (cur[1] + 1 == size) {
//已到底
x = cur[0] - 1, y = cur[1]; //向左一格
dir = [0, -1]; //方向改为向上
} else {
x = cur[0], y = cur[1] + 1; //向下一格
dir = [1, 0]; //方向改为向右
}
}
}
path.set(curStr, [x, y]); //记录
cur = [x, y];
}
return path;
}
// ...........省略其他代码
//查找最优下一步
findNext(sn, food) {
if (food === undefined) return sn[sn.length - 1];
let lns0 = this.getNeighbors(sn[0]).filter(v => this.isValid(v, sn));//蛇头周围点
if (lns0.length == 0) return sn[0] - sn[1] + sn[0];//当前方向下一位置
if (lns0.length == 1) return lns0[0];
// 3/4进度时开启优先巡航路线
if (sn.length >= parseInt(this.mLen * 3 / 4)) {
//在保证可以到达蛇尾的情况下,慢慢纠正路线到巡航路线上
let c_next = this.cruisePath.get(sn[0].toString());
if (lns0.find(p => p[0] == c_next[0] && p[1] == c_next[1])) {
let tmp = this.moveStep(sn, c_next, food);
let tail_next = this.aStarSearch(tmp, tmp[tmp.length - 1]);
if (tail_next) return c_next;
}
}
//查找食物路线
let f_next = this.aStarSearch(sn, food, food);
if (f_next) return f_next;
//查找食物路线失败,移向蛇尾
lns0.sort((a, b) => this.hDist(b, food) - this.hDist(a, food));
for (const neighbor of lns0) {
let tmp = this.moveStep(sn, neighbor, food);
let tail_next = this.aStarSearch(tmp, tmp[tmp.length - 1]);
if (tail_next) return neighbor;
}
return lns0[0];
}
}
我在进度到达3/4时,开始巡航路线,可以保证百分百铺满通关。当然你也可以不使用寻路算法,一开始就巡航,但是这样前期就会花费太多时间。