完整代码已上传到github上,地址:https://github.com/Alexlingl/GoBang。有需要的可以自取。
五子棋系列博客(总共三篇,从简单功能简单界面到人机对战,以及较美观的登录及对战界面。第三篇博客中有最终实现的界面效果):
JAVA五子棋的实现(一)
JAVA五子棋的实现(二)
在JAVA五子棋的实现(二)中我们已经实现了以下几个功能:1.判断输赢;2.实现悔棋操作;3.实现认输操作。还差一个人机对战还没有实现。今天我们就来实现这个人机对战。同样地在开始之前我们还是要做一些准备工作。
一、涉及到的问题
1.考虑谁先下棋
如果是AI先下棋则必须考虑AI的第一步落在哪里,这里一般可以默认它下中间的点即可。
如果是人先下则不用考虑这种情况。我们这里选择了人先下。
2.定义一个权值数组,用来储存棋盘中各个位置的权值
AI在下棋时必须考虑当前的整个棋盘情况,乍一听感觉无从下手,毕竟整个棋盘位置那么多,怎么找到合适的位置。因此我们在这里引入了一个权值数组,这个数组保存了当前棋盘中所有位置的权值信息。权值越大则说明这个位置越优。因此每一次AI在下棋时就相当于在搜索权值数组,只要找到最大权值对应的位置即可。这里我们所要做的就是设计一个算法来计算出各个位置的权值。
3.影响权值的因素——棋子相连的情况
我们把有棋子的位置权值都设置为0,计算权值的时候跳过这些位置,只计算空位。而每一个空位的权值大小会受到它身边的位置的影响。具体受到多少个位置的影响呢?八个方向上的四个位置。如图
对于每一个空位我们只需要考虑这八个方向上五个棋子相连的情况即可。棋子相连的情况我们把它分为活连和眠连。如果在一个方向上棋子没有被另一种颜色的棋子堵住,我们就称之为活连,否则为眠连。活连又分为四种:活1连、活2连、活3连、活4连;同样的眠连也分为四种:眠1连、眠2连、眠3连、眠4连。(其实4连可以不分眠和活,它们两个的紧急程度是一样的)最后我们还要考虑同一直线上两个方向结合起来的相连情况,我称之为联合相连,比如如果往左有是一个活1连,往右是一个活2连,那么当前位置就相当于一个活3连。
4.用HaspMap的API类对象来储存各种相连情况对应的权值
用法大致如下
static HashMap map = new HashMap();
static {
map.put("010",10);
map.put("020",10);
map.put("01010",80);
map.put("02020",80);
}
其中0表示空位,1表示黑棋子,2表示白棋子。当然上面只是一个示例,实际情况比这个多得多。
权值如何给定?A.随着相连的棋子数增加,它的权值要相应地增加;B.相同的相连棋子数,活连的权值要比眠连要大;C.人执黑,AI 执白。如果相同的相连情况,黑子的权值大于白子,那么AI就偏防守;如果黑子的权值小于白子,那么AI偏进攻。由于黑子先手,一般可以使用黑子的权值大于白子,让AI进行后手防守。(我这里用的、AI用的就是偏防守)。
细节:A.比如当出现某个空位出现了4连,那么这个时候不管其他位置情况如何,我们都必须先下这个位置,因此4连的权值要比3连大得多,保证它一定能够最先被选择。B.如果出现了权值相同的位置怎么办?我们这里默认选择第一个位置。
二、权值算法
1.人下棋后先判断输赢,如果赢了则游戏结束,否则就进入AI算法
2.AI算法要做的事情就是遍历棋盘的每一个空位置,根据当前棋盘的棋子数组,来确定与该空位相关的棋子相连情况。然后和在Haspmap中进行匹配搜索,找到相应的权值,把这个权值加到当前位置。
3.遍历权值数组,选择最大的权值,找到相应位置落子
4.判断输赢,如果赢了则游戏结束,否则回到人下棋。
三、细节的改进
在进行实现人机对战的实现过程中,发现了原有五子棋的一些不足,因此做了一些改进。
1.实现可选框的功能选择。
先对可选框设置时间监听时间,然后利用可选框JBoxComb自带的setSelectedItem方法来获取当前可选框的内容
2.“开始新游戏”之前的界面控制
我们前面是通过控制给界面添加监听事件来控制的。只有“开始新游戏”的按钮被点击时,我们才给界面添加监听机制。这个后来发现不太实用。就改成了用turn来控制。初始化turn=0,当turn=0时则不进入执行下棋的代码。
四、关键部分的代码
//棋子相连情况的划分
public static HashMap map = new HashMap();//设置不同落子情况和相应权值的数组
static {
//被堵住
map.put("01", 17);//眠1连
map.put("02", 12);//眠1连
map.put("001", 17);//眠1连
map.put("002", 12);//眠1连
map.put("0001", 17);//眠1连
map.put("0002", 12);//眠1连
map.put("0102",17);//眠1连,15
map.put("0201",12);//眠1连,10
map.put("0012",15);//眠1连,15
map.put("0021",10);//眠1连,10
map.put("01002",19);//眠1连,15
map.put("02001",14);//眠1连,10
map.put("00102",17);//眠1连,15
map.put("00201",12);//眠1连,10
map.put("00012",15);//眠1连,15
map.put("00021",10);//眠1连,10
map.put("01000",21);//活1连,15
map.put("02000",16);//活1连,10
map.put("00100",19);//活1连,15
map.put("00200",14);//活1连,10
map.put("00010",17);//活1连,15
map.put("00020",12);//活1连,10
map.put("00001",15);//活1连,15
map.put("00002",10);//活1连,10
//被堵住
map.put("0101",65);//眠2连,40
map.put("0202",60);//眠2连,30
map.put("0110",65);//眠2连,40
map.put("0220",60);//眠2连,30
map.put("011",65);//眠2连,40
map.put("022",60);//眠2连,30
map.put("0011",65);//眠2连,40
map.put("0022",60);//眠2连,30
map.put("01012",65);//眠2连,40
map.put("02021",60);//眠2连,30
map.put("01102",65);//眠2连,40
map.put("02201",60);//眠2连,30
map.put("00112",65);//眠2连,40
map.put("00221",60);//眠2连,30
map.put("01010",75);//活2连,40
map.put("02020",70);//活2连,30
map.put("01100",75);//活2连,40
map.put("02200",70);//活2连,30
map.put("00110",75);//活2连,40
map.put("00220",70);//活2连,30
map.put("00011",75);//活2连,40
map.put("00022",70);//活2连,30
//被堵住
map.put("0111",150);//眠3连,100
map.put("0222",140);//眠3连,80
map.put("01112",150);//眠3连,100
map.put("02221",140);//眠3连,80
map.put("01101",1000);//活3连,130
map.put("02202",800);//活3连,110
map.put("01011",1000);//活3连,130
map.put("02022",800);//活3连,110
map.put("01110", 1000);//活3连
map.put("02220", 800);//活3连
map.put("01111",3000);//4连,300
map.put("02222",3500);//4连,280
}
//人机对战部分的代码
//AI联合算法
public Integer unionWeight(Integer a,Integer b ) {
//必须要先判断a,b两个数值是不是null
if((a==null)||(b==null)) return 0;
//一一
else if((a>=10)&&(a<=25)&&(b>=10)&&(b<=25)) return 60;
//一二、二一
else if(((a>=10)&&(a<=25)&&(b>=60)&&(b<=80))||((a>=60)&&(a<=80)&&(b>=10)&&(b<=25))) return 800;
//一三、三一、二二
else if(((a>=10)&&(a<=25)&&(b>=140)&&(b<=1000))||((a>=140)&&(a<=1000)&&(b>=10)&&(b<=25))||((a>=60)&&(a<=80)&&(b>=60)&&(b<=80)))
return 3000;
//二三、三二
else if(((a>=60)&&(a<=80)&&(b>=140)&&(b<=1000))||((a>=140)&&(a<=1000)&&(b>=60)&&(b<=80))) return 3000;
else return 0;
}
public void mouseClicked(java.awt.event.MouseEvent e) {
//如果选择的是人机对战
else {
if(gf.turn==1) {
//人先落子
//先获取要落的地方
g.setColor(Color.black);
//落子
g.fillOval(countx-size/2, county-size/2, size, size);
//设置当前位置已经有棋子了,棋子为黑子
gf.isAvail[Arrayi][Arrayj]=1;
//把当前所下的棋子位置保存在动态数组中
gf.ChessPositonList.add(new ChessPosition(Arrayi,Arrayj));
gf.turn++;
//判断是否已经出现五科棋子了
//列判断
//首先界定数组范围,防止越界
int Blackimin=Arrayi-4,Blackimax=Arrayi+4;
if(Blackimin<0) Blackimin=0;
if(Blackimax>14) Blackimax=14;
int count1=0;//判断相连的棋子数
for(int i=Blackimin;i<=Blackimax;i++) {
if(gf.isAvail[i][Arrayj]==1) count1++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count1=0;
if(count1==5) {
System.out.println("黑方赢");
gf.PopUp("黑方赢");
return;
}
}
//行判断
//首先界定数组范围,防止越界
int Blackjmin=Arrayj-4,Blackjmax=Arrayj+4;
if(Blackjmin<0) Blackjmin=0;
if(Blackjmax>14) Blackjmax=14;
int count2=0;//判断相连的棋子数
for(int j=Blackjmin;j<=Blackjmax;j++) {
if(gf.isAvail[Arrayi][j]==1) count2++;
else count2=0;
if(count2==5) {
System.out.println("黑方赢");
gf.PopUp("黑方赢");
return;
}
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
}
//135度判断
//首先界定数组范围,防止越界
int count3=0;//判断相连的棋子数
for(int i=-4;i<=4;i++) {
if((Arrayi+i>=0)&&(Arrayj+i>=0)&&(Arrayi+i<=14)&&(Arrayj+i<=14)) {
if(gf.isAvail[Arrayi+i][Arrayj+i]==1) count3++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count3=0;
if(count3==5) {
System.out.println("黑方赢");
gf.PopUp("黑方赢");
return;
}
}
}
int count4=0;//判断相连的棋子数
for(int i=-4;i<=4;i++) {
if((Arrayi+i>=0)&&(Arrayj-i>=0)&&(Arrayi+i<=14)&&(Arrayj-i<=14)) {
//System.out.print("count4:"+count4);
if(gf.isAvail[Arrayi+i][Arrayj-i]==1) count4++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count4=0;
if(count4==5) {
System.out.println("黑方赢");
gf.PopUp("黑方赢");
return;
}
}
}
//机器落子
//先计算出各个位置的权值
for(int i=0;i=jmin;positionj--) {
//依次加上前面的棋子
ConnectType=ConnectType+gf.isAvail[i][positionj];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueleft=gf.map.get(ConnectType);
if(valueleft!=null) gf.weightArray[i][j]+=valueleft;
//往右延伸
ConnectType="0";
int jmax=Math.min(14, j+4);
for(int positionj=j+1;positionj<=jmax;positionj++) {
//依次加上前面的棋子
ConnectType=ConnectType+gf.isAvail[i][positionj];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueright=gf.map.get(ConnectType);
if(valueright!=null) gf.weightArray[i][j]+=valueright;
//联合判断,判断行
gf.weightArray[i][j]+=unionWeight(valueleft,valueright);
//往上延伸
ConnectType="0";
int imin=Math.max(0, i-4);
for(int positioni=i-1;positioni>=imin;positioni--) {
//依次加上前面的棋子
ConnectType=ConnectType+gf.isAvail[positioni][j];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valueup=gf.map.get(ConnectType);
if(valueup!=null) gf.weightArray[i][j]+=valueup;
//往下延伸
ConnectType="0";
int imax=Math.min(14, i+4);
for(int positioni=i+1;positioni<=imax;positioni++) {
//依次加上前面的棋子
ConnectType=ConnectType+gf.isAvail[positioni][j];
}
//从数组中取出相应的权值,加到权值数组的当前位置中
Integer valuedown=gf.map.get(ConnectType);
if(valuedown!=null) gf.weightArray[i][j]+=valuedown;
//联合判断,判断列
gf.weightArray[i][j]+=unionWeight(valueup,valuedown);
//往左上方延伸,i,j,都减去相同的数
ConnectType="0";
for(int position=-1;position>=-4;position--) {
if((i+position>=0)&&(i+position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+gf.isAvail[i+position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueLeftUp=gf.map.get(ConnectType);
if(valueLeftUp!=null) gf.weightArray[i][j]+=valueLeftUp;
//往右下方延伸,i,j,都加上相同的数
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i+position>=0)&&(i+position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+gf.isAvail[i+position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueRightDown=gf.map.get(ConnectType);
if(valueRightDown!=null) gf.weightArray[i][j]+=valueRightDown;
//联合判断,判断行
gf.weightArray[i][j]+=unionWeight(valueLeftUp,valueRightDown);
//往左下方延伸,i加,j减
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i+position>=0)&&(i+position<=14)&&(j-position>=0)&&(j-position<=14))
ConnectType=ConnectType+gf.isAvail[i+position][j-position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueLeftDown=gf.map.get(ConnectType);
if(valueLeftDown!=null) gf.weightArray[i][j]+=valueLeftDown;
//往右上方延伸,i减,j加
ConnectType="0";
for(int position=1;position<=4;position++) {
if((i-position>=0)&&(i-position<=14)&&(j+position>=0)&&(j+position<=14))
ConnectType=ConnectType+gf.isAvail[i-position][j+position];
}
//从数组中取出相应的权值,加到权值数组的当前位置
Integer valueRightUp=gf.map.get(ConnectType);
if(valueRightUp!=null) gf.weightArray[i][j]+=valueRightUp;
//联合判断,判断行
gf.weightArray[i][j]+=unionWeight(valueLeftDown,valueRightUp);
}
}
}
//打印出权值
for(int i=0;i14) imax=14;
count1=0;//判断相连的棋子数
for(int i=imin;i<=imax;i++) {
if(gf.isAvail[i][AIj]==2) count1++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count1=0;
if(count1==5) {
System.out.println("白方赢");
gf.PopUp("白方赢");
gf.turn=0;
return;
}
}
//行判断
//首先界定数组范围,防止越界
int jmin=AIj-4,jmax=AIj+4;
if(jmin<0) jmin=0;
if(jmax>14) jmax=14;
count2=0;//判断相连的棋子数
for(int j=jmin;j<=jmax;j++) {
if(gf.isAvail[AIi][j]==2) count2++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count2=0;
if(count2==5) {
System.out.println("白方赢");
gf.PopUp("白方赢");
gf.turn=0;
return;
}
}
//135度判断
//首先界定数组范围,防止越界
count3=0;//判断相连的棋子数
for(int i=-4;i<=4;i++) {
if((AIi+i>=0)&&(AIj+i>=0)&&(AIi+i<=14)&&(AIj+i<=14)) {
if(gf.isAvail[AIi+i][AIj+i]==2) count3++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count3=0;
if(count3==5) {
System.out.println("白方赢");
gf.PopUp("白方赢");
gf.turn=0;
return;
}
}
}
count4=0;//判断相连的棋子数
for(int i=-4;i<=4;i++) {
if((AIi+i>=0)&&(AIj-i>=0)&&(AIi+i<=14)&&(AIj-i<=14)) {
if(gf.isAvail[AIi+i][AIj-i]==2) count4++;
//如果出现了其他棋子,或者是没有棋子时,就重新开始计数
else count4=0;
if(count4==5) {
System.out.println("白方赢");
gf.PopUp("白方赢");
gf.turn=0;
return;
}
}
}
}
}
}
}
五、总结
1.对于Integer类型的数据,一定要先判断它是不是为null。null是不包括在else其他情况中的
public Integer unionWeight(Integer a,Integer b ) {
//必须要先判断a,b两个数值是不是null
if((a==null)||(b==null)) return 0;
2.权值数组要及时情况。每一次AI算法结束都要情况权值数组,因为下一次棋盘中的棋子情况又已经变了。
六、后期完善
后面对整个五子棋的界面和权值法进行了进一步的完善。AI基本可以到达中级水平。新的代码已经更新到github上了。
完善后的界面如下: