1引言
本文档是考试系统项目的内容汇总,其主要内容包括:
- 项目说明
- 需求分析
- 项目设计
- 编码与实现
- 测试说明
- 课程设计体会与总结
2项目说明
2.1项目要求
五子棋是全国智力运动会竞技项目之一,是一种两人对弈的纯策略型棋类游戏。通常双方分别使用黑白两色的棋子,下在棋盘直线与横线的交叉点上,先形成五子连线者获胜。具体要求如下:
功能需求:
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编码与实现
5.1系统目录结构
5.2系统详细代码
详情见附录文件(与本报告一起解压保存)。
Domain目录下代码:
AI类
package Domain;
import Listiner.StopListener;
import Panel.ChessPadPanel;
import Panel.MainPanel;
import Utils.PositionUtils;
import javax.swing.*;
import java.util.*;
/**
* AI对象,人机对战模式时被激活
*
*
*/
public class AI {
private static Map<String,Integer> AttackMap=new HashMap<>();
private static Map<String,Integer> DefendMap=new HashMap<>();
/**
* 静态代码块,往map里面加入权值计算的规则
*
* -:表示空棋
* *:表示白棋
* o:表示白棋
*/
static {
//还要重新设计赋分策略,才能保证五子棋机器人的准确判断
//因为已经固定了 *为黑子,且默认将机器人设为黑方,所以机器人只能是黑方
//还要设置防守方的数值,防止被gank掉
//右边put的map值,是防守分数,这样Ai就不会一味的猛冲
//左边:进攻棋链,权值越大说明该棋链越接近胜利 右边:防御棋链,权值越大说明该棋链约值得防御
AttackMap.put("*****", 100000);//连五
/* */DefendMap.put("ooooo", 30000);
AttackMap.put("-****-", 5000);//活四
/* */DefendMap.put("-oooo-", 3000);
AttackMap.put("*-***", 700);//冲四 1
/* */DefendMap.put("o-ooo", 150);
AttackMap.put("***-*", 700);//冲四 1 反向
/* */DefendMap.put("ooo-o", 150);
AttackMap.put("-****o", 1000);//冲四 2
/* */DefendMap.put("-oooo*", 200);
AttackMap.put("o****-", 1000);//冲四 2 反向
/* */DefendMap.put("*oooo-", 200);
AttackMap.put("**-**", 700);//冲四 3
/* */DefendMap.put("oo-oo", 200);
AttackMap.put("-***-", 500);//活三 1
/* */DefendMap.put("-ooo-", 100);
AttackMap.put("*-**", 150);//活三 2
/* */DefendMap.put("o-oo", 50);
AttackMap.put("**-*", 150);//活三 2 反向
/* */DefendMap.put("oo-o", 50);
AttackMap.put("--***o", 100);//眠三 1
/* */DefendMap.put("--ooo*", 20);
AttackMap.put("o***--", 100);//眠三 1 反向
/* */DefendMap.put("*ooo--", 20);
AttackMap.put("-*-**o", 80);//眠三 2
/* */DefendMap.put("-o-oo*", 15);
AttackMap.put("o**-*-", 80);//眠三 2 反向
/* */DefendMap.put("*oo-o-", 15);
AttackMap.put("-**-*o", 60);//眠三 3
/* */DefendMap.put("-oo-o*", 10);
AttackMap.put("o*-**-", 60);//眠三 3 反向
/* */DefendMap.put("*o-oo-", 10);
AttackMap.put("*--**", 60);//眠三 4
/* */DefendMap.put("o--oo", 10);
AttackMap.put("**--*", 60);//眠三 4 反向
/* */DefendMap.put("oo--o", 10);
AttackMap.put("*-*-*", 60);//眠三 5
/* */DefendMap.put("o-o-o", 10);
AttackMap.put("o-***-o", 60);//眠三 6
/* */DefendMap.put("*-ooo-*", 2);
AttackMap.put("--**--", 50);//活二 1
/* */DefendMap.put("--oo--", 2);
AttackMap.put("-*-*-", 20);//活二 2
/* */DefendMap.put("-o-o-", 2);
AttackMap.put("*--*", 20);//活二 3
/* */DefendMap.put("o--o", 2);
AttackMap.put("---**o", 10);//眠二 1
/* */DefendMap.put("---oo*", 1);
AttackMap.put("o**---", 10);//眠二 1 反向
/* */DefendMap.put("*oo---", 1);
AttackMap.put("--*-*o", 10);//眠二 2
/* */DefendMap.put("--o-o*", 1 );
AttackMap.put("o*-*--", 10);//眠二 2 反向
/* */DefendMap.put("*o-o--", 1);
AttackMap.put("-*--*o", 10);//眠二 3
/* */DefendMap.put("-o--o*", 1);
AttackMap.put("o*--*-", 10);//眠二 3 反向
/* */DefendMap.put("*o--o-", 1);
AttackMap.put("*---*", 10);//眠二 4
/* */DefendMap.put("o---o", 1);
//上面之所以int类型不能自动向上转换为long类型
//是因为hashMap中的value使用的是包装类Long
//包装类虽然能自动装箱,但是不能将基础类型转换,再装箱
//所以需要手动转换为long类型
}
//Ai下棋主流程
public static void AiPlay(){
//清空权值矩阵,避免前一回合影响本回合的判断
ChessPad.ClearValuePad();
//判断是否为黑棋回合,AI永远下黑棋
if (!ChessPad.isBlackTurn()){
return;
}
Position bestPosition=null;
if(isEmptyPad()){
//如果是空棋盘,则永远下中间点的位置,
Random random=new Random();
//bestPosition = new Position(random.nextInt(15),random.nextInt(15));
bestPosition = new Position(7,7);
}
else {
//先计算防御棋链,看看当前棋局是否有需要防御的地方
boolean defend=false;
/*x:
for (int i=0;i<15;i++){
for (int j=0;j<15;j++){
List<String> defends = getPositionChessLink(new Position(i, j), "defend");
for (String s : defends) {
if (s.equals("ooooo")){
System.out.println("防御");
bestPosition=new Position(i,j);
defend=true;
break x;
}
}
}
}*/
if (!defend){
//计算整个棋盘的权值
getAllValue();
//选取最佳下棋位置
bestPosition = getBestPosition();
}
}
//将棋下入颜色棋盘
putChess(bestPosition);
Sequence.push(bestPosition,ChessColor.Black); //当前这一步棋入栈
//更换回合
ChessPad.ChangeTurn();
}
/**
* 该方法实现Ai的下棋操作,传入的Position对象 坐标为行列坐标
* @param p
*/
public static void putChess(Position p){
System.out.println(p);
//更改颜色矩阵,完成下棋操作
ChessPad.colorPad[p.getX()][p.getY()]=ChessColor.Black;
}
/**
* 该方法啊实现选取权值最大的点,AI下的棋就是下在这个点上
* @return 返回的是一个行列坐标的position对象
*/
public static Position getBestPosition(){
//遍历权值棋盘,找出权值最大的点
int[][] valuePad = ChessPad.valuePad;
ChessColor[][] colorPad = ChessPad.colorPad;
int size = ChessPad.size;
int max=-1;
Position position=new Position();
for(int i=0;i<size;i++){
for (int j=0;j<size;j++){
//判断该点的权值是否大于最大值,同时该点必须为空棋
if(colorPad[i][j]==ChessColor.Blank&&valuePad[i][j]>max){
max=valuePad[i][j];
position.setX(i);
position.setY(j);
}
}
}
/* return position;
*/ //遍历找出所有的最大权值点
List<Position> allMaxPosition=new ArrayList<>();
for(int i=0;i<size;i++){
for (int j=0;j<size;j++){
//判断该点的权值是否大于最大值,同时该点必须为空棋
if(colorPad[i][j]==ChessColor.Blank&&valuePad[i][j]==max){
allMaxPosition.add(new Position(i,j));
}
}
}
//在最大权值点列表种,随机取出一个点(随机取的原因是:遍历数组都是按固定顺序遍历的,若不采取随机取的方法,则玩家能够通过固定的套路取胜)
int s=allMaxPosition.size()-1;
return allMaxPosition.get((int)(Math.random()*s));
}
/**
* 为整个权值棋盘,赋值
*/
public static void getAllValue(){
//获取权值棋盘
int[][] valuePad = ChessPad.valuePad;
int size = ChessPad.size;
for (int i=0;i<size;i++){
for (int j = 0; j < size; j++) {
//将每一个点传入计算权值
Position position=new Position(i,j);
valuePad[i][j]=CalculateValue(position);
}
}
}
public static int getPositionDefendValue(Position p){
List<String> defendList = getPositionChessLink(p,"defend");
//计算权值
int defendValue=0;
for (String s : defendList) {
if(DefendMap.containsKey(s)){
defendValue+=DefendMap.get(s);
}
}
return defendValue;
}
/**
* 该方法实现计算传入点的权值
* p的坐标应该是行列坐标
* @return
*/
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<>();
// System.out.println("竖方向:");
//先从竖方向获取棋链
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; //计算竖方向的结束坐标
//此处需要判断开始的点和结束的点是否超出了棋盘
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); //将该棋链加入结果列表
}
}
// System.out.println("横方向:");
//计算横方向的棋链权值
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;
}
/**
* 该方法用于判断棋盘是不是都为空
* @return
*/
public static boolean isEmptyPad(){
for (ChessColor[] chessColors : ChessPad.colorPad) {
for (ChessColor chessColor : chessColors) {
if(chessColor!=ChessColor.Blank){
return false;
}
}
}
return true;
}
/**
* 判断游戏进行状态
*/
public static void Judge(){
//获取所有的棋链
List<String> allChessLink = getAllChessLink();
for (String s : allChessLink) {
if(s.equals("ooooo")){
JOptionPane.showMessageDialog(MainPanel.getInstance(),"白方获胜","游戏结束",JOptionPane.INFORMATION_MESSAGE);
new StopListener().actionPerformed(null); //完成重置工作
return;
}
else if(s.equals("*****")){
JOptionPane.showMessageDialog(MainPanel.getInstance(),"黑方获胜","游戏结束",JOptionPane.INFORMATION_MESSAGE);
new StopListener().actionPerformed(null); //完成重置工作
return;
}
}
//判断是否为平局
boolean flag=true;
for (int i=0;i<15;i++){
for (int j=0;j<15;j++){
flag=ChessPad.colorPad[i][j]==ChessColor.Blank;
}
}
if(!flag){
JOptionPane.showMessageDialog(MainPanel.getInstance(),"平局","游戏结束",JOptionPane.INFORMATION_MESSAGE);
new StopListener().actionPerformed(null); //完成重置工作
return;
}
}
public static List<String> getAllChessLink(){
//获取棋盘
ChessColor[][] colorPad = ChessPad.colorPad;
int size = ChessPad.size; //棋盘的大小
List<String> resultList=new ArrayList<>();
// System.out.println("竖方向:");
for (int k=0;k<15;k++){
for (int z=0;z<15;z++){
Position p=new Position(k,z);
//先从竖方向获取棋链
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; //计算竖方向的结束坐标
//此处需要判断开始的点和结束的点是否超出了棋盘
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); //将该棋链加入结果列表
}
}
// System.out.println("横方向:");
//计算横方向的棋链权值
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 两个方向的改变,从左下往右上的方向来计算 两个坐标的变化为 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);
}
}
}
}
return resultList;
}
}
ChessColor:该类里面有三个枚举值分别为:White、Black、Blank,用来表示棋盘上的颜色。
package Domain;
//棋盘颜色的枚举值
//一共三种棋,空白表示无棋,White表示白棋,Black表示黑棋
public enum ChessColor {
Blank,White,Black
}
ChessPad:整个棋盘的数据
package Domain;
/*
棋盘值
*/
public class ChessPad {
public static int size=15; //棋盘的规格
public static ChessColor[][] colorPad=new ChessColor[size][size]; //棋盘的颜色矩阵,用来记录哪些地方下了棋
public static int[][] valuePad=new int[size][size]; //棋盘的权值矩阵,用来记录每一个位置的权值
private static Boolean BlackTurn=null; //该值用来判断是否为黑棋的回合,黑棋永远先手,所以初值为true
public static String mode=""; //该值用来表示当前整个游戏的对战模式,初值为空串
//构造函数,完成程序启动时,第一次初始化棋盘的操作
public ChessPad(){
}
//回合转换
public static void ChangeTurn(){
BlackTurn=!BlackTurn;
}
/**
* 该函数用于判断当前是否为黑棋的回合
* @return
*/
public static Boolean isBlackTurn(){
return BlackTurn;
}
public static void setBlackTurn(boolean flag){
BlackTurn=flag;
}
public static void setBlackTurn(Boolean blackTurn) {
BlackTurn = blackTurn;
}
/**
* 请除棋盘上所有的颜色,将棋盘全部置为Blank
*/
public static void ClearColorPad(){
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
colorPad[i][j]=ChessColor.Blank;
}
}
}
/**
* 清除棋盘上所有的权值,权值设为-1
*/
public static void ClearValuePad(){
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
valuePad[i][j]=-1;
}
}
}
}
Position:表示棋子位置的类,实现以像素为单位的坐标与矩阵中行列坐标之间的转换
package Domain;
public class Position {
private int x;
private int y;
public Position() {
}
public Position(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
@Override
public String toString() {
return "Position{" +
"x=" + x +
", y=" + y +
'}';
}
public void setY(int y) {
this.y = y;
}
}
Sequence:记录下棋顺序的类
package Domain;
import java.util.ArrayList;
import java.util.List;
/**
* 该对象保存的是下棋的顺序,用于悔棋操作
* 使用List数据结构保存,按栈的先进先出方式对数据进行存取
*/
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<>();
}
}
Listener目录下代码:
JumpListener:跳过按钮的监督器
package Listiner;
import Domain.AI;
import Domain.ChessPad;
import Panel.ButtonPanel;
import Panel.ChessPadPanel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 跳过按钮的监听器,跳过操作的含义是,落子权的交接,所以点击该按钮后,更改当前回合即可
*/
public class JumpListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if(ChessPad.mode.equals("人人对战")){
ChessPad.ChangeTurn();
//更改提示信息
ButtonPanel.changeMsg();
}
else {
ChessPad.ChangeTurn();
ButtonPanel.changeMsg();
AI.AiPlay();
ChessPadPanel.getInstance().repaint();
}
}
}
ModelListener:模式选择按钮的监听器,该监听器作用在两个按钮上,分别为“人机对战” 和“人人对战”
package Listiner;
import Domain.AI;
import Domain.ChessPad;
import Panel.ButtonPanel;
import Panel.ChessPadPanel;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 模式监听器,作用在模式按钮上
*/
public class ModeListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String actionCommand = e.getActionCommand();
ChessPadPanel.getInstance().addMouseListener(ChessPadPanel.listener);
if(actionCommand.equals("人机对战")){
//选择的模式为人机对战,则将当前对战模式设为人机对战,同时禁用人人对战按钮
ChessPad.mode="人机对战";
//人机对战永远时机器人先手,所以在此处设置回合为机器人回合
ChessPad.setBlackTurn(true);
JButton pvp = ButtonPanel.getPVP();
pvp.setEnabled(false);
ButtonPanel.getPVE().setEnabled(false);
//清空棋盘
ChessPad.ClearColorPad();
ChessPad.ClearValuePad();
//一选择模式,就要完成人机下棋操作,并重绘棋盘
AI.AiPlay();
ChessPadPanel.getInstance().repaint();
}
else {
ChessPad.setBlackTurn(true);
ChessPad.mode="人人对战";
JButton pve = ButtonPanel.getPVE();
pve.setEnabled(false);
ButtonPanel.getPVP().setEnabled(false);
//清空棋盘
ChessPad.ClearColorPad();
ChessPad.ClearValuePad();
ChessPadPanel.getInstance().repaint();
}
ButtonPanel.ActiveButton();
}
}
PutChessListener:下棋监听器,该监听器是一个鼠标监听器,监听鼠标的点击动作,实现玩家的下棋操作。该监听器作用再棋盘容器(ChessPadPanel)上。
package Listiner;
import Domain.*;
import Panel.ChessPadPanel;
import Utils.PositionUtils;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
//鼠标监听器,该监听器作用对象是棋盘(ChessPadPanel),当棋盘对象上有鼠标点击且释放的事件时,执行该监听器,完成下棋动作
//由于有两个对战模式,所以需要
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) {
}
}
RegretListener:悔棋按钮监听器
package Listiner;
import Domain.ChessPad;
import Domain.Position;
import Domain.Sequence;
import Panel.ButtonPanel;
import Panel.ChessPadPanel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 悔棋按钮的操作
*/
public class RegretListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if(!(Sequence.positions.size()>=3)){
return;
}
//判断当前的模式,如果是人机对战的话,就将栈顶前两个棋子出栈,如果是人人对战模式,就出前一个
if(ChessPad.mode.equals("人机对战")){
Sequence.pop();
Sequence.pop();
}
else {
Sequence.pop();
ChessPad.ChangeTurn();
}
//安照栈重置颜色棋盘
ChessPad.ClearColorPad();
for (int i=0;i<Sequence.positions.size();i++){
ChessPad.colorPad[Sequence.positions.get(i).getX()][Sequence.positions.get(i).getY()]=Sequence.colorsSequence.get(i);
}
ButtonPanel.changeMsg();
ChessPadPanel.getInstance().repaint();
}
}
StopListener:结束按钮监听器
package Listiner;
import Domain.ChessPad;
import Domain.Sequence;
import Panel.ButtonPanel;
import Panel.ChessPadPanel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 结束按钮的监听器
*/
public class StopListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
//点击结束按钮以后,清空棋盘上所有的棋子
ChessPad.ClearColorPad();
ChessPad.ClearValuePad();
//清空上一局的下棋顺序
Sequence.ClearSequence();
//清空棋盘以后,重置按钮
ButtonPanel.ResetButton();
ChessPadPanel.getInstance().removeMouseListener(ChessPadPanel.listener);
ChessPad.mode="";
ChessPad.setBlackTurn(null);
//然后重新绘制棋盘
ChessPad.ClearColorPad();
ChessPad.ClearValuePad();
ChessPadPanel.getInstance().repaint();
}
}
Panel目录下代码:
ButtonPanel:按钮容器代码
package Panel;
import Domain.ChessPad;
import Listiner.JumpListener;
import Listiner.ModeListener;
import Listiner.RegretListener;
import Listiner.StopListener;
import javax.swing.*;
import java.awt.*;
//按钮容器
/*
本五子棋项目,实现的功能有,人机对战、人人对战、悔棋、结束游戏功能
*/
public class ButtonPanel extends JPanel{
private static Font font = new Font("黑体", Font.BOLD, 20); //按钮文本字体
private static JButton PVE=new JButton("人机对战"); //人机模式
private static JButton PVP=new JButton("人人对战"); //人人对战
private static JButton stop = new JButton("结束"); //结束按钮
private static JButton regret = new JButton("悔棋"); //悔棋按钮
private static JButton jump=new JButton("跳过"); //跳过按钮,该按钮的功能交换回合
private static JLabel msg=new JLabel("请选择对局模式");
//构造函数,初始化各个按钮和容器
public ButtonPanel(){
stop.setFont(font);
regret.setFont(font);
PVE.setFont(font);
PVP.setFont(font);
jump.setFont(font);
msg.setFont(font);
//将各个组件加入到容器内
this.add(msg);
this.add(PVP);
this.add(PVE);
this.add(stop);
this.add(jump);
this.add(regret);
ResetButton();
PVE.addActionListener(new ModeListener());
PVP.addActionListener(new ModeListener());
jump.addActionListener(new JumpListener());
stop.addActionListener(new StopListener());
regret.addActionListener(new RegretListener());
}
/**
* 该方法实现重置各个按钮的状态(即按钮是否能够被点击),在点击结束按钮,以及棋局结束时被调用
*
*/
public static void ResetButton(){
PVE.setEnabled(true);
PVP.setEnabled(true);
jump.setEnabled(false);
regret.setEnabled(false);
stop.setEnabled(false);
}
/**
* 该方法实现按钮的激活,在选择了对局模式后被调用
*/
public static void ActiveButton(){
jump.setEnabled(true);
regret.setEnabled(true);
stop.setEnabled(true);
}
public static JButton getStop() {
return stop;
}
public static void setStop(JButton stop) {
ButtonPanel.stop = stop;
}
public static JButton getRegret() {
return regret;
}
public static void setRegret(JButton regret) {
ButtonPanel.regret = regret;
}
public static JButton getPVE() {
return PVE;
}
public static void setPVE(JButton PVE) {
ButtonPanel.PVE = PVE;
}
public static JButton getPVP() {
return PVP;
}
public static void setPVP(JButton PVP) {
ButtonPanel.PVP = PVP;
}
public static JButton getJump() {
return jump;
}
public static void setJump(JButton jump) {
ButtonPanel.jump = jump;
}
public static JLabel getMsg() {
return msg;
}
public static void setMsg(JLabel msg) {
ButtonPanel.msg = msg;
}
public static void changeMsg(){
if(ChessPad.isBlackTurn()==null){
msg.setText("请选择对局模式");
}
else if(!ChessPad.isBlackTurn()){
msg.setText("白棋回合");
}
else {
msg.setText("黑棋回合");
}
}
}
ChessPadPanel:棋盘容器
package Panel;
import Domain.ChessColor;
import Domain.ChessPad;
import Domain.Position;
import Utils.PositionUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseListener;
/*
棋盘容器
*/
public class ChessPadPanel extends JPanel{
public static int gap=80; //棋盘与窗口的间隔
public static int size=40; //棋盘每一个的长宽
public static int radius=10; //旗子的半径
public static int row=15; //棋盘行数
public static int col=15; //棋盘列数
private static ChessPadPanel instance=new ChessPadPanel();
public static MouseListener listener;
//构造函数,初始化棋盘
public ChessPadPanel(){
this.setSize(800,600); //设置容器大小
this.setBackground(Color.GRAY); //设置容器背景颜色
}
public static ChessPadPanel getInstance() {
return instance;
}
@Override
public void paint(Graphics g) {
super.paint(g);
drawPad(g);
drawChess(g);
}
//绘制棋盘,传入画笔对象
public void drawPad(Graphics g){
//将模式显示在此处
g.setFont(new Font("宋体",Font.BOLD,20));
if (ChessPad.mode==""){
g.drawString("请先选择模式",30,30);
}
else{
g.drawString(ChessPad.mode,30,30);
}
ButtonPanel.changeMsg();
//绘制棋盘行
for(int i=0;i<row;i++){
g.drawLine(gap,gap+i*size,(col+1)*size,gap+i*size);
}
for(int i=0;i<col;i++){
g.drawLine(gap+i*size,gap,gap+i*size,(row+1)*size);
}
}
/**
* 该方法根据棋盘的颜色矩阵,在棋盘上画棋
*/
public void drawChess(Graphics g){
ChessColor[][] colorPad = ChessPad.colorPad;
//先画棋盘上的黑棋,为画笔设置颜色
g.setColor(Color.black);
//遍历颜色数组,若颜色值等于黑色,则将该点画在棋盘上
for (int i=0;i<row;i++){
for (int j=0;j<col;j++){
if(colorPad[i][j]==ChessColor.Black){
//获取该行列的 像素坐标
Position position=new Position(i,j);
Position y = PositionUtils.RowAndColChangeTOXAndY(position);
g.fillOval( y.getX()-radius, y.getY()-radius,radius*2,radius*2);
}
}
}
//遍历颜色数组,若颜色值等于白色,则将该点画在棋盘上,白起需要加边框
for (int i=0;i<row;i++){
for (int j=0;j<col;j++){
if(colorPad[i][j]==ChessColor.White){
//获取该行列的 像素坐标
Position position=new Position(i,j);
Position y = PositionUtils.RowAndColChangeTOXAndY(position);
g.setColor(Color.WHITE);
g.fillOval((int) y.getX()-radius,(int) y.getY()-radius,radius*2,radius*2); //将棋画在对应的坐标上
g.setColor(Color.BLACK);
g.drawOval((int) y.getX()-radius,(int) y.getY()-radius,radius*2,radius*2);
}
}
}
}
}
MainPanel:主容器
package Panel;
import Domain.ChessPad;
import Listiner.PutChessListener;
import javax.swing.*;
//主界面,父类是JSplitPane
public class MainPanel extends JSplitPane {
private static JSplitPane instance;
//主界面的构造函数,用于初始化界面
static {
ButtonPanel buttonPanel=new ButtonPanel();
ChessPadPanel chessPadPanel=ChessPadPanel.getInstance();
chessPadPanel.listener=new PutChessListener();
ChessPad.ClearColorPad(); //初始化棋盘
instance=new JSplitPane(JSplitPane.VERTICAL_SPLIT,chessPadPanel,buttonPanel);
instance.setDividerLocation(700);
instance.setEnabled(false);
}
public static JSplitPane getInstance(){
return instance;
}
}
Utils目录下代码:
PositionUtils:实现两类坐标之间的转换
package Utils;
import Domain.Position;
import Panel.ChessPadPanel;
import java.util.ArrayList;
import java.util.List;
/**
* 该类实现像素坐标和矩阵的行列坐标之间的转换
*/
public class PositionUtils {
/**
* 该方法实现将行列坐标转换为像素坐标 行代表的是y轴,列代表的是x轴
* @param p
* @return 一个Positon对象,该对象的x和y属性的值时像素坐标
*/
public static Position RowAndColChangeTOXAndY(Position p){
ChessPadPanel chessPadPanel = ChessPadPanel.getInstance();
int gap=chessPadPanel.gap;
int size=chessPadPanel.size;
//获取该行列坐标的横、纵的值
int row = p.getX(); //此处获取的是行,所以该值用来计算y的值
int col = p.getY(); //此处获取的是列,所以该值用来计算x的值
Position position=new Position();
position.setX(gap+col*size);
position.setY(gap+row*size);
return position;
}
/**
* 该方法实现将像素坐标,转换为行列坐标
* @param p
* @return 返回一个Position,该对象的x和y值是行列坐标
*/
public static Position XAndYChangeToRowAndCol(Position p){
//获取该点的像素坐标
int x = p.getX();
int y = p.getY();
//获取棋盘数据
int radius = ChessPadPanel.radius;
int row = ChessPadPanel.row;
int col = ChessPadPanel.col;
//计算出棋盘上所有交点的像素坐标
List<Position> positionList = getAllPositionXAndY();
//遍历该列表,计算每一个点与鼠标点击的点的距离,若距离小于棋子的半径,则说明该棋属于那一个点
int count=-1; //记录第几个点
for (Position position : positionList) {
double distance = Math.sqrt(Math.pow((position.getX() - x), 2) + Math.pow((position.getY() - y), 2)); //两像素点之间的距离
count++;
if(distance<=radius){
break;
}
}
//若像素点在两个焦点的中间段,即两个棋子之间的空白段,或无效位置,则上述遍历无法计算出满足距离小于棋子半径的点,所以会完成整个列表的遍历计算,因此需要对最后一各交点也就是第224个点进行单独的判断
if (count==224){
//判断传入的像素坐标距离最后一个点是否距离小于半径
//获取最后一个位置
Position position = positionList.get(positionList.size()-1);
double distance = Math.sqrt(Math.pow((position.getX() - x), 2) + Math.pow((position.getY() - y), 2)); //两像素点之间的距离
if (!(distance<=radius)){
return null;
}
}
//通过计数的到的点编号,计算出行列坐标
Position XAndY=new Position(count/row,count%col);
return XAndY;
}
/**
* 该方法用于获取棋盘上所有交点的像素坐标
* @return
*/
public static List<Position> getAllPositionXAndY(){
List<Position> positionList=new ArrayList<>();
//获取棋盘数据
int gap = ChessPadPanel.gap;
int size = ChessPadPanel.size;
int row = ChessPadPanel.row;
int col = ChessPadPanel.col;
for(int i=0;i<row;i++){
for (int j=0;j<col;j++){
int x=gap+i*size;
int y=gap+j*size;
positionList.add(new Position(y,x));
}
}
return positionList;
}
//判断该店是否为棋盘的边缘 p为行列坐标
public static boolean isBound(Position p){
return p.getX()>=0&&p.getX()<15;
}
}
Test目录下代码:
PanelTest:系统的运行类(主函数在该类中定义)
package Test;
import Panel.MainPanel;
import javax.swing.*;
public class PanelTest {
public static void main(String[] args) {
JFrame btn=new JFrame();
btn.add(MainPanel.getInstance());
btn.setTitle("计科*** 郑** 193050**** 五子棋");
btn.setSize(800,800);
btn.setResizable(false);
btn.setVisible(true);
}
}