目錄
- 前言
- 專案介紹
- 功能演示
- 登錄
- 注冊
- 選擇對手
- 落子提示
- 局時步時
- 查看戰績
- 落子五連
- 悔棋
- 聊天
- 新局
- 棋譜
- 保存棋譜
- 打開棋譜
- 其它功能
- 重繪
- 上下頁
- 認輸
- 退出
- 輪播圖片
- 背景音樂
- 求助小棋仙
- 組織結構
- 核心代碼決議
- com.wupgig.login.UserLogin
- com.wupgig.login.UserRegister
- com.wupgig.robot.RobotPlay
- 實作原理
- 評分步驟
- 核心代碼
- com.wupgig.chess.Chess
- com.wupgig.chess.ChessBoard
- 顯示在線玩家
- 處理對戰請求
- 己方落子
- 顯示對手的落子
- 其它
- 環境搭建
- 開發工具
- 開發環境
- 搭建步驟
- 啟動專案
- 完整原始碼
- Eclipse版本
- IDEA版本
- END
前言
這個落子難道真的沒得選擇了嗎?不!我不能輸!
出來吧!宇宙究極無敵巴啦啦小棋仙~
專案介紹
gobang專案是一個五子棋對戰平臺,基于JavaFX + Socket + JDBC + MySLQ 實作
包含注冊、登錄、選擇對手、落子提示、局時步時、查看戰績、悔棋、聊天、認輸、退出、新局、保存/打開棋譜、落子聲、背景音樂、背景圖片輪播和求助小棋仙等功能
功能演示
登錄
首先啟動該專案后會出現一個如下圖的登錄界面

默認會記住上一次登錄成功的賬號,選中記住密碼的復選框可以實作記住上一次登錄成功的密碼,
賬號密碼從資料庫中進行查詢,登錄失敗則提示賬號或密碼錯誤,登錄成功則打開棋盤界面,并關閉登錄界面,
限制重復登錄

不能登錄一個已經在線的賬號
注冊
點擊注冊按鈕會打開一個如下的注冊界面

賬號密碼使用正則運算式進行判斷是否符合規則,
點擊注冊后會到資料庫中根據賬號查詢,如果賬號存在則提示賬號已存在,如果不存在則注冊成功,把賬號資訊保存到資料庫,密碼用MD5進行加密,關閉注冊界面,打開登錄界面,
選擇對手

點擊對手名字,發送對戰請求

收到對戰請求

拒絕對戰請求

無法向正在對戰的玩家發送請求

顯示棋盤后會將當前所有在線的玩家的賬號分頁查詢顯示在棋盤如圖位置,點擊想要對戰的玩家即會向對方發送對戰申請
(無法向正在對戰、正在接受對戰請求、正在打譜、已經離線的玩家發送對戰請求)
如果成功發送對戰請求必須等對手回應(拒絕)后才能重新發送對戰請求,
對方同意后即可開始對戰,拒絕后會提示拒絕資訊,
落子提示

對局開始后如上圖位置會顯示 :
我方賬號 我方棋子的顏色
VS
對手賬號 對手棋子的顏色
當前落子 棋子顏色
每次落子后當前落子的顏色會動態改變
局時步時

自己超時

自己超時后直接判負,并發送超時訊息和對局結果訊息給對手
對手超時

收到對手超時的訊息后,直接判贏,收到對戰結果訊息后根據訊息所帶的資訊,保存結果和更新戰績表
局時總共10分鐘,步時一分鐘,每次輪到自己下棋的時候,局時、步時開始倒計時,輪到對手下棋的時候,局時暫停,步時重置為一分鐘,如果局時和步時兩者之中有一個為0后,就會直接判負,
查看戰績
查看自己的戰績

點擊我的戰績按鈕直接從資料庫中查詢自己的戰績資訊,然后展示到界面上
查看對手的戰績

點擊對手戰績按鈕后,發送訊息給對手,對手收到訊息后,回復一個帶賬號的訊息,我方接受到訊息后根據對手的賬號查詢其戰績資訊后展示到頁面上
落子五連
落子

在棋盤點擊的位置落下棋子,同時給對手發送落子訊息,攜帶棋子的資訊,對手接到訊息后將該棋子顯示到指定的位置
五連

每次落子后判斷是否五連,如果五連,游戲結束,顯示贏棋彈窗,并發送對戰結果訊息給對手,由對手將保存結果和更新戰績表
每次顯示對手棋子時判斷是否五連,如果五連,游戲結束,顯示輸棋彈窗
悔棋
點擊悔棋按鈕

對手同意悔棋請求

對手拒絕悔棋請求

點擊悔棋按鈕,給對手發送悔棋訊息,對手接受到悔棋訊息,棋盤上彈出提示框,
如果同意悔棋請求,則會移除棋盤上的最后一顆棋子,同時回傳同意悔棋的訊息給請求方,請求方接受到同意悔棋的訊息后,移除棋盤上的最后一顆棋子,
如果拒絕悔棋請求,則會給請求方回傳拒絕悔棋的訊息,請求方接受到拒絕悔棋的訊息后,會彈出一個提示框,
注:每個人只能成功悔棋一次,且輪到自己落子的時候無法悔棋
聊天
對戰時的親切問候

對局結束后的友好交談

透明的多行文本框

在輸入框輸入訊息,點擊發送按鈕或者敲下回車鍵 ,顯示訊息在自己棋盤的指定位置,并發送聊天訊息給對手,對手收到聊天訊息后,將訊息顯示在棋盤的指定位置
即使當前對局結束,只要玩家還在同一個房間內,那么他們依然可以互相發送聊天訊息
房間概念:
玩家一向玩家二發起對戰請求,玩家二同意后,此時可以理解為玩家一和玩家二在同一個房間,此局游戲結束后,他們還是在當前房間,直到有一方退出游戲或者和別的玩家開始對戰了,那么此時玩家一和玩家二才不在同一個房間
新局
點擊新局按鈕

拒絕新局
點擊新局按鈕后,給對手發送新局訊息,同時在自己的棋盤上顯示提示資訊
如果同意,先初始化自己的棋盤,然后發送同意新局的訊息給請求方,請求方收到同意訊息后,初始化自己的棋盤
如果拒絕,直接發送拒絕新局的訊息給請求方,請求方收到拒絕訊息后,顯示拒絕的訊息提示框
沒有對手

值得注意的是,當兩個玩家在同一個房間的時候,新局按鈕才有效,什么意思呢,玩家一向玩家二發起對戰請求,玩家二同意后,此時可以理解為玩家一和玩家二在同一個房間,此局游戲結束后,他們還是在當前房間,直到有一方退出游戲或者和別的玩家開始對戰了,那么此時玩家一和玩家二才不在同一個房間
棋譜
保存棋譜

點擊保存棋譜按鈕,通過io流將每個棋子的 x y 坐標和顏色分別保存到檔案中的每行,通過相同的分隔符隔開,方便打開棋譜時讀取
注:只有棋盤上有棋子且對局結束后才能保存棋譜
打開棋譜

點擊打開棋譜按鈕,選擇之前保存過的棋譜檔案,進入打譜界面,可以通過上一步、下一步按鈕來還原之前對局的落子
注意:打開棋譜時,除了上圖的四個按鈕,其它多余的按鈕和文本都要隱藏或者清除掉,且只有對局結束后才能打開棋譜
其它功能
重繪

點擊重繪按鈕,重新從資料庫中分頁查詢當前所有在線玩家,將其顯示到棋盤上的指定位置,并給每個文本系結點擊事件,實作點擊之后可以發送對戰請求
上下頁


點擊上一頁、下一頁按鈕,從資料庫分頁查詢在線玩家并展示到棋盤上的指定位置,并給每個文本系結點擊事件,實作點擊之后可以發送對戰請求
注意:當上一頁沒有資料時,上一頁按鈕失效,同理,當下一頁沒有資料時,下一頁按鈕失效
認輸
點擊按鈕

確認提示框

提示對手

點擊認輸按鈕,顯示確認提示框,點擊確認,直接判負,發送認輸訊息和對戰結果訊息給對手,對手收到認輸訊息后,顯示贏棋提示框,并根據對戰結果保存結果和更新戰績表
退出


注意:在對戰時退出游戲,會直接判定為逃跑,同時發送逃跑訊息和對戰結果訊息通知對手,對手收到訊息后彈出贏棋提示框,根據對戰結果保存結果和更新戰績表
輪播圖片
點擊輪播按鈕前

點擊輪播按鈕后

點擊開始輪播按鈕,棋盤背景圖開始輪播,按鈕變成暫停輪播,再次點擊即可定格背景圖,輪播的速度和圖片的順序皆可隨便調整
背景音樂
暫停

播放

求助小棋仙



點擊求助小棋仙按鈕,會彈出確認提示框,并提示還有幾次求助機會,點擊確認,小棋仙機器人會分析當前局勢,得到最終落子的位置,然后幫玩家在該位置落子,
注意:游戲未開始或沒輪到該玩家落子時,求助按鈕無效
小棋仙的具體實作邏輯,請查看代碼決議
組織結構
gobang
├── com-wupgig-dao-- 資料庫層
├── com-wupgig-service-- 業務邏輯層
├── com-wupgig-pojo-- 資料庫表中對應的物體類
├── com-wupgig-login-- 登錄、注冊
├── com-wupgig-record-- 我的戰績、對手戰績
├── com-wupgig-chess-- 棋盤、棋子
├── com-wupgig-robot-- 小棋仙機器人
├── com-wupgig-meassage-- 訊息類
├── com-wupgig-common -- 工具類和通用代碼
└── com-wupgig-main-- 啟動類
核心代碼決議
com.wupgig.login.UserLogin
核心代碼:
// 記住賬號
public void rememberAccount() {
if (Global.myIP != null) {
// 通過ip查詢賬號
Address address = addressService.queryAccountByIP(Global.myIP);
// 如果資料庫中有這個賬號,則直接將這個賬號寫入賬號框
if (address != null) {
this.account.setText(address.getAccount());
}
}
}
// 記住密碼
public void isRememberPassword() {
Address address = addressService.queryAccountByIP(Global.myIP);
// 資料庫用戶地址表中有該賬號和ip地址
if (address != null) {
boolean isRemember = address.getRemember() == 1 ? true : false;
check.setSelected(isRemember);
// 如果用戶選擇了記住密碼
if (isRemember) {
// 記住密碼到密碼框
passwordField.setText(userService.queryUserByAccount(address.getAccount()).getPassword());
}
}
}
// 登錄邏輯
private void login(Pane pane) {
// 賬號或密碼不能為空
if ("".equals(account.getText()) || "".equals(passwordField.getText())) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號或密碼不能為空!");
alert.initOwner(this);
alert.show();
return;
}
// 根據輸入的賬號密碼查詢
User user = userService.queryUserByAccountAndPassword(account.getText(), passwordField.getText());
// 如果密碼正確或加密后的密碼正確,登錄成功,否則登錄失敗
if (user == null) {
// md5加密
String md5Password = MD5Util.digest(passwordField.getText());
User md5User = userService.queryUserByAccountAndPassword(account.getText(), md5Password);
if (md5User == null) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號或密碼輸入錯誤!");
alert.initOwner(this);
alert.show();
return;
} else {
// 判斷該玩家是否在線
Sinfo sinfo = sinfoService.queryIPByAccount(account.getText());
if (sinfo.getStatus() != 0) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號已在線,無法重復登錄!");
alert.initOwner(this);
alert.show();
return;
}
}
} else {
Sinfo sinfo = sinfoService.queryIPByAccount(account.getText());
if (sinfo.getStatus() != 0) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號已在線,無法重復登錄!");
alert.initOwner(this);
alert.show();
return;
}
}
// 將用戶賬號保存到Global類中
Global.account = account.getText();
// 查看用戶地址表中是否存在該ip和賬號
Address address = addressService.queryAccountByIP(Global.myIP);
// 不存在就保存到資料庫
if (address == null) {
Address saveAddress = new Address();
saveAddress.setAccount(Global.account);
saveAddress.setAddress(Global.myIP);
addressService.saveAddress(saveAddress);
// 存在且賬號不相同
} else if (!Global.account.equals(address.getAccount())) {
// 更新賬號
addressService.updateAccount(Global.myIP, Global.account);
}
// 將用戶是否記住密碼的選擇更新到資料庫
if (check.isSelected()) {
// 用戶選擇記住密碼
addressService.updateRemember(1, Global.account);
} else {
// 用戶選擇不記住密碼
addressService.updateRemember(0, Global.account);
}
Sinfo queryIPByAccount = sinfoService.queryIPByAccount(Global.account);
// 如果對應的賬號下的ip發生了改變,則更新他的ip和在線空閑狀態即可
if (!queryIPByAccount.getAddress().equals(Global.myIP)) {
// 更新用戶ip地址
sinfoService.updateIPByAccount(Global.account, Global.myIP);
// 更改在在線狀態為空閑
sinfoService.updateStatusByAccount(Global.account, 1);
// 如果對應的賬號下的ip沒變,則更改為在線空閑狀態即可
} else {
// 更改在在線狀態為空閑
sinfoService.updateStatusByAccount(Global.account, 1);
}
// 關閉登錄界面
this.close();
// 登錄后,關閉主界面
this.stage.close();
// 開啟server執行緒監聽對手客戶端在棋盤打開后發送的訊息
ServerThread serverThread = new ServerThread();
Thread boardThread = new Thread(serverThread);
boardThread.start();
// 打開棋盤界面
ChessBoard chessBoard = new ChessBoard();
chessBoard.show();
}
com.wupgig.login.UserRegister
核心代碼:
// 注冊
private void register(Pane pane) {
// 輸入框不能為空
if ("".equals(account.getText()) || "".equals(password.getText())
|| "".equals(confirmPassword.getText())) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號或密碼不能為空!");
alert.initOwner(this);
alert.show();
return;
}
// 密碼和確認密碼不一致
if (!(password.getText().equals(confirmPassword.getText()))) {
Alert alert = new Alert(AlertType.INFORMATION,"輸入的兩次密碼不一致!");
alert.initOwner(this);
alert.show();
return;
}
// 正則運算式規范賬號密碼
String patternAccount = "[\u4e00-\u9fa5_a-zA-Z0-9_]{1,15}";
String patternPassword = "[a-zA-Z0-9_]{6,15}";
boolean isPassword = Pattern.matches(patternPassword, password.getText());
boolean isAccount = Pattern.matches(patternAccount, account.getText());
if (!isAccount) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號需要為1-15位的中文,英文字母和數字及下劃線");
alert.initOwner(this);
alert.show();
return;
}
if (!isPassword) {
Alert alert = new Alert(AlertType.INFORMATION,"密碼需要為6-15位的英文字母和數字及下劃線");
alert.initOwner(this);
alert.show();
return;
}
// 賬號已經存在
String accountString = account.getText();
User user = userService.queryUserByAccount(accountString);
if (user != null) {
Alert alert = new Alert(AlertType.INFORMATION,"賬號已存在!!!");
alert.initOwner(this);
alert.show();
return;
}
// 將用戶資訊保存到資料庫中
User nowUser = new User();
nowUser.setAccount(accountString);
// md5加密密碼
nowUser.setPassword(MD5Util.digest(confirmPassword.getText()));
nowUser.setRegTime(new Timestamp(System.currentTimeMillis()));
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
JdbcUtils.disableAutocommit(conn);
userService.saveUser(nowUser);
// 保存離線用戶到資料庫
Sinfo sinfo = new Sinfo();
sinfo.setAccount(accountString);
sinfo.setAddress(Global.myIP);
sinfoService.saveSinfo(sinfo);
JdbcUtils.commit(conn);
} catch (Exception e) {
JdbcUtils.rollback(conn);
} finally {
if (conn != null) {
JdbcUtils.close(conn);
}
}
// 顯示登錄界面
UserLogin userLogin = new UserLogin();
userLogin.show();
// 關閉注冊界面
this.close();
}
com.wupgig.robot.RobotPlay
用于獲取機器人判斷出的落子坐標
實作原理
第一步:獲取當前棋盤上所有棋子附近不重復的空位(棋子周圍米字形所包含的空位位置即為棋子附近的空位),并將其以棋子物件的形式保存到集合中
第二步:為所有的空位打分,分數最高的那個空位即為小棋仙選擇的落子處,如果有多個位置的分數最高且相同,則隨機選擇一個位置落子,
第二步提到了一個為空位打分的概念,那么怎么打分呢?
為空位打分我們需要定義一張評分表作為評分的標準:
五子棋型及對應的分數

四子棋型及對應的分數

三子棋型及對應的分數

二子棋型及對應的分數

一子棋型及對應的分數

該評分表對五連、活四、沖四、死四、活三、沖三、死三、活二、沖二、死二、活一、沖一、死一的棋型分別給予了相應的分數,有興趣的可以將跳活的棋型和對應的分數加進去,得到的評分表會可以使小棋仙考慮得更加全面
有了評分表之后就可以對空位進行評分了
評分步驟
橫向掃描:
以空位的左側為原點,向左掃描
如果遇到空格,記錄下左側為空格,停止向左掃描
如果遇到己方棋子,棋子個數加1,繼續向左掃描
如果遇到對方棋子,記錄下左側為對方棋子,停止向左掃描
如果已到達最左側,記錄下左側為墻,停止向左掃描
以空位為原點,向右掃描
如果遇到空格,記錄下右側為空格,停止向右掃描
如果遇到己方棋子,棋子個數加1,繼續向右掃描
如果遇到對方棋子,記錄下右側為對方棋子,停止向右掃描
如果已到達最右側,記錄下右側為墻,停止向右掃描
根據形成的棋型,對比評分表,得到該空位的評分score1
縱向掃描:
原理和橫向一樣
根據形成的棋型,對比評分表,得到該空位的評分score2
左斜方向掃描:
原理和橫向一樣
根據形成的棋型,對比評分表,得到該空位的評分score3
右斜方向掃描:
原理和橫向一樣
根據形成的棋型,對比評分表,得到該空位的評分score4
那么該空位的評分即為score1+score2+score3+score4
這就是這個空位的最終評分了嗎?
仔細想想即可發現,該空位的評分只考慮了己方棋子的棋型,而完全沒有考慮到對方棋子的棋型
如果只根據這個評分所得到的最終落子位置,則完全只會考慮進攻,而不會防守
所以我們還需要讓小棋仙去判斷對方棋子的棋型,并將對方棋型的評分和己方棋型的評分相加,最終評分最高的空位即為最終落子的位置,可謂是攻防皆備
核心代碼
/**
* 獲取該點在橫向上的得分
* @param x 位置橫坐標
* @param y 位置縱坐標
* @param color 機器人落子的顏色
* @param colors 所有位置棋子的顏色
* @param size 橫縱棋子最大個數
* @return 評分
*/
private static int getYScore(int x, int y, Color color, Color[][] colors, int size) {
// 自己棋子的顏色
Color myself = color;
// 對方棋子的顏色
Color other = myself.equals(Color.BLACK) ? Color.ALICEBLUE : Color.BLACK;
// 模擬落子
colors[x][y] = myself;
//左側、右側的狀態,用來記錄棋型
int leftStatus = 0;
int rightStatus = 0;
// 相連棋子個數
int count = 0;
//掃描記錄棋型
for (int i = x; i < size; i++) {
if (myself.equals(colors[i][y]))
count++;
else {
if (colors[i][y] == null)
rightStatus = 1;// 右側為空
else if (other.equals(colors[i][y]))
rightStatus = 2;// 右側被對方堵住
break;
}
}
for (int i = x - 1; i >= 0; i--) {
if (myself.equals(colors[i][y]))
count++;
else {
if (colors[i][y] == null)
leftStatus = 1;// 左側為空
else if (other.equals(colors[i][y]))
leftStatus = 2;// 左側被對方堵住
break;
}
}
// 恢復
colors[x][y] = null;
return getScoreBySituation(count, leftStatus, rightStatus);
}
/**
* 根據棋型計算位置得分
* @param count 連子個數
* @param leftStatus 左側封堵情況 1:空位,2:對方或墻
* @param rightStatus 右側封堵情況 1:空位,2:對方或墻
* @return 分數
*/
private static int getScoreBySituation(int count, int leftStatus, int rightStatus) {
int score = 0;
// 五子情況
if (count >= 5)
score += 200000;// 贏了
// 四子情況
else if (count == 4) {
if (leftStatus == 1 && rightStatus == 1)
score += 50000;
if ((leftStatus == 2 && rightStatus == 1) || (leftStatus == 1 && rightStatus == 2))
score += 3000;
if (leftStatus == 2 && rightStatus == 2)
score += 1000;
}
//三子情況
else if (count == 3) {
if (leftStatus == 1 && rightStatus == 1)
score += 3000;
if ((leftStatus == 2 && rightStatus == 1) || (leftStatus == 1 && rightStatus == 2))
score += 1000;
if (leftStatus == 2 && rightStatus == 2)
score += 500;
}
//二子情況
else if (count == 2) {
if (leftStatus == 1 && rightStatus == 1)
score += 500;
if ((leftStatus == 2 && rightStatus == 1) || (leftStatus == 1 && rightStatus == 2))
score += 200;
if (leftStatus == 2 && rightStatus == 2)
score += 100;
}
//一子情況
else if (count == 1) {
if (leftStatus == 1 && rightStatus == 1)
score += 100;
if ((leftStatus == 2 && rightStatus == 1) || (leftStatus == 1 && rightStatus == 2))
score += 50;
if (leftStatus == 2 && rightStatus == 2)
score += 30;
}
return score;
}
/**
* 獲取需要打分的空位的集合
* 對每個非空位置,將其米字形周圍的空位添加到集合中
* 注意去掉重復的位置
* @param arr 用于判斷棋盤上指定坐標是否有棋子
* @param size 棋盤的橫豎線的條數
* @return 需要打分的空位的集合
*/
private static List<Chess> getallMayRobotChess(boolean[][] arr, int size) {
List<Chess> allMayRobotChess = new ArrayList<>();
// 搜索棋盤獲取可行棋的點,存在重復,
// 利用addToList(List<RobotChess> allMayRobotChess, int x, int y)去重
// 原理為,遍歷棋盤上所有棋子,其周圍米字形(九宮格除了中間的剩下八個)內的空位即為可行棋的點
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++) {
if (arr[i][j]) {
if (j != 0 && !arr[i][j - 1])
addToList(allMayRobotChess, i, j - 1);
if (j != (size - 1) && !arr[i][j + 1])
addToList(allMayRobotChess, i, j + 1);
if (i != 0 && j != 0 && !arr[i - 1][j - 1])
addToList(allMayRobotChess, i - 1, j - 1);
if (i != 0 && !arr[i - 1][j])
addToList(allMayRobotChess, i - 1, j);
if (i != 0 && j != (size - 1) && !arr[i - 1][j + 1])
addToList(allMayRobotChess, i - 1, j + 1);
if (i != (size - 1) && j != 0 && !arr[i + 1][j - 1])
addToList(allMayRobotChess, i + 1, j - 1);
if (i != (size - 1) && !arr[i + 1][j])
addToList(allMayRobotChess, i + 1, j);
if (i != (size - 1) && j != (size - 1) && !arr[i + 1][j + 1])
addToList(allMayRobotChess, i + 1, j + 1);
}
}
return allMayRobotChess;
}
/**
* 為坐標為(x,y)的空位評分
* @param x
* @param y
* @param color 機器人落子的顏色
* @param colors 所有棋子的顏色
* @param size 棋盤的橫豎線的條數
* @return 分數
*/
private static int getScore(int x, int y, Color color, Color[][] colors, int size) {
// 對方棋子顏色
Color otherColor = color.equals(Color.BLACK) ? Color.ALICEBLUE : Color.BLACK;
//己方棋子和對方棋子模擬落子計算分數和,以達到攻守皆備
// 縱向得分
int verticalScore = getVerticalScore(x, y, color, colors, size) + getVerticalScore(x, y, otherColor, colors, size);
// 橫向得分
int levelScore = getLevelScore(x, y, color, colors, size) + getLevelScore(x, y, otherColor, colors, size);
// 正斜得分
int skewScore1 = getSkewScore1(x, y, color, colors, size) + getSkewScore1(x, y, otherColor, colors, size);
// 反斜得分
int skewScore2 = getSkewScore2(x, y, color, colors, size) + getSkewScore2(x, y, otherColor, colors, size);
return verticalScore + levelScore + skewScore1 + skewScore2;
}
com.wupgig.chess.Chess
棋子類
/**
* 棋子類,里面包含棋子的顏色,和在棋盤上的x y坐標
* 和該棋子在小棋仙幫忙落子時評估的分數
*/
public class Chess {
// 棋子在棋盤上的x軸坐標
private int x;
// 棋子在棋盤上的y軸坐標
private int y;
// 棋子顏色
private Color color;
// 小棋仙對空位評估的分數
private int score;
public Chess(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
public Chess(int x, int y) {
this.x = x;
this.y = y;
}
// get、set方法
}
com.wupgig.chess.ChessBoard
棋盤類,所有類中最重要的一個類
下面會經常用到這個方法
NetUtils.sendMessage(message, oppoIP)
/**
* 客戶端給服務端發送訊息的工具類
*/
public class NetUtils {
/**
* 客戶端給服務器發送訊息
* @param message 需要發送的訊息
*/
public static void sendMessage(Message message, String oppoIP) {
try (Socket socket = new Socket(oppoIP, Global.oppoPort);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
oos.writeObject(message);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
Alert alert = new Alert(Alert.AlertType.ERROR, "連接對手出錯!請稍后再試!");
alert.showAndWait();
}
}
}
該方法用于客戶端給服務端發送訊息,即對戰雙方一方給另一方發送訊息,而在com.wupgig.chess.UserLogin類中已啟動服務端,代碼如下
// 開啟server執行緒監聽對手客戶端在棋盤打開后發送的訊息
ServerThread serverThread = new ServerThread();
Thread boardThread = new Thread(serverThread);
boardThread.start();
// 接受客戶端發送的訊息
public class ServerThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
ServerSocket serverSocket = null;
try {
// 創建服務器端的ServerSocket,指明自己的埠號
serverSocket = new ServerSocket(Global.myPort);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
// 出現例外后,終止該執行緒
return;
}
// 一直監聽客戶端的訊息
while (true) {
// 呼叫accept()表示接受來自客戶端的socket
try (Socket socket = serverSocket.accept()) {
// 獲取客戶端的輸入流
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
// 將流中的訊息物件讀取出來
Message message = (Message)ois.readObject();
// 處理訊息,指定將訊息發送到ChessBoard類中的upDateUI方法里面
Platform.runLater(() -> Global.chessBoard.upDateUI(message));
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
值得注意的是:
// 處理訊息,指定將訊息發送到ChessBoard類中的upDateUI方法里面
Platform.runLater(() -> Global.chessBoard.upDateUI(message));
這行代碼,會將服務端接受到的訊息傳到 com.wupgig.chess.ChessBoard中的 void upDateUI(Message message) 方法中,所以處理訊息的代碼,會寫在upDateUI方法中
顯示在線玩家
/**
* 分頁查詢所有在線玩家賬號名,并顯示在棋盤上
* @param index 分頁查詢起始索引
* @param size 每頁的數量
*/
private void queryAllAccountShowSinfo(int index, int size) {
// 分頁查詢所有在線用戶,一頁顯示兩個在線用戶
List<Sinfo> list = sinfoService.queryAllByLimit(index, size);
// 移除棋盤上的在線玩家
if (!textList.isEmpty()) {
pane.getChildren().removeAll(this.textList);
textList.clear();
}
this.showSinfo(list);
}
/**
* 顯示在線玩家的賬號名在棋盤右上方
* 并給每個顯示的玩家名系結點擊發送對戰請求事件
* @param list 所有在線玩家的集合
*/
private void showSinfo(List<Sinfo> list) {
int count = 0;
for (Sinfo sinfo : list) {
Text text = new Text(770, 160 + (count++ * 40),
sinfo.getAccount() + "(" + (sinfo.getStatus() == 1 ? "空閑" : "忙碌") + ")");
text.setFill(Color.MAGENTA);
text.setFont(Font.font("宋體",
FontPosture.REGULAR, 25));
// 加入文本集合
this.textList.add(text);
pane.getChildren().add(text);
//給每個顯示的玩家名系結點擊發送對戰請求事件
text.setOnMouseClicked(e -> {
// 游戲已經開始
if (!gameOver) {
return;
}
// 滑鼠點擊玩家賬號名后,從資料庫中重新查詢玩家當前在線狀態,
// 防止別的玩家棋盤展示的是之前的在線玩家,可能已經離線
Sinfo nowSinfo = sinfoService.queryIPByAccount(sinfo.getAccount());
// 已經發送過對戰請求
if (isSend) {
Alert alert = new Alert(AlertType.INFORMATION,"已經發送過對戰請求,請耐心等待!");
alert.initOwner(this);
alert.show();
return;
}
// 對方正在對戰中
if (nowSinfo.getStatus() == 2) {
Alert alert = new Alert(AlertType.INFORMATION, sinfo.getAccount() + "正在激戰中,請換個對手!");
alert.initOwner(this);
alert.show();
return;
}
// 對方離線
if (nowSinfo.getStatus() == 0) {
Alert alert = new Alert(AlertType.INFORMATION, sinfo.getAccount() + "已經離線,請換個對手!");
alert.initOwner(this);
alert.show();
return;
}
// 不能和自己對戰
if (sinfo.getAccount().equals(Global.account)) {
Alert alert = new Alert(AlertType.INFORMATION, "你個憨憨,點自己干吉爾!");
alert.initOwner(this);
alert.show();
return;
}
// 獲取對手ip
Global.oppoIP = sinfoService.queryIPByAccount(sinfo.getAccount()).getAddress();
// 取反
this.isSend = !isSend;
// 給對手發送對戰請求訊息
GameRequestMeaasge gameRequestMeaasge = new GameRequestMeaasge();
gameRequestMeaasge.setAccount(Global.account);
gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_REQUEST);
this.waitText.setText("已給" + sinfo.getAccount() + "發送對戰請求,請耐心等待……");
NetUtils.sendMessage(gameRequestMeaasge, Global.oppoIP);
// 請求對戰后由于未知原因會停止背景音樂,需要繼續播放背景音樂
mediaPlayer.play();
musicButton.setText("暫停音樂");
playMusic = !playMusic;
});
}
}
處理對戰請求
之前有個房間的概念:玩家一向玩家二發起對戰請求,玩家二同意后,此時可以理解為玩家一和玩家二在同一個房間,此局游戲結束后,他們還是在當前房間,直到有一方退出游戲或者和別的玩家開始對戰了,那么此時玩家一和玩家二才不在同一個房間
這里有個臨時對手ip的概念,就是為了能讓同一房間的玩家點擊新局按鈕后能夠在來一局,當玩家一和玩家二下完一盤棋后,玩家二又去和玩家三開始下棋,此時玩家二就需要通知玩家一,我退出房間了啊,趕緊把我的臨時ip清掉,我們已經不可能通過新局再次開始游戲了,別了~
同理,游戲結束后,在同一房間里還能聊天也是通過臨時ip實作的
值得注意的是,不加個臨時ip的話,上述功能也完全能實作,不過需要判斷的邏輯就會繁瑣很多,所以最終我選擇了添加臨時ip
// 如果是對戰請求訊息
else if (message instanceof GameRequestMeaasge) {
this.gameRequestMeaasge(message);
}
/**
* 對戰請求訊息
* @param message
*/
private void gameRequestMeaasge(Message message) {
GameRequestMeaasge gameRequestMeaasge = (GameRequestMeaasge)message;
// 如果是請求訊息
if (gameRequestMeaasge.getRequestType() == GameRequestMeaasge.GAME_REQUEST) {
if (this.isAccept) {
// 我方正在接受對戰請求
// 不同意
gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_REFUSE);
// 發送訊息
NetUtils.sendMessage(gameRequestMeaasge, sinfoService.queryIPByAccount(gameRequestMeaasge.getAccount()).getAddress());
return;
}
// 已接受對戰請求
this.isAccept = true;
// 更新對手ip
Global.oppoIP = sinfoService.queryIPByAccount(gameRequestMeaasge.getAccount()).getAddress();
Alert alert = new Alert(AlertType.CONFIRMATION, gameRequestMeaasge.getAccount() + "請求一戰,是否給個面子?",
new ButtonType("拒絕", ButtonData.NO),
new ButtonType("同意", ButtonData.YES));
alert.initOwner(this);
Optional<ButtonType> button = alert.showAndWait();
// 如果同意
if (button.get().getButtonData() == ButtonData.YES) {
this.stopThread();
// 告訴原先對手,讓他死心(清除臨時對手ip)
if (Global.temporaryOppoIP != null) {
// 發送訊息
NetUtils.sendMessage(new EscapeMessage(), Global.temporaryOppoIP);
}
// 更新臨時對手ip
Global.temporaryOppoIP = Global.oppoIP;
// 隨機選擇棋子顏色
this.selectColor();
// 游戲初始化
this.startNew(gameRequestMeaasge.getAccount());
// 將自己的賬號放入訊息類中
gameRequestMeaasge.setAccount(Global.account);
// 發送訊息
gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_AGRRE);
NetUtils.sendMessage(gameRequestMeaasge, Global.oppoIP);
} else {
// 更新對手ip
Global.oppoIP = sinfoService.queryIPByAccount(gameRequestMeaasge.getAccount()).getAddress();
// 拒絕后變為沒接受對戰請求
this.isAccept = false;
// 如果不同意
gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_REFUSE);
// 發送訊息
NetUtils.sendMessage(gameRequestMeaasge, Global.oppoIP);
// 移除對手ip
Global.oppoIP = null;
}
// 同意對戰請求
} else if (gameRequestMeaasge.getRequestType() == GameRequestMeaasge.GAME_AGRRE) {
this.stopThread();
// 告訴原先對手,讓他死心,我不在愛你了(清除臨時對手ip)
if (Global.temporaryOppoIP != null) {
// 發送訊息
NetUtils.sendMessage(new EscapeMessage(), Global.temporaryOppoIP);
}
// 更新臨時對手ip
Global.temporaryOppoIP = Global.oppoIP;
// 初始化資料
this.startNew(gameRequestMeaasge.getAccount());
Alert alert = new Alert(AlertType.INFORMATION);
alert.setContentText(gameRequestMeaasge.getAccount() + "同意對戰,開始游戲!");
alert.initOwner(this);
alert.show();
// 拒絕對戰請求
} else if (gameRequestMeaasge.getRequestType() == GameRequestMeaasge.GAME_REFUSE) {
// 拒絕后,改回請求對戰狀態
this.isSend = !isSend;
// 清除對手ip
Global.oppoIP = null;
// 移除
this.waitText.setText("");
Alert alert = new Alert(AlertType.INFORMATION);
alert.setContentText("對方不給面子,拒絕了你的請求!");
alert.initOwner(this);
alert.show();
}
}
己方落子
/**
* 滑鼠點擊棋盤后的邏輯
*/
private void mouseClikedChessboard() {
pane.setOnMouseClicked(e -> {
// 游戲開始且輪到你落子
if (!gameOver && isPlay) {
double x = e.getX();
double y = e.getY();
// 當 點擊的x 或 y 坐標超出棋盤范圍時,落子無效,設定10的偏移量
if (x < 40 || x > 630 || y < 40 || y > 620) {
return;
}
// 給棋盤上的橫軸交叉的點定義坐標
int xIndex = (int)Math.round((x - 50) / 40);
int yIndex = (int)Math.round((y - 50) / 40);
// 把棋子加入到棋盤中
this.piece(xIndex, yIndex);
}
});
}
/**
* 落子
* @param x
* @param y
*/
private void piece(int x, int y) {
if (chessList.size() == SIZE * SIZE) {
System.out.println("棋盤已滿,游戲結束");
// 平局
// 給對手發送訊息,讓他更新資料庫資訊
ResultMessage resultMessage = new ResultMessage();
// 根據當前用戶的棋子的顏色設定訊息類結果屬性
resultMessage.setResult(Color.BLACK.equals(this.color) ?
ResultMessage.BLACK_DRAW : ResultMessage.WHITE_DRAW);
// 顯示提示框
chessFullReminder();
return;
}
// 判斷下子是否重復
if (arr[x][y]) {
System.out.println("同一坐標重復落子,無效!");
return;
}
// 播放落子聲
this.soundMoveLater();
// 局時倒計時暫停
gameTimeline.pause();
// 暫停并重置步時
stepTimeline.pause();
this.stepTimeText.setText("步時 60");
this.stepTimeNum = 60;
// 去除上一個紅色的標志
if (!chessList.isEmpty()) {
pane.getChildren().remove(redCircle);
}
// 當前落子文本后面棋子的顏色
nowChess.setFill(isBlack ? Color.ALICEBLUE: Color.BLACK);
// 落完一子后,要先等對手落子,才能繼續落子
isPlay = !isPlay;
arr[x][y] = true;
int tempX = x * LINE_SPACING + 50;
int tempY = y * LINE_SPACING + 50;
// 繪制棋子
Circle circle = new Circle();
// 棋子落點的x坐標
circle.setCenterX(tempX);
// 棋子落點的y坐標
circle.setCenterY(tempY);
// 設定棋子的顏色
circle.setFill(this.color);
// 將棋子的顏色記錄到陣列colors 中
colors[x][y] = this.color;
// 設定棋子的半徑
circle.setRadius(CHESS_RADIUS);
// 把棋子加入到棋盤中
pane.getChildren().add(circle);
// 標志落點的x坐標
redCircle.setCenterX(tempX);
// 標志落點的y坐標
redCircle.setCenterY(tempY);
// 設定為紅色
redCircle.setFill(Color.RED);
// 把標志加入到棋盤中
pane.getChildren().add(redCircle);
// 將棋子的資訊保存到陣列中
Chess chess = new Chess(x, y, this.color);
chessList.add(chess);
// 更換棋子顏色
isBlack = !isBlack;
// 給對手發送該落子的資訊
NetUtils.sendMessage(new ChessMessage(x, y,this.blackOrWhite), Global.oppoIP);
// 出現五連子,結束游戲
if (isWin(chess)) {
// 局時倒計時停止
gameTimeline.pause();
// 步時倒計時停止
stepTimeline.pause();
// 出現五連,給對手發送訊息,對手將更新資料庫
ResultMessage resultMessage = new ResultMessage();
// 設定為不是認輸
resultMessage.setLose(false);
resultMessage.setAccount(Global.account);
resultMessage.setResult(Color.BLACK.equals(this.color) ?
ResultMessage.BLACK_WIN : ResultMessage.WHITE_WIN);
// 發送對戰結果訊息
NetUtils.sendMessage(resultMessage, Global.oppoIP);
// 清除對手ip
Global.oppoIP = null;
// 清除棋盤上雙方VS的文字和后面的棋子
// 重新添加重繪、上一頁和下一頁按鈕
this.removeEndTextAndCircle();
// 顯示對局結束彈窗
this.gameOverReminder("勝利");
}
}
顯示對手的落子
// 在自己的棋盤上顯示對手下的棋子
if (message instanceof ChessMessage) {
this.chessMessage(message);
}
/**
* 在自己的棋盤上顯示對手下的棋子
* @param message 對手發送的棋子訊息
*/
private void chessMessage(Message message) {
// 播放落子聲
this.soundMoveLater();
// 局時倒計時開始
this.gameTimeline.play();
// 步時倒計時開始
this.stepTimeline.play();
// 去除上一個紅色的標志
if (!chessList.isEmpty()) {
pane.getChildren().remove(redCircle);
}
// 設定當前落子文本后面棋子的顏色
nowChess.setFill(isBlack ? Color.ALICEBLUE: Color.BLACK);
// 對手下完棋后,自己可以下棋了
this.isPlay = true;
ChessMessage chessMessage = (ChessMessage)message;
// 獲取對手棋子的坐標和顏色
int x = chessMessage.getX();
int y = chessMessage.getY();
Color nowColor = chessMessage.getBlackOrWhite() == 1 ? Color.BLACK : Color.ALICEBLUE;
Circle circle = new Circle(x * 40 + 50,
y * 40 + 50,CHESS_RADIUS);
circle.setFill(nowColor);
// 如果對手是黑棋,那么自己就是白棋
if (chessMessage.getBlackOrWhite() == 1) {
this.blackOrWhite = 0;
this.color = Color.ALICEBLUE;
}
// 更新當前棋子資訊
isBlack = !isBlack;
// 記錄對手下的棋的資訊
arr[x][y] = true;
colors[x][y] = nowColor;
Chess chess = new Chess(x, y, nowColor);
chessList.add(chess);
pane.getChildren().add(circle);
// 標志落點的x坐標
redCircle.setCenterX(x * 40 + 50);
// 標志落點的y坐標
redCircle.setCenterY(y * 40 + 50);
// 設定為紅色
redCircle.setFill(Color.RED);
// 把標志加入到棋盤中
pane.getChildren().add(redCircle);
// 對手五連,結束游戲
if (this.isWin(chess)) {
// 局時倒計時停止
gameTimeline.pause();
// 步時倒計時停止
stepTimeline.pause();
// 清除對手ip
Global.oppoIP = null;
// 清除棋盤上雙方VS的文字和后面的棋子
// 重新添加重繪、上一頁和下一頁按鈕
this.removeEndTextAndCircle();
this.gameOverReminder("失敗");
}
// 棋盤已滿
if (chessList.size() == SIZE * SIZE) {
// 提示框
this.chessFullReminder();
}
}
其它
由于篇幅太長了,其它的代碼就不繼續往這里放了,完整原始碼已上傳GitHub,需要的直接過去下載即可,Eclipse 和IDEA兩個版本都有,鏈接在文章的最后面
環境搭建
開發工具
| 工具 | 說明 | 官網 |
|---|---|---|
| IDEA | 最好的java開發工具 | https://www.jetbrains.com/idea/download |
| Eclipse | 開源的java開發工具 | https://www.eclipse.org/downloads/ |
| Navicat | 資料庫連接工具 | http://www.formysql.com/xiazai.html |
| PowerDesigner | 資料庫設計工具 | http://powerdesigner.de/ |
| Xmind | 思維導圖設計工具 | https://www.xmind.cn/ |
| ProcessOn | 流程圖繪制工具 | https://www.processon.com/ |
| Typora | Markdown編輯器 | https://typora.io/ |
| 螢屏截圖工具 | https://im.qq.com/ | |
| Snipaste | 螢屏截圖工具 | https://www.snipaste.com/ |
開發環境
| 工具 | 版本號 | 下載 |
|---|---|---|
| JDK | 1.8 | https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html |
| MySQL | 5.7 | https://downloads.mysql.com/archives/installer/ |
搭建步驟
Windows 環境部署
IDEA
- 關于IDEA的安裝與使用請參考:https://github.com/judasn/IntelliJ-IDEA-Tutorial
- 將專案下載到本地,然后直接打開:


Eclipse
- 關于Eclipse的安裝與使用請參考:https://blog.csdn.net/rothschild666/article/details/82914600?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=1330144.8071.16180379552492035&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
- 將專案下載到本地,然后直接打開:


MySQL
- 下載并安裝mysql5.7版本,下載地址:https://dev.mysql.com/downloads/installer/
- 設定資料庫帳號密碼:root root
- 下載并安裝客戶端連接工具Navicat,下載地址:http://www.formysql.com/xiazai.html
- 創建資料庫 gobang
- 匯入gobang/sql下的gobang.sql檔案
啟動專案
-
將src下的db.properties檔案中url后面的ip和埠改成你自己的主機ip和mysql的埠號
-
將com.wupgig.common.Global類中的myPort(我的埠號) 和oppoPort (別人的埠號) 都設定為主機一般不會被占用的埠比如 8088 (兩個埠設定要相同,這是不同電腦之間對戰的設定)
-
如果你想在你自己的電腦上啟動兩個五子棋程式,并讓他們兩個程式之間進行對戰,可以在第一次啟動的時候將myPort設定為8088,oppoPort設定為8089,第二次啟動的時候myPort設定為8089,oppoPort設定為8088,那么即可在同一臺電腦上開始對戰了(不一定非得8088、和8089,只要保證兩個埠沒被占用,且兩次啟動的埠反過來設定即可)
-
不同電腦之間的對戰必須保證你的MySQL打開了遠程訪問權限,
執行sql陳述句:grant all privileges on . to ‘root’@’%’ identified by ‘root’ with grant option;flush privileges;即可開放MySQL的遠程訪問權限
注意:前面的root為賬號名,后面的root為密碼 -
不同電腦之間的對戰需要關閉電腦的防火墻或者打開相關埠的遠程權限
-
由于上網所用的ip地址基本都是路由器或者運營商提供的局域網ip地址,這種ip地址是不能在外網直接訪問到的,即不同電腦之間的對戰只能在同一局域網中,如果想要在不同的網路下實作聯機對戰,可以使用一些工具對ip進行內網穿透,至于怎么做內網穿透,請自行百度
-
最后運行com.wupgig.main.GobangMainApplication的main方法即可
完整原始碼
Eclipse版本
GitHub地址:https://github.com/wupgig/GoBang-Eclipse
IDEA版本
GitHub地址:https://github.com/wupgig/Gobang-IDEA
END
最后:如果對代碼有任何疑問可以直接在評論區留言,看到了就會及時回復,當然,這個代碼肯定會存在一些問題,歡迎發現問題的朋友在評論區指正,大家共同進步的哈
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/276277.html
標籤:其他
上一篇:圖的題目基礎類與方法
下一篇:回圈結構程式設計練習2
