一、简介:
本程序功能是实现一个简单的AI五子棋小游戏,大致程序模块如下:
- 棋盘界面
- 鼠标监听器
- 按钮监听器
- 悔棋功能
- AI权值算法
在开始之前,我们先利用接口定义后面经常用上的几个常量:
/*Config.java*/
public interface Config {
public static final int X1 = 50; //棋盘起始点的横坐标
public static final int Y1 = 50; //棋盘起始点的纵坐标
public static final int LINE = 15; //行数/列数
public static final int SIZE = 50; //线间距
public static final int CHESS = 30; //棋子直径
} //以上单位均为像素
二、画棋盘
画棋盘就要用到JFrame类了,这个类是窗体控件,可以做出GUI程序。编写ChessUI类继承JFrame类即可。
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
public class ChessUI extends JFrame implements Config{
private static final long serialVersionUID = 1L;
public ChessUI() {//UI的构造方法
//窗体的基本组成
setTitle("五子棋V1.0");
setSize(800, 800); //窗体大小800px*800px
setDefaultCloseOperation(EXIT_ON_CLOSE); //关闭按钮
setVisible(true); //窗体可视化,将窗体在电脑屏幕上显示
}
@override
public void paint(Graphics g) {
super.paint(g); //调用父类方法体内容
for (int i = 0; i < LINE; i++) {//利用循环逻辑画棋盘线
g.setColor(Color.black);
g.drawLine(X1, X1 + SIZE * i, X1 + SIZE * (LINE - 1), Y1 + SIZE * i);
g.drawLine(X1 + SIZE * i, X1, Y1 + SIZE * i, Y1 + SIZE * (LINE - 1));
}
}
public static void main(String[] args) {
new ChessUI();//在本类中直接调用构造方法
}
}
- 注意:
这里,棋盘用drawLine方法绘制,并且要重写在paint方法体内,否则,如果在构造方法中画棋盘,窗体界面一旦刷新重绘,棋盘会消失。 - 运行的结果应该是如下界面:
三、实现鼠标点击界面落子(鼠标监听器)
- 鼠标监听器是干什么的?
鼠标监听器用来接收鼠标的动作,然后相应的动作会对应实现怎样的功能。
代码如下,我们以鼠标在窗体中按下时落子:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class ChessListener implements MouseListener,Config{
public Graphics g = null;//定义一个画笔,用于后面画棋子
int x , y; //鼠标点击的点坐标
@Override//鼠标点击时执行的方法
public void mouseClicked(MouseEvent arg0) {}
@Override//鼠标进入组件(窗体)时执行的方法
public void mouseEntered(MouseEvent arg0) {}
@Override//鼠标离开组件时执行的方法
public void mouseExited(MouseEvent arg0) {}
@Override//鼠标按压下去时执行的方法
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
g.setColor(Color.black); //设置图形颜色为黑色
g.fillOval(x-CHESS/2, y-CHESS/2, CHESS, CHESS); //以(x,y)为圆心,CHESS(30px)为直径画圆
}
@Override//鼠标松开时执行的方法
public void mouseReleased(MouseEvent arg0) {}
}
效果图如下:
但是,我们的目的是要将棋子下在交叉点处,不能下到交叉点以外对方任何地方,并且要做到黑白交替落子。添加修改后的方法如下:
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
//接下来进入坐标校正
if (x % SIZE < SIZE / 2 && y % SIZE < SIZE / 2) {
x -= x % SIZE;
y -= y % SIZE;
} else if (x % SIZE >= SIZE / 2 && y % SIZE >= SIZE / 2) {
x = x - x % SIZE + SIZE;
y = y - y % SIZE + SIZE;
}
else if (x % SIZE >= SIZE / 2 && y % SIZE < SIZE / 2) {
x = x - x % SIZE + SIZE;
y -= y % SIZE;
} else if (x % SIZE < SIZE / 2 && y % SIZE >= SIZE / 2) {
x -= x % SIZE;
y = y - y % SIZE + SIZE;
}
if ((x > X1+SIZE*(LINE-1) || y > Y1+SIZE*(LINE-1))) {
return;
}
//校正完毕
if(flag == 1) {
g.setColor(Color.black); //设置图形颜色为黑色
g.fillOval(x-CHESS/2, y-CHESS/2, CHESS, CHESS); //以(x,y)为圆心,CHESS(30px)为直径画圆
flag = 2;
}else if(flag == 2) {
g.setColor(Color.white); //设置图形颜色为白色
g.fillOval(x-CHESS/2, y-CHESS/2, CHESS, CHESS); //以(x,y)为圆心,CHESS(30px)为直径画圆
flag = 1;
}
}
效果图如下:
但是这样,窗体界面一旦刷新,棋子就会消失,并且会出现同一点重复落子的情况。为了不让棋子在窗体刷新时候消失,并且要记住哪个点已经落子了以防下次不能再在同一点落子,我们需要用一种存储结构来存储当前棋盘的落子情况。
- 利用二维数组来储存落子情况
定义一个chessArray[][]数组,对应棋盘。落黑子的点在数组相应位置标1,白子标2,未落子为0.
继续修改鼠标监听器的方法和paint方法:
int[][] chessArray = new int[LINE][LINE];
int xx,yy; //棋盘坐标(xx,yy)的点所下的棋的情况对应元素chessArray[xx][yy]
//这里xx,yy的取值范围是[0,14],代表棋子所在棋盘的列数和行数。
@Override//鼠标按压下去时执行的方法
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
//接下来进入坐标校正
if (x % SIZE < SIZE / 2 && y % SIZE < SIZE / 2) {
x -= x % SIZE;
y -= y % SIZE;
} else if (x % SIZE >= SIZE / 2 && y % SIZE >= SIZE / 2) {
x = x - x % SIZE + SIZE;
y = y - y % SIZE + SIZE;
}
else if (x % SIZE >= SIZE / 2 && y % SIZE < SIZE / 2) {
x = x - x % SIZE + SIZE;
y -= y % SIZE;
} else if (x % SIZE < SIZE / 2 && y % SIZE >= SIZE / 2) {
x -= x % SIZE;
y = y - y % SIZE + SIZE;
}
if ((x > X1+SIZE*(LINE-1) || y > Y1+SIZE*(LINE-1))) {
return;
}
//校正完毕
xx = x / SIZE - 1;
yy = y / SIZE - 1;
if(chessArray[xx][yy] != 0) return; //防止在同一点重复落子
if(flag == 1) {
g.setColor(Color.black); //设置图形颜色为黑色
g.fillOval(x-CHESS/2, y-CHESS/2, CHESS, CHESS); //以(x,y)为圆心,CHESS(30px)为直径画圆
chessArray[xx][yy] = 1;
flag = 2;
}else if(flag == 2) {
g.setColor(Color.white); //设置图形颜色为白色
g.fillOval(x-CHESS/2, y-CHESS/2, CHESS, CHESS); //以(x,y)为圆心,CHESS(30px)为直径画圆
chessArray[xx][yy] = 2;
flag = 1;
}
}
再修改paint方法:
public void paint(Graphics g) {
super.paint(g);
for (int i = 0; i < LINE; i++) { //画棋盘线
g.setColor(Color.black);
g.drawLine(X1, X1 + SIZE * i, X1 + SIZE * (LINE - 1), Y1 + SIZE * i);
g.drawLine(X1 + SIZE * i, X1, Y1 + SIZE * i, Y1 + SIZE * (LINE - 1));
}
for(int i = 0;i<LINE;i++) { //重绘棋子
for(int j = 0;j<LINE;j++) {
if(cl.chessArray[i][j] == 1) {
g.setColor(Color.black);
g.fillOval((i+1)*SIZE-CHESS/2,(j+1)*SIZE-CHESS/2, CHESS, CHESS);
} else if(cl.chessArray[i][j] == 2) {
g.setColor(Color.white);
g.fillOval((i+1)*SIZE-CHESS/2,(j+1)*SIZE-CHESS/2, CHESS, CHESS);
}
}
}
}
这样,就实现了棋子的重绘和防止重复落子,后面还可以以此实现判断输赢和AI功能。
四、判断胜负
如何判断胜负?思路是,当前所落子的四个方向直线上至少有一组五子同色连线。
在最新落下一个白子时,其所在水平方向形成同色五子连线。在一方赢了之后,清空棋盘。代码如下:
@Override//鼠标松开时执行的方法
public void mouseReleased(MouseEvent e) {
if(ifWin()) {
if (chessArray[xx][yy] == 1) {
JOptionPane.showConfirmDialog(null, "黑方胜利", "游戏结束", JOptionPane.WARNING_MESSAGE);
}
else {
JOptionPane.showConfirmDialog(null, "白方胜利", "游戏结束", JOptionPane.WARNING_MESSAGE);
}
clear();
jf.repaint();
}
}
private boolean ifWin() {
if (win1(xx, yy) >= 5 || win2(xx, yy) >= 5 || win3(xx, yy) >= 5 || win4(xx, yy) >= 5) {
return true;
}
else
return false;
}
private int win1(int x, int y) {//横向判断输赢
int count = 0;
//向右
for (int i = x + 1; i < LINE; i++) {
if (chessArray[x][y] == chessArray[i][y]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
//向左
for (int i = x; i >= 0; i--) {
if (chessArray[x][y] == chessArray[i][y]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
return count;
}
private int win2(int x, int y) {//纵向判断输赢
int count = 0;
int i = 0;
for (i = y + 1; i < LINE; i++) {//向下
if (chessArray[x][y] == chessArray[x][i] && chessArray[x][y] != 0) {
count++;
} else
break;
}
for (i = y; i >= 0; i--) {
if (chessArray[x][y] == chessArray[x][i] && chessArray[x][y] != 0) {//向上
count++;
} else
break;
}
return count;
}
private int win3(int x, int y) {//主对角线判断
int count = 0;
int i = 0, j = 0;
for (i = x + 1, j = y + 1; i < LINE && j < LINE; i++, j++) {
if (chessArray[x][y] == chessArray[i][j]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
for (i = x, j = y; i >= 0 && j >= 0; i--, j--) {
if (chessArray[x][y] == chessArray[i][j]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
return count;
}
private int win4(int x, int y) {//副对角线判别
int count = 0;
int i = 0, j = 0;
for (i = x + 1, j = y - 1; i < LINE && j >= 0; i++, j--) {
if (chessArray[x][y] == chessArray[i][j]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
for (i = x, j = y; i >= 0 && j < LINE; i--, j++) {
if (chessArray[x][y] == chessArray[i][j]&&chessArray[x][y]!=0) {
count++;
} else
break;
}
return count;
}
void clear() { //矩阵元素清零
for (int i = 0; i < LINE; i++) {
for (int j = 0; j < LINE; j++) {
chessArray[i][j] = 0;
}
}
}
五、实现悔棋
我们利用栈来储存每一步下棋的横坐标xx和纵坐标yy,如要悔棋,对栈进行pop操作,对pop出的点在二维数组上置零然后重绘一次窗体即可。
class UndoData { //自定义一个二元组
int x, y;
}
public class ChessStack implements Config{ //自定义一个栈
UndoData[] Undo = new UndoData[LINE * LINE];
int Top = -1;
public UndoData pop() {
return Undo[Top--];
}
public void push(int x, int y) {
UndoData ud = new UndoData();
ud.x = x;
ud.y = y;
Undo[++Top] = ud;
}
}
六、添加按钮功能
我们给窗体添加按钮来实现开始游戏和悔棋的功能 。
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
//按钮事件监听器,先实现开始游戏和悔棋功能!
public class ButtonListener implements ActionListener{
public ChessListener cl;
public ChessUI jf;
UndoData uData = new UndoData();
Object selectedValue1 = "";
Object selectedValue2 = "";
@Override
public void actionPerformed(ActionEvent e) {
String btnstr = e.getActionCommand(); //获取按钮的字符串
Object[] possibleValues1 = { "PVP" };
Object[] possibleValues2 = { "黑方先手", "白方先手" }; //设定下拉菜单选项
switch(btnstr) {
case "开始/重开" :
System.out.println("开始按钮被点击了");
if(cl.removeFlag == 1) {
jf.removeMouseListener(cl);
}
cl.clear();
jf.repaint(); //清空,重绘
//设置下拉菜单,获取用户选项
selectedValue1 = JOptionPane.showInputDialog(null, "选择对战模式", "提示",
JOptionPane.INFORMATION_MESSAGE, null, possibleValues1, possibleValues1[0]);
selectedValue2 = JOptionPane.showInputDialog(null, "选择先后手", "提示",
JOptionPane.INFORMATION_MESSAGE, null, possibleValues2, possibleValues2[0]);
switch((String)selectedValue1) {
case "PVP":
System.out.println("人人对战");
jf.addMouseListener(cl);
cl.removeFlag = 1;
default:
break;
}
switch((String)selectedValue2) {
case "黑方先手":
cl.flag = 1;
break;
case "白方先手":
cl.flag = 2;
break;
default:
break;
}
break;
case "悔棋" :
if (cl.cStack.Top == -1) {
JOptionPane.showMessageDialog(null, "不能再悔棋了!", "提示", JOptionPane.ERROR_MESSAGE);
break;
}
uData = cl.cStack.pop();
if (cl.chessArray[uData.x][uData.y] == 1) {
cl.flag = 1; //这一步悔了黑子,要重下黑子
} else if (cl.chessArray[uData.x][uData.y] == 2) {
cl.flag = 2; //这一步悔了白子,要重下白子
}
cl.chessArray[uData.x][uData.y] = 0;
jf.repaint();
break;
default:
break;
}
}
}
相应的,也要在其他类修改方法内容:
在ChessListener类中:
ChessStack cStack = new ChessStack(); //建立悔棋所用的栈
int removeFlag = 0; //拿来判断是否要移除鼠标监听器(防止第一次开始游戏时未add监听器就执行remove)
@Override//鼠标按压下去时执行的方法
public void mousePressed(MouseEvent e) {
此处略去获取坐标并校正的方法......
//校正完毕
xx = x / SIZE - 1;
yy = y / SIZE - 1;
if(chessArray[xx][yy] != 0) return; //防止在同一点重复落子
cStack.push(xx, yy); //每落一个子,把坐标入栈
//接下来画棋子
此处略去画棋子的方法......
}
}
在ChessUI类中:
ButtonListener bl = new ButtonListener();
public ChessUI() {//UI的构造方法
//窗体的基本组成
setTitle("五子棋V1.0");
setSize(1000, 800);
setDefaultCloseOperation(EXIT_ON_CLOSE); //关闭按钮
//addMouseListener(cl); //给窗体加上鼠标监听器
setLayout(null);
JButton btn1 = new JButton("开始/重开");
JButton btn2 = new JButton("悔棋");
add(btn1);
add(btn2);
btn1.setBounds(800, 100, 150, 40);
btn2.setBounds(850, 170, 100, 40); //自定义按钮位置
setVisible(true); //窗体可视化,将窗体在电脑屏幕上显示
btn1.addActionListener(bl);
btn2.addActionListener(bl);
cl.g = getGraphics(); //把窗体的画笔传给鼠标监听器
cl.jf = this;
bl.cl = this.cl;
bl.jf = this;
}
这样,我们的五子棋基本功能已经完成!