五子棋双人对战项目(5)——对战模块

news2024/11/28 9:28:16

目录

一、需求分析

二、约定前后端交互接口

三、实现游戏房间页面(前端代码)

game_room.html

game_room.css

srcipt.js

四、实现后端代码

GameAPI

Room

Mapper

五、线程安全问题


一、需求分析

        在对局中,玩家需要知道实时对局情况,所以需要用到消息推送机制,只要有玩家一落子,把落子情况发送给服务器,服务器收到后马上发送给对手,让对手玩家立马能知道我的落子位置。

        同时,我们还需要棋盘,这个可以使用 HTML5 引入的一个标签:canvas,称为画布,为什么这么说呢?因为它在网页上可以实现一个 “画画” 的效果,我们需要的棋盘,就可以使用这个标签,给它画上去。(同理,黑棋、白旗也可以画上去)。


二、约定前后端交互接口

        对战模块和匹配模块,使用的是两套逻辑,使用不同的 websocket 的路径进行处理,可以做到更好的解耦合。

        在匹配成功后,进入房间页面,服务器主动给客户端发送的响应:(客户端无需发送请求给服务器)

        针对 落子请求和响应:(我落子后,对方也要知道实时棋局情况,需要把请求发给服务器,服务器在返回响应给对方;对方落子后,也同理)


三、实现游戏房间页面(前端代码)

game_room.html

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

            </canvas>
            <!-- 显示区域 -->
            <div id="screen"> 等待玩家连接中... </div>
        </div>
    </div>
    <script src="js/script.js"></script>
</body>
</html>

game_room.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: orange;
    font-size: 22px;
    line-height: 50px;
    text-align: center;

    color: white;
    border: none;
    outline: none;
    border-radius: 10px;
}

/* 在 game_room.css 中添加下面的样式 */
.button {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: green;
    font-size: 22px;
    line-height: 50px;
    text-align: center;

    color: white;
    border: none;
    outline: none;
    border-radius: 10px;
}

.button:active{
    background-color: gray;
} 

srcipt.js

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
//
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("和服务器的连接出现异常");
}

websocket.onbeforeunload = function () {
    websocket.close();
}

//处理服务器返回的响应数据 (游戏就绪响应)
websocket.onmessage = function (event) {
    console.log("[handlerGameReady] " + event.data);
    let resp = JSON.parse(event.data);

    if (!resp.ok) {
        alert("连接游戏失败! reason: " + resp.reason);
        // 如果出现连接失败的情况,回到游戏大厅
        // location.assign("/game_hall.html");
        location.replace("/game_hall.html");
        return;
    }

    if (resp.message == 'gameReady') {
        // 把后端返回的数据放进 gameInfo 对象中
        gameInfo.roomId = resp.roomId;
        gameInfo.thisUserId = resp.thisUserId;
        gameInfo.thatUserId = resp.thatUserId;
        // 判断自己是不是先手
        gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);
        // 初始化棋盘
        initGame();
        //设置显示区域的内容(轮到谁落子了)
        setScreenText(gameInfo.isWhite);
    } else if (resp.message == 'repeatConnection') {
        console.log("检测到游戏多开!");
        alert("检测到游戏多开, 请使用其他账户进行登录!");
        // location.assign("/login.html");
        location.replace("/login.html");
        return;
    }
}


//
// 初始化一局游戏
//
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 = "#FFFFFF";

    // 背景图片
    let logo = new Image();
    logo.src = "image/6.jpg";
    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();
    //     }
    // }

    // 绘制棋盘网格
    function initChessBoard() {
        for (let i = 0; i < 15; i++) {
            context.moveTo(15 + i * 30, 15);
            context.lineTo(15 + i * 30, 15 + 30 * 14);  // 这里确保到450
            context.stroke();

            context.moveTo(15, 15 + i * 30);
            context.lineTo(15 + 30 * 14, 15 + i * 30);  // 这里确保到450
            context.stroke();
        }
    }

    // 绘制一个棋子, me 为 true
    function 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) {
            // 发送坐标给服务器, 服务器要返回结果
            send(row, col);
        }
    }

    function send(row, col) {
        let req = {
            message: 'putChess',
            userId: gameInfo.thisUserId,
            row: row,
            col: col
        };
        websocket.send(JSON.stringify(req));
    }

    // 之前 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 backButton = document.createElement('button');
            backButton.innerHTML = '回到游戏大厅'
            backButton.className = 'button';  // 添加样式类
            backButton.onclick = function() {
                location.replace('/game_hall.html');
            }
            let fatherDiv = document.querySelector('.container>div');
            fatherDiv.appendChild(backButton);
        }
    }
}

四、实现后端代码

1、WebSocketConfig 

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry.addHandler(matchAPI, "/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
        webSocketHandlerRegistry.addHandler(gameAPI, "/game")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

2、GameAPI

@Slf4j
@Component
public class GameAPI extends TextWebSocketHandler {
    ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private RoomManager roomManager;

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private UserMapper userMapper;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        GameReadyResponse resp = new GameReadyResponse();

        // 1、先获取到用户的身份信息(从 HttpSession 里拿到)
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            resp.setOk(false);
            resp.setReason("用户尚未登录!");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(jsonString));
            return;
        }

        // 2、判定当前用户是否已经进入房间(拿着房间管理器进行查询)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 如果为 null,当前没有找到对应的房间,该玩家还没匹配到
            resp.setOk(false);
            resp.setReason("用户尚未匹配到");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(jsonString)));
            return;
        }

        // 3、判定当前是不是多开(该用户是不是已经在其他地方进行游戏了)
        //    前面多准备了一个 OnlineUserManager
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            // 如果一个账号,一边是在游戏大厅,一边是在游戏房间,也视为多开~
            resp.setOk(true);
            resp.setReason("禁止多开游戏页面");
            resp.setMessage("repeatConnection");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(jsonString));
            return;
        }

        // 4、经过一些列校验都没问题后,设置当前玩家上线(房间中上线)
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        synchronized (room) {
            // 5、把两个玩家加入到游戏房间中
            //    前面的创建房间/匹配过程,是在 game_hall.html 页面中完成的
            //    因此前面匹配到对手之后,需要经过页面跳转,来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)
            //    当前这个逻辑是在 game_room.html 页面加载的时候进行的
            //    执行到当前逻辑,说明玩家已经页面跳转成功了
            //    页面跳转,其实是个大活~(很有可能出现 “失败” 的情况的)
            if (room.getUser1() == null) {
                // 第一个玩家尚未加入房间
                // 就把当前连上 WebSocket 的玩家作为 user1,加入到房间中
                room.setUser1(user);
                // 把先连入房间的玩家作为先手方
                room.setWhiteUser(user.getUserId());
                log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
                return;
            }
            if (room.getUser2() == null) {
                // 如果进入这个房间,说明玩家1 已经加入房间,现在要把玩家2 加入房间
                room.setUser2(user);
                log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");

                // 当两个玩家都加入成功之后,就要让服务器,给这两个玩家都返回 WebSocket 的响应数据
                // 通知两个玩家说:游戏双方都已经准备好了
                // 通知玩家1
                noticeGameReady(room, room.getUser1(), room.getUser2());
                // 通知玩家2
                noticeGameReady(room, room.getUser2(), room.getUser1());
                return;
            }
        }

        // 6、此处如果又有一个玩家尝试连接同一个房间,就会提示报错
        //    这种情况理论上是不存在的,为了让程序更加健壮,还是做一个判定和提示
        resp.setOk(false);
        resp.setReason("当前房间已满,您不能加入房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

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

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1、先从 Session 拿到当前用户的身份信息
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            log.info("[handleTextMessage] 当前玩家尚未登录");
            return;
        }

        // 2、根据 玩家id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3、通过 room对象 来处理这次具体的请求
        room.putChess(message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            //此处就简单处理,在断开连接的时候就不给客户端返回响应了
            return;
        }

        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        // 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("当前这个用户 {}", user.getUsername() + " 游戏房间连接异常");
        }

        // 通知对手获胜
        noticeThatUserWin(user);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            //此处就简单处理,在断开连接的时候就不给客户端返回响应了
            return;
        }

        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        // 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("当前这个用户 {}", user.getUsername() + " 已经离开游戏房间");
        }

        // 通知对手获胜
        noticeThatUserWin(user);
    }

    private void noticeThatUserWin(User user) throws IOException {
        // 1、根据当前玩家,找到对应房间,再找到当前玩家的对手
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 这个情况意味着房间已经被释放,也就没有对手了
            log.info("当前房间已经释放, 无需通知对手");
            return;
        }

        // 2、根据房间找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();

        // 3、找到对手的在线状态
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
        if(webSocketSession == null) {
            // 这就意味着对手也掉线了
            log.info("对手也已经掉线了, 无需通知");
            return;
        }

        // 4、构造一个响应,来通知对手,你是获胜方
        GameResponse resp = new GameResponse();
        resp.setMessage("putChess");
        resp.setUserId(thatUser.getUserId());
        resp.setWinner(thatUser.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));

        // 5、更新玩家的分数信息
        int winUserId = thatUser.getUserId();
        int loseUserId = user.getUserId();
        userMapper.userWin(winUserId);
        userMapper.userLose(loseUserId);
        // 6、释放房间对象
        roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());
    }
}

(1)游戏房间准备就绪响应(GameReadyResponse )

// 客户端连接到游戏房间后,返回的响应
@Data
public class GameReadyResponse {
    private String message;
    private boolean ok;
    private String reason;
    private String roomId;
    private int thisUserId;
    private int thatUserId;
    private int whiteUser;
}

(2)“落子”请求(GameRequest )

// 这个类表示落子请求
@Data
public class GameRequest {
    private String message;
    private int userId;
    private int row;
    private int col;
}

(3)“落子”响应(GameResponse )

// 这个类表示一个落子响应
@Data
public class GameResponse {
    private String message;
    private int userId;
    private int row;
    private int col;
    private int winner;
}

(4)Room

@Slf4j
@Data
public class Room {
    // 使用字符串类型来表示,方便生成唯一值
    private String roomId;

    private User user1;
    private User user2;

    // 先手方的玩家 id
    private int whiteUser;

    // 行 | 列
    private static final int MAX_ROW = 15;
    private static final int MAX_COL = 15;

    // 这个二维数组表示棋盘(服务端这边数组的状态有三种,但客户端那边只有两种,主要是用来判断当前棋盘有没有棋子,用来避免一个问题:同一个位置重复落子的情况)
    // 约定:
    // 1) 使用 0 表示当前位置未落子(初始化好的二维数组,相对于 全都是0)
    // 2) 使用 1 表示 user1 的落子位置
    // 3) 使用 2 表示 user2 的落子位置

    private int[][] board = new int[MAX_ROW][MAX_COL];

    // 创建 ObjectMapper 用来转换JSON
    private ObjectMapper objectMapper = new ObjectMapper();

    // 引入 OnlineUserManager
//    @Autowired
    private OnlineUserManager onlineUserManager;

    // 引入 RoomManager, 用于房间销毁
//    @Autowired
    private RoomManager roomManager;

    // 引入 UserMapper, 用于更新用户数据
//    @Autowired
    private UserMapper userMapper;

    // 通过这个方法来处理一次落子操作
    // 要做的事情:
    // 1、记录当前落子的位置
    // 2、进行胜负判定
    // 3、给客户端返回响应
    public void putChess(String reqJson) throws IOException {
        // 1、记录当前落子的位置
        GameRequest request = objectMapper.readValue(reqJson, GameRequest.class);
        GameResponse response = new GameResponse();
        // 当前这个棋子是玩家1落子,还是玩家2落子;根据玩家1 和 玩家2 来决定往数组中放1还是2
        int chess = request.getUserId() == user1.getUserId() ? 1 : 2;
        int row = request.getRow();
        int col = request.getCol();
        // 判断当前位置是不是已经有棋子了
        if (board[row][col] != 0) {
            // 在客户端已经针对重复落子进行判定过了,此处为了程序更加稳健,在服务器再判断一次
            log.info("当前位置 row: " + row + " col: " + col + " 已经有棋子了");
        }
        board[row][col] = chess;

        // 2、打印出当前的棋盘信息,方便来观察局势,也方便后面验证胜负关系的判定
        printBoard();

        // 3、进行胜负判定
        int winner = checkWinner(row, col, chess);

        // 4、给房间的所有客户端都返回响应
        response.setMessage("putChess");
        response.setUserId(request.getUserId());
        response.setRow(row);
        response.setCol(col);
        response.setWinner(winner);
        // 要想给用户发送 WebSocket 数据,就需要获取到这个用户的 WebSocketSession
        WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
        // 万一当前查到的会话为空(玩家下线了), 特殊处理一下
        if (session1 == null) {
            // 玩家1 下线了,直接认为 玩家2 获胜
            response.setWinner(user2.getUserId());
            log.info("玩家: {}", user1.getUsername() + " 下线, 直接判定玩家1获胜");
        }
        if (session2 == null) {
            // 玩家2 下线了,直接认为 玩家1 获胜
            response.setWinner(user1.getUserId());
            log.info("玩家: {}", user2.getUsername() + " 下线, 直接判定玩家1获胜");
        }
        // 把响应构造成 JSON 字符串,通过 Session进行传输
        String respJson = objectMapper.writeValueAsString(response);
        if (session1 != null) {
            session1.sendMessage(new TextMessage(respJson));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(respJson));
        }

        // 5、如果当时胜负已分, 这个房间就已经失去存在的意义了,就把这个房间从房间管理器中删除
        if (response.getWinner() != 0) {
            log.info("游戏结束, 当前房间即将销毁! rommId= {}", roomId + " 获胜方为: " + response.getWinner());

            // 更新获胜方和失败方的信息
            int winUserId = response.getWinner();
            int loseUserId = (response.getWinner() == user1.getUserId()) ? user2.getUserId() : user1.getUserId();
            userMapper.userWin(winUserId);
            userMapper.userLose(loseUserId);

            // 销毁房间
            roomManager.remove(roomId, user1.getUserId(), user2.getUserId());
        }
    }


    private void printBoard() {
        // 打印出棋盘
        log.info("[打印棋盘信息] " + "roomId: {}", 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 获胜,就返回 玩家1 的userId
    //  2) 如果 玩家2 获胜,就返回 玩家2 的userId
    //  3) 如果 胜负未分,就返回 0
    private int checkWinner(int row, int col, int chess) {
        // 1、检查所有的行
        //   先遍历这五种情况
        for (int c = col - 4; c <= col; c++) {
            // 针对其中的一种情况,来判定这五个棋子是不是连在一起了
            // 不光是这五个字得连着,而且还得和玩家落的子是一样(才算是获胜)
            try {
                if (board[row][c] == chess
                        && board[row][c + 1] == chess
                        && board[row][c + 2] == chess
                        && board[row][c + 3] == chess
                        && board[row][c + 4] == chess) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("行 五子连珠! 胜负已分!");
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断
                continue;
            }
        }

        // 2、检查所有的列
        for(int r = row - 4; r <= row; r++) {
            try {
                if (board[r][col] == chess
                        && board[r + 1][col] == chess
                        && board[r + 2][col] == chess
                        && board[r + 3][col] == chess
                        && board[r + 4][col] == chess) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("列 五子连珠! 胜负已分!");
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断
                continue;
            }
        }

        // 3、检查所有主对角线 (左对角线,从左上往右下)
        for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {
            try {
                if (board[r][c] == chess
                        && board[r + 1][c + 1] == chess
                        && board[r + 2][c + 2] == chess
                        && board[r + 3][c + 3] == chess
                        && board[r + 4][c + 4] == chess) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("主对角线 五子连珠! 胜负已分!");
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断
                continue;
            }
        }

        // 4、检查所有副对角线 (右对角线, 从左下往右上)
        for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
            try {
                if (board[r][c] == chess
                        && board[r + 1][c - 1] == chess
                        && board[r + 2][c - 2] == chess
                        && board[r + 3][c - 3] == chess
                        && board[r + 4][c - 4] == chess) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("副对角线 五子连珠! 胜负已分!");
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断
                continue;
            }
        }
        // 胜负未分,就直接返回 0 了
        return 0;
    }

    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();
        // 通过入口类记录中的 context,来手动获取到前面的 RoomManager 和 OnlineUserManager
        onlineUserManager = SpringGobangApplication.context.getBean(OnlineUserManager.class);
        roomManager = SpringGobangApplication.context.getBean(RoomManager.class);
        userMapper = SpringGobangApplication.context.getBean(UserMapper.class);
    }
}

(5)Mapper

@Mapper
public interface UserMapper {
    // 根据用户名,查询用户的详情信息,用于登录功能
    @Select("select * from user where user_name = #{username}")
    User selectByName(String username);

    // 往数据库里插入信息,用于注册功能
    @Insert("insert into user values (null, #{username}, #{password}, 1000, 0, 0);")
    void register(User userInfo);

    // 总比赛场数 + 1    获胜场数 + 1    天梯积分 + 30
    @Update("update user set total_count = total_count + 1, " +
            "win_count = win_count + 1, score = score + 30 where user_id = #{userId}")
    void userWin(int userId);

    // 总比赛场数 + 1    获胜场数 不变    天梯积分 - 30
    @Update("update user set total_count = total_count + 1, " +
            "score = score - 30 where user_id = #{userId}")
    void userLose(int userId);
}

五、线程安全问题

        针对同一个房间,既有查询,又有修改操作。

        因为该项目是多人联机游戏,所以会有多个玩家针对这一个房间同时进行 查询/修改 数据,所以存在线程安全问题,那么怎么解决呢?——核心操作:加锁

        我们先思考一下,要进行修改的对象是谁?是不是就是这个房间。

        每时每刻都有不同玩家进行游戏,那么也就说明房间是多个的,这些房间每个都是相互独立、互不干扰的。(保证这个房间玩家的操作,不会影响到别的房间里的游戏)

        所以我们针对 房间对象 进行加锁,这样我在进游戏时,拿到锁,执行完一段逻辑后,再释放掉,给对手进行分配,这样也不会影响到别的房间上的玩家。如图:

        这六个玩家访问的是同一个服务器,也就都会执行afterConnectionEstablished方法。

        此处保证玩家1 和 玩家2 互斥、玩家3 和 玩家4互斥、玩家5 和 玩家6互斥就行了(玩家1 和 玩家3之间 不必互斥)。

        经过上述思考,要想达到这种效果,针对 房间对象 加锁 就可以满足。

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

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

相关文章

高阶数据结构-------图

文章目录 图图的基本概念图的存储结构邻接矩阵邻接表 图的遍历广度优先遍历深度优先遍历 最小生成树Kruskal算法Prim算法 最短路径单源最短路径-Dijkstra算法单源最短路径-Bellman-Ford算法多源最短路径-Floyd-Warshall算法 图 图的基本概念 图的基本概念 图是由顶点集合和边的…

【10】纯血鸿蒙HarmonyOS NEXT星河版开发0基础学习笔记-泛型基础全解(泛型函数、泛型接口、泛型类)及参数、接口补充

序言&#xff1a; 本文详细讲解了关于ArkTs语言中的泛型&#xff0c;其中包含泛型函数、泛型接口、泛型约束、泛型类及其中参数的使用方法&#xff0c;补充了一部分接口相关的知识&#xff0c;包括接口的继承和具体实现&#xff0c;也写到了一些边边角角的小知识&#xff0c;剩…

【Linux】进程替换、命令行参数及环境变量(超详解)

目录 进程替换 替换函数的含义 命令行参数 环境变量 PATH 进程替换 我们先看代码&#xff1a; 1 #include<stdio.h>2 #include<unistd.h>3 int main()4 {5 printf("process...begin!\n");6 7 execl("/usr/bin/ls","ls"…

前端面试如何说解vue项目性能优化,你确定不来看看吗?

文末有福利 面试时&#xff0c;很经常会说对某某项目进行了性能优化&#xff0c;使性能有很大的提高之类的话。如果面试官问&#xff0c;来讲讲做了那些优化&#xff0c;这时候你就要很清晰地把你做过的优化一一说出来。 本文谨以自己的Vue项目经验来教你怎么在面试中说优化&am…

【算法与图】通向高效解决方案的钥匙

文章目录 遍历算法BFS&#xff08;广度优先遍历&#xff09;1. 什么是 BFS&#xff1f;2. 特点和应用3. BFS 示例 DFS&#xff08;深度优先搜索&#xff09;1. 什么是 DFS&#xff1f;2. DFS 的基本步骤3. 特点4. DFS 的应用5. DFS 示例 最小生成树问题1. 什么是最小生成树&…

【算法笔记】双指针算法深度剖析

【算法笔记】双指针算法深度剖析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;算法笔记 文章目录 【算法笔记】双指针算法深度剖析前言一.移动零1.1题目1.2思路分析1.3代码实现二.复写零2.1题目2.2思路分析2.3代码实现 三.快乐数3.1题目3…

微服务实战——ElasticSearch(保存)

商品上架——ElasticSearch&#xff08;保存&#xff09; 0.商城架构图 1.商品Mapping 分析&#xff1a;商品上架在 es 中是存 sku 还是 spu &#xff1f; 检索的时候输入名字&#xff0c;是需要按照 sku 的 title 进行全文检索的检索使用商品规格&#xff0c;规格是 spu 的…

基于Springboot+Vue的小区停车场管理系统登录(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 在这个…

uniapp 微信发布注意事项

uniapp的微信播放不支持本地文件&#xff0c;起始微信原生语言是支持的 所以在编写uniapp代码时 要写两套逻辑 // #ifdef MP-WEIXIN 微信原封不变的自己写法 //#endif // #ifndef MP-WEIXIN 其他写法 //#endif 这样可实现 发布到微信后 微信原封不动的使用自己写…

初识算法 · 双指针(3)

目录 前言&#xff1a; 和为s的两数之和 题目解析&#xff1a; ​编辑 算法原理&#xff1a; 算法编写&#xff1a; 三数之和 题目解析 算法原理 算法编写 前言&#xff1a; 本文通过介绍和为S的两数之和&#xff0c;以及三数之和&#xff0c;对双指针算法进行深一步…

进度条(倒计时)Linux

\r回车(回到当前行开头) \n换行 行缓冲区概念 什么现象&#xff1f; 什么现象&#xff1f;&#xff1f; 什么现象&#xff1f;&#xff1f;&#xff1f; 自己总结&#xff1a; #pragma once 防止头文件被重复包含 倒计时 在main.c中&#xff0c;windows.h是不可以用的&…

Windows 环境搭建 CUDA 和 cuDNN 详细教程

CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由NVIDIA公司推出的一个并行计算平台和编程模型&#xff0c;它允许开发者使用NVIDIA GPU进行通用计算&#xff08;即GPGPU&#xff09;&#xff0c;从而加速各种计算密集型任务。CUDA提供了一套基于C/C…

linux文件编程_线程

1. 基本概念 1.1. 进程与线程的概念 典型的UNIX/linux进程可以看成是只有一个控制线程&#xff0c;一个进程在同一时刻只做一件事情&#xff0c;有了多个控制线程后&#xff0c;在程序设计时可以把进程设计成在同一时刻做不止一件事&#xff0c;每个线程各自处理独立的任务。…

Web安全 - 文件上传漏洞(File Upload Vulnerability)

文章目录 OWASP 2023 TOP 10导图定义攻击场景1. 上传恶意脚本2. 目录遍历3. 覆盖现有文件4. 文件上传结合社会工程攻击 防御措施1. 文件类型验证2. 文件名限制3. 文件存储位置4. 文件权限设置5. 文件内容检测6. 访问控制7. 服务器配置 文件类型验证实现Hutool的FileTypeUtil使用…

STM32使用Keil5 在运行过程中不复位进入调试模式

一、选择Options for Target进入设置 二、选择所使用的调试器&#xff0c;这里以ST-Link为例。取消勾选Load Application at Startup 可以在进入调试模式的时候不会从新加载程序&#xff01;从而不破坏现场 三、点击Setting进入 四、取消勾选Reset after Connect 使得调试器连接…

探索 aMQTT:Python中的AI驱动MQTT库

文章目录 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库背景介绍aMQTT是什么&#xff1f;如何安装aMQTT&#xff1f;简单库函数使用方法场景应用常见问题及解决方案总结 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库 背景介绍 在物联网和微服务架构的浪潮中&#xff0c;MQ…

Redis:string类型

Redis&#xff1a;string类型 string命令设置与读取SETGETMSETMGET 数字操作INCRINCRBYDECRDECRBYINCRBYFLOAT 字符串操作APPENDSTRLENGETRANGESETRANGE 内部编码intembstrraw 在Redis中&#xff0c;字符串string存储的是二进制&#xff0c;以byte为单位&#xff0c;输入的二进…

ICPC-day1(NTT)

NTT经典例题 CCPC-Winter-Camp-day6-A——NTT经典例题 对于上面格式&#xff0c;如果想求出每个i的值可以使用卷积求出&#xff0c;因为阶乘j和阶乘i-j相乘的值为(i(i-j))i 补充一个二次剩余定理 P5491 【模板】二次剩余 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) //#in…

【MySQL】DML数据操作语句和基本的DQL语句

目录 一、Mysql对数据的增删改 1. 增加数据 2. 修改数据&#xff08;UPDATE语句&#xff09; 3. 删除 3.1 delete、truncate、drop区别 二、DQL语言&#xff08;重点&#xff09; 1. 单表查询 1.1 最简单的查询 1.2 从表中获取数据 1.3 字段名起别名 1.4 添加字段 1…