项目特点
- 两种游戏模式: 人机对战和玩家对战.
- 可以多步悔棋.
- 项目未使用图片, 图形都是绘制而成.
项目结构以及源码
- src
- ButtonAction 类
- Config 接口
- Core 类
- GameMouse 类
- GameUI 类
- MyFrame 类
ButtonAction 类
此类继承于 ActionListener
接口, 主要用于传递按钮点击事件的函数.
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ButtonAction implements ActionListener {
public Graphics gr;
public MyFrame mf;
public JButton redoButton;
public JButton restartButton;
public JRadioButton aiButton;
public JRadioButton playerButton;
//利用构造器把画布中的按钮和画笔全部传进来
public ButtonAction(Graphics gr, MyFrame mf, JButton redoButton, JButton restartButton, JRadioButton aiButton, JRadioButton playerButton) {
this.gr = gr;
this.mf = mf;
this.redoButton = redoButton;
this.restartButton = restartButton;
this.aiButton = aiButton;
this.playerButton = playerButton;
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println(e.getSource());
Object target = e.getSource();
if(aiButton.isSelected()) {//选择 AI 对战模式
Core.mode = 0;
}
else if(playerButton.isSelected()) {//选择玩家对战模式
Core.mode = 1;
}
if(target == aiButton || target == playerButton) { //如果点击了模式选择的选择按钮
for (int i = 0; i <15 ; i++) {
for (int j = 0; j < 15; j++) {
Core.map[i][j] = 0;//一旦重新选择模式, 棋盘将清空
}
}
Core.flag = 1;//棋子颜色变为黑色
Core.res = 0;//对战结果为 无结果
mf.paint(gr);
System.out.println("MODE: " + Core.mode);
}
if(target == redoButton) { //如果点击会千牛
if(!Core.chessStack.empty() && Core.mode == 1) { //玩家对战模式,只需悔棋到上一步
int y = Core.chessStack.pop();//弹栈
int x = Core.chessStack.pop();
Core.map[x][y] = 0;
Core.flag = - Core.flag;
Core.res = 0;
mf.paint(gr);
}
if(!Core.chessStack.empty() && Core.mode == 0) {//人机对战模式需要悔棋两步
int y = Core.chessStack.pop();//弹栈第一次
int x = Core.chessStack.pop();
Core.map[x][y] = 0;
Core.flag = - Core.flag;
y = Core.chessStack.pop();//弹栈第二次
x = Core.chessStack.pop();
Core.map[x][y] = 0;
Core.flag = - Core.flag;
Core.res = 0;
mf.paint(gr);
}
}
else if(target == restartButton) { //重新开始按钮, 棋盘清零, 结果重置
for (int i = 0; i <15 ; i++) {
for (int j = 0; j < 15; j++) {
Core.map[i][j] = 0;
}
}
Core.flag = 1;
Core.res = 0;
mf.paint(gr);
}
}
}
GameMouse 类
此类继承于MouseListener
接口, 主要负责定义鼠标点击时候的函数. 包括获取坐标, 绘制棋子,下棋等操作.
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.HashMap;
public class GameMouse implements MouseListener, Config {
public Graphics gr;
public MyFrame mf;
public int chessX, chessY;
int res = 0;
private HashMap<String, Integer> Map = new HashMap<>();//用于存储ai算法的权值
private HashMap<String, Integer> aiValues = Core.generateAIMap(Map);//生成权值
public GameMouse(Graphics gr) {
this.gr = gr;
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
// 获取坐标
int x = e.getX();
int y = e.getY();
int xTemp, yTemp;
System.out.println(x + " " + y);
gr.setColor(Color.black);
//计算交点值
if ((x - X0) % SIZE > SIZE / 2) {
xTemp = (x - X0) / SIZE + 1;
} else {
xTemp = (x - X0) / SIZE;
}
if ((y - Y0) % SIZE > SIZE / 2) {
yTemp = (y - Y0) / SIZE + 1;
} else {
yTemp = (y - Y0) / SIZE;
}
// 只有棋子坐标在特定范围内的时候, 再赋值, 否则给个赋值, 下面的代码也不用再执行
if (xTemp >= 0 && xTemp <= 14) {
chessX = xTemp;
} else {
chessX = -1;
}
if (yTemp >= 0 && yTemp <= 14) {
chessY = yTemp;
} else {
chessY = -1;
}
// 只有没有结果的时候才下棋, 如果有结果, 不下棋,也不重绘棋盘
if (Core.res == 0 && chessX >= 0 && chessX <= 15 && chessY >= 0 && chessY <= 15) {
if (Core.mode == 0) {
//调用下棋函数
Core.aiPlaceChess(mf, gr, Core.map, chessX, chessY, aiValues);
//传当前方法内的横纵坐标到 mf 对象内, 用于悔棋
System.out.println("当前结果: " + Core.res);
}
if (Core.mode == 1) {
//调用下棋函数
Core.placeChess(mf, gr, Core.map, chessX, chessY);
//传当前方法内的横纵坐标到 mf 对象内, 用于悔棋
Core.chessStack.push(chessX);
Core.chessStack.push(chessY);
res = Core.isWin(Core.map, chessX, chessY);
System.out.println("当前结果: " + Core.res);
if (res == 4) {
JOptionPane.showMessageDialog(null, "黑色胜利.");
Core.res = res;
}
if (res == -4) {
JOptionPane.showMessageDialog(null, "白色胜利.");
Core.res = res;
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
MyFrame 类
继承了Jframe
类, 主要用于提供画笔, 重绘棋盘等功能.
import javax.swing.*;
import java.awt.*;
public class MyFrame extends JFrame implements Config {
//重写 paint 方法, 用于改变视窗大小和重新开始等情况的重绘棋盘
public void paint(Graphics g) {
super.paint(g);
g.setColor(Color.black);
for (int i = 0; i < LINE; i++) {
g.setColor(Color.black);
g.drawLine(X0, Y0 + i * SIZE, (LINE - 1) * SIZE + X0, Y0 + i * SIZE);
g.drawLine(X0 + i * SIZE, Y0, X0 + i * SIZE, (LINE - 1) * SIZE + Y0);
}
g.drawRect(48,78,704,704);
g.fillOval(200 - 5,230 - 5,10,10);
g.fillOval(600 - 5,630 - 5,10,10);
g.fillOval(600 - 5,230 - 5,10,10);
g.fillOval(200 - 5,630 - 5,10,10);
g.fillOval(400 - 5,430 - 5,10,10);
for (int i = 0; i < LINE; i++) {
for (int j = 0; j < LINE; j++) {
// System.out.print(map[j][i] + " ");
if (Core.map[i][j] == 1) {
for (int k = 0; k < 50; k++) {
Color c = new Color(4*k,4*k,4*k);
g.setColor(c);
g.fillOval(X0 + i * SIZE - 25 + k/3, Y0 + j * SIZE - 25 + k/3, 50 - k, 50 - k);
}
} else if (Core.map[i][j] == -1) {
for (int k = 0; k < 50; k++) {
Color c = new Color(k+200,k+200,k+200);
g.setColor(c);
g.fillOval(X0 + i * SIZE - 25 + k/3, Y0 + j * SIZE - 25 + k/3, 50 - k, 50 - k);
}
}
}
// System.out.print("\n");
}
}
}
Core 类
此类主要提供一些列静态算法, 为游戏的核心逻辑类.
import javax.swing.*;
import java.awt.*;
import java.util.HashMap;
import java.util.Stack;
public class Core implements Config {
static int[][] map = new int[16][16]; // 棋盘二维数组
static Stack<Integer> chessStack = new Stack<>();//用于悔棋的堆栈
static int mode = 0; //人机或者是玩家对战模式
static int flag = 1; // 下一步颜色
static int res = 0; //最后结果
static int max = -100; //通过权值表算出的最大值
static int maxI, maxJ; //最大权值的棋盘位置,用于白棋落子
//
//哈希表的内容不能直接在这里添加, 要在函数体内
private HashMap<String, Integer> Map = new HashMap<>();
/**
* 将全指标输入空的哈希表
* @param aiMap 初始的空哈希表
* @return
*/
public static HashMap generateAIMap(HashMap<String, Integer> aiMap) {
aiMap.put("0000", 10);
aiMap.put("B", 7);
aiMap.put("BB", 800);
aiMap.put("BBB", 15000);
aiMap.put("BBBB", 800000);
aiMap.put("W", 15);
aiMap.put("WW", 400);
aiMap.put("WWW", 1800);
aiMap.put("WWWW", 100000);
return aiMap;
}
//region 权值 复杂
// public static HashMap generateAIMap1(HashMap<String, Integer> aiMap) {
// aiMap.put("0000", 10);
// aiMap.put("B0WW", 7);
// aiMap.put("0B0W", 8);
// aiMap.put("B0W0", 9);
// aiMap.put("B00W", 30);
// aiMap.put("00B0", 40);
// aiMap.put("0B00", 70);
// aiMap.put("000B", 100);
// aiMap.put("00BB", 200);
// aiMap.put("BB0W", 500);
// aiMap.put("0BB0", 800);
// aiMap.put("BB00", 900);
// aiMap.put("B0BB", 1000);
// aiMap.put("BBBW", 1100);
// aiMap.put("BB0B", 1500);
// aiMap.put("BBB0", 100000);
// aiMap.put("BBBB", 800000);
// aiMap.put("W0BB", 15);
// aiMap.put("0W0B", 16);
// aiMap.put("0W0B", 16);
// aiMap.put("WW", 400);
// aiMap.put("WWW", 1800);
// aiMap.put("WWWW", 100000);
// return aiMap;
// }
//endregion
/**
* 与 AI 对战的下棋模式
* @param mf 画布
* @param gr 画笔
* @param map 棋局
* @param chessX AI 下棋前的黑棋横坐标
* @param chessY AI 下棋前的黑棋纵坐标
* @param aiValues 权值表
*/
public static void aiPlaceChess(MyFrame mf, Graphics gr, int[][] map, int chessX, int chessY, HashMap<String, Integer> aiValues) {
int res = 0;
if (Core.flag == 1) {
gr.setColor(Color.black);
//判断落子位置是否有棋子
if (map[chessX][chessY] == 0) {
map[chessX][chessY] = 1;
Core.chessStack.push(chessX); //用于悔棋
Core.chessStack.push(chessY);
for (int k = 0; k < 50; k++) {
Color c = new Color(4 * k, 4 * k, 4 * k);
gr.setColor(c);
gr.fillOval(X0 + chessX * SIZE - 25 + k / 3, Y0 + chessY * SIZE - 25 + k / 3, 50 - k, 50 - k);
}
System.out.println("下黑棋");
System.out.println("棋子坐标为:" + chessX + "," + chessY + " 棋子颜色为: " + Core.flag);
Core.flag = -1;
aiCheckMax(aiValues, map);
res = isWin(map, chessX, chessY);
if (res >= 4) {
JOptionPane.showMessageDialog(null, "宝宝真厉害,奖励宝宝一个亲亲.");
Core.res = res;
}
}
if (Core.res == 0) {
gr.setColor(Color.white);
chessX = maxI;
chessY = maxJ;
Core.chessStack.push(chessX);
Core.chessStack.push(chessY);
map[chessX][chessY] = -1;
for (int k = 0; k < 50; k++) {
Color c = new Color(k + 200, k + 200, k + 200);
gr.setColor(c);
gr.fillOval(X0 + chessX * SIZE - 25 + k / 3, Y0 + chessY * SIZE - 25 + k / 3, 50 - k, 50 - k);
}
System.out.println("下白棋");
System.out.println("棋子坐标为:" + maxI + "," + maxJ + " 棋子颜色为: " + Core.flag);
Core.flag = 1;
res = isWin(map, chessX, chessY);
if (res <= -4) {
JOptionPane.showMessageDialog(null, "西白,几把人机竟然赢了,下一把给它锤爆嗷.");
Core.res = res;
}
}
}
}
/**
* 查找出权值最大的位置, 并将最大值坐标存储到 Core 类的静态变量中
*
* @param aiValues 棋子对应的权值表
* @param map 当前棋盘
*/
public static void aiCheckMax(HashMap<String, Integer> aiValues, int[][] map) {
int[][] score = new int[16][16];
max = -100;
StringBuilder sbHorizon = new StringBuilder(5);
// 先试一下一个点的算法
for (int i = 0; i < LINE; i++) {
for (int j = 0; j < LINE; j++) {
//如果位置没有棋子, 再去做下面四个循环
if (map[i][j] == 0) {
int[] scoreArray = new int[4]; //存储每个空位周围 4 个方向的权值
//水平方向的分数
// 双循环原因: 第一层来看控制五元组的位置, 第二层变量五元组中的每个元素
for (int z = -4; z < 1; z++) {
for (int k = z; k < 5 + z; k++) {
if (i + k < 15 && i + k >= 0) {
if (map[i + k][j] == 1) sbHorizon.append("B");
if (map[i + k][j] == -1) sbHorizon.append("W");
}
}
String horString = sbHorizon.toString();
scoreArray[0] = scoreArray[0] + checkScore(horString);
sbHorizon.delete(0, 5);
}
//垂直方向
for (int z = -4; z < 1; z++) {
for (int k = z; k < 5 + z; k++) {
if (j + k < 15 && j + k >= 0) {
if (map[i][j + k] == 1) sbHorizon.append("B");
if (map[i][j + k] == -1) sbHorizon.append("W");
}
}
String horString = sbHorizon.toString();
scoreArray[1] = scoreArray[1] + checkScore(horString);
sbHorizon.delete(0, 5);
}
// " \ " 方向
for (int z = -4; z < 1; z++) {
for (int k = z; k < 5 + z; k++) {
if (j + k < 15 && j + k >= 0 && i + k < 15 && i + k >= 0) {
if (map[i + k][j + k] == 1) sbHorizon.append("B");
if (map[i + k][j + k] == -1) sbHorizon.append("W");
}
}
String horString = sbHorizon.toString();
scoreArray[2] = scoreArray[2] + checkScore(horString);
sbHorizon.delete(0, 5);
}
// " / " 方向
for (int z = -4; z < 1; z++) {
for (int k = z; k < 5 + z; k++) {
if (j - k < 15 && j - k >= 0 && i + k < 15 && i + k >= 0) {
if (map[i + k][j - k] == 1) sbHorizon.append("B");
if (map[i + k][j - k] == -1) sbHorizon.append("W");
}
}
String horString = sbHorizon.toString();
scoreArray[3] = scoreArray[3] + checkScore(horString);
sbHorizon.delete(0, 5);
}
score[i][j] = scoreArray[0] + scoreArray[1] + scoreArray[2] + scoreArray[3];
if (score[i][j] > max) {
max = score[i][j];
maxI = i;
maxJ = j;
}
}
}
}
System.out.println("最大值坐标: x: " + maxI + " y: " + maxJ);
}
/**
* 通过对照全指标, 得出五元组的分数
*
* @param s 五元组中的棋子分布情况
* @return
*/
public static int checkScore(String s) {
int score = 0;
if (s.contains("B") && s.contains("W")) score = -1; // 尝试解决这个问题 1, 如果最后扫描到白子, 应该结果分数小一点
else if (s.contains("B") && !s.contains("W")) {
if (s.contains("BBBB")) score = 800000;
else if (s.contains("BBB")) score = 15000;
else if (s.contains("BB")) score = 800;
else if (s.contains("B")) score = 7;
} else if (!s.contains("B") && s.contains("W")) {
if (s.contains("WWWW")) score = 100000;
else if (s.contains("WWW")) score = 1800;
else if (s.contains("WW")) score = 400;
else if (s.contains("W")) score = 15;
} else score = 0;
return score;
}
/**
* 判断输赢
*
* @param map 棋盘的二维数组 (注意此矩阵直观上看实际是棋盘的90度旋转)
* @param chessX 当前下的棋子横坐标
* @param chessY 当前下的棋子纵坐标
* @return 4 为黑赢 -4 为白赢
*/
public static int isWin(int[][] map, int chessX, int chessY) {
int num = 0;
// 设置一个最大的结果值, 最后返回
// 原因是下面的四个循环因为存在先后顺序, 可能最后小的 num 覆盖掉之前大的 num 导致返回的 num 不准确. 该赢的赢不了.
int maxNum = 0;
// 横赢算法
if ((map[chessX + 1][chessY] == map[chessX][chessY] && chessX + 1 <= LINE) || (chessX > 1 && map[chessX - 1][chessY] == map[chessX][chessY])) {
num = 0;
for (int i = -4; i < 5; i++) {
if (chessX + i >= 0 && chessX + i + 1 <= LINE) {
if (map[chessX + i][chessY] == map[chessX + i + 1][chessY] && map[chessX + i][chessY] != 0) {
num = num + map[chessX + i][chessY];
System.out.println("横赢num" + num);
if(Math.abs(num) >= maxNum) maxNum =num;
}
}
}
}
// 竖赢算法
if ((map[chessX][chessY + 1] == map[chessX][chessY] && chessY + 1 <= LINE) || (chessY > 1 && map[chessX][chessY - 1] == map[chessX][chessY])) {
num = 0;
for (int i = -4; i < 5; i++) {
if (chessY + i >= 0 && chessY + i + 1 <= LINE) {
if (map[chessX][chessY + i] == map[chessX][chessY + i + 1] && map[chessX][chessY + i] != 0) {
num = num + map[chessX][chessY + i];
System.out.println("竖赢num" + num);
if(Math.abs(num) >= maxNum) maxNum =num;
}
}
}
}
// 右斜下赢
// 先判断斜下是否有两个相连的情况, 并且横坐标等于 0 的时候 不做 x-1 的判断, 横坐标等于 15 的时候 不做 +1 判断.
if ((chessX > 1 && chessY > 1 && map[chessX][chessY] == map[chessX - 1][chessY - 1]) || (chessY < 15 && chessX < 15 && map[chessX][chessY] == map[chessX + 1][chessY + 1])) {
num = 0;
for (int i = -4; i < 5; i++) {
if (chessX + i >= 0 && chessX + i <= LINE - 1 && chessY + i >= 0 && chessY + i <= LINE - 1) {
if (map[chessX + i][chessY + i] == map[chessX + i + 1][chessY + i + 1] && map[chessX + i][chessY + i] != 0) {
num = num + map[chessX + i][chessY + i];
System.out.println("斜下num" + num);
if(Math.abs(num) >= maxNum) maxNum =num;
}
}
}
}
//左斜下赢
if ((chessX < 15 && chessY > 1 && map[chessX][chessY] == map[chessX + 1][chessY - 1]) || (chessY < 15 && chessX > 1 && map[chessX][chessY] == map[chessX - 1][chessY + 1])) {
num = 0;
for (int i = -4; i < 5; i++) {
if (chessX - i >= 1 && chessX - i <= LINE - 1 && chessY + i >= 0 && chessY + i <= LINE - 1) {
if (map[chessX - i][chessY + i] == map[chessX - i - 1][chessY + i + 1] && map[chessX - i][chessY + i] != 0) {
num = num + map[chessX - i][chessY + i];
System.out.println("左斜下num" + num);
if(Math.abs(num) >= maxNum) maxNum =num;
}
}
}
}
return maxNum;
}
/**
* 下棋子
*
* @param mf 棋子所在画布
* @param gr 画布的画笔
* @param map 存储棋盘的二位数字
* @param chessX 当前棋子横坐标
* @param chessY 当前其子纵坐标
*/
public static void placeChess(MyFrame mf, Graphics gr, int[][] map, int chessX, int chessY) {
if (Core.flag == 1) {
gr.setColor(Color.black);
//判断落子位置是否有棋子
if (map[chessX][chessY] != 1 && map[chessX][chessY] != -1) {
map[chessX][chessY] = 1;
for (int k = 0; k < 50; k++) {
Color c = new Color(4 * k, 4 * k, 4 * k);
gr.setColor(c);
gr.fillOval(X0 + chessX * SIZE - 25 + k / 3, Y0 + chessY * SIZE - 25 + k / 3, 50 - k, 50 - k);
}
System.out.println("下黑棋");
System.out.println("棋子坐标为:" + chessX + "," + chessY + " 棋子颜色为: " + Core.flag);
Core.flag = -1;
}
} else if (Core.flag == -1) {
gr.setColor(Color.white);
if (map[chessX][chessY] != 1 && map[chessX][chessY] != -1) {
map[chessX][chessY] = -1;
for (int k = 0; k < 50; k++) {
Color c = new Color(k + 200, k + 200, k + 200);
gr.setColor(c);
gr.fillOval(X0 + chessX * SIZE - 25 + k / 3, Y0 + chessY * SIZE - 25 + k / 3, 50 - k, 50 - k);
}
System.out.println("下白棋");
System.out.println("棋子坐标为:" + chessX + "," + chessY + " 棋子颜色为: " + Core.flag);
Core.flag = 1;
}
}
}
}
Config 接口
提供棋盘的一些基本数据, 例如行数列数,左上角的起始坐标.
public interface Config {
public static final int X0 = 50;
public static final int Y0 = 80;
public static final int LINE = 15;
public static final int SIZE = 50;
}
GameUI 类
初始化 UI 的类, 定义了 UI 中所需要的画布, 按钮等可视元素. 并将事件监听器添加到这些可视元素上.
import javax.swing.*;
import java.awt.*;
public class GameUI {
public static void main(String[] args) {
GameUI ui = new GameUI();
ui.initUI();
}
public void initUI() {
//需要调用有重写paint方法的JFrame子类
MyFrame jf = new MyFrame();
jf.setSize(1000, 830);
jf.setTitle("五子棋游戏");
jf.setLocationRelativeTo(null);
JButton restartButton = new JButton("重新开始");
JButton redoButton = new JButton("悔棋");
JRadioButton aiButton = new JRadioButton("人机对战",true);
JRadioButton playerButton = new JRadioButton("玩家对战");
ButtonGroup modeButtonGroup = new ButtonGroup();
modeButtonGroup.add(aiButton);
modeButtonGroup.add(playerButton);
Box b1 = Box.createHorizontalBox();
Box b2 = Box.createVerticalBox();
jf.add(b1);
b1.add(Box.createRigidArea(new Dimension(800,50)));
// b1 内嵌套一个 b2
b1.add(b2);
b2.add(restartButton);
b2.add(redoButton);
b2.add(aiButton);
b2.add(playerButton);
// 把两个按钮撑到上面
b2.add(Box.createRigidArea(new Dimension(0,600)));
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.setVisible(true);
// 获取画笔对象
Graphics g = jf.getGraphics();
GameMouse mouse = new GameMouse(g);
jf.addMouseListener(mouse);
ButtonAction buttonAction = new ButtonAction(g, jf, redoButton, restartButton,aiButton,playerButton);
restartButton.addActionListener(buttonAction);
redoButton.addActionListener(buttonAction);
aiButton.addActionListener(buttonAction);
playerButton.addActionListener(buttonAction);
}
}
遇到的一些细节上的问题
g.drawLine() 方法无法执行的原因
- 画不出来的原因是 paint 方法在创建 JFrame 实例的时候自动执行, 和 drawLine 方法是并发的, 画线的线程可能会在 paint 方法之前就执行了.
- 此时, 通过手动将 drawLine 方法延迟执行, 就可以避免前后颠倒的情况发生
- 另外, 通过重写 paint 方法, 也可以达到同样的目的
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
g.drawLine(20,20,50,50);
单独通过单击改变棋子颜色
- 未使用数组的情况下
if (flag == 1) {
gr.setColor(Color.black);
gr.fillOval(X0 + chessX * SIZE - 25, Y0 + chessY * SIZE - 25, 50, 50);
System.out.println("下黑棋");
flag = 2;
}
else if (flag == 2) {
gr.setColor(Color.white);
gr.fillOval(X0 + chessX * SIZE - 25, Y0 + chessY * SIZE - 25, 50, 50);
System.out.println("下白棋");
flag = 1;
}
System.out.println("棋子坐标为:" + chessX + "," + chessY + " 棋子颜色为: " + flag);