Java 项目之五子棋对战
用java写一个五子棋对战游戏,逐渐完善功能。
文章目录
一、开发思路
-
1.用户可以注册登录-登录界面的开发
-
2.登录完成以后进入主菜单页面-用户可以选择人人对战,人机对战,机机对战等,可以退出登录,退出游戏。
-
3.下棋界面-棋盘绘制,棋子落点,界面显示下棋信息,悔棋,存档,读档,棋局录制等功能的实现。
-
4.五子棋规则:
一黑一白交替轮流下棋可以决定哪个玩家先手
不可以重复下棋到同一个位置
不可以将棋子下到边界外
可以悔棋
一方横竖斜到达5个棋子为赢
注:此规则部分来自一位大佬的文章五子棋项目
二、开发实现
界面开发
目前主要有三个界面:登录注册界面,游戏主界面,对战界面。
1 登录注册界面
本界面继承JFrame,通过Jframe来实现添加组件功能。
主要模块:
1.1 窗体的创建-显示登录界面的窗体
代码如下:
public void initLoginUI() {
this.setTitle("Welcome");//设置login界面title
this.setLocationRelativeTo(null);
this.setLocation(500,200);//设置login界面位置
this.setSize(500, 500);//设置login界面大小
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置点击关闭按钮结束程序
BorderLayout border=new BorderLayout();
this.setLayout(border);//布局为边框布局
this.setResizable(false);//不可以resize登录界面大小
}
因为没有固定文本框和按钮的位置,如果窗体改变文本框和按钮会随之改变,所以用this.setResizable(false)固定login界面大小。
1.2 背景图片添加-登录界面添加一个背景图片美化界面
通过JPanel组件在登录界面上半部分添加一张图片。
代码如下:
JPanel ImagePanel= new JPanel();
Dimension ImagepanelSize=new Dimension(100,this.getHeight()/2);
ImagePanel.setPreferredSize(ImagepanelSize);
Image img = bgimage.getImage();// 获得此图标的Image
img = img.getScaledInstance(500, this.getHeight()/2, Image.SCALE_AREA_AVERAGING);// 将image压缩后得到压缩后的img
bgimage.setImage(img);// 将图标设置为压缩后的图像
JLabel iconLabel = new JLabel(bgimage);
ImagePanel.add(iconLabel);
将ImagePanel添加到窗体上。
1.3 文本输入框和按钮的创建
1.3.1 创建一个JTextField类型的的用户账号输入框和JPasswordField类型的用户密码输入框,通过loginTextPanel添加到窗体上。
代码如下:
JPanel loginTextPanel= new JPanel();
Dimension TextpanelSize=new Dimension(100,this.getHeight()/4);
loginTextPanel.setPreferredSize(TextpanelSize);
//创建用户名文本框
JTextField username = new JTextField();
Dimension username_text = new Dimension(300, 50);
username.setPreferredSize(username_text);
loginTextPanel.add(username);
//创建密码文本框
JPasswordField password = new JPasswordField();
Dimension password_text = new Dimension(300, 50);
password.setPreferredSize(password_text);
loginTextPanel.add(password);
1.3.2 创建登录注册按钮
目前暂时没有与数据库连接,所以是一个单机版的五子棋项目,暂时不添加注册按钮,直接将账号密码写在程序内部,通过登录按钮去验证。
代码如下:
JButton login_button = new JButton("登录");
login_button.setPreferredSize(new Dimension(90, 50));
Color fg = new Color(0, 173, 232);
login_button.setForeground(fg);
loginbutPanel.add(login_button);
1.4 窗体布局
将图片,文本框,按钮等组件添加到窗体上
代码如下:
this.add(ImagePanel,BorderLayout.NORTH);
this.add(loginTextPanel,BorderLayout.CENTER);
this.add(loginbutPanel,BorderLayout.SOUTH);
1.5 添加监听器
给文本框,按钮添加监听器,操作鼠标时执行对应的操作。
创建一个StartLister类型的loginListener监听器。StartLister继承了GoBangLister,GoBangLister后面会在对战界面部分提到,该类实现了MouseListener, MouseMotionListener, GoabngConfig, ActionListener等接口。
loginListener类中有setusername和setpassword方法,用来绑定用户输入的信息,与数据库中的用户信息去进行比较。
loginListener类中还重写了actionPerformed方法,点击会获取用户输入的信息,并验证登录,返回登录结果。
代码如下:
public class LoginLister extends GoBangLister {
//创建用户名和密码属性
private JTextField username;
private JPasswordField password;
private static final String default_name="admin";
private static final String default_password="123456";
public LoginUI loginUI;
public void setLoginUI(LoginUI loginUI) {
this.loginUI = loginUI;
}
//创建set方法
public void setUsername(JTextField username){
this.username=username;
}
public void setPassword(JPasswordField password){
this.password = password;
}
//处理方法
public void actionPerformed(ActionEvent e){
String name=username.getText();//获取用户输入得信息
String password1 = String.valueOf(password.getPassword());
resultWindow result=new resultWindow();//创建一个显示结果的窗体
if (default_name.equals(name) && default_password.equals(password1)){//登陆成功
System.out.println("登陆成功");
loginUI.setVisible(false);//登录界面隐藏
StartUI startUI=new StartUI();//创建一个开始界面的窗体并初始化
startUI.initStratUI();
}else if(default_name.equals(name)){
result.loginresult("密码不正确");//密码不正确
}else{
result.loginresult("用户名不存在");//用户名不存在
}
}
}
登录信息不正确会创建一个新的窗体显示结果:
登陆成功会直接进入开始界面。
最终的的登录界面效果:
2 游戏主界面
本界面继承JFrame,通过Jframe来实现添加组件功能。创建窗体,添加背景图片,添加需要实现的按钮功能代码与login界面类似,不在放置重复代码。
StartUI需要添加的监听器为StartListener,这个监听器同样的集继承了GoBangLister,使用继承的好处是在下棋界面设置的一些参数比如棋子位置存放信息矩阵,已经下了的棋子信息数组可以在这个界面调用,可以这个界面实现存档读档等功能。此项目将读档功能放在了下棋界面。
StartListener代码如下:
public class StartLister extends GoBangLister {
public void actionPerformed(ActionEvent e) {
String operation = e.getActionCommand();//获取点击的按钮信息
if (operation.equals("双人对战")) {// 人人对战
GongbangUI gobang=new GongbangUI();//创建一个下棋界面并初始化
gobang.initGoBangWindow();
this.getStartUI().setVisible(false);
}else if (operation.equals("人机对战")) {// 人机器对战
GongbangUI gobang=new GongbangUI();
gobang.initGoBangWindow();
this.getStartUI().setVisible(false);
}else if(operation.equals("关闭声音")) { //关闭当前正在播放的背景音乐声音
System.out.println("111");
}else if (operation.equals("退出")) {//退出游戏,将当前页面设置为不可见
this.getStartUI().setVisible(false);
}else if (operation.equals("退出登录")) {//退出登录,将当前页面设置为不可见,新建一个login对象并初始化
this.getStartUI().setVisible(false);
LoginUI login=new LoginUI();
login.initLoginUI();
}
}
}
最终的的游戏主界面界面效果:
3 对战界面
本界面继承JFrame,通过Jframe来实现添加组件功能。创建窗体,创建窗体,添加背景图片,添加需要实现的按钮功能代码与login界面类似,不在放置重复代码。
GongbangUI对象需要添加的监听器为GongbangListener。
GongbangUI类里面要重写一个paint方法,使用户在刷新窗体时会重新绘制棋盘信息和棋子信息。
重写的paint代码如下:
public void paint(Graphics g) {
super.paint(g);
// 绘制背景图
g.drawImage(bgimage,X-SIZE,Y-SIZE,ROW*SIZE+2*SIZE,COLUM*SIZE+2*SIZE,null);
// 绘制棋盘
for (int i=0;i<=15;i++){
g.drawLine(X,Y+i*SIZE,X+ROW*SIZE,Y+i*SIZE);
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+COLUM*SIZE);
}
//新建一个QIZI类,通过GoBangLister的getchessinfor方法得到当前已经下了棋的棋子位置信息然后重新绘画
QIZI[] location=gl.getchessinfor();
for (int i=0;i<location.length;i++){
if(location[i]!=null){
g.setColor(location[i].c);
g.fillOval(location[i].x,location[i].y,SIZE,SIZE);
}
}
}
其中bgimage为棋盘的背景图片,SIZE为棋盘的行列间距,X,Y为棋盘线的初始位置,QIZI类为自定义的类,这些信息第二部分会详细介绍,这一部分只介绍界面的创建。
最终的的游戏主界面界面效果:
下棋功能实现
1 初始化棋盘信息
因为棋盘行列间距,棋盘行数列数在游戏中是固定的值,在绘画棋盘和计算棋子坐标时需要用到这些信息,所以将这些信息写在GoabngConfig这个接口里,绘制棋盘的GongbangUI对象直接继承GoabngConfig接口即可获取这些信息。
GoabngConfig代码如下():
public interface GoabngConfig {
/**
* 棋盘行间距与列间距, 也是旗子的直径
*/
int SIZE=50;
/**
* 棋盘左上角坐标的x值
*/
int X=100;
/**
* 棋盘左上角坐标的y值
*/
int Y=100;
/**
* 棋盘的行列值
*/
int ROW=15;
int COLUM=15;
}
定义完基本信息以后就可以绘制背景图图,然后在背景图片上绘画棋盘,这部分代码写在GongbangUI对象的重写paint方法中,因为需要在刷新时重绘棋盘信息。
public void paint(Graphics g) {
super.paint(g);
// 绘制背景图
g.drawImage(bgimage,X-SIZE,Y-SIZE,ROW*SIZE+2*SIZE,COLUM*SIZE+2*SIZE,null);
// 绘制棋盘
for (int i=0;i<=15;i++){
g.drawLine(X,Y+i*SIZE,X+ROW*SIZE,Y+i*SIZE);
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+COLUM*SIZE);
}
注意要用super调用父类的JFrame中的paint方法,然后在GongbangUI类中写上新添加的绘制棋盘的代码。
绘制后的棋盘样式如下:
2 绘制棋子,确定棋子落点
棋子可以通过g.fillOval(x, y , width, height)来绘制,其中x,y为棋子的落点位置,既棋盘中的交点处,width和height在这个项目中就是SIZE的值。
在开始界面中点击了对战模式后会选择先手白棋还是黑旗,对应不同的firstcolor值,如果firstcolor=1,白棋先行,如果firstcolor=0,黑棋先行。
1.下面要实现黑白交替落子,设置一个int step=0,每次下完一颗棋子step的值加一。首先通过firstcolor判断是黑棋先手还是白棋先手,如果是黑棋先手,则step % 2 == 0时画笔获取黑色,step % 2==1时获取白色,然后去绘制棋子。
具体代码为:
if (step % 2 == 0) {
//设置颜色
g.setColor(Color.BLACK);
// 将棋子状态的二维数组中的值由0变为1,代表当前为黑旗
chessLocationArray[tempx][tempy] = 1;
//绘画棋子
g.fillOval(chessX - SIZE / 2, chessY - SIZE / 2, SIZE, SIZE);
}else{
g.setColor(Color.WHITE);
chessLocationArray[tempx][tempy] = -1;
g.fillOval(chessX - SIZE / 2, chessY - SIZE / 2, SIZE, SIZE);
如果为白棋先手则判断条件相反。
2.当玩家点击某一点时,首先判断该点坐标是不是在棋盘范围内,通过下面的代码来进行判断:
int x = e.getX();
int y = e.getY();
if (x > X && x < X + SIZE * ROW && y > Y && y < Y + SIZE * COLUM)
判断完边界以后,将x,y的坐标转化为可以落子的交点坐标,可以发现棋盘交点横纵坐标全部是SIZE的倍数,所以用获取到的当前点击坐标x,y对SIZE进行取余操作。
如果x%SIZE的值小于SIZE/2,代表这个点的横坐标应该是当前获取坐标x的前面一个SIZE的倍数,下棋位置的横坐标chessx应该等于x - (x%SIZE)。
如果x%SIZE的值大于SIZE/2,代表这个点的横坐标应该是当前获取坐标x的后面一个SIZE的倍数,下棋位置的横坐标chessx应该等于x + (SIZE - (x%SIZE))。对纵坐标Y同理。
举个🌰:如果说当前用户点击的坐标为(235,304),前面设置的SIZE的值为50。对于横坐标x而言,235%50=35,SIZE的/2=25,35大于25,所以棋子的落点横坐标应该为点击横坐标的下一个SIZE倍数,既落子横坐标chessX=x+ (SIZE - (x%SIZE))=235+(50-(235%50))=250。对纵坐标y而言304%50=4,SIZE的/2=25,4小于25,所以棋子的落点纵坐标应该为点击横坐标的前一个SIZE倍数,既落子纵坐标chessY=y - (y%SIZE)=304-(304%50)=300,既如果用户点击坐标(235,304),妻子的实际落点坐标应该是棋盘交点(250,300)。
具体的转换代码为:
public static int transformLocationX(int x) {
int tempx = x % SIZE;
if (tempx < (SIZE / 2)) {
return (x - tempx);
} else {
return ( x + (SIZE - tempx));
}
}
3.将用户点击的坐标转换为交点可落子坐标后就可以在交点处落子了,下面要解决的问题是已经落子的位置不能再下其他棋子。解决办法是创建一个二维数组chessLocationArray,长宽为row和column的值加1,因为有row+1条线。初始值全部为0,如果当前交点下了颗白色棋子,将对应的二维数组中的0改为-1,如果当前交点下了颗黑色棋子,将对应的二维数组中的0改为1,这样在每次落子的时候可以加一个判断,如果二维数组中对应的值为0才可以绘画棋子,否则已经有棋子不可以进行绘画。
注意坐标的x在数组中代表列值,坐标的y在数组中代表行值。
创建二位数组的代码:
private int[][] chessLocationArray = new int[ROW + 1][COLUM + 1];
判断当前位置是否有棋子的代码:
if (chessLocationArray[tempx][tempy] == 0)
所以绘制棋子之前要先判断点击的点是不是在边界内,然后判断当前位置是否有棋子落下。
至此已经初步完成了在棋盘上绘制棋子的功能。
3 判断输赢
可以进行下棋功能以后就可以判断输赢了,判断输赢通过二维数组chessLocationArray去进行判断,因为在下棋的过程中chessLocationArray中的数值已经被修改,如果在横向或者竖向或者左下到右上的斜向或者右下到左上的斜向有相同的5个1(黑棋)或者5个-1(白棋)既有一方已经获胜,所以在每次下完一步棋之后都要进行判断,当前位置的几个方向是否有5子连在一起。
用for循环去判断每个方向是否有5个相连的棋子,具体代码为:
public static boolean checkRow(int x, int y, int[][] array) {
int count = 0;
for (int i = y+1; i < array[0].length; i++) {
if (array[x][y] == array[x][i]) {
count++;
} else {
break;
}
}
for (int i = y; i>=0; i--) {
if (array[x][y] == array[x][i]) {
count++;
} else {
break;
}
}
return count >= 5;
}
其他方向同理,每次落子都要判断四个大方向,其余代码见最后的完整版代码。
4 悔棋
该游戏允许玩家进行悔棋操作,悔棋的思路是创建一个棋子类,该类有属性x,y和c,x代表一颗棋子的横坐标,y代表一颗棋子的纵坐标,c代表一颗棋子的颜色信息。
public class QIZI implements GoabngConfig {
public int x;
public int y;
public Color c;
/**
* GoBnagv2.QIZI 实体构造器,传入时需要三个参数x,y,c并设置给QIZI
* @param x 当前棋子传入的坐标的x值
* @param y 当前棋子传入的坐标的y值
* @param c 传入的坐标的颜色信息,当前棋子是白棋还是黑棋
*/
public QIZI(int x,int y,Color c){
this.x=x;
this.y=y;
this.c=c;
}
}
创建完棋子类以后就可以创建一个棋子类数组,在每次落子的时候创建一个新的棋子对象,记录完信息以后存放在棋子数组中。
创建存放棋子信息的棋子数组:
private final QIZI[] chessinfor = new QIZI[(ROW + 1) * (COLUM + 1) + 1];
每次绘画棋子的时候创建一个棋子对象,然后调用qizi.drawChess()去绘画棋子,并将棋子添加进数组中
QIZI qizi = new QIZI(chessX - SIZE / 2, chessY - SIZE / 2, Color.BLACK);
qizi.drawChess(g);
chessinfor[step] = qizi;
绘画棋子的代码:
public void drawChess(Graphics g){
if (c.equals(Color.black)){
g.setColor(c);
g.fillOval(x, y, SIZE, SIZE);
}if (c.equals(Color.white)){
g.setColor(c);
g.fillOval(x, y, SIZE, SIZE);
}
}
如果记录了下棋的每一步的棋子信息,当用户点击悔棋按钮的时候,就可以将chessinfor数组中最后一个棋子的信息删除,然后step值减少1,然后重新绘制chessinfor中的所有棋子,注意要将画笔颜色变为黑色,不然如果最后一步棋是白色棋盘的格子会被绘制成白色。悔棋完成还要更新信息板上的步数信息,步数信息会在后面介绍。
4 重新开始
如果玩家选择了重新开始,则会重新开始一局游戏,重置chessLocationArray二维数组,重置chessinfor数组,重置step,然后调用paint画出棋盘。
state = 1;
//重置矩阵
for (int i = 0; i < chessLocationArray.length; i++) {
for (int j = 0; j < chessLocationArray[0].length; j++) {
chessLocationArray[i][j] = 0;
}
}
// 重置chessinfor数组
Arrays.fill(chessinfor, null);
step = 0;
g.setColor(Color.BLACK);
gongbangUI.paint(g);
5 存档
该游戏允许玩家玩到一半的时候进行存档记录某一时刻的棋盘信息,后期可以通过读档继续游戏。
创建一个新的二维数组savechessLocationArray,大小和chessLocationArray一样。
创建一个新的棋子数组savechessinfor,大小和chessinfor一样。
创建一个savestep。
当玩家点击存档按钮的时候,将chessinfor,chessLocationArray,step的数值copy给savechessinfor,savechessLocationArray和savestep。这样就记录了某一时刻的棋局信息。
else if (operation.equals("存档")) {
//copy chessinfor
System.arraycopy(chessinfor, 0, savechessinfor, 0, (ROW + 1) * (COLUM + 1) + 1);
//copy chessLocationArray
for (int i = 0; i < chessLocationArray.length; i++) {
System.arraycopy(chessLocationArray[i], 0, savechessLocationArray[i], 0, chessLocationArray[i].length);
}
avestep = step;
6 读档
当玩家点击读档按钮的时候,将savechessinfor,savechessLocationArray和savestep的数值copy给chessinfor,chessLocationArray,step。这样就可以读取某一时刻的棋局信息,然后重新绘棋子棋盘。
for (int i = 0; i < savechessLocationArray.length; i++) {
System.arraycopy(savechessLocationArray[i], 0, chessLocationArray[i], 0, chessLocationArray[i].length);
}
step = savestep;
System.arraycopy(savechessinfor, 0, chessinfor, 0, (ROW + 1) * (COLUM + 1) + 1);
g.setColor(Color.BLACK);
gongbangUI.paint(g);
7 其他辅助功能
设置一个state变量,控制是否可以下棋,初始设置为0,如果玩家点击了开始游戏,将state设置为1,只有当state=1的时候才可以在棋盘上绘制棋子和悔棋。如果游戏结束,state变为0,这就意味着如果有人获胜了就不可以在绘制棋子也不可以进行悔棋。
8 对局信息
玩家在下棋的过程在右边可以显示什么颜色的棋子走了多少步,不重要的小功能直接看代码helpfunction,写的有点墨迹,需要完善,有机会进行修改。
9 完整代码
完整代码放在了github
总结
这个项目还有录制棋局,播放录制棋局,软件日志,人机对战等部分没有完成,后续会逐步完成。写文档实在是痛苦,以后争取做到敲完一部分功能的代码就写一部分文档。