目录
网页五子棋项目
一、项目核心流程
用户管理:使用服务器实现用户注册、登录功能,以及用户信息的管理,包括用户的天梯分数记录和比赛场次记录;
实时游戏互动:利用WebSocket技术实现客户端和服务器之间的实时通信,确保玩家的移动可以立即被对方看到,并由服务器进行处理;
匹配对战系统:设计一个机制以匹配具有相似技能水平的玩家进行对战。涉及到用户评分系统和等待室的实现,以便玩家可以根据自己的排名找到合适的对手;
游戏逻辑:使用服务器处理用户请求、维护游戏状态、执行匹配算法、管理用户会话以及提供必要的游戏逻辑支持,包括棋盘的初始化、落子规则的执行、胜负条件的判断以及棋局的更新显示;
界面设计:使用HTML、CSS和JavaScript技术构建用户友好的游戏界面,包括棋盘的可视化、棋子的放置和游戏状态的反馈;
数据库交互:使用数据库管理系统存储用户信息、游戏记录和其他相关数据,并提供数据的增删改查功能;
在接下来的流程中,我将项目分为了三个板块进行讲解,这样有利于大家的理解,分别是:
- 登录模块
- 游戏大厅模块
- 游戏对局模块
在这三个模块中以第一个模块最为简单,大家可以先提前适应一下,在2和3模块将会给大家上难度了
二、 登录模块
这个最为简单,只需要前端进行输入用户信息,后端进行数据处理并返回用户信息,前端以后端返回的数据为基础来判断是否要进行跳转进入游戏大厅界面。
- 前端输入用户信息,并判断数据正确性进行跳转
- 后端进行数据库查询用户信息
2.1 前端输入用户信息
进行前端的界面构建,并提示用户输入信息
前端代码如下:
<!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">
<h3 class="pa">五子棋对战</h3>
</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>
<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.userId > 0) {
// 登录成功
alert("登录成功!");
// 重定向跳转到 "游戏大厅页面".
location.assign('/game_hall.html');
} else {
alert("登录失败!");
}
},
error: function() {
// 请求执行失败之后的回调函数
alert("登录失败!");
}
});
}
</script>
</body>
</html>
在代码中可以看到,如果后端返回的数据类型为空或者为其他类型,前端则进行提示错误,提示用户输入正确的用户名和密码。
2.2 后端进行数据库查询用户信息
后端根据前端发出的响应,对拿到的前端的用户名在数据库中进行查找,与用户输入的密码比对是否正确,正确则返回用户的完整信息,并且将该用户的信息存储到服务器的Session中,以便在接下来游戏大厅的获取信息做准备。
后端代码如下:
@PostMapping("/login")
@ResponseBody
public Object login( String username, String password, HttpServletRequest req){
//判断传进来的是否为空字符串
if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password)){
log.info("用户输入错误");
return new UserInfo();
}
//从数据库中取出该用户的用户信息
UserInfo user=userService.selectByName(username);
//判断用户信息是否正确
if(user==null||!user.getPassword().equals(password)){
//不正确则返回空对象
log.info("用户登录失败");
return new UserInfo();
}
//用户信息正确
//将用户信息存储在Session中
HttpSession httpSession = req.getSession(true);
httpSession.setAttribute("user", user);
log.info(user.toString()+"===============================================");
return user;
}
三、 游戏大厅模块
接下来我们开始进入主题,开始正式上难度,游戏大厅的流程如下:
- 前端通过Ajax请求用户数据,后端从Session中拿取并从数据库中查询后返回
- 前后端建立WebSocket连接,并进行判断,同时后端进行用户在线处理
- 前端利用WebSocket发送按钮信息,后端进行等级队列的处理和维护游戏房间
3.1 前端通过Ajax请求用户数据,后端从Session中拿取并从数据库中查询后返回
前端请求后端该用户的数据信息,后端根据登录时存储的Session信息,拿到用户Id,根据用户Id进入数据库中进行查找,并返回给前端进行界面用户信息的显示
前端代码Ajax请求如下:
$.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("获取用户信息失败!");
}
});
后端根据前端的请求进行数据查询并返回:
后端该部分代码如下:
@RequestMapping("/userInfo")
@ResponseBody
public Object getUserInfo(HttpServletRequest req){
try {
HttpSession httpSession = req.getSession(false);
UserInfo user = (UserInfo) httpSession.getAttribute("user");
// 拿着这个 user 对象, 去数据库中找, 找到最新的数据
UserInfo newUser = userService.selectByName(user.getUsername());
return newUser;
}catch (Exception e){
return new UserInfo();
}
}
如图可见,已经显示出了该用户的数据库信息
3.2 前后端建立WebSocket连接,并进行判断,同时后端进行用户在线处理
什么是WebSocket?其实他的底层原理是Tcp来实现的,作用也和Tcp类似,都是用来建立一个传输通道,进行数据传输,在建立通道前由前端发送一个请求,这时服务器建立连接后应该返回一个类似于Ack的应答报文来告诉前端,咱俩已经连接成功了,可以发送信息了。
前端发送WebSocket连接请求代码如下:
// 此处进行初始化 websocket, 并且实现前端的匹配逻辑.
// 此处的路径必须写作 /findMatch, 千万不要写作 /findMatch/
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("onerror");
}
// 监听页面关闭事件. 在页面关闭之前, 手动调用这里的 websocket 的 close 方法.
window.onbeforeunload = function() {
websocket.close();
}
同时后端在建立请求时要进行一个逻辑的判断,通过维护一个Hash表,Key存储的是用户的Id,Value存储的是用户用来建立连接时WebSocketSession,然后就可以通过前端传入的用户Id在后端进行查询,如果查询到了,那么就意味着该用户已经登录了,没查询到的话就把该用户的信息放进去,表示该用户现在是在线状态。
后端进行连接处理的代码如下:
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 玩家上线, 加入到 OnlineUserManager 中
try {
UserInfo user = (UserInfo) session.getAttributes().get("user");
// 先判定当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑.
if (onlineUserManagerService.getFromGameHall(user.getUserId()) != null
|| onlineUserManagerService.getFromGameRoom(user.getUserId()) != null) {
// 当前用户已经登录了!!针对这个情况要告知客户端, 你这里重复登录了.
MatchResponse response = new MatchResponse();
response.setOk(true);
response.setReason("当前禁止多开!");
response.setMessage("repeatConnection");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
// 拿到了身份信息之后, 就把玩家设置成在线状态了
onlineUserManagerService.enterGameHall(user.getUserId(), session);
System.out.println("玩家 " + user.getUsername() + " 进入游戏大厅!");
} catch (NullPointerException e) {
System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
// 出现空指针异常, 说明当前用户的身份信息是空, 用户未登录呢.
// 把当前用户尚未登录这个信息给返回回去
MatchResponse response = new MatchResponse();
response.setOk(false);
response.setReason("您尚未登录! 不能进行后续匹配功能!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
}
3.3 前端利用WebSocket发送按钮信息,后端进行等级队列的处理和维护游戏房间
在WebSocket确定前后端已经建立好通道好就可以发送信息了,如下图可见,前端一共有两个按钮,一个是开始匹配,一个是停止匹配,前端通过用户点击的按钮来构建不同的数据通够WebSocket来发送给后端进行一个数据的处理
前端发送不同按钮信息的代码如下:
// 一会重点来实现, 要处理服务器返回的响应
websocket.onmessage = function(e) {
// 处理服务器返回的响应数据. 这个响应就是针对 "开始匹配" / "结束匹配" 来对应的
// 解析得到的响应对象. 返回的数据是一个 JSON 字符串, 解析成 js 对象
let resp = JSON.parse(e.data);
let matchButton = document.querySelector('#match-button');
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");
location.replace("/game_room.html");
} else if (resp.message == 'repeatConnection') {
alert("当前检测到多开! 请使用其他账号登录!");
location.replace("/login.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.replace('/login.html');
}
}
以上处理完毕后,开始由后端开始接收数据,进行一个数据处理的过程:
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 实现处理开始匹配请求和处理停止匹配请求.
UserInfo user = (UserInfo) session.getAttributes().get("user");
// 获取到客户端给服务器发送的数据
String payload = message.getPayload();
// 数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
MatchResponse response = new MatchResponse();
if (request.getMessage().equals("startMatch")) {
// 进入匹配队列
matcherService.add(user);
// 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
response.setOk(true);
response.setMessage("startMatch");
} else if (request.getMessage().equals("stopMatch")) {
// 退出匹配队列
matcherService.remove(user);
// 移除之后, 就可以返回一个响应给客户端了.
response.setOk(true);
response.setMessage("stopMatch");
} else {
response.setOk(false);
response.setReason("非法的匹配请求");
}
String jsonString = objectMapper.writeValueAsString(response);
session.sendMessage(new TextMessage(jsonString));
}
在该段代码中的主要部分是进行一个等级队列的处理,以及对游戏房间的维护有以下两部分组成:
- 天梯分数进行分级
package com.example.gobangproject.service;
import com.example.gobangproject.model.MatchResponse;
import com.example.gobangproject.model.UserInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
@Component
public class MatcherService {
@Autowired
private OnlineUserManagerService onlineUserManagerService;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private RoomManagerService roomManagerService;
private Queue<UserInfo> normalQueue=new LinkedList<>();
private Queue<UserInfo> middleQueue=new LinkedList<>();
private Queue<UserInfo> highQueue=new LinkedList<>();
// 操作匹配队列的方法.
// 把玩家放到匹配队列中
public void add(UserInfo 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 (middleQueue) {
middleQueue.offer(user);
middleQueue.notify();
}
System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue 中!");
} else {
synchronized (highQueue) {
highQueue.offer(user);
highQueue.notify();
}
System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中!");
}
}
// 当玩家点击停止匹配的时候, 就需要把玩家从匹配队列中删除
public void remove(UserInfo 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 (middleQueue) {
middleQueue.remove(user);
}
System.out.println("把玩家 " + user.getUsername() + " 移除了 highQueue!");
} else {
synchronized (highQueue) {
highQueue.remove(user);
}
System.out.println("把玩家 " + user.getUsername() + " 移除了 veryHighQueue!");
}
}
public MatcherService(){
Thread t1=new Thread(){
@Override
public void run(){
while (true){
handlerMatch(normalQueue);
}
}
};
Thread t2=new Thread(){
@Override
public void run(){
while (true){
handlerMatch(middleQueue);
}
}
};
Thread t3=new Thread(){
@Override
public void run(){
while (true){
handlerMatch(highQueue);
}
}
};
t1.start();
t2.start();
t3.start();
}
private void handlerMatch(Queue<UserInfo> matchQueue) {
synchronized (matchQueue) {
try {
while (matchQueue.size() < 2) {
matchQueue.wait();
}
// 尝试从队列中取出两个玩家
UserInfo player1 = matchQueue.poll();
UserInfo player2 = matchQueue.poll();
System.out.println("匹配出两个玩家: " + player1.getUsername() + ", " + player2.getUsername());
// 获取到玩家的 websocket 的会话
// 获取到会话的目的是为了告诉玩家, 你排到了~~
WebSocketSession session1 = onlineUserManagerService.getFromGameHall(player1.getUserId());
WebSocketSession session2 = onlineUserManagerService.getFromGameHall(player2.getUserId());
// 前面的逻辑里进行了处理, 当玩家断开连接的时候就把玩家从匹配队列中移除了.
if (session1 == null) {
// 如果玩家1 现在不在线了, 就把玩家2 重新放回到匹配队列中
matchQueue.offer(player2);
return;
}
if (session2 == null) {
// 如果玩家2 现在下线了, 就把玩家1 重新放回匹配队列中
matchQueue.offer(player1);
return;
}
if (session1 == session2) {
// 把其中的一个玩家放回匹配队列.
matchQueue.offer(player1);
return;
}
// 把这两个玩家放到一个游戏房间中.
RoomService roomService = new RoomService();
roomManagerService.add(roomService, player1.getUserId(), player2.getUserId());
// 此处是要给两个玩家都返回 "匹配成功" 这样的信息.
// 因此就需要返回两次
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 (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里的代码至关重要,我这里是根据用户天梯分数的不同来划分出来三个队列,分别是normalQueue、middleQueue、以及highQueue,然后将该用户放入队列中,通过创建三个线程来同时扫描这三个不同队列,这里请同学们一定要注意线程安全的问题,当线程扫描该队列时如果该队列内有没两个用户则线程等待,直到有用户再次放入时唤醒线程,当扫描时发现队列中有两个以上玩家,则将这两名用户进行游戏房间的维护,这是在第二段代码中了。
- 游戏房间的初步维护
package com.example.gobangproject.service;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RoomManagerService {
private ConcurrentHashMap<String, RoomService>rooms=new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer,String>userIdToRoomID=new ConcurrentHashMap<>();
public void add(RoomService roomService, Integer userId1, Integer userId2) {
rooms.put(roomService.getRoomId(), roomService);
userIdToRoomID.put(userId1, roomService.getRoomId());
userIdToRoomID.put(userId2, roomService.getRoomId());
}
public void remove(String roomId ,Integer userId1, Integer userId2){
rooms.remove(roomId);
userIdToRoomID.remove(userId1);
userIdToRoomID.remove(userId2);
}
public RoomService getRoomByRoomId(String roomId){
return rooms.get(roomId);
}
public RoomService getRoomByUserId(Integer userId){
String roomId=userIdToRoomID.get(userId);
if(roomId==null){
return null;
}
return rooms.get(roomId);
}
}
以上代码的主要意义:维护一个游戏房间,在里面创建两个Hash表,因为前面我们使用的并发编程,考虑到线程安全问题,这里我们通过使用ConcurrentHashMap来创建Hash表结构,然后通过UUid来给该房间生成一个唯一的房间id,然后Key设置为该房间Id,Value则设置成Room实体类,里面存储了这两个用户的ID,方便我们后续的操作,当两名用户放入游戏房间后,通过获取存储的UserId来获取该用户的WebSocketSession,然后将这个信息分别返回给这两名用户。
再后端处理完数据返回给前端之后,前端对后端的数据进行判定,如果数据正确则会进行页面跳转,开始进入游戏页面——game_room页面。
四、游戏对局模块
游戏对局模块至关重要,主要是将两名玩家在前端的下棋构造成一个响应返回给后端,在后端处理完成之后在返回给前端,前端根据数据来构造棋子显示在页面之上.
主要分为以下几大步骤:
- 前端构造新的WebSocket连接,后端在等待两名玩家同时连接上服务器后开始游戏
- 前端通过监听用户落子的下标返回给后端,后端维护一个二维数组来进行判断胜负并返回给前端
4.1 前端构造新的WebSocket连接,后端在等待两名玩家同时连接上服务器后开始游戏
前端代码如下:
// 此处写的路径要写作 /game, 不要写作 /game/
let websocketUrl = "ws://" + location.host + "/game";
let websocket = new WebSocket(websocketUrl);
websocket.onopen = function() {
console.log("连接游戏房间成功!");
}
websocket.close = function() {
console.log("和游戏服务器断开连接!");
}
websocket.onerror = function() {
console.log("和服务器的连接出现异常!");
}
window.onbeforeunload = function() {
websocket.close();
}
前端game_room.html通过重新创建一个WebSocke通道来进行用户下子的数据传输.
后端代码如下:
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
GameReadyResponse readyResponse=new GameReadyResponse();
UserInfo user= (UserInfo) session.getAttributes().get("user");
// 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
if(user==null){
readyResponse.setOk(false);
readyResponse.setReason("用户未登录");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(readyResponse)));
session.close();
return;
}
RoomService roomService = roomManagerService.getRoomByUserId(user.getUserId());
// 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
if(roomService ==null){
readyResponse.setOk(false);
readyResponse.setReason("用户未匹配到");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(readyResponse)));
session.close();
return;
}
// 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
if (onlineUserManagerService.getFromGameHall(user.getUserId()) != null
|| onlineUserManagerService.getFromGameRoom(user.getUserId()) != null) {
readyResponse.setOk(true);
readyResponse.setReason("禁止多开游戏页面");
readyResponse.setMessage("repeatConnection");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(readyResponse)));
return;
}
// 设置当前玩家上线!
onlineUserManagerService.enterGameRoom(user.getUserId(), session);
// 把两个玩家加入到游戏房间中.
// 前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.
synchronized (roomService) {
if (roomService.getUser1() == null) {
// 第一个玩家还尚未加入房间.
// 就把当前连上 websocket 的玩家作为 user1, 加入到房间中.
roomService.setUser1(user);
// 把先连入房间的玩家作为先手方.
roomService.setWhiteUser(user.getUserId());
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
return;
}
if (roomService.getUser2() == null) {
// 如果进入到这个逻辑, 说明玩家1 已经加入房间, 现在要给当前玩家作为玩家2 了
roomService.setUser2(user);
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");
// 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.
// 通知这两个玩家说, 游戏双方都已经准备好了.
noticeGameReady(roomService, roomService.getUser1(), roomService.getUser2());
noticeGameReady(roomService, roomService.getUser2(), roomService.getUser1());
return;
}
}
// 此处如果又有玩家尝试连接同一个房间, 就提示报错.
// 这种情况理论上是不存在的, 为了让程序更加的健壮, 还是做一个判定和提示.
readyResponse.setOk(false);
readyResponse.setReason("当前房间已满, 您不能加入房间");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(readyResponse)));
}
private void noticeGameReady(RoomService roomService, UserInfo thisUser, UserInfo thatUser) throws IOException {
GameReadyResponse gameReadyResponse=new GameReadyResponse();
gameReadyResponse.setMessage("gameReady");
gameReadyResponse.setOk(true);
gameReadyResponse.setReason("");
gameReadyResponse.setRoomId(roomService.getRoomId());
gameReadyResponse.setThisUserId(thisUser.getUserId());
gameReadyResponse.setThatUserId(thatUser.getUserId());
gameReadyResponse.setWhiteUser(roomService.getWhiteUser());
WebSocketSession session= onlineUserManagerService.getFromGameRoom(thisUser.getUserId());
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(gameReadyResponse)));
}
当两名玩家进入游戏房间后,开始操作,注意我这里的先手玩家和后手玩家的判定是根据用户先进入房间的顺序,通过将先进入房间的顺序来决定先后手的,那么该如何告诉前端该先后手的顺序呢?这里我采用的是通过构建实体类将先手的UserId传给前端,然后前端进行判定同时进行不同的页面显示,并且在后端返回数据之后构建出了一张棋盘显示给用户.
4.2 前端通过监听用户落子的下标返回给后端,后端维护一个二维数组来进行判断胜负并返回给前端
前端代码如下:
// 之前 websocket.onmessage 主要是用来处理了游戏就绪响应. 在游戏就绪之后, 初始化完毕之后, 也就不再有这个游戏就绪响应了.
// 就在这个 initGame 内部, 修改 websocket.onmessage 方法~~, 让这个方法里面针对落子响应进行处理!
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[resp.row][resp.col] = 1;
// 交换双方的落子轮次
me = !me;
setScreenText(me);
// 判定游戏是否结束
let screenDiv = document.querySelector('#screen');
if (resp.winner != 0) {
if (resp.winner == gameInfo.thisUserId) {
// alert('你赢了!');
screenDiv.innerHTML = '你赢了!';
} else if (resp.winner = gameInfo.thatUserId) {
// alert('你输了!');
screenDiv.innerHTML = '你输了!';
} else {
alert("winner 字段错误! " + resp.winner);
}
// 回到游戏大厅
// location.assign('/game_hall.html');
// 增加一个按钮, 让玩家点击之后, 再回到游戏大厅~
let backBtn = document.createElement('button');
//let backBtn = document.querySelector('button');
backBtn.innerHTML = '返回游戏大厅';
backBtn.onclick = function() {
location.replace('/game_hall.html');
}
let fatherDiv = document.querySelector('.container>div');
fatherDiv.appendChild(backBtn);
}
}
前端也创建一个二维的数组用于记录该坐标是否有棋子,前端通过监听用户的鼠标点击位置来确定坐标,然后在二维数组中查询该坐标是否有棋子,若无则将该下子请求返回给后端,等待后端处理结果后在进行响应.
后端代码如下:
package com.example.gobangproject.service;
import com.example.gobangproject.GoBangProjectApplication;
import com.example.gobangproject.mapper.UserMapper;
import com.example.gobangproject.model.GameRequest;
import com.example.gobangproject.model.GameResponse;
import com.example.gobangproject.model.UserInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.UUID;
@Data
@Slf4j
public class RoomService {
private String roomId;
private UserInfo user1;
private UserInfo user2;
private int whiteUser;
private UserMapper userMapper;
private RoomManagerService roomManagerService;
private ObjectMapper objectMapper;
private OnlineUserManagerService onlineUserManagerService;
private static final int MAX_ROW = 15;
private static final int MAX_COL = 15;
private int[][]board=new int[MAX_ROW][MAX_COL];
public RoomService(){
this.roomId= UUID.randomUUID().toString();
userMapper=GoBangProjectApplication.context.getBean(UserMapper.class);
roomManagerService =GoBangProjectApplication.context.getBean(RoomManagerService.class);
onlineUserManagerService = GoBangProjectApplication.context.getBean(OnlineUserManagerService.class);
objectMapper=GoBangProjectApplication.context.getBean(ObjectMapper.class);
}
public void putChess(String jsonString) throws IOException {
GameRequest request=objectMapper.readValue(jsonString,GameRequest.class);
GameResponse response=new GameResponse();
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;
printBoard();
// 进行胜负判定
int winner = checkWinner(row, col, chess);
// 给房间中的所有客户端都返回响应.
response.setMessage("putChess");
response.setUserId(request.getUserId());
response.setRow(row);
response.setCol(col);
response.setWinner(winner);
WebSocketSession session1 = onlineUserManagerService.getFromGameRoom(user1.getUserId());
WebSocketSession session2 = onlineUserManagerService.getFromGameRoom(user2.getUserId());
if (session1 == null) {
// 玩家1 已经下线了. 直接认为玩家2 获胜!
response.setWinner(user2.getUserId());
System.out.println("玩家1 掉线!");
}
if (session2 == null) {
// 玩家2 已经下线. 直接认为玩家1 获胜!
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));
}
// 如果当前胜负已分, 就可以直接把房间从房间管理器中给移除
if (response.getWinner() != 0) {
// 胜负已分
log.info("游戏结束! 房间即将销毁! roomId=" + roomId + " 获胜方为: " + response.getWinner());
// 更新获胜方和失败方的信息.
int winUserId = response.getWinner();
int loseUserId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();
userMapper.userWin(winUserId);
userMapper.userLose(loseUserId);
// 销毁房间
roomManagerService.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
// 如果玩家2 获胜, 就返回玩家2 的 userId
// 如果胜负未分, 就返回 0
private int checkWinner(int row, int col, int chess) {
// 检查所有的行
// 先遍历这五种情况
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;
}
}
// 检查所有列
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;
}
}
// 检查左对角线
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;
}
}
// 检查右对角线
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;
}
}
// 胜负未分, 就直接返回 0 了.
return 0;
}
}
以上后端代码在接收到前端的下子请求后,会在后端相应的构造出一个二维数组,然后对该数组进行判断,根据传入的该棋子的下标,分别检索其上、下、左对角线、右对角线来判定有无胜负,若未分出胜负则返回0,user1获胜返回该用户id,user2同理,
结果如下图所示:
在后端进行判定出胜负之后,不要忘记了对这两名用户的数据修改,修改后如下图所示:
到此,我们的网页五子棋项目算是落下了真正的帷幕,希望小伙伴们能够理解。