游戏介绍:
中国象棋是起源于中国的一种棋戏,属于二人对抗性游戏的一种,在中国有着悠久的历史。由于规则简单,趣味性强,成为流行极为广泛的棋类游戏。 中国象棋使用方形格状棋盘及红黑二色圆形棋子进行对弈,棋盘上有十条横线、九条竖线共分成90个交叉点;中国象棋的棋子共有32个,每种颜色16个棋子,分为7个兵种,摆放和活动在交叉点上。双方交替行棋,先把对方的将(帅)“将死”的一方获胜。
本篇博文开发了一个基于UDP通信的中国象棋联机版游戏,游戏符合传统中国象棋规则(包括老将见面判法),具备基本的联机和悔棋功能,悔棋需要得到对方同意。运行效果如下:
使用素材文件夹:
素材及完整源码链接:https://pan.baidu.com/s/1m-z1eDGOabN4iLx5VEcZMw 提取码: rpbr
中国象棋介绍:
Ⅰ棋盘
棋子活动的场所叫做“棋盘”,在长方形的平面上,绘有9条平行的竖线和10条平行的横线相交组成,共90个交叉点,棋子摆在这些交叉点上。中间第5根横线和第6根横线之间未画竖线的空白地带,叫做“楚河 汉界”,整个棋盘就以“楚河 汉界”分为相等的两部分;两方将帅坐阵,画有“米”字方格的地方叫做“九宫”。
Ⅱ棋子
象棋的棋子共32个,分为红黑两组,各16个,由对弈双方各执一组,每组兵种是一样的,各分为七种。
红方:帅、仕、相、车、马、炮、兵;
黑方:将、士、象、车、马、炮、卒。
其中帅和将、士和仕、相和象、兵和卒的作用完全相同,仅仅是为了区分红棋和黑棋。
Ⅲ各棋子走法说明
①将或帅
移动范围:它只能在九宫内移动
移动规则:它每一步只可以水平或垂直移动一点
特殊说明:红方帅和黑方将不能直接见面,即帅和将中间必须有其他棋子间隔,否则构成“老将见面”情况;
假如现在是红方执子,此时构成“老将见面”状况,那么玩家可以移动帅直接吃掉对方的将,红方获胜。
②士或仕
移动范围:它只能在九宫内移动
移动规则:它每一步只能沿对角线方向移动一点
③象或相
移动范围:“楚河 汉界”的一侧
移动规则:它每一步只能沿对角线走两点(走田字),另外,移动的对角线方向上一点不能有其他棋子(不能被堵象眼)。
④马
移动范围:任何位置
移动规则:它每一步只能走曰字并且不能被绊马脚(斜着走,横位移量*纵位移量等于2)。
⑤车
移动范围:任何位置
移动规则:它可以水平或垂直方向移动任意个无阻碍的点。
⑥炮
移动范围:任何位置
移动规则:移动跟车很相似,它既可以像车一样水平或垂直方向移动任意无阻碍的点,也可以通过隔一个棋子吃掉对方一个棋子的方式进行移动。
⑦卒或兵
移动范围:过河后可以走过河后的一侧的任何位置,未过河只能走该棋子向上方向的直线区域
移动规则:它每步只能移动一个点,它未过河时只能向前移动,如果它过河了,增加向左向右移动的能力。
Ⅳ胜负说明
对局中,如果一方玩家认输或者该玩家的“将”或“帅”被对方棋子吃掉,该玩家算输,对方算赢。
UDP通信基础
这里涉及到的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类,包含 移动的棋子信息、移动后所在行数、移动后所在列数、移动前所在行数、移动前所在列数、该步棋吃掉了的棋子的索引值;主要作用是为了实现悔棋功能,当然也可以对此进行拓展,完成棋谱记录及按棋谱还原棋局的功能。
游戏具体实现步骤
Ⅰ设计棋子类(Chess.java)
棋子类的成员信息主要包含 棋子所属玩家、棋子类别、棋子所在行数、棋子所在列数、棋子图案的信息;
方法主要包括:
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类用于记录每步棋的信息(Node.java)
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;