Java遊戲開發——中國象棋聯機版
遊戲介紹:
中國象棋是起源於中國的一種棋戲,屬於二人對抗性遊戲的一種,在中國有著悠久的歷史。由於規則簡單,趣味性強,成為流行極為廣泛的棋類遊戲。 中國象棋使用方形格狀棋盤及紅黑二色圓形棋子進行對弈,棋盤上有十條橫線、九條豎線共分成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;
this.x = x;
this.y = y;
this.oldX = oldX;
this.oldY = oldY;
this.eatChessIndex = eatChessIndex;
}
}
Ⅲ設計棋盤類(ChessBoard.javb)
棋盤類是遊戲面板,先定義一個數組chess存放雙方32個棋子物件。二維陣列map儲存了當前棋盤的棋子佈局,當map[x][y]=i時說明棋盤第x行第y列是棋子i,否則-1說明此處為空;聲明瞭ArrayList<Node>物件list用於儲存每步棋的資訊;以下是成員變數定義:
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<Node> list = new ArrayList<Node>();//儲存棋譜
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;i<oldX;i++){
if(map[i][y]!=-1){
return false;
}
}
return true;
}
if((x-oldX)*(y-oldY)!=0){//如果是斜著走
return false;
}
if(Math.abs(x-oldX)>1||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;i<x;i++){
if(map[i][oldY]!=-1){//如果中間有其他子
return false;
}
}
}
if(y!=oldY){//如果車橫向移動
if(oldY>y){//將判斷過程簡化為橫向從左到右查詢中間是否有其他子
int t = y;
y = oldY;
oldY = t;
}
for(int i=oldY+1;i<y;i++){
if(map[oldX][i]!=-1){//如果中間有其他子
return false;
}
}
}
return true;
}
...
}
⑥如果移動的棋子是“炮”
先使用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;i<x;i++){
if(map[i][oldY]!=-1){//如果中間有子
c += 1;
}
}
}
if(y!=oldY){//如果炮是橫向 移動
if(oldY>y){//簡化後續判斷
int t = y;
y = oldY;
oldY = t;
swapFlagY = true;
}
for(int i=oldY+1;i<y;i++){
if(map[oldX][i]!=-1){//如果中間有子
c += 1;
}
}
}
if(c>1){//中間超過一個子
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();
}
}
遊戲視窗類(GameClient.java)
遊戲視窗類實現整體介面的組裝,包括認輸,請求悔棋,開始三個按鈕的點選事件處理;
需要注意的是,這裡監聽實現的比較簡單,假如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)的操作;暫時不想去嘗試了。
如果各位童鞋還有其他的什麼疑問。歡迎大家在評論區一起溝通。創作不易,感謝支援~