【在线五子棋对战】在线用户

目录

在线用户管理模块

游戏房间用户管理

协议定制

房间设计


在线用户管理模块

当用户登录/注册成功后,会跳转到大厅界面,或之后进入游戏房间,为了通过该用户的websocket连接传输消息(构建 用户id与websocket连接 的映射关系),需要有数据结构管理 大厅&房间用户

两个数据结构:

  • 大厅用户管理
  • 房间用户管理

对大厅/房间用户管理数据结构进行增删查

cpp 复制代码
/*
-------------
online.hpp     |在线用户管理模块|
在线用户管理
- 大厅用户
- 房间用户
-------------
*/

#pragma once
#include <iostream>
#include <mutex>
#include <unordered_map>

#include <websocketpp/server.hpp>
#include <websocketpp/config/asio.hpp>

using websocket_server = websocketpp::server<websocketpp::config::asio>;

class Online_Manager // 在线用户管理
{
public:
    void EnterHall(int id, const websocket_server::connection_ptr &conn) // 进入游戏大厅(用户登录成功后的操作)
    {
        std::lock_guard<std::mutex> lock(_mutex);
        // 当在线用户重复进入时更新连接
        _hallUsers.insert_or_assign(id, conn); // C++17新接口,若值存在就覆盖
        //_HallUsers.insert(std::make_pair(id, conn)); // 插入用户id和conn的映射
    }

    void ExitHall(int id) // 退出游戏大厅
    {
        std::lock_guard<std::mutex> lock(_mutex);

        _hallUsers.erase(id); // 删除该映射
    }

    void EnterRoom(int id, const websocket_server::connection_ptr &conn) // 进入游戏房间
    {
        // [TRACE] 是否需要判断该用户在大厅?
        std::lock_guard<std::mutex> lock(_mutex);
        // 当房间用户重复进入时更新连接
        _roomUsers.insert_or_assign(id, conn); // C++17接口:重复值则覆盖
        //_RoomUsers.insert(std::make_pair(id, conn)); // 插入用户id和conn的映射
    }

    void ExitRoom(int id) // 退出游戏房间
    {
        std::lock_guard<std::mutex> lock(_mutex);
        _roomUsers.erase(id); // 删除该映射
    }

    bool IsHall(int id) // 判断用户是否在大厅
    {
        std::lock_guard<std::mutex> lock(_mutex);

        return _hallUsers.find(id) != _hallUsers.end();
    }

    bool IsRoom(int id) // 判断用户是否在房间
    {
        std::lock_guard<std::mutex> lock(_mutex);

        return _roomUsers.find(id) != _roomUsers.end();
    }

    // 获取用户id所对应的websocket连接

    websocket_server::connection_ptr GetConn_FromHall(int id)
    {
        std::lock_guard<std::mutex> lock(_mutex);

        auto usr = _hallUsers.find(id);
        if(usr == _hallUsers.end()) // 没找到,返回空对象
            return websocket_server::connection_ptr();
        else
            return usr->second; // 返回conn
    }

    websocket_server::connection_ptr GetConn_FromRoom(int id)
    {
        std::lock_guard<std::mutex> lock(_mutex);

        auto usr = _roomUsers.find(id);
        if(usr == _roomUsers.end()) // 没找到,返回空对象
            return websocket_server::connection_ptr();
        else
            return usr->second; // 返回conn
    }

private:
    std::mutex _mutex; // 在进行在线用户操作时加互斥锁
    // unordered_map<用户id, websocket连接>
    std::unordered_map<int, websocket_server::connection_ptr> _hallUsers; // 大厅用户
    std::unordered_map<int, websocket_server::connection_ptr> _roomUsers; // 房间用户
};

游戏房间用户管理

协议定制

在设计游戏房间管理之前,需要统一一套服务端与客户端通信的JSON格式标准:

bash 复制代码
# 游戏房间 JSON 协议

房间 WebSocket 使用 UTF-8 编码的 JSON 文本帧。所有请求必须是 JSON 对象,并包含:

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `optype` | string | 操作类型 |
| `room_id` | integer | 房间编号,必须为正数 |
| `uid` | integer | 请求用户编号,必须为正数;服务端必须将其与认证会话中的用户编号比较 |

客户端传入的 `uid` 只用于协议完整性校验,不能作为身份认证依据。

## 通用响应

成功响应至少包含 `optype` 和 `result: true`。失败响应统一包含 `result: false`、数值 `code` 和可展示的 `reason`:

```json
{
  "optype": "put_chess",
  "result": false,
  "code": 1003,
  "reason": "该位置已有棋子"
}
```

未知或缺少 `optype` 时,`optype` 返回 `unknown`,错误码为 `1000`。

## 房间准备

`room_ready` 只允许作为服务端响应,不是客户端操作:

```json
{
  "optype": "room_ready",
  "result": true,
  "room_id": 10001,
  "uid": 10086,
  "white_id": 10086,
  "black_id": 10087
}
```

## 下棋

请求中的 `row` 和 `col` 为 `0` 到 `14` 的整数。白棋先手,之后由服务端严格校验回合、位置和棋局状态。

```json
{
  "optype": "put_chess",
  "room_id": 10001,
  "uid": 10086,
  "row": 7,
  "col": 8
}
```

落子成功且未分胜负:

```json
{
  "optype": "put_chess",
  "result": true,
  "room_id": 10001,
  "uid": 10086,
  "row": 7,
  "col": 8,
  "winner": 0
}
```

游戏结束时,`winner` 为获胜用户编号;对方掉线时 `row` 和 `col` 为 `-1`:

```json
{
  "optype": "put_chess",
  "result": true,
  "room_id": 10001,
  "uid": 10086,
  "row": 7,
  "col": 8,
  "winner": 10086,
  "reason": "五星连珠,游戏结束!"
}
```

## 聊天

请求和成功响应都必须包含非空 `message`。服务端应在广播前进行敏感词和长度校验。

```json
{
  "optype": "chat",
  "room_id": 10001,
  "uid": 10086,
  "message": "赶紧点"
}
```

成功响应:

```json
{
  "optype": "chat",
  "result": true,
  "room_id": 10001,
  "uid": 10086,
  "message": "赶紧点"
}
```

## 退出房间

```json
{
  "optype": "exit_room",
  "room_id": 10001,
  "uid": 10086
}
```

游戏中退出会把胜者写入 `winner`,并更新双方战绩:

```json
{
  "optype": "exit_room",
  "result": true,
  "room_id": 10001,
  "uid": 10086,
  "winner": 10087,
  "reason": "对方退出游戏,判定胜利"
}
```

## 错误码

| 范围 | 错误码 | 含义 |
| --- | ---: | --- |
| 通用 | 0 | 成功 |
| 通用 | 1 | 服务端内部错误 |
| 请求 | 1000 | 请求格式或操作类型无效 |
| 请求 | 1001 | 房间编号无效 |
| 请求 | 1002 | 用户无效或不属于房间 |
| 棋局 | 1003 | 位置已有棋子 |
| 棋局 | 1004 | 未轮到当前用户 |
| 棋局 | 1005 | 游戏未开始或已经结束 |
| 棋局 | 1006 | 对手不在线 |
| 棋局 | 1007 | 落子位置越界或格式错误 |
| 聊天 | 2001 | 消息为空、过长或包含敏感词 |

再根据这个标准,实现Protocol.hpp:

cpp 复制代码
/*
-----------------------------------------------
Protocol.hpp Protocol.md
制定客户端与服务端websocket请求/响应的协议标准
-----------------------------------------------
*/
#pragma once

#include <json/json.h>

#include <string>

// Room.hpp 和网络层之间只传递 Json::Value。
// 本文件负责定义 JSON 协议,不负责判断棋局是否合法。

enum class RoomOp
{
    ROOM_READY, // 进入房间
    PUT_CHESS,  // 下棋操作
    CHAT,       // 聊天操作
    EXIT_ROOM   // 退出房间操作
};

enum class RoomCode // 错误码
{
    SUCCESS = 0,        // 成功
    INTERNAL_ERROR = 1, // 内部错误(例如代码)

    INVALID_REQUEST = 1000,   // 请求无效
    INVALID_ROOM = 1001,      // 房间无效
    INVALID_USER = 1002,      // 用户无效
    POSITION_OCCUPIED = 1003, // 落子位置已有棋子
    NOT_YOUR_TURN = 1004,     // 未轮到当前玩家
    GAME_OVER = 1005,         // 游戏未开始
    PLAYER_OFFLINE = 1006,    // 玩家不在线
    INVALID_POSITION = 1007,  // 落子位置无效

    INVALID_MESSAGE = 2001 // 消息无效
};

namespace Protocol
{

    constexpr const char *FIELD_OPTYPE = "optype";
    constexpr const char *FIELD_ROOM_ID = "room_id";
    constexpr const char *FIELD_UID = "uid";
    constexpr const char *FIELD_ROW = "row";
    constexpr const char *FIELD_COL = "col";
    constexpr const char *FIELD_MESSAGE = "message";
    constexpr int BOARD_SIZE = 15;

    inline const char *ToString(RoomOp op) // 将操作类型字段从枚举转为字符串
    {
        switch (op)
        {
        case RoomOp::ROOM_READY:
            return "room_ready";
        case RoomOp::PUT_CHESS:
            return "put_chess";
        case RoomOp::CHAT:
            return "chat";
        case RoomOp::EXIT_ROOM:
            return "exit_room";
        }
        return "unknown";
    }

    inline bool ParseRoomOp(const Json::Value &request, RoomOp &op) // 通过request,提取op
    {
        if (!request.isObject() || !request.isMember(FIELD_OPTYPE) ||
            !request[FIELD_OPTYPE].isString())
        {
            return false;
        }

        const std::string value = request[FIELD_OPTYPE].asString();
        if (value == "room_ready")
            op = RoomOp::ROOM_READY;
        else if (value == "put_chess")
            op = RoomOp::PUT_CHESS;
        else if (value == "chat")
            op = RoomOp::CHAT;
        else if (value == "exit_room")
            op = RoomOp::EXIT_ROOM;
        else
            return false;

        return true;
    }

    inline int ToInt(RoomCode code) // 枚举转错误码
    {
        return static_cast<int>(code);
    }

    inline Json::Value MakeBaseResponse(RoomOp op, bool result) // 构建基础响应
    {
        Json::Value response(Json::objectValue);
        response[FIELD_OPTYPE] = ToString(op);
        response["result"] = result;
        return response;
    }

    inline Json::Value MakeError(RoomOp op, RoomCode code, // 构建错误响应
                                 const std::string &reason)
    {
        Json::Value response = MakeBaseResponse(op, false);
        response["code"] = ToInt(code);
        response["reason"] = reason;
        return response;
    }

    inline Json::Value MakeError(RoomCode code, const std::string &reason) // 构建未知错误
    {
        Json::Value response(Json::objectValue);
        response["optype"] = "unknown";
        response["result"] = false;
        response["code"] = ToInt(code);
        response["reason"] = reason;
        return response;
    }

    inline Json::Value MakeRoomReady(int room_id, int uid, // 进入房间的响应
                                     int white_id, int black_id)
    {
        Json::Value response = MakeBaseResponse(RoomOp::ROOM_READY, true);
        response[FIELD_ROOM_ID] = room_id;
        response[FIELD_UID] = uid;
        response["white_id"] = white_id;
        response["black_id"] = black_id;
        return response;
    }

    inline Json::Value MakeChessResult(int room_id, int uid, int row, int col, // 下棋成功响应
                                       int winner = 0,
                                       const std::string &reason = "")
    {
        Json::Value response = MakeBaseResponse(RoomOp::PUT_CHESS, true);
        response[FIELD_ROOM_ID] = room_id;
        response[FIELD_UID] = uid;
        response[FIELD_ROW] = row;
        response[FIELD_COL] = col;
        response["winner"] = winner;
        if (!reason.empty())
            response["reason"] = reason;
        return response;
    }

    inline Json::Value MakeChatResult(int room_id, int uid, // 发送消息成功响应
                                      const std::string &message)
    {
        Json::Value response = MakeBaseResponse(RoomOp::CHAT, true);
        response[FIELD_ROOM_ID] = room_id;
        response[FIELD_UID] = uid;
        response[FIELD_MESSAGE] = message;
        return response;
    }

    inline Json::Value MakeExitResult(int room_id, int uid, int winner = 0, // 退出房间成功响应
                                      const std::string &reason = "")
    {
        Json::Value response = MakeBaseResponse(RoomOp::EXIT_ROOM, true);
        response[FIELD_ROOM_ID] = room_id;
        response[FIELD_UID] = uid;
        if (winner != 0)
            response["winner"] = winner;
        if (!reason.empty())
            response["reason"] = reason;
        return response;
    }

    // 统一读取请求字段,避免 Room 中到处重复 isMember/isInt 判断。
    inline bool GetInt(const Json::Value &request, const char *name, int &value)
    {
        if (!request.isObject() || name == nullptr ||
            !request.isMember(name) || !request[name].isInt())
            return false;

        value = request[name].asInt();
        return true;
    }

    inline bool GetRoomId(const Json::Value &request, int &room_id)
    {
        return GetInt(request, FIELD_ROOM_ID, room_id) && room_id > 0;
    }

    inline bool GetUid(const Json::Value &request, int &uid)
    {
        return GetInt(request, FIELD_UID, uid) && uid > 0;
    }

    inline bool GetPosition(const Json::Value &request, int &row, int &col)
    {
        return GetInt(request, FIELD_ROW, row) &&
               GetInt(request, FIELD_COL, col) &&
               row >= 0 && row < BOARD_SIZE &&
               col >= 0 && col < BOARD_SIZE;
    }

    inline bool GetString(const Json::Value &request, const char *name,
                          std::string &value)
    {
        if (!request.isObject() || name == nullptr ||
            !request.isMember(name) || !request[name].isString())
        {
            return false;
        }
        value = request[name].asString();
        return !value.empty();
    }

} // namespace Protocol

构建响应都通过Protocol层完成,可以确保协议字段一致

房间设计

当用户进入游戏房间后,房间内的操作,就在此设计

cpp 复制代码
/*
------------------
room.hpp romm.cpp   |游戏房间管理模块|
一个房间为一个对象
实现对房间内的操作
------------------
*/

/*
*/

#pragma once
#include <vector>
#include "Util.hpp"
#include "Db.hpp"
#include "Online.hpp"
#include "Protocol/Protocol.hpp"

enum class RoomStatus // 房间状态
{
    GAME_WAIT, // 游戏未开始
    GAME_START // 游戏进行中
};

enum class Chess // 棋盘中一个位置的三种状态
{
    VACANCY = 0, // 空
    WHITE_CHESS, // 白棋
    BLACK_CHESS // 黑棋
};

class Room
{
    // 棋盘行列
    int ROW = 15;
    int COL = 15;

public:
    // 房间初始0人、房间状态:未开始、棋盘初始化为0
    Room(int room_id, User_Table *user, Online_Manager *online)
        : _room_id(room_id), _statu(RoomStatus::GAME_WAIT), _player_cnt(0), _id_white(0), _id_black(0), _turn_uid(0), _user(user), _online(online), _board(ROW, std::vector<Chess>(COL, Chess::VACANCY))
    {
        LOG(LogLv::DEBUG, "创建房间 id:[%d]", _room_id);
    }

    ~Room()
    {
        LOG(LogLv::DEBUG, "回收房间 id:[%d]", _room_id);
    }

    Json::Value Chess_Handle(Json::Value &request, int authenticated_uid); // 处理下棋操作,返回JSON格式的响应

    Json::Value Chat_Handle(Json::Value &request, int authenticated_uid); // 处理聊天操作,返回JSON格式的响应

    Json::Value Exit_Handle(Json::Value &request, int authenticated_uid); // 处理用户退出房间操作

    // 总处理接口
    void Handle_Request(Json::Value &request, int authenticated_uid); // 用于区分该请求属于那种类型的请求并交给对应处理接口

    void Broadcast(Json::Value response); // 将信息广播给房间内玩家

    bool AddUser(int id, Chess chess); // 为该房间添加黑/白棋用户

    int GetId() { return _room_id; } // 获取房间号

    RoomStatus GetStatu() { return _statu; } // 获取房间状态

    uint16_t GetPlayerCnt() { return _player_cnt; } // 获取房间人数

    int GetUser_White() { return _id_white; } // 获取白棋用户id

    int GetUser_Black() { return _id_black; } // 获取黑棋用户id

private:
    int Check_Win(int row, int col, Chess color); // 检查这一步棋是否胜利,返回胜利者id或0

    bool Five(int row, int col, int row_off, int col_off, Chess color); // 判断该位置以特定偏移的格式是否有五子连珠

private:
    int _room_id;         // 房间码
    RoomStatus _statu;    // 房间状态
    uint16_t _player_cnt; // 房间现在人数
    int _id_white;        // 执白棋的用户id
    int _id_black;        // 执黑棋的用户id
    int _turn_uid;        // 当前轮到的用户id

    User_Table *_user;                      // 用户表操作指针
    Online_Manager *_online;                // 在线用户管理指针
    std::vector<std::vector<Chess>> _board; // 棋盘
};

三种操作类型:下棋、聊天、退出房间

统一由Handle_Request分配,在处理完后,最后由Handle_Request执行广播

cpp 复制代码
#include "Room.hpp"

Json::Value Room::Chess_Handle(Json::Value &request, int authenticated_uid) // 处理下棋操作,返回JSON格式的响应
{
    if (_statu == RoomStatus::GAME_WAIT) // 游戏未开始
        return Protocol::MakeError(RoomOp::PUT_CHESS, RoomCode::GAME_OVER, "游戏未开始或已经结束");

    int chess_row = 0;
    int chess_col = 0;
    if (!Protocol::GetPosition(request, chess_row, chess_col))
        return Protocol::MakeError(RoomOp::PUT_CHESS, RoomCode::INVALID_POSITION, "落子位置无效");

    int self_id = authenticated_uid;
    if (self_id != _id_white && self_id != _id_black)
        return Protocol::MakeError(RoomOp::PUT_CHESS,
                                   RoomCode::INVALID_USER,
                                   "用户不属于当前房间");
    int request_uid = 0;
    if (!Protocol::GetUid(request, request_uid) || request_uid != authenticated_uid)
        return Protocol::MakeError(RoomOp::PUT_CHESS, RoomCode::INVALID_USER, "请求用户身份无效");
    if (self_id != _turn_uid)
        return Protocol::MakeError(RoomOp::PUT_CHESS, RoomCode::NOT_YOUR_TURN, "还没有轮到您走棋");
    int enermy_id = (self_id == _id_white ? _id_black : _id_white); // 敌人id

    // 若对方掉线,则直接判赢
    if (!(_online && _user)) // 如果_online或_user为空
        return Protocol::MakeError(RoomCode::INTERNAL_ERROR, "在线用户或房间用户获取失败");
    if (_online->IsRoom(enermy_id) == false)
    {
        return Protocol::MakeChessResult(_room_id,
                                         authenticated_uid,
                                         -1,
                                         -1,
                                         self_id,
                                         "对方掉线,您已获胜");
    }

    // 获取走棋位置,判断当前走棋是否合理

    if (_board[chess_row][chess_col] != Chess::VACANCY) // 该位置已经有棋
        return Protocol::MakeError(RoomOp::PUT_CHESS,
                                   RoomCode::POSITION_OCCUPIED,
                                   "该位置已有棋子");

    Chess self_color = (self_id == _id_white ? Chess::WHITE_CHESS : Chess::BLACK_CHESS);
    _board[chess_row][chess_col] = self_color; // 下棋

    // 判断是否有玩家胜利
    int winner_id = Check_Win(chess_row, chess_col, self_color);
    if(winner_id == 0) // 没有胜利者,就让对面下棋
    {
        _turn_uid = enermy_id;
    }

    // 若winner_id != 0,再设置reason,否则不设置
    return Protocol::MakeChessResult(_room_id, self_id, chess_row, chess_col, winner_id,
                                     winner_id == 0 ? "" : "获胜");
}

Json::Value Room::Chat_Handle(Json::Value &request, int authenticated_uid) // 处理聊天操作,返回JSON格式的响应
{
    std::string msg;
    if (!Protocol::GetString(request, Protocol::FIELD_MESSAGE, msg))
        return Protocol::MakeError(RoomOp::CHAT, RoomCode::INVALID_MESSAGE, "消息不能为空");
    int request_uid = 0;
    if (!Protocol::GetUid(request, request_uid) || request_uid != authenticated_uid)
        return Protocol::MakeError(RoomOp::CHAT, RoomCode::INVALID_USER, "请求用户身份无效");
    if (msg.find("垃圾") != std::string::npos)
        return Protocol::MakeError(RoomOp::CHAT, RoomCode::INVALID_MESSAGE, "消息包含敏感词");

    Broadcast(Protocol::MakeChatResult(_room_id, authenticated_uid, msg));
    return Protocol::MakeChatResult(_room_id, authenticated_uid, msg);
}

Json::Value Room::Exit_Handle(Json::Value &, int authenticated_uid)
{
    if (authenticated_uid != _id_white && authenticated_uid != _id_black)
        return Protocol::MakeError(RoomOp::EXIT_ROOM, RoomCode::INVALID_USER, "用户不属于当前房间");

    const int opponent_id = authenticated_uid == _id_white ? _id_black : _id_white;
    int winner_id = 0;
    // 若游戏还未结束,直接判对方胜
    if (_statu == RoomStatus::GAME_START && opponent_id != 0)
        winner_id = opponent_id;
        
    _turn_uid = 0;
    _player_cnt--;
    return Protocol::MakeExitResult(_room_id, authenticated_uid,
                                    winner_id,
                                    winner_id == 0 ? "玩家退出房间" : "对方退出游戏,判定胜利");
}

void Room::Handle_Request(Json::Value &request, int authenticated_uid) // 用于区分该请求属于那种类型的请求并交给对应处理接口
{
    Json::Value response;
    while (false)
    {
        RoomOp op;
        // 判断该request的optype字段是否合法
        if (!Protocol::ParseRoomOp(request, op))
        {
            response = Protocol::MakeError(RoomCode::INVALID_REQUEST, "无效的房间操作类型");
            break;
        }

        // 判断请求房间号与当前房间号是否一致

        int room_id = 0;
        if (!Protocol::GetRoomId(request, room_id) || room_id != _room_id)
        {
            response = Protocol::MakeError(RoomCode::INVALID_ROOM, "请求房间号与该房间号不一致");
            break;
        }
        int request_uid = 0;
        if (!Protocol::GetUid(request, request_uid) || request_uid != authenticated_uid)
        {
            response = Protocol::MakeError(RoomCode::INVALID_USER, "请求用户身份无效");
            break;
        }
        // 判断操作类型并转给对应处理接口
        switch (op)
        {
        case RoomOp::PUT_CHESS: // 下棋操作
            response = Chess_Handle(request, authenticated_uid);
            break;
        case RoomOp::CHAT:
            response = Chat_Handle(request, authenticated_uid);
            break;
        case RoomOp::ROOM_READY:
            response = Protocol::MakeError(RoomOp::ROOM_READY, RoomCode::INVALID_REQUEST, "room_ready只能作为服务器响应");
            break;
        case RoomOp::EXIT_ROOM:
            response = Exit_Handle(request, authenticated_uid);
            break;
        default:
            response = Protocol::MakeError(RoomCode::INVALID_REQUEST, "未知房间操作");
        }
    }

    int winner_id;
    if (Protocol::GetInt(request, "winner", winner_id) && winner_id != 0) // 胜负已分,更新数据库
    {
        int loser_id = winner_id == _id_black ? _id_white : _id_black;
        // 更新数据库
        _user->Win(winner_id);
        _user->Lose(loser_id);

        // 更新房间状态
        _statu = RoomStatus::GAME_WAIT; // 游戏结束,房间重新变为等待
    }
    Broadcast(response); // 广播消息
}

bool Room::AddUser(int id, Chess chess) // 为该房间添加黑/白棋用户
{
    if (chess == Chess::BLACK_CHESS) // 添加黑棋用户
    {
        if (_id_black != 0 && _id_black != id)
        {
            LOG(LogLv::ERROR, "该房间黑棋用户已被占");
            return false;
        }
        if (_id_black == id)
            return true;
        _id_black = id;
        _player_cnt++;
    }
    else if (chess == Chess::WHITE_CHESS) // 添加白棋用户
    {
        if (_id_white != 0 && _id_white != id)
        {
            LOG(LogLv::ERROR, "该房间白棋用户已被占");
            return false;
        }
        if (_id_white == id)
            return true;
        _id_white = id;
        _player_cnt++;
    }
    else // 传入的是Chess::VACANSY
        return false;
    if (_id_white != 0 && _id_black != 0)
    {
        _statu = RoomStatus::GAME_START;
        _turn_uid = _id_white;
    }
    return true;
}

void Room::Broadcast(Json::Value response) // 广播消息
{
    std::string body;
    if (!Json_Util::Serialization(response, body)) // 序列化JSON
        return;

    const auto white = _online->GetConn_FromRoom(_id_white);
    const auto black = _online->GetConn_FromRoom(_id_black);
    if (white)
        white->send(body);
    if (black && black != white)
        black->send(body);
}

int Room::Check_Win(int row, int col, Chess color) // 检查这一步棋是否胜利
{
    if (Five(row, col, 0, 1, color) || // 横行
        Five(row, col, 1, 0, color) || // 竖列
        Five(row, col, 1, 1, color) || // 正斜
        Five(row, col, -1, 1, color))  // 反斜
    {
        return (color == Chess::BLACK_CHESS) ? _id_black : _id_white;
    }
    // 没有胜利者
    return 0;
}

bool Room::Five(int row, int col, int row_off, int col_off, Chess color) // 判断该位置以特定偏移的格式是否有五子连珠
{
    int cnt = 1; // 当前连续相同颜色棋子的数量
    // 先搜索一边
    int search_row = row + row_off;
    int search_col = col + col_off;
    while (search_row >= 0 && search_col >= 0 &&
           search_row < ROW && search_col < COL &&
           _board[search_row][search_col] == color)
    {
        cnt++; // 同色棋子+1
        // 继续偏移
        search_row += row_off;
        search_col += col_off;
    }
    // 一边搜索完再搜索另外一边
    search_row = row - row_off;
    search_col = col - col_off;
    while (search_row >= 0 && search_col >= 0 &&
           search_row < ROW && search_col < COL &&
           _board[search_row][search_col] == color)
    {
        cnt++; // 同色棋子+1
        // 继续偏移
        search_row -= row_off;
        search_col -= col_off;
    }

    return cnt >= 5;
}

上面所设计的只是单个房间,当房间增多时,需要将所有房间统一管理起来,因此还需要一个房间管理类

cpp 复制代码
using room_ptr = std::shared_ptr<Room>;

class Room_Manager
{   

public:
    Room_Manager(User_Table *user, Online_Manager *online)
        : _next_rid(1), _user(user), _online(online)
    {
        LOG(LogLv::DEBUG, "房间管理模块初始化成功");
    }

    ~Room_Manager()
    {
        LOG(LogLv::DEBUG, "房间管理模块即将销毁");
    }

    room_ptr Create_Room(int uid1, int uid2); // 通过匹配成功的两个用户创建房间

    room_ptr Get_Room_FromRid(int rid); // 通过房间id获取房间指针

    room_ptr Get_Room_FromUid(int uid); // 通过用户id获取房间指针

    void Remove_Room(int rid); // 通过房间id销毁房间

    void Remove_RoomUser(int uid); // 删除房间中指定用户

private:
    int _next_rid;           // 下一个房间创建时的id
    std::mutex _mutex;       // 创建房间时保证线程安全
    User_Table *_user;       // 用户表操作指针
    Online_Manager *_online; // 在线用户管理指针

    std::unordered_map<int, room_ptr> _rooms; // 房间id和房间指针的映射
    std::unordered_map<int, int> _roomid;     // 用户id和房间id的映射
};

_rooms负责管理所有房间,而_roomid负责提供用户id到房间的关系

cpp 复制代码
room_ptr Room_Manager::Create_Room(int uid1, int uid2) // 通过匹配成功的两个用户创建房间
{
    // 校验两个用户是否在大厅
    if (!_online->IsHall(uid1))
    {
        LOG(LogLv::ERROR, "用户[%d]不在大厅,创建房间失败", uid1);
        return room_ptr();
    }
    else if (!_online->IsHall(uid2))
    {
        LOG(LogLv::ERROR, "用户[%d]不在大厅,创建房间失败", uid2);
        return room_ptr();
    }

    std::lock_guard<std::mutex> lock(_mutex); // 加锁保证线程安全
    // 创建房间
    room_ptr room(new Room(_next_rid, _user, _online));
    room->AddUser(uid1, Chess::BLACK_CHESS);
    room->AddUser(uid2, Chess::WHITE_CHESS);

    // 建立房间id与房间指针的映射
    _rooms.insert(std::make_pair(_next_rid, room));
    _roomid.insert(std::make_pair(uid1, _next_rid));
    _roomid.insert(std::make_pair(uid2, _next_rid));

    _next_rid++; // 更新下一个房间号

    return room;
}

room_ptr Room_Manager::Get_Room_FromRid(int rid) // 通过房间id获取房间指针
{
    std::lock_guard<std::mutex> lock(_mutex); // 加锁保证线程安全

    auto roomit = _rooms.find(rid);
    if (roomit == _rooms.end())
    {
        LOG(LogLv::ERROR, "房间id --> 房间指针 查找失败");
        return room_ptr();
    }
    return roomit->second;
}

room_ptr Room_Manager::Get_Room_FromUid(int uid) // 通过用户id获取房间指针
{
    std::lock_guard<std::mutex> lock(_mutex); // 加锁保证线程安全

    // 1. 通过 用户id 获取 房间id
    auto idit = _roomid.find(uid);
    if (idit == _roomid.end())
    {
        LOG(LogLv::ERROR, "用户id --> 房间id 查找失败");
        return room_ptr();
    }

    // 2. 通过 房间id 获取 房间指针
    int roomid = idit->second;
    auto roomit = _rooms.find(roomid);
    if (roomit == _rooms.end())
    {
        LOG(LogLv::ERROR, "房间id --> 房间指针 查找失败");
        return room_ptr();
    }
    return roomit->second;
}

void Room_Manager::Remove_Room(int rid) // 通过房间id销毁房间
{
    // 通过 房间id 获取 房间指针
    room_ptr room = Get_Room_FromRid(rid);
    if (room == room_ptr())
    {
        LOG(LogLv::WARNING, "没有房间[%d],无需销毁", rid);
        return;
    }
    // 通过房间指针,获取 黑棋 & 白棋 用户id
    int id_black = room->GetUser_Black();
    int id_white = room->GetUser_White();

    // 删除对应 用户id -> 房间id 映射
    _roomid.erase(id_black);
    _roomid.erase(id_white);

    // 删除对应 房间id -> 房间指针 映射
    _rooms.erase(rid);
}

void Room_Manager::Remove_RoomUser(int uid) // 删除房间中指定用户
// 若房间没用户了,就销毁房间
{
    // 通过 用户id 获取 房间指针
    room_ptr room = Get_Room_FromUid(uid);
    if (room == room_ptr())
    {
        LOG(LogLv::WARNING, "用户[%d]已离开房间,无需删除", uid);
        return;
    }

    // 在房间内删除该玩家
    room->Exit_Handle(Json::Value(), uid);
    // 若房间中没有玩家了,就销毁房间
    if(room->GetPlayerCnt() == 0)
        Remove_Room(room->GetId());
}
相关推荐
程序员-Benothing1 小时前
Linux编码转换命令:iconv、dos2unix、base64、md5sum
linux·运维·服务器
RoboWizard1 小时前
搭配W790芯片组的超频服务器内存(架构选型)FAQ合集
运维·服务器·架构
2402_882893861 小时前
Linux 进程概念详解(上)—— 从冯诺依曼体系到僵尸进程与孤儿进程
linux·运维·服务器
qetfw1 小时前
Debian iptables 防火墙:默认拒绝、SNAT 与 DNAT 验证
linux·服务器·debian·php
Black蜡笔小新2 小时前
老旧小区改造难题:新旧摄像头如何统一接入?EasyCVR一招融合
网络·音视频·easycvr
xixiaoyunya2 小时前
实验室检测数据内网自动化备份方案:满足合规可追溯的落地实践
服务器·网络·自动化
优化Henry3 小时前
光路异常告警排查实战:LTE室分与5G小区两个案例解析
运维·网络·学习·5g·tdd
Apipi*3 小时前
30天速通Linux 第九章Linux网络编程
linux·运维·网络