刚毕业时,我在农村老家写了一篇棋谱
我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛
一、那是十年前的十年前
我老家是山东临沂农村的,记得我小时候,可玩的东西有很多,其中有一项我非常喜欢,那就是下棋。
“下棋”这个词,很多人第一感觉是很文雅,因为它属于“琴棋书画”之一。但是在农村,它可能会打破你的想象。
首先,什么人下棋?都是农村老头儿,一般在冬天阳光下的街头,或是夏天烈日下的树荫,此时节没法干活,无事可做。他们胡子拉碴,脸也不洗,一屁股坐在泥土地面上,讲究点儿的垫块砖头铺层秸秆。他们在地面上划出线条作为棋盘,用石子儿和树枝条当棋子,这棋一下就是一上午。然后,他们下什么棋?围棋?象棋?不好意思,那需要专业器具,他们下的棋可以不受时间、空间限制,信手拈来,名字都是方言土语,比如:“四”、“六”、“憋死牛”等等。
我,十来岁时,就爱凑到老头儿堆里看下棋,其中我最喜欢的一种棋叫:大炮轰小兵。
后来,我进城上学,学了软件编程。大学毕业那年,二十来岁,我怀念旧时光,于是把“大炮轰小兵”搬上了智能手机,并且我给它取了个高端的名字,叫:兵将棋。
兵将棋上线后,并没有太多人关注。因为,那是我的童年,不是别人的童年。因此,这一沉寂又是十年。
十年后,我三十来岁,恰逢掘金搞了这么个活动,我打算把“兵将棋”重新展现给网友,公布规则、算法以及源码,以纪念我那二十年的青春。
二、兵将棋展示
下面是兵将棋的主界面,如需体验可到文章末尾处获取,整个安装包只有1.5M大小,无任何权限。
2.1 棋盘和布局
棋盘是6行6列,总共36个交叉点,交叉点是棋子活动的区域。
其中白棋(将)2个,横向并排在下3横的中心位置。黑棋(兵)18个,铺满上三横所有位置。
2.2 走法和规则
因为将(白棋)少兵(黑棋)多,所以将(白棋)先走,作为开局。
2.2.1 将(白棋)的走法
将(白棋)者,骑着战马,手持长矛,可远距离斩杀士兵(黑棋)。
规则上,只允许隔1个空格吃掉敌人。
在无兵可杀的时候,它每次只能移动1格。它最喜欢走一步出现两个击杀对象的情况。此招式土语叫:一拉两观子,表示往下一拉,一下看着两个棋子,对方顾此失彼,自己必胜无疑。
将(白棋)的胜利在于将对方杀的片甲不留,吃掉对方所有棋子。
2.2.2 兵(黑棋)的走法
兵(黑棋)者,个体单薄,会被任意斩杀,但是可以形成人肉围墙,需要依靠团队的力量取胜。
兵(黑棋)每次只能移动1格,和将(白棋)紧贴在一起是没有危险的,把将(黑棋)围堵到无路可走视为胜利。
2.2.3 游戏功能介绍
游戏支持单人游戏(人机对战),多人游戏(人人对战),点击即可进入。可以通过开新局,选择玩家身份,以及地图。
单人游戏(人机对战)可以实现同机器人下棋。
游戏过程中,支持无限次悔棋。
三、重点实现代码讲解
本游戏的源码已经完全开放,如需下载可到文件末尾处获取地址。
源码可运行在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列是一个黑棋,但是想要准确画出来它,还是要费一番周折的。
下面是详细代码:
/**
* 根据棋子所在的行列数返回在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的代码屏蔽掉,它就会出现,棋子会飞,并且随意残杀。
写程序更多的是处理异常情况,要做到不管用户怎么操作,只有规则内是起作用的。
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;//将坐标数组返回
}
用户的小手指按下之后,获取了点击的棋子信息,然后交给BasePlayer
的play(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) {
}
它主要的目的是,处理玩家点击了棋子之后的事情,虽然你只是点击了一个棋子,其实它会牵扯很多事情的:
- 这个棋子第一次选择,我要选中它。
- 之前选择了一个棋子,这次又点击了这个棋子一下,放弃选择。
- 之前选择了一个棋子,这次又点击另一个棋子,换一个选择。
- 之前选择了一个棋子,这次又点击了另一个棋子,要吃子。
- 之前选择了一个棋子,这次又点击了空白,要走棋。
- ……
上面的代码,基本就是描述的这个逻辑。
有了选择棋子,就知道它下一步该干什么了。不管是走步,还是吃子,只要更新地图,记录历史,重新绘制棋盘就可以了。
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人机对战
上大学那会儿,学习山寨机编程,那时候游戏里就有人机对战,那里的人机对战就开始叫人工智能了,其实都是各种if
和else
判断。
我这个也是。
但是,这里面有一些思路,是值得借鉴的,比如凭什么走这一步就比那一步要好,你的判断逻辑是什么?
首先,电脑玩家也是一个玩家,需要建立一个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);
}
……
}
区别之处在于,普通玩家是点击屏幕选棋子,而电脑玩家则是自动计算选棋子。
所以,你看ComputerPlayer
的paly
方法被重写,它并没有使用什么屏幕点击传来位置坐标,屏幕随便点击一下就行,完全根据游戏当前的局势和自己的身份,自己调用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;
}
上面可以针对每种身份,写出单独的方法。因为这是我刚毕业时写的,把所有身份融合到一起了,到里面再用if
、esle
去判断。这种方式后期不好维护,写法也不规范,请大神们忍让。
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;//选为最佳
}
}
四、下一个十年
上面说的,都是最基础、最朴素的小白走法,没有任何城府,和那时刚毕业的我一样幼稚。
其实,还应该考虑下一步之后的情况,甚至几步之后的情况。
拿兵(黑棋)来说,走这一步虽然看似堵住了将(白棋),但这是局部的胜利。因为下一步将(白棋)就会吃了你。
拿将(白棋)来说,走这一步虽然看似吃了一个兵(黑棋),但这也是局部胜利。因为下一步兵(黑棋)会将你团团围住。
除此之外,还有很多套路。比如兵(黑棋)故意送死,从而请将(白棋)入瓮,将其堵死。
这时将(白棋)就需要判断后面的后面会怎么样,甚至后面的后面还会有反转的反转。
上面所述种种情况,一旦反映到程序上,复杂的就不是一点半点了,复杂到用无限的if
和else
来描述。
如果你想要写好这个游戏,首先你得是下棋的高手,另外你也得是编程的高手。
但是,从今往后,我们可以借助于深度学习和神经网络,它只需要你简单了解走棋的规则以及网络模型的结构,其他的交给机器去学习,只需要经过几个小时的训练,就可以让你的程序具有100个大爷30年的功力。
人工智能时代的游戏,大家快点去参与吧。
不要等明天,就是现在开始,让我们一起学习吧。
游戏安装包apk的下载地址和项目源码地址:
地址1:github.com/xitu/game-g… 下【兵将棋-TF男孩】文件夹。
地址2:兵将棋-TF男孩
特别说明:
安装包只有1.5M大小,无任何权限请求。
源码除SDK外,未引用任何第三方包,全部手写代码,是新手入门游戏编程的教科书。
推荐阅读