五子棋对战(网页版)

目录

一、项目背景

用户模块

匹配模块

对战模块

二、核心技术

三、相关知识

WebSocket

原理

报文格式

代码

服务器代码

客户端代码

四、项目创建

4.1、实现用户模块

编写数据库代码

数据库设计

配置MyBatis

创建实体类

创建UserMapper

创建UserMapper接口

实现UserMapper.xml

前后端接口交互

登录接口

请求

响应

注册接口

请求

响应

获取用户信息

请求

响应

服务器开发

客户端开发

登录页面

注册页面

4.2、实现匹配模块

前后端接口交互

客户端开发

实现页面基本结构

实现匹配功能

服务器开发

创建并注册MatchAPI类

实现用户管理器

创建匹配请求/响应对象

处理上线下线状态

处理开始匹配/取消匹配请求

实现匹配器

创建房间类

实现房间管理器

实现匹配器

4.3、实现对战模块

前后端交互接口

客户端开发

实现页面基本结构

实现棋盘绘制

初始化websocket

发送落子请求

处理落子响应

服务器开发

创建落子请求/响应对象

处理连接成功

玩家下线的处理

处理落子请求

修改Room类

实现对弈功能

处理途中玩家掉线

更新玩家分数

五、部署云服务器

构造数据库中的数据

调整websocket建立连接的url

打包上传

通过外网访问

六、后续扩展功能

计时

保存棋谱/录像回放

观战功能

界面聊天

人机对战


一、项目背景

用户模块

用户的注册和登录

管理用户的天梯分数,比赛场数,获胜场数等信息

匹配模块

依据用户的天梯积分,实现匹配机制

对战模块

把两个匹配到的玩家放到一个游戏房间中,对方通过网页的形式来进行对战比赛

二、核心技术

Spring/SpringBoot/SpringMVC

WebSocket

MySQL

MyBatis

HTML/CSS/JS/Ajax

三、相关知识

WebSocket

原理

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

报文格式

代码

spring内置websocket,可以直接进行使用

服务器代码

新建api.TestAPI类

用来处理websocket请求,并返回响应(websocket内置一组session,通过这个session可以给客户端返回数据,或者主动断开连接)


@Component
public class TestAPI extends TextWebSocketHandler {@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("连接成功");}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println("接收消息:"+message.getPayload());}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("连接异常");}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("连接关闭");}
}

创建config.WebSocketConfig类

这个类用来配置请求路径和TextWebSocketHandler之间的关系


@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(testAPI,"/test");}
}
客户端代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>TestAPI</title>
</head>
<body><input type="text" id="message"><button id="submit">提交</button><script>//创建websocket实例let websocket=new WebSocket("ws://127.0.0.1:8080/test");//给实例挂一些回调函数websocket.onopen=function(){console.log("建立连接!");}websocket.onmessage=function(e){console.log("收到消息!"+e.data);}websocket.onerror=function(){console.log("连接异常!");}websocket.onclose=function(){console.log("连接关闭!");}//实现点击按钮后,通过websocket发送请求let input=document.querySelector('#message');let button=document.querySelector('#submit');button.onclick=function(){console.log("发送消息"+input.value);websocket.send(input.value);}</script>
</body>
</html>

四、项目创建

4.1、实现用户模块

编写数据库代码

数据库设计

创建user表,表示用户信息和分数信息


create database if not exists java_gobang;use java_gobang;drop table if exists user;
create table user(userId int primary key auto_increment,username varchar(50) unique,password varchar(50),score int,  --天梯分数totalCount int,  --比赛总场次winCount int  --获胜场次
);insert into user value(null,'baekhyun','2012',1000,0,0);
insert into user value(null,'DO','2012',1000,0,0);
insert into user value(null,'sehun','2012',1000,0,0);
insert into user value(null,'sohu','2012',1000,0,0);
insert into user value(null,'chanyeol','2012',1000,0,0);
insert into user value(null,'kai','2012',1000,0,0);
配置MyBatis

创建application.yml


# 配置数据库的连接字符串
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&&useSSL=falseusername: rootpassword: "19930112"driver-class-name: com.mysql.cj.jdbc.Driver#
mybatis:mapper-locations: classpath:mapper/**Mapper.xml
创建实体类

public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public int getScore() {return score;}public void setScore(int score) {this.score = score;}public int getTotalCount() {return totalCount;}public void setTotalCount(int totalCount) {this.totalCount = totalCount;}public int getWinCount() {return winCount;}public void setWinCount(int winCount) {this.winCount = winCount;}
}
创建UserMapper
创建UserMapper接口

package com.example.java_gobang.model;@Mapper
public interface UserMapper {//根据用户名来查询用户的信息,用于登录功能User selectByName(String username);//往数据库里插入一个用户,用于注册功能void insert(User user);
}
实现UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.model.UserMapper"><select id="selectByName" resultType="com.example.java_gobang.model.User">select * from user where username=#{username};</select><insert id="insert">insert into user values(null,#{username},#{password},1000,0,0);</insert></mapper>

前后端接口交互

登录接口
请求
POST /login HTTP/ 1.1
Content-Type: application/x-www-form-urlencoded
username=baekhyun&password=2012
响应
HTTP/ 1.1 200 OK
Content-Type: application/json

{
userId: 1,
username: 'baekhyun',
score: 1000,
totalCount: 0,
winCount: 0
}

如果登录失败, 返回的是一个无效的user对象

注册接口
请求
POST /register HTTP/ 1.1
Content-Type: application/x-www-form-urlencoded
username=baekhyun&password=2012
响应
HTTP/ 1.1 200 OK
Content-Type: application/json

{
userId: 1,
username: 'baekhyun',
score: 1000,
totalCount: 0,
winCount: 0
}
获取用户信息
请求
GET /userInfo HTTP/ 1.1
响应
HTTP/ 1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: 'baekhyun',
score: 1000,
totalCount: 0,
winCount:0
}

服务器开发

实现三种方法:

  1. login:用来实现登录逻辑;

  1. register:用来实现注册逻辑;

  1. getUserInfo:用来实现登录成功后显示用户分数的信息


@RestController
public class UserAPI {@Resourceprivate UserMapper userMapper;@PostMapping("/login")@ResponseBodypublic Object login(String username, String password, HttpServletRequest req){//根据username在数据库中进行查询//如果找到匹配的用户,并且密码也一致,就认为登录成功User user= userMapper.selectByName(username);System.out.println("[login] username="+username);if (user==null || !user.getPassword().equals(password)){System.out.println("登录失败!");return new User();}HttpSession httpsession=req.getSession(true);httpsession.setAttribute("user",user);return user;}@PostMapping("/register")@ResponseBodypublic Object register(String username,String password){try {User user=new User();user.setUsername(username);user.setPassword(password);userMapper.insert(user);return user;}catch (org.springframework.dao.DuplicateKeyException){User user=new User();return user;}}@GetMapping("/userinfo")@ResponseBodypublic Object getUserInfo(HttpServletRequest req){try {HttpSession httpSession=req.getSession(false);User user=(User) httpSession.getAttribute("user");return user;}catch (NullPointerException e){return new User();}}
}

客户端开发

登录页面

login.html


<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/login.css">
</head>
<body><div class="nav">五子棋对战</div><div class="login-container"><div class="login-dialog"><!-- 标题 --><h3>登录</h3><!-- 输入用户名 --><div class="row"><span>用户名</span><input type="text" id="username"></div><!-- 输入密码 --><div class="row"><span>密码</span><input type="password" id="password"></div><!-- 提交按钮 --><div class="row"><button id="submit">提交</button> </div></div>  </div>
</body>
</html>

common.css


* {margin: 0;padding: 0;box-sizing: border-box;
}html, body {height: 100%;background-image: url(../image/1.png);background-repeat: no-repeat;background-position: center;background-size: cover;
}.nav {width: 100%;height: 50px;background-color: rgb(51, 51, 51);color: white;display: flex;align-items: center;line-height: 50px;padding-left: 20px;
}.container {height: calc(100% - 50px);width: 100%;display: flex;justify-content: center;align-items: center;background-color: rgba(255, 255, 255, 0.7);
}

login.css


.login-container {width: 100%;height: calc(100% - 50px);display: flex;justify-content: center;align-items: center;
}.login-dialog {width: 400px;height: 320px;background-color: rgba(255, 255, 255, 0.8);border-radius: 10px;
}.login-dialog h3 {text-align: center;padding: 50px 0;
}.login-dialog .row {width: 100%;height: 50px;display: flex;justify-content: center;align-items: center;
}.login-dialog .row span {display: block;/* 设置固定宽度, 能让文字和后面的输入框之间有间隙 */width: 100px;font-weight: 700;
}.login-dialog #username,
.login-dialog #password {width: 200px;height: 40px;font-size: 20px;text-indent: 10px;border-radius: 10px;border: none;outline: none;
}.login-dialog .submit-row {margin-top: 10px;
}.login-dialog #submit {width: 300px;height: 50px;color: white;background-color: rgb(133, 23, 23);border: none;border-radius: 10px;font-size: 20px;
}.login-dialog #submit:active {background-color: #666;
}

通过 jQuery 中的 AJAX 和服务器进行交互(在login.html中写js)


    <script src="./js/jquery.min.js"></script><script>let usernameInput=document.querySelector("#username");let passwordInput=document.querySelector("#password");let submitButton=document.querySelector("#submit");submitButton.onclick=function(){$.ajax({type: 'post',url: '/login',data:{username:usernameInput.value,password:passwordInput.value,},success:function(body){//请求执行成功的回调函数//判定当前是否登录成功//如果登录成功,服务器会返回当前的user对象//如果登录失败,服务器则会返回一个空的user对象if(body && body.userId>0){//登录成功alert("登录成功");//重定向跳转到游戏大厅页面location.assign('/game_hall.html');}else{alert("登录失败!");}},error:function(){//请求执行失败的回调函数alert("登录失败!");}});}</script>
注册页面

register.html


<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>注册</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/login.css">
</head>
<body><div class="nav">五子棋对战</div><div class="login-container"><div class="login-dialog"><!-- 标题 --><h3>注册</h3><!-- 输入用户名 --><div class="row"><span>用户名</span><input type="text" id="username"></div><!-- 输入密码 --><div class="row"><span>密码</span><input type="password" id="password"></div><!-- 提交按钮 --><div class="row"><button id="submit">提交</button> </div></div>  </div>
</body>
</html>

4.2、实现匹配模块

前后端接口交互

连接

ws://127.0.0.1:8080/findMatch

请求

{ message: 'startMatch' / 'stopMatch',}

响应1(收到请求后立即响应)

{
ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false
reason: '', // 错误原因
message: 'startMatch' / 'stopMatch'
}

响应2(匹配成功后的响应)

{
ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false
reason: '', // 错误原因
message: 'matchSuccess',
}

客户端开发

实现页面基本结构

game_hall.html


<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>游戏大厅</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_hall.css">
</head>
<body><div class="nav">五子棋对战</div><div class="container"><div><!--展示用户信息--><div id="screen"></div><!--匹配按钮--><div id="match-button">开始匹配</div></div></div>
</body>
</html>

game_hall.css


#screen {width: 400px;height: 200px;font-size: 20px;background-color: gray;color: white;border-radius: 10px;}#match-button {width: 400px;height: 50px;font-size: 20px;line-height: 50px;color:white;background-color: orange;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;
}#match-button:active {background-color: gray;
}

编写js代码来实现用户的信息


    <script src="js/jquery.min.js"></script><script>$.ajax({type:'get',url:'/userInfo',success:function(body){let screenDiv=document.querySelector('#screen');screenDiv.innerHTML='玩家:'+body.username+"分数:"+body.score+"<br> 比赛场次:"+body.totalCount+"获胜次数:"+body.winCount},error:function(){alert("获取用户信息失败!");}});</script>
实现匹配功能

点击匹配按钮,就会进入匹配逻辑,同时按钮上提示“匹配中...(点击取消)”

再次点击匹配按钮,则会取消匹配

当匹配成功后,服务器会返回匹配成功响应,页面跳转到游戏房间


        //初始化websockrt,并且实现前端的匹配逻辑let websocket=new WebSocket('ws://127.0.0.1:8080/findMatch');websocket.onopen=function(){console.log("onopen");}websocket.onclose=function(){console.log("onclose");}websocket.onerror=function(){console.log("onerror");}//监听页面关闭事件,在页面关闭之前,手动调用这里的websocket的close方法window.onbeforeload=function(){websocket.close();}//处理服务器返回的响应websocket.onmessage=function(e){//针对服务器返回的响应数据,这个响应就是针对“开始匹配”/“结束匹配”来对应的//解析得到的响应对象,返回的数据是一个JSON字符串,解析成js对象let resp=JSON.parse(e.data);if(!resp.ok){console.log("游戏大厅中接收到了失败响应!"+resp.reason);return;}if(resp.message=='startMatch'){//开始匹配请求发送成功console.log("进入匹配队列成功!");matchButton.innerHTML='匹配中...(点击停止)';}else if(resp.message=='stopMatch'){//结束匹配请求发送成功console.log("离开匹配队列成功!");matchButton.innerHTML='开始匹配';}else if(resp.message=='matchSuccess'){// 匹配到了对手console.log("匹配成功!进入游戏界面!");location.assign("/game_room.html");}else{console.log("接受了非法的响应!message="+resp.message);}}//给匹配按钮添加一个点击事件let matchButton=document.querySelector('#match-button');matchButton.onclick=function(){//在触发websocket请求之前,先确认websocket连接是否好if(websocket.readyState==websocket.OPEN){//如果当前readyState处于OPEN状态,说明连接好着//发送的数据:开始匹配/停止匹配if (matchbutton.innerHTML == '开始匹配') {console.log('开始匹配!');websocket.send(JSON.stringify({message: 'startMatch',}));} else if (matchbutton.innerHTML == '匹配中...(点击停止)') {console.log('停止匹配!');websocket.send(JSON.stringify({message: 'stopMatch',}));}}else{//连接是异常状态alert("当前您的连接已经断开!请重新登录!");location.assign('/login.html');}}</script>

服务器开发

创建并注册MatchAPI类

创建MatchAPI


@Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMapper=new ObjectMapper();@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);}
}

修改WebSocketConfig

在 addHandler 之后, 再加上一个 .addInterceptors(new HttpSessionHandshakeInterceptor()) 代码, 这样可以把之前登录过程中往 HttpSession 中存放的数据(主要是 User 对象), 放到 WebSocket 的 session 中. 方便后面的代码中获取到当前用户信息


@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Autowiredprivate MatchAPI matchAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(testAPI,"/test");registry.addHandler(matchAPI,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());}
}
实现用户管理器

创建 OnlineUserManager 类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.

  • 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.

  • 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.

  • 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.


@Component
public class OnlineUserManager {//这个哈希表用来表示当前用户在游戏大厅的在线状态private HashMap<Integer, WebSocketSession> gameHall=new HashMap<>();public void enterGameHall(int userId,WebSocketSession webSocketSession){gameHall.put(userId,webSocketSession);}public void exitGameHall(int userId){gameHall.remove(userId);}public WebSocketSession getFromGameHall(int userId){return gameHall.get(userId);}
}

给 MatchAPI 注入 OnlineUserManager


        @Autowiredprivate OnlineUserManager onlineUserManager;
创建匹配请求/响应对象

创建MatchRequest类


//表示一个websocket的匹配请求
public class MatchRequest {private String message="";public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}
}

创建MatchResponse类


//表示一个websocket的匹配响应
public class MatchResponse {private boolean ok;private String reason;private String message;public boolean isOk() {return ok;}public void setOk(boolean ok) {this.ok = ok;}public String getReason() {return reason;}public void setReason(String reason) {this.reason = reason;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}
}
处理上线下线状态

当前是使用HashMap来存储用户的在线状态的,如果是多线程访问一个HashMap,容易出现线程安全问题,所以针对HashMap进行修改


    private ConcurrentHashMap<Integer, WebSocketSession> gameHall=new ConcurrentHashMap<>();

实现 afterConnectionEstablished 方法.

通过参数中的 session 对象, 拿到之前登录时设置的 User 信息.

使用 onlineUserManager 来管理用户的在线状态.

先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开).

设置玩家的上线状态.


//通过这个类来处理匹配功能中的websocket请求
@Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMapper=new ObjectMapper();@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate Matcher matcher;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {//玩家上线,加入到onlineUserManager中//1、先获取到当前用户的身份信息(谁在游戏大厅中,建立的连接)//由于在注册webSocket时加上了.addInterceptors(new HttpSessionHandshakeInterceptor(),能够getAttributes()//这个逻辑就是把HttpSession中的Attribute拿到WebSocketSession中了//在Http登录逻辑中,往HttpSession中存入了User数据,httpsession.setAttribute("user",user)//此时就可以在WebSocketSession中把之前HttpSession里存的User对象给拿到了try {User user=(User) session.getAttributes().get("user");//2、先判定当前用户是否已经登录过(是在线状态),如果已经在线,不进行后续逻辑WebSocketSession tmpSession=onlineUserManager.getFromGameHall(user.getUserId());if (tmpSession!=null){//当前已经登录过了,告知客户端重复登录了MatchResponse response=new MatchResponse();response.setOk(false);response.setReason("当前禁止多开!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));session.close();return;}//3、拿到身份信息之后,就可以把玩家设置为在线状态onlineUserManager.enterGameHall(user.getUserId(), session);System.out.println("玩家"+user.getUsername()+"进入游戏大厅!");}catch (NullPointerException e){e.printStackTrace();//出现空指针异常,说明当前用户的身份信息为空,用户未登录//把当前用户尚未登录这个信息返回回去MatchResponse response=new MatchResponse();response.setOk(false);response.setReason("您尚未登录!不能进行玩家匹配功能!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}} @Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {try {//玩家下线,退出onlineUserManagerUser user=(User) session.getAttributes().get("user");WebSocketSession tmpSession=onlineUserManager.getFromGameHall(user.getUserId());if (tmpSession==session){onlineUserManager.exitGameHall(user.getUserId());}//如果玩家正在匹配中,websocket连接断开了,就应该移除匹配队列matcher.remove(user);}catch (NullPointerException e){e.printStackTrace();MatchResponse response=new MatchResponse();response.setOk(false);response.setReason("您尚未登录!不能进行玩家匹配功能!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {try {//玩家下线,退出onlineUserManagerUser user=(User) session.getAttributes().get("user");WebSocketSession tmpSession=onlineUserManager.getFromGameHall(user.getUserId());if (tmpSession==session){onlineUserManager.exitGameHall(user.getUserId());}//如果玩家正在匹配中,websocket连接断开了,就应该移除匹配队列matcher.remove(user);}catch (NullPointerException e){e.printStackTrace();MatchResponse response=new MatchResponse();response.setOk(false);response.setReason("您尚未登录!不能进行玩家匹配功能!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}}
}
处理开始匹配/取消匹配请求

实现 handleTextMessage

先从会话中拿到当前玩家的信息.

解析客户端发来的请求

判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.

此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.


        @Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {//实现处理开始匹配请求和处理停止匹配请求User user=(User) session.getAttributes().get("user");//获取到客户端给服务器发送的数据String payload=message.getPayload();//当前这个数据是一个JSON格式的字符串,需要转成java对象MatchRequest request=objectMapper.readValue(payload,MatchRequest.class);MatchResponse response=new MatchResponse();if (request.getMessage().equals("startMatch")){//进入匹配队列//先创建一个类表示匹配队列,把当前用户加进去//把玩家信息放入匹配队列之后,就可以返回一个响应给客户端response.setOk(true);response.setMessage("startMatch");}else if (request.getMessage().equals("stopMatch")){//退出匹配队列//先创建一个类表示匹配队列,把当前用户取进去//把玩家信息放入匹配队列之后,就可以返回一个响应给客户端response.setOk(true);response.setMessage("stopMatch");}else{//非法情况response.setOk(false);response.setReason("非法的匹配请求");}}
实现匹配器

创建 game.Matcher 类.

在 Matcher 中创建三个队列 (队列中存储 User 对象), 分别表示不同的段位的玩家. (此处约定 <2000 一档, 2000-3000 一档, >3000 一档)

提供 add 方法, 供 MatchAPI 类来调用, 用来把玩家加入匹配队列.

提供 remove 方法, 供 MatchAPI 类来调用, 用来把玩家移出匹配队列.

同时 Matcher 找那个要记录 OnlineUserManager, 来获取到玩家的 Session.


//这个类表示匹配器,通过这个类完成整个匹配功能
@Component
public class Matcher {//创建三个匹配队列private Queue<User> normalQueue=new LinkedList<>();private Queue<User> highQueue=new LinkedList<>();private Queue<User> veryHighQueue=new LinkedList<>();@Autowiredprivate OnlineUserManager onlineUserManager;//操作匹配队列的方法//把玩家放入到匹配队列中public void add(User user){if (user.getScore()<2000){synchronized (normalQueue){normalQueue.offer(user);}System.out.println("把玩家"+user.getUsername()+"加入到了normalQueue中!");}else if (user.getScore()>=2000 && user.getScore()<3000){synchronized (highQueue){highQueue.offer(user);}System.out.println("把玩家"+user.getUsername()+"加入到了highQueue中!");}else {synchronized (veryHighQueue){veryHighQueue.offer(user);}System.out.println("把玩家"+user.getUsername()+"加入到了veryHighQueue中!");}}//当玩家点击停止匹配时,就需要把玩家从匹配队列中删除public void remove(User user){if (user.getScore()<2000){normalQueue.remove(user);}else if (user.getScore()>=2000 && user.getScore()<3000){highQueue.remove(user);}else {veryHighQueue.remove();}}}

修改 game.Matcher , 实现匹配逻辑.

在 Matcher 的构造方法中, 创建一个线程, 使用该线程扫描每个队列, 把每个队列的头两个元素取出来, 匹配到一组中.


    public Matcher(){//创建三个线程,分别针对三个匹配队列进行操作Thread t1=new Thread(){@Overridepublic void run() {//扫描normalQueuewhile (true){handlerMatch(normalQueue);}}};t1.start();Thread t2=new Thread(){@Overridepublic void run() {//扫描highQueuewhile (true){handlerMatch(highQueue);}}};t2.start();Thread t3=new Thread(){@Overridepublic void run() {//扫描veryHighQueuewhile (true){handlerMatch(veryHighQueue);}}};t3.start();}

实现 handlerMatch

由于 handlerMatch 在单独的线程中调用. 因此要考虑到访问队列的线程安全问题. 需要加上锁.

每个队列分别使用队列对象本身作为锁即可.

在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列.


    private void handlerMatch(Queue<User> matchQueue) {synchronized (matchQueue){try {//1、检测队列中元素个数是否达到2while (matchQueue.size()<2){matchQueue.wait();}//2、尝试从队列中取出两个玩家User player1= matchQueue.poll();User player2= matchQueue.poll();System.out.println("匹配出两个玩家:"+player1.getUsername()+","+player2.getUsername());//3、获取到玩家的websocket的会话WebSocketSession session1=onlineUserManager.getFromGameHall(player1.getUserId());WebSocketSession session2=onlineUserManager.getFromGameHall(player2.getUserId());if (session1==null){//如果玩家1不在线了,就把玩家2重新放回到匹配队列中matchQueue.offer(player2);return;}if (session2==null){matchQueue.offer(player1);return;}if (session1==session2){matchQueue.offer(player1);return;}//4、把这两个玩家放到同一个房间//5、给玩家反馈匹配成功的信息MatchResponse response1=new MatchResponse();response1.setOk(true);response1.setMessage("matchSuccess");String json1=objectMapper.writeValueAsString(response1);session1.sendMessage(new TextMessage(json1));MatchResponse response2=new MatchResponse();response2.setOk(true);response2.setMessage("matchSuccess");String json2=objectMapper.writeValueAsString(response2);session2.sendMessage(new TextMessage(json2));}catch (InterruptedException | IOException e){e.printStackTrace();}}

需要给上面的插入队列元素, 删除队列元素也加上锁.


    //操作匹配队列的方法//把玩家放入到匹配队列中public void add(User user){if (user.getScore()<2000){synchronized (normalQueue){normalQueue.offer(user);normalQueue.notify();}System.out.println("把玩家"+user.getUsername()+"加入到了normalQueue中!");}else if (user.getScore()>=2000 && user.getScore()<3000){synchronized (highQueue){highQueue.offer(user);highQueue.notify();}System.out.println("把玩家"+user.getUsername()+"加入到了highQueue中!");}else {synchronized (veryHighQueue){veryHighQueue.offer(user);veryHighQueue.notify();}System.out.println("把玩家"+user.getUsername()+"加入到了veryHighQueue中!");}}//当玩家点击停止匹配时,就需要把玩家从匹配队列中删除public void remove(User user){if (user.getScore()<2000){synchronized (normalQueue){normalQueue.remove(user);}System.out.println("把玩家"+user.getUsername()+"移除出了normalQueue中!");}else if (user.getScore()>=2000 && user.getScore()<3000){synchronized (highQueue){highQueue.remove(user);}System.out.println("把玩家"+user.getUsername()+"移除出了highQueue中!");}else {synchronized (veryHighQueue){veryHighQueue.remove(user);}System.out.println("把玩家"+user.getUsername()+"移除出了veryHighQueue中!");}}
创建房间类

匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中.

创建 game.Room 类

一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识;房间内要记录对弈的玩家双方信息


//这个类就表示一个游戏房间
public class Room {//使用字符串来表示,方便生成唯一值private String roomId;private User user1;private User user2;public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId = roomId;}public User getUser1() {return user1;}public void setUser1(User user1) {this.user1 = user1;}public User getUser2() {return user2;}public void setUser2(User user2) {this.user2 = user2;}public Room(){//构造Room的时候生成一个唯一的字符串来表示房间id//使用UUID来作为房间idroomId= UUID.randomUUID().toString();}
}
实现房间管理器

Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象.

需要一个管理器对象来管理所有的 Room.

创建 game.RoomManager

使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象

再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.

提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).


//房间管理器
//这个类也有唯一实例
@Component
public class RoomManager {private ConcurrentHashMap<String,Room> rooms=new ConcurrentHashMap<>();private ConcurrentHashMap<Integer,String> userIdToRoomId=new ConcurrentHashMap<>();public void add(Room room,int userId1,int userId2){rooms.put(room.getRoomId(),room);userIdToRoomId.put(userId1,room.getRoomId());userIdToRoomId.put(userId2,room.getRoomId());}public void remove(String roomId,int userId1,int userId2){rooms.remove(roomId);userIdToRoomId.remove(userId1);userIdToRoomId.remove(userId2);}public Room getRoomByRoomId(String roomId){return rooms.get(roomId);}public Room getRoomByUserId(int userId){String roomId=userIdToRoomId.get(userId);if (roomId==null){//userId--》roomId映射关系不存在return null;}return rooms.get(roomId);}}
实现匹配器

给 Matcher 找注入 RoomManager 对象,修改 Matcher.handlerMatch


    @Autowiredprivate RoomManager roomManager;//4、把这两个玩家放到同一个房间Room room=new Room();roomManager.add(room, player1.getUserId(), player2.getUserId());

4.3、实现对战模块

前后端交互接口

建立连接

ws://127.0.0.1:8080/game

连接响应

{
message: 'gameReady', // 游戏就绪
ok: true, // 是否成功.
reason: '', // 错误原因
roomId: 'abcdef', // 房间号.
thisUserId: 1, // 玩家自己的 id
thatUserId: 2, // 对手的 id
whiteUser: 1, // 先手方的 id}

落子请求

{
message: 'putChess',
userId: 1,
row: 0,
col: 0}

落子响应

{
message: 'putChess',
userId: 1,
row: 0,
col: 0,
winner: 0}

客户端开发

实现页面基本结构

创建 game_room.html, 表示对战页面.


<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>游戏房间</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_room.css">
</head>
<body><div class="nav">五子棋对战</div><div class="container"><div><!--棋盘区域,需要基于canvas进行实现--><canvas id="chess" width="450px" height="450px"></canvas><!--显示区域--><div id="screen">等待玩家连接中。。。</div></div></div>
</body>
</html>
实现棋盘绘制

创建script.js

使用一个二维数组来表示棋盘. 虽然胜负是通过服务器判定的, 但是客户端的棋盘可以避免 "一个位置重复落子" 这样的情况

oneStep 函数起到的效果是在一个指定的位置上绘制一个棋子. 可以区分出绘制白字还是黑子. 参数是横坐标和纵坐标, 分别对应列和行.

用 onclick 来处理用户点击事件. 当用户点击的时候通过这个函数来控制绘制棋子.

me 变量用来表示当前是否轮到我落子. over 变量用来表示游戏结束.


gameInfo={roomId:null,thisUserId:null,thatUserId:null,isWhite:true,
}//设定页面显示相关操作
function setScreenText(me){let screen=document.querySelector("#screen");if(me){screen.innerHTML="轮到你落子了!";}else{screen.innerHTML="轮到对方落子了!";}
}//初始化websocket
//初始化一局游戏
function initGame(){//根据服务器分配的先后手情况决定谁先下let me=gameInfo.isWhite;//游戏是否结束let over=false;let chessBoard=[];//初始化chessBoard数组(表示棋盘的数组)for(let i=0;i<15;i++){chessBoard[i]=[];for(let j=0;j<15;j++){chessBoard[i][j]=0;}}let chess=document.querySelector("#chess");let context=chess.getContext('2d');context.strokeStyle="#BFBFBF";//背景图片let logo=new Image();logo.src="image/ee.jpeg";logo.onload=function(){context.drawImage(logo,0,0,450,450);initChessBoard();}// 绘制棋盘网格function initChessBoard() {for (let i = 0; i < 15; i++) {context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 430);context.stroke();context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30);context.stroke();}}// 绘制一个棋子, me 为 truefunction oneStep(i, j, isWhite) {context.beginPath();context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);if (!isWhite) {gradient.addColorStop(0, "#0A0A0A");gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1");gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}chess.onclick = function (e) {if (over) {return;}if (!me) {return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行let col = Math.floor(x / 30);let row = Math.floor(y / 30);if (chessBoard[row][col] == 0) {// TODO 发送坐标给服务器, 服务器要返回结果oneStep(col, row, gameInfo.isWhite);chessBoard[row][col] = 1;}}}initGame();
初始化websocket

在 game_room.html 中, 加入 websocket 的连接代码, 实现前后端交互.

先删掉原来的 initGame 函数的调用. 一会在获取到服务器反馈的就绪响应之后, 再初始化棋盘.

创建 websocket 对象, 并注册 onopen/onclose/onerror 函数. 其中在 onerror 中做一个跳转到游戏大厅的逻辑. 当网络异常断开, 则回到大厅.

实现 onmessage 方法. onmessage 先处理游戏就绪响应.


//初始化websocket
let websocket=new WebSocket("ws://127.0.0.1:8080/game");websocket.onopen=function(){console.log("连接游戏房间成功!");
}websocket.onclose=function(){console.log("和游戏服务器断开连接!");
}websocket.onerror=function(){console.log("和服务器的连接出现异常!");
}window.onbeforeunload=function(){websocket.close();
}websocket.onmessage=function(event){console.log("[handlerGameReady]"+event.data);let resp=JSON.parse(event.data);if(resp.message!='gameReady'){console.log("响应类型错误!");return;}if(!resp.ok){alert("游戏连接失败!reason="+resp.reason);//如果出现连接失败的情况,回到游戏大厅location.assign("/game_hall.html");return;}//初始化游戏信息gameInfo.roomId=resp.roomId;gameInfo.thisUserId=resp.thisUserId;gameInfo.thatUserId=resp.thatUserId;gameInfo.isWhite=resp.isWhite;//初始化棋盘initGame();//设置显示区域内容setScreenText(gameInfo.isWhite);
}
发送落子请求

修改 onclick 函数, 在落子操作时加入发送请求的逻辑


        chess.onclick = function (e) {if (over) {return;}if (!me) {return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行let col = Math.floor(x / 30);let row = Math.floor(y / 30);if (chessBoard[row][col] == 0) {// TODO 发送坐标给服务器, 服务器要返回结果send(row,col);// oneStep(col, row, gameInfo.isWhite);// chessBoard[row][col] = 1;}}function send(row,col){let req={message:'putChess',userId:gameInfo.thisUserId,row:row,col:col};websocket.send(JSON.stringify(req));}
处理落子响应

在 initGame 中, 修改 websocket 的 onmessage

在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了;在处理落子响应中要处理胜负手.


//在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了websocket.onmessage=function(event){console.log("[handlerPutChess]"+event.data);let resp=JSON.parse(event.data);if(resp.message!='putChess'){console.log("响应类型错误!");return;}//判断当前这个响应是自己落的子,还是别人落的子if(resp.userId==gameInfo.thisUserId){//自己落的子oneStep(resp.col,resp.row,gameInfo.isWhite);}else if(resp.userId==gameInfo.thatUserId){//对手落的子oneStep(resp.col,resp.row,!gameInfo.isWhite);}else{//响应错误,userId有问题console.log('[handlerPutChess] resp UserId错误!');return;}//给对应的位置设为1,方便后续逻辑判定当前位置是否已经有子chessBoard[row][col]=1;//交换双方的落子轮次me=!me;setScreenText(me);//判定游戏是否结束if(resp.winner!=0){if(resp.winner==gameInfo.thisUserId){alert("你赢了!");}else if(resp.winner==gameInfo.thatUserId){alert("你输了!");}else{alert("winner字段错误!"+resp.winner);}//回到游戏大厅location.assign('/game_hall.html');}}

服务器开发

创建并注册GameAPI类

创建 api.GameAPI , 处理 websocket 请求.


@Component
public class GameAPI extends TextWebSocketHandler {@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

修改 WebSocketConfig, 将 GameAPI 进行注册


    @Autowiredprivate GameAPI gameAPI;registry.addHandler(gameAPI,"/game").addInterceptors(new HttpSessionHandshakeInterceptor());
创建落子请求/响应对象

创建 game.GameReadyResponse 类


//客户端连接到游戏房间后,服务器返回的响应
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public boolean isOk() {return ok;}public void setOk(boolean ok) {this.ok = ok;}public String getReason() {return reason;}public void setReason(String reason) {this.reason = reason;}public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId = roomId;}public int getThisUserId() {return thisUserId;}public void setThisUserId(int thisUserId) {this.thisUserId = thisUserId;}public int getThatUserId() {return thatUserId;}public void setThatUserId(int thatUserId) {this.thatUserId = thatUserId;}public int getWhiteUser() {return whiteUser;}public void setWhiteUser(int whiteUser) {this.whiteUser = whiteUser;}
}

创建 game.GameRequest 类


//落子请求
public class GameRequest {private String message;private int userId;private int row;private int col;public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public int getRow() {return row;}public void setRow(int row) {this.row = row;}public int getCol() {return col;}public void setCol(int col) {this.col = col;}
}

创建 game.GameResponse 类


//落子响应
public class GameResponse {private String message;private int userId;private int row;private int col;private int winner;public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public int getRow() {return row;}public void setRow(int row) {this.row = row;}public int getCol() {return col;}public void setCol(int col) {this.col = col;}public int getWinner() {return winner;}public void setWinner(int winner) {this.winner = winner;}
}
处理连接成功

实现 GameAPI 的 afterConnectionEstablished 方法.

首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息.

然后要判定当前玩家是否是在房间中.

接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.


    @Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp=new GameReadyResponse();//1、先获取到用户的身份信息(从HttpSession中拿到当前用户的对象)User user=(User) session.getAttributes().get("user");if (user==null){resp.setOk(false);resp.setReason("用户尚未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//2、判定当前用户是否已经进入房间(使用房间管理器进行查询)Room room=roomManager.getRoomByUserId(user.getUserId());if (room==null){//如果为null,当前没有找到对应的房间,该玩家还没有匹配到resp.setOk(false);resp.setReason("用户尚未匹配到!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//3、判断是不是多开if (onlineUserManager.getFromGameHall(user.getUserId())!=null|| onlineUserManager.getFromGameRoom(user.getUserId())!=null){//如果一个账号,一边是在游戏大厅,一边是在游戏房间,也是为多开resp.setOk(false);resp.setReason("禁止多开游戏界面");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//4、设置当前玩家上线onlineUserManager.enterGameRoom(user.getUserId(), session);//5、把两个玩家加入到游戏房间if (room.getUser1()==null){//第一个玩家还尚未加入房间room.setUser1(user);//把先连入房间的玩家设为先手方room.setWhiteUser(user.getUserId());System.out.println("玩家"+user.getUsername()+"已经准备就绪!作为玩家1");return;}if (room.getUser2()==null){//玩家1已经进入房间room.setUser2(user);System.out.println("玩家"+user.getUsername()+"已经准备就绪!作为玩家2");//当两个玩家都加入成功之后,就要让服务器,给这两个玩家都返回websocket的响应数据//通知这两个玩家,游戏双方都准备好了//通知玩家1noticeGameReady(room,room.getUser1(),room.getUser2());//通知玩家2noticeGameReady(room,room.getUser2(),room.getUser1());return;}//6、此处如果又有玩家尝试连接同一个房间,就提示报错resp.setOk(false);resp.setReason("当前房间已满,您不能加入房间!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}

实现通知玩家就绪


    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {GameReadyResponse resp=new GameReadyResponse();resp.setMessage("gameReady");resp.setOk(true);resp.setReason("");resp.setRoomId(room.getRoomId());resp.setThisUserId(thisUser.getUserId());resp.setThatUserId(thatUser.getUserId());resp.setWhiteUser(room.getWhiteUser());//把当前的响应数据传回给玩家WebSocketSession webSocketSession=onlineUserManager.getFromGameRoom(thisUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}
玩家下线的处理

    @Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user=(User) session.getAttributes().get("user");if (user==null){//在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSession=onlineUserManager.getFromGameRoom(user.getUserId());if (session==exitSession){//避免在多开的情况下,第二个用户退出连接动作onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户"+user.getUsername()+"游戏房间连接异常");}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user=(User) session.getAttributes().get("user");if (user==null){//在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSession=onlineUserManager.getFromGameRoom(user.getUserId());if (session==exitSession){//避免在多开的情况下,第二个用户退出连接动作onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户"+user.getUsername()+"离开游戏房间");}
处理落子请求

实现 handleTextMessage


    @Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {//1、先从session里拿到当前用户的身份信息User user=(User) session.getAttributes().get("user");if (user==null){System.out.println("[handleTextMessage]当前玩家尚未登录!");return;}//2、根据玩家id获取到房间对象Room room=roomManager.getRoomByUserId(user.getUserId());//3、通过room对象来处理这次的具体请求room.putChess(message.getPayload());}
修改Room类

由于我们的 Room 并没有通过 Spring 来管理. 因此内部就无法通过 @Autowired 来自动注入.

需要手动的通过 SpringBoot 的启动类来获取里面的对象.


@SpringBootApplication
public class JavaGobangApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) {context=SpringApplication.run(JavaGobangApplication.class, args);}}

    public Room(){//构造Room的时候生成一个唯一的字符串来表示房间id//使用UUID来作为房间idroomId= UUID.randomUUID().toString();//通过入口类中记录的context来手动获取到前面的RoomManager和OnlineUserManageronlineUserManager= JavaGobangApplication.context.getBean(OnlineUserManager.class);roomManager=JavaGobangApplication.context.getBean(RoomManager.class);}
实现对弈功能

实现 room 中的 putChess 方法.


    //二维数组用来表示棋盘//使用0表示当前位置未落子//使用1表示user1的落子位置//使用2表示user2的落子位置private int[][] board=new int[15][15];//创建objectMapper用来转换JSONprivate ObjectMapper objectMapper=new ObjectMapper();@Autowiredprivate OnlineUserManager onlineUserManager;//引入roommanager,用于房间销毁@Autowiredprivate RoomManager roomManager;//通过这个方法来处理一次落子操作public void putChess(String reqJson) throws IOException {//1、记录当前落子的情况GameRequest request=objectMapper.readValue(reqJson,GameRequest.class);GameResponse response=new GameResponse();//判断当前是玩家1落子还是玩家2int chess=request.getUserId()==user1.getUserId()?1:2;int row= request.getRow();int col= request.getCol();if (board[row][col]!=0){System.out.println("当前位置("+row+","+col+")已经有子了!");return;}board[row][col]=chess;//2、进行胜负判定int winner=checkWinner(row,col);//3、给客户端返回响应response.setMessage("putChess");response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//要想给用户发送websocket数据,就要获得这个用户的websocketSessionWebSocketSession session1=onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2=onlineUserManager.getFromGameRoom(user2.getUserId());if (session1==null){response.setWinner(user2.getUserId());System.out.println("玩家1掉线!!!");}if (session2==null){response.setWinner(user1.getUserId());System.out.println("玩家2掉线!!!");}//把响应构造成Json字符串,通过session进行传输String respJson=objectMapper.writeValueAsString(response);if (session1!=null){session1.sendMessage(new TextMessage(respJson));}if (session2!=null){session2.sendMessage(new TextMessage(respJson));}//4、如果当前胜负已分,就把room从管理器中销毁if (response.getWinner()!=0){System.out.println("游戏结束!房间即将销毁!roomId="+roomId+"获胜方为"+response.getWinner());//销毁房间roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}}

实现打印棋盘的逻辑


    private void printBoard() {//打印出棋盘System.out.println("打印棋盘信息"+roomId);System.out.println("======================");for (int r=0;r<MAX_ROW;r++){for (int c=0;c<MAX_COL;c++){System.out.print(board[r][c]+" ");}System.out.println();}System.out.println("======================");}

实现胜负判定


    //如果玩家1获胜就返回玩家1的userId//胜负未分返回0private int checkWinner(int row, int col,int chess) {//1、检查所有的行//先遍历这五种情况for (int c=col-4;c<=col;c++){//针对其中一种情况,来判断这五个子是不是连在一起try {if (board[row][c]==chess&& board[row][c+1]==chess&& board[row][c+2]==chess&& board[row][c+3]==chess&& board[row][c+4]==chess){return chess==1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//2、判定所有列for (int r=row-4;r<=row;r++){//针对其中一种情况,来判断这五个子是不是连在一起try {if (board[r][col]==chess&& board[r+1][col]==chess&& board[r+2][col]==chess&& board[r+3][col]==chess&& board[r+4][col]==chess){return chess==1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//3、左对角线for (int r=row-4,c=col-4;r<=row && c<=col;r++,c++){//针对其中一种情况,来判断这五个子是不是连在一起try {if (board[r][c]==chess&& board[r+1][c+1]==chess&& board[r+2][c+2]==chess&& board[r+3][c+3]==chess&& board[r+4][c+4]==chess){return chess==1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//4、右对角线for (int r=row-4,c=col+4;r<=row && c>=col;r++,c--){//针对其中一种情况,来判断这五个子是不是连在一起try {if (board[r][c]==chess&& board[r+1][c-1]==chess&& board[r+2][c-2]==chess&& board[r+3][c-3]==chess&& board[r+4][c-4]==chess){return chess==1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}return 0;}
处理途中玩家掉线

在GameAPI中的handleTransportError和afterConnectionClosed添加noticeThatUserWin()方法


    private void noticeThatUserWin(User user) throws IOException {//1、根据当前玩家,找到玩家所在的房间Room room=roomManager.getRoomByUserId(user.getUserId());if (room==null){//该房间已经被释放,没有“对手”System.out.println("当前房间已经被释放,无需通知对手!");return;}//2、根据房间找对手User thatUser=(user==room.getUser1())?room.getUser2():room.getUser1();//3、找到对手的在线状态WebSocketSession webSocketSession=onlineUserManager.getFromGameRoom(thatUser.getUserId());if (webSocketSession==null){//意味着对手掉线了System.out.println("对手也已经掉线了,无需通知!");return;}//4、构造一个响应,来通知对手,你是获胜方GameResponse resp=new GameResponse();resp.setMessage("putChess");resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));//5、释放房间对象roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());}
更新玩家分数

修改UserMapper和UserMapper.xml


@Mapper
public interface UserMapper {//根据用户名来查询用户的信息,用于登录功能User selectByName(String username);//往数据库里插入一个用户,用于注册功能void insert(User user);//总比赛场数+1,获胜场数+1,天梯分数+30void userWin(int userId);//总比赛场数+1,获胜场数不变,天梯分数-30void userLose(int userId);
}

    <update id="userWin">update user set totalCount=totalCount+1,winCount=winCount+1,score=score+30where userId=#{userId}</update><update id="userLose">update user set totalCount=totalCount+1,score=score-30where userId=#{userId}</update>

修改putChess方法


    //通过这个方法来处理一次落子操作public void putChess(String reqJson) throws IOException {//1、记录当前落子的情况GameRequest request=objectMapper.readValue(reqJson,GameRequest.class);GameResponse response=new GameResponse();//判断当前是玩家1落子还是玩家2int chess=request.getUserId()==user1.getUserId()?1:2;int row= request.getRow();int col= request.getCol();if (board[row][col]!=0){System.out.println("当前位置("+row+","+col+")已经有子了!");return;}board[row][col]=chess;//2、打印出当前的棋盘信息printBoard();//3、进行胜负判定int winner=checkWinner(row,col,chess);//4、给客户端返回响应response.setMessage("putChess");response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//要想给用户发送websocket数据,就要获得这个用户的websocketSessionWebSocketSession session1=onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2=onlineUserManager.getFromGameRoom(user2.getUserId());if (session1==null){response.setWinner(user2.getUserId());System.out.println("玩家1掉线!!!");}if (session2==null){response.setWinner(user1.getUserId());System.out.println("玩家2掉线!!!");}//把响应构造成Json字符串,通过session进行传输String respJson=objectMapper.writeValueAsString(response);if (session1!=null){session1.sendMessage(new TextMessage(respJson));}if (session2!=null){session2.sendMessage(new TextMessage(respJson));}//5、如果当前胜负已分,就把room从管理器中销毁if (response.getWinner()!=0){System.out.println("游戏结束!房间即将销毁!roomId="+roomId+"获胜方为"+response.getWinner());int winUserId=response.getWinner();int loseUserId=response.getWinner()==user1.getUserId()?user2.getUserId():user1.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);//销毁房间roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}}

修改GameAPI中noticeThatUserWin方法


    private void noticeThatUserWin(User user) throws IOException {//1、根据当前玩家,找到玩家所在的房间Room room=roomManager.getRoomByUserId(user.getUserId());if (room==null){//该房间已经被释放,没有“对手”System.out.println("当前房间已经被释放,无需通知对手!");return;}//2、根据房间找对手User thatUser=(user==room.getUser1())?room.getUser2():room.getUser1();//3、找到对手的在线状态WebSocketSession webSocketSession=onlineUserManager.getFromGameRoom(thatUser.getUserId());if (webSocketSession==null){//意味着对手掉线了System.out.println("对手也已经掉线了,无需通知!");return;}//4、构造一个响应,来通知对手,你是获胜方GameResponse resp=new GameResponse();resp.setMessage("putChess");resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));//5、更新玩家信息int winUserId=thatUser.getUserId();int loseUserId=user.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);//6、释放房间对象roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());}

五、部署云服务器

构造数据库中的数据

调整websocket建立连接的url


        let websocketUrl='ws://'+ location.host+'/findMatch';let websocket=new WebSocket(websocketUrl);

打包上传

通过外网访问

五子棋实战

六、后续扩展功能

计时

一步落子过程中, 玩家能思考的时间.

保存棋谱/录像回放

首先需要在数据库中创建一个新的表, 用来表示每个玩家的游戏房间编号,服务器把每一局对局, 玩家轮流落子的位置都记录下来(比如保存到一个文本文件中),然后玩家可以选定某个曾经的比赛, 在页面上回放出对局的过程.

观战功能

在游戏大厅除了显示匹配按钮之外, 还能显示当前所有的对局房间,玩家可以选中某个房间, 以观众的形式加入到房间中. 同时能实时的看到选手的对局情况.

界面聊天

同一个房间中的选手之间可以发送文本消息,或者在对战中可接受到游戏大厅好友的消息

人机对战

支持 AI 功能, 实现人机对战.

根据以上扩展功能,后续将对此项目进行扩充,敬请期待!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/11480.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

train_gpt2.c

llm.c/train_gpt2.c at master karpathy/llm.c (github.com) 源码 /* This file trains the GPT-2 model. This version is the clean, minimal, reference. As such: - it runs on CPU. - it does not make the code too complex; it is readable. - it does not use any p…

等保测评技术方案(五)

&#xff08;八&#xff09;漏洞扫描方案 1.参与人员 乙方工程师&#xff1a;谭 然、张 剑等。 范围经过双方确认&#xff0c;此次评估的对象包括&#xff1a; 2.网络设备 IP 地址 设备型号 备注 / / / / / / 以现场测评实际数据为准 3.应用系统 地址 …

SpringBoot集成Curator实现Watch事件监听

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 Zookeeper是一个Ap…

有哪些可以用电脑做的挣钱副业,有电脑就行

以下是一些可以用电脑做的挣钱副业 1. 写作和翻译 可以在各大网络平台上接单进行写作或者翻译。 2. 做任务 还在做致米宝库这个软件&#xff0c;软件每天会发布一些项目任务&#xff0c;也能学到一些网上赚钱的知识技术&#xff0c;我平时就做些简单任务和一个虚拟项目。 任…

下载npm I就包错解决方案

npm i xxxx -S --legacy-peer-deps 如果包错就执行以上命令

杨校老师项目之基于大数据技术栈hadoop商业web应用的日志分析系统

获取全套资料&#xff1a; 有偿获取&#xff1a;mryang511688 摘要&#xff1a; 互联网世界的先驱者们一致认为大数据将是未来互联网产业&#xff0c;甚至是整个人类各个产业的基础资源&#xff0c;那么到底什么是大数据&#xff0c;大数据给我们的世界是如何带来变化的呢&am…

电子作业指导书系统如何提升医疗设备工厂的生产效率

在医疗设备工厂中&#xff0c;电子作业指导书&#xff08;ESOP&#xff09;正逐渐成为提升生产效率的关键因素。 一、电子作业指导书系统提供了即时可得的准确信息。 电子作业指导书系统与传统的纸质作业指导书相比&#xff0c;员工可以在工作现场通过电子设备随时查阅最新、最…

2024年5月树莓集团快讯

树莓集团近期快讯 1 园区专场招聘会进校园 国际数字影像产业园联合四川城市职业学院的专场招聘会成功召开&#xff0c;共计提供400余个工作岗位。 2 园区硬件优化再升级 园区硬件优化再升级&#xff0c;智能门禁系统及人脸识别系统下周投入使用。 3 基地短剧合作交流 天府…

Apple store 静安·苹果店欣赏

官网&#xff1a; https://www.apple.com/today/Apple 亚洲第一大商店&#xff1a;Apple 静安零售店现已在上海开幕 静安苹果欣赏

Verilog中信号发生器的代码实现

目录 描述 输入描述&#xff1a; 输出描述&#xff1a; 描述 题目描述&#xff1a; 请编写一个信号发生器模块&#xff0c;根据波形选择信号wave_choise发出相应的波形&#xff1a;wave_choice0时&#xff0c;发出方波信号&#xff1b;wave_choice1时&#xff0c;发出锯齿…

网页版五子棋的自动化测试

目录 前言 一、主要技术 二、测试环境的准备部署 三、测试用例 四、执行测试 4.1、公共类设计 创建浏览器驱动对象 测试套件 释放驱动类 4.2、功能测试 登录页面 注册页面 游戏大厅页面 游戏房间页面 测试套件结果 4.3、界面测试 登录页面 注册页面 游戏大…

MoonBit 本周有重磅更新!枚举构造器支持可变字段、数组字面量默认构建 Vector

MoonBit更新 支持了构造器的 payload 中出现 mutable field&#xff0c;使用方式如下&#xff1a; enum E {C(mut ~x : Int, mut ~y : Int) } derive(Debug)fn swap_xy(x : E) -> Unit {match x {// ~y 会绑定模式匹配前 C 中的 y 字段的值// 当看到 C(..) as c 这种模式时…

visual sudio使用-创建空项目-创建cpp文件

新建空项目 新建cpp文件 #include <iostream> using namespace std;int main() {cout << "hello vs" << endl;cout << "hello c" << "\n";cout << "hello first day\n"; }

LeetCode416:分割等和子集

题目描述 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 解题思想 [1,5,11,5] 和为22&#xff0c;其中一半为 11。如果能寻找到若干数的和为11则成立可以抽象为一个0-1背包问题&#xff1a;容…

MATLAB绘制蒸汽压力和温度曲线

蒸汽压力与温度之间的具体关系公式一般采用安托因方程&#xff08;Antoine Equation&#xff09;&#xff0c;用于描述纯物质的蒸汽压与温度之间的关系。安托因方程的一般形式如下&#xff1a; [\log_{10} P A - \frac{B}{C T}] 其中&#xff0c; (P) 是蒸汽压&#xff08…

【刷题】一篇文章搞定“位运算”

只要春天不死&#xff0c;就有迎春的花朵年年岁岁开放&#xff0c;生命讲涅槃&#xff0c;生生不息&#xff0c;并会以另一种形式永存。 – 路遥 《平凡的世界》 (◦′ᆺ‵◦) ♬ ✧❥✧.•✧♡✧ ℒℴѵℯ ✧♡✧•.❥ (◦′ᆺ‵◦) ♬ ✧❥✧.•✧♡✧ ℒℴѵℯ ✧♡✧•.❥…

NL6621 WIFI模块烧录及其他

某宝淘得NL6621: 测了一下引脚&#xff1a; 做了以下功课&#xff1a; 新岸线物联网NL6621解决方案是高性价比、完全开源、高成熟度的解决方案&#xff0c;特别为高数据吞吐率低成本的无线局域网产品而设计。它集成了MCU&#xff0c; MAC&#xff0c;1T1R基带和带功放RF收发机于…

数据结构的二叉树(c语言版)

一.二叉树的概念 1.二叉树的基本概念 二叉树是一种常见的树状数据结构&#xff0c;它由若干个节点组成&#xff0c;这些节点通过边连接起来。每个节点最多可以有两个子节点&#xff0c;分别称为左子节点和右子节点。 二叉树的特点是每个节点最多有两个子节点&#xff0c;而且…

Eplan2.9版本安装教程指南【附安装包】

文章目录 前言一、Eplan是什么&#xff1f;二、安装指南1、安装包获取2、安装步骤在这里插入图片描述 总结 前言 随着人工智能的不断发展&#xff0c;机器学习这门技术也越来越重要&#xff0c;很多人都开启了学习机器学习&#xff0c;本文就介绍了机器学习的基础内容。 提示&…

【报错合集】完美解决“虚拟机使用的是此版本 VMware Workstation 不支持的硬件版本”

文章目录 解决方案&#xff1a;更改设置的硬件版本 今天我需要将别人的虚拟机克隆到我的VMware Workstation上运行&#xff0c;结果发生了以下的错误&#xff1a; 刚开始以为是VMware Workstation的版本问题太低导致的&#xff0c;所以我删除了原来的那个版本&#xff0c;下载…