中国象棋是起源于中国的一种棋戏,属于二人对抗性游戏的一种,在中国有着悠久的历史。由于规则简单,趣味性强,成为流行极为广泛的棋类游戏。 中国象棋使用方形格状棋盘及红黑二色圆形棋子进行对弈,棋盘上有十条横线、九条竖线共分成90个交叉点;中国象棋的棋子共有32个,每种颜色16个棋子,分为7个兵种,摆放和活动在交叉点上。双方交替行棋,先把对方的将(帅)“将死”的一方获胜。
本篇博文开发了一个基于UDP通信的中国象棋联机版游戏,游戏符合传统中国象棋规则(包括老将见面判法),具备基本的联机和悔棋功能,悔棋需要得到对方同意。运行效果如下:
素材及完整源码链接:https://pan.baidu.com/s/1m-z1eDGOabN4iLx5VEcZMw 提取码: rpbr
棋子活动的场所叫做“棋盘”,在长方形的平面上,绘有9条平行的竖线和10条平行的横线相交组成,共90个交叉点,棋子摆在这些交叉点上。中间第5根横线和第6根横线之间未画竖线的空白地带,叫做“楚河 汉界”,整个棋盘就以“楚河 汉界”分为相等的两部分;两方将帅坐阵,画有“米”字方格的地方叫做“九宫”。
象棋的棋子共32个,分为红黑两组,各16个,由对弈双方各执一组,每组兵种是一样的,各分为七种。
红方:帅、仕、相、车、马、炮、兵;
黑方:将、士、象、车、马、炮、卒。
其中帅和将、士和仕、相和象、兵和卒的作用完全相同,仅仅是为了区分红棋和黑棋。
①将或帅
移动范围:它只能在九宫内移动
移动规则:它每一步只可以水平或垂直移动一点
特殊说明:红方帅和黑方将不能直接见面,即帅和将中间必须有其他棋子间隔,否则构成“老将见面”情况;
假如现在是红方执子,此时构成“老将见面”状况,那么玩家可以移动帅直接吃掉对方的将,红方获胜。
②士或仕
移动范围:它只能在九宫内移动
移动规则:它每一步只能沿对角线方向移动一点
③象或相
移动范围:“楚河 汉界”的一侧
移动规则:它每一步只能沿对角线走两点(走田字),另外,移动的对角线方向上一点不能有其他棋子(不能被堵象眼)。
④马
移动范围:任何位置
移动规则:它每一步只能走曰字并且不能被绊马脚(斜着走,横位移量*纵位移量等于2)。
⑤车
移动范围:任何位置
移动规则:它可以水平或垂直方向移动任意个无阻碍的点。
⑥炮
移动范围:任何位置
移动规则:移动跟车很相似,它既可以像车一样水平或垂直方向移动任意无阻碍的点,也可以通过隔一个棋子吃掉对方一个棋子的方式进行移动。
⑦卒或兵
移动范围:过河后可以走过河后的一侧的任何位置,未过河只能走该棋子向上方向的直线区域
移动规则:它每步只能移动一个点,它未过河时只能向前移动,如果它过河了,增加向左向右移动的能力。
对局中,如果一方玩家认输或者该玩家的“将”或“帅”被对方棋子吃掉,该玩家算输,对方算赢。
这里涉及到的UDP通信基础可以参考我之前写的一篇博文:https://blog.csdn.net/A1344714150/article/details/85495088
前面说过棋盘是有10条横线和9条竖线组成,一共有90个交叉点,棋子必须放置在这些交叉点上。这里可以使用二维数组对棋盘信息进行存储,每个交叉点存储棋子下标索引,如果交叉点上没有棋子存储-1表示它上面没有棋子。为了更容易地计算坐标,这里直接按照二维数组的分布对棋盘进行分割,声明10行9列数组map,行数从0开始到9为止,列数从0开始到8为止;例如,对方第二个兵或卒位于棋盘第3行第2列,所以map[3][2]存储的是对方第二个兵或卒的棋子的下标索引值;
棋子设计成对应的类,每种棋子都有自己对应的棋子图案;
中国象棋一共有32个棋子,这里可以将32个棋子对象数组存储到一个棋子对象数组chess。chess[i]中下标i的值如果是0~15表示黑方棋子,16~31表示的是红方棋子;
具体含义如下:
0将 1~2仕 3~4象 5~6马 7~8车 9~10炮 11~15卒;
16帅 17~18士 19~20相 21~22马 23~24车 25~26炮 27~31兵;
棋子初始化时根据玩家执子颜色和棋子上的字获取指定棋子图案,根据x,y的值,将棋子的位置信息初始化到第x行第y列;
游戏开始时根据双方执子颜色初始化棋盘信息(红方和黑方棋子放置位置会不同);
游戏保证了执子方一定处于棋盘下方,判棋规则目前都是基于我方下棋去判断的,如果我方下棋符合下棋规则,先将下棋信息进行坐标转换,然后发送给对方;接收到对方的下棋信息时,直接按照对方下棋信息对棋盘和棋子信息进行变更即可(发送之前坐标已经转换,无需再次转换)。
对于中国象棋来说,有马走日、象走田等一系列规则。
根据不同的棋子,按不同的规则进行走法判断。
判断是否能走棋的算法如下:
①如果棋子为“帅”或“将”,检查是否走直线并且走一步,以及走一步是否超出范围(老将碰面情况需要提前特殊处理)
②如果棋子为“仕”或“士”,检查是否沿斜对角线走一步,以及走一步是否超过范围
③如果棋子为“象”或“象”,检查是否走“田”字,是否被堵象眼,走一步是否超出范围
④如果棋子为“马”,检查是否走“日”字,是否被绊马脚
⑤如果棋子为“车”,检查是否走直线,移动前的位置和移动后的位置中间是否还有其他子
⑥如果棋子为“炮”,检查是否走直线,判断是否吃子,如果吃子判断中间是否只有一个棋子,否则判断中间是否还有其他子
⑦如果棋子为“卒”或“兵”,检查是否走直线,走一步;如果棋子没有过河,判断棋子是否向前走;如果棋子已经过河,判断是否向前、左、右移动
走棋过程中,需要将鼠标点击的像素坐标转换成棋盘坐标,用到analyse方法;
根据鼠标点击的像素坐标去和每个交叉点的小矩形进行匹配,如果该坐标位于矩形内,说明鼠标点击的是该交叉点。
假设点击的交叉点转换成棋盘坐标是(x,y),如果map[x][y]没有棋子索引,返回空,否则返回该棋子索引对应的棋子对象。
联机版程序的难度在于对方需要通信,这里使用UDP通信;一方玩家输入对方IP和对方端口,点击开始向对方发送联机请求;
发送的通信信息包括以下功能:
①请求联机
格式:join|
②联机成功
格式:conn|
③认输
格式:lose|
④一方退出游戏
格式:quit|
⑤对方棋子移动信息
格式:move|对方移动的棋子下标|移动后所在行数|移动后所在列数|移动前所在行数|移动前所在列数|被吃掉的棋子索引|
如果该步没有吃掉棋子,被吃掉的棋子索引存储信息为-1
⑥请求悔棋
格式:ask|
⑦同意悔棋
格式:agree|
⑧拒绝悔棋
格式:refuse|
⑨游戏结束
格式:succ|黑方赢了 或者 succ|红方赢了
自定义数据结构Node类,包含 移动的棋子信息、移动后所在行数、移动后所在列数、移动前所在行数、移动前所在列数、该步棋吃掉了的棋子的索引值;主要作用是为了实现悔棋功能,当然也可以对此进行拓展,完成棋谱记录及按棋谱还原棋局的功能。
棋子类的成员信息主要包含 棋子所属玩家、棋子类别、棋子所在行数、棋子所在列数、棋子图案的信息;
方法主要包括:
setPos(int x,int y):将棋子放在第x行第y列
ReversePos():将棋子位置进行对调
paint(Graphics g,JPanel i):在指定的JPanel上画棋子
DrawSelectedChess(Graphics g):给选中的棋子画选中框
package 中国象棋;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.ImageObserver;
import javax.swing.JPanel;
public class Chess {
public static final short REDPLAYER = 1;
public static final short BLACKPLAYER = 0;
public short player;
public String typeName;
public int x,y;//网格地图对应的二维数组的下标
private Image chessImage;//棋子图案
private int leftX=28,leftY=20;
public Chess(short player,String typeName,int x,int y){
this.player = player;
this.typeName = typeName;
this.x = x;
this.y = y;
if(player == REDPLAYER){
switch (typeName){
case "帅":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess7.png");
break;
case "仕":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess8.png");
break;
case "相":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess9.png");
break;
case "马":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess10.png");
break;
case "车":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess11.png");
break;
case "炮":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess12.png");
break;
case "兵":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess13.png");
break;
}
}else{
switch(typeName){
case "将":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess0.png");
break;
case "士":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess1.png");
break;
case "象":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess2.png");
break;
case "马":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess3.png");
break;
case "车":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess4.png");
break;
case "炮":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess5.png");
break;
case "卒":
chessImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chess6.png");
break;
}
}
}
public void setPos(int x,int y){
this.x = x;
this.y = y;
}
public void ReversePos(){
x = 9 - x;
y = 8 - y;
}
protected void paint(Graphics g,JPanel i){
g.drawImage(chessImage, leftX+y*62, leftY+x*57, 40, 40,(ImageObserver)i);
}
//绘画选中框
public void DrawSelectedChess(Graphics g){
g.drawRect(leftX+y*62, leftY+x*57, 40, 40);
}
}
Node.java成员包括:移动的棋子下标index、棋子移动后位于(x,y)、棋子移动前位于(oldX,oldY)、被吃掉的棋子下标eatChessIndex;
package 中国象棋;
//存储棋谱的每一步
public class Node {
int index;//移动的棋子下标
int x,y;//棋子移动后位于(x,y)
int oldX,oldY;//棋子移动前位于(oldX,oldY)
int eatChessIndex;//被吃掉的棋子下标
//如果棋子移动过程没有吃子,eatChessIndex = -1;
public Node(int index,int x,int y,int oldX,int oldY,int eatChessIndex){
this.index = index;
this.x = x;
this.y = y;
this.oldX = oldX;
this.oldY = oldY;
this.eatChessIndex = eatChessIndex;
}
}
棋盘类是游戏面板,先定义一个数组chess存放双方32个棋子对象。二维数组map保存了当前棋盘的棋子布局,当map[x][y]=i时说明棋盘第x行第y列是棋子i,否则-1说明此处为空;声明了ArrayList
public static final short REDPLAYER = 1;
public static final short BLACKPLAYER = 0;
public Chess[] chess = new Chess[32];//棋子数组
public int[][] map = new int[10][9];//存储棋盘布局信息数组10行9列
public Image bufferImage;
private Chess firstChess = null;
private Chess secondChess = null;
private boolean isFirstClick = true;//标记是否第一次点击
private int x1,y1,x2,y2;
private int tempX,tempY;
private boolean isMyTurn = true;//标记是否自己执子
public short LocalPlayer = REDPLAYER;//记录当前执子方
private String message = "";//提示信息
private boolean flag = false;
private int otherPort=3003;//对方端口
private int receivePort=3004;//本地端口
public ArrayList list = new ArrayList();//存储棋谱
private String ip = "127.0.0.1";//存储目标IP
对存储当前棋盘布局信息的二维数组map进行初始化,由于棋子索引从0开始到31,所以全部初始化为-1(表示没有棋子)
//初始化棋盘布局信息为空
private void initMap(){
int i,j;
for(i=0;i<10;i++){
for(j=0;j<9;j++){
map[i][j]= -1;
}
}
}
棋盘构造方法主要先对棋盘信息进行初始化,接着为棋盘添加鼠标监听;
监听事件先判断是否是自己执子,如果是自己执子,再判断自己是第几次点击;
⑴如果是第一次点击(isFirstClick为true):
根据点击处的像素坐标转换成自己点击的棋盘坐标,将该棋盘坐标上的Chess对象赋值给firstChess,并用x1和y1对棋盘坐标进行记录;
如果自己选中了棋子(firstChess不为空),判断是否选中的是对方的棋子,
①如果是提示“点击成对方棋子了”,
②如果否,将isFirstClick改为false
⑵如果是第二次点击(isFirstClick为false):
根据点击处的像素坐标转换成自己点击的棋盘坐标,将该棋盘坐标上的Chess对象赋值给secondChess,并用x2和y2对棋盘坐标进行记录;
接着,判断第二次点击的棋子
①如果第二次选中的棋子是自己的,将该棋子对方赋值给firstChess(重新选中棋子),return ;
②如果第二次没有选中任何棋子,判断是否可以走棋
⒈如果isAbleToMove(firstChess,x2,y2)返回true,说明可以走棋,对棋盘信息和棋子信息进行变更,记录棋谱信息,并发送倒置后的下棋信息给对方,重置isFirstClick为true,将isMyTurn改为false
⒉否则说明不符合走棋规则,修改提示信息为“不符合走棋规则”
③如果第二次选中的棋子是对面的,判断是否可以走棋
⒈如果isAbleToMove(firstChess,x2,y2)返回true,说明可以走棋,对棋盘信息和棋子信息进行变更,记录棋谱信息,并发送倒置后的下棋信息给对方,重置isFirstClick为true,同时判断被吃掉的是不是帅或者将,如果是发送输赢信息并结束游戏,最后将isMyTurn改为false
⒉否则说明不能吃子,修改提示信息为“不能吃子”
public ChessBoard(){
initMap();
// initChess();
message = "程序处于等待联机状态!";
addMouseListener(new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e){
if(isMyTurn == false){
message = "现在该对方走棋";
repaint();
return ;
}
selectedChess(e);
repaint();
}
private void selectedChess(MouseEvent e) {
int index1,index2;//保存第一次和第二次被单击的棋子对应数组下标
if(isFirstClick){//第一次点击
firstChess = analyse(e.getX(),e.getY());
x1 = tempX;
y1 = tempY;
if(firstChess != null){
if(firstChess.player != LocalPlayer){
message = "点击成对方棋子了";
return ;
}
isFirstClick = false;
}
}
else{
secondChess = analyse(e.getX(), e.getY());
x2 = tempX;
y2 = tempY;
if(secondChess!=null){//如果第二次点击选中了棋子
if(secondChess.player == LocalPlayer){//如果第二次点击的棋子是自己的棋子,则对第一次选中的棋子进行更换
firstChess = secondChess;
x1 = tempX;
y1 = tempY;
secondChess = null;
return ;
}
}
if(secondChess == null){//如果目标处没有棋子,判断是否可以走棋
if(IsAbleToMove(firstChess,x2,y2)){
index1 = map[x1][y1];
map[x1][y1] = -1;
map[x2][y2] = index1;
chess[index1].setPos(x2, y2);
//send
send("move"+"|"+index1 + "|"+(9-x2)+"|"+(8-y2)+"|"+(9-x1)+"|"+(8-y1)+"|"+"-1|");
list.add(new Node(index1,x2,y2,x1,y1,-1));//存储我方下棋信息
//置第一次选中标记量为空
isFirstClick = true;
repaint();
SetMyTurn(false);//该对方了
}else{
message = "不符合走棋规则";
}
return ;
}
if(secondChess != null&&IsAbleToMove(firstChess, x2, y2)){//可以吃子
isFirstClick = true;
index1 = map[x1][y1];
index2 = map[x2][y2];
map[x1][y1] = -1;
map[x2][y2] = index1;
chess[index1].setPos(x2, y2);
chess[index2] = null;
repaint();
send("move"+"|"+index1+"|"+(9-x2)+"|"+(8-y2)+"|"+(9-x1)+"|"+(8-y1)+"|"+index2+"|");
list.add(new Node(index1,x2,y2,x1,y1,index2));//记录我方下棋信息
if(index2 == 0){//被吃掉的是将
message = "红方赢了";
JOptionPane.showConfirmDialog(null, "红方赢了","提示",JOptionPane.DEFAULT_OPTION);
//send
send("succ"+"|"+"红方赢了"+"|");
return ;
}
if(index2 == 16){//被吃掉的是帅
message = "黑方赢了";
JOptionPane.showConfirmDialog(null, "黑方赢了","提示",JOptionPane.DEFAULT_OPTION);
//send
send("succ"+"|"+"黑方赢了"+"|");
return ;
}
SetMyTurn(false);//该对方了
}else{//不能吃子
message = "不能吃子";
}
}
}
使用analyse()方法分析鼠标选中的棋盘坐标
由于棋盘图片大小和棋子间距的关系,这里使用了leftX和leftY表示水平和垂直偏移量;
具体转换过程是使用点击处的像素坐标去和每个棋盘坐标的小矩形进行匹配,如果点位于矩形内,说明点击了该棋盘坐标。
private Chess analyse(int x, int y) {
int leftX = 28,leftY = 20;
int index_x=-1,index_y=-1;//记录点击处是第几行第几列
for(int i=0;i<=9;i++){
for(int j=0;j<=8;j++){
Rectangle r = new Rectangle(leftX+j*62, leftY+i*57, 40, 40);
if(r.contains(x,y)){
index_x = i;
index_y = j;
break;
}
}
}
tempX = index_x;
tempY = index_y;
if(index_x==-1&&index_y==-1){//没有点击到任何棋盘可点击处
return null;
}
if(map[index_x][index_y]==-1){
return null;
}else{
return chess[map[index_x][index_y]];
}
}
当玩家输入完对方的IP地址和端口号,点击开始,向对方IP地址的指定端口号发送联机请求,同时自己启动线程开始监听端口。
具体过程可以简述成:
A发送请求联机要求给B的某端口并且开始监听自己的端口,但是此时B是没有启动线程监听端口的,所以收不到A的请求;
接着B输入A的IP地址和端口点击开始发送联机请求,由于刚才A开始监听端口了,这次B发送的联机请求A可以收到,A收到B的联机请求,发送个联机成功的消息回B,双方游戏开始。
//加入对局
public void startJoin(String ip,int otherPort,int receivePort){
flag = true;
this.otherPort = otherPort;
this.receivePort = receivePort;
this.ip = ip;
System.out.println("能帮我连接到"+ip+"吗");
send("join|");
Thread th = new Thread(this);
th.start();
//
}
//联机请求及联机响应相关部分代码
@Override
public void run() {
System.out.println("我是客户端,我绑定的端口是"+receivePort);
DatagramSocket s = new DatagramSocket(receivePort);
byte[] data = new byte[100];
DatagramPacket dgp = new DatagramPacket(data, data.length);
while(flag==true){
s.receive(dgp);
String strData = new String(data);
String[] array = new String[6];
array = strData.split("\\|");
if(array[0].equals("join")){//对局被加入,我是黑方
LocalPlayer = BLACKPLAYER;
startNewGame(LocalPlayer);
if(LocalPlayer==REDPLAYER){
SetMyTurn(true);
}else{
SetMyTurn(false);
}
//发送联机成功信息
send("conn|");
}else if(array[0].equals("conn")){//我成功加入别人的对局,联机成功。我是红方
LocalPlayer = REDPLAYER;
startNewGame(LocalPlayer);
if(LocalPlayer==REDPLAYER){
SetMyTurn(true);
}else{
SetMyTurn(false);
}
}
其余部分先省略......
}
}
省略......
}
当双方联机成功后,startNewGame(short player)根据玩家的执子颜色使用initChess()初始化棋子布局;布局按红下黑上分布,如果玩家执黑子,使用ReverseBoard()方法将棋子位置进行对调,变成黑下红上。布局后将所有棋子和棋盘重画显示。
public void startNewGame(short player){
initMap();
initChess();
if(player == BLACKPLAYER){
reverseBoard();
}
repaint();
}
//初始化棋子布局
private void initChess(){
//布置黑方棋子
chess[0] = new Chess(BLACKPLAYER,"将",0,4);//第0行第4列
map[0][4] = 0;
chess[1] = new Chess(BLACKPLAYER,"士",0,3);//第0行第3列
map[0][3] = 1;
chess[2] = new Chess(BLACKPLAYER,"士",0,5);//第0行第5列
map[0][5] = 2;
chess[3] = new Chess(BLACKPLAYER,"象",0,2);//第0行第2列
map[0][2] = 3;
chess[4] = new Chess(BLACKPLAYER,"象",0,6);//第0行第6列
map[0][6] = 4;
chess[5] = new Chess(BLACKPLAYER,"马",0,1);//第0行第1列
map[0][1] = 5;
chess[6] = new Chess(BLACKPLAYER,"马",0,7);//第0行第7列
map[0][7] = 6;
chess[7] = new Chess(BLACKPLAYER,"车",0,0);//第0行第0列
map[0][0] = 7;
chess[8] = new Chess(BLACKPLAYER,"车",0,8);//第0行第8列
map[0][8] = 8;
chess[9] = new Chess(BLACKPLAYER,"炮",2,1);//第2行第1列
map[2][1] = 9;
chess[10] = new Chess(BLACKPLAYER,"炮",2,7);//第2行第7列
map[2][7] = 10;
for(int i=0;i<5;i++){//5个黑方卒布局
chess[11+i] = new Chess(BLACKPLAYER,"卒",3,i*2);
map[3][i*2] = 11+i;
}
//布置红方棋子
chess[16] = new Chess(REDPLAYER,"帅",9,4);//第9行第4列
map[9][4] = 16;
chess[17] = new Chess(REDPLAYER,"仕",9,3);//第9行第3列
map[9][3] = 17;
chess[18] = new Chess(REDPLAYER,"仕",9,5);//第9行第5列
map[9][5] = 18;
chess[19] = new Chess(REDPLAYER,"相",9,2);//第9行第2列
map[9][2] = 19;
chess[20] = new Chess(REDPLAYER,"相",9,6);//第9行第6列
map[9][6] = 20;
chess[21] = new Chess(REDPLAYER,"马",9,1);//第9行第1列
map[9][1] = 21;
chess[22] = new Chess(REDPLAYER,"马",9,7);//第9行第7列
map[9][7] = 22;
chess[23] = new Chess(REDPLAYER,"车",9,0);//第9行第0列
map[9][0] = 23;
chess[24] = new Chess(REDPLAYER,"车",9,8);//第9行第8列
map[9][8] = 24;
chess[25] = new Chess(REDPLAYER,"炮",7,1);//第7行第1列
map[7][1] = 25;
chess[26] = new Chess(REDPLAYER,"炮",7,7);//第7行第7列
map[7][7] = 26;
for(int i=0;i<5;i++){//5个红方兵布局
chess[27+i] = new Chess(REDPLAYER,"兵",6,i*2);
map[6][i*2] = 27+i;
}
}
//翻转所有棋子位置
private void reverseBoard(){
//对棋子的位置进行互换
for(int i=0;i<32;i++){
if(chess[i]!=null){
chess[i].ReversePos();
}
}
//对两方的棋盘信息进行倒置互换
for(int i=0;i<5;i++){
for(int j=0;j<9;j++){
int temp = map[i][j];
map[i][j] = map[9-i][8-j];
map[9-i][8-j] = temp;
}
}
}
使用paint(Graphics g)方法重画游戏中的背景棋盘和所有棋子对象以及提示消息。
//对场景对象进行绘画
public void paint(Graphics g){
g.clearRect(0, 0, this.getWidth(), this.getHeight());
Image backgroundImage = Toolkit.getDefaultToolkit().getImage("D://Game//ChineseChessGame//chessBoard.png");
g.drawImage(backgroundImage,0,0,600,600,this);
for(int i=0;i<32;i++){
if(chess[i]!=null){
chess[i].paint(g, this);
}
}
if(firstChess!=null){
firstChess.DrawSelectedChess(g);
}
if(secondChess!=null){
secondChess.DrawSelectedChess(g);
}
g.drawString(message, 0, 620);
}
使用IsAbleToMove(firstChess,x,y)判断是否能走棋并返回逻辑值;
传入的参数说明,firstChess是玩家第一次选中的棋子对象,x,y表示该棋子想要移动到第x行第y列;
根据不同的棋子对象有不同的走法规则判断:
①如果移动的棋子是“将”或“帅”
先根据棋子移动的起点和终点,判断是否符合老将碰面的情况;如果玩家尝试用“将”去吃“帅”,并且它们之间没有其他棋子,返回true;玩家尝试用“帅”去吃“将”同理;
如果不符合老将碰面的情况,再判断是否符合只在直线上移动一步并且移动范围没超过超过“九宫”的条件,如果符合返回true
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
if(chessName.equals("将")||chessName.equals("帅")){
//如果玩家尝试用"将"去吃"帅",或者相反,则判断是否符合两将碰面的情况(必须在同一列,并且中间没有其他子)
if(oldY==y&&(map[x][y]==0||map[x][y]==16)){
for(int i=x+1;i1||Math.abs(y-oldY)>1){//如果横走或竖走超过一格
return false;
}
if((x>2&&x<7)||y<3||y>5){//如果超出九宫格区域
return false;
}
return true;
}
...
}
②如果移动的棋子是“仕”或“士”
根据棋子移动的起点坐标(oldX,oldY)和棋子移动的终点坐标(x,y),通过判断是否沿直线走,判断斜走的时候横位移量和纵位移量是否有一个大于1,判断移动后的位置是否超过九宫 三个条件来筛选出 只能在九宫内斜走一步的情况。
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("士")||chessName.equals("仕")){
if((x-oldX)*(y-oldY)==0){//如果横走或者竖走
return false;
}
if(Math.abs(x-oldX)>1||Math.abs(y-oldY)>1){//如果横向或者纵向的位移量大于1,即不是斜走一格
return false;
}
if((x>2&&x<7)||y<3||y>5){//如果超出九宫格区域
return false;
}
return true;
}
...
}
③如果移动的棋子是“象”或“相”
根据棋子移动的起点坐标(oldX,oldY)和棋子移动的终点坐标(x,y),通过判断是否直线行走、判断是否走田字、判断是否越过“楚河-汉界”、判断是否被堵象眼 四个判断条件筛选出 棋子走田字并且不越界的情况。
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("相")||chessName.equals("象")){
if((x-oldX)*(y-oldY)==0){//如果横走或者竖走
return false;
}
if(Math.abs(x-oldX)!=2||Math.abs(y-oldY)!=2){//如果横向或者纵向的位移量不同时为2,即不是走田字
return false;
}
if(x<5){//如果象越过“楚河-汉界”
return false;
}
int i=0,j=0;//记录象眼位置
if(x-oldX==2){//象向下跳
i=oldX+1;
}
if(x-oldX==-2){//象向上跳
i=oldX-1;
}
if(y-oldY==2){//象向右跳
j=oldY+1;
}
if(y-oldY==-2){//象向左跳
j=oldY-1;
}
if(map[i][j]!=-1){//被堵象眼
return false;
}
return true;
}
...
}
④如果移动的棋子是“马”
根据棋子移动的起点坐标(oldX,oldY)和棋子移动的终点坐标(x,y),通过判断马是否走“曰”字、判断马是否被绊马脚两个判断条件筛选出 棋子走“曰”字并不被绊马脚的情况。补充:熟悉象棋的童鞋应该知道,马踏八方,但是实际上被绊马脚的情况只有四种。
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("马")){
if(Math.abs(x-oldX)*Math.abs(y-oldY)!=2){//如果横向位移量乘以竖向位移量不等于2,即如果马不是走日字
return false;
}
if(x-oldX==2){//如果马向下跳,并且横向位移量为1,纵向位移量为2
if(map[oldX+1][oldY]!=-1){//如果被绊马脚
return false;
}
}
if(x-oldX==-2){//如果马向上跳,并且横向位移量为1,纵向位移量为2
if(map[oldX-1][oldY]!=-1){//如果被绊马脚
return false;
}
}
if(y-oldY==2){//如果马向右跳,并且横向位移量为2,纵向位移量为1
if(map[oldX][oldY+1]!=-1){//如果被绊马脚
return false;
}
}
if(y-oldY==-2){//如果马向左跳,并且横向位移量为2,纵向位移量为1
if(map[oldX][oldY-1]!=-1){//如果被绊马脚
return false;
}
}
return true;
}
...
}
⑤如果移动的棋子是“车”
根据棋子移动的起点坐标(oldX,oldY)和棋子移动的终点坐标(x,y),通过判断是否斜走、判断两坐标中间是否还有其他子 两个判断条件 筛选出正确的车的走法。
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("车")){
if((x-oldX)*(y-oldY)!=0){//如果横向位移量和纵向位移量同时都不为0,说明车在斜走,故return false
return false;
}
if(x!=oldX){//如果车纵向移动
if(oldX>x){//将判断过程简化为纵向从上往下查找中间是否有其他子
int t = x;
x = oldX;
oldX = t;
}
for(int i=oldX+1;iy){//将判断过程简化为横向从左到右查找中间是否有其他子
int t = y;
y = oldY;
oldY = t;
}
for(int i=oldY+1;i
⑥如果移动的棋子是“炮”
先使用swapFlagX和swapFlagY表示x和y值是否交换过,如果棋子斜走返回false;使用变量c记录棋子移动前和移动后的位置中间有几个棋子;根据横走和纵走的情况分别计算两个位置中间的棋子数目;如果c>1说明两个位置中间的棋子超过1个,所以不能移动;如果c==0说明两个位置中间没有棋子,如果之前交换过x或者y需要先交换回来,再判断移动的终点是否有其他棋子,如果有棋子占位,则不能移动;如果c==1说明两个位置中间有一个棋子,如果之前交换过x或者y需要先交换回来,再判断移动的终点是否有其他棋子,如果没有其他棋子,则不能移动(不能打空炮)
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
System.out.println("判断前:"+x+":"+y);
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("炮")){
boolean swapFlagX = false;//记录纵向棋子是否交换过
boolean swapFlagY = false;//记录横向棋子是否交换过
if((x-oldX)*(y-oldY)!=0){//如果棋子斜走
return false;
}
int c = 0;//记录两子中间有多少个子
if(x!=oldX){//如果炮是纵向移动
if(oldX>x){//简化后续判断
int t = x;
x = oldX;
oldX = t;
swapFlagX = true;
}
for(int i=oldX+1;iy){//简化后续判断
int t = y;
y = oldY;
oldY = t;
swapFlagY = true;
}
for(int i=oldY+1;i1){//中间超过一个子
return false;
}
if(c==0){//如果中间没有子
if(swapFlagX==true){//如果之间交换过,需要重新交换回来
int t = x;
x = oldX;
oldX = t;
}
if(swapFlagY==true){
int t = y;
y = oldY;
oldY = t;
}
if(map[x][y]!=-1){//如果目标处有子存在,则不能移动
return false;
}
}
if(c==1){//如果中间只有一个子
if(swapFlagX==true){//如果之间交换过,需要重新交换回来
int t = x;
x = oldX;
oldX = t;
}
if(swapFlagY==true){
int t = y;
y = oldY;
oldY = t;
}
if(map[x][y]==-1){//如果目标处没有棋子,即不能打空炮
return false;
}
}
return true;
}
...
}
⑦如果移动的棋子是“兵”或“卒”
根据棋子移动的起点坐标(oldX,oldY)和棋子移动的终点坐标(x,y),通过判断棋子是否斜走、判断棋子一次移动是否超过一步、根据棋子是否过河分别判断移动方向是否符合规则(过河前只许前走,过河后可以前、左、右走)这几个判断条件筛选出正确的走法情况。
//判断是否可以落子
private boolean IsAbleToMove(Chess firstChess, int x, int y) {
int oldX,oldY;
oldX = firstChess.x;
oldY = firstChess.y;
String chessName = firstChess.typeName;
...
if(chessName.equals("卒")||chessName.equals("兵")){
if((x-oldX)*(y-oldY)!=0){//如果斜走
return false;
}
if(Math.abs(x-oldX)>1||Math.abs(y-oldY)>1){//如果一次移动了一格以上
return false;
}
if(oldX>=5){//如果兵未过河,则只能向上移动,不能左右移动
if(Math.abs(y-oldY)>0){//没过河尝试左右移动
return false;
}
if(x-oldX==1){//兵向下移动
return false;
}
}else{//如果已经过河,可以进行上左右移动,但不能进行向下移动
if(x-oldX==1){//兵向下移动
return false;
}
}
return true;
}
...
}
使用rebackChess(int index,int x,int y,int oldX,int oldY)方法将棋子进行位置的回退
//将棋子回退到上一步的位置,并把棋子未回退前的棋盘位置信息清空
private void rebackChess(int index,int x,int y,int oldX,int oldY){
chess[index].setPos(oldX, oldY);
map[oldX][oldY] = index;//棋子放回到(oldX,oldY)
map[x][y] = -1;//棋盘里原有棋子位置信息清除
}
使用resetChess(int index,int x,int y)方法将一个被吃了的棋子放回到棋盘
//将一个被吃了的子重新放回到棋盘,传入参数说明:index棋子数组下标,第x行,第y列
private void resetChess(int index,int x,int y){
short temp = index<16?BLACKPLAYER:REDPLAYER;//存储是哪方的棋子
String name = null;//存储棋子上的字
switch(index){//根据棋子索引,得到棋子上面的字
case 0:name="将";break;
case 1:;
case 2:name="士";break;
case 3:;
case 4:name="象";break;
case 5:;
case 6:name="马";break;
case 7:;
case 8:name="车";break;
case 9:;
case 10:name="炮";break;
case 11:;
case 12:;
case 13:;
case 14:;
case 15:name="卒";break;
case 16:name="帅";break;
case 17:;
case 18:name="仕";break;
case 19:;
case 20:name="相";break;
case 21:;
case 22:name="马";break;
case 23:;
case 24:name="车";break;
case 25:;
case 26:name="炮";break;
case 27:;
case 28:;
case 29:;
case 30:;
case 31:name="兵";break;
}
chess[index] = new Chess(temp,name,x,y);
map[x][y] = index;//将棋子放回到棋盘
}
使用SetMyTurn(boolean b)控制执子权利,b为true时我方下棋,否则对方下棋
private void SetMyTurn(boolean b) {
isMyTurn = b;
if(b){
message = "请您开始走棋";
}else{
message = "对方正在思考";
}
}
发送信息是使用send(String s)方法,主要实现创建UDP网络服务,传送信息到指定计算机的指定端口号后,关闭UDP套接字。
public void send(String str) {
DatagramSocket s = null;
try{
s = new DatagramSocket();
byte[] buffer;
buffer = new String(str).getBytes();
// InetAddress ia = InetAddress.getLocalHost();//获取本机地址
InetAddress ia = InetAddress.getByName(ip );//获取目标地址
System.out.println("请求连接的ip是"+ip);
DatagramPacket dgp = new DatagramPacket(buffer, buffer.length,ia,otherPort);
s.send(dgp);
System.out.println("发送信息:"+str);
}catch(Exception e){
e.printStackTrace();
}finally{
if(s!=null){
s.close();
}
}
}
使用run()方法不断侦听本地设定的端口,得到对方的信息根据自己定义的通信信息设计规则,解析成不同的指令,并分别处理:
@Override
public void run() {
try{
System.out.println("我是客户端,我绑定的端口是"+receivePort);
DatagramSocket s = new DatagramSocket(receivePort);
byte[] data = new byte[100];
DatagramPacket dgp = new DatagramPacket(data, data.length);
while(flag==true){
s.receive(dgp);
String strData = new String(data);
String[] array = new String[6];
array = strData.split("\\|");
if(array[0].equals("join")){//对局被加入,我是黑方
LocalPlayer = BLACKPLAYER;
startNewGame(LocalPlayer);
if(LocalPlayer==REDPLAYER){
SetMyTurn(true);
}else{
SetMyTurn(false);
}
//发送联机成功信息
send("conn|");
}else if(array[0].equals("conn")){//我成功加入别人的对局,联机成功。我是红方
LocalPlayer = REDPLAYER;
startNewGame(LocalPlayer);
if(LocalPlayer==REDPLAYER){
SetMyTurn(true);
}else{
SetMyTurn(false);
}
}else if(array[0].equals("succ")){
if(array[1].equals("黑方赢了")){
if(LocalPlayer==REDPLAYER)
JOptionPane.showConfirmDialog(null, "黑方赢了,你可以重新开始","你输了",JOptionPane.DEFAULT_OPTION);
else
JOptionPane.showConfirmDialog(null, "黑方赢了,你可以重新开始","你赢了",JOptionPane.DEFAULT_OPTION);
}
if(array[1].equals("红方赢了")){
if(LocalPlayer==REDPLAYER)
JOptionPane.showConfirmDialog(null, "红方赢了,你可以重新开始","你赢了",JOptionPane.DEFAULT_OPTION);
else
JOptionPane.showConfirmDialog(null, "红方赢了,你可以重新开始","你输了",JOptionPane.DEFAULT_OPTION);
}
message = "你可以重新开局";
GameClient.buttonStart.setEnabled(true);//可以点击开始按钮了
//
}else if(array[0].equals("move")){
//对方的走棋信息,move|棋子索引号|x|y|oldX|oldY|背驰棋子索引
System.out.println("接受信息:"+array[0]+"|"+array[1]+"|"+array[2]+"|"+array[3]+"|"+array[4]+"|"+array[5]+"|"+array[6]+"|");
int index = Short.parseShort(array[1]);
x2 = Short.parseShort(array[2]);
y2 = Short.parseShort(array[3]);
// String z = array[4];//对方上步走棋的棋谱信息
// message = x2 + ":" +y2;
int oldX = Short.parseShort(array[4]);//棋子移动前所在行数
int oldY = Short.parseShort(array[5]);//棋子移动前所在列数
int eatChessIndex = Short.parseShort(array[6]);//被吃掉的棋子索引
list.add(new Node(index,x2,y2,oldX,oldY,eatChessIndex));//记录下棋信息
message = "对方将棋子\""+chess[index].typeName+"\"移动到了("+x2+","+y2+")\n现在该你走棋";
Chess c = chess[index];
x1 = c.x;
y1 = c.y;
index = map[x1][y1];
int index2 = map[x2][y2];
map[x1][y1] = -1;
map[x2][y2] = index;
chess[index].setPos(x2, y2);
if(index2!=-1){// 如果吃了子,则取下被吃掉的棋子
chess[index2] = null;
}
repaint();
isMyTurn = true;
}else if(array[0].equals("quit")){
JOptionPane.showConfirmDialog(null, "对方退出了,游戏结束!","提示",JOptionPane.DEFAULT_OPTION);
message = "对方退出了,游戏结束!";
GameClient.buttonStart.setEnabled(true);//可以点击开始按钮了
}else if(array[0].equals("lose")){
JOptionPane.showConfirmDialog(null, "恭喜你,对方认输了!","你赢了",JOptionPane.DEFAULT_OPTION);
SetMyTurn(false);
GameClient.buttonStart.setEnabled(true);//可以点击开始按钮了
}else if(array[0].equals("ask")){//对方请求悔棋
String msg = "对方请求悔棋,是否同意?";
int type = JOptionPane.YES_NO_OPTION;
String title = "请求悔棋";
int choice = 0;
choice = JOptionPane.showConfirmDialog(null, msg,title,type);
if(choice==1){//否,拒绝悔棋
send("refuse|");
}else if(choice == 0){//是,同意悔棋
send("agree|");
message = "同意了对方的悔棋,对方正在思考";
SetMyTurn(false);//对方下棋
Node temp = list.get(list.size()-1);//获取棋谱最后一步棋的信息
list.remove(list.size()-1);//移除
if(LocalPlayer==REDPLAYER){//假如我是红方
if(temp.index>=16){//上一步是我下的,需要回退两步
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
temp = list.get(list.size()-1);
list.remove(list.size()-1);
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,需要回退一步
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}
}else{//假如我是黑方
if(temp.index<16){//上一步是我下的,需要回退两步
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
temp = list.get(list.size()-1);
list.remove(list.size()-1);
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,需要回退一步
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}
}
repaint();
}
}else if(array[0].equals("agree")){//对方同意悔棋
JOptionPane.showMessageDialog(null, "对方同意了你的悔棋请求");
Node temp = list.get(list.size()-1);//获取棋谱最后一步棋的信息
list.remove(list.size()-1);//移除
if(LocalPlayer==REDPLAYER){//假如我是红方
if(temp.index>=16){//上一步是我下的,回退一步即可
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,需要回退两步
//第一次回退,此时回退到的状态是我刚下完棋轮到对方下棋的状态
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
temp = list.get(list.size()-1);
list.remove(list.size()-1);
//第二次回退,此时回退到的状态是我上一次刚轮到我下棋的状态
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}
}else{//假如我是黑方
if(temp.index<16){//上一步是我下的,回退一步即可
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,需要回退两步
//第一次回退,此时回退到的状态是我刚下完棋轮到对方下棋的状态
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
temp = list.get(list.size()-1);
list.remove(list.size()-1);
//第二次回退,此时回退到的状态是我上一次刚轮到我下棋的状态
rebackChess(temp.index, temp.x, temp.y, temp.oldX, temp.oldY);//回退棋子
if(temp.eatChessIndex!=-1){//如果上一步吃了子,将被吃的子重新放回到棋盘
resetChess(temp.eatChessIndex, temp.x, temp.y);//将被吃的棋子放回棋盘
}
}
}
SetMyTurn(true);
repaint();
}else if(array[0].equals("refuse")){//对方拒绝悔棋
JOptionPane.showMessageDialog(null, "对方拒绝了你的悔棋请求");
}
// System.out.println(new String(data));
//s.send(dgp);
}
}catch(Exception e){
e.printStackTrace();
}
}
游戏窗口类实现整体界面的组装,包括认输,请求悔棋,开始三个按钮的点击事件处理;
需要注意的是,这里监听实现的比较简单,假如A向3003端口发送联机请求,那么A就监听3004端口;假如A向3004端口发送联机请求,那么A就监听3003端口;
package 中国象棋;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.*;
public class GameClient extends JFrame{
static ChessBoard gamePanel = new ChessBoard();
static JButton buttonGiveIn = new JButton("认输");
static JButton buttonStart = new JButton("开始");
JButton buttonAskRegret = new JButton("请求悔棋");
JTextField textIp = new JTextField("127.0.0.1");//IP
JTextField textPort = new JTextField("3004");//对方端口
public static final short REDPLAYER = 1;
public static final short BLACKPLAYER = 0;
public GameClient(){
JPanel panelBottom = new JPanel(new FlowLayout());
panelBottom.add(new JLabel("输入对方IP:"));
panelBottom.add(textIp);
panelBottom.add(new JLabel("输入对方端口:"));
panelBottom.add(textPort);
panelBottom.add(buttonGiveIn);
panelBottom.add(buttonAskRegret);
panelBottom.add(buttonStart);
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(gamePanel,BorderLayout.CENTER);
this.getContentPane().add(panelBottom,BorderLayout.SOUTH);
this.setSize(610,730);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("中国象棋客户端");
this.setVisible(true);
buttonGiveIn.setEnabled(false);
buttonAskRegret.setEnabled(false);
buttonStart.setEnabled(true);
setVisible(true);
this.addWindowListener(new WindowAdapter() {//窗口关闭事件
public void windowClosing(WindowEvent e){
try{
gamePanel.send("quit|");
System.exit(0);
}catch(Exception ex){
ex.printStackTrace();
}
}
});
buttonGiveIn.addMouseListener(new MouseAdapter() {//认输事件
public void mouseClicked(MouseEvent e){
try{
gamePanel.send("lose|");//发送认输信息
}catch(Exception ex){
ex.printStackTrace();
}
}
});
buttonAskRegret.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e){
if(gamePanel.list.size()==0){
JOptionPane.showMessageDialog(null, "不能悔棋");
return ;
}
if(gamePanel.list.size()==1){
int flag = gamePanel.LocalPlayer==REDPLAYER?REDPLAYER:BLACKPLAYER;
if(flag==REDPLAYER){//如果我是红方,判断上一步是不是对方下的,如果是,不能悔棋
if(gamePanel.list.get(0).index<16){
JOptionPane.showMessageDialog(null, "不能悔棋");
return ;
}
}else{
if(gamePanel.list.get(0).index>=16){
JOptionPane.showMessageDialog(null, "不能悔棋");
return ;
}
}
}
gamePanel.send("ask|");//发送请求悔棋请求
}
});
buttonStart.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e){
String ip = textIp.getText();
int otherPort = Integer.parseInt(textPort.getText());
int receivePort;
if(otherPort == 3003){
receivePort = 3004;
}else{
receivePort = 3003;
}
gamePanel.startJoin(ip, otherPort, receivePort);
buttonGiveIn.setEnabled(true);
buttonAskRegret.setEnabled(true);
buttonStart.setEnabled(false);
}
});
}
public static void main(String[] args) {
new GameClient();
}
}
经过博主一顿瞎搞,程序总算可以真正地进行联机了,真不容易~哈哈,需要注意的是IP地址不能搞错了,得到本机IP地址可以使用cmd的ipconfig命令获取,在这里我是使用手机热点形式,让两台电脑同时连接一个手机热点,然后再输入命令得到本机IP,实现连接的;
具体如下:
①使用ipconfig命令查询本机IP
②在游戏界面输入对方IP地址,点击开始(一定要双方都输入对方的IP地址,并且一个端口为3003,另一个端口为3004,具体原因前面关于联机实现的时候说过了)进行联机
③联机成功
emmmmm...上图吧
这是博主电脑运行效果
这是博主室友电脑运行效果
到这里整个游戏的开发过程已经详尽介绍完毕了;
不过这个游戏依旧存在很多可以优化的地方,但是真正要做好一件事情其实是很繁琐的,这里我想简单地谈下我的解决思路:
Q:我希望对方将我军时,提示音效,我该怎么做?
A:在收到对方发送过来的走棋信息并更新完棋局信息的时候,调用自定义的一个方法,方法的功能是判断对方是否在将我方军,具体实现就是遍历对方的棋子数组,用每个对方的棋子去尝试移动到我方“将”或“帅”的位置,如果某个棋子按照象棋规则可以移动到我方“将”或“帅”的位置(即对方下一步可以获胜),返回true;如果对方将我方军,那么播放音效;音效的播放后续博文将进行播放音效的代码模板补充;
Q:可以记录棋谱信息到文件里,然后根据棋谱文件还原对局信息吗?
A:可以的,前面说到过棋盘类有个存储Node对象的ArrayList对象list用于我们对每一步棋的信息进行存储,我们可以在一方获胜的时候,将list对象以某种自定义的形式(或者直接用JSON去封装)进行重组,变成一个特殊的字符串,使用文件IO存储到电脑内存中,需要注意的就是存储的时候需要另外记录下自己这方的执子颜色;根据棋谱文件还原棋局的话,需要加些UI方面的工作,在窗口类新加按钮和文本框用于读取指定名称的棋谱,解析后根据棋谱中我方执子颜色初始化棋盘信息,然后可以点击下一步按钮,一步一步地对棋谱信息进行棋局还原。游戏常用的历史文件的IO以及UI组件的使用后续博文也会补充相关模板;
Q:为什么我游戏结束后,不能重新点击开始重开一局?
A:emmm...博主尝试实现过,但是效果不理想(重新让开始按钮变成可点击状态时,只有一个游戏窗口的开始可以点击),我摸不着头脑,索性没去优化了。也许可以另创一个类,将窗口对象实例化为一个类静态成员,然后类似“工厂模式”那样,进行工厂类A.gameClient.button.setEnable(true)的操作;暂时不想去尝试了。
如果各位童鞋还有其他的什么疑问。欢迎大家在评论区一起沟通。创作不易,感谢支持~