吴昊品游戏核心算法 Round 2 (特别篇)—— 吴昊教你玩中国象棋(模拟算法+AI会在特别篇的附加版中予以说明)(HDOJ 1691)

 所谓模拟算法, 则是用计算机语言将现实世界中可以用自然语言描述出来的东西用计算机的某种语言加以描述出来。我给出的Round 2中的数独游戏也是棋类游戏,但是,由于其规则相比中国象棋比较简单,其AI还是比较好实现的。这里我所讲的中国象棋由于其规则本身的复杂性,其AI的实 现方式本来就是多样的,所在只在特别篇的附加版中给出目前做得最优秀的中国象棋的AI引擎,并不详细阐明其方法或者是源代码。

 

中 国象棋在中国有着悠久的历史,属于二人对抗性游戏的一种。由于用具简单,趣味性强,成为流行极为广泛的棋艺活动。是我国正式开展的78个体育项目之一,为 促进该项目在世界范围内的普及和推广,在中国古代,象棋被列为士大夫们的修身之艺,现在则被视为怡神益智的一种有益的活动。在棋战中,人们可以从攻与防、 虚与实、整体与局部等复杂关系的变化中悟出某种哲理。

由于我在前文中已经阐述过了,所谓的模拟算法,就是利用计算机语言(这里选取C语言)将自然语言中的一些规则进行描述,所以,关于中国象棋的模拟算法,我准备利用“翻译”的方式,将计算机语言的魅力展示出来。

Source这一次还是来源于HDOJ(杭电OJ) Problem 1691,给出问题如下:给出一个残局,并给出一系列走法,你设计一个程序来判断其走法是否是合法的。

这里,我采用“自然语言”与“C语言”互相翻译的方式阐述源代码:(由于这里我采用的是“翻译”的方式,所以,源代码没有经过编译,可能有一些小BUG,见谅!),其中,关于精彩之处的评析,我已经融入到具体的对代码的注释中了。

#include<stdio.h>

#include<stdlib.h>

#include<string.h> /*
  头文件不解释,string.h主要是处理输入输出的一些问题
*/

 
/*
   首先,我们定义一个棋盘,一个棋盘必然有其固定的行和列,为了在之后开的二位数组中运用方便,这里用宏的方式给出行和列(Row and Column)在长方形的平面上,绘有九条平行的竖线和十条平行的横线相交组成,共有九十个交叉点,棋子就摆在交叉点上。中间部分,也就是棋盘的第五,第 六两横线之间末 画竖线的空白地带称为“河界”。两端的中间,也就是两端第四条到第六条竖线之间的正方形部位,以斜交叉线构成“米”字方格的地方,叫作“九宫”(它恰好有 九个交叉点)。
*/
 

#define R 10

#define C 9

 

/*
定义红方和黑方,这里介绍一下相关的历史常识,红方代表刘邦(正统),黑方代表项羽。
*/

#define Red 0

#define Black 1

 

/*
  下面给出的宏主要定义了棋盘上的棋子(其中,包括空缺的部分)SPACE代表棋盘没有放置任何棋子,King代笔“将”或者是“帅”,Mandarin代 表“士”,Elephant代表“象”,Knight代表“马”,Rook代表“车”,Cannon代表“炮”,Pawn代表“兵”。
  
*/

#define SPACE 0

#define King 1

#define Mandarin 2

#define Elephant 3

#define Knight 4

#define Rook 5

#define Cannon 6

#define Pawn 7

  

  /*以上定义了两个结构体,第一个结构体描述的是棋盘上的某个子的类型以及属于哪一方。第二个结构体则定义了该子的具体位置。
  
*/
  typedef struct chess

 {

   int type,owner;       

 }Chess;

 

 typedef struct

 {

   int x,y;       

 }Point;

 

  /*
  由于一个回合总是黑方和白方轮流下的,所以,这里定义出两个点k1,k2,意思是双方的王的位置,由于一步棋总是由一个原位置跳转到一个新位置,所 以,这里对于一步棋来说,还是需要定义两个点。最后,我们根据以前定义的行常数和列常数定义一个名叫board的二维数组。
  
*/
Point k1,k2;

Point from,to;

int board[R][C];

 

/*以下三个函数称为辅助函数,其功能分别为求取最小值,最大值和绝对值,所谓辅助函数与正统函数的区别,主要是辅助函数并不直接对自然语言的描述其作用。
 
*/
int Min(int a,int b)

 {

   return a>b ? b:a;   

 }

 

 int Max(int a,int b)

 {

   return a<b ? b:a;   

 }

 

 int Abs(int a)

 {

   return a>0 ? a:-a;   

 }

 

 /*大家注意一个地方,也就是说,我们之前只标识了红方,也就是刘邦一方的所有棋子,却没有标识黑方的棋子,这是因为我们找出了红方与黑方棋子的内在规律性,是一个公差为7的数。所以,我们只用再些一个GetType函数,就可以得到具体的棋子了!
*/
Chess GetType(int c)

 {

   Chess ret;

   ret.owner=c>7 ? 1:0;

   if(c>7) c-=7;

   ret.type=c;

   return ret;     

 }

 

/*下面,我们来编写一些实用性质的函数,这些函数基于中国象棋的相关规则,所以说,这里的函数都是基于中国象棋的规则编写出来的。
*/

/*中国象棋规则1:任何的棋子不能出界,于是,我们需要写一个Breakout()函数。函数的类型为布尔型的,出界与否,可以根据“行出界”&&“列出界”来判断。如果出界了,判为true,没有出界,则判为false。
*/
bool BreakOut()

 {

   if(from.x<0||from.x>=R||to.x<0||to.x>=R)

     return true;

   if(from.y<=||from.y>=C||to.y<0||to.y>=C)

     return true;

   return false;    

 }

 

/*Conflict函数中有一个参数,这里参数主要判断是否会发生冲突,当然,这种冲突在现实世界中的反映主要是两种情况(1)出发位置是空白的(2)出发位置的子不应该是属于自己范围内的,这当然包括黑方和白方两种可能。
*/

bool Conflict(int k)

 {

   if(board[from.x][from.y]==SPACE) return true;

   if(k&&board[from.x][from.y]<8||(!k)&&board[from.x][from.y]>7)

     return true;

   if(k&&board[to.x][to.y]>7||((!k)&&board[to.x][to.y]<8&&board[to.x][to.y]>0))

     return true;

   return false;    

 }

 

/*这是描述中国象棋的走子规则,也是整个源代码最为精彩的地方!因为这才是整个中国象棋的本质。我这里会详细地描述。
*/

bool LegalMove()

{

//得到这个棋子的具体类型。

    Chess cur=GetType(board[from.x][from.y]);

    int i,j,c;

   

    switch(cur.type){

              /*中国象棋规定了国王的走法:红方为“帅”,黑方为“将”。帅和将是棋中的首脑,是双方竭力争夺的目标。它只能在"九宫"之内活动,可上可下,可左可右,每次走动只能按竖线或横线走动一格。帅与将不能在同一直线上直接对面,否则走方判负。
  
*/

    case King:

           if(Abs(from.x-to.x)+Abs(from.y-to.y)!=1)return false;

           if(to.y<3||to.y>5return false;

           if(cur.owner){

                  if(to.x>2)      return false;

           }

           else if(to.x<7return false;

           break;

           /*中国象棋规定了士的走法:仕(士)是将(帅)的贴身保镖,它也只能在九宫内走动。它的行棋路径只能是九宫内的斜线。
  
*/
    case Mandarin:

           if(Abs(from.x-to.x)!=1||Abs(from.y-to.y)!=1)

                  return false;

           if(to.y<3||to.y>5)   return false;

           if(cur.owner){

                  if(to.x>2)      return false;

           }

           else if(to.x<7return false;

           break;

              /*中国象棋规定了象的走法:红方为“相”,黑方为“象”。相(象)的主要作用是防守,保护自己的帅(将)。它的走法是每次循对角线走两格,俗称“象飞 田”。相(象)的活动范围限于"河界"以内的本方阵地,不能过河,且如果它走的"田"字中央有一个棋子,就不能走,俗称“塞象眼”。
    
*/
    case Elephant:

           //Law,这里是常规的规则。

           if(Abs(from.x-to.x)!=2||Abs(from.y-to.y)!=2)return false;

           if(to.y&1)return false;

           //Scope

           if(cur.owner){

                  if(to.x>4)      return false;

                  if(to.x&1)      return false;

           }

           else{

                  if(to.x<5)       return false;

                  if(!(to.x&1))   return false;

           }

           /*Path,这里是路径的规则,也就是说,我们不能允许“塞象眼”,也就是撇角的发生。
  
*/
           if(board[(from.x+to.x)>>1][(from.y+to.y)>>1])return false;

           break;

           /*中国象棋规定了马的走法:马走动的方法是一直一斜,即先横着或直着走一格,然后再斜着走一个对角线,俗称“马走日”。马一次可走的选择点可以达到四周 的八个点,故有"八面威风"之说。如果在要去的方向有别的棋子挡住,马就无法走过去,俗称“蹩马腿”。*/

    case Knight:

      //第一个if判断Law。

           if(! ( Abs(from.x-to.x)==2&&Abs(from.y-to.y)==1

                  || Abs(from.x-to.x)==1&&Abs(from.y-to.y)==2 ) )

                  return false;

      //第二个if判断Path。

           if(Abs(from.x-to.x)==2){

                  if(from.x-to.x<0){

                         if(board[from.x+1][from.y]) return false;

                  }

                  else if(board[from.x-1][from.y]) return false;

           }

           else{

                  if(from.y-to.y<0){

                         if(board[from.x][from.y+1]) return false;

                  }

                  else if(board[from.x][from.y-1])return false;

           }

           break;

      /*中国象棋规定了车的走法:车在象棋中威力最大,无论横线、竖线均可行走,只要无子阻拦,步数不受限制。因此,一车可以控制十七个点,故有“一车十子寒”之称。
  
*/
    case Rook:

           if(Abs(from.x-to.x)&&Abs(from.y-to.y))    return false;

           if(from.x-to.x)

                  for(i=Min(from.x,to.x)+1,j=Max(from.x,to.x);i<j;i++)

                         if(board[i][from.y])return false;

           }

           else{

                  for(i=Min(from.y,to.y)+1,j=Max(from.y,to.y);i<j;i++)

                         if(board[from.x][i]) return false;

           }

           break;

      /*在中国象棋中,炮的走法最为复杂,先给出官方的走法,再对源代码进行进一步说明,炮在不吃子的时候,走动与车完全相同,但炮在吃子时,必须跳过一个棋 子,我方的和敌方的都可以,俗称“炮打隔子”。炮的走法和车的走法类似,但是,由于有“隔山打炮”这个说法,而且只能是“隔一山”,所以,在这里增补一个 计数器,专门来记录山的数量,在最后可以进行如下的判断:如果隔的山数为0或2以上的数,则排除。并且,在山数为1的时候,还是需要判断是否会存在隔山打 “空炮”这种情况。
     
*/
    case Cannon:

           if(Abs(from.x-to.x)&&Abs(from.y-to.y)) return false;

           if(from.x-to.x){

                  for(i=Min(from.x,to.x)+1,j=Max(from.x,to.x),c=0;i<j;i++)

                         if(board[i][from.y]) c++;

           }

           else{

                  for(i=Min(from.y,to.y)+1,j=Max(from.y,to.y),c=0;i<j;i++)

                         if(board[from.x][i]) c++;

           }

           if(c>1||(c==1&&(!board[to.x][to.y]))||(c==0&&board[to.x][to.y]))

                  return false;

          

           break;

      /*中国象棋规定了兵的走法:红方为“兵”,黑方为“卒”。兵(卒)在未过河前,只能向前一步步走,不能后退外,可向前移动。过河以后可左、右移动,但也只能一次一步,即使这样,兵(卒)的威力也大大增强,故有“过河的卒子顶半个车”之说。
  
*/
case Pawn:

           if(Abs(from.x-to.x)+Abs(from.y-to.y)!=1return false;

           /*这里的兵分为了越界兵和非越界兵两种情况进行讨论。*/

           if(cur.owner){

                  if(from.x>to.x) return false;

                  if(from.x==to.x&&from.x<5)      return false;

           }

           else{

                  if(from.x<to.x) return false;

                  if(from.x==to.x&&from.x>4return false;

           }

           break;

    }

    return true;

}

 

/* 这里的函数主要是基于“老帅不能相对”的原则,翻译成计算机语言,结论如下,如果两个老帅的列数相同,就看它们之间相隔的行是否有子,如果全部是空白 (SPACE)的话,就返回true,否则就返回false。这里的精彩之处在于那个末尾带分号的for循环。一般情况下,这种写法是不提倡的,但是,在 特殊的情况下,末尾带分号的for循环往往会收到一些神奇的效果,比如这里,利用一个带分号的for循环,来检验是否之间全部都是空格,实为一个不错的方法!
*/
bool FaceToFace()

 {

   int i;

   if(k1.y==k2.y)

   {

     for(i=k2.x+1;i<k1.x&&board[i][k1.y]==0;i++);

     return i>=k1.x;             

   }    

   return false;

 }

/*这里留一个“注释函数”,其意在说明这里随时保留着一个函数,可以将board进行输出。*/

void Print()

 {

   int i,j;

   system("cls");

   for(i=0;i<R;i++)

   {

     for(j=0;j<C;j++)

       printf("%3d",board[i][j]);

     puts("");               

   }    

 }

/*最后,我们来看看主函数。tt代表的是每一个案例的编号,用布尔变量标志的ended表示一盘棋是否已经结束(这里的结束意味着将死对方)。
*/
int main()

 {

   int t,i,j;

   int q,k,tt=1;

   bool ended;

   scanf("%d",&t);

   while(t--)

   {

     for(i=0;i<R;i++)

     {

       for(j=0;j<C;j++)

       {

           //输入一个棋盘,并表示双方的老帅的位置,因为,老帅的存亡乃是事关全局的。

         scanf("%d",&board[i][j]);

         if(board[i][j]==1)

         {

           k1.x=i,k1.y=j;                  

         }              

         if(board[i][j]==8)

         {

           k2.x=i,k2.y=j;                 

         }

       }                  

     }

     //这里的q表示一共有多少步棋,k代表最初走棋的一方,0代表红方,1代表黑方。         

     scanf("%d%d",&q,&k);

     for(i=1,ended=false,j=0;i<=q;i++)

     {

       scanf("%d%d%d%d",&from.x,&from.y,&to.x,&to.y);

     /*为什么一开始要定义一个j并在这里给出这样一句话呢?我首先还是不以为然,事后总算是领悟了。因为,题目要求是记录走错的第一步棋(注意了,是第一 步,如果所有的步数都没有任何错误的话,就返回Legal Move.),所以,当发现了一步棋走错之后,对于后面的一些i直接continue掉,直到得到所有的步数都遍历完,跳出for循环为止。
     
*/
       if(j) continue;

    //如果整局棋结束了,就跳出循环。

       if(ended)

       {

         j=i;

         continue;         

       }         

    //为了和定义的数组相匹配,这里有一个棋盘移位的过程。

       from.x-=1;

       from.y-=1;

       to.x-=1;

       to.y-=1;

    //如果出界了,则发生错误,找到错误。

       if(BreakOut())

       {

         j=i;

         continue;              

       }         

    //如果导致争议了(详情见描述的争议函数),则发生错误,找到错误。

       if(Conflict(k))

       {

         j=i;

         continue;              

       }         

    //如果发现非法的走子方法,则发生错误,找到错误。

       if(!LegalMove())

       {

         j=i;

         continue;               

       }  

    //如果杀死了对方的将帅,则整局棋结束,退出。

       if(board[to.x][to.y]%7==1)

       {

         ended=true;

         continue;                         

       }

     //双方的王的移动。

       if(board[from.x][from.y]==1)

       {

         k1.x=to.x,k1.y=to.y;                           

       }

       else if(board[from.x][from.y]==8)

       {

         k2.x=to.x,k2.y=to.y;    

       }

       board[to.x][to.y]=board[from.x][from.y];

       board[from.x][from.y]=0;

  //之所以将该函数放在最后,是因为将不能相对这个函数只有在走了棋之后才可以进行判断。

       if(FaceToFace())

       {

         j=i;

         continue;               

       }

     //这里的语句颇为精妙!k!=k来进行黑白双方的换子,这个小伎俩在棋盘游戏的算法中颇为常见。

       k=!k;

     }

    //以下就是输出函数了!

     printf("Case %d: ",tt++);

     printf(!j?"Legal move":"Illegal move on step %d",j);

     puts("");

   }   

   return 0;

 }

 
 

 

posted on 2013-02-27 20:24  吴昊系列  阅读(854)  评论(0编辑  收藏  举报

导航