【Spring Boot】网页五子棋项目实现,手把手带你全盘解析(长达两万3千字的干货,坐好了,要发车了......)

目录

  • 网页五子棋项目
    • 一、项目核心流程
    • 二、 登录模块
      • 2.1 前端输入用户信息
      • 2.2 后端进行数据库查询用户信息
    • 三、 游戏大厅模块
      • 3.1 前端通过Ajax请求用户数据,后端从Session中拿取并从数据库中查询后返回
      • 3.2 前后端建立WebSocket连接,并进行判断,同时后端进行用户在线处理
      • 3.3 前端利用WebSocket发送按钮信息,后端进行等级队列的处理和维护游戏房间
    • 四、游戏对局模块
      • 4.1 前端构造新的WebSocket连接,后端在等待两名玩家同时连接上服务器后开始游戏
      • 4.2 前端通过监听用户落子的下标返回给后端,后端维护一个二维数组来进行判断胜负并返回给前端

网页五子棋项目

一、项目核心流程

  1. 用户管理:使用服务器实现用户注册、登录功能,以及用户信息的管理,包括用户的天梯分数记录和比赛场次记录;

  2. 实时游戏互动:利用WebSocket技术实现客户端和服务器之间的实时通信,确保玩家的移动可以立即被对方看到,并由服务器进行处理;

  3. 匹配对战系统:设计一个机制以匹配具有相似技能水平的玩家进行对战。涉及到用户评分系统和等待室的实现,以便玩家可以根据自己的排名找到合适的对手;

  4. 游戏逻辑:使用服务器处理用户请求、维护游戏状态、执行匹配算法、管理用户会话以及提供必要的游戏逻辑支持,包括棋盘的初始化、落子规则的执行、胜负条件的判断以及棋局的更新显示;

  5. 界面设计:使用HTML、CSS和JavaScript技术构建用户友好的游戏界面,包括棋盘的可视化、棋子的放置和游戏状态的反馈;

  6. 数据库交互:使用数据库管理系统存储用户信息、游戏记录和其他相关数据,并提供数据的增删改查功能;

在接下来的流程中,我将项目分为了三个板块进行讲解,这样有利于大家的理解,分别是:

  1. 登录模块
  2. 游戏大厅模块
  3. 游戏对局模块

在这三个模块中以第一个模块最为简单,大家可以先提前适应一下,在2和3模块将会给大家上难度了

二、 登录模块

这个最为简单,只需要前端进行输入用户信息,后端进行数据处理并返回用户信息,前端以后端返回的数据为基础来判断是否要进行跳转进入游戏大厅界面。

  1. 前端输入用户信息,并判断数据正确性进行跳转
  2. 后端进行数据库查询用户信息

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")@ResponseBodypublic 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;}

三、 游戏大厅模块

接下来我们开始进入主题,开始正式上难度,游戏大厅的流程如下:

  1. 前端通过Ajax请求用户数据,后端从Session中拿取并从数据库中查询后返回
  2. 前后端建立WebSocket连接,并进行判断,同时后端进行用户在线处理
  3. 前端利用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")@ResponseBodypublic  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在后端进行查询,如果查询到了,那么就意味着该用户已经登录了,没查询到的话就把该用户的信息放进去,表示该用户现在是在线状态。

后端进行连接处理的代码如下:

@Overridepublic 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');}}

以上处理完毕后,开始由后端开始接收数据,进行一个数据处理的过程:

@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 实现处理开始匹配请求和处理停止匹配请求.UserInfo user = (UserInfo) session.getAttributes().get("user");// 获取到客户端给服务器发送的数据String payload = message.getPayload();// 数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequestMatchRequest 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));}

在该段代码中的主要部分是进行一个等级队列的处理,以及对游戏房间的维护有以下两部分组成:

  1. 天梯分数进行分级
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 {@Autowiredprivate OnlineUserManagerService onlineUserManagerService;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate 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(){@Overridepublic  void run(){while (true){handlerMatch(normalQueue);}}};Thread t2=new Thread(){@Overridepublic  void run(){while (true){handlerMatch(middleQueue);}}};Thread t3=new Thread(){@Overridepublic  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,然后将该用户放入队列中,通过创建三个线程来同时扫描这三个不同队列,这里请同学们一定要注意线程安全的问题,当线程扫描该队列时如果该队列内有没两个用户则线程等待,直到有用户再次放入时唤醒线程,当扫描时发现队列中有两个以上玩家,则将这两名用户进行游戏房间的维护,这是在第二段代码中了。

  1. 游戏房间的初步维护
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页面。

四、游戏对局模块

游戏对局模块至关重要,主要是将两名玩家在前端的下棋构造成一个响应返回给后端,在后端处理完成之后在返回给前端,前端根据数据来构造棋子显示在页面之上.

主要分为以下几大步骤:

  1. 前端构造新的WebSocket连接,后端在等待两名玩家同时连接上服务器后开始游戏
  2. 前端通过监听用户落子的下标返回给后端,后端维护一个二维数组来进行判断胜负并返回给前端

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通道来进行用户下子的数据传输.

后端代码如下:

@Overridepublic 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
@Slf4jpublic 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// 如果胜负未分, 就返回 0private 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同理,

结果如下图所示:
在这里插入图片描述

在后端进行判定出胜负之后,不要忘记了对这两名用户的数据修改,修改后如下图所示:

在这里插入图片描述
到此,我们的网页五子棋项目算是落下了真正的帷幕,希望小伙伴们能够理解。

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

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

相关文章

如何处理 PostgreSQL 中死锁的情况?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 如何处理 PostgreSQL 中死锁的情况&#xff1f;一、认识死锁二、死锁的症状三、死锁的检测四、预防死锁…

【MySQL】:想学好数据库,不知道这些还想咋学

客户端—服务器 客户端是一个“客户端—服务器”结构的程序 C&#xff08;client&#xff09;—S&#xff08;server&#xff09; 客户端和服务器是两个独立的程序&#xff0c;这两个程序之间通过“网络”进行通信&#xff08;相当于是两种角色&#xff09; 客户端 主动发起网…

Java语言程序设计——篇六(1)

字符串 概述创建String类对象     字符串基本操作实战演练 字符串查找字符串转换为数组字符串比较实战演练 字符串的拆分与组合 概述 字符串 用一对双引号“”括起来的字符序列。Java语言中&#xff0c;字符串常量或变量均用类实现。 字符串有两大类&#xff1a; 1&…

设计模式学习[2]---策略模式+简单工厂回顾

文章目录 前言1.简单工厂模式回顾2.策略模式3.策略模式简单工厂的结合总结 前言 上一篇讲到简单工厂模式。 在我的理解中工厂的存在就是&#xff0c;为了实例化对象。根据不同条件实例化不同的对象的作用。 这篇博客写的策略模式&#xff0c;可以说是把这个根据不同情况实例化…

pyinstaller 打包基于PyQt5和PaddleOCR的项目为.exe

简介&#xff1a; 最近做了一个小项目&#xff0c;是基于PyQt5和PaddleOCR的。需要将其打包为.exe&#xff0c;然后打包过程中遇到了很多问题&#xff0c;也看了很多教程&#xff0c;方法千奇百怪的&#xff0c;最后也是一步一步给试出来了。记录一下&#xff0c;防止以后忘记…

华为路由器SSH登录实验

概念 SSH全称安全外壳&#xff08;Secure Shell&#xff09;协议&#xff0c;这个协议的目的就是为了取代缺乏机密性保障的远程管理协议&#xff0c;SSH基于TCP协议的加密通道&#xff0c;让客户端使用服务器的RSA公钥来验证SSHv2服务器的身份。 创建密钥对 在充当SSH服务器的…

C语言随机数的生成相关案例

随机数的方式&#xff1a; 1、设置种子&#xff1a;srand(初始值) 2、获取随机数&#xff1a;rand(); 引导案例&#xff1a; 通过for循环简单生成10个随机数 #include<stdio.h> #include<stdlib.h> //添加包含随机数的库函数 int main() {srand(1); …

嵌入式人工智能(15-基于树莓派4B的电机控制-直流电机TB6612)

电机是传动以及控制系统的重要组成部分&#xff0c;现在的电机已从过去简单的传动向复杂的控制转移&#xff0c;尤其是对电机的速度、位置、转矩的精确控制&#xff0c;本系列将介绍如何使用树莓派驱动并控制3种最为常见的控制电机&#xff1a;直流电机&#xff08;风扇&#x…

大语言模型推理优化--键值缓存--Key-value Cache

文章目录 一、生成式预训练语言模型 GPT 模型结构二、FastServe 框架三、Key-value Cache1.大模型推理的冗余计算2.Self Attention3.KV Cache 一、生成式预训练语言模型 GPT 模型结构 目前&#xff0c;深度神经网络推理服务系统已经有一些工作针对生成式预训练语言模型 GPT 的独…

安全防御---防火墙综合实验3

安全防御—防火墙综合实验3 一、实验拓扑图 二、实验要求 12&#xff0c;对现有网络进行改造升级&#xff0c;将当个防火墙组网改成双机热备的组网形式&#xff0c;做负载分担模式&#xff0c;游客区和DMZ区走FW3&#xff0c;生产区和办公区的流量走FW1 13&#xff0c;办公区…

Ubuntu22.04安装OMNeT++

一、官网地址及安装指南 官网地址&#xff1a;OMNeT Discrete Event Simulator 官网安装指南&#xff08;V6.0.3&#xff09;&#xff1a;https://doc.omnetpp.org/omnetpp/InstallGuide.pdf 官网下载地址&#xff1a;OMNeT Downloads 旧版本下载地址&#xff1a;OMNeT Old…

【动态规划】整数拆分

整数拆分&#xff08;难度&#xff1a;中等&#xff09; 该题对应力扣网址 AC代码 class Solution { public:int integerBreak(int n) {//动态规划//感觉这个题和零钱兑换有点像&#xff0c;只是零钱兑换提供了coin列表vector <int> dp(n1,0);//1、定义子问题//将原问题…

PolarisMesh源码系列--Polaris-Go注册发现流程

导语 北极星是腾讯开源的一款服务治理平台&#xff0c;用来解决分布式和微服务架构中的服务管理、流量管理、配置管理、故障容错和可观测性问题。在分布式和微服务架构的治理领域&#xff0c;目前国内比较流行的还包括 Spring Cloud&#xff0c;Apache Dubbo 等。在 Kubernete…

错误:PHP:Deprecated: Required parameter $xxx follows optional parameter $yyy

前言 略 错误 Deprecated: Required parameter $xxx follows optional parameter $yyy 解决办法 设置 error_reporting E_ALL & ~E_DEPRECATED & ~E_STRICT 参考 https://blog.csdn.net/lxw1844912514/article/details/100028023

创建自己的 app: html网页直接打包成app;在线网页打包app工具fusionapp、pake

1、html网页直接打包成app 主要通过hbuilderx框架工具来进行打包 https://www.dcloud.io/hbuilderx.html 参考&#xff1a; https://www.bilibili.com/video/BV1XG411r7QZ/ https://www.bilibili.com/video/BV1ZJ411W7Na 1&#xff09;网页制作 这里做的工具是TodoList 页面&a…

【数据结构--查找】

目录 一、查找&#xff08;Searching&#xff09;的概念1.1、基本概念1.2、算法的评价指标 二、顺序查找2.1、算法思想2.2、算法实现2.2.1、常规顺序查找2.2.2、带哨兵的顺序查找 2.3、效率分析2.4、优化2.4.1、针对有序表2.4.2、被查效率不相等 三、折半查找3.1、算法思想3.2、…

C语言项目篇:二、课程管理系统

为加强对于C语言的巩固和复习&#xff0c;以实战项目为导向&#xff0c;串起所有C语言的语法&#xff0c;达到活学活用的目的&#xff0c;本篇博客&#xff0c;详细总结利用C语言编码简单编码实现生活中的课程管理系统后台开发的整个过程&#xff0c;学习多文件编程和调试&…

Internet 控制报文协议 —— ICMPv4 和 ICMPv6 详解

ICMP 是一种面向无连接的协议&#xff0c;负责传递可能需要注意的差错和控制报文&#xff0c;差错指示通信网络是否存在错误 (如目的主机无法到达、IP 路由器无法正常传输数据包等。注意&#xff0c;路由器缓冲区溢出导致的丢包不包括在 ICMP 响应范围内&#xff0c;在 TCP 负责…

Docker、containerd、CRI-O 和 runc 之间的区别

容器与 Docker 这个名称并不紧密相关。你可以使用其他工具来运行容器 您可以使用 Docker 或一堆非Docker 的其他工具来运行容器。docker只是众多选项之一&#xff0c;Docker&#xff08;公司&#xff09;在生态系统中创建了一些很棒的工具&#xff0c;但不是全部。 容器方面有…

利用【MATLAB】和【Python】进行【图与网络模型】的高级应用与分析】

目录 一、图与网络的基本概念 1. 无向图与有向图 2. 简单图、完全图、赋权图 3. 顶点的度 4. 子图与连通性 5. 图的矩阵表示 MATLAB代码实例 Python代码实例 二、最短路径问题 1. 最短路径问题的定义 2. Dijkstra算法 MATLAB代码实例 Python代码实例 三、最小生…