背景:设计五子棋游戏背景,
初步猜想:
① 设计一个五子棋小游戏,实现人机对战和双人对战两功能。
② 设计五子棋小游戏,使用Java Swing设计可视化操作界面,并使用GraPhic 2D技术设计图形
③ 使用五元组算法实现人机对战方法
参考算法链接:https://blog.csdn.net/m0_51336041/article/details/125633406
目录
一、团队成员及任务
队长:
高晔川 计科(智能)22-1 202203200035
负责模块: Java Swing界面搭建,Graphic 2D图形绘制,Panel面板搭建,五子棋过程的实现构思
个人博客地址:(140条消息) 面向对象程序设计(Java) 课程设计——三少五子棋 Part1_Aimless.的博客-CSDN博客
队员:
成员① 戚彦良 计科(智能)22-1 202203200029
负责模块: 负责函数算法的编写,使用函数方法体判断哪方获胜,人机对战中如何实现 排行榜功能的实现
个人博客地址:
(134条消息) 面向对象程序设计(Java)课程设计——三少五子棋Part2_m0_73640433的博客-CSDN博客
成员② 吴沅峰 计科(智能)22-1 202203200020
负责模块:鼠标监听器、动作监听器、人机对战自动下棋算法
个人博客地址:(134条消息) 面对程序设计(Java)课程设计————三少五子棋_a1067793829的博客-CSDN博客
二、项目简介
功能描述:由三人合作运用Java编写五子棋小游戏,具有双人对战,人机对战,悔棋,Replay,GameOver,投降,排行榜功能。可从排行榜中刷新,查看战绩,并且在About us 中加入了我们制作人的信息。
最终实现的功能如下:
基本功能:生成界面,绘制棋盘,绘制棋子,实现轮流点击下棋并判断输赢。
开始游戏功能:初始化相应的各项数据与变量,清空棋盘,重新游戏。
游戏说明功能:生成对话框,介绍游戏规则。
认输功能:轮到某一方下棋时,点击此按钮,该方认输,游戏结束。
保存战绩:贯穿整个游戏的变量胜场数,需连接数据库并将数据保存。
查看战绩:连接并读取数据库,将存入的胜场数据读取并输出。
三、项目架构图
黄标:队长负责的板块
黄标:成员①的负责板块
黄标:成员②负责的板块
四、运行结果图
五、项目git地址:
https://gitee.com/HitoriBocchi0113/lottery_-final
六、个人部分实现过程(代码关键部分说明)
〇 我们假设每个数组可以存储对应的五子棋的显色情况,声明存储五子棋存在的二维数组,
① 总体界面如图所示
总体界面搭建我们使用了继承JPanel类的子类,在构造方法中声明Panel面板的信息
总体工作模块如图所示:
JLabel selectLabel = new JLabel("游戏选择:");
JButton doublePerson = new JButton("双人对战");
JButton singleBlack = new JButton("人机持黑");
JButton singleWrite = new JButton("人机持白");
JLabel elseLabel = new JLabel("其他设置:");
JButton regret = new JButton("悔棋");
JButton restart = new JButton("Replay");
JButton forExit = new JButton("GameOver");
JButton surrender = new JButton("投降");
JButton leaderBoard = new JButton("排行榜");
JButton aboutUs = new JButton("About us");
在这里呈现了我们的代码块
public TablePanel() {
setLayout(null);
setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));//设置组件的首选大小
setBackground(Color.yellow); //设置背景颜色
init(); //初始化一些属性
isStart = 0;
addMouseListener(mouseAdapter); //添加鼠标监听
addMouseMotionListener(mouseAdapter);
selectLabel.setFont(font1);
doublePerson.setBounds(50,600,100,30);
doublePerson.setFont(font2);
singleBlack.setBounds(175,600,100,30);
singleBlack.setFont(font2);
singleWrite.setBounds(300,600,100,30);
singleWrite.setFont(font2);
elseLabel.setFont(font1);
regret.setBounds(425,600,100,30);
regret.setFont(font2);
restart.setBounds(550,600,100,30);
restart.setFont(font2);
forExit.setBounds(50,660,100,30);
forExit.setFont(font2);
surrender.setBounds(175,660,100,30);
surrender.setFont(font2);
leaderBoard.setBounds(300,660,100,30);
leaderBoard.setFont(font2);
aboutUs.setBounds(425,660,100,30);
aboutUs.setFont(font2);
titleLabel.setFont(font3);
doublePerson.addActionListener(actionListener);
singleBlack.addActionListener(actionListener);
singleWrite.addActionListener(actionListener);
regret.addActionListener(actionListener);
restart.addActionListener(actionListener);
forExit.addActionListener(actionListener);
surrender.addActionListener(actionListener);
leaderBoard.addActionListener(actionListener);
aboutUs.addActionListener(actionListener);
add(selectLabel);
add(doublePerson);
add(singleBlack);
add(singleWrite);
add(elseLabel);
add(regret);
add(restart);
add(forExit);
add(surrender);
add(leaderBoard);
add(titleLabel);
add(aboutUs);
}
通过上述代码我们可以搭建出基本的操作界面,为后续功能实现创造条件
②使用Graphic绘图
在实现过程中,我们首先考虑了使用棋盘图片作为背景,但图片的质量可能会影响到面板的清晰度和整体的观感,甚至出现锐度低效果失真的情况,遂不考虑使用Icon类方法插入图片。最终我们使用了Graphic类方法创建了五子棋网格,代码如图所示
private void initPaint(Graphics g, Graphics2D gg) {
super.paint(g);
g.setColor(Color.BLACK);
for (int i = 0; i < num; i++) {
int x = Initial_X + SP * i;
int y = Initial_Y + SP * (num - 1);
g.drawLine(x, Initial_Y, x, y);
}
for (int i = 0; i < num; i++) {
int x = Initial_X + SP * (num - 1);
int y = Initial_Y + SP * i;
g.drawLine(Initial_X, y, x, y);
}
int[][] positions = { {3, 3}, {11, 3}, {3, 11}, {11, 11}, {7, 7} };
for (int[] pos : positions) {
int x = Initial_X + SP * pos[0] - RECT_SIZE / 2;
int y = Initial_Y + SP * pos[1] - RECT_SIZE / 2;
g.fillOval(x, y, RECT_SIZE, RECT_SIZE);
}
bs = new BasicStroke(5);
gg.setStroke(bs);
gg.drawRect(Initial_X - 7, Initial_Y - 7, (num - 1) * SP + 14, (num - 1) * SP + 14);
bs = new BasicStroke(3);
gg.setStroke(bs);
for (int i = 1; i < num; i += 4) {
int x = Initial_X + SP * i;
int y = Initial_Y + SP * (num - 1);
gg.drawLine(x, Initial_Y, x, y);
}
for (int i = 1; i < num; i += 4) {
int x = Initial_X + SP * (num - 1);
int y = Initial_Y + SP * i;
gg.drawLine(Initial_X, y, x, y);
}
}
我们首先清除之前绘制的内容,使用前两组for循环创建基本的网格,确定棋盘的基本框架。
position数组存储的是特殊的坐标,用于存储棋盘上key point的位置,第三组循环是用于绘制棋盘上的关键点。之后两组循环是用来加粗边界。我们创建ovalPaint方法确定了每个棋子的显示状况
private void ovalPaint(Graphics2D gg) {
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
int x = Initial_X + SP * i - cheeseSize / 2;
int y = Initial_Y + SP * j - cheeseSize / 2;
if (table[i][j] == 2) {
gg.setColor(Color.BLACK);
gg.fillOval(x, y, cheeseSize, cheeseSize);
} else if (table[i][j] == 1) {
gg.setColor(Color.WHITE);
gg.fillOval(x, y, cheeseSize, cheeseSize);
} else if (table[i][j] == 3) {
gg.setColor(Color.RED);
gg.drawOval(x, y, cheeseSize, cheeseSize);
}
}
}
if (win) {
select_X = -10;
select_Y = -10;
} else {
bs = new BasicStroke(1); // 画笔宽度为1
gg.setStroke(bs);
//画选择框
gg.setColor(Color.BLACK);
gg.drawOval(Initial_X + SP * select_X - cheeseSize / 2,
Initial_Y + SP * select_Y - cheeseSize / 2,
cheeseSize, cheeseSize);
}
}
这样,我们可以确定基本的操作面板。
③函数算法的编写,使用函数方法体判断哪方获胜
判断某种棋子是否已经在当前局面中胜利。它通过遍历八个方向来判断该种棋子的连子情况,如果存在一个方向中有四个或以上的同色棋子,则将win标记置为true,表示该种棋子获胜。
具体来说,该段代码中的directions数组存储了八个方向的偏移量,其中{ -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 }分别表示横向、纵向、左斜和右斜四个方向,{ -1, -1 }, { 1, 1 }, { -1, 1 }, { 1, -1 }分别表示左上到右下、右下到左上、左下到右上和右上到左下四个方向。然后对于每个方向,程序将棋子逐个向一个方向延伸,直到遇到一个空位置或者边界,如果在此过程中遇到了相同颜色的棋子,则将 sum 计数器加1,表示已经扩展了一个棋子,直到 sum 累计数值等于或者大于4,则表示该种棋子已经获胜。
当某种棋子获胜时,将win标记置为true,同时记录下获胜时的棋局情况,包括步数和胜利方。这段代码将数据写入到一个文件中,文件路径为a.txt。如果文件不存在,则会创建一个空文件。
private void judgement(int type, int x, int y) {
// 传入参数,来判断是黑(2)或白(1)子
int[][] directions = { {-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1} };
// 定义八个方向的偏移量
for (int[] direction : directions) {
int sum = 0;
int dx = direction[0];
int dy = direction[1];
int i = x + dx;
int j = y + dy;
while (i >= 0 && i < num && j >= 0 && j < num && table[i][j] == type) {
sum++;
i += dx;
j += dy;
}
i = x - dx;
j = y - dy;
while (i >= 0 && i < num && j >= 0 && j < num && table[i][j] == type) {
sum++;
i -= dx;
j -= dy;
}
if (sum >= 4) {
win = true;
if (win){
try{
java.io.File file = new java.io.File("a.txt");
if (!file.exists()){
file.createNewFile();
}
if (oval_type == 1){
String result = "步数:" + step + "黑方赢" + '\n';
java.io.FileWriter fileWriter = new java.io.FileWriter(file,true);
fileWriter.write(result);
fileWriter.close();
} else {
String result = "步数:" + step + "白方赢" + '\n';
java.io.FileWriter fileWriter = new java.io.FileWriter(file,true);
fileWriter.write(result);
fileWriter.close();
}
}catch (java.io.IOException e){
e.printStackTrace();
}
}
return;
}
}
}
core()方法
用来评估当前棋局的分数的,以便计算计算机下一步的最佳落子点。该方法的输入参数w和b分别表示白色和黑色棋子在当前局面中连续的棋子个数,其中w和b至少有一个为0。如果两个参数都为0,则返回7,表示此处可以下子。如果w和b都大于0,则返回0,表示此处不能下子。如果只有其中一种连续的棋子,则根据对应的分数返回结果。分数越高,表明这个位置越有利。
具体来说,当白子连续1颗时返回15分,连续2颗时返回400分,连续3颗时返回1800分,连续4颗时返回100000分;当黑子连续1颗时返回35分,连续2颗时返回800分,连续3颗时返回15000分,连续4颗时返回800000分。
private int score(int w, int b) {
if (w > 0 && b > 0) {
return 0;
}
if (w == 0 && b == 0) {
return 7;
}
if (w == 1) {
return 35;
}
if (w == 2) {
return 800;
}
if (w == 3) {
return 15000;
}
if (w == 4) {
return 800000;
}
if (b == 1) {
return 15;
}
if (b == 2) {
return 400;
}
if (b == 3) {
return 1800;
}
if (b == 4) {
return 100000;
}
return -1;
}
④ 实现具体思路:
如果点击的是"Replay"按钮,调用init()
方法重新初始化游戏状态,如果当前是人机对战模式并且轮到机器下棋,则调用automatically()
方法让机器自动下棋。最后更新按钮的状态。
如果点击的是"悔棋"按钮,从coordinate
列表中获取最后两个坐标,将对应位置的棋子设置为0(空),并移除这两个坐标。如果是人机对战模式或者机器下棋模式,则再次执行一次悔棋操作。更新oval_type
(当前下棋方),并根据情况更新步数和胜负状态。
如果点击的是"GameOver"按钮,重置游戏状态,设置isStart
为0,使得重新选择游戏模式的按钮可用,并禁用其他按钮。
如果点击的是"排行榜"按钮,创建一个LeaderboardFrame
对象,显示排行榜窗口。
如果点击的是"投降"按钮,根据当前下棋方弹出相应的投降提示框,然后重置游戏状态,使得重新选择游戏模式的按钮可用,并禁用其他按钮。
如果点击的是"About us"按钮,弹出制作人的信息对话框。
Object source = e.getSource();
if (source instanceof JButton){
JButton jButton = (JButton) e.getSource();
String text = jButton.getText();
if ("Replay".equals(text)) {
init();
if (isStart == 3) {
automatically();
step++;
table[robot_x][robot_y] = 2;
oval_type = 1;
}
regret.setEnabled(false);
restart.setEnabled(false);
} else if ("悔棋".equals(text)) {
int x = coordinate.get(coordinate.size() - 2);
int y = coordinate.get(coordinate.size() - 1);
table[x][y] = 0;
coordinate.remove(coordinate.size() - 2);
coordinate.remove(coordinate.size() - 1);
oval_type = oval_type % 2 + 1;
if (isStart == 2 || isStart == 3) {
x = coordinate.get(coordinate.size() - 2);
y = coordinate.get(coordinate.size() - 1);
table[x][y] = 0;
coordinate.remove(coordinate.size() - 2);
coordinate.remove(coordinate.size() - 1);
oval_type = oval_type % 2 + 1;
}
if (oval_type == 2 || isStart == 3) {
step--;
}
if (win) {
win = false;
}
if (coordinate.size() == 0) {
regret.setEnabled(false);
restart.setEnabled(false);
}
} else if ("GameOver".equals(text)) {
isStart = 0;
init();
doublePerson.setEnabled(true);
singleBlack.setEnabled(true);
singleWrite.setEnabled(true);
regret.setEnabled(false);
restart.setEnabled(false);
forExit.setEnabled(false);
}else if ("排行榜".equals(text)){
LeaderboardFrame leaderboardFrame = new LeaderboardFrame();
} else if ("投降".equals(text)){
if (oval_type == 1){
JOptionPane.showMessageDialog(null,"白棋投降,游戏结束","",JOptionPane.INFORMATION_MESSAGE);
} else if (oval_type == 2){
JOptionPane.showMessageDialog(null,"黑棋投降,游戏结束","",JOptionPane.INFORMATION_MESSAGE);
}
init();
isStart = 0;
doublePerson.setEnabled(true);
singleBlack.setEnabled(true);
singleWrite.setEnabled(true);
leaderBoard.setEnabled(true);
regret.setEnabled(false);
restart.setEnabled(false);
forExit.setEnabled(false);
surrender.setEnabled(false);
} else if ("About us".equals(text)) {
JOptionPane.showMessageDialog(null,"制作人:高晔川 戚彦良 吴沅峰");
} else {
//上面三个按钮
if ("双人对战".equals(text)) {
isStart = 1;
} else if ("人机持白".equals(text)) { //根据规则,黑棋先下
isStart = 2;
} else if ("人机持黑".equals(text)) {
isStart = 3;
automatically();
step++;
table[robot_x][robot_y] = 2;
oval_type = 1;
}
doublePerson.setEnabled(false);
singleBlack.setEnabled(false);
singleWrite.setEnabled(false);
forExit.setEnabled(true);
}
repaint();
同样,人机下棋的代码可以这种代码方式实现:
private void automatically() {
//传入棋子种类,判断颜色
int[][] ts = new int[num][num]; //来记录每个点上的得分
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
ts[i][j] = 0;
}
}
int wn; //白色个数
int bn; //黑色个数
//分4种情况
//横向
for (int i = 0; i < num; i++) {
for (int j = 0; j < num - 4; j++) {
wn = 0;
bn = 0;
//5个
for (int k = j; k < j + 5; k++) {
if (table[i][k] == 1) {
wn++;
} else if (table[i][k] == 2) {
bn++;
}
}
for (int k = j; k < j + 5; k++) {
if (table[i][k] == 0) {
ts[i][k] += score(wn, bn);
}
}
}
}
//纵向
for (int j = 0; j < num; j++) {
for (int i = 0; i < num - 4; i++) {
wn = 0;
bn = 0;
for (int k = i; k < i + 5; k++) {
if (table[k][j] == 1) {
wn++;
} else if (table[k][i] == 2) {
bn++;
}
}
for (int k = i; k < i + 5; k++) {
if (table[k][i] == 0) {
ts[k][i] += score(wn, bn);
}
}
}
}
//左上 右下
for (int i = 0; i < num - 4; i++) {
for (int j = 0; j < num - 4; j++) {
wn = 0;
bn = 0;
for (int ki = i, kj = j; ki < i + 5; ki++, kj++) {
if (table[ki][kj] == 1) {
wn++;
} else if (table[ki][kj] == 2) {
bn++;
}
}
for (int ki = i, kj = j; ki < i + 5; ki++, kj++) {
if (table[ki][kj] == 0) {
ts[ki][kj] += score(wn, bn);
}
}
}
}
//右上 左下
for (int i = 4; i < num; i++) {
for (int j = 0; j < num - 4; j++) {
wn = 0;
bn = 0;
for (int ki = i, kj = j; kj < j + 5; ki--, kj++) {
if (table[ki][kj] == 1) {
wn++;
} else if (table[ki][kj] == 2) {
bn++;
}
}
for (int ki = i, kj = j; kj < j + 5; ki--, kj++) {
if (table[ki][kj] == 0) {
ts[ki][kj] += score(wn, bn);
}
}
}
}
Vector<Integer> vv = new Vector<>();
int max = Integer.MIN_VALUE;
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
if (ts[i][j] > max) {
max = ts[i][j];
}
}
}
for (int i = 0; i < num; i++) {
for (int j = 0; j < num; j++) {
if (ts[i][j] == max) {
vv.add(i);
vv.add(j);
}
}
}
Random random = new Random();
int r = random.nextInt(vv.size() / 2);
robot_x = vv.get(r * 2);
robot_y = vv.get(r * 2 + 1);
vv.clear();
}
⑤ 彩蛋:
else if ("About us".equals(text)) {
JOptionPane.showMessageDialog(null,"制作人:高晔川 戚彦良 吴沅峰");
}
······
总结/展望:
双人对战仅限于本机用户,后期若可以使用server平台,希望可以实现不同设备在局域网之下实现联机对战。
在五子棋棋盘上不一定智能使用五子棋,也可以本次课题搭建的界面为基础提供围棋的设计算法,将五子棋和围棋的判定标准分别封装为不同的函数,进一步体现封装性思想。
如果要实现五子棋和围棋的组合应用应该较为容易,前期界面的搭建以及棋子的显示都有共通之处。