基于 SpringBoot + MyBatis 的在线五子棋对战
文章目录
- 1. 项目设计
- 2. 效果图展示
- 3. 创建项目以及配置文件
- 3.1 创建项目
- 3.2 配置文件
- 3.2.1 在 application.properties 中添加配置文件
- 3.2.2 在 resources 目录下创建mapper
- 4. 数据库设计与实现
- 5. 登录注册模块
- 5.1 设计登录注册交互接口
- 5.2 设置登录注册功能返回的响应类
- 5.3 使用 BCrypt 对密码进行加密
- 5.4 完成 MyBatis 操作
- 5.5 后端的实现
- 5.5.1 登录功能后端实现
- 5.5.2 注册功能后端实现
- 5.5.3 注销功能
- 5.6 前端的实现
- 5.6.1 登录前端实现
- 5.6.2 注册前端实现
- 5.7 添加拦截器
- 6. 大厅界面
- 6.1 交互接口设计
- 6.2 用户加载前后交互接口
- 6.3 前端和后端实现用户信息加载
- 6.3.1 后端的实现
- 6.3.2 前端的实现
- 6.4 实现匹配功能的前端代码
- 6.5 实现匹配功能的后端代码
- 6.5.1 创建在线状态
- 6.5.2 创建房间对象
- 6.5.3 创建房间管理器
- 6.5.4 创建匹配队列
- 6.5.5 写完 MatchController
- 6.6 大厅界面总结
- 7. 房间界面
- 7.1 交互接口设计
- 7.2 实现房间界面前端代码
- 7.2.1 设置棋盘界面, 以及显示框.
- 7.2.2 对应的js文件
- 7.2.3 初始化 websocket
- 7.2.4 落子时,发送落子请求
- 7.2.5 落子时, 发送落子响应
- 7.3 实现房间界面后端代码
- 7.3.1 注册GameController
- 7.3.2 创建GameController
- 7.3.3 创建对应的响应类和请求类
- 7.3.4 完成用户房间在线状态管理
- 7.3.5 添加 MyBatis 用来更新玩家积分
- 7.3.6 完成处理连接方法
- 7.3.7 完成处理连接断开的方法和连接异常的方法
- 7.3.8 在房间管理器中添加代码
- 7.3.9 Room类添加棋盘代码
- 7.3.10 实现handleTextMessage方法
- 7.3.11 实现putChess方法
- 7.3.12 完成用户胜负判断
1. 项目设计
前端 : HTML + CSS + JavaScript + Jquery + AJAX
后端 : Spring MVC + Spring Boot + MyBatis
2. 效果图展示
3. 创建项目以及配置文件
3.1 创建项目
3.2 配置文件
3.2.1 在 application.properties 中添加配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/onlineGobang?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Drivermybatis.mapper-locations=classpath:mapper/**Mapper.xml
3.2.2 在 resources 目录下创建mapper
mapper下添加 目录 **.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.onlinemusicserver.mapper."对应的Mapper""></mapper>
4. 数据库设计与实现
这里使用数据库存储每一个用户的信息, 初始的时候, 天梯分和场次都是默认的.
create database if not exists onlineGobang;use onlineGobang;drop table if exists user;
create table user(userId int primary key auto_increment,username varchar(20) unique,password varchar(255) not null,score int,totalCount int,winCount int
);
5. 登录注册模块
5.1 设计登录注册交互接口
登录功能
请求
POST /user/login HTTP/1.1{username: "",password: ""}响应
{status: 1/-1,message: "",data: ""
}
注销功能
请求
GET /user/logout HTTP/1.1响应
HTTP/1.1 200
注册功能
请求
POST /user/register HTTP/1.1{username: "",password: ""}响应
{status: 1/-1,message: "",data: ""
}
5.2 设置登录注册功能返回的响应类
通过这个类, 方便前端接收内容
@Data
public class ResponseBodyMessage<T> {private int status;private String message;private T data;public ResponseBodyMessage(int status, String message, T data) {this.status = status;this.message = message;this.data = data;}
}
5.3 使用 BCrypt 对密码进行加密
在 pom.xml中添加依赖
<!-- security依赖包 (加密)--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency>
在启动类中添加注解
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
在 cofig 包下, 创建一个类 AppConfig.
@Configuration
public class AppConfig implements WebMvcConfigurer {@Beanpublic BCryptPasswordEncoder getBCryptPasswordEncoder() {return new BCryptPasswordEncoder();}
}
5.4 完成 MyBatis 操作
在 model 包中, 创建 User 实体类
@Data
public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;
}
在 mapper 包中, 创建 UserMapper 接口
这个接口中 主要是完成
- 注册, 插入一个用户
- 登录的时候, 通过名字查询当前用户是否存在.
@Mapper
public interface UserMapper {// 注册一个用户, 初始的天梯积分默认为1000, 场次默认为0int insert(User user);// 通过username查询当前用户是否存在User selectByName(String username);
}
在 resources 目录下, 创建一个目录 mapper, 在目录下创建 UserMapper.xml
在 UserMapper.xml 中写对应UserMapper接口中对应的操作
<?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.gobang.mapper.UserMapper"><insert id="insert">insert into user values(null,#{username},#{password},1000,0,0)</insert><select id="selectByName" resultType="com.example.gobang.model.User">select * from user where username = #{username}</select>
</mapper>
创建 service 包, 在包下创建 UserService 类, 这个类调用 Mapper接口中的方法
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public int insert(User user){return userMapper.insert(user);}public User selectByName(String username){return userMapper.selectByName(username);}
}
5.5 后端的实现
创建 controller包, 在包下创建一个 UserController 类
这个类是实现登录模块的功能的
- 这里需要注入 UserService, 调用数据库中的方法
- 还需要注入 BCryptPasswordEncoder, 对密码进行加密和比较
5.5.1 登录功能后端实现
注意这里的登录.
- 首先去数据库根据用户名查询是否存在当前用户.
- 如果不存在, 登录失败.
- 如果存在, 用输入的密码, 和数据库中的密码进行比较, 看是否相等. (注: 数据中的密码是加密的)
- 如果不相等, 登录失败.
- 如果相等, 创建 session, 并登录成功.
@RequestMapping("/login")public ResponseBodyMessage<User> login(@RequestBody User user, HttpServletRequest request) {User truUser = userService.selectByName(user.getUsername());if (truUser == null) {System.out.println("登录失败!");return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);}else {boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());if (!flg) {return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);}System.out.println("登录成功!");HttpSession session = request.getSession(true);session.setAttribute(Constant.USER_SESSION_KEY,truUser);return new ResponseBodyMessage<>(1,"登录成功!",truUser);}}
5.5.2 注册功能后端实现
- 首先查看是否该用户是否存在
- 存在, 就注册失败
- 不存在, 就进行注册, 首先对当前密码进行加密.
- 加密之后对这个用户添加到数据库中.
@RequestMapping("/register")public ResponseBodyMessage<User> register(@RequestBody User user) {User truUser = userService.selectByName(user.getUsername());if (truUser != null) {return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",user);} else{String password = bCryptPasswordEncoder.encode(user.getPassword());user.setPassword(password);userService.insert(user);return new ResponseBodyMessage<>(1,"注册成功!",user);}}
5.5.3 注销功能
直接删除对应session 为
Constant.USER_SESSION_KEY
, 然后跳转到login.html
@RequestMapping("/logout")public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException, IOException {HttpSession session = request.getSession(false);// 拦截器的拦截, 所以不可能出现session为空的情况session.removeAttribute(Constant.USER_SESSION_KEY);response.sendRedirect("login.html");}
注意: 这里的Constant.USER_SESSION_KEY
是存储的 session 字符串, 由于该 字符串是不变的, 所以存入 Constant 类中.
5.6 前端的实现
5.6.1 登录前端实现
let loginButton = document.querySelector('#loginButton');loginButton.onclick = function() {let username = document.querySelector('#loginUsername');let password = document.querySelector('#loginPassword');if (username.value.trim() == ""){alert('请先输入用户名!');username.focus();return;}if (password.value.trim() == ""){alert('请先输入密码!');password.focus();return;}$.ajax({url: "user/login",method: "POST",data: JSON.stringify({username: username.value.trim(), password: password.value.trim()}),contentType: "application/json;charset=utf-8",success: function(data, status) {if(data.status == 1) {location.assign("index.html");}else{alert(data.message);username.value="";password.value="";username.focus();}}})}
5.6.2 注册前端实现
let Reg = document.querySelector('#Reg');Reg.onclick = function() {let username = document.querySelector('#RegUsername');let password1 = document.querySelector('#RegPassword1');let password2 = document.querySelector('#RegPassword2');if(!$('#checkbox').is(':checked')) {alert("请勾选条款");return;}if(username.value.trim() == ""){alert("请先输入用户名!");username.focus();return;}if(password1.value.trim() == ""){alert('请先输入密码!');password1.focus();return;}if(password2.value.trim() == ""){alert('请再次输入密码!');password2.focus();return;}if(username.value.trim().length > 20) {alert("用户名长度过长");username.value="";username.focus();return;}if(password1.value.trim() != password2.value.trim()) {alert('两次输入的密码不同!');passwrod1.value="";password2.value="";return;}if(password1.value.trim().length > 255) {alert("当前密码长度过长!");password1.value="";password2.value="";password1.focus();return;}$.ajax({url: "user/register",method: "POST",data: JSON.stringify({username: username.value.trim(), password: password1.value.trim()}),contentType: "application/json;charset=utf-8",success: function(data,status){if(data.status == 1) {alert(data.message);location.assign("login.html");}else{alert(data.message);username.value="";password1.value="";password2.value="";username.focus();}}})}
5.7 添加拦截器
LoginIntercepter
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(Constant.USER_SESSION_KEY) != null){return true;}response.sendRedirect("/login.html");return false;}
}
AppConfig
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registry) {LoginInterceptor loginInterceptor = new LoginInterceptor();registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/**/login.html").excludePathPatterns("/**/css/**.css").excludePathPatterns("/**/images/**").excludePathPatterns("/**/fonts/**").excludePathPatterns("/**/js/**.js").excludePathPatterns("/**/scss/**").excludePathPatterns("/**/user/login").excludePathPatterns("/**/user/register").excludePathPatterns("/**/user/logout");}
}
6. 大厅界面
6.1 交互接口设计
这里客户端1, 点击匹配发送消息给服务器, 客户端2, 也点击匹配发送消息给服务器, 当服务器收到两个人的请求之后, 就需要服务器主动向客户端发送消息, 这里就需要用到 websocket
URL: ws://127.0.0.1:8080/findMatch
匹配请求
{message: ' startMatch ' / ' stopMatch'
}
匹配响应1 这个响应是点击匹配之后, 立刻返回的响应
{status: '1' / '-1' message: ' startMatch ' / ' stopMatch '
}
匹配响应2 这个响应是匹配成功之后的响应
{status: '1' / '-1'message: 'matchSuccess'
}
6.2 用户加载前后交互接口
请求
GET /user/userInfo HTTP/1.1响应
{status: 1/-1 (1 为成功, -1 为失败),message: "对应信息",data: "内容", (用户信息)
}
6.3 前端和后端实现用户信息加载
6.3.1 后端的实现
根据当前存储的session对象, 来查找对应的用户
@RequestMapping("/userInfo")public ResponseBodyMessage<User> getUserInfo(HttpServletRequest request) {HttpSession session = request.getSession(false);User user = (User) session.getAttribute(Constant.USER_SESSION_KEY);if (user == null) {return new ResponseBodyMessage<>(-1,"当前用户不存在",null);}else{return new ResponseBodyMessage<>(1,"查找成功!", newUser);}}
6.3.2 前端的实现
load();function load() {$.ajax({url: "user/userInfo",method: "GET",success:function(data) {if(data.status == 1) {let h2 = document.querySelector('#myname');h2.innerHTML = "你好! " + data.data.username;let game = document.querySelector('#gameMes');game.innerHTML = "天梯分数: " + data.data.score + " | " + "场数: " + data.data.totalCount + " | " + "获胜场数: "+ data.data.winCount;}else{alert(data.message);location.assign("login.html");}}})}
6.4 实现匹配功能的前端代码
let websocketUrl = 'ws://'+ location.host +'/findMatch';let websocket = new WebSocket(websocketUrl);// 连接成功的时候调用的方法websocket.onopen = function() {console.log("onopen");}// 连接关闭的时候调用的方法websocket.onclose = function() {console.log("onclose");}// 连接异常的时候调用的方法websocket.onerror = function() {console.log("onerrot");}// 监听整个窗口关闭的事件, 当窗口关闭, 主动的去关闭websocket连接window.onbeforeunload = function() {websocket.close();}// 连接成功收到的响应websocket.onmessage = function(e) {// 先将Json格式 e 化为 响应对象let resp = JSON.parse(e.data);// 获取到 匹配按钮let play = document.querySelector('#beginPlay');// 等于-1是错误的起来, 打印错误的信息, 并跳转到登录页面if (resp.status == -1) {alert(resp.message);location.assign("login.html");return;}// 这里就都是正常的响应, 那么就判断是开始匹配, 还是结束匹配if (resp.message == 'startMatch') {//开始匹配console.log("开始匹配");play.innerHTML = '匹配中...(点击停止)';}else if(resp.message == 'stopMatch') {//结束匹配console.log("结束匹配");play.innerHTML = '开始匹配';}else if(resp.message == 'matchSuccess') {//匹配成功console.log("匹配成功");location.assign('room.html');}else{// 按理不会触发这个elsealert(resp.message);console.log("收到非法响应");}}// 获取到匹配按钮let play = document.querySelector('#beginPlay');// 匹配按钮点击事件play.onclick = function() {// 判断当前 readyState 是否是OPEN状态的if (websocket.readyState == websocket.OPEN) {// 当前 readyState 处于OPEN 状态, 说明链接是好的if (play.innerHTML == '开始匹配') {// 发送开始匹配的请求websocket.send(JSON.stringify({message: 'startMatch',}))}else if(play.innerHTML == '匹配中...(点击停止)'){// 发送停止匹配的请求websocket.send(JSON.stringify({message: 'stopMatch',}))}}else{// 这里就是链接异常的情况alert('当前您的链接已经断开, 请重新登录');location.assign("login.html");}}
6.5 实现匹配功能的后端代码
这里是触发url的响应地址
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer{@Autowiredprivate MatchController matchController;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(matchController,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());}
}
6.5.1 创建在线状态
当用户登录的时候, 就让用户状态添加到哈希表中
由于这里是 多线程的状态下, 很多用户访问同一个哈希表就会出现线程安全的问题, 所以这里就使用 ConcurrentHashMap, 确保了线程安全问题.
- 这里存储的, key是用户的Id, value是对应的WebSocketSession的信息.
- 提供三个方法
- 进入房间的时候, 将用户的状态存入哈希表中
- 退出房间的时候, 将用户的状态从哈希表中删除
- 获取当前用户的 WebSocketSession 信息
@Component
public class OnlineUserManager {// 这个哈希表是表示当前用户在游戏大厅的在线状态private ConcurrentHashMap<Integer, WebSocketSession> gameState = new ConcurrentHashMap<>();public void enterGameIndex(int userId, WebSocketSession webSocketSession) {gameState.put(userId,webSocketSession);}public void exitGameHall(int userId) {gameState.remove(userId);}public WebSocketSession getState(int userId) {return gameState.get(userId);}
}
6.5.2 创建房间对象
房间对象, 每一房间中, 会有RoomId, 和2个用户信息.
所以这里需要有一个完全不可重复的RoomId, 这里就使用Java中的 UUID来解决
// 游戏房间
@Data
public class Room {private String roomId;private User user1;private User user2;public Room() {this.roomId = UUID.randomUUID().toString();}
}
6.5.3 创建房间管理器
按理 也是使用哈希表存储, 也有线程安全问题, 所以也使用ConcurrentHashMap
提供3个方法
- 添加用户进入到房间
- 删除房间中的用户
- 提供房间Id得到房间对象
// 房间管理器
@Component
public class RoomManager {private ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();public void insert(Room room) {rooms.put(room.getRoomId(),room);}public void remove(String roomId) {rooms.remove(roomId);}public Room findRoomByRoomId(String roomId) {return rooms.get(roomId);}
6.5.4 创建匹配队列
匹配队列, 首先按照分数将用户分为三个等级.
<2000
, 属于简单用户>= 2000 && < 3000
, 属于普通用户>=3000
, 属于高级用户
// 创建匹配队列 按等级划分// 1. < 2000private Queue<User> simpleQueue = new LinkedList<>();// 2. >= 2000 && < 3000private Queue<User> normalQueue = new LinkedList<>();// 3. >= 3000private Queue<User> highQueue = new LinkedList<>();
这里就通过队列来分为为三个等级, 来完成匹配和退出
- 点击匹配的时候, 按照用户当前的等级, 将用户入队
- 取消匹配的时候, 按照用户当前的等级, 将用户从队列中删除
- 创建三个线程, 一直循环的去对应等级队列中进行获取用户, 如果当前队列中的用户有2个以上的时候, 就进行匹配.
这里也有线程安全的问题, 这里同一个队列中, 用户并发的入队, 和删除用户操作, 就会产生线程安全的问题. 如果是不同的队列, 就不涉及线程安全的问题
解决办法: 对于同一个队列中的操作进行加锁.
问题2: 这里的三个线程, 是循环的去等待, 如果当前队列中迟迟没有人进来, 而线程还是循环的执行下去, 这样的资源消耗就非常的大.
所以在进行判断当前用户是否有2个以上的时候, 如果当前用户小于2个, 就将当前的队列进行wait(), 直到再次有用户加入进来的时候,就解锁, 再去判断当前用户是否有2个以上的用户.
// 匹配器, 这个类是用来完成匹配功能的
@Component
public class Matcher {// 创建匹配队列 按等级划分// 1. < 2000private Queue<User> simpleQueue = new LinkedList<>();// 2. >= 2000 && < 3000private Queue<User> normalQueue = new LinkedList<>();// 3. >= 3000private Queue<User> highQueue = new LinkedList<>();@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RoomManager roomManager;/*** 将当前玩家添加到匹配队列中* @param user*/public void insert(User user) {// 按等级加入队列中if (user.getScore() < 2000) {synchronized (simpleQueue) {simpleQueue.offer(user);// 只要有用户进入了, 就进行唤醒simpleQueue.notify();}}else if (user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (normalQueue) {normalQueue.offer(user);normalQueue.notify();}}else {synchronized (highQueue) {highQueue.offer(user);highQueue.notify();}}}/*** 将当前玩家匹配队列中删除* @param user*/public void remove(User user) {// 按照当前等级去对应匹配队列中删除if (user.getScore() < 2000) {synchronized (simpleQueue){simpleQueue.remove(user);}}else if (user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (normalQueue) {normalQueue.remove(user);}}else {synchronized (highQueue) {highQueue.remove(user);}}}/*** 这里使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配*/public Matcher() {// 创建三个线程, 操作三个匹配队列Thread t1 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(simpleQueue);}}};t1.start();Thread t2 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(normalQueue);}}};t2.start();Thread t3 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(highQueue);}}};t3.start();}private void handlerMatch(Queue<User> matchQueue) {synchronized (matchQueue) {try{// 1. 先查看当前队列中的元素个数, 是否满足两个// 这里使用while, 以防为0的时候, 被唤醒,然后没有再次判断导致进入下面操作.while (matchQueue.size() < 2) {// 用户小于2个的时候, 就进行等待, 以免浪费资源matchQueue.wait();}// 2. 尝试从队列中取出两个玩家User player1 = matchQueue.poll();User player2 = matchQueue.poll();// 打印日志System.out.println("匹配到的两个玩家: " + player1.getUsername()+ " , " + player2.getUsername());// 3. 获取到玩家的 websocket 的会话.WebSocketSession session1 = onlineUserManager.getState(player1.getUserId());WebSocketSession session2 = onlineUserManager.getState(player2.getUserId());// 再次判断是否为空if (session1 == null && session2 != null) {matchQueue.offer(player2);return;}if (session1 != null && session2 == null) {matchQueue.offer(player1);return;}if (session1 == null && session2 == null) {return;}if (session1 == session2) {matchQueue.offer(player1);return;}// 4. 把两个玩家放入一个游戏房间中Room room = new Room();roomManager.insert(room,player1.getUserId(),player2.getUserId());// 5. 给玩家反馈信息, 通知匹配到了对手MatchResponse response1 = new MatchResponse();response1.setStatus(1);response1.setMessage("matchSuccess");String json1 = objectMapper.writeValueAsString(response1);session1.sendMessage(new TextMessage(json1));MatchResponse response2 = new MatchResponse();response2.setMessage("matchSuccess");response2.setStatus(1);String json2 = objectMapper.writeValueAsString(response2);session2.sendMessage(new TextMessage(json2));} catch (IOException | InterruptedException e) {e.printStackTrace();}}}
}
6.5.5 写完 MatchController
websocket 有4个方法.
- 连接成功的时候调用的方法, 这里需要去判断多开的问题, 由于用户同时登录一个账号的时候, 就会出现多开, 解决办法就是查询当前用户的在线状态, 如果当前用户在线, 就退出当前登录. 如果没有多开就设置登陆状态
- 异常关闭的情况, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
- 退出的时候调用的方法, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
- 处理收到请求的方法, 通过前端发来的请求, 判断是否是开始匹配还是停止匹配.
@Component
public class MatchController extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate Matcher matcher;// 连接成功的时候就会调用该方法@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// 玩家上线// 1. 获取用户信息User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 2. 判断当前用户是否已经登录if (onlineUserManager.getState(user.getUserId()) != null ) {// 当前用户已经登录MatchResponse message = new MatchResponse();message.setMessage("当前用户已经登录!");message.setStatus(-1);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(message)));session.close();return;}// 3. 设置在线状态onlineUserManager.enterGameIndex(user.getUserId(),session);}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 处理开始匹配 和 停止匹配User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);String payload = message.getPayload();MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class);MatchResponse matchResponse = new MatchResponse();if (matchRequest.getMessage().equals("startMatch")) {// 进入匹配队列// 创建匹配队列, 加入用户matcher.insert(user);// 返回响应给前端matchResponse.setStatus(1);matchResponse.setMessage("startMatch");}else if(matchRequest.getMessage().equals("stopMatch")) {// 退出匹配队列// 创建匹配队列, 将用户移除matcher.remove(user);matchResponse.setMessage("stopMatch");matchResponse.setStatus(1);}else{matchResponse.setStatus(-1);matchRequest.setMessage("非法匹配");// 非法情况}session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(matchResponse)));}// 异常情况@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 玩家下线// 1. 获取用户信息User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session) {// 2. 设置在线状态onlineUserManager.exitGameHall(user.getUserId());}matcher.remove(user);}// 关闭情况@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 玩家下线// 1. 获取用户信息User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session) {// 2. 设置在线状态onlineUserManager.exitGameHall(user.getUserId());}matcher.remove(user);}
}
6.6 大厅界面总结
- 这里要注意多线程环境下, 多个用户同时使用同一个哈希表的时候, 进行添加和删除的时候, 会有线程安全的问题, 那么这里就需要使用 ConcurrentHashMap
- 在多线程环境下, 按照等级分的队列, 在多线程环境下, 并发的进行入队的时候, 删除的队列中用户的时候, 也会有线程安全问题, 这里针对同一个队列就可以进行加锁.
- 由于创建3个线程循环的进入队列中查看是否满足2个用户, 如果当前的环境下, 用户特别少, 一直去循环的进入, 会造成CPU占用率特别高, 所以这里就使用wai()等待, 在有用户进入匹配队列的时候,再去唤醒notify().
- 防止多开, 多个地方登录同一个账号就会出现很多问题, 这里在进行连接的时候判断, 如果用户已经在线, 就不让该地方用户登录.
- 要想让 房间是第一无二, 就需要使用 UUID, 那么roomId也要使用 字符串的格式.
7. 房间界面
7.1 交互接口设计
连接URL
ws://127.0.0.1:8080/game
当双方玩家都已经连接好了 发送响应
{message: 'gameReady' status: '1 / -1' (1是正常响应, -1 是错误响应) roomId: ' ' thisUserId: ' ' (自己用户Id)thatUserId: ' ' (对方用户Id)whiteUser: ' ' (先手方)
}
落子的时候的请求
{message: ' putChess ' userId: ' ' (落子的用户Id)row: ' ' (落子的第几行)col: ' ' (落子的第几列)
}
落子的时候的响应
{message: 'putChess;userId: ' 'row: ' 'col: ' 'winner: ' ' (获胜者, 和用户Id一致, 如果没有获胜, 就是0)
}
7.2 实现房间界面前端代码
7.2.1 设置棋盘界面, 以及显示框.
这里的 canvas 是用来绘制棋盘的,
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 href="css/game_room.css" rel="stylesheet" type="text/css" media="all" />
</head>
<body><div class="container"><div class="one"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示区域 --><div id="screen"> 等待玩家连接中... </div></div></div><script src="js/script.js"></script>
</body>
</html>
game_room.css
* {margin: 0;padding: 0;box-sizing: border-box;
}
html, body {height: 100%;background-image: url(../images/bg.jpg);background-repeat: no-repeat;background-position: center;background-size: cover;
}
.container {height: 100%;display: flex;justify-content: center;align-items: center;
}#screen {width: 450px;height: 50px;margin-top: 10px;background-color: #fff;font-size: 22px;line-height: 50px;text-align: center;
}
.backButton {width: 450px;height: 50px;font-size: 20px;color: white;background-color: orange;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;
}.backButton:active {background-color: gray;
}
7.2.2 对应的js文件
- 这里的
setScreenText
这个方法是用来将显示框中的内容, 根据当前是谁下棋来改变内容.- 这里的
initGame
这个方法是用来初始画棋盘的, 棋盘大小为 15 * 15- 内部的
oneStep
是当点击下子之后, 会绘制对应颜色的棋子.- 注意这里的棋盘数组, 为0是没有落子, 为1是落子了.
- 这里的gameInfo, 内部内容是全局的.用来接收传过来的响应
let 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
//
// TODO//
// 初始化一局游戏
//
function initGame() {// 是我下还是对方下. 根据服务器分配的先后手情况决定let me = gameInfo.isWhite;// 游戏是否结束let over = false;let chessBoard = [];//初始化chessBord数组(表示棋盘的数组)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/sky.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;}}// TODO 实现发送落子请求逻辑, 和处理落子响应逻辑.
}initGame();
7.2.3 初始化 websocket
- 在服务器传过来请求的时候, 两个用户都已经准备好了, 首先判断是否是正确的请求.
- 在请求是正确的时候, 将传过来的信息存入到
gameInfo
中, 注意这里的isWhite 是判断是否是先手方.- 注意只有2个人都建立连接了, 才初始画棋盘, 所以在这里初始化棋盘为好.
- 棋盘绘制好之后, 在显示框中, 显示对应的信息, 调用对应的
setScreenText
方法
let websocketUrl = 'ws://'+ location.host +'/game';
let websocket = new WebSocket(websocketUrl);websocket.onopen = function() {console.log("房间链接成功!");
}
websocket.onclose = function() {console.log("房间断开链接");
}
websocket.onerror = function() {console.log("房间出现异常");
}
window.onbeforeunload = function() {websocket.close();
}
websocket.onmessage = function(e) {console.log(e.data);let resp = JSON.parse(e.data);if(resp.message != 'gameReady') {console.log("响应类型错误");location.assign("index.html");return;}if(resp.status == -1) {alert("游戏链接失败!");location.assign("index.html");return;}gameInfo.roomId == resp.roomId;gameInfo.thisUserId = resp.thisUserId;gameInfo.thatUserId = resp.thatUserId;gameInfo.isWhite = resp.whiteUser == resp.thisUserId;// 初始化棋盘initGame();// 设置显示内容setScreenText(gameInfo.isWhite);
}
7.2.4 落子时,发送落子请求
在初始化棋盘之后, 在点击的时候, 发送落子请求
注意发送的对应的格式
function send(row,col) {let req = {message: 'putChess',userId: gameInfo.thisUserId,row: row,col: col};websocket.send(JSON.stringify(req));}
7.2.5 落子时, 发送落子响应
- 注意这里的响应是在落子之后, 所以要写在initGame() 中
- 在接收的时候, 首先将JSON格式响应转成可以接收的格式
- 判断响应是否正常, 排除响应错误的情况
- 判断当前是自己落子还是对方落子, 然后根据落子绘制棋子
- 落子之后, 交换落子的权利, 然后将显示的内容改变.
- 再次去判断是否游戏结束. 结束的时候,在显示框显示获胜信息, 并添加一个返回大厅的按钮, 以免直接返回了(用户看不到失败的信息.
websocket.onmessage = function(e) {console.log(e.data);let resp = JSON.parse(e.data);if (resp.message != 'putChess') {console.log("响应类型错误!");location.assign("index.html")return;}if (resp.userId == gameInfo.thisUserId) {// 自己落子oneStep(resp.col, resp.row, gameInfo.isWhite);chessBoard[resp.row][resp.col] = 1;} else if (resp.userId == gameInfo.thatUserId) {// 别人落子oneStep(resp.col, resp.row, !gameInfo.isWhite);chessBoard[resp.row][resp.col] = 1;}else{// 落子异常console.log("userId 异常");return;}// 交换落子me = !me;setScreenText(me);// 判断游戏是否结束let screenDiv = document.querySelector('#screen');if (resp.winner != 0) {console.log(resp.winner+" " + gameInfo.thisUserId+" " + gameInfo.thatUserId);if (resp.winner == gameInfo.thisUserId) {screenDiv.innerHTML = "恭喜你, 获胜了!";}else if(resp.winner == gameInfo.thatUserId) {screenDiv.innerHTML = "游戏结束, 失败了!";}else {console.log("winner 错误");alert("当前 winner字段错误 winner = "+ resp.winner);}// location.assign('index.html');// 增加一个按钮, 返回游戏大厅let backBtn = document.createElement('button');backBtn.innerHTML = "返回游戏大厅";backBtn.className = "backButton";let one = document.querySelector('.one');backBtn.onclick = function() {location.assign("index.html");}one.appendChild(backBtn);}
7.3 实现房间界面后端代码
7.3.1 注册GameController
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{@Autowiredprivate GameController gameController;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(gameController,"/game").addInterceptors(new HttpSessionHandshakeInterceptor());}
7.3.2 创建GameController
afterConnectionEstablished
这个方法是在建立连接时候的方法.handleTextMessage
这个方法是接收发送的响应handleTransportError
这个方法是出现异常的时候执行的afterConnectionClosed
这个方法是关闭websocket的时候执行的
@Component
public class GameController 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 {}
}
7.3.3 创建对应的响应类和请求类
双方进入房间准备就绪的响应
// 客户端链接成功后, 返回的响应
@Data
public class GameReadyResponse {private String message;private int status;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;
}
落子请求
// 落子的请求
@Data
public class GameRequest {private String message;private int userId;private int row;private int col;
}
落子响应
//落子响应
@Data
public class GameResponse {private String message;private int userId;private int row;private int col;private int winner;
}
7.3.4 完成用户房间在线状态管理
在之前的 OnlineUserManager 中添加代码
- enterGameRoom, 进入房间添加到哈希表中(上线)
- exitGameRoom, 退出房间从哈希表中删除(下线)
- getRoomState, 获取当前用户的websocketsession信息
// 这个哈希表是表示当前用户在游戏房间的在线状态private ConcurrentHashMap<Integer, WebSocketSession> roomState = new ConcurrentHashMap<>();public void enterGameRoom(int userId, WebSocketSession webSocketSession){roomState.put(userId,webSocketSession);}public void exitGameRoom(int userId) {roomState.remove(userId);}public WebSocketSession getRoomState(int userId) {return roomState.get(userId);}
7.3.5 添加 MyBatis 用来更新玩家积分
UserMapper
// 总场数 + 1, 获胜场数+1, 天梯分数 + 50void userWin(int userId);// 总场数 + 1, 天梯分数 -50void userLose(int userId);
UserMapper.xml
<update id="userWin">update user set totalCount = totalCount+1 , winCount = winCount+1, score = score + 50 where userId = #{userId}</update><update id="userLose">update user set totalCount = totalCount+1, score = score - 50 where userId = #{userId}</update>
UserService
// 总场数 + 1, 获胜场数+1, 天梯分数 + 50public void userWin(int userId){userMapper.userWin(userId);}// 总场数 + 1, 天梯分数 -50public void userLose(int userId) {userMapper.userLose(userId);}
7.3.6 完成处理连接方法
- 首先获取用户的信息
- 判断当前是否已经进入房间了, 防止未匹配成功
- 判断是否多开, 这里要查询房间在线情况, 和大厅在线情况.
- 然后让用户房间的在线状态处于在线.
- 首先判断用户1是否上线, 上线就添加到当前房间来, 用户2再上线的时候也添加房间来, 这里可以设置谁是先手方, 根据自己设定的规则.我这里是随机取0~9的数字, 如果是偶数用户1就是先手, 如果是奇数用户2就是先手
- 当用户都进入房间的时候, 通知玩家准备就绪了
- 注意这里的线程安全问题. 多个用户进入同一个方法,就有可能出现线程安全问题, 由于是同一个房间的用户进行, 只需要对房间对象加锁就可以了.
@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse readyResponse = new GameReadyResponse();// 获取用户信息User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 判断当前是否已经进入房间Room room = roomManager.findRoomByUserId(user.getUserId());if (room == null) {readyResponse.setStatus(-1);readyResponse.setMessage("用户尚未匹配到!");session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 判断当前是否多开if (onlineUserManager.getRoomState(user.getUserId()) != null || onlineUserManager.getState(user.getUserId()) != null) {readyResponse.setMessage("当前用户已经登录!");readyResponse.setStatus(-1);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 上线onlineUserManager.enterGameRoom(user.getUserId(), session);synchronized (room) {if (room.getUser1() == null) {room.setUser1(user);System.out.println("玩家1 " + user.getUsername() + " 已经准备好了");return;}if (room.getUser2() == null) {room.setUser2(user);System.out.println("玩家2 " + user.getUsername() + " 已经准备好了");Random random = new Random();int num = random.nextInt(10);if (num % 2 == 0) {room.setWhiteUser(room.getUser1().getUserId());} else{room.setWhiteUser(room.getUser2().getUserId());}// 通知玩家1noticeGameReady(room,room.getUser1(),room.getUser2());// 通知玩家2noticeGameReady(room,room.getUser2(),room.getUser1());return;}}readyResponse.setStatus(-1);readyResponse.setMessage("房间已经满了");session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));}
private void noticeGameReady(Room room, User user1, User user2) throws IOException {GameReadyResponse resp = new GameReadyResponse();resp.setStatus(1);resp.setMessage("gameReady");resp.setRoomId(room.getRoomId());resp.setThisUserId(user1.getUserId());resp.setThatUserId(user2.getUserId());resp.setWhiteUser(room.getWhiteUser());WebSocketSession webSocketSession = onlineUserManager.getRoomState(user1.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}
7.3.7 完成处理连接断开的方法和连接异常的方法
- 首先获取用户的信息
- 然后设置用户房间状态为下线
- 注意这里掉线了, 就需要判断对方赢了.
- 判断对方是否掉线, 如果对方也掉线了, 就无需通知谁赢了
- 如果对方没有掉线, 就通知对方赢了
- 获胜之后, 要对玩家的信息, 场次, 胜场进行更新. 然后关闭房间
@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 异常下线// 下线User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session) {onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: " + user.getUsername()+" 异常下线了");noticeThatUserWin(user);}
@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 下线User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session) {onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: " + user.getUsername()+" 离开房间");noticeThatUserWin(user);}
private void noticeThatUserWin(User user) throws IOException {Room room = roomManager.findRoomByUserId(user.getUserId());if (room == null) {System.out.println("房间已经关闭");return;}// 找到对手User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();// 找到对手的状态WebSocketSession session = onlineUserManager.getRoomState(thatUser.getUserId());if (session == null) {// 都掉线了System.out.println("都掉线了, 无需通知");return;}// 这里通知对手获胜GameResponse gameResponse = new GameResponse();gameResponse.setMessage("putChess");gameResponse.setUserId(thatUser.getUserId());gameResponse.setWinner(thatUser.getUserId());session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameResponse)));// 更新玩家分数信息int winId = thatUser.getUserId();int loseId = user.getUserId();userService.userWin(winId);userService.userLose(loseId);// 释放房间对象roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());}
7.3.8 在房间管理器中添加代码
- 添加哈希表, 管理用户对应的房间号
- key为用户的Id, value为用户的对应房间号
private ConcurrentHashMap<Integer,String> Ids = new ConcurrentHashMap<>();public void insert(Room room,int userId1, int userId2) {Ids.put(userId1,room.getRoomId());Ids.put(userId2,room.getRoomId());}public void remove(String roomId,int userId1, int userId2) {Ids.remove(userId1);Ids.remove(userId2);}public Room findRoomByUserId(int userId) {String roomId = Ids.get(userId);if (roomId == null) {return null;}return rooms.get(roomId);}
7.3.9 Room类添加棋盘代码
- 这里的 Constant.ROW 和 Constant.COL 都是不变的常量. 放到 Constant类中. 这里初始化的棋盘数组也是15 * 15的
- 这里Room要注入Spring对象, 不能使用@Autowired @Resource注解. 需要使用context
修改启动类
public class GobangApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) {context = SpringApplication.run(GobangApplication.class, args);}}
// 游戏房间
@Data
public class Room {private String roomId;private User user1;private User user2;private int whiteUser;private OnlineUserManager onlineUserManager;private RoomManager roomManager;private UserService userService;public Room() {this.roomId = UUID.randomUUID().toString();onlineUserManager = GobangApplication.context.getBean(OnlineUserManager.class);roomManager = GobangApplication.context.getBean(RoomManager.class);userService = GobangApplication.context.getBean(UserService.class);}// 为0就是为落子, 为1就是用户1落子, 为2就是用户2落子private int[][] board= new int[Constant.ROW][Constant.COL];private ObjectMapper objectMapper = new ObjectMapper();}
7.3.10 实现handleTextMessage方法
落子请求
@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 获取用户对象User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 根据 玩家 Id 获取房间对象Room room = roomManager.findRoomByUserId(user.getUserId());// 通过room对象处理这次请求room.putChess(message.getPayload());}
7.3.11 实现putChess方法
- 注意这里的落子, 与前端不同, 这里的棋盘数组, 为0就是没落子, 为1就是用户1落得子, 为2就是用户2落得子
- 每次落子都要进行胜负判断, 使用checkWinner方法来实现
- 给房间中的用户返回响应
- 注意这里的玩家掉线的情况
- 如果胜负已分, 更新玩家获胜的信息, 并销毁房间
// 这个方法是用来处理一次落子的操作public void putChess(String reqJson) throws IOException {// 1. 记录当前落子的位子GameRequest request = objectMapper.readValue(reqJson,GameRequest.class);GameResponse response = new GameResponse();// 1.1 判断当前落子是谁int 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,chess);// 3. 给房间中所有的客户端返回响应response.setMessage("putChess");response.setRow(row);response.setCol(col);response.setWinner(winner);response.setUserId(request.getUserId());WebSocketSession session1 = onlineUserManager.getRoomState(user1.getUserId());WebSocketSession session2 = onlineUserManager.getRoomState(user2.getUserId());// 这里对下线进行判断if (session1 == null) {// 玩家1下线response.setWinner(user2.getUserId());System.out.println("玩家1掉线");}if (session2 == null) {// 玩家2下线, 就认为玩家1获胜System.out.println("玩家2掉线");}String respJson = objectMapper.writeValueAsString(response);if (session1 != null) {session1.sendMessage(new TextMessage(respJson));}if (session2 != null) {session2.sendMessage(new TextMessage(respJson));}// 4. 如果当前获胜, 销毁房间if (response.getWinner() != 0) {System.out.println("游戏结束, 房间即将销毁");// 更新获胜方的信息int winId = response.getWinner();int LoseId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();userService.userLose(LoseId);userService.userWin(winId);// 销毁房间roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}}
7.3.12 完成用户胜负判断
这里要判断四种情况
- 一行有五个子连珠
- 一列有五个子连珠
- 从左到右的斜着的五子连珠
- 从右到左的斜着的五子连珠
完成 checkWinner
方法
// 谁获胜就返回谁的Id, 如果还没有获胜者, 就返回0private int checkWinner(int row, int col, int chess) {// 判断当前是谁获胜// 1. 一行五子连珠for (int i = col -4 ;i >= 0 && i <= col && i <= Constant.COL-5; i++) {if (board[row][i] == chess&& board[row][i+1] == chess&& board[row][i+2] == chess&& board[row][i+3] == chess&& board[row][i+4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}}// 2. 一列五子连珠for (int i = row - 4; i >= 0 && i <= row && i <= Constant.ROW-5; i++) {if (board[i][col] == chess&& board[i+1][col] == chess&& board[i+2][col] == chess&& board[i+3][col] == chess&& board[i+4][col] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}}// 3. 斜着五子连珠 -> 左上到右下for (int i = row - 4, j = col - 4; i <= row && j <= col;j++,i++){try {if (board[i][j] == chess&& board[i+1][j+1] == chess&& board[i+2][j+2] == chess&& board[i+3][j+3] == chess&& board[i+4][j+4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e) {continue;}}// 4. 斜着五子连珠 -> 右上到左下for (int i = row+4,j=col-4; i>=row && j <= col; i--,j++) {try {if (board[i][j] == chess&& board[i-1][j+1] == chess&& board[i-2][j+2] == chess&& board[i-3][j+3] == chess&& board[i-4][j+4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e) {continue;}}return 0;}
基于 SpringBoot + MyBatis 的在线五子棋对战相关推荐
- 基于 SpringBoot + MyBatis 的在线音乐播放器
文章目录 4. 数据库的设计与实现 6.3 了解 MD5 加密 和 BCrypt 加密 6.4 在Config中 注入 BCryptPasswordEncoder 对象 6.5.1 LoginInte ...
- Jeecg-Boot 1.0 版本发布,基于SpringBoot+Mybatis+AntDesign快速开发平台
基于SpringBoot+Mybatis+AntDesign企业级快速开发平台 引言: Jeecg-Boot 一款基于代码生成器的J2EE快速开发框架! 采用前后端分离技术: Spring ...
- 基于springboot+mybatis+mysql+html实现宠物医院管理系统(包含实训报告)
基于springboot+mybatis+mysql+html实现宠物医院管理系统(包含实训报告) 一.系统简介 二.系统主要功能界面 2.1登陆 2.2系统设置 2.3宠物管理 2.4预约管理 2. ...
- 基于Springboot+mybatis+mysql+html图书管理系统2
基于Springboot+mybatis+mysql+html图书管理系统2 一.系统介绍 二.功能展示 1.用户登陆 2.用户主页 3.图书查询 4.还书 5.个人信息修改 6.图书管理(管理员) ...
- 基于springboot+mybatis+mysql+vue运动会报名管理系统
基于springboot+mybatis+mysql+vue运动会报名管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.报名详情(运行员) 3.比赛报名(运动员) 4.个人参赛项目(运动员) 5 ...
- 基于springboot+mybatis+mysql+vue房屋租赁管理系统
基于springboot+mybatis+mysql+vue房屋租赁管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.管理员端主要功能 2.房主角色端主要功能 3.租客角色端主要功能 三.其它系 ...
- 基于Springboot+mybatis+mysql+html教育培训中心教学系统
基于Springboot+mybatis+mysql+html教育培训中心教学系统 一.系统介绍 二.功能展示 1.用户登陆 2.用户注册 3.个人中心 4.人员信息管理 5.课程管理 6.缴费管理 ...
- 基于SpringBoot+MyBatis的餐饮点餐系统
基于SpringBoot+MyBatis的餐饮点餐系统,适用于毕业设计. package com.example.zxdmeal.controller;import com.alibaba.fastj ...
- 基于springboot+mybatis+mysql+html企业人事管理系统
基于springboot+mybatis+mysql+html企业人事管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.员工奖惩--员工 3.合同管理--员工 4.个人薪酬--员工 5.培训管理 ...
- 基于Springboot+Mybatis+mysql+vue技术交流博客论坛系统
基于Springboot+Mybatis+mysql+vue技术交流博客论坛系统 一.系统介绍 二.功能展示 1.主页(普通用户) 2.登陆.注册(普通用户) 3.博客(普通用户) 4.文章详情(点赞 ...
最新文章
- Java Websocket实例【服务端与客户端实现全双工通讯】
- 字节二面 | 26图揭秘线程安全
- Entity Framework 的事务 DbTransaction
- sqlite数据库插入和读取图片数据 (for ios)
- 自制仿360首页支持拼音输入全模糊搜索和自动换肤
- 华科考研834计算机网络,2018年华中科技大学834计算机专业基础综合 (模)...
- 《C++ Primer 5th》笔记(8 / 19):IO库
- java拦截器_springMVC入门(八)------拦截器
- 借条已经收回,他以没收据为由让我继续还款怎样办?
- IC设计EDA软件虚拟机环境配置与安装教程
- 实验前准备:CPU学习实验的头文件.vh
- 互联网发展阶段的三阶段是什么?从门户到搜索,到移动互联网
- 【数字基带传输】误码率的分析方法(BER Performance)
- IDEA使用Maven构建Spring+SpringMVC+MyBatis整合项目demo成功执行但控制台Tomcat Locahost log输出No Spring WebApplicationIn
- aop:aspectj-autoproxy /作用
- [保姆级教程]解决Centos 8下无法更新的问题 (附无痕迁徙到Rocky Linux的方法)
- Web 框架的替代方案来了!
- 强大的多语言版本在线图片处理网站
- 韩国计算机专业,韩国计算机专业发展前瞻
- refresh方法_Android Shake to Refresh教程
热门文章
- python爬虫使用seleium+超级鹰+Image模块自动登录12306(附源码和登录视频)
- Sourcetree下载-使用-快速入门
- fgo服务器维护更新,fgo命运冠位指定活动维护公告 12月2日更新
- Java 如何获取线程状态呢?
- hpm1005能扫描不能打印_「惠普m1005怎么扫描」HP惠普M1005打印机不能扫描文件该怎么办? - seo实验室...
- VIM 插件管理工具——vim-plug
- 开氏温度与摄氏度换算_8789 单位换算小技巧
- 【Word】论文公式居中,编号右对齐
- 运筹优化算法工程师面试问题汇总
- 使用python制作pdf的格式转换程序