目录

  • 前言
  • 项目介绍
  • 功能演示
    • 登录
    • 注册
    • 选择对手
    • 落子提示
    • 局时步时
    • 查看战绩
    • 落子五连
    • 悔棋
    • 聊天
    • 新局
    • 棋谱
      • 保存棋谱
      • 打开棋谱
    • 其它功能
      • 刷新
      • 上下页
      • 认输
      • 退出
      • 轮播图片
      • 背景音乐
    • 求助小棋仙
  • 组织结构
  • 核心代码解析
    • 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 exceptione.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{@Overridepublic void run() {// TODO Auto-generated method stubServerSocket serverSocket = null;try {// 创建服务器端的ServerSocket,指明自己的端口号serverSocket = new ServerSocket(Global.myPort);} catch (Exception e) {// TODO: handle exceptione.printStackTrace();// 出现异常后,终止该线程return;}// 一直监听客户端的消息while (true) {// 调用accept()表示接受来自客户端的sockettry (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 exceptione.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;}// 获取对手ipGlobal.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;// 更新对手ipGlobal.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);}// 更新临时对手ipGlobal.temporaryOppoIP = Global.oppoIP;// 随机选择棋子颜色this.selectColor();// 游戏初始化this.startNew(gameRequestMeaasge.getAccount());// 将自己的账号放入消息类中gameRequestMeaasge.setAccount(Global.account);// 发送消息gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_AGRRE);NetUtils.sendMessage(gameRequestMeaasge, Global.oppoIP);} else {// 更新对手ipGlobal.oppoIP = sinfoService.queryIPByAccount(gameRequestMeaasge.getAccount()).getAddress();// 拒绝后变为没接受对战请求this.isAccept = false;// 如果不同意gameRequestMeaasge.setRequestType(GameRequestMeaasge.GAME_REFUSE);// 发送消息NetUtils.sendMessage(gameRequestMeaasge, Global.oppoIP);// 移除对手ipGlobal.oppoIP = null;}// 同意对战请求} else if (gameRequestMeaasge.getRequestType() == GameRequestMeaasge.GAME_AGRRE) {this.stopThread();// 告诉原先对手,让他死心,我不在爱你了(清除临时对手ip)if (Global.temporaryOppoIP != null) {// 发送消息NetUtils.sendMessage(new EscapeMessage(), Global.temporaryOppoIP);}// 更新临时对手ipGlobal.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;// 清除对手ipGlobal.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);// 清除对手ipGlobal.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();// 清除对手ipGlobal.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/
qq 屏幕截图工具 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
别忘了给个start哟 ̄▽ ̄

END

最后:如果对代码有任何疑问可以直接在评论区留言,博主看到了就会及时回复,当然,这个代码肯定会存在一些问题,欢迎发现问题的朋友在评论区指正,大家共同进步的哈(如果能动动你们发财的小手点一个小赞就再好不过了,谢谢~)

java实现注册登录版五子棋对战平台(超详细注释,内含人机实现)相关推荐

  1. java security 详解_Spring Security入门教程 通俗易懂 超详细 【内含案例】

    Spring Security的简单使用 简介 SSM 整合 Security 是比较麻烦的,虽然Security的功能比 Shiro 强大,相反却没有Shiro的使用量多 SpringBoot出现后 ...

  2. 二改注册登录版素材代下载搜索引擎系统源码,自带火车头采集

    简介: 二改注册登录版/素材代下载搜索引擎系统/自带火车头采集接口/源码素材付费下载系统 程序原创开发,对接易,码,百度搜狗关键词:源码代下,代下源码,均在首页 缺点:这个用户登录必须对接QQ互联登录 ...

  3. PMD【 Java 代码检查工具】入门使用教程(超详细)

    PMD[ Java 代码检查工具] 介绍 使用方式 1.使用插件的方式 2.maven项目引入依赖的方式 3.pmd 命令行的方式 4.Java API的方式 * 项目结构 测试代码 pmdArgs方 ...

  4. Java实现注册登录系统——基于Java Swing实现

    技术简介:Java Swing 介绍 Swing 是一个为Java设计的GUI工具包. Swing是JAVA基础类的一部分. Swing包括了图形用户界面(GUI)器件如:文本框,按钮,分隔窗格和表. ...

  5. JAVA课设单人版五子棋小游戏

    内容介绍:该程序为Java课设的单人版五子棋小游戏,通过eclipse编辑,实现了动作事件的监听与处理,以及JavaSwing的界面编程.  编辑排行榜,包含局数,结果,步数,以及"关于我们 ...

  6. 2023版软件测试学习路线图(超详细自学路线)

    送福利了!超详细的软件测试学习路线图来啦,2023版是首发哟!软件测试学习路线图分为9个阶段,包含: 软件测试环境配置和管理-->软件测试数据管理与数据库测试-->web前端测试技术--& ...

  7. project项目导入java路径_Eclipse项目怎么导入IDEA并运行(超详细)

    导入项目 集成环境:intellij idea 2020.1.2 演示系统:dell windows 10 eclipse项目如何导入idea并成功运行,从头到尾步骤,保姆式图解如下: 首先准备好一个 ...

  8. 使用java实现注册登录信息验证

    编写java工具包,用来验证字符串格式和获取登录注册验证码.然后再编写注册窗口,实现注册验证功能. 一.编写工具包 1.编写字符串验证类 考虑到进行字符串验证时,用户会根据不同需求从而需要不同的验证方 ...

  9. Java实现注册登录代码通用

    注册逻辑 用户在前端输入账户和密码.校验码 校验用户的账户.密码.校验密码是否符合要求 非空 账户不小于4位 密码不小于8位 账户不能包含特殊字符 账户不能重复 密码和校验密码相同 对密码进行加密(不 ...

最新文章

  1. 批量更新zabbix中的主机名
  2. UA OPTI570 量子力学 原子结构基础 公式与结论总结
  3. AS3 鼠标指针样式
  4. android usb集线器,通过Maven将多个部署到USB集线器上的Android设备
  5. halcon对光源打光不均匀进行平场矫正
  6. 安卓开发面试技能介绍,来一份全面的面试宝典练练手,不吃透都对不起自己
  7. ue4集合类型_UE4项目问题集合
  8. 事务的隔离级别(Transaction isolation levels)5
  9. PyQt5多线程的执行和停止
  10. 【转】DBMS_STATS.GATHER_TABLE_STATS详解
  11. 计算机网关,如何查看计算机的IP地址和网关
  12. package.json 入门
  13. 八皇后问题程序及注解
  14. JQuery 实战第三讲:绚丽菜单
  15. linux 传真 邮件,基于Linux的传真系统(转)
  16. 企业网中nextcloud与iRedmail邮件系统的配合
  17. 最小生成树 无根树转有根树 树形动态规划
  18. 实现app第一次安装浮层引导View!
  19. 有道 linux 安装路径,「Linux」- 安装有道词典
  20. 我爱计算机:张俊林专访

热门文章

  1. jquery中的find()和next()的用法及区别
  2. java8 Arrays.sort 与Arrays.parallelSort
  3. kwm转mp3,kwm转flac
  4. 在微型计算机中任何外部设备,微型计算机与外部设备之间的信息传输方式有( )。...
  5. TensorFlow1.14或TensorFlow2内部获取mfcc原理探索(matlab复现或python复现)
  6. 多元(二元)函数极限的存在性问题
  7. PAT乙级全题解存档
  8. MySQL安装与应用
  9. 【Tableau 图表大全21】之箱型图(盒须图)
  10. echarts-箱线图(盒须图)