游戏智能
作业要求
P&D 过河游戏智能帮助实现,程序具体要求:
- 实现状态图的自动生成
- 讲解图数据在程序中的表示方法
- 利用算法实现下一步的计算
- 参考:P&D 过河游戏智能帮助实现
实现过程
首先我们对这个过河问题进行一下简单的分析。游戏中的每一个状态可以由当前左右两边各有多少个恶魔和牧师,以及现在船在哪一边来唯一的标识。而且在保证每一边恶魔数量不多于牧师,船上人数大于0小于等于2的情况下,不同状态之间存在相互转移关系。所以整个游戏的运行过程可以表示为一个图的形式,而且这个图是无向有环的,因为两个相连的状态必然能够相互转换,而且经过一段时间的变换可能会回到之前的状态。那么整个问题就变为在状态转移图上寻找一条从起始状态(3个恶魔和3个牧师都在右边)到结束状态(3个恶魔和3个牧师都在左边)的路径。所以整个算法实际上就是一个图搜索算法,这里我们可以使用深度优先搜索。
为了方便调试,我首先写了一个C++的模拟程序。这里使用一个三元组(ld, lp, side)ld:左边恶魔数、lp:左边牧师数、side:船在哪一边,这三个信息唯一的标识一个状态。使用一个visited数组记录当前是否展开过某个状态,这样可以防止搜索过程中重复展开已经展开过的状态。使用一个search函数递归的进行搜索,然后返回一个记录了可行路径的栈。详细程序如下:
#include <bits/stdc++.h>
using namespace std;
bool visited[4][4][2]; //标志一个状态是否被展开过
bool is_valid(int ld, int lp){ //判断一个状态是否违反规则(某一边恶魔大于牧师)
if((lp!=0 && ld > lp) || ((3-lp!=0) && (3-ld)> (3-lp)))return false;
return true;
}
stack<pair<int, int> > search(int ld, int lp, int side){ //输入一个状态,进行寻路
visited[ld][lp][side] = true;
stack<pair<int, int> > path;
if(ld==3 && lp==3){ //结束状态,用全0标记
path.push(pair<int, int>(0,0));
return path;
}
int D = (side==0)?ld:(3-ld);
int P = (side==0)?lp:(3-lp);
int next_side = (side==0)?1:0;
for(int d = 0; d <= D; d++){
for(int p = 0; p <= P; p++){
if(d+p>2 || d+p==0)continue;
int temp_ld = (side==0)?ld-d:ld+d;
int temp_lp = (side==0)?lp-p:lp+p;
if(!is_valid(temp_ld, temp_lp))continue; //检查变换之后是否违反规则
if(visited[temp_ld][temp_lp][next_side])continue; //检测变换之后的状态是否已经展开过
printf("(%d, %d, %d)->(%d, %d, %d)\n",ld,lp,side,temp_ld,temp_lp,next_side);
path = search(temp_ld, temp_lp, next_side); //递归查找一条路径
if(path.size()!=0){ //如果返回结果为空,说明是死路
path.push(pair<int,int>(d,p));
return path;
}
}
}
return path;
}
int main(){
memset(visited, false, sizeof(visited));
stack<pair<int, int> > path;
path = search(0,0,1); //左边没有Devil和Priest,船在右边作为起始状态
int curr_side = 1;
while(path.size()!=0){
pair<int, int> move = path.top();
path.pop();
curr_side = (curr_side==1)?0:1;
if(move.first==0 && move.second==0){
cout<<"success!"<<endl;
break;
}
printf("move %d Devils and %d Priests ", move.first,move.second);
string move_str = (curr_side==0)?"from right to left":"from left to right";
cout<<move_str<<endl;
}
}
程序运行结果
程序运行之后显示了从(0,0,1)到(3,3,0)状态,也就是从起始状态到结束状态的搜索路径。最后输出了一系列的移动策略。
状态转化图
将上面程序输出的状态转移信息放到一个状态转换图中,就得到了以下结果:
上图并不是完整的状态转移关系,准确来说只是深度优先搜索过程中的状态转移关系。这个状态图显示了一条从起始状态到结束状态的路径,以及搜索过程中展开的一些死路。在展开过程中我没有展开那些违反规则(GameOver)的状态,也不会展开那些曾经展开过的状态,所以整个状态转移图就非常简单。
当然,开始状态不一定是(0,0,1),我们可以从任何合法的状态起步。由于整个状态转移图是全联通的,所以我们必然能够找到一个到达结束状态的路径。(这里可以简单证明一下,我们选择的开始状态必然是从(0,0,1)转换来的,所以存在到达(0,0,1)的路径,而(0,0,1)存在到达(3,3,0)的路径,所以这个问题一定有解)
实现代码
接下来的任务就是将这一段代码结合到之前的代码中,从而实现自动运行的功能。C#中的库和C++有些区别,我使用C#的Stack代替C++的stack,C#的KeyValuePair代替C++的pair。为了使整个移动效果依次进行,而不是计算出了路径一下就运行完了,我在update函数中每隔一段时间调用一下nextstep,然后nextstep会从栈顶得到当前路径并进行移动,其他部分的实现大体没变。
public class Controller : MonoBehaviour, ISceneController, IUserAction
{
private GameObject left_land, right_land, river;
private CharacterModel[] MCharacter;
private BoatModel MBoat;
private bool[,,] visited;
public CCActionManager actionManager;
public CCJudgement judgement;
Stack<KeyValuePair<int, int>> path;
bool auto_mode;
float spendTime;
// Start is called before the first frame update
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
MCharacter = new CharacterModel[6];
director.currentSceneController.LoadResources();
actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
visited = new bool[4,4,2];
auto_mode = false;
}
public void LoadResources()
{
Vector3 left_land_pos = new Vector3(-8F, 0, 0);
Vector3 right_land_pos = new Vector3(8F, 0, 0);
Vector3 river_pos = new Vector3(0, -0.5F, 0);
Vector3 boat_pos = new Vector3(4F, 0.25F, 0);
Vector3[] charactor_pos = { new Vector3(5.25F, 1.25F, 0), new Vector3(6.25F, 1.25F, 0), new Vector3(7.25F, 1.25F, 0),
new Vector3(8.25F, 1.25F, 0), new Vector3(9.25F, 1.25F, 0), new Vector3(10.25F, 1.25F, 0) };
left_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), left_land_pos, Quaternion.identity, null) as GameObject;
left_land.name = "left_land";
right_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), right_land_pos, Quaternion.identity, null) as GameObject;
right_land.name = "right_land";
river = Object.Instantiate(Resources.Load("River", typeof(GameObject)), river_pos, Quaternion.identity, null) as GameObject;
river.name = "river";
MBoat = new BoatModel(boat_pos);
for (int i = 0; i < 6; i++)
{
if (i < 3)
MCharacter[i] = new CharacterModel(0, charactor_pos[i], i);
else
MCharacter[i] = new CharacterModel(1, charactor_pos[i], i);
}
}
public int get_character_side(int num)
{
return MCharacter[num].get_side();
}
public void change_game_situation()
{
UserGUI.situation = judgement.GetSituation();
if (UserGUI.situation != 0) stop_all();
}
public void stop_all()
{
for (int i = 0; i < 6; i++)
MCharacter[i].stop_character();
MBoat.stop_boat();
}
public void enable_all()
{
for (int i = 0; i < 6; i++)
MCharacter[i].enable_character();
MBoat.enable_boat();
}
bool is_valid(int ld, int lp)
{ //判断一个状态是否违反规则(某一边恶魔大于牧师)
if ((lp != 0 && ld > lp) || ((3 - lp != 0) && (3 - ld) > (3 - lp))) return false;
return true;
}
Stack<KeyValuePair<int, int> > search(int ld, int lp, int side)
{ //输入一个状态,进行寻路
visited[ld,lp,side] = true;
Stack<KeyValuePair<int, int> > path = new Stack<KeyValuePair<int, int>>();
if (ld == 3 && lp == 3)
{ //结束状态,用全0标记
path.Push(new KeyValuePair<int, int>(0, 0));
return path;
}
int D = (side == 0) ? ld : (3 - ld);
int P = (side == 0) ? lp : (3 - lp);
int next_side = (side == 0) ? 1 : 0;
for (int d = 0; d <= D; d++)
{
for (int p = 0; p <= P; p++)
{
if (d + p > 2 || d + p == 0) continue;
int temp_ld = (side == 0) ? ld - d : ld + d;
int temp_lp = (side == 0) ? lp - p : lp + p;
if (!is_valid(temp_ld, temp_lp)) continue; //检查变换之后是否违反规则
if (visited[temp_ld,temp_lp,next_side]) continue; //检测变换之后的状态是否已经展开过
//Debug.Log("(%d, %d, %d)->(%d, %d, %d)\n", ld, lp, side, temp_ld, temp_lp, next_side);
path = search(temp_ld, temp_lp, next_side); //递归查找一条路径
if (path.Count != 0)
{ //如果返回结果为空,说明是死路
path.Push(new KeyValuePair<int, int>(d, p));
return path;
}
}
}
return path;
}
public void auto_move()
{
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
for (int k = 0; k < 2; k++)
visited[i, j, k] = false;
int left_d = 0, left_p = 0, right_d = 0, right_p = 0;
for (int i = 0; i < 6; i++)
{
if (i < 3)
{
if (get_character_side(i) == -1)
right_d += 1;
else
left_d += 1;
}
else
{
if (get_character_side(i) == -1)
right_p += 1;
else
left_p += 1;
}
}
int side = (MBoat.get_side()==-1)?1:0;
path = search(left_d, left_p, side);
auto_mode = true;
this.stop_all();
}
void next_step()
{
if (path.Count == 0) return;
int side = MBoat.get_side();
KeyValuePair<int, int> move = path.Pop();
if (move.Key == 0 && move.Value == 0)
{
Debug.Log("success");
return;
}
string output = "move " + (move.Key).ToString() + " Devils and " + (move.Value).ToString() + " Priests ";
string move_str = (side == 0) ? "from right to left" : "from left to right";
output += move_str;
Debug.Log(output);
for(int i = 0; i < 6; i++)
{
if (MCharacter[i].get_whether_on_boat())
{
to_land(i);
}
}
int curr_d = 0, curr_p = 0;
for(int i = 0; i < 6; i++)
{
if(MCharacter[i].get_side() == side)
{
if(i < 3 && curr_d < move.Key)
{
take_boat(i);
curr_d+=1;
}
if(i >= 3 && curr_p < move.Value)
{
take_boat(i);
curr_p += 1;
}
}
}
move_boat();
}
public void move_boat()
{
Debug.Log("Move boat");
if (MBoat.is_empty())
return;
MBoat.turn_side();
int[] custom_num = MBoat.get_customs();
if (custom_num[0] != -1)
{
MCharacter[custom_num[0]].turn_side();
actionManager.Move(MCharacter[custom_num[0]].character, MCharacter[custom_num[0]].get_dst(), 20);
}
if (custom_num[1] != -1)
{
MCharacter[custom_num[1]].turn_side();
actionManager.Move(MCharacter[custom_num[1]].character, MCharacter[custom_num[1]].get_dst(), 20);
}
actionManager.Move(MBoat.boat, MBoat.get_dst(), 20);
this.stop_all();
Debug.Log(UserGUI.situation);
}
public void click_character(int character_num)
{
if (MCharacter[character_num].get_whether_on_boat())
to_land(character_num);
else
take_boat(character_num);
}
public void take_boat(int character_num)
{
if (!MBoat.has_empty() || MCharacter[character_num].get_side() != MBoat.get_side()||
MCharacter[character_num].get_whether_on_boat())
return;
Vector3 boat_seat = MBoat.get_seat(character_num);
MCharacter[character_num].take_boat(boat_seat);
}
public void to_land(int character_num)
{
if (!MCharacter[character_num].get_whether_on_boat())
return;
MBoat.clear_seat(character_num);
MCharacter[character_num].to_land();
}
// Update is called once per frame
void Update()
{
if (!auto_mode) return;
spendTime += Time.deltaTime;
if(spendTime > 1.5)
{
spendTime = 0;
next_step();
}
}
public void restart()
{
MBoat.restart();
auto_mode = false;
for (int i = 0; i < 6; i++)
MCharacter[i].restart();
enable_all();
}
}
运行效果
点击Auto Move按钮之后,程序就会根据当前状态计算出一条可以达到最终状态的运动路径,并自动控制角色的上下船和船的左右移动。
完整工程文件请查看我的Github,如果有什么问题请及时指出,谢谢。