1.人员分工
组员 | 职务 | 负责模块 |
刘新臻 | 组长 | AI算法设计、程序逻辑设计 |
陈钦毅 | 组员 | 游戏画面框架,监听器、工具类 |
文才学 | 组员 | 棋子设计类、菜单功能 |
2.前期调查
根据调研我们需要以下功能
功能需求:
1、实现人与人对决。
2、实现人与机器对决,对局双方各执一色棋子,要求其中一方为机器。
3、游戏开始要求为空棋盘。
4、黑先、白后,交替下子,每次只能下一子。
5、棋子下在棋盘的空白点上,棋子下定后,不得向其它点移动,不得从棋盘上拿掉或拿起另落别处。
6、黑方的第一枚棋子可下在棋盘任意交叉点上。
7、轮流下子是双方的权利,但允许任何一方放弃下子权
8、直到有一方获胜,结束对局。
9、要有图形界面,且界面设计美观、交互性好
10、允许悔棋
11、可以直接结束正在进行的对局
3.需求分析
3.1系统数据结构分析
五子棋的棋盘是一个15*15规格的棋盘,对于棋局使用同等大小的二维数组存放棋盘上当前的格局。使用同样大小的二维数组,存放棋盘上各个点计算出来的权值。为了计算实现悔棋操作,定义了一个栈,用来记录每一步的棋子。这三个数据结构定义在ChessPad中,分别命名为:ColorPad 和 ValuePad 以及 Stack。ChessPad相当于一个虚拟的棋盘,系统界面就是根据该类的上述三个数据,去绘制棋盘,形成可视化界面的。
机器方AI类的定义,该类中使用到的数据结构有两个Map,分别为AttackMap,其中存放的是进攻棋型,以及棋型对应的权值;DefendMap存放的是防守棋型,以及棋型对应的权值。这两个Map中,棋型是key,权值是value。
3.2系统界面分析
系统界面布局为上下布局,上半部分为棋盘,大小为800*700像素,棋盘单独为一个容器定义为ChessPadPanel;下半部分为按钮区,也是一个单独的容器ButtonPanel,各个按钮用于实现对系统的各个功能的操作。
主页面MainPanel中包含两个组件,ChessPadPanle 和 ButtonPanel,为实现上下布局,则MainPanel需要继承JSplitPane类,实现对容器的上下布局。
3.3AI的下棋决策分析
本系统的AI博弈决策采用的是向后看一步的机制,即AI在下棋之前,会假设棋盘中每一个空点下黑棋以及下白棋以后,各个点会得到什么样的棋型,然后计算各点棋型的权值总和,作为该点的权值。最后,在ValuePad中选取一个权值最大的点作为AI的下一步棋。
3.4系统按钮功能的分析
模式按钮,本系统具有两种模式,分别为“人人对战”和“人机对战”,用于选择一个按钮点击,则进入相应的对局模式,整个对局的过程中的每一步走棋系统都需要判断当前的对局模式是什么,然后来选择是否需要调用AI进行下棋,为此在ChessPad中定义一个mode参数,该参数用来记录当前对局的模式为什么。
结束按钮,该按钮实现结束当前正在进行的对局,同时结束以后上一局的棋局不能够影响到了下一局游戏,所以当点击结束按钮以后,需要清除棋盘上的数据,ColorPad 和 ValuePad都应重置为空棋盘。
跳过按钮,实现用户弃棋的操作(即放弃本回合下棋),当点击该按钮是,将当前下棋方的下棋权力交给对手方,为了实现该功能,系统需要能够判断当前下棋的是白方还是黑方,为此在ChessPad中定义了一个BlackTurn参数,该参数为一个布尔值,值为true时,表示当前为黑方回合,值为false时表示当前回合为白方回合,所以要实现跳过的功能,只需要在用户每次点击该按钮以后,修改BalckTurn的值即可。
悔棋按钮,实现用户悔棋操作,需要记录棋局的下棋顺序,因此系统有一个栈Stack用来记录当前棋局的每一步走棋,用户点击悔棋,则将栈顶的走棋出栈,同时重新绘制棋盘即可。
4.主要功能流程图
5.UML图
6.项目运行截图
6.1进入游戏,选择模式
6.2若干步后
6.3悔棋操作
6.4黑棋五子连珠获胜
6.5重新选择模式继续游戏或者退出游戏
7.关键代码
7.1AI下棋位置
public static int CalculateValue(Position p) {
// 调用方法,返回该点的棋链
List<String> attackList = getPositionChessLink(p, "attack");
List<String> defendList = getPositionChessLink(p, "defend");
// 计算权值
int attackValue = 0;
int defendValue = 0;
for (String s : attackList) {
if (AttackMap.containsKey(s)) {
attackValue += AttackMap.get(s);
}
}
for (String s : defendList) {
if (DefendMap.containsKey(s)) {
defendValue += DefendMap.get(s);
}
}
return Math.abs(attackValue - defendValue); // 值越大说明该点即适合防御又适合进攻
}
/**
* 该方法实现获取一个点的所有棋链 参数p必须为行列坐标
*
* 棋链的获取分为四个方向:横、竖、斜、反斜
*
* @return
*/
public static List<String> getPositionChessLink(Position p, String type) {
// 获取棋盘
ChessColor[][] colorPad = ChessPad.colorPad;
int size = ChessPad.size; // 棋盘的大小
boolean flag = false;
if (colorPad[p.getX()][p.getY()] == ChessColor.Blank && type.equals("attack")) {
// 假设该空点下的棋为黑棋
colorPad[p.getX()][p.getY()] = ChessColor.Black;
flag = true;
} else if (colorPad[p.getX()][p.getY()] == ChessColor.Blank && type.equals("defend")) {
colorPad[p.getX()][p.getY()] = ChessColor.White;
flag = true;
}
List<String> resultList = new ArrayList<>();
// 计算竖方向的棋链权值
for (int i = 4; i <= 7; i++) { // 棋链长度最短4 最长不超过7
for (int j = 0; j < i; j++) { // 判断传入的棋子在棋链上的位置(即第几个棋子,从左向右数)
String s = "";
int startRow = p.getX() - j; // 计算方向的开始坐标,从该点为原点遍历
int endRow = startRow + i - 1; // 计算方向的结束坐标,减去本身,决定4-7的长度
// 此处需要判断开始的点和结束的点是否超出了棋盘
if (startRow < 0 || endRow >= size) {
// 该点超过了棋盘的范围
continue;
}
for (; startRow <= endRow; startRow++) {
if (colorPad[startRow][p.getY()] == ChessColor.Blank) {
s = s + "-";
} else if (colorPad[startRow][p.getY()] == ChessColor.White) {
s = s + "o";
} else {
s = s + "*";
}
}
// System.out.println(s);
resultList.add(s); // 将该棋链加入结果列表
}
}
// 计算横方向的棋链权值
for (int i = 4; i <= 7; i++) { // 棋链长度,最短4,最长7
for (int j = 0; j < i; j++) { // 棋在棋链上的位置,(从上往下)
String s = "";
// 由于是横方向,故只需要计算开始的y和结束的y坐标
int startCol = p.getY() - j;
int endCol = startCol + i - 1;
// 判断开始位置和结束位置是否在棋盘范围内
if (startCol < 0 || endCol >= size) {
continue;
}
for (; startCol <= endCol; startCol++) {
if (colorPad[p.getX()][startCol] == ChessColor.Blank) {
s = s + "-";
} else if (colorPad[p.getX()][startCol] == ChessColor.White) {
s = s + "o";
} else {
s = s + "*";
}
}
// System.out.println(s);
resultList.add(s);
}
}
// System.out.println("斜方向:");
// 从斜方向获取棋链
for (int i = 4; i <= 7; i++) {
for (int j = 0; j < i; j++) {
// 此处为斜方向,改变棋在棋链上的位置,涉及 x 和 y 两个方向的改变,从左下往右上的方向来计算 两个坐标的变化为
int startRow = p.getX() + j;
int startCol = p.getY() - j;
int endRow = startRow - i + 1;
int endCol = startCol + i - 1;
// 判断开始点和结束点是否在棋盘内
if (!((startRow >= 0 && startRow < size && startCol >= 0 && startCol < size)
&& (endRow >= 0 && endRow < size && endCol >= 0 && endCol < size))) {
continue;
}
String s = "";
for (int row = startRow, col = startCol; row >= endRow && col <= endCol; row--, col++) {
if (colorPad[row][col] == ChessColor.Blank) {
s = s + "-";
} else if (colorPad[row][col] == ChessColor.White) {
s = s + "o";
} else {
s = s + "*";
}
}
// System.out.println(s);
resultList.add(s);
}
}
// System.out.println("反斜方向:");
// 反斜方向
for (int i = 4; i <= 7; i++) {
for (int j = 0; j < i; j++) {
// 计算开始的点
int startRow = p.getX() - j;
int startCol = p.getY() - j;
int endRow = startRow + i - 1;
int endCol = startCol + i - 1;
String s = "";
if (!((startRow >= 0 && startRow < size && startCol >= 0 && startCol < size)
&& (endRow >= 0 && endRow < size && endCol >= 0 && endCol < size))) {
continue;
}
for (int row = startRow, col = startCol; row <= endRow && col <= endCol; row++, col++) {
if (colorPad[row][col] == ChessColor.Blank) {
s = s + "-";
} else if (colorPad[row][col] == ChessColor.White) {
s = s + "o";
} else {
s = s + "*";
}
}
// System.out.println(s);
resultList.add(s);
}
}
// 返回之前将临时下的棋恢复
if (flag) {
colorPad[p.getX()][p.getY()] = ChessColor.Blank;
}
return resultList;
}
7.2悔棋、跳过操作(使用栈后进先出的思想)
public class Sequence {
public static List<Position> positions=new ArrayList<>(); //下棋位置的顺序
public static List<ChessColor> colorsSequence=new ArrayList<>(); //下棋颜色的顺序
/**
* 出栈操作
* @return
*/
public static Position pop(){
colorsSequence.remove(colorsSequence.size()-1);
return positions.remove(positions.size()-1);
}
public static void push(Position p,ChessColor color){
colorsSequence.add(color);
positions.add(p);
}
/**
* 清空下棋顺序
*/
public static void ClearSequence(){
positions=new ArrayList<>();
colorsSequence=new ArrayList<>();
}
}
7.3鼠标落子
public class PutChessListener implements MouseListener {
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
//鼠标释放事件
@Override
public void mouseReleased(MouseEvent e) {
//获取鼠标释放的位置,像素坐标
int x = e.getX();
int y = e.getY();
Position position=new Position(x,y);
Position change = PositionUtils.XAndYChangeToRowAndCol(position); //将像素坐标转换为行列坐标
//判断点击的区域是否为有效区域
if(change!=null){
ChessColor[][] colorPad = ChessPad.colorPad;
//由于有人人对战模式,所以需要判断当前下棋是哪一方下
//判断下棋位置是否有棋
if (colorPad[change.getX()][change.getY()]!=ChessColor.Blank){
return;
}
if(ChessPad.mode.equals("人人对战")){
if(ChessPad.isBlackTurn()){
colorPad[change.getX()][change.getY()]=ChessColor.Black;
Sequence.push(change,ChessColor.Black); //当前这一步棋入栈
}
else {
colorPad[change.getX()][change.getY()] = ChessColor.White;
Sequence.push(change,ChessColor.White); //当前这一步棋入栈
}
ChessPadPanel.getInstance().repaint();
//变更下棋回合
ChessPad.ChangeTurn();
AI.Judge(); //Ai判断本局游戏是否结束
}
else {
//当前模式为人机对战,则人下棋完成以后,从新绘制了棋盘,然后在让AI完成下棋
if(!ChessPad.isBlackTurn()){
//鼠标点击了棋盘,且当前不是黑棋回合,则完成下棋
colorPad[change.getX()][change.getY()]=ChessColor.White;
Sequence.push(change,ChessColor.White); //当前这一步棋入栈
ChessPadPanel.getInstance().repaint();
//变更下棋回合
ChessPad.ChangeTurn();
AI.Judge(); //Ai判断本局游戏是否结束
//启用Ai,完成下棋
AI.AiPlay();
ChessPadPanel.getInstance().repaint();
AI.Judge(); //Ai判断本局游戏是否结束
}
}
}
//需要判断棋盘的结果,看看是否有获胜
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
8.项目总结
感受:决定入手这一项大作业经过了我们小组的缜密讨论,原以为凭借一学期地艰辛求学可以较为顺利地完成这一项目。但是入手后才发现,并没有我们想象中的那么一帆风顺。既然决定开发出一款简单的游戏,我们就得精心准备,通过以往我们对五子棋的游戏经验和上网试玩了评分优良的游戏之后,我们开启了项目的进程。一款游戏,首先必须要有一个界面,要设置交互,设置按钮,要使得界面美观、简单,要有吸引玩家继续游玩的特色。于是我们精心打造了AI算法,使得游戏难度提升,刺激了玩家的胜负欲。这也是我们这次项目的优点之一。考虑到玩家可能出现的误差错误,我们提供了可悔棋,增加了玩家游戏体验。
建议:虽然我们实现了一些基本的功能,但是我们并没有因此感到骄傲,我们仍然存在着诸多不足。我们本学期学习到的网络编程以及多线程我们并没有运用到该项目中,导致我们的项目游玩限制比较大,未能更加丰富玩家的体验。但是我们并不会因此而放弃对Java面向对象程序设计地探索,今后我们依然会保持住学习的热情,继续奋发图强,争取做到更好!
不足之处:
1、我们的AI函数没有设置难度选择。导致挑战难度单一。
2、没有设置先手的禁手,因为强大的AI可以实现先手必胜。
3、没有实现网络对战等特殊玩法。
4、项目菜单过于零散,没有很好地整合功能,界面较为简陋。
5、固定了AI的先手性,使得挑战过于单一。