2020-10-03 Java初级项目——从零开始制作一个简易五子棋游戏

一、棋盘的绘制

使用JFrame容器制作五子棋的窗体

创建一个类——UI,如下:


public class UI {
    private JFrame frame = new JFrame();
    public void init() {
        frame.setTitle("五子棋");
        frame.setSize(518, 540);
        frame.setLocationRelativeTo(null); //居中
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true); //让窗体显示
    }

    public static void main(String[] args) {
        new UI().init();
    }

}

使用JPanel和Graphics画出大小为15X15的棋盘

再创建一个新的类——Chessboard,继承JPanel:


public class Chessboard extends JPanel {

    //规定由15条横竖线组成
    private static final int CHESSBOARD_SIZE = 15;

    //外边距
    private int margin = 20;

    /**
     * 绘图工具
     *
     * @param g 画笔工具
     */
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        drawChessBoard(g);
        drawPieces(g);
    }

    /**
     * 画棋盘
     *
     * @param g 画笔工具
     */

    private void drawChessBoard(Graphics g) {
        int cellSize = (getWidth() - 2 * margin) / (CHESSBOARD_SIZE - 1);
        for (int i = 0; i < CHESSBOARD_SIZE; i++) {
            //画横线
            g.drawLine(margin, margin + cellSize * i, getWidth() - margin, margin + cellSize * i);
            //画竖线
            g.drawLine(margin + cellSize * i, margin, margin + cellSize * i, getHeight() - margin);
        }
    }
}

实现点击鼠标落子的功能

在UI类中添加以下代码:

public class UI {
    private JFrame frame = new JFrame();
    private Chessboard chessboard = new Chessboard();
    public void init() {
        frame.setTitle("五子棋");
        frame.setSize(518, 540);
        frame.setLocationRelativeTo(null); //居中
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true); //让窗体显示

        //给棋盘添加鼠标监听事件,具体来说就是鼠标点击事件
        chessboard.addMouseListener(new MouseAdapter() { //匿名内部类
            @Override
            public void mouseClicked(MouseEvent e) {
                super.mouseClicked(e);
                //调用画棋子的方法
                play(e);
            }
        });
    }

    /**
     * 处理鼠标点击事件的方法
     */
    private void play(MouseEvent e) {
        int cellSize = chessboard.getCellSize();
        int x = (e.getX() - 5) / cellSize;
        int y = (e.getY() - 5) / cellSize;
        chessboard.move(new Pieces(x, y, 1));
    }

    public static void main(String[] args) {
        new UI().init();
    }
}

再在Chessboard类中添加drawPieces、move、getCellSize方法,如下:

public class Chessboard extends JPanel {
    private List piecesList = new ArrayList<>();
    /**
     * 绘图工具
     *
     * @param g 画笔工具
     */
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        drawChessBoard(g);
        drawPieces(g);
    }

    public int getCellSize() {
        return (getWidth() - 2 * margin) / (CHESSBOARD_SIZE - 1);
    }

    public void drawPieces(Graphics g) {
        for (Pieces piece : piecesList) {
            if (piece.getPlayer() == 1) {
                //默认值为1代表人类,棋子颜色为黑色
                g.setColor(Color.black);
            } else {
                g.setColor(Color.white);
            }
            int cellSize = (getWidth() - 2 * margin) / (CHESSBOARD_SIZE - 1);
            g.fillOval(piece.getX() * cellSize + margin - cellSize / 2, piece.getY() * cellSize + margin - cellSize / 2, cellSize, cellSize);
        }
    }

    /**
     * 落子的方法
     */
    public void move(Pieces piece) {
        piecesList.add(piece);
        repaint();
    }
}

然后创建一个新的类——Pieces,如下:

/***
* 棋子对象
*/
public class Pieces {
    private int x;
    private int y;
    private int player; //表示黑棋还是白棋,1代表黑棋,-1代表白棋
    public Pieces(int x, int y, int player) {
        this.x = x; //非实际坐标,而是格子数,第x格
        this.y = y; //同上,第y格
        this.player = player;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getPlayer() {
        return player;
    }
}

二、判断胜负

实现五子棋胜负判断的思路是:

  1. 判断胜负就看哪个颜色的棋子首先达到五子相连。若黑棋先达到五子相连,则黑棋胜,否则,白棋胜;

  2. 也就是下在每个棋子时都要判断它的八个方向的棋子相连个数,如果判断某一方向与其相邻点的颜色相同,将循环计数;

  3. 还需要注意边界点的范围,在循环的时候应注意设置边界条件;

  4. 可以建一个类,在类中写判断输赢的方法;

在Chessboard类中添加如下代码:


public class Chessboard extends JPanel {
    private static final int CHESSBOARD_SIZE = 15;
    private int margin = 20;
    private List piecesList = new ArrayList<>();
    //创建一个数组,用来表示棋盘上被占用的位置
    private int[][] location = new int[CHESSBOARD_SIZE][CHESSBOARD_SIZE];
}

但是,落子的位置是有限制的,具体来说就是:

  1. 落子的位置不能有其它棋子;

  2. 不能超过棋盘的边界;

所以要在UI类中的play()方法里添加一个落子合法性的判定:

    private void play(MouseEvent e) {
        int cellSize = chessboard.getCellSize();
        int x = (e.getX() - 5) / cellSize;
        int y = (e.getY() - 5) / cellSize;
        if (chessboard.isLegal(x, y)) {
            //添加棋子
            chessboard.move(new Pieces(x, y, 1));
            //记录人类落子的位置
            chessboard.setLocation(x, y, 1);
            //判断输赢
            if (chessboard.checkWinner(x, y, 1)) {
                JOptionPane.showMessageDialog(frame, "人类获胜", "您赢了!", JOptionPane.PLAIN_MESSAGE);
                return;
            }
        }
    }

而对应Chessboard类中新增的isLegal方法setLocation方法以及checkWinner方法就是:

    /**
     * 判断是否重复落子以及落子位置是否合法
     */
    public boolean isLegal(int x, int y) {
        return x >= 0 && x <= CHESSBOARD_SIZE && y >= 0 && y <= CHESSBOARD_SIZE && location[x][y] == 0;
    }

    /**
     * 记录落子后,棋子占用棋盘的位置
     */
    public void setLocation(int x, int y, int player) {
        location[x][y] = player;
    }

    /**
     * 判断胜负
     */
    public boolean checkWinner(int x, int y, int player) {
        int sum = 0;
        //判断水平方向,水平左侧
        for (int i = x - 1; i >= 0; i--) {
            if (location[i][y] == player) {
                sum++;
            } else {
                break;
            }
        }

        //水平右侧
        for (int i = x + 1; i <= CHESSBOARD_SIZE; i++) {
            if (location[i][y] == player) {
                sum++;
            } else {
                break;
            }
        }
        if (sum >= 4) {
            return true;
        }

        //判断垂直方向
        sum = 0;
        for (int i = y - 1; i >= 0; i--) {
            if (location[x][i] == player) {
                sum++;
            } else {
                break;
            }
        }

        for (int i = y + 1; i <= CHESSBOARD_SIZE; i++) {
            if (location[x][i] == player) {
                sum++;
            } else {
                break;
            }
        }

        if (sum >= 4) {
            return true;
        }

        //判断左上到右下这个对角线方向
        sum = 0;
        for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
            if (location[i][j] == player) {
                sum++;
            } else {
                break;
            }
        }

        for (int i = x + 1, j = y + 1; i <= CHESSBOARD_SIZE && j <= CHESSBOARD_SIZE; i++, j++) {
            if (location[i][j] == player) {
                sum++;
            } else {
                break;
            }
        }

        if (sum >= 4) {
            return true;
        }

        //判断右上到左下这个对角线方向
        sum = 0;
        for (int i = x + 1, j = y - 1; i <= CHESSBOARD_SIZE && j >= 0; i++, j--) {
            if (location[i][j] == player) {
                sum++;
            } else {
                break;
            }
        }

        for (int i = x - 1, j = y + 1; i >= 0 && j <= CHESSBOARD_SIZE; i--, j++) {
            if (location[i][j] == player) {
                sum++;
            } else {
                break;
            }
        }

        if (sum >= 4) {
            return true;
        }
        sum = 0;
        return false;
    }

三、简易的AI算法

思路:

五元组:棋盘中横竖斜四个方向的所有相邻五个可连成一条线的格子

五元组的表示:用某格子的横纵坐标以及+1/-1来表示相邻四个格子的坐标,从而计算五元组坐标

  • 五子棋棋盘大小是15X15,横竖斜四个方向共有572个五元组,给每个五元组一个评分

  • 这个五元组将为它的每个位置贡献的分数就是这个五元组自身的得分

  • 对整个棋盘来说,每个位置的得分就是该位置所在的横竖斜四个方向所有五元组的得分之和

  • 然后从所有空位置中选得分最高的位置即为机器落子的位置

首先,添加机器落子的代码,完善UI类中play方法:


    private void play(MouseEvent e) {
        int cellSize = chessboard.getCellSize();
        int x = (e.getX() - 5) / cellSize;
        int y = (e.getY() - 5) / cellSize;
        if (chessboard.isLegal(x, y)) {
            //添加棋子
            chessboard.move(new Pieces(x, y, 1));
            //记录人类落子的位置
            chessboard.setLocation(x, y, 1);
            //判断输赢
            if (chessboard.checkWinner(x, y, 1)) {
                JOptionPane.showMessageDialog(frame, "人类获胜", "您赢了!", JOptionPane.PLAIN_MESSAGE);
                return;
            }

            //机器落子
            Pieces piece = chessboard.searchLocation();
            chessboard.move(piece);
            chessboard.setLocation(piece.getX(),piece.getY(),piece.getPlayer());
            //判断胜负
            if (chessboard.checkWinner(piece.getX(),piece.getY(),piece.getPlayer())){
                JOptionPane.showMessageDialog(frame, "电脑获胜", "您输了!", JOptionPane.PLAIN_MESSAGE);
            }
        }
    }

其次,还需要一个评估函数(评分表),来对整个棋局中有效位置进行评价,但评分表很难确定,没有所谓最好的,只能根据经验和测试来选择,我选择的评分表如下:

  • 既有人类落子,又有机器落子,判分为0;

  • 全部为空,没有落子,判分为7;

  • 机器落1子,判分为35

  • 机器落2子,判分为800

  • 机器落3子,判分为15000

  • 机器落4子,评分为800000

  • 人类落1子,评分为15

  • 人类落2子,评分为400

  • 人类落3子,评分为1800

  • 人类落4子,评分为100000

在Chessboard类中添加tupleScore方法


    private int tupleScore(int humanChessmanNum, int machineChessmanNum) {
        if (humanChessmanNum > 0 && machineChessmanNum > 0) {
            return 0;
        }

        if (humanChessmanNum == 0 && machineChessmanNum == 0) {
            return 7;
        }

        if (machineChessmanNum == 1) {
            return 35;
        }

        if (machineChessmanNum == 2) {
            return 800;
        }

        if (machineChessmanNum == 3) {
            return 15000;
        }

        if (machineChessmanNum == 4) {
            return 800000;

        }

        if (humanChessmanNum == 1) {
            return 15;
        }

        if (humanChessmanNum == 2) {
            return 400;
        }

        if (humanChessmanNum == 3) {
            return 1800;
        }

        if (humanChessmanNum == 4) {
            return 100000;
        }
        return -1;
    }

在Chessboard类中添加searchLocation方法

    /**
     * 计算机器落子的最佳位置
     */
    public Pieces searchLocation() {
        //每次都初始化一下score的评分数组
        for (int i = 0; i < CHESSBOARD_SIZE; i++) {
            for (int j = 0; j < CHESSBOARD_SIZE; j++) {
                score[i][j] = 0;
            }
        }

        //每次机器寻找了落子位置,评分都重新计算一遍
        int humanChessmanNum = 0;
        int machineChessmanNum = 0;
        int tupleScoreTmp = 0;
        int goalX = -1;
        int goalY = -1;
        int maxScore = -1;
        //纵向扫描棋盘
        for (int i = 0; i < 15; i++) {
            for (int j = 0; j < 11; j++) {
                int k = j;
                while (k < j + 5) {
                    if (location[j][k] == -1) machineChessmanNum++;
                    else if (location[i][k] == 1) humanChessmanNum++;
                    k++;
                }

                // 将每一个五元组中的黑棋个数与白棋个数传入评分表中
                tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
                //为该五元组的每一个位置添加分数
                for (k = j; k < j + 5; k++) {
                    score[i][k] += tupleScoreTmp;
                }
                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }
        }
        //横向扫描棋盘
        for (int i = 0; i < 15; i++) {
            for (int j = 0; j < 11; j++) {
                int k = j;
                while (k < j + 5) {
                    if (location[k][i] == -1) machineChessmanNum++;
                    else if (location[k][i] == 1) humanChessmanNum++;
                    k++;
                }
                // 将每一个五元组中的黑棋个数与白棋个数传入评分表中
                tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
                //为该五元组的每一个位置添加分数
                for (k = j; k < j + 5; k++) {
                    score[k][i] += tupleScoreTmp;
                }
                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }
        }

        //3.扫描右对角线 上侧部分

        for (int i = 14; i >= 4; i--) {
            for (int k = i, j = 0; j < 15 && k >= 0; j++, k--) {
                int m = k; //x轴
                int n = j; //y轴
                while (m > k - 5 && k - 5 >= -1) {
                    if (location[m][n] == -1) machineChessmanNum++;
                    else if (location[m][n] == 1) humanChessmanNum++;
                    m--;
                    n++;
                }

                //注意斜向判断时,可能不构成五元组(靠近四个角落),遇到这种情况要忽略掉
                if (m == k - 5) {
                    tupleScoreTmp = tupleScore(machineChessmanNum, humanChessmanNum);
                    for (m = k, n = j; m > k - 5; m--, n++) {
                        score[m][n] += tupleScoreTmp;
                    }
                }

                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }
        }

        //4.扫描右对角线 下侧部分
        for (int i = 1; i < 15; 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 (location[n][m] == -1) machineChessmanNum++;
                    else if (location[n][m] == 1) humanChessmanNum++;
                    m++;
                    n--;
                }

                if (m == k + 5) {
                    tupleScoreTmp = tupleScore(machineChessmanNum, humanChessmanNum);
                    for (m = k, n = j; m < k + 5; m++, n--) {
                        score[n][m] += tupleScoreTmp;
                    }
                }

                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }
        }

        //5.扫描左对角线上侧部分
        for (int i = 0; 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 (location[m][n] == -1) machineChessmanNum++;
                    else if (location[m][n] == 1) humanChessmanNum++;
                    m++;
                    n++;
                }
                if (m == k + 5) {
                    tupleScoreTmp = tupleScore(machineChessmanNum, humanChessmanNum);
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        score[m][n] += tupleScoreTmp;
                    }
                }
                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }

        }

        //6.扫描左对角线下侧部分
        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 (location[n][m] == -1) machineChessmanNum++;
                    else if (location[n][m] == 1) humanChessmanNum++;
                    m++;
                    n++;
                }
                if (m == k + 5) {
                    tupleScoreTmp = tupleScore(machineChessmanNum, humanChessmanNum);
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        score[n][m] += tupleScoreTmp;
                    }
                }
                //归零
                humanChessmanNum = 0;
                machineChessmanNum = 0;
                tupleScoreTmp = 0;
            }
        }
        //从空位置中找到最大的位置
        for (int i = 0; i < 15; i++) {
            for (int j = 0; j < 15; j++) {
                if (location[i][j] == 0 && score[i][j] > maxScore) {
                    goalX = i;
                    goalY = j;
                    maxScore = score[i][j];
                }
            }
        }

        if (goalX != -1) {
            return new Pieces(goalX, goalY, -1);
        }

        //没找到坐标说明平局,暂不处理
        return new Pieces(-1, -1, -1);
    }

(完)

你可能感兴趣的:(2020-10-03 Java初级项目——从零开始制作一个简易五子棋游戏)