本次作业之所以选择做牧师与恶魔(P&D)的智能帮助,主要是我觉得用代码能够写出游戏中的状态图挺有意思。
接下来看看实现的效果,这个是3个牧师和3个恶魔的效果。
这个是5个牧师和4个恶魔的效果(为什么没有4个牧师和恶魔的效果呢?因为那无解!我也是用了算法算后才发现这种情况是无解的)
算法简述
接下来说说算法,算法主要就是先创建一个有向图,图的存储结构可以用邻接矩阵或者邻接表,这里我选择了后者,用两个链表List来实现。创建完图后,每次要提示就通过当前状态来找到在图中的位置,然后宽度优先搜索找到终点。看一下这条最短路径的起始点就可以得出在当前状态下,下一步应该走去哪一个状态。
因为不想自己画一个图而且代码里的图也不好展示出来,所以不要脸的引用了师兄博客(https://blog.csdn.net/kiloveyousmile/article/details/71727667)的图,这个图是3个牧师和3个恶魔时候的状态图。
节点类
然后就是实现这个图了,首先我先定义了一个状态节点类,这个节点中的P,D是指这个状态下左边的牧师和恶魔数量,其实右岸也行,不过我选择了左岸,这主要是在建图的时候要注意就好了,同时要注意到,船在不同的岸也是不同的状态。
public class node{
//node存的是左岸的P,D
public int P;
public int D;
public bool ifBoatSizeRight; //false left , true right
public List<node> adjacentNodes;
public node (node cpyNode){
this.P = cpyNode.P;
this.D = cpyNode.D;
this.ifBoatSizeRight = cpyNode.ifBoatSizeRight;
this.adjacentNodes = new List<node>(cpyNode.adjacentNodes);
}
public node(int P , int D , bool boatSize){
this.P = P;
this.D = D;
this.ifBoatSizeRight = boatSize;
adjacentNodes = new List<node> ();
}
public void addAdjacentNode(node newAdjacentNode){
if (!ifNodeExitInList (newAdjacentNode , adjacentNodes)) {
adjacentNodes.Add (newAdjacentNode);
}
}
public static bool ifNodeExitInList(node findNode , List<node> searchList){
foreach (node temp in searchList) {
if (ifTwoNodeSame(temp , findNode) ) {
return true;
}
}
return false;
}
//两个statu等价,除了P和D要相等外,船也要在同一岸.
public static bool ifTwoNodeSame(node first , node second){
if (first.P == second.P && first.D == second.D
&& first.ifBoatSizeRight == second.ifBoatSizeRight) {
return true;
} else {
return false;
}
}
//到相接的点所用的操作
public operation[] getOperationByAnthoerNode(){
operation[] result = new operation[adjacentNodes.Count];
for (int i = 0; i < adjacentNodes.Count; i++) {
result [i] = new operation (Mathf.Abs(adjacentNodes [i].P - P),Mathf.Abs(adjacentNodes [i].D - D) );
}
return result;
}
public static node operator - (node first , operation second ){
return new node (first.P - second.P, first.D - second.D , !first.ifBoatSizeRight);
}
public static node operator + (node first , operation second ){
return new node (first.P + second.P, first.D + second.D , !first.ifBoatSizeRight);
}
}
操作类
注意到刚才node的类里用到一个operation类,这个类是用来描述一个”操作”,例如P=1,D=1,的时候,意味着把一个牧师和一个恶魔放上船并且开船的操作。那么可以看到我重载了节点node和”操作”operation的+和-的操作符,这里是什么意义呢?其实一个节点加上一个”操作”,就意味着船把另一岸的P和D运到了这一岸,该节点的P和D增加了。那么同理,意味着的话就是把当前岸的P和D运到另一岸。在这个游戏中,”操作”主要有5种,就是1P,1D,1P1D,2P和2D。
public class operation {
public int P;
public int D;
public operation(int P , int D){
this.P = P;
this.D = D;
}
}
带权节点类
然后我还定义了一个带权节点类,在搜索的时候用,继承node,其中的length代表走到该节点的长度(事实上我后来并没有用到),然后startNode就代表着走到该节点时最开始的节点是哪个。
public class withLengthNode:node{
public int length;
public node startNode;
public withLengthNode(node thisNode , int length , node startNode = null) :base(thisNode){
this.length = length;
this.startNode = startNode;
}
public static bool ifNodeExitInList(node findNode , List<withLengthNode> searchList){
foreach (node temp in searchList) {
if (node.ifTwoNodeSame(temp , findNode) ) {
return true;
}
}
return false;
}
}
状态图类
接来下就是状态图这个类了,主要两个功能,建图和宽度优先找下一步,
建图就是先把初始点放进一个队列,然后进行五个操作,将合法且不在队列中的点再放入队列中,并互相相连,然后直到队列中的每一个点都已经进行过五次操作后,图就建好了。
而宽度优先找下一步也是很类似,也是用一个队列先把起始点放入,然后将邻接点都放入队列,直到找到终点。注意到只要状态合法,就一定能找到终点,返回值是一个“操作”operation,给场记来让人上船并开船。
public class statusGraph{
private List<node> allGraphNodes;
private operation[] nodeOperations = {new operation (0, 1) , new operation (1, 0) ,
new operation (1, 1) , new operation (2, 0) , new operation (0, 2) };
private int maxP;
private int maxD;
private bool boatStartSize ;
private node endStatusNode ;
public statusGraph(int maxP ,int maxD , bool boatStartSize){
this.maxP = maxP;
this.maxD = maxD;
this.boatStartSize = boatStartSize;
if (boatStartSize == true) {
endStatusNode = new node (maxP, maxD, !boatStartSize);
} else {
endStatusNode = new node (0, 0, !boatStartSize);
}
createGraph ();
}
private void createGraph(){
allGraphNodes = new List<node> ();
if (boatStartSize == true) {
//开始船在右岸
allGraphNodes.Add(new node(0, 0, boatStartSize));
} else {
//开始船在左岸
allGraphNodes.Add(new node(maxP, maxD, boatStartSize));
}
for (int index = 0 ; index != allGraphNodes.Count ; index++) {
node thisNode = allGraphNodes[index];
foreach (operation op in nodeOperations) {
node adjcentNode;
if (thisNode.ifBoatSizeRight ) {
//当前船在右岸,由于node是左岸,因此船送人过来,node的P和D应该增加。
adjcentNode = thisNode + op;
} else {
adjcentNode = thisNode - op;
}
node anotherSizeNode = getAnotherSizeNode (adjcentNode);
//若两岸node都合法
if (ifNodeValid (adjcentNode) && ifNodeValid(anotherSizeNode) ) {
//若node在Graph中,node不加入graph
adjcentNode = getNodeFromList (adjcentNode);
//两个node互相接通(函数内会判断是否已有那个新的node,有就不加了)
adjcentNode.addAdjacentNode (thisNode);
thisNode.addAdjacentNode (adjcentNode);
/*Debug.Log (thisNode.P + " " + thisNode.D + " " + thisNode.ifBoatSizeRight +
"->" + adjcentNode.P + " " + adjcentNode.D + " " + adjcentNode.ifBoatSizeRight );*/
}
}
}
}
private bool ifNodeValid(node test){
return (test.P >= test.D || test.P == 0) && test.D <= maxD && test.P <= maxP;
}
private node getNodeFromList(node findNode){
foreach (node temp in allGraphNodes) {
//两个statu等价,除了P和D要相等外,船也要在同一岸,list有node就返回node
if (node.ifTwoNodeSame(temp , findNode) ) {
return temp;
}
}
//linkedList没有node,就把node加入里面
allGraphNodes.Add(findNode);
return findNode;
}
//得到另一岸的node
private node getAnotherSizeNode(node thisSizeNode){
return new node(maxP - thisSizeNode.P
, maxD - thisSizeNode.D , !thisSizeNode.ifBoatSizeRight );
}
public operation getNextStep(node nowNode){
node anotherSizeNode = getAnotherSizeNode (nowNode);//用当前node得到graph中的node
if(ifNodeValid (nowNode) && ifNodeValid(anotherSizeNode) ){
nowNode = getNodeFromList (nowNode);
node nextNode = getStartNodeByWidthSearch (nowNode);
if (nextNode == null) {
return null;
} else {
return new operation (Mathf.Abs (nowNode.P - nextNode.P), Mathf.Abs (nowNode.D - nextNode.D));
}
}
else{
return null;
}
}
private node getStartNodeByWidthSearch(node startNode){
List<withLengthNode> alreadySearchNode = new List<withLengthNode>();
//开始点已搜索
alreadySearchNode.Add (new withLengthNode(startNode , 0) );
//与开始点相接的点全部放入list中,起始点就是相接的node自身,也即当前下一步要走的地方
foreach (node adjacentNode in startNode.adjacentNodes) {
if (node.ifTwoNodeSame (adjacentNode, endStatusNode)) {
return adjacentNode;
} else {
alreadySearchNode.Add (new withLengthNode (adjacentNode, 1, adjacentNode));
}
}
for (int i = 1; i < alreadySearchNode.Count; i++) {
//alreadySearchNode [i]是当前要搜索的点,将所有与它相接且不在list中的node都放到待搜索list中
foreach (node adjacentNode in alreadySearchNode[i].adjacentNodes ) {
//如果当前node的下一个node是目的点,返回node的开始点,也就是下一步要走的地方
if (node.ifTwoNodeSame (adjacentNode, endStatusNode)) {
return alreadySearchNode[i].startNode;
}
//如果当前node的下一个node不是目的点,看它是否已被搜索,没被搜索就加入list
else if (!withLengthNode.ifNodeExitInList (adjacentNode, alreadySearchNode)) {
alreadySearchNode.Add (new withLengthNode(adjacentNode , alreadySearchNode[i].length + 1 , alreadySearchNode[i].startNode ) );
}
}
}
return null;
}
}
状态图的代码已经给完了,这个类最重要的作用就是提供一个得到下一步应该做什么的接口,然后写完这些,在原来的P&D的基础上稍做修改(之前写的不是很好,改起来也挺麻烦),点击提示按钮完成相应的动作即可,P&D的代码就不再详细讲述了。
一些想法
状态图(有限状态机)对于这些当前状态已知,操作较少,状态较少而且后果已知的游戏感觉还是挺好的,但假若当前状态未知,比如打牌是不知道对方有什么牌的,又或者执行一个操作可后果未知,比如放一个技能伤害不固定,还有生成出来的状态太多,这些情况下状态图可能就不太有效了。