亲测,我这里用的AI算法还是蛮强的
其中,类是继承JPanel的
public void Showui(){ JFrame jf = new JFrame(); //初始化 jf.setSize(1300,1045); jf.setTitle("五子棋"); jf.setLocationRelativeTo(null); jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); //布局管理器 BorderLayout br = new BorderLayout(); jf.setLayout(br); //面板 jf.add(this,br.CENTER); JPanel eastJpanel = new JPanel(); eastJpanel.setPreferredSize(new Dimension(250,0)); jf.add(eastJpanel,br.EAST); //添加鼠标监听器 mouseListener mouseListener = new mouseListener(); this.addMouseListener(mouseListener); //添加右侧按钮和按钮监听器 ArrayListfunctionlist = new ArrayList(); functionlist.add("开始"); functionlist.add("悔棋(一步)"); functionlist.add("悔棋(多步)"); functionlist.add("重新开始"); functionlist.add("和棋"); functionlist.add("人机执黑"); functionlist.add("人机执白"); functionlist.add("提示"); for(String f:functionlist){ JButton a = new JButton(f); a.setPreferredSize(new Dimension(200,50)); eastJpanel.add(a); a.addActionListener(mouseListener); } //可见 jf.setVisible(true); //从窗口获取画板并传递给鼠标监听器 Graphics g = this.getGraphics(); mouseListener.setG(g); mouseListener.setUi(this); }
这些没有什么好说的,在之前我写的创意画板中都有具体解释
由于要绘制棋盘,而且棋盘每次绘制都要加载,所以将棋盘的绘制方法写在paint()中
public void paint(Graphics g){ Graphics2D d2 = (Graphics2D)g; g = d2; d2.setStroke(new BasicStroke(2));//设置线段的粗细 super.paint(g);//注意,必不可少 for (int i = 0; i < 15; i++) { g.drawLine(45+65*i,45,45+65*i,955); } for (int i = 0; i < 15; i++) { g.drawLine(45,45+65*i,955,45+65*i); } g.fillOval(235,235,10,10); g.fillOval(235,755,10,10); g.fillOval(755,235,10,10); g.fillOval(755,755,10,10); g.fillOval(495,495,10,10); g.drawLine(25,25,975,25); g.drawLine(25,25,25,975); g.drawLine(975,975,25,975); g.drawLine(975,975,975,25);//绘制棋盘,根据你之前绘制的窗口大小进行划分,绘制 }
在棋盘绘制好了之后,我们要在上面下棋就要绘制棋子,所以这里我们新建chess类
public class chess { private int x,y;//棋子在棋盘上的坐标值 private int flag;//棋子的三种状态flag=0:没有落子,flag=1:落白字,flag=2,:落黑子 private int value;//棋子的权值 //对应get,set方法 public int getValue() { return value; } public void setValue(int value) { this.value = value; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getFlag() { return flag; } public void setFlag(int flag) { this.flag = flag; } //构造器 public chess(){ x = 0; y = 0; flag = 0; value = 0; } public chess(int x, int y) { this.x = x; this.y = y; } public chess(int x, int y, int flag) { this.x = x; this.y = y; this.flag = flag; } }
这里我提供了三种构造器,也不是一开始就全写出来的,是在写后续程序的时候根据需要后添加的。
新建mouselistener类实现MouseListener,ActionListener接口。
在释放方法中获取释放时鼠标在组件中的坐标,代入并调用playchess()方法
这里我用了flag旗帜来达到黑白交替进行下棋的功能
public void playChees(int x, int y, Graphics g){ x = x - 45; y = y - 45; //选择就近点 if (x % 65 < 32.5) { x = x - x % 65; } else { x = x - x % 65 + 65; } if (y % 65 < 32.5) { y = y - y % 65; } else { y = y - y % 65 + 65; } //如果无子,则下棋 if (enterflag) { //下棋flag确定黑白棋 if (flag) {//黑子 //画子 g.fillOval(x - 32 + 45, y - 32 + 45, 65, 65); flag = false; } else {//白子 //画子 g.clearRect(x - 32 + 45, y - 32 + 45, 65, 65); g.drawOval(x - 32 + 45, y - 32 + 45, 65, 65); flag = true; } }
现在,棋子绘制好了,我们就遇到了一个问题,如果在下棋的时候下在了已经有棋子的地方,就会覆盖之前已经下的棋子,这明显是不符合实际规则的
所以我这里新建了三个集合用于保存已经下下去的棋子,顺带保存下子的顺序,方面后面实现悔棋功能
public static boolean flag = true;//黑白棋 public static ArrayListxlist = new ArrayList(); public static ArrayList ylist = new ArrayList(); public static ArrayList flaglist = new ArrayList();
然后,每计算出鼠标释放点对应的坐标之后,就遍历这两个集合,用于校验是否有子
//检查该点是否有子 for (int i = 0; i < xlist.size(); i++) { if (x == xlist.get(i) - 13 && y == ylist.get(i) - 13) { enterflag = false; break; }else { enterflag = true; } }
用enterflag旗帜判断是否该绘制棋子,如果需要绘制则将棋子数据保存进入集合中
//将棋子数据保存进集合 xlist.add(x + 13); ylist.add(y + 13); flaglist.add(flag);
现在,我们已经可以在棋盘上交替的下黑白子了。
下一步就该定制规则了,这里我们可以选择创建二维数组用于存取chess数据,这个二维数组可以在内存中展现整个棋盘的黑白空三种形式。
chess[][] arr = new chess[15][15];//存子的xy
用startflag控制开始,在未按开始前不能绘制棋子,在按下“开始”按钮后,startflag = true;将二维数组arr清零
if(e.getActionCommand() == "开始"){ startflag = true; //先将棋盘遍布flag = 0的棋子 for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { arr[i][j] = new chess(i*65+45,j*65+45); } } }
然后在每次绘制棋子时,将棋子存入其坐标对应的二维数组的位置中
//黑子 chess blackchess = new chess(x+45,y+45); blackchess.setFlag(2); arr[x/65][y/65] = blackchess;
//白子 chess whitechess = new chess(x+45,y+45); whitechess.setFlag(1); arr[x/65][y/65] = whitechess;
现在我们已经在内存层面拥有了棋盘的数据,可以通过分析arr数组来判断是否有获胜的情况了
判断情况需要在每下一个棋子之后对该棋子的横向,纵向,正对角线向,反对角线向进行判断
JOptionPane op = new JOptionPane(); //初始化变量用来存储在 水平 垂直 正对角线 反对角线方向上的连续棋子 int count1=1; int count2=1; int count3=1; int count4=1; //获取刚刚下的棋子在数组中对应的位置(坐标转换) x = x/65; y = y/65; //水平方向上 for (int i = y+1; i < 15; i++) { if(arr[x][y].getFlag() == arr[x][i].getFlag()){ count1++; }else { break; } } for (int i = y-1; i >= 0; i--) { if(arr[x][y].getFlag() == arr[x][i].getFlag()){ count1++; }else { break; } } //竖直方向上 for (int i = x+1; i < 15; i++) { if(arr[x][y].getFlag() == arr[i][y].getFlag()){ count2++; }else { break; } } for (int i = x-1; i >= 0; i--) { if(arr[x][y].getFlag() == arr[i][y].getFlag()){ count2++; }else { break; } } //正对角线方向 for (int i = x+1,j = y-1; i < 15 && j >=0; i++,j--) { if(arr[x][y].getFlag() == arr[i][j].getFlag()){ count3++; }else { break; } } for (int i = x-1,j = y+1; i >= 0 && j < 15; i--,j++) { if(arr[x][y].getFlag() == arr[i][j].getFlag()){ count3++; }else { break; } } //反对角线方向 for (int i = x+1,j = y+1; i < 15 && j < 15; i++,j++) { if(arr[x][y].getFlag() == arr[i][j].getFlag()){ count4++; }else { break; } } for (int i = x-1,j = y-1; i >= 0 && j >= 0; i--,j--) { if(arr[x][y].getFlag() == arr[i][j].getFlag()){ count4++; }else { break; } } if(count1 >= 5 || count2 >= 5 || count3 >= 5|| count4 >= 5){ if(arr[x][y].getFlag()==1) op.showMessageDialog(null, "白棋胜出"); else op.showMessageDialog(null, "黑棋胜出"); startflag = false; }
代码很简单,思路清晰就很容易搞懂
整个游戏的核心部分
由于也是刚学Java没多久,强博弈算法的实现对于现在的我还较为困难,所以这里我选择了一个简单,但是实战却又表现很好的算法
具体思路参照这篇博客人机版五子棋两种算法概述_叶飘っ叶相偎 的博客-CSDN博客_五子棋算法
简单来说,五子棋是五个子练成线就算赢,所以这个算法将棋盘划分为572个五元组,即为所有可能练成五个子的地方都是一个五元组,然后根据五元组内部黑白空三种情况的数量对这个五元组进行加权赋予权值,每一次落子都遍历一遍这572个五元组,算出棋盘上所有点的权值,然后选取状况为空的点中权值最高的点落子。
具体的权值在我给的博客中有介绍,也可以在我给的代码中查看
//计算权重 //游戏核心! public void concluate(chess ch){ int blackNum = 0;//五元组中的黑棋数量 int whiteNum = 0;//五元组中的白棋数量 int tupScore = 0;//五元组得分临时变量 //1.横向15行 for (int i = 0; i < 15; i++) { for (int j = 0; j < 11; j++) { int k = j; while (k < j+5){ if(arr[i][k].getFlag() == 1){ whiteNum++; }else if(arr[i][k].getFlag() == 2){ blackNum++; } k++; } tupScore = tupleScore(blackNum,whiteNum); //为五元组添加分数 for (k = j; k < j+5; k++) { arr[i][k].setValue(tupScore); } //归零 blackNum = 0;//五元组中的黑棋数量 whiteNum = 0;//五元组中的白棋数量 tupScore = 0;//五元组得分临时变量 } } //纵向15行 for (int i = 0; i < 15; i++) { for (int j = 0; j < 11; j++) { int k = j; while (k < j+5){ if(arr[k][i].getFlag() == 1){ whiteNum++; }else if(arr[k][i].getFlag() == 2){ blackNum++; } k++; } tupScore = tupleScore(blackNum,whiteNum); for (k = j; k < j+5; k++) { arr[k][i].setValue(tupScore); } //归零 blackNum = 0; whiteNum = 0; tupScore = 0; } } //右上角到左下角的上侧部分 for (int i = 14; i >= 4 ; i--) { for (int k = i,j = 0;j < 15 && k >= 0;j++,k--){ int m = k; int n = j; while (m > k - 5 && k - 5 >= -1){ if(arr[m][n].getFlag() == 1){ whiteNum++; }else if(arr[m][n].getFlag() == 2){ blackNum++; } m--; n++; } if(m == k-5){ tupScore = tupleScore(blackNum,whiteNum); for(m = k, n = j; m > k - 5 ; m--, n++){ arr[m][n].setValue(tupScore); } } blackNum = 0; whiteNum = 0; tupScore = 0; } } //右上角到左下角下侧部分 for(int i = 1; i <= 14; i++){ for(int k = i, j = 14; j >= 0 && k < 15; j--, k++){ int m = k; int n = j; while(m < k + 5 && k + 5 <= 15){ if(arr[m][n].getFlag() == 1){ whiteNum++; }else if(arr[m][n].getFlag() == 2){ blackNum++; } m++; n--; } if(m == k+5){ tupScore = tupleScore(blackNum,whiteNum); for(m = k, n = j; m < k + 5; m++, n--){ arr[m][n].setValue(tupScore); } } blackNum = 0; whiteNum = 0; tupScore = 0; } } //左上角到右下角上侧部分 for(int i = 0; i < 11; i++){ for(int j = 0, k = i; j < 15 && k < 15; j++, k++){ int m = k; int n = j; while(m < k + 5 && k + 5 <= 15){ if(arr[m][n].getFlag() == 1){ whiteNum++; }else if(arr[m][n].getFlag() == 2){ blackNum++; } m++; n++; } if(m == k + 5){ tupScore = tupleScore(blackNum,whiteNum); for(m = k, n = j; m < k + 5; m++, n++){ arr[m][n].setValue(tupScore); } } blackNum = 0; whiteNum = 0; tupScore = 0; } } //左上角到右下角下侧部分 for(int i = 1; i < 11; i++){ for(int k = i, j = 0; j < 15 && k < 15; j++, k++){ int m = k; int n = j; while(m < k + 5 && k + 5 <= 15){ if(arr[n][m].getFlag() == 1){ whiteNum++; }else if(arr[n][m].getFlag() == 2){ blackNum++; } m++; n++; } if(m == k + 5){ tupScore = tupleScore(blackNum,whiteNum); for(m = k, n = j; m < k + 5; m++, n++){ arr[n][m].setValue(tupScore); } } blackNum = 0; whiteNum = 0; tupScore = 0; } } } //各种情况评分 public int tupleScore(int blackNum, int whiteNum){ if(blackNum > 0 && whiteNum > 0){ return 0; } if(blackNum == 0 && whiteNum == 0){ return 7; } if(whiteNum == 1){ return 35; } if(whiteNum == 2){ return 800; } if(whiteNum == 3){ return 15000; } if(whiteNum == 4){ return 800000; } if(blackNum == 1){ return 15; } if(blackNum == 2){ return 400; } if(blackNum == 3){ return 1800; } if(blackNum == 4){ return 100000; } return -1; } //遍历flag = 0的棋子选取最大权重 public chess che(){ chess chessmax = new chess(); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if(chessmax.getValue() < arr[i][j].getValue() && arr[i][j].getFlag() == 0){ chessmax = arr[i][j]; } } } if(chessmax.getValue() == 0){ JOptionPane op = new JOptionPane(); op.showMessageDialog(null,"和棋"); startflag = false; } //清空权重 for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { arr[i][j].clearValue(); } } return chessmax; }
上面我将的算法在cacluate()方法中实现
在che()方法中选取了flag=0的点位中最大权值点返回,注意其中我添加了和棋的情况,当棋盘上所有的五元组都不能成线时,就达到了和棋的条件。
然后AI()方法调用了这两个方法,所以我们当我们需要AI时,只需要调用AI()方法就行了
具体怎么实现人机的黑子先行和白字先行根据自己的想法来就行了
悔棋分为两种情况
人人悔棋和人机悔棋
其中有划分为悔棋和强制悔棋,因为一般悔棋只能悔棋一步,想要悔棋多布则需要强制悔棋
if(e.getActionCommand() == "悔棋(一步)"){ if(name.equals("人机执黑")){ if(huiqiflag){ if(xlist.size()>0){ for (int i = 0; i < 2; i++) { int x = (xlist.get(step)-13)/65; int y = (ylist.get(step)-13)/65; arr[x][y] = new chess(x*65+45,y*65+45); xlist.remove(step); ylist.remove(step); flaglist.remove(step); step--; ui.rep(g); } } } huiqiflag = false; }else if(huiqiflag){ if(xlist.size()>0){ int x = (xlist.get(step)-13)/65; int y = (ylist.get(step)-13)/65; arr[x][y] = new chess(x*65+45,y*65+45); xlist.remove(step); ylist.remove(step); flaglist.remove(step); step--; flag = !flag; ui.rep(g); } huiqiflag = false; } }else if(e.getActionCommand() == "悔棋(多步)"){ if(name.equals("人机执黑")){ if(xlist.size()>0){ for (int i = 0; i < 2; i++) { int x = (xlist.get(step)-13)/65; int y = (ylist.get(step)-13)/65; arr[x][y] = new chess(x*65+45,y*65+45); xlist.remove(step); ylist.remove(step); flaglist.remove(step); step--; ui.rep(g); } } }else if(xlist.size()>0){ int x = (xlist.get(step)-13)/65; int y = (ylist.get(step)-13)/65; arr[x][y] = new chess(x*65+45,y*65+45); xlist.remove(step); ylist.remove(step); flaglist.remove(step); step--; ui.rep(g); }
具体的代码实现思路没有什么绕的地方,注意不要有遗漏就好
面板重绘是在我们创建的面板发生边框改变或者移动之后,会自动调用paint方法,所以我们需要在paint方法中把棋子的数据导入,这样我们在下棋下一半时,不管怎么摆弄面板,只要不叉掉,我们已经下的棋子的数据都会呈现在面板上
for (int i = 0; i < mouseListener.xlist.size(); i++) { if(mouseListener.flaglist.get(i)){ g.fillOval(mouseListener.xlist.get(i), mouseListener.ylist.get(i),65,65); }else { g.setColor(new Color(238,238,238)); g.fillRect(mouseListener.xlist.get(i), mouseListener.ylist.get(i),65,65); g.setColor(Color.black); g.drawOval(mouseListener.xlist.get(i), mouseListener.ylist.get(i),65,65); } } if(mouseListener.flaglist.size()!=0){ mouseListener.flag = !mouseListener.flaglist.get(mouseListener.flaglist.size()-1);
其中的颜色调试是我自己慢慢调试的,由于调用fillRect方法时默认使用的是背景色,所以这里我将画笔改为了背景色