一:总体思路

1:与人人对战最主要不同,人机对战需要一个能够评价当前棋盘,并作出一个较为合理的抉择的AI。

此版本我用了权值法:权值法是指AI扫描棋盘每一个点,根据这个点周围的情况给点赋一个“权值”来评价在此点下棋的价值多大。

直观判断,如果黑棋方有如下(A)局面,那么在Q点下棋将是他下一步的必然选择(更不必说(B))




人机对弈五子棋python 五子棋人机对决_人机对弈五子棋python


(A)


人机对弈五子棋python 五子棋人机对决_java_02


(B)


有趣的是,黑方即使在如下局面,选择Q点依然是必然。(进攻就是最好的防守!!!)


人机对弈五子棋python 五子棋人机对决_Powered by 金山文档_03


人机对弈五子棋python 五子棋人机对决_权值_04


所以我们只要考察这个点某一方向的同样颜色连子的情况,再将各个方向的权值相加,就可以判断该点下棋的价值。

各种情况的权值怎么衡量呢?

1: 活连: 两端都是空位

- 010 10

- 0110 50

- 01110 100

- 011110 5000

- 020 10

- 0220 50

- 02220 100

- 022220 5000

- 2: 半活: 一端是空位,一端是边界或者对方棋子

- 01 8

- 011 40

- 0111 800

- 01111 5000

- 02 8

- 022 40

- 0222 800

- 02222 5000

0表示该点为空(可下棋),1表示黑棋,2表示白棋。在这种判断方法下,只有下在现有棋子旁边才有意义(包括是斜对角线挨着)(当然有些时候下的位置不挨着任何棋子才是妙手,但这就是这个版本是简单版的原因)。

那么怎么得到每一个点每一个方向的棋子情况呢?

构造的连子情况很苛刻,if分支就很多:

public int toRight(int[][] arr, int r, int c) {                                //c和r是0起步,0——15共16列
        String temS;
        if (arr[r][c] == 0) {                                 //只有这个0右边相邻的不是0才有意义赋权值
            if (c == arr[0].length - 1) {
                //System.out.println("1布");
                return 0;
            }
            int color = arr[r][c + 1];
            if (color == 0) {
                //System.out.println("————2布");
                return 0;
            } else {
                //System.out.println("——————3布");
                temS="0"+color;
                for (int i = c + 2; i < arr[0].length; i++) {    //最右边再向右没有权值
                    if (arr[r][i] == color) {
                        temS+=color;
                    } else if (arr[r][i] == 0) {
                        temS+="0";
                        return map.get(temS);
                    }else{
                        return map.get(temS);
                    }
                }
            }
        }
        //System.out.println("————————4布");
        return 0;
    }

这是对于右侧连子情况的判断,首先在此方法外要if判断以下这个点是否可下(为空),接下来判断是否是最右边的点(因为下面的代码对于最右边不兼容,会有数组越界问题),然后再判断该点右侧是否也为空。

接下来遍历右侧每个点考察是否有颜色相同的连子。注意最右边是空和最右边是边界或者棋子是不同情况。

补充一点:我们将各种连子情况与权值构成了hashmap,连子情况以字符串表示做key。

接下来八个方向都走一遍就好

public int[][] getCoderArr(int[][] array) {
        int[][] arr = new int[COLS][ROWS];
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array[0].length; j++) {
                if (array[i][j] == 0) {                  //不等于0的,返回值当默认值0处理
                    int coderSum = 0;
                    coderSum += toRight(array, i, j);
                    coderSum += toLeft(array, i, j);
                    coderSum += toUp(array, i, j);
                    coderSum += toDown(array, i, j);
                    coderSum += toNorthWest(array, i, j);
                    coderSum += toNorthEast(array, i, j);
                    coderSum += toSouthWest(array, i, j);
                    coderSum += toSouthEast(array, i, j);
                    arr[i][j] = coderSum;
                }
            }
        }
        return arr;
    }

2:有了一个AI后,我们还需要调整原本的鼠标监听器,从一点下一颗棋到一点下两颗棋。

public void mouseClicked(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();

        int r = (y - Y + SIZE / 2) / SIZE;
        int c = (x - X + SIZE / 2) / SIZE;
        if (chessFlag == 0) {
            JOptionPane.showMessageDialog(null, "还没开始");
            return;
        }
        if (r < 0 || c < 0 || r > ROWS || c > COLS) {
            JOptionPane.showMessageDialog(null, "不能在边界外下棋");
            return;
        }
        if (CHESS_ARRAY[r][c] != 0) {
            return;
        }
        CHESS_ARRAY[r][c] = chessFlag;
        chessList.add(new Chess(r, c, chessFlag));


        //人人对战
        if (choice == 1) {
            if (chessFlag == 1) {
                g.setColor(Color.BLACK);
                chessFlag = 2;
            } else if (chessFlag == 2) {
                g.setColor(Color.WHITE);
                chessFlag = 1;
            }
            int cx = c * SIZE + X - SIZE / 2;
            int cy = r * SIZE + Y - SIZE / 2;

            g.fillOval(cx, cy, SIZE, SIZE);


            if (check(r, c)) {
                String ans;
                if (CHESS_ARRAY[r][c] == 1) {
                    ans = "黑方";
                    blackWin++;
                } else {
                    ans = "白方";
                    whiteWin++;
                }
                int a = JOptionPane.showConfirmDialog(null, new Object[]{"再来一局?"}, "游戏结束,胜者是" + ans, JOptionPane.YES_NO_OPTION);

                if (a == 0) {
                    ui.startButton.setText("开始游戏");
                    for (int i = 0; i < ROWS; i++) {
                        for (int j = 0; j < COLS; j++) {
                            CHESS_ARRAY[i][j] = 0;
                        }
                    }
                    //btn.setText("开始游戏");
                    HISTORY.add((ArrayList<Chess>) chessList.clone());           //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
                    chessList.clear();
                    chessPanel.paint(g);
                    chessFlag = 0;

                } else {
                    ui.jf.setVisible(false);
                }
            }
        }


        //人机对战
        else if (choice == 2) {

            int cx = c * SIZE + X - SIZE / 2;
            int cy = r * SIZE + Y - SIZE / 2;
            g.setColor(Color.BLACK);
            g.fillOval(cx, cy, SIZE, SIZE);              //黑方画完验黑方

            if (check(r, c)) {
                String ans;
                blackWin++;
                int a = JOptionPane.showConfirmDialog(chessPanel, new Object[]{"再来一局?"}, "游戏结束,胜者是玩家,太遗憾了" , JOptionPane.YES_NO_OPTION);
                if (a == 0) {
                    ui.startButton.setText("开始游戏");
                    for (int i = 0; i < ROWS; i++) {
                        for (int j = 0; j < COLS; j++) {
                            CHESS_ARRAY[i][j] = 0;
                        }
                    }
                    //btn.setText("开始游戏");
                    HISTORY.add((ArrayList<Chess>) chessList.clone());           //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
                    chessList.clear();
                    chessPanel.paint(g);
                    chessFlag = 0;

                } else {
                    ui.jf.setVisible(false);
                }
            }else {

                int xAI = this.findMax()[1];
                int yAI = this.findMax()[0];

                CHESS_ARRAY[yAI][xAI] = 2;

                chessList.add(new Chess(yAI, xAI, 2));
                int cxAI = xAI * SIZE + X - SIZE / 2;
                int cyAI = yAI * SIZE + Y - SIZE / 2;
                g.setColor(Color.WHITE);
                g.fillOval(cxAI, cyAI, SIZE, SIZE);

                if (check(yAI, xAI)) {
                    whiteWin++;
                    int a = JOptionPane.showConfirmDialog(chessPanel, new Object[]{"再来一局?"}, "游戏结束,胜者是电脑,哈哈垃圾" , JOptionPane.YES_NO_OPTION);
                    if (a == 0) {
                        ui.startButton.setText("开始游戏");
                        for (int i = 0; i < ROWS; i++) {
                            for (int j = 0; j < COLS; j++) {
                                CHESS_ARRAY[i][j] = 0;
                            }
                        }
                        //btn.setText("开始游戏");
                        HISTORY.add((ArrayList<Chess>) chessList.clone());           //chesslist在clear之后,连带加入数组的也清空了(猜测是同一个地址的问题)
                        chessList.clear();
                        chessPanel.paint(g);
                        chessFlag = 0;

                    } else {
                        ui.jf.setVisible(false);
                    }
                }
            }
            //权值表
            int[][] AICheck = ai.getCoderArr(CHESS_ARRAY);
            for (int i = 0; i < AICheck.length; i++) {
                for (int j = 0; j < AICheck[i].length; j++) {
                    System.out.print(AICheck[i][j] + " ");
                }
                System.out.println(" ");
            }
            for (int i = 0; i < CHESS_ARRAY.length; i++) {
                for (int j = 0; j < CHESS_ARRAY[i].length; j++) {
                    System.out.print(CHESS_ARRAY[i][j]);
                }
                System.out.println(" ");
            }
        }
    }

    public int[] findMax() {
        int[][] codeArr = ai.getCoderArr(CHESS_ARRAY);
        int max = 0;
        int[] location = {0, 0};
        for (int i = 0; i < codeArr.length; i++) {
            for (int j = 0; j < codeArr[0].length; j++) {
                if (codeArr[i][j] > max) {
                    max = codeArr[i][j];
                    location = new int[]{i, j};
                }
            }
        }
        System.out.println("最大权值对应坐标为:"+ Arrays.toString(location));
        System.out.println("最大权值是:"+max);
        return location;

    }

代码69行开始是人机对战的监听器。总体思路是,我选择黑棋画完后判断一次输赢,再让AI画白棋,再判断一次输赢。

131-142行打印出了棋盘和权值表。

147-163行定义了从权值表中找到最大值所对应的坐标。

值得注意的是:120-121行:当我们先将数组A加入到另一个数组B,再把A清空,那么B中的A也被清空了,原因是clear断开了对地址的链接。

3:然后我们发现,还需要一个主页面,以及两个选择按钮(甚至更多)来选择模式或者退出。

UI设计我遗留了许多问题,在此记录一部分,以后查看或者求求路过大佬指教:

1:


人机对弈五子棋python 五子棋人机对决_java_05


人机对弈五子棋python 五子棋人机对决_System_06


左上角只有在鼠标滑过的时候按钮才能显示,猜测是JFrame上画的背景遮挡了,但是明明是先画的后加的按钮,具体情况未知

2:关于最小化页面后棋子消失,或者界面移出后棋子消失的问题,虽然问题解决了但我没搞明白。


人机对弈五子棋python 五子棋人机对决_权值_07


从观看历史界面返回下棋界面,右侧又出现了遮挡但并没有完全遮挡的情况——接下来我在remove这个panel前先把可视设置为false就好了。原来是remove,但没有完全remove掉。

4:主页面上我加的JLabel莫名消失!

5:关于主界面和下棋界面:最合理的想法当然是一开始只显示主界面,点击对战后再弹出下棋界面。

问题是:UI界面里的初始化方法中,我们从chessPanel中产生了画笔,然后将画笔赋给了监听器。但是这个产生画笔的方法必须要求桌面上这个panel可见,于是一开始只能可视。我的解决方法是一开始就显示两个界面,但用主界面遮挡住棋盘界面。并且可以设置一个boolean,只有点击了主界面某个按钮才能下棋。

这显然并不是最优解,如果我们想要不同按钮唤醒不同棋盘界面怎么办?可以一开始全部显示,在选择某个界面同时其他不显示,但还是无法避免一启动弹出许多界面的问题。

6:关于用for循环创建组件。缺点在于,这个过程一定是在方法中的,每个组件没有名字,更不可能把声明放到成员区。最常用循环创建的可能就是按钮,label等等,某些来回要搞的操作,比如setText(),最好还是指明具体对象:在之前的代码中我用actionevent.getSource()再强转JButton类型得到点击的按钮后,进行操作,这就容易操作到其他按钮,一旦我们在其他代码区想操作这些button,那getSource()的就可能是最近点击的某个按钮了。

7:showPaint(),hidePaint(),showPaintQuick()。现在的一个问题是:showPaintQuick()想要实现从showPaint()或者hidePaint()后一些棋子后开始自动下,我的操作是将表示一局中所有棋子顺序的index也作为showPaintQuick的参数,用index将所有棋子的列表切片。这样操作会跳过之前点下一步的几个棋子重showPaintQuick()。showPaintQuick()重画了棋盘,想要在showPaintQuick启动前画棋子只能想想在方法内,画棋盘后考虑,于是我先for循环画了下过的,再for循环加sleep画后面的。:

public void showPaintQuick(Graphics g, ArrayList<Chess> arrayList,int index){
        g.setColor(Color.lightGray);
        g.fillRect(0,0,getWidth(),getHeight());
        g.setColor(Color.BLACK);
        System.out.println("重新画线中");
        for (int i = 0; i < ROWS; i++) {
            g.drawLine(X,Y+i*SIZE,X+GRID_NUM*SIZE,Y+i*SIZE);
        }
        for (int i = 0; i < COLS; i++) {
            g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+GRID_NUM*SIZE);
        }
        for (Chess chess:arrayList.subList(0,index)) {

            int cx = chess.c * SIZE + X - SIZE / 2;
            int cy = chess.r * SIZE + Y - SIZE / 2;
            if (chess.chessFlag == 1) {
                g.setColor(Color.BLACK);
            } else {
                g.setColor(Color.WHITE);
            }
            g.fillOval(cx, cy, SIZE, SIZE);
        }

        for (Chess chess:arrayList.subList(index,arrayList.size())) {

            int cx=chess.c*SIZE+X-SIZE/2;
            int cy=chess.r*SIZE+Y-SIZE/2;
            if(chess.chessFlag==1){
                g.setColor(Color.BLACK);
            }else{
                g.setColor(Color.WHITE);
            }
            g.fillOval(cx,cy,SIZE,SIZE);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

二:较上一版本功能的更新

1:回放功能

首先展示效果:


人机对弈五子棋python 五子棋人机对决_权值_08


人机对弈五子棋python 五子棋人机对决_权值_09


人机对弈五子棋python 五子棋人机对决_权值_10


回放功能内部实现的功能有:

1:记录这一次启动下的所有回合

2:选择回合后,可选择点击上一步,下一步来一个棋子一个棋子观看下棋

3:也可以选择速播速播来观看自动下棋

4:也可以选择观看最终结局来直接下完

5:点击返回则返回

实现是主要思路如下:

1:每一次下棋用于记录下棋顺序的那个列表,我们要存储到一个专门的列表中,围绕此高维列表展开操作。

2:“上一步”是最骚的,我选择用棋盘的一小格遮住下的最后一个棋子。也可以考虑重画前面除了最后一个的所有棋子。

3:“速播”要求电脑知道从顺序表的哪一步开始自动下棋,这就要用index记录一下上一步下一步的点击。同时用Thread.sleep(1000)使下棋过程可视。

代码如下:

1:用于“下一步”,只画一个棋子

public void showPaint(Graphics g, ArrayList<Chess> arrayList, int index) {
            int cx = arrayList.get(index).c * SIZE + X - SIZE / 2;
            int cy = arrayList.get(index).r * SIZE + Y - SIZE / 2;
            if (arrayList.get(index).chessFlag == 1) {
                g.setColor(Color.BLACK);
            } else {
                g.setColor(Color.WHITE);
            }

            g.fillOval(cx, cy, SIZE, SIZE);
        }

2:用于”上一步“,遮挡上一个棋子

public void hidePaint(Graphics g, ArrayList<Chess> arrayList, int index){
        int cx = arrayList.get(index-1).c * SIZE + X - SIZE / 2;
        int cy = arrayList.get(index-1).r * SIZE + Y - SIZE / 2;
        Image icon=new ImageIcon("imgs/img_1.png").getImage();
        g.drawImage(icon,cx,cy,SIZE,SIZE,null);
    }

3:用于快速看局(代码确实有点冗长,重用性确实太差,后面我慢慢优化)

public void showPaintQuick(Graphics g, ArrayList<Chess> arrayList,int index){
        g.setColor(Color.lightGray);
        g.fillRect(0,0,getWidth(),getHeight());
        g.setColor(Color.BLACK);
        System.out.println("重新画线中");
        for (int i = 0; i < ROWS; i++) {
            g.drawLine(X,Y+i*SIZE,X+GRID_NUM*SIZE,Y+i*SIZE);
        }
        for (int i = 0; i < COLS; i++) {
            g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+GRID_NUM*SIZE);
        }
        for (Chess chess:arrayList.subList(0,index)) {

            int cx = chess.c * SIZE + X - SIZE / 2;
            int cy = chess.r * SIZE + Y - SIZE / 2;
            if (chess.chessFlag == 1) {
                g.setColor(Color.BLACK);
            } else {
                g.setColor(Color.WHITE);
            }
            g.fillOval(cx, cy, SIZE, SIZE);
        }

        for (Chess chess:arrayList.subList(index,arrayList.size())) {

            int cx=chess.c*SIZE+X-SIZE/2;
            int cy=chess.r*SIZE+Y-SIZE/2;
            if(chess.chessFlag==1){
                g.setColor(Color.BLACK);
            }else{
                g.setColor(Color.WHITE);
            }
            g.fillOval(cx,cy,SIZE,SIZE);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

4:选择对局界面

public void choiceHistory() {
        jFrame.setSize(180, 350);
        JPanel choiceH = new JPanel();
        ButtonGroup buttonGroup = new ButtonGroup();
        Box box = Box.createVerticalBox();
        for (int i = 0; i < HISTORY.size(); i++) {
            JButton jrb = new JButton("第" + (i + 1) + "局");
            jrb.addActionListener(this);
            buttonGroup.add(jrb);
            box.add(jrb);
        }
        choiceH.add(box);
        //JButton ok=new JButton("确定");
        jFrame.add(choiceH);
        //jFrame.add(ok,BorderLayout.SOUTH);
        jFrame.setVisible(true);
    }

5:监听器中相关部分

else if (actionStr.equals("历史记录")) {
            //JOptionPane.showMessageDialog(null,"黑方总计获胜次数:"+blackWin+"\n白方总计获胜次数:"+whiteWin);
            int choice = JOptionPane.showConfirmDialog(null, "黑方总计获胜次数:" + blackWin + "\n白方总计获胜次数:" + whiteWin + "\n是否回看精彩对局?", "请选择", JOptionPane.YES_NO_OPTION);
            if (choice == 0) {
                if (HISTORY.size() != 0) {
                    ui.jf.remove(ui.btnPanel);
                    ui.jf.add(ui.reviewPanel, BorderLayout.EAST);
                    ui.jf.setVisible(true);
                    //ui.btnPanel.setVisible(false);
                    //ui.reviewPanel.setVisible(true);
                    this.choiceHistory();
                } else {
                    JOptionPane.showMessageDialog(null, "暫無對局");
                }
            }
        }
        for (int i = 0; i < HISTORY.size(); i++) {

            if (actionStr.equals("第" + (i + 1) + "局")) {
                chessPanel.paint(g);
                index = 0;
                System.out.println("第" + (i + 1) + "局qq");
                System.out.println(HISTORY.get(i));
                temArray = HISTORY.get(i);
            }
        }
        if (actionStr.equals("上一步")) {
            System.out.println("1");
            if (index > 0) {
                chessPanel.hidePaint(g, temArray, index);
                index--;
            } else {
                JOptionPane.showMessageDialog(null, "没有上一步了");
            }

        } else if (actionStr.equals("下一步")) {
            if (index < temArray.size()) {
                chessPanel.showPaint(g, temArray, index);
                index++;
            } else {
                JOptionPane.showMessageDialog(null, "已经结束辣");
            }
        } else if (actionStr.equals("观看最终结局")) {
            int staticIndex = index;
            for (int i = 0; i < temArray.size() - staticIndex; i++) {          //假如在for内部调整i的上界,是会影响循环的(上界在次次循环中会刷新)
                chessPanel.showPaint(g, temArray, index);
                index++;
            }
        } else if (actionStr.equals("速播速播")) {

            chessPanel.showPaintQuick(g,temArray,index);
            index=temArray.size();

        } else if (actionStr.equals("返回")) {
            ui.reviewPanel.setVisible(false);
            jFrame.setVisible(false);
            ui.jf.remove(ui.reviewPanel);
            //ui.reviewPanel.setVisible(false);
            ui.jf.add(ui.btnPanel, BorderLayout.EAST);
            ui.jf.setVisible(true);
        }

2:悔棋功能

悔棋我可没用遮住的办法,总体思路是底层修改底层二维数组棋盘,重画界面棋盘。

要注意的是:

我们重新调用chessPanel的paint方法,却不能直观的调用repaint方法,原因我盲猜是repaint内部并不直接重画,这导致每次画的速度赶不上代码进行到下面的速度。所以在此我们手动调用paint方法解决。

代码如下:

else if (actionStr.equals("悔棋")) {
            if (chessList.size() == 0) {
                JOptionPane.showMessageDialog(null, "没有棋子可以悔棋了");
                return;
            }
            if(choice==1) {
                Chess chess = chessList.get(chessList.size() - 1);
                //System.out.println(chess.c+"@@@"+chess.r);
                CHESS_ARRAY[chess.r][chess.c] = 0;
                chessFlag = chess.chessFlag;
                chessList.remove(chessList.size() - 1);
            }
            else if(choice==2){
                Chess chess = chessList.get(chessList.size() - 1);
                CHESS_ARRAY[chess.r][chess.c] = 0;
                chessList.remove(chessList.size() - 1);
                Chess chess2 = chessList.get(chessList.size() - 1);
                CHESS_ARRAY[chess2.r][chess2.c] = 0;
                chessList.remove(chessList.size()-1);
            }
            chessPanel.paint(g);

(其中choice=1是人人对战,悔一颗棋,choice=2是人机对战,悔两颗棋)