刚毕业那年,我写了个农村老家的下棋游戏

1,768 阅读11分钟

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

一、那是十年前的十年前

我老家是山东临沂农村的,记得我小时候,可玩的东西有很多,其中有一项我非常喜欢,那就是下棋。

“下棋”这个词,很多人第一感觉是很文雅,因为它属于“琴棋书画”之一。但是在农村,它可能会打破你的想象。

首先,什么人下棋?都是农村老头儿,一般在冬天阳光下的街头,或是夏天烈日下的树荫,此时节没法干活,无事可做。他们胡子拉碴,脸也不洗,一屁股坐在泥土地面上,讲究点儿的垫块砖头铺层秸秆。他们在地面上划出线条作为棋盘,用石子儿和树枝条当棋子,这棋一下就是一上午。然后,他们下什么棋?围棋?象棋?不好意思,那需要专业器具,他们下的棋可以不受时间、空间限制,信手拈来,名字都是方言土语,比如:“四”、“六”、“憋死牛”等等。

我,十来岁时,就爱凑到老头儿堆里看下棋,其中我最喜欢的一种棋叫:大炮轰小兵。

后来,我进城上学,学了软件编程。大学毕业那年,二十来岁,我怀念旧时光,于是把“大炮轰小兵”搬上了智能手机,并且我给它取了个高端的名字,叫:兵将棋。

兵将棋上线后,并没有太多人关注。因为,那是我的童年,不是别人的童年。因此,这一沉寂又是十年。

十年后,我三十来岁,恰逢掘金搞了这么个活动,我打算把“兵将棋”重新展现给网友,公布规则、算法以及源码,以纪念我那二十年的青春。

二、兵将棋展示

下面是兵将棋的主界面,如需体验可到文章末尾处获取,整个安装包只有1.5M大小,无任何权限。

Snip20220410_4.png

2.1 棋盘和布局

棋盘是6行6列,总共36个交叉点,交叉点是棋子活动的区域。

其中白棋(将)2个,横向并排在下3横的中心位置。黑棋(兵)18个,铺满上三横所有位置。

2.2 走法和规则

因为将(白棋)少兵(黑棋)多,所以将(白棋)先走,作为开局。

2.2.1 将(白棋)的走法

将(白棋)者,骑着战马,手持长矛,可远距离斩杀士兵(黑棋)。

规则上,只允许隔1个空格吃掉敌人。

大炮吃子.gif

在无兵可杀的时候,它每次只能移动1格。它最喜欢走一步出现两个击杀对象的情况。此招式土语叫:一拉两观子,表示往下一拉,一下看着两个棋子,对方顾此失彼,自己必胜无疑。

一拉两观子.gif

将(白棋)的胜利在于将对方杀的片甲不留,吃掉对方所有棋子。

将军胜利.gif

2.2.2 兵(黑棋)的走法

兵(黑棋)者,个体单薄,会被任意斩杀,但是可以形成人肉围墙,需要依靠团队的力量取胜。

兵(黑棋)每次只能移动1格,和将(白棋)紧贴在一起是没有危险的,把将(黑棋)围堵到无路可走视为胜利。

士兵胜利.gif

2.2.3 游戏功能介绍

游戏支持单人游戏(人机对战),多人游戏(人人对战),点击即可进入。可以通过开新局,选择玩家身份,以及地图。

人机对战.gif

单人游戏(人机对战)可以实现同机器人下棋。

人机对战2.gif

游戏过程中,支持无限次悔棋。

悔棋.gif

三、重点实现代码讲解

本游戏的源码已经完全开放,如需下载可到文件末尾处获取地址。

源码可运行在Android Studio下,精简通俗,带有注释。

因内容较多,此处只讲解部分关键代码。

3.1 绘制棋局

首先,我们建立一个GameView类,它继承自View,是游戏界面相关的主要类。

public class GameView extends View {

    /**
     * 画图方法
     */
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        clearCanvas(canvas);    // 清空画布
        drawChessBoard(canvas); // 绘制棋盘
        drawQiZi(canvas);       // 绘制棋子
    }
}

重写onDraw方法,可以通过它在界面上绘制图形。

3.1.1 绘制棋盘

首先,确定要画几道线,例如要画6道线(6纵6横),那就循环6次,每次拉开间距,画出横线和竖线。

/**
 * 画棋盘
 *
 * @param canvas
 */
private void drawChessBoard(Canvas canvas) {
    for (int i = 0; i < lineNumber; i++) {
        canvas.drawLine(startX, startY + cellWidth * i, startX + cellWidth * (lineNumber - 1), startY + cellWidth * i, paint);
        canvas.drawLine(startX + cellHeight * i, startY, startX + cellHeight * i, startY + cellHeight * (lineNumber - 1), paint);
    }
}

3.1.2 绘制棋子

下面是绘制棋子的代码:

private void drawQiZi(Canvas canvas) {
    Bitmap qizi = null;
    for (int i = 0; i < map.length; i++) {//根据map里的排列进行画棋子,依照行
        for (int j = 0; j < map[i].length; j++) {//根据map里的排列进行画棋子,依照列
            int type = map[i][j];//获取这个棋子的类型
            if (type != RoleBean.ROLE_BLANK) {//如果不是空白
                if (type == RoleBean.ROLE_GENERAL) {//是将军
                    qizi = img_balu;//把当前棋子设置成将军的棋子
                } else if (type == RoleBean.ROLE_SOLDIER) {//是士兵
                    qizi = img_guizi;//把当前棋子设置成士兵的棋子
                }
                int[] xy = getPointXyByCellXy(i, j, qizi);
                int x = xy[0];//设置棋子的坐标
                int y = xy[1];
                canvas.drawBitmap(qizi, x, y, paint);//画棋子
            }//end  (type != ROLE_BLANK)
        }//end  map[i].length
    }//end  map.length
}

先定义了一个Bitmap图片,它是兵(黑棋)还是将(白棋),它的位置在哪里,后面会根据逻辑赋值。

第一个循环的map是地图,里面标记了各类棋子在哪里。

// 5横5纵的地图
public static int[][] Map_5 = {
         {1,1,1,1,1}
        ,{1,1,1,1,1}
        ,{0,0,0,0,0}
        ,{0,2,0,2,0}
        ,{0,0,0,0,0}
};
// 6横6纵的地图
public static int[][] Map_6 = {
         {1,1,1,1,1,1}
        ,{1,1,1,1,1,1}
        ,{1,1,1,1,1,1}
        ,{0,0,0,0,0,0}
        ,{0,0,2,2,0,0}
        ,{0,0,0,0,0,0}
};
// 7横7纵的地图
public static int[][] Map_7 = {
         {1,1,1,1,1,1,1}
        ,{1,1,1,1,1,1,1}
        ,{1,1,1,1,1,1,1}
        ,{1,1,1,1,1,1,1}
        ,{0,0,0,0,0,0,0}
        ,{0,0,2,2,2,0,0}
        ,{0,0,0,0,0,0,0}
};

如上所示,二维数组正好对应了棋盘的横竖线,其中元素“1”表示兵(黑棋),“2”表示将(白棋),“0”表示空位。通过循环的方式,将每一个元素画到棋盘上。

其中需要特别说明一下的是int[] xy = getPointXyByCellXy(i, j, qizi)这段代码中的getPointXyByCellXy方法,它的作用是根据地图数组中的第几行、第几列,以及棋子图片,算出棋子在当前View下绘制的(x,y)坐标。 我们知道,在Canvas上想要绘制一个图片,需要提供横纵坐标,这个坐标指的是图像左上角的坐标。所以,虽然我们知道第1行第1列是一个黑棋,但是想要准确画出来它,还是要费一番周折的。

image.png

下面是详细代码:

/**
 * 根据棋子所在的行列数返回在View上的物理坐标
 *
 * @param cellX 每个格子横轴的间距
 * @param cellY 每个格子纵轴的间距
 * @return
 */
public int[] getPointXyByCellXy(int cellX, int cellY, Bitmap bitmap) {
    int x = 0, y = 0;
    if (bitmap != null) {
        x = startX + cellY * cellWidth - bitmap.getWidth() / 2;
        y = startY + cellX * cellHeight - bitmap.getHeight() / 2;
    } else {
        x = startX + cellY * cellWidth;
        y = startY + cellX * cellHeight;
    }
    return new int[]{x, y};
}

3.2 走棋的规则

前面我们已经了解到,兵(黑棋)和将(白棋)每次只能走一个格子,但是将(白棋)有个特权,那就是如果和兵(黑棋)隔1个空白,是可以直接吃掉兵(黑棋)的。

那么,这些规则反应到代码上会是怎么样呢?它要比上面说的复杂的多。

下面的ChessRule这个类,主要用于规则管理,里面定义了一些属性:

public class ChessRule {

   private int[][] map;//棋盘的布局
   private int fromX; // 出发位置X
   private int fromY;// 出发位置Y
   private int toX;// 目的位置X
   private int toY;// 目的位置Y
   private int moveChessID;//起始位置是什么棋子
   private int targetID;//目的地是什么棋子或空地
   
   ……

我们由简到难,先看兵(黑棋)的逻辑。

3.2.1 兵(黑棋)的走棋逻辑

兵(黑棋)走棋很简单,就是每次走一个格子。但是,反应到代码这里,情况就多了。

/**
 * 判断士兵能不能走棋
 * @return false不让走,true可以移动
 */
public  boolean canMove(){

   if (fromX < 0 || fromX > (lineNumber-1) || fromY < 0 || fromY > (lineNumber-1)){
      //1、X轴超出屏幕的,不移动
      return false;
   }
   
   if (toX < 0 || toX > (lineNumber-1) || toY < 0 || toY > (lineNumber-1)){
      //2、Y轴超出屏幕的,不移动
      return false;
   }
   
   if(fromX==toX && fromY==toY){
       //3、目的地与出发点相同,不移动
      return false;
   }
   
   if((Math.abs(fromY - toY) + Math.abs(toX - fromX)) > 1){
       //4、步长超过1的,不移动
      return false;
   }
   
   if(map[toY][toX]!=0){
       //5、如果终点有棋,不移动
      return false;
   }
   
   //除此之外的其他情况,返回true可以移动
   return true;
   
} 

那位说了,上面情况会出现吗?

当然会!因为你无法限制用户的行为。

比如,我们把情况4、5的代码屏蔽掉,它就会出现,棋子会飞,并且随意残杀。

兵无规则.gif

写程序更多的是处理异常情况,要做到不管用户怎么操作,只有规则内是起作用的。

3.2.2 将(白棋)的走棋逻辑

将(白棋)的走棋和兵(黑棋)的走棋类似,也是每次走一个格子。但是除此之外,它还有一个特性,那就是隔一个空格有敌人的情况下,它可以一次走2格,并且取代敌人的位置。

由此,也会增加很多额外的代码:

/**
 * 判断将军能不能走棋
 * @return false不让走,true可以移动
 */
public  boolean canMove(){

    ……
    
    moveChessID = map[fromY][fromX];//得到起始棋子
    targetID = map[toY][toX];//得带终点棋子
    if(isSameSide(moveChessID,targetID)){
        // 如果起点和终点都是自己人,不移动
        // 因为它可以吃子,但是不能自吃,所以需要判断
       return false;
    }
    
    if (targetID == 0) {
        // 如果将军的目的地是空地时
       if(Math.abs(fromY - toY)  + Math.abs(toX - fromX) > 1){
           // 超过一个格子,不能移动 
          return false
       }
       
    }else{ // 将军的目的地不是空地,上面排查自己人了,此处肯定是敌人
       if(fromY!=toY && fromX!=toX){
           // 是斜线时,不移动
          return false;
       }
       if(toY == fromY){
           // 横着走时
          if(Math.abs(toX - fromX) != 2){
              // 和目标敌人的距离不是2时,不能移动
             return false;
          }
          if(fromX > toX){
              // 向左走,中间不是空格,不能移动
             if(map[toY][toX+1] != 0){
                return false;
             }
          }else{// 向右走,中间不是空格,不能移动
             if(map[toY][toX-1] != 0){
                return false;
             }
          }
       }
       
       if(toX == fromX){
           // 竖着走时
          if(Math.abs(toY - fromY) != 2){
              // 和目标敌人的距离不是2时,不能移动
             return false;
          }
          if(toY > fromY){
              // 向下走,中间不是空格,不能移动
             if(map[toY-1][toX] != 0){
                return false;
             }
          }else{// 向上走,中间不是空格,不能移动
             if(map[toY+1][toX] != 0){
                return false;
             }
          }
       }
    }
    
    // 其他情况,可以移动
    return true;
}

3.2.3 悔棋和走棋的视觉逻辑

悔棋,就是回退记录。那么,我们首先要把每一步都记录下来,才能回退。

所以,在GameView中,有一个存放棋局历史的变量allSteps,它是一个存放二维数组的列表。

public class GameView extends View {

    ……
    public ArrayList<int[][]> allSteps;

}

之前,我们介绍了棋子是根据地图画上去的。地图的定义也是一个二维数组,比如下面这个:

// 5横5纵的地图
int[][] Map_5 = {
     {1,1,1,1,1}
    ,{1,1,1,1,1}
    ,{0,0,0,0,0}
    ,{0,2,0,2,0}
    ,{0,0,0,0,0}
};

当棋局变化时,肯定是棋子位置或者数量发生了变化。比如,开局2(将军)吃了个1(士兵),那么局势就会变为如下:

// 走第一步后的局面
int[][] step1 = {
     {1,1,1,1,1}
    ,{1,2,1,1,1}
    ,{0,0,0,0,0}
    ,{0,0,0,2,0}
    ,{0,0,0,0,0}
};

当每走一步棋时,allSteps.add(step1)一下。当悔棋时,执行一下allSteps.remove(size-1)。然后,刷新一下界面,就做到了棋局视觉的更新。

3.3 玩家和输赢

面向对象编程,肯定要有对象。

这里面,将军和士兵,都是玩家,属于不同的角色。因此,我们要有一个玩家类BasePlayer

/**
 * 玩家基础类
 */
public class BasePlayer {

   GameView gameView; // 游戏视图
   int playerID; // 身份,将军还是士兵
   
   // 选择了哪个棋,要走到哪里,-1代表未选择
   public int selectX = -1, selectY = -1, targetX = -1, targetY = -1; 
   public int selectID = -1, targetID = -1; 
   
   public boolean isFocus = false; // 是否选中了棋子
   private boolean isEnable = false;//是否玩家可以控制,该对方走棋时,你不能动
   
   // 构造方法
   public BasePlayer(GameView gameView, int playerID){
      this.gameView = gameView; // 你面临的棋局
      this.playerID = playerID; // 你的身份
   }
   
   ……
   
}

3.3.1 选择棋子和移动棋子

当用户从GameView上触摸棋盘时,我们根据绘制的逻辑,可以判断出来用户点击了哪个棋子。

如果你脑补不出来,我可以贴上代码:

/**
 * 根据点击的物理坐标转换成棋盘点对应的行列数
 *
 * @param e 触摸事件
 * @return
 */
public int[] getPos(MotionEvent e) {
    //将坐标换算成数组的维数
    int[] pos = new int[2];
    double x = e.getX();//得到点击位置的x坐标
    double y = e.getY();//得到点击位置的y坐标
    
    int d = img_qizi.getHeight() / 2;
    if (e.getX() > startX - d && e.getX() < startX + cellWidth * lineNumber + d 
        && e.getY() > startY - d && e.getY() < startY + cellWidth * lineNumber + d) {
        //点击的是棋盘时
        pos[0] = Math.round((float) ((y - startY) / cellHeight));//取得所在的行
        pos[1] = Math.round((float) ((x - startX) / cellWidth));//取得所在的列
    } else {//点击的位置不是棋盘时
        pos[0] = -1;//将位置设为不可用
        pos[1] = -1;
    }
    return pos;//将坐标数组返回
}

用户的小手指按下之后,获取了点击的棋子信息,然后交给BasePlayerplay(int[] pointIJ)方法,这个方法是选择棋子。

/**
 * 根据用户点击,选择棋子
 * @param pointIJ 点击的位置
 */
public void play(int[] pointIJ){

   int i = pointIJ[0];
   int j = pointIJ[1];
   
   if (i != -1 && j != -1) {//如果选择的是有效棋子
      if (isFocus) {//之前选择过
         if (gameView.map[i][j] != selectID) {//后来选的不是自己的棋子
            //意思就是,要么吃棋,要么走空格
            targetX = i;
            targetY = j;
            targetID = gameView.map[i][j];
            ChessRule cr = new ChessRule(gameView.map, selectY, selectX, targetY, targetX);
            if (cr.canMove()) {
               ChessMove cm = new ChessMove(selectID, selectY, selectX, targetID, targetY, targetX, 0);
               runPoint(cm);
               selectX = -1;
               selectY = -1;
               selectID = -1;
               targetX = -1;
               targetY = -1;
               targetID = -1;
            }else{
               selectX = -1;
               selectY = -1;
               selectID = -1;
            }
         }
         isFocus = !isFocus;
      }else{
          //之前没有选择过,第一次肯定要选择自己的棋子,第一次不可以选择空白和对方的棋子
         //选的就设为起点
         if (gameView.map[i][j] == playerID) {
            // 播放音效,选中棋子的“哒哒”声
            SoundUtil.playSound(SoundUtil.SOUND_SELECT);
            selectX = i; 
            selectY = j;
            selectID = gameView.map[selectX][selectY];
            targetX = -1; 
            targetY = -1;
            targetID = -1;
            isFocus = !isFocus;
         }// end if (gameView.map[i][j] == playerID)
      }//else{
   }//end if (i != -1 && j != -1) {
}

它主要的目的是,处理玩家点击了棋子之后的事情,虽然你只是点击了一个棋子,其实它会牵扯很多事情的:

  1. 这个棋子第一次选择,我要选中它。
  2. 之前选择了一个棋子,这次又点击了这个棋子一下,放弃选择。
  3. 之前选择了一个棋子,这次又点击另一个棋子,换一个选择。
  4. 之前选择了一个棋子,这次又点击了另一个棋子,要吃子。
  5. 之前选择了一个棋子,这次又点击了空白,要走棋。
  6. ……

上面的代码,基本就是描述的这个逻辑。

有了选择棋子,就知道它下一步该干什么了。不管是走步,还是吃子,只要更新地图,记录历史,重新绘制棋盘就可以了。

3.3.2 输赢的判断

输赢是相对的,将(白棋)赢了,其实就是兵(黑棋)输了,这两个指的同一件事。

此处,我们就拿赢来说吧。

将(白棋)赢的条件是什么?就是它杀光了所有的兵(黑棋),兵(黑棋)的数量变为0。

/**
 * 将军玩家胜利 
 * @return
 */
public boolean winChess(){
   
   int count=0;
   for (int i = 0; i < gameView.map.length; i++) {
      for (int j = 0; j < gameView.map[i].length; j++) {
         if (gameView.map[i][j] == RoleBean.ROLE_SOLDIER) {//如果某一个棋子是士兵
            count++;//数量给加1
         }
      }
   }
   if (count == 0) {
      return true;
   }
   
   return false;
}

那么,兵(黑棋)赢的条件又是什么呢?恭喜你都会抢答了,就是:将(白棋)被堵死,将(白棋)的可移动数量为0。

在“2.2.1 将(白棋)的走法”中,我们描述了将(白棋)可移动的条件,如果所有情况都返回false,那说明它无路可走了。

//向上走1格
ChessRule chessRule10 = new ChessRule(map, x, y, x, y-1);
if(chessRule20.canMove()){
    ……
 }
 //向下走1格
ChessRule chessRule20 = new ChessRule(map, x, y, x, y+1);
if(chessRule10.canMove()){
    ……
}
//向左走1格
ChessRule chessRule30 = new ChessRule(map,  x, y, x-1, y);
if(chessRule30.canMove()){
    ……
}
//向右走1格
ChessRule chessRule40 = new ChessRule(map,  x, y, x+1, y);
if(chessRule40.canMove()){
    ……
}
//向下走2格
ChessRule chessRule102 = new ChessRule(map, x, y, x, y+2);
if(chessRule102.canMove()){
    ……
}
//向上走2格
//向左走2格
//向右走2格

3.4 AI人机对战

上大学那会儿,学习山寨机编程,那时候游戏里就有人机对战,那里的人机对战就开始叫人工智能了,其实都是各种ifelse判断。

我这个也是。

但是,这里面有一些思路,是值得借鉴的,比如凭什么走这一步就比那一步要好,你的判断逻辑是什么?

首先,电脑玩家也是一个玩家,需要建立一个ComputerPlayer类,它继承了基础玩家类BasePlayer,基础玩家该有的选棋,走棋,悔棋,赢棋,这些行为它都有。

/**
 * 电脑玩家的类
 */
public class ComputerPlayer extends BasePlayer {

   GameView gameView;
   int playerID;
   AIPlayer aiPlayer; 
   
   public ComputerPlayer(GameView gameView, int playerID) {
      super(gameView, playerID);
      this.gameView = gameView;
      this.playerID = playerID;
      aiPlayer = new AIPlayer();
   }

   @Override
   public void play(int[] pointIJ) {
      // 重写走棋方法
      ChessMove cm = aiPlayer.searchAGoodMove(gameView.map, playerID);
      runPoint(cm);
   }
   
 ……  
 
 }

区别之处在于,普通玩家是点击屏幕选棋子,而电脑玩家则是自动计算选棋子。

所以,你看ComputerPlayerpaly方法被重写,它并没有使用什么屏幕点击传来位置坐标,屏幕随便点击一下就行,完全根据游戏当前的局势和自己的身份,自己调用runPoint走棋。

其关键点就是人工智能玩家AIPlayer寻找最佳走棋searchAGoodMove这个方法。


/**
 * 人工智能
 */ 
public class AIPlayer {

    // 对走法进行优劣评估,选出一个最好的走法
    public ChessMove searchAGoodMove(int[][] qizi, int chessRole){//查询一个好的走法
            List<ChessMove> ret = allPossibleMoves(qizi,chessRole);//产生所有走法
            int id = bestsorce(qizi,ret,chessRole);// 去评分
            return ret.get(id); // 返回最好结果
    }
	
    //对走法进行优劣评估,选出一个最好的走法;
    public int bestsorce(int[][] qizi,List<ChessMove> ret, int chessRole){

    }
    
    ……
}

最佳走法,其实就是一个原则,那就是“趋利避害”,这个走法肯定是让我方壮大,让敌方减损的。

它的逻辑就分两步,先找出所有走法,然后通过算法给每种走法打分,选出分数最高的那个,就是最佳走法。

一般机器人所谓的“初级”、“中级”、“高级”的电脑棋手,其实就是对应的不同得分的走法。

3.4.1 先找到所有走法

找到所有走法很简单,只要遍历一遍当前棋盘的所有位置,如果发现位置上有棋子,判断一下是什么身份,然后根据身份依据走法规则,穷举每一种情况,对每一步进行规则验证,如果验证通过,则加入到走法列表。

/**
 * 获得所有的走法
 * @param map 当前局势地图
 * @param chessRole 角色
 * @return
 */
public List<ChessMove> allPossibleMoves(int[][] map, int chessRole){

   List<ChessMove> moveList =new ArrayList<ChessMove>();//产生所有走法

   // 循环每一个格子,找到棋子
   for (int y = 0; y < lineNumber; y++){
      for (int x = 0; x < lineNumber; x++){
         //循环所有的位置
         int chessman = map[y][x];
         if (RoleBean.ROLE_SOLDIER == chessman){// 如果是士兵
            //能不能向下走1格
            ChessRule chessRule1 = new ChessRule(map, x, y, x, y+1);
            if(chessRule1.soldierCanMove()){ // 如果合规可以走,加入走法列表
               moveList.add(new ChessMove(chessman, x, y, map[x][y+1], x, y+1, 0));
            }
            //向上走1格
            ……
            //向左走1格
            ……
            //向右走1格
            ……
         }else if (RoleBean.ROLE_GENERAL == chessman){// 如果是将军
            //能不能向下走1格
            ChessRule chessRule10 = new ChessRule(map, x, y, x, y+1);
            if(chessRule10.generalCanMove()){ // 如果合规可以走,加入走法列表
               moveList.add(new ChessMove(chessman, x, y, map[x][y+1],  x, y+1, 0));
            }
            //向上走1格
            ……
            //向左走1格
            ……
            //向右走1格
            ……
            //向下走2格
            ……
            //向上走2格
            ……
            //向左走2格
            ……
            //向右走2格
            ……
         }
      }
   }
   ……
   return moveList;
}

上面可以针对每种身份,写出单独的方法。因为这是我刚毕业时写的,把所有身份融合到一起了,到里面再用ifesle去判断。这种方式后期不好维护,写法也不规范,请大神们忍让。

3.4.2 为每一种走法打分

所有走法有了,机器人该选择哪一种呢?这就bestSorce的作用。


/**
 * 对走法进行优劣评估,选出一个最好的走法
 * @param map 当前局势地图
 * @param ret 所有走法
 * @param chessRole 角色
 * @return 最佳走法的索引
 */
public int bestSorce(int[][] map,List<ChessMove> ret, int chessRole){
    
     ……
     return bestIndex;
 }

对于兵(黑棋)来说,它的目的是堵死将(白棋)。

如果,它走的这步棋,把对手堵死了,获得胜利,那这步棋就是就最佳走法。

如果没法一招制敌,那它走的这一步能让将(白棋)的走法变少,可缩小敌人的活动范围,那相对一步废棋来说,这也算是最佳走法。

下面看一看,通过代码如何找到兵(黑棋)的最佳走法:

// 循环出自己的所有走法,拿出每一步进行判断
// 如果真走了这一步,判断下对手还有多少走法
List<ChessMove> list =  allPossibleMoves(testqizi, RoleBean.ROLE_GENERAL);

if (list == null) { // 如果对手找不到走法,那说明被堵死
    return i; // 返回这一步作为最佳走法
}

int count = list.size();
if (count < general_min_steps) {// 如果这个走法比其他走法让对手更窘迫
  general_min_steps = count; // 记录下窘迫值,下一次还得比较
  bestID = i; // 记录下这个ID,目前它是最优走法
}

上面的操作,就是找将(白棋)的allPossibleMoves的最小值。

下面再来说将(白棋)如何找最优的走法。将(白棋)的目的是吃光兵(黑棋)的棋子。

如果,它走的这步棋,兵(黑棋)数量变为了0,那这步棋就是就最佳走法。

如果没法一招制敌,那它走的这一步能让兵(黑棋)的数量减少,可削弱敌人的数量,那相对一步废棋来说,这也算是最佳走法。另外,相同条件下,就算是一步废棋,也得让自己可活动范围越来越开阔,这也是相对更优走法。

// 循环出自己的所有走法,拿出每一步进行判断
// 走了这一步后,判断自己还有多少步可走
List<ChessMove> list =  allPossibleMoves(testqizi, RoleBean.ROLE_GENERAL);
int mySteps = 0;
if (list != null) {
    mySteps = list.size(); // 记录自己作答步数
}

int soldierCount = chessRule.getSoldierCount(); // 对手还有多少人数

if (soldierCount <= minSoldierCount) {//如果走这一步能使士兵数量减少,那肯定首选这一步
    minSoldierCount = soldierCount; // 记录下目前最小士兵数量
    bestID = i;//选为最佳
    //相同条件下,将军走的这一步,不但能使士兵减少,而且还能使自己活动范围加大
    if (mySteps >= myMaxSteps) {
       myMaxSteps = mySteps;
       bestID = i;//选为最佳
    }
}

四、下一个十年

上面说的,都是最基础、最朴素的小白走法,没有任何城府,和那时刚毕业的我一样幼稚。

其实,还应该考虑下一步之后的情况,甚至几步之后的情况。

拿兵(黑棋)来说,走这一步虽然看似堵住了将(白棋),但这是局部的胜利。因为下一步将(白棋)就会吃了你。

将诡计.gif

拿将(白棋)来说,走这一步虽然看似吃了一个兵(黑棋),但这也是局部胜利。因为下一步兵(黑棋)会将你团团围住。

兵诡计.gif

除此之外,还有很多套路。比如兵(黑棋)故意送死,从而请将(白棋)入瓮,将其堵死。

连续.gif

这时将(白棋)就需要判断后面的后面会怎么样,甚至后面的后面还会有反转的反转。

上面所述种种情况,一旦反映到程序上,复杂的就不是一点半点了,复杂到用无限的ifelse来描述。

如果你想要写好这个游戏,首先你得是下棋的高手,另外你也得是编程的高手。

但是,从今往后,我们可以借助于深度学习和神经网络,它只需要你简单了解走棋的规则以及网络模型的结构,其他的交给机器去学习,只需要经过几个小时的训练,就可以让你的程序具有100个大爷30年的功力。

人工智能时代的游戏,大家快点去参与吧。

不要等明天,就是现在开始,让我们一起学习吧。

游戏安装包apk的下载地址和项目源码地址:

地址1:github.com/xitu/game-g… 下【兵将棋-TF男孩】文件夹。

地址2:兵将棋-TF男孩

特别说明:
安装包只有1.5M大小,无任何权限请求。
源码除SDK外,未引用任何第三方包,全部手写代码,是新手入门游戏编程的教科书。