人机版五子棋开发之篇1
人机版五子棋开发之篇一
到如今,写五子棋也有一段时间了,虽然一开始的人人版并没用多长的时间,但后面的人机对战版可没让人头疼。从对思路的一无所知(在此感谢陆亮小盆友
的“精心”指导,呵呵)到了解算法的大概思想,再到真正实现过程中遇到的问题,一路走来,最深的体悟就是,有些错误是想出来而难的调试出来的,而且此种想问题的方法不是对着电脑看着代码想,那样会让人睡觉并且只会感到强烈的挫折感,而是在一个人路上想,去吃饭,去上自习,说不定哪个时候灵光一乍现,突然就发现症结所在了。可惜,经本人不怎么灵泛的大脑,找出问题重见蓝天的同时发现花儿早就谢了,因此也就没有了那种见到新鲜事物时的兴奋激动,唯有的是走出迷雾后的释然。但不管怎么说,现在总算是把那纠结的人机对战五子棋大体完工了。有图有真相,也让自己的思路进入正题。呵呵。


总体来说,它应该算有三岁小孩的智商,会守会攻,而且以攻为主,虽然这攻守都不怎么高级,但一不小心你也是会输的,呵呵。
废话讲了许多,下面进入正题,给这一五子棋来个阶段性的总结:
首先,算法思路:
并没有用到贪心法系统的算法策略,而只是自创的暴力型算法。
首先提供数据结构:一个棋盘二维数组,初始值全为0.用1代表下的黑子,-1代表下的白字。没下一颗子,相应位置上的值改变。 两个权值数组,分别记录玩家的当前棋盘和电脑的当前棋盘。用权值代表该位置的重要性,每放一个棋子,权值数组都重新赋值。
其次就是权值的设置问题,四连的权值最大,其次是活三连,再活二连等等。这里情况分的越细越好,但对本人这类菜鸟,情况越多会把问题搞的越复杂,因此也就作罢。只分了最简单的几种情况:四连,三连,二连,单个子,其中,三连二连每种情况又分双边活和单边活,因此也就六种不同的权值。
最后关键就是找到最大的权值点了。我的思路是,在玩家权值数组中找到权值最大的点,在电脑权值数组中也找到最大的点,再两个比较,谁大电脑就下子在那个权值大的位置。
接下来的问题就是每个权值数组中权值最大点怎么找了。其实无非也就是多次的用两个循环遍历二维数组,在每个点上做四个方向上的判断,求得每个方向上有几个相同 的子,并用四个变量做暂时记录,然后比较这四个变量,在权值数组中赋上这最多棋子个数对应的权值。
代码如下:
此段代码中注意:若if(VALUES_PALYER[MaxValuep.y][MaxValuep.x]<=VALUES_COM[MaxValuec.y][MaxValuec.x]) 改成<,则表示当玩家的权值点小于电脑的权值点时才取电脑的权值所在点,则此为以电脑防守为主。但<=则表示以电脑进攻为主。
下面是遍历权值数组:
下面是给点赋权值代码:
此处注意:vacant表示空格数,即要判断死活情况,当vacant=2时,表示两边空(相连棋子的一侧和该点的另一侧),vacant=1表示任何一边活。
下面是找权值数组中的最大值:
注意:此处可能有细心地朋友注意到,若有两个相同的权值怎么办?这时候应该选哪个点?在此,我是粗略的用遇到大的则覆盖,不比前一个位置的权值大就跳过的办法来处理的。当然此种处理不精确,也会影响到了机器的放子,即决定了他的聪明程度。不过这是第一个版本,也就暂且这么处理了。
关键的思路就到这了,最后只剩下一个判断输赢问题。需要注意的是,判断输赢肯定是写在一个判断输赢的类中,此时玩家下子后判断和电脑下子后判断传进来的参数不同,玩家下子判断后传进来的时鼠标所点击的位置交叉点的下标,而电脑下子后判断传进来的是电脑找到的最大权值所在位置的下标。
附: 其实,本人这种思路可能是最笨的一个了。完全没有必要用到两个权值数组,这完全是把简单问题复杂话,甚至引起中间有些大的漏洞,就像遇到相同权值的这种情况。另一种稍微简单点的思路是只用一个权值数组表示当前棋局,电脑计算出每个点白子的权值和黑子的权值,把他们相加,最后找到最大权值所在点放子。这样就省去了中间很多比较的环节。有兴趣的朋友可自己研究下。
最后,说说这段时间写五子棋过程中所收获的经验,互相学习:
1 在代码布局上,记住“一个类对应一个功能,一个方法实现一个功能”的原则。代码的结构很重要。本次虽然完成了人机对战的大体功能,但在代码布局上还有很大的问题,因此还得有下一个版本的完善。
2 通过这次五子棋的开发,进一步了解了重绘。
3 在得到画布(需在setVisible(true)之后)与调用画布顺序出冲突时,可以“曲线救国”,重新写一个方法,在该方法中用到画布。
例如:
4 在一个类中要用到某个接口中的成员,可以直接用接口类.成员,也可以用该类实现该接口,在类中直接使用。
5 学语言得边敲代码边总结,否者像我做玩家对战五子棋中遇到的问题就给忘了。可能下次遇到又得想个老半天了。
码了这么多字,却还不怎么想睡觉。没办法明天还得上课。朋友,晚安。呵呵
到如今,写五子棋也有一段时间了,虽然一开始的人人版并没用多长的时间,但后面的人机对战版可没让人头疼。从对思路的一无所知(在此感谢陆亮小盆友
总体来说,它应该算有三岁小孩的智商,会守会攻,而且以攻为主,虽然这攻守都不怎么高级,但一不小心你也是会输的,呵呵。
废话讲了许多,下面进入正题,给这一五子棋来个阶段性的总结:
首先,算法思路:
并没有用到贪心法系统的算法策略,而只是自创的暴力型算法。
首先提供数据结构:一个棋盘二维数组,初始值全为0.用1代表下的黑子,-1代表下的白字。没下一颗子,相应位置上的值改变。 两个权值数组,分别记录玩家的当前棋盘和电脑的当前棋盘。用权值代表该位置的重要性,每放一个棋子,权值数组都重新赋值。
其次就是权值的设置问题,四连的权值最大,其次是活三连,再活二连等等。这里情况分的越细越好,但对本人这类菜鸟,情况越多会把问题搞的越复杂,因此也就作罢。只分了最简单的几种情况:四连,三连,二连,单个子,其中,三连二连每种情况又分双边活和单边活,因此也就六种不同的权值。
最后关键就是找到最大的权值点了。我的思路是,在玩家权值数组中找到权值最大的点,在电脑权值数组中也找到最大的点,再两个比较,谁大电脑就下子在那个权值大的位置。
接下来的问题就是每个权值数组中权值最大点怎么找了。其实无非也就是多次的用两个循环遍历二维数组,在每个点上做四个方向上的判断,求得每个方向上有几个相同 的子,并用四个变量做暂时记录,然后比较这四个变量,在权值数组中赋上这最多棋子个数对应的权值。
代码如下:
else {//人机对战 System.out.println(MenuJPanel.jc.getSelectedItem()); if(chesses[i][j]==0) {//当前无子时才能放 g.setColor(Color.black); //玩家下黑子 g.fillOval(x - CHESS_SIZE / 2, y - CHESS_SIZE / 2, CHESS_SIZE, CHESS_SIZE); //下子之后该点权值赋值为1 chesses[i][j]=-1; //电脑找权值最大的点放子 g.setColor(Color.white); //遍历两棋盘 new MaxValue(VALUES_PALYER).lookforMaxValue(-1);//黑子为-1 new MaxValue(VALUES_COM).lookforMaxValue(1);//白字为1 /在两个权值数组中找最大权值的点 MaxValuep=getMAXVALUE(VALUES_PALYER); MaxValuec=getMAXVALUE(VALUES_COM); if(VALUES_PALYER[MaxValuep.y][MaxValuep.x]<=VALUES_COM[MaxValuec.y][MaxValuec.x]) { //进攻 Max=MaxValuec; }else {//防守 Max=MaxValuep; } int xx = X0+Max.x*SIZE; int yy = Y0+Max.y*SIZE;//最大权值坐在点的坐标(由下标转化而来)chesses[Max.y][Max.x]=1; g.fillOval(xx - CHESS_SIZE / 2, yy - CHESS_SIZE / 2, CHESS_SIZE, CHESS_SIZE);
此段代码中注意:若if(VALUES_PALYER[MaxValuep.y][MaxValuep.x]<=VALUES_COM[MaxValuec.y][MaxValuec.x]) 改成<,则表示当玩家的权值点小于电脑的权值点时才取电脑的权值所在点,则此为以电脑防守为主。但<=则表示以电脑进攻为主。
下面是遍历权值数组:
for(int i=0;i<ROWS;i++ ) { for(int j=0;j<COLUMNS;j++) { //给空白处的点赋值 if(chesses[i][j]==0) { //System.out.println(chesses[i++][j]); //System.out.println("开始"+ count++); m=i; n=j; num[0]=num[1]=num[2]=num[3]=0; vacant[0]=vacant[1]=vacant[2]=vacant[3]=0; //横向向右 while(n<COLUMNS-1&&chesses[m][++n]==color) { num[0]++; if(n<COLUMNS-1&&chesses[m][n+1]==0) vacant[0]++; } if(j>0&&chesses[i][j-1]==0) vacant[0]++; n=j; //横向向左 while(n>0&&chesses[m][--n]==color) { num[0]++; if(n>0&&chesses[m][n-1]==0) vacant[0]++; } if(j<COLUMNS-1&&chesses[i][j+1]==0) vacant[0]++; n=j; //纵向向下 while(m<ROWS-1&&chesses[++m][n]==color) { num[1]++; if(m<ROWS-1&&chesses[m+1][n]==0) vacant[1]++; } if(i>0&&chesses[i-1][j]==0) vacant[1]++; m=i; //纵向向上 while(m>0&&chesses[--m][n]==color) { num[1]++; if(m>0&&chesses[m-1][n]==0) vacant[1]++; } if(i<ROWS-1&&chesses[i+1][j]==0) vacant[1]++; m=i; //斜向左下 while(n>0&&m<ROWS-1&&chesses[++m][--n]==color) { num[2]++; if(m<ROWS-1&&n>0&&chesses[m+1][n-1]==0) vacant[2]++; } if(i>0&&j<ROWS-1&&chesses[i-1][j+1]==0) vacant[2]++; m=i;n=j; //斜向右上 while(n<COLUMNS-1&&m>0&&chesses[--m][++n]==color) { num[2]++; if(m>0&&n<COLUMNS-1&&chesses[m-1][n+1]==0) vacant[2]++; } if(i<ROWS-1&&j>0&&chesses[i+1][j-1]==0) vacant[2]++; m=i;n=j; //斜向右下 while(m<ROWS-1&&n<COLUMNS-1&&chesses[++m][++n]==color) { num[3]++; if(m<ROWS-1&&n<COLUMNS-1&&chesses[m+1][n+1]==0) vacant[3]++; } if(i>0&&j>0&&chesses[i-1][j-1]==0) vacant[3]++; m=i;n=j; //斜向左上 while(m>0&&n>0&&chesses[--m][--n]==color) { num[3]++; if(m>0&&n>0&&chesses[m-1][n-1]==0) vacant[3]++; } if(i<ROWS-1&&j<COLUMNS-1&&chesses[i+1][j+1]==0) vacant[3]++; m=i;n=j; int max = num[0]; int temp=0; //找出有相同棋子最多的边,并给该位置赋权值 for(int q=0;q<4;q++ ){ if(max<num[q]) { max=num[q]; temp=q; } } //给该点设置权值 setValue(values,max,vacant[temp],i,j); }
下面是给点赋权值代码:
public void setValue(int values[][],int num,int vacant,int i,int j) { if(num==1) {//一连 if(vacant==1) {//一活 values[i][j]=100; } else if(vacant==2) {//两活 values[i][j]=500; } }else if(num==2) { if(vacant==1) {//一活 values[i][j]= 200; }else if(vacant==2) {//两活 values[i][j]=2400; } }else if(num==3) { if(vacant==1) {//一活 values[i][j]=1000; }else if(vacant==2) {//两活 values[i][j]=5000; } }else if(num==4) { values[i][j]=10000; }else { values[i][j]=0; } }
此处注意:vacant表示空格数,即要判断死活情况,当vacant=2时,表示两边空(相连棋子的一侧和该点的另一侧),vacant=1表示任何一边活。
下面是找权值数组中的最大值:
//在两个权值数组中找最大权值 public Point getMAXVALUE(int values[][]) { Point max = new Point(); max.x = 0; max.y = 0; for(int i=0;i<ROWS;i++) { for(int j=0;j<COLUMNS;j++) { if(values[max.y][max.x]<values[i][j]) { max.y = i; max.x = j; } } } return max; }
注意:此处可能有细心地朋友注意到,若有两个相同的权值怎么办?这时候应该选哪个点?在此,我是粗略的用遇到大的则覆盖,不比前一个位置的权值大就跳过的办法来处理的。当然此种处理不精确,也会影响到了机器的放子,即决定了他的聪明程度。不过这是第一个版本,也就暂且这么处理了。
关键的思路就到这了,最后只剩下一个判断输赢问题。需要注意的是,判断输赢肯定是写在一个判断输赢的类中,此时玩家下子后判断和电脑下子后判断传进来的参数不同,玩家下子判断后传进来的时鼠标所点击的位置交叉点的下标,而电脑下子后判断传进来的是电脑找到的最大权值所在位置的下标。
附: 其实,本人这种思路可能是最笨的一个了。完全没有必要用到两个权值数组,这完全是把简单问题复杂话,甚至引起中间有些大的漏洞,就像遇到相同权值的这种情况。另一种稍微简单点的思路是只用一个权值数组表示当前棋局,电脑计算出每个点白子的权值和黑子的权值,把他们相加,最后找到最大权值所在点放子。这样就省去了中间很多比较的环节。有兴趣的朋友可自己研究下。
最后,说说这段时间写五子棋过程中所收获的经验,互相学习:
1 在代码布局上,记住“一个类对应一个功能,一个方法实现一个功能”的原则。代码的结构很重要。本次虽然完成了人机对战的大体功能,但在代码布局上还有很大的问题,因此还得有下一个版本的完善。
2 通过这次五子棋的开发,进一步了解了重绘。
3 在得到画布(需在setVisible(true)之后)与调用画布顺序出冲突时,可以“曲线救国”,重新写一个方法,在该方法中用到画布。
例如:
//添加棋盘 chessboard = new ChessJPanel(); //得到画布对象 g = chessboard.getGraphics(); chesslistener = new ChessListener(g,this); //添加监听器 public void addListener() { chessboard.addMouseListener(chesslistener); }
4 在一个类中要用到某个接口中的成员,可以直接用接口类.成员,也可以用该类实现该接口,在类中直接使用。
5 学语言得边敲代码边总结,否者像我做玩家对战五子棋中遇到的问题就给忘了。可能下次遇到又得想个老半天了。
码了这么多字,却还不怎么想睡觉。没办法明天还得上课。朋友,晚安。呵呵