当前位置:主页 > 查看内容

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

发布时间:2021-07-06 00:00| 位朋友查看

简介:目录 前言 项目介绍 功能演示 登录 注册 选择对手 落子提示 局时步时 查看战绩 落子五连 悔棋 聊天 新局 棋谱 保存棋谱 打开棋谱 其它功能 刷新 上下页 认输 退出 轮播图片 背景音乐 求助小棋仙 组织结构 核心代码解析 com.wupgig.login.UserLogin com.wupgi……

前言

这个落子难道真的没得选择了吗?不!我不能输!
出来吧!宇宙究极无敌巴啦啦小棋仙~


项目介绍

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/
TyporaMarkdown编辑器https://typora.io/
qq屏幕截图工具https://im.qq.com/
Snipaste屏幕截图工具https://www.snipaste.com/

开发环境

工具版本号下载
JDK1.8https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
MySQL5.7https://downloads.mysql.com/archives/installer/

搭建步骤

Windows 环境部署
IDEA


Eclipse


MySQL


启动项目

  • 将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://blog.csdn.net/uirlvelo/article/details/115531897
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:循环结构程序设计练习2 下一篇:没有了

推荐图文


随机推荐