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

news2024/12/24 11:40:26

目录

  • 网页五子棋项目
    • 一、项目核心流程
    • 二、 登录模块
      • 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")
    @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;

    }

三、 游戏大厅模块

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

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

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

  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 {

    @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,然后将该用户放入队列中,通过创建三个线程来同时扫描这三个不同队列,这里请同学们一定要注意线程安全的问题,当线程扫描该队列时如果该队列内有没两个用户则线程等待,直到有用户再次放入时唤醒线程,当扫描时发现队列中有两个以上玩家,则将这两名用户进行游戏房间的维护,这是在第二段代码中了。

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

后端代码如下:

@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同理,

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

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

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

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

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

相关文章

xxl-job登录没反应问题解决方法

最近在写一个关于xxl-job的项目&#xff0c;然后遇到了如下的问题&#xff0c;可以正常访问到xxl-job的登录界面但是点击登录按钮发现没有反应&#xff0c;并且没有发送任何请求。 排查步骤&#xff08;使用docker&#xff09; 1.重启mysql 2.重启docker 3.重写安装mysql 4.查看…

Mysql-索引结构

一.什么是索引&#xff1f; 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 二.无索引的情况 找到…

【Linux】Linux的基本使用

一.Linux的背景知识. 1.1什么是Linux Linux是一种开源的类Unix操作系统内核. 和Windows是" 并列 "的关系. 1.2Linux的发行版本. Linux 严格意义来说只是一个 “操作系统内核”.一个完整的操作系统 操作系统内核 配套的应用程序. 由于 Linux 是一个完全开源免费…

基于JSP的高校二手交易平台

开头语&#xff1a;你好&#xff0c;我是专注于计算机技术的学姐码农小野&#xff0c;如果有任何技术需求&#xff0c;欢迎随时联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 JAVA MySQL 工具&#xff1a;常见Web浏览器&#xff0…

【开发踩坑】 MySQL不支持特殊字符(表情)插入问题

背景 线上功能报错&#xff1a; Cause:java.sql.SQLException:Incorrect string value:xFO\x9F\x9FxBO for column commentat row 1 uncategorized SQLException; SQL state [HY000]:error code [1366]排查 初步觉得是编码问题&#xff08;utf8 — utf8mb4&#xff09; 参考上…

Linux环境下dockes使用MongoDB,上传zip文件如何解压并备份恢复到MongoDB数据库中

1、准备 Docker 和 MongoDB 容器 建议主机端口改一下 docker run --name mongodb -d -p 27018:27017 mongo 2. 创建一个工作目录并将 zip 文件上传到dockers容器中 docker cp data.zip mongodb:/data.zip 3. 在 MongoDB 容器中解压 zip 文件&#xff08;也可以解压完再复制…

大语言模型LLM-三种模型架构

架构&#xff1a;由Transformer论文衍生出来的大语言模型&#xff0c;主要有三种模型架构预训练目标&#xff1a;FLM&#xff0c;PLM&#xff0c;MLM调整&#xff1a;微调&#xff1a; Transformer transfomer可以并行地计算&#xff1f; transformer中encoder模块是完全并行…

深入理解Linux网络(四):TCP接收阻塞

TCP socket 接收函数 recv 发出 recvfrom 系统调用。 进⼊系统调⽤后&#xff0c;⽤户进程就进⼊到了内核态&#xff0c;通过执⾏⼀系列的内核协议层函数&#xff0c;然后到 socket 对象的接收队列中查看是否有数据&#xff0c;没有的话就把⾃⼰添加到 socket 对应的等待队列⾥…

MYSQL——库表操作

MYSQL——库表操作 1.1 SQL语句基础1.1.1. SQL简介1.1.2. SQL语句分类1.1.3. SQL语句的书写规范 1.2 数据库的操作1.2.1 数据库的登录及退出1.2.2 查看数据库1.2.3 创建数据库1.2.4 切换数据库1.2.5 查看当前用户1.2.6 删除数据库 1.3 MySQL字符集1.3.1. 字符集1.3.2. 字符序1.…

myBatis的基本操作(持续更新中。。。)

目录 1. 简介2. 简单使用3. 代理开发4. 小技巧5. 动态查询6. 注解&#xff08;待更新&#xff09;底部 1. 简介 mybatis是一款优秀的持久层框架&#xff0c;用来简化JDBC开发 持久层&#xff1a;负责将数据保存到数据库的那一层代码 2. 简单使用 依赖 <dependencies>…

LabVIEW断路器操动机构运动速度检测

开发了一种基于LabVIEW设计平台开发的断路器操动机构运动速度检测系统。通过集成高速相机和图像处理技术&#xff0c;该系统能够实时监控和分析操动机构的动态性能&#xff0c;为电力系统提供关键的技术支持。 项目背景 随着工业化的发展&#xff0c;对电力系统的稳定性和可靠…

python的tkinter、socket库开发tcp的客户端和服务端

一、tcp通讯流程和开发步骤 1、tcp客户端和服务端通讯流程图 套接字是通讯的利器&#xff0c;连接时要经过三次握手建立连接&#xff0c;断开连接要经过四次挥手断开连接。 2、客户端开发流程 1&#xff09;创建客户端套接字 2&#xff09;和服务端器端套接字建立连接 3&#x…

钡铼分布式I/O系统边缘计算Modbus,MQTT,OPC UA耦合器BL206

BL206系列耦合器是一个数据采集和控制系统&#xff0c;基于强大的32 位微处理器设计&#xff0c;采用Linux操作系统&#xff0c;支持Modbus&#xff0c;MQTT&#xff0c;OPC UA协议&#xff0c;可以快速接入现场PLC、DCS、PAS、MES、Ignition和SCADA以及ERP系统&#xff0c;同时…

习题2.21

(defn rever [a](defn item[l r](if ( nil (first l)) r(item (rest l) (cons (first l) r))))(item a nil)) 这段代码非常有助于理解什么是深度优先&#xff0c;什么是广度优先。 很久没有写习题的代码了&#xff0c;倒不是懒得做习题了&#xff0c;是私事多&#xff0c;状态…

【局域网服务器连接】如何远程连入实验室linux系统服务器?| 局域网 | 内网穿透

文章目录 前言服务器基本配置安装 ssh 服务防火墙放行 局域网内网穿透获取SN码添加映射 总结 前言 简单记录连接实验室服务器步骤。如服务器直接有公网 ip 地址&#xff0c;ssh 直接连入即可&#xff0c;无需参考本文。 与服务器连同一 wifi&#xff0c; 参考 局域网 方式连接…

Android:requestLayout、invalidate 和 postInvalidate 的区别

提醒&#xff1a;下面源码来自SDK里Android-34版本 一、requestLayout 点击查看requestLayout官网文档 1.1 requestLayout方法源码 /*** Call this when something has changed which has invalidated the* layout of this view. This will schedule a layout pass of the v…

【C++航海王:追寻罗杰的编程之路】关于空间配置器你知道多少?

目录 1 -> 什么是空间配置器 2 -> 为什么需要空间配置器 3 -> SGI-STL空间配置器的实现原理 3.1 -> 一级空间配置器 3.2 -> 二级空间配置器 3.2.1 -> 内存池 3.2.2 -> SGI-STL中二级空间配置器设计 3.2.3 -> SGI-STL二级空间配置器之空间申请 …

Spring Boot 3.3 【三】Spring Boot RESTful API 增删改查详细教程

Spring Boot RESTful API 增删改查详细教程 一、RESTful 架构风格简介 1. 简介 RESTful API 是一种基于HTTP协议的网络应用接口设计风格&#xff0c;它遵循REST&#xff08;Representational State Transfer&#xff0c;表述性状态转移&#xff09;原则。RESTful架构风格的出…

花几千上万学习Java,真没必要!(二十)

ArrayList 是一种可以动态增长和缩减的数组&#xff0c;与普通的数组相比&#xff0c;它提供了更加灵活的操作方式。ArrayList 内部使用数组来存储元素&#xff0c;但是它会根据需要自动调整数组的大小&#xff0c;以便能够存储更多的元素。 ArrayList 的主要特点包括&#xf…

如何成为学习高手

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 所有的学习方式&#xff0c;核心都是动脑加动手。 区别在于如何让…