A*算法和AI贪吃蛇的具体实现

本着最近在写一个java的贪吃蛇项目,觉得人工手动控制太过于单调,于是,想着加入AI部分,让蛇自己能够智能移动。
适用场景:游戏里的寻路问题.

相信大家已经对A*算法有了初步理解,下面我将讲解如何具体实现.
贪吃蛇的具体实现,我在上一篇博客就已经写到。假设只给一个蛇的开始节点,和食物节点,那么走的最短距离应该就是曼哈顿距离(城市街区距离),这是以下算法的基础

算法种类?

搜索路径的算法其实有多种,分为盲目式搜索和启发式搜索。其中盲目式搜索有DFS和BFS,启发式搜索有A*和有序搜索(或者最佳优先搜索),

BFS

若使用BFS,首先将蛇头节点放入队列,然后循环弹出队列直到队列为空,为空返回-1,没找到路径,每弹出一个队列里的节点,判断该节点是不是食物,如果不是食物,把该节点添加到vis表说明已经走过,并且把该节点相邻的上下左右四个节点添加到队列里并记录下节点的父节点(我用的HashMap),添加时如果节点在vis表或者出了墙或者是身体节点,就不添加到队列里。如果是食物,返回去食物的第一步的方向,因为我是线程每刷新一次,蛇走一步。这样每走一步就BFS找最优路径。

DFS

这种方法有点傻,因为是走一步看一步,所以往往会把自己绕死,这里不是重点。

所以

以下我是借鉴网上的一些方法,并附加我的一些看法。
当我们不知道下一个食物的位置时就只能模拟一条蛇去吃了,我们派一条虚拟蛇(不画在屏幕上)去吃,虚拟蛇吃后生成的食物也是虚拟的所以我们不知道真实食物吃完又会在哪出现,吃完怎么判断是否能去吃下一个呢? 当它吃完后能跟着尾巴走就表明是安全的,于是策略是如下

if(能吃到食物)
         派虚拟蛇去吃,
                if(吃完能跟着蛇尾走) 真蛇去吃
                if(吃完不能跟着蛇尾) 真蛇跟着蛇尾走
    else
        真蛇跟着蛇尾
    if(不能吃食物也不能跟着蛇尾)随便逛逛,

问题是如何派虚拟蛇去吃食物.

首先用Node类来存储蛇节点.

包含G,H,F.(由于是A*算法,必须有这个.)
	包含该点坐标x,y.
	包含该节点的父节点father
	包含函数equals判断是否等价

用Snake类来存储蛇身

包含蛇身大小size=10,包含地图大小map_size=150
	包含头节点first,尾巴节点last,尾巴上一节点tail.
	包含蛇的数据结构,类型为Node的线性表s
	包含蛇的元素集合,类型为String的HashSet,名map
	包含食物Food节点
	包含函数add_Node(Node n)来添加蛇身节点到s中
	包含函数move()来移动身体
	包含函数canMove()来判断是否能移动
	包含函数updataMap()来更新地图访问过的位置
	包含函数RandomFood()来随机刷新食物

重点就是AI控制函数

//定义方向 2,4,6,8代表下,左,右,上
	类似于DFS方法.(简单的控制.)
	play(Snake s,Node f)函数{
		if (f.x>s.x){
			判断能否往这个方向走,能就返回
		}
		if (f.x<s.x){
			判断能否往这个方向走,能就返回
		}
		if (f.x==s.x){
			判断f.y与s.y关系,然后分别判断其他三个方向能否走,依次换方向.
		}
		if (f.y>s.y){
			判断能否往这个方向走,能就返回
		}
		if (f.y==s.y){
			同f.x==s.x
		}
		if (f.y<s.y){
			判断能否往这个方向走,能就返回
		}
		return -1;	//这么多方向都不能走,也就走不了了.
	}
//能到的话返回路径的第一步,不然就返回-1,目标可能不是食物,也可能是蛇尾.
	//主要判断路径是否可达.
	play1(Snake s,Node f){
		/*	队列,集合,HashMap,栈
		*/
		队列<Node>q;
		集合<String>vis;	//记录访问过的节点.
		HashMap<String,String>path;	//记录访问的路径,不过好像可以去掉
		栈<String>stack;	//蛇去吃的路径,因为倒序回来.
		q.add(s的头节点)
		while(q不为空){
			移除q的首节点并赋值于n
			if (n于f坐标相同){
				//搜到了食物,那就开始解析路径.从后面添加,用栈倒推回来.
				String state = f.toString();	//把Node的x,y坐标转换成String模式.
				while(state不为首节点){
					stack.push(state);	//入栈
					state = path.get(state);	//得到上一节点.解析父节点(注意看path的存储方法实现.)
				}
				x,y为栈stack的首元素坐标,返回路径的第一步.
				if (x,y与s首节点坐标比较)	return 2;
				if ()	return 4;
				if ()	return 6;
				if ()	return 8;
			}
		}
		Node up = new Node(n.x,n.y-10);
		Node down,left,right;	//类似,表示该首节点附近方向的节点.
		if (s.Map中没有该元素up且vis中也没有该元素up,并且不越界){
			q.add(up);vid.add(up.toString());path.put(up.toString(),n.toString());	//存储起来.
		}
		if (类似上down){}
		if (类似上left){}
		if (类似上right){}
	}
	return -1;
}

现在讲讲上面提到的这个思想,如下,重点写如何实现.

if(能吃到食物)
         派虚拟蛇去吃,
                if(吃完能跟着蛇尾走) 真蛇去吃
                if(吃完不能跟着蛇尾) 真蛇跟着蛇尾走
    else
        真蛇跟着蛇尾
    if(不能吃食物也不能跟着蛇尾)随便逛逛,
//如果不可以吃食物就追尾巴,可以吃就先派一条虚拟蛇去吃,如果吃到食物后还可以去追尾巴那就去吃,
	//否者先追尾巴,直到去吃食物后也是安全的,就去吃。
	play2(Snake snake,Node f){
		Snake visSnake;	//跟实际的蛇snake有相同的数据结构,是一个对snake的复制.
		int realGoToFood = play1(snake,f);	//判断真蛇能否吃到食物
		if (realGoToFood!=-1){
			//表示真蛇能吃到食物.
			while(虚拟蛇vis的头节点还没到节点f时){
				vis.move(play1(visSnake,f));	//虚拟蛇按实际方向去移动.
			}
			//虚拟蛇到尾巴去的方向.
			int goToTailDir = Asearch(visSnake,visSnake.Tail);	
			if (goToTailDir!=-1){
				return realGoToFood;	//虚拟蛇吃完食物能到尾巴,真蛇去吃.
			}else{
				//吃到后不能去自己的尾巴,就跟着蛇尾跑
				return Asearch(snake,snake.Tail);	//真蛇跟着蛇尾走
			}
		}else{
			//吃不到食物.真蛇到尾巴的方向.
			int realGoToTailDir = Asearch(snake,snake.Tail);
			if (realGoToTailDir==-1){
				//吃不到食物,也追不到蛇尾,随便走走
				realGoToTailDir = randomDir();
				return realGoToTailDir;
			}
			return realGoToTailDir;	//能到蛇尾,就去追蛇尾.
		}
	}

A*算法核心…

//这里走最远路径,追尾巴
	Asearch(Snake s,Node f){
		ArrayList<Node> openList,closeList;	//开放,封闭线性表.
		Stack<Node> stack;	//蛇去吃的路径.
		openList.add(s.First);	//把蛇头加入开放表中
		s.First.setH(dis(s.First,f));	//计算蛇头到f的曼哈顿距离并设置.
		while(!openList.isEmpty()){
			//开放表不为空时循环.
			Node now = 开放表中F值最大的节点.;	//初始化过程节点.
			int max = 开放表中最大的F值
			把当前节点从openList删除,移入closeList中.
			Node up,down,right,left;	//初始化为now节点附近四个点的坐标
			up,down,right,left加入线性表temp中.
			for(Node n:temp){
				if(该节点在closeList中或该节点不可达(墙壁,蛇身)){
					continue;
				}
				//如果该节点不在开放列表中,添加进入,并将该节点now设置为其父节点.
				if (!openList.contains(n)){
					n.setFather(now);
					n.setG(now.G+10);
					n.setF(dis(n,f));
					openList.add(n);
					if (终点节点f被加入到openList链表中){
						//路径是从最后一个节点开始往前走.
						Node node = openList.get(openList.size()-1);	//f节点.
						while(!node.equals(s.First)){
							stack.push(node);	//加入栈.
							node = node.getFather();
						}
						int x,y = 栈顶元素的坐标.
						if (坐标判断1)	return 方向1
						if (坐标判断2)	return 方向2
						if (坐标判断3)	return 方向3
						if (坐标判断4)	return 方向4
					}
				}
				if (openList.contains(n)){
					//相邻节点在开放链表中,判断经由当前节点到达相邻节点G值是否大于或小于(这里大于)原G值
					if (n.G>(now.G+10)){
						n.setFather(now);	//设置路径
						n.setG(now.G+10);
					}
				}
			}
		}
	}

在主方法中 ,用线程调用,每次直走一个方向即可.(但每一步都是最优的).

总结:

先学会通过伪代码思考背后算法思想,以及用如何的数据结构去存储数据。再者,通过对原码的阅读理解,进一步加深自己的编程能力和阅读代码的能力。通过一个小项目,加深对编程语言的理解。若有不足之处,请谅解。