大一实训---贪吃蛇+走全图AI实现(2)

来源:互联网 发布:java rpc zookeeper 编辑:程序博客网 时间:2024/05/03 17:47

              接下来就是ai部分,老实说,楼楼的ai其实写得并不是很好。楼楼随意测试N遍,目测成功几率最高10%,失败原因是策略性死循环。但还是献丑一下,若有什么好的意见,也请各位指导一下我。

              总体上的ai策略是分别先对3个方向的下一步进行bfs寻找蛇尾,对于能找到蛇尾的方向进行求蛇头与食物的最短距离,然后进行动作,策略完成了。

              在这里的策略中,bfs蛇尾是第一步,原因是无论到未来的局面有多么的复杂,能从蛇头bfs到蛇尾就说明蛇不管怎么样都不会死。接下来就是考虑吃的问题,这里的求距离并没有使用bfs去求,而是直接采用两点间的距离来求,原因就是每走一步,局势都会有变化,3,4步下来就变得完全不一样,而且还有就是到了最后蛇身很长的时候,很多时候都并不能bfs求解了(想象一下那个揉成一团的蛇身的情况你就知道了)。接下来上代码:

string autofound()//ai实现{int i,j,t,i1,vi[4],pan;double fo,keepfo[4];sanke ke,hd,keep[4];hd = s.back();for(i=0;i<=3;i++){keep[i]=ke;keepfo[i]=0;}pan = 0;//是否可以搜蛇尾判定for(i = 0;i <= 3;i++){here:j = rand()%4;for(i1 = 0;i1 < i;i1++)//生成随机方向{if(vi[i1]==j){goto here;}}vi[i] = j;ke.x = dire[j][0],ke.y = dire[j][1];//记录方向状态if(bfs(ke,s.front()))//此方向可以搜到蛇尾就求次方向和食物的距离{keep[j] = ke;t = j;keepfo[j] =sqrt(double((hd.x+ke.x - foodxy.x)*(hd.x+ke.x - foodxy.x) + (hd.y+ke.y - foodxy.y)*(hd.y+ke.y - foodxy.y)));fo = keepfo[j];pan = 1;}else{keepfo[j] = 100000;}}if(pan){pan = 0;for(i = 0;i <= 3 ;i++)//寻找离食物最近的方向走{if(fo>keepfo[i]){t = i;fo = keepfo[i];}}if(judge(keep[t].x,keep[t].y))//行走{if(s.size() == ((n-2)*(m-2)-1)){return "win";}else{return "go on";}}}}


还有就是bfs的:

bool bfs(sanke a,sanke b)//bfs寻找如今蛇尾{queue<sanke> q;int i,j;sanke tem,hd,ke;tem = s.front();hd = s.front();vis[tem.x][tem.y]=' ';tem = s.back();vis[tem.x][tem.y]='*';tem.x = tem.x + a.x,tem.y = tem.y + a.y;if(vis[tem.x][tem.y] == '*' || vis[tem.x][tem.y] == 'I' || vis[tem.x][tem.y] == '-' || (tem.x == hd.x && tem.y == hd.y)){if((tem.x == hd.x && tem.y == hd.y)){for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return true;}else{for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return false;}}hd = tem;vis[hd.x][hd.y] = '*';q.push(hd);while(!q.empty()){hd = q.front();q.pop();for(i = 0;i <= 3;i++){ke.x = hd.x + dire[i][0],ke.y = hd.y + dire[i][1];if(ke.x == b.x && ke.y == b.y){for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++)vis[i][j] = map[i][j];}return true;}if(vis[ke.x][ke.y] == ' ' || vis[ke.x][ke.y] == '$'){vis[ke.x][ke.y] = '*';q.push(ke);}}}for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return false;}


至于上面的代码,有一个叫"随机生成方向"的东西,其实楼楼当时是按照一定顺序去bfs的,即这个数组

int dire[4][2]={{0,1},{1,0},{0,-1},{-1,0}};//右下左上


但写完后运行发现出现的策略性的死循环,即明明食物就在蛇头面前且吃了食物还能bfs到蛇尾,但就是不去吃,一直死磕着蛇尾,并无限走同一个轨迹。这种情况让我很脑疼,我觉得应该是有同多个方向到食物的最短距离是一样的,于是就出现的rand编号这么一个东西来增加他的可变性,但尽管已经做到这样了,通关率也只是上升了一点。接着就上程序源码了:

#include <iostream>#include <stdlib.h>#include <Windows.h>#include <conio.h>#include <time.h>#include <queue>#include <string >#include <algorithm>#include <math.h>using namespace std;//#墙 $食物 *蛇身 @头int dire[4][2]={{0,1},{1,0},{0,-1},{-1,0}};//右下左上const int MAX = 30;char map[MAX][MAX],vis[MAX][MAX];//map为当前状态地图 vis为map的复制在寻路时作模拟行走int n,m;int score,ti,ju,to;//分数,控制变速变量,自动寻路判定double second;//时间控制bool judge(int i,int j);void Gotoxy(int x, int y)//光标定位函数{ COORD pos = {y,x}; HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursor_info; GetConsoleCursorInfo(hOut, &cursor_info); cursor_info.bVisible = false; cursor_info.dwSize = 20; SetConsoleCursorPosition(hOut, pos); SetConsoleCursorInfo(hOut, &cursor_info);}struct sanke//用结构体储存蛇身坐标{int x,y;sanke(int i1=0,int j1=0)//构造函数{x=i1;y=j1;}}keepdire,foodxy;//保存当前方向 记录食物坐标queue<sanke> s; //用队列特点储存蛇身void foodpro()//生成食物函数{int i,j;i=rand()%(n-1);j=rand()%(m-1);while(1){if(map[i][j]=='*' || map[i][j]=='@' || map[i][j]=='-' || map[i][j]=='I'){i=rand()%(n-1);j=rand()%(n-1);}else{map[i][j]='$';vis[i][j]='$';foodxy.x = i,foodxy.y = j;Gotoxy(i,2*j);cout<<"$";return;}}}void printf(){Gotoxy(0,0);int i,j;for(i=0;i<n;i++){for(j=0;j<m;j++){cout<<map[i][j]<<' ';}cout<<endl;}Gotoxy(n,0);cout<<"当前分数:"<<score;}void mapint()//数据初始化函数{while(1){cout<<"请选择模式"<<endl;cout<<"1: ai 快速测试模式"<<'\t'<<"2: ai  正常速度模式"<<"\t"<<"3: 游戏模式"<<endl;char ke[100];cin>>ke;if(strlen(ke)>1 ||(strlen(ke)==1&& (ke[0]-'0'!=1) && (ke[0]-'0'!=2) && (ke[0]-'0'!=3))){system("cls");cout<<"输入错误  请重新输入"<<endl;}else if(ke[0]=='1'){m = 10;n = 10;ju = 1;second = 0;system("cls");break;}else if(ke[0]=='2'){m = 10;n = 10;ju = 1;second = 300;system("cls");break;}else{system("cls");cout<<"使用wasd控制方向 p为暂停 f为自动寻路"<<endl;system("pause");system("cls");m =20;n =20;ju = 0;second = 300;break;}}int i,j;sanke a;score=0;to = 0;keepdire.x=0,keepdire.y=1;while(!s.empty()){s.pop();}for(i=0;i<n;i++){for(j=0;j<m;j++){map[i][j]=' ';vis[i][j]=' ';}}for(i=0;i<m;i++)   //地图画墙 {map[0][i]='-';vis[0][i]='-';map[n-1][i]='-';vis[n-1][i]='-';}for(i=1;i<=n-2;i++){map[i][0]='I';vis[i][0]='I';map[i][m-1]='I';vis[i][m-1]='I';}for(i=1;i<=6;i++)//蛇身构造{map[1][i]='*';vis[1][i]='*';a.x=1;a.y=i;s.push(a);if(i==6){map[1][i]='@';vis[1][i]='@';}}Gotoxy(n+1,0);cout<<"按p键暂停"<<endl;}bool bfs(sanke a,sanke b)//bfs寻找如今蛇尾{queue<sanke> q;int i,j;sanke tem,hd,ke;tem = s.front();hd = s.front();vis[tem.x][tem.y]=' ';tem = s.back();vis[tem.x][tem.y]='*';tem.x = tem.x + a.x,tem.y = tem.y + a.y;if(vis[tem.x][tem.y] == '*' || vis[tem.x][tem.y] == 'I' || vis[tem.x][tem.y] == '-' || (tem.x == hd.x && tem.y == hd.y)){if((tem.x == hd.x && tem.y == hd.y)){for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return true;}else{for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return false;}}hd = tem;vis[hd.x][hd.y] = '*';q.push(hd);while(!q.empty()){hd = q.front();q.pop();for(i = 0;i <= 3;i++){ke.x = hd.x + dire[i][0],ke.y = hd.y + dire[i][1];if(ke.x == b.x && ke.y == b.y){for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++)vis[i][j] = map[i][j];}return true;}if(vis[ke.x][ke.y] == ' ' || vis[ke.x][ke.y] == '$'){vis[ke.x][ke.y] = '*';q.push(ke);}}}for(i=0;i<n;i++)//状态返回{for(j=0;j<m;j++){vis[i][j] = map[i][j];}}return false;}string autofound()//ai实现{int i,j,t,i1,vi[4],pan;double fo,keepfo[4];sanke ke,hd,keep[4];hd = s.back();for(i=0;i<=3;i++){keep[i]=ke;keepfo[i]=0;}pan = 0;//是否可以搜蛇尾判定for(i = 0;i <= 3;i++){here:j = rand()%4;for(i1 = 0;i1 < i;i1++)//生成随机方向{if(vi[i1]==j){goto here;}}vi[i] = j;ke.x = dire[j][0],ke.y = dire[j][1];//记录方向状态if(bfs(ke,s.front()))//此方向可以搜到蛇尾就求次方向和食物的距离{keep[j] = ke;t = j;keepfo[j] =sqrt(double((hd.x+ke.x - foodxy.x)*(hd.x+ke.x - foodxy.x) + (hd.y+ke.y - foodxy.y)*(hd.y+ke.y - foodxy.y)));fo = keepfo[j];pan = 1;}else{keepfo[j] = 100000;}}if(pan){pan = 0;for(i = 0;i <= 3 ;i++)//寻找离食物最近的方向走{if(fo>keepfo[i]){t = i;fo = keepfo[i];}}if(judge(keep[t].x,keep[t].y))//行走{if(s.size() == ((n-2)*(m-2)-1)){return "win";}else{return "go on";}}}}char ifscanf()//按键判断{char ch;if(kbhit()){ch=getch();return ch;}else{return 0;}}bool judge(int i,int j)//行走判定函数{sanke b,f,tem;int k;b=s.back ();f=s.front();if(map[b.x+i][b.y+j]=='$')//若吃了食物{map[b.x][b.y]='*';vis[b.x][b.y]='*';Gotoxy(b.x,2*b.y);cout<<'*';map[b.x+i][b.y+j]='@';vis[b.x+i][b.y+j]='@';Gotoxy(b.x+i,2*(b.y+j));cout<<'@';b.x=b.x+i,b.y=b.y+j;s.push(b);foodpro();keepdire.x=i,keepdire.y=j;score++;ti++;Gotoxy(n,10);for(k=10;k<=5;k++){cout<<' ';}Gotoxy(n,10);cout<<score;if(ti==3){ti=0;second=second*0.95;}if(ju!=1 && second<100){second = 100;}return true;}else if((map[b.x+i][b.y+j]=='*' || map[b.x+i][b.y+j]=='I' || map[b.x+i][b.y+j]=='-') && (!(b.x + i == f.x && b.y + j == f.y)))//若撞墙和撞到自己{                                                                         //(b.x + i != f.x && b.y + j != f.y  对蛇尾预判忽略return false;}else {map[f.x][f.y]=' ';vis[f.x][f.x]=' ';Gotoxy(f.x,2*f.y);cout<<' ';s.pop();map[b.x][b.y]='*';vis[b.x][b.y]='*';Gotoxy(b.x,2*b.y);cout<<'*';map[b.x+i][b.y+j]='@';vis[b.x+i][b.y+j]='@';Gotoxy(b.x+i,2*(b.y+j));cout<<'@';b.x=b.x+i,b.y=b.y+j;s.push(b);keepdire.x=i,keepdire.y=j;return true;}}string move(char ch=0)//按键筛选并行走{int i,j;if(ch=='p'){Gotoxy(n+1,0);cout<<"暂停中 按任意键恢复"<<'\b';getch();Gotoxy(n+1,0);cout<<"按p键暂停            "<<endl;}if(ch == 'f'){ju = 1;return autofound();}if(keepdire.y)//向左向右走的状态{if(ch=='w'){i = -1, j = 0;if(judge(i,j)){if(s.size()==(n-2)*(m-2)-1){return "win";}}else {return "loser";}}else if(ch=='s'){i = 1,j = 0;if(judge(i,j)){if(s.size()==(n-2)*(m-2)-1){return "win";}}else {return "loser";}}else{if(judge(keepdire.x,keepdire.y)){if(s.size()==(n-2)*(m-2)-1){return "win";}}else {return "loser";}}}else//向上或下走的状态{if(ch=='a'){i = 0;j = -1;if(judge(i,j)){if(s.size()==(n-2)*(n-2)-1){return "win";}}else {return "loser";}}else if(ch=='d'){i = 0,j = 1;if(judge(i,j)){if(s.size()==(n-2)*(m-2)-1){return "win";}}else {return "loser";}}else{if(judge(keepdire.x,keepdire.y)){if(s.size()==(n-2)*(m-2)-1){return "win";}}else {return "loser";}}}return "go on";}int main(){sanke tem;int ch,q,q1,i;char again;//是否在来一局的判定string pan;//输和赢的判定while(1){q=0,q1=0;srand((unsigned)time(NULL));//以系统时间作为随机数的种子mapint();printf();foodpro();while(1) {  ch=ifscanf();if(ju){if(ch == 'f'){ju = 0;Gotoxy(n+2,0);for(i = 0 ;i <= 20 ;i++){cout<<" ";}continue;}if(ch == 'p'){ch = 'p';}else {if(to==0){Gotoxy(n+2,0);    cout<<"自动寻路中 按f解除..."<<endl;}ch = 'f';}to++;}else{if(ch == 'f' ){Gotoxy(n+2,0);cout<<"自动寻路中 按f解除..."<<endl;ch = 'f';to++;}} pan=move(ch);if(pan == "win"){cout<<"你赢了"<<endl;cout<<"再来一把??(y or n)"<<endl;cin>>again;system("cls");q1=1;}else if(pan=="loser"){system("cls");cout<<"你输了"<<endl;cout<<"再来一把??(y or n)"<<endl;cin>>again;system("cls");q1=1;}if(q1){while(1){if(again=='n' || again=='N') exit(0);  else if(again=='y' || again=='Y') {q=1;break;}else {cout<<"你耍我??"<<endl;cout<<"再来一把??(y or n)"<<endl;cin>>again;}}}if(q)break;Sleep(second);}}return 0;}


如果看官们只是看一下ai的话,看到这句话就直接可以alt+f4了,下面我要废话一下。

                  关于这次的实训,收获当然不仅仅是写出了贪吃蛇,对于楼楼的代码风格,变量命名习惯,代码的调试方式都有的一定程度上的纠正。风格自然不用说,篇幅较长的代码变量名的命名就显得尤为重要,随便就出来了一个a,b什么的就会让人看着蛋疼(相信平时刷惯了题习惯了写短小精悍的程序的acmer应该是多少有点体会)。至于调试(大牛教主请无视),楼楼学到一招在你认为有问题代码处用getch()来停顿程序,然后输出"关键信息",当然关键信息看具体程序而定,在这里就是蛇头坐标,蛇尾坐标,现在的运动趋势,将来的运动趋势等等,对比一下信息就知道哪里出了问题。

                              还有就是跟我做同一项目的基友,他的通过关率就比我高多了,起码在80%以上(惭愧啊),我跟他交流了一下思想,本质上差不多,唯一的明显不同就是他有一个"推空格"操作,简单的说就是他的蛇尾与蛇头起码保持了一个空格的距离,至于为什么,他也说不清楚。还有就是一些其他的项目,有一个是手机上比较流行的"pop star",只是在这里游戏方式就变成了手动输入坐标,看着个坐标的图案能否消除,本质上就是dfs+图案移动,看着貌似还挺不错的样子,反正之后就是他在也没玩过这个游戏,因为测bug的时候玩到他想吐(其实楼楼玩贪吃蛇也玩到想吐了)。还有就是有人写五子棋之类的。

                   好吧废话完毕。