Bladeren bron

添加游戏房间管理、在线用户管理、对战匹配管理模块

yaozheng0922 2 maanden geleden
bovenliggende
commit
4603290d40
3 gewijzigde bestanden met toevoegingen van 561 en 0 verwijderingen
  1. 173 0
      source/matcher.hpp
  2. 76 0
      source/online.hpp
  3. 312 0
      source/room.hpp

+ 173 - 0
source/matcher.hpp

@@ -0,0 +1,173 @@
+#ifndef __MATCHER_HPP__
+#define __MATCHER_HPP__
+#include "db.hpp"
+#include "room.hpp"
+#include "util.hpp"
+#include "online.hpp"
+#include <list>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+
+/*用户对战匹配管理模块 -- 将用户按分数分为青铜、黄金、王者三档,并分别为它们设计一个匹配队列,队列元素>=2则匹配成功,否则阻塞*/
+/*匹配队列类*/
+template <class T>
+class match_queue {
+public:
+    match_queue() {}
+    ~match_queue() {}
+    /*目标元素入队列,并唤醒线程*/
+    void push(const T& data) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _list.push_back(data);
+        LOG(DEBUG, "%d用户加入匹配队列", data);
+        // 匹配队列每新增一个元素,就唤醒对应的匹配线程,判断是否满足匹配要求(队列人数>=2)
+        _cond.notify_all();
+    }
+
+    /*队头元素出队列并返回队头元素*/
+    bool pop(T& data) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        if(_list.empty()) return false;
+        data = _list.front();
+        _list.pop_front();
+        LOG(DEBUG, "%d用户从匹配队列中移除", data);
+        return true;
+    }
+
+    /*移除队列中的目标元素*/
+    void remove(const T& data) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _list.remove(data);
+        LOG(DEBUG, "%d用户从匹配队列中移除", data);
+    }
+    
+    /*阻塞线程*/
+    void wait() {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _cond.wait(lock);
+    }
+
+    /*获取队列元素个数*/
+    int size() { 
+        std::unique_lock<std::mutex> lock(_mutex);
+        return _list.size(); 
+    }
+
+    /*判断队列是否为空*/
+    bool empty() {
+        std::unique_lock<std::mutex> lock(_mutex); 
+        return _list.empty();
+    }
+private:
+    std::list<T> _list;  // 使用双向链表而不是queue充当匹配队列,便于用户取消匹配时将该用户从匹配队列中移除
+    std::mutex _mutex;   // 实现线程安全
+    std::condition_variable _cond;  // 条件变量,当向队列中push元素时唤醒,用于阻塞消费者
+};
+
+/*匹配管理类*/
+class matcher {
+private:
+    void handler_match(match_queue<uint64_t> &mq) {
+        while(true) {
+            // 检查匹配条件是否满足(人数>=2),不满足则继续阻塞
+            while(mq.size() < 2) mq.wait();
+            // 条件满足,从队列中取出两个玩家
+            uint64_t uid1, uid2;
+            if(mq.pop(uid1) == false) continue;
+            if(mq.pop(uid2) == false) {
+                // 如果第二个玩家出队列失败,则需要将第一个玩家重新添加到队列中
+                this->add(uid1);
+                continue;
+            }
+            // 检查两个玩家是否都处于大厅在线状态,若一方掉线,则需要将另一方重新添加到队列
+            wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);
+            wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);
+            if(conn1.get() == nullptr) {
+                this->add(uid2);
+                continue;
+            }
+            if(conn2.get() == nullptr) {
+                this->add(uid1);
+                continue;
+            }
+            // 为两个玩家创建房间,失败则重新添加到队列
+            room_ptr rp = _rm->create_room(uid1, uid2);
+            if(rp.get() == nullptr) {
+                this->add(uid1);
+                this->add(uid2);
+                continue;
+            }
+            // 给玩家返回匹配成功的响应
+            Json::Value resp;
+            resp["optype"] = "match_success";
+            resp["result"] = true;
+            std::string body;
+            json_util::serialize(resp, body);
+            conn1->send(body);
+            conn2->send(body);
+        }
+    }
+
+    /*三个匹配队列的线程入口*/
+    void th_low_entry() { handler_match(_q_low); }
+    void th_mid_entry() { handler_match(_q_mid); }
+    void th_high_entry() { handler_match(_q_high); }
+public:
+    matcher(user_table *ut, online_manager *om, room_manager *rm)
+    : _ut(ut), _om(om), _rm(rm), 
+    _th_low(std::thread(&matcher::th_low_entry, this)),
+    _th_mid(std::thread(&matcher::th_mid_entry, this)),
+    _th_high(std::thread(&matcher::th_high_entry, this)) {
+        LOG(DEBUG, "游戏对战匹配管理模块初始化完毕");
+    }
+
+    ~matcher() {
+        LOG(DEBUG, "游戏对战匹配管理模块已被销毁");
+    }
+
+    /*添加用户到匹配队列*/
+    bool add(uint64_t uid) {
+        // 根据用户id获取用户数据库信息
+        Json::Value user;
+        if(_ut->select_by_id(uid, user) == false) {
+            LOG(DEBUG, "查找玩家%d信息失败", uid);
+            return false;
+        }
+        // 根据用户分数将用户添加到对应的匹配队列中去
+        int score = user["score"].asInt();
+        if(score < 2000) _q_low.push(uid);
+        else if(score >= 2000 && score < 3000) _q_mid.push(uid);
+        else _q_high.push(uid);
+        return true;
+    }
+
+    /*将用户从匹配队列中移除*/
+    bool remove(uint64_t uid) {
+        // 根据用户id获取用户数据库信息
+        Json::Value user;
+        if(_ut->select_by_id(uid, user) == false) {
+            LOG(DEBUG, "查找用户%d信息失败", uid);
+            return false;
+        }
+        // 根据用户分数将用户从对应的匹配队列中移除
+        int score = user["score"].asInt();
+        if(score < 2000) _q_low.remove(uid);
+        else if(score >= 2000 && score < 3000) _q_mid.remove(uid);
+        else _q_high.remove(uid);
+        return true;        
+    }
+private:
+    // 三个匹配队列(青铜/黄金/王者 -> low/mid/high)
+    match_queue<uint64_t> _q_low;
+    match_queue<uint64_t> _q_mid;
+    match_queue<uint64_t> _q_high;
+    // 三个管理匹配队列的线程
+    std::thread _th_low;
+    std::thread _th_mid;
+    std::thread _th_high;
+    room_manager *_rm;    // 游戏房间管理句柄
+    online_manager *_om;  // 在线用户管理句柄
+    user_table *_ut;      // 用户数据管理句柄
+};
+#endif

+ 76 - 0
source/online.hpp

@@ -0,0 +1,76 @@
+#ifndef __ONLINE_HPP__
+#define __ONLINE_HPP__
+#include "util.hpp"
+#include <mutex>
+#include <unordered_map>
+#include <websocketpp/server.hpp>
+#include <websocketpp/config/asio_no_tls.hpp>
+
+typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
+
+/*在线用户管理模块 -- 用于管理在游戏大厅以及游戏房间中的用户,建立用户id与websocket长连接的对应关系*/
+class online_manager {
+public:
+    online_manager() { LOG(DEBUG, "在线用户管理模块初始化完毕"); }
+    ~online_manager() { LOG(DEBUG, "在线用户管理模块已被销毁"); }
+
+    /*用户进入游戏大厅(此时用户websocket长连接已建立好)*/
+    void enter_game_hall(uint64_t uid, wsserver_t::connection_ptr conn) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _hall_user[uid] = conn;
+    }
+    /*用户进入游戏房间*/
+    void enter_game_room(uint64_t uid, wsserver_t::connection_ptr conn) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _room_user[uid] = conn;
+    }
+
+    /*用户离开游戏大厅(websocket长连接断开时)*/
+    void exit_game_hall(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _hall_user.erase(uid);
+    }
+
+    /*用户对战结束离开游戏房间回到游戏大厅*/
+    void exit_game_room(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        _room_user.erase(uid);
+    }
+
+    /*判断当前用户是否在游戏大厅*/
+    bool is_in_game_hall(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        auto it = _hall_user.find(uid);
+        if(it == _hall_user.end()) return false;
+        return true;
+    }
+
+    /*判断当前用户是否在游戏房间*/
+    bool is_in_game_room(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        auto it = _room_user.find(uid);
+        if(it == _room_user.end()) return false;
+        return true;
+    }
+
+    /*通过用户id获取游戏大厅用户的通信连接*/
+    wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        auto it = _hall_user.find(uid);
+        if(it == _hall_user.end()) return nullptr;
+        return _hall_user[uid];
+    }
+
+    /*通过用户id获取游戏房间用户的通信连接*/
+    wsserver_t::connection_ptr get_conn_from_room(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        auto it = _room_user.find(uid);
+        if(it == _room_user.end()) return nullptr;
+        return _room_user[uid];
+    }
+private:
+    std::mutex _mutex;  // 解决多线程模式下的线程安全问题
+    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user;  // 建立游戏大厅用户id与通信连接之间的关系
+    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user;  // 建立游戏房间用户id与通信连接之间的关系
+};
+#endif

+ 312 - 0
source/room.hpp

@@ -0,0 +1,312 @@
+#ifndef __ROOM_HPP__
+#define __ROOM_HPP__
+#include "util.hpp"
+#include "db.hpp"
+#include "online.hpp"
+#include <vector>
+
+#define BOARD_ROW 15
+#define BOARD_COL 15
+#define CHESS_WHITE 1
+#define CHESS_BLACK 2
+
+typedef enum {
+    GAME_START,
+    GAME_OVER
+} room_status;
+
+/*游戏房间管理模块 -- 用于管理在游戏房间中产生的各种数据以及动作,同时也包括对多个游戏房间本身的管理*/
+/*游戏房间类*/
+class room {
+private:
+    /*check_win子函数,其中row/col表示下棋位置,row_off/col_off表示是否偏移*/
+    bool five_piece(int row, int col, int row_off, int col_off, int color) {
+        int count = 1; 
+        // 处理正方向
+        int search_row = row + row_off;
+        int search_col = col + col_off;
+        while((search_row >= 0 && search_row < BOARD_ROW) && (search_col >= 0 && search_col < BOARD_COL)
+               && (_board[search_row][search_col] == color)) {
+            ++count;
+            search_row += row_off;
+            search_col += col_off;
+        }
+        // 处理反方向
+        search_row = row - row_off;
+        search_col = col - col_off;
+        while((search_row >= 0 && search_row < BOARD_ROW) && (search_col >= 0 && search_col < BOARD_COL)
+               && (_board[search_row][search_col] == color)) {
+            ++count;
+            search_row -= row_off;
+            search_col -= col_off;
+        }
+        return count >= 5;
+    }
+
+    /*判断是否有用户胜利并返回winner_id (0表示没有用户胜利,非0表示有)*/
+    uint64_t check_win(int chess_row, int chess_col, int cur_color) {
+        uint64_t winner_id = cur_color == CHESS_WHITE ? _white_user_id : _black_user_id;
+        // 横行方向:当前位置开始,行不变,列++/--
+        if(five_piece(chess_row, chess_col, 0, 1, cur_color)) return winner_id;
+        // 纵列方向:当前位置开始,行++/--,列不变
+        if(five_piece(chess_row, chess_col, 1, 0, cur_color)) return winner_id;
+        // 正斜方向:当前位置开始,行++列-- 以及 行--列++
+        if(five_piece(chess_row, chess_col, 1, -1, cur_color)) return winner_id;
+        // 反斜方向:当前位置开始,行++列++ 以及 行--列--
+        if(five_piece(chess_row, chess_col, 1, 1, cur_color)) return winner_id;
+        // 没有人获胜返回0
+        return 0;
+    }
+
+    /*用户胜利或失败后更新用户数据库信息*/
+    void update_db_info(uint64_t winner_id, uint64_t loser_id) {
+        _tb_user->win(winner_id);
+        _tb_user->lose(loser_id);
+    }
+public:
+    room(uint64_t room_id, user_table *tb_user, online_manager *online_user)
+    : _room_id(room_id), _statu(GAME_START), _tb_user(tb_user), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
+    {
+        LOG(DEBUG, "%d号房间创建成功", _room_id);
+    }
+    ~room() { LOG(DEBUG, "%d号房间已被销毁", _room_id); }
+
+    /*添加白棋用户*/
+    void add_white_user(uint64_t id) {
+        _white_user_id = id;
+        ++_player_count;
+    }
+
+    /*添加黑棋用户*/
+    void add_black_user(uint64_t id) {
+        _black_user_id = id;
+        ++_player_count;
+    }
+
+    /*处理玩家下棋动作并返回响应*/
+    Json::Value handler_chess(Json::Value &req) {
+        Json::Value resp = req;
+        // 判断白棋与黑棋用户是否在线,若一方不在线,另一方直接获胜
+        if(_online_user->is_in_game_room(_white_user_id) == false) {
+            resp["result"] = true;
+            resp["reason"] = "对方已掉线,游戏获胜";  // 在黑棋的视角,白棋是"对方"  
+            resp["winner"] = (Json::UInt64)_black_user_id;  // 白棋掉线,黑棋用户
+        }
+        if(_online_user->is_in_game_room(_black_user_id) == false) {
+            resp["result"] = true;
+            resp["reason"] = "对方已掉线,游戏胜利";    
+            resp["winner"] = (Json::UInt64)_white_user_id;  
+        }
+        // 获取下棋位置,判断位置是否合理并下棋
+        uint64_t cur_uid = req["uid"].asUInt64();
+        int chess_row = req["row"].asInt();
+        int chess_col = req["col"].asInt();
+        if(_board[chess_row][chess_col] != 0) {
+            resp["result"] = false;
+            resp["reason"] = "该位置已被占用";
+            return resp;            
+        }
+        int cur_color = (cur_uid == _white_user_id ? CHESS_WHITE : CHESS_BLACK);
+        _board[chess_row][chess_col] = cur_color;
+        // 判断是否有玩家获胜(存在五星连珠的情况) 其中0表示没有玩家胜利,非0表示胜利的玩家id
+        uint64_t winner_id = check_win(chess_row, chess_col, cur_color);
+        resp["result"] = true;
+        resp["reason"] = "下棋成功";  
+        resp["winner"] = (Json::UInt64)winner_id;
+        if(winner_id != 0) { resp["reason"] = "五星连珠,游戏胜利"; }
+        return resp;
+    }
+
+    /*处理玩家聊天动作并返回响应*/
+    Json::Value handler_chat(Json::Value &req) {
+        Json::Value resp = req;
+        // 检查消息中是否包含敏感词
+        std::string msg = req["message"].asString();
+        size_t pos = msg.find("垃圾");
+        if(pos != std::string::npos) {
+            resp["result"] = false;
+            resp["reason"] = "消息中包含敏感词";
+            return resp;
+        }
+        resp["reslut"] = true;
+        return resp;
+    }
+
+    /*处理玩家退出动作并返回响应*/
+    void handler_exit(uint64_t uid) {
+        // 如果玩家在下棋中,则对方直接获胜
+        if(_statu == GAME_START) {
+            Json::Value resp;
+            resp["optype"] = "put_chess";
+            resp["result"] = true;
+            resp["reason"] = "对方已退出,游戏胜利";
+            resp["room_id"] = (Json::UInt64)_room_id;
+            resp["uid"] = (Json::UInt64)uid;
+            resp["row"] = -1;
+            resp["col"] = -1;
+            resp["winner"] = (Json::UInt64)(uid == _white_user_id ? _black_user_id : _white_user_id);
+            // 更新用户数据库信息与游戏房间的状态
+            uint64_t loser_id = uid;
+            uint64_t winner_id = loser_id == _white_user_id ? _black_user_id : _white_user_id;
+            update_db_info(winner_id, loser_id);
+            _statu = GAME_OVER;
+            // 将消息广播给房间其他玩家
+            broadcast(resp);
+        }
+        // 游戏结束正常退出直接更新玩家数量
+        --_player_count;
+    }
+
+    /*总的动作处理函数,负责判断动作类型并调用对应的处理函数,得到处理响应后将其广播给房间中其他用户*/
+    /*注意:玩家退出动作属于玩家断开连接后调用的操作,不属于handler的一种*/
+    void handler(Json::Value &req) {
+        Json::Value resp;
+        // 判断房间号是否匹配
+        if(_room_id != req["room_id"].asUInt64()) {
+            resp["optype"] = req["optype"].asString();
+            resp["result"] = false;
+            resp["reason"] = "房间号不匹配";
+            broadcast(resp);
+            return;
+        }
+        // 根据请求类型调用不同的处理函数
+        std::string type = req["optype"].asString();
+        if(type == "put_chess") {
+            resp = handler_chess(req);
+            // 判断是否有玩家获胜,如果有则需要更新用户数据库信息与游戏房间的状态
+            if(resp["winner"].asUInt64() != 0) {
+                uint64_t winner_id = resp["winner"].asUInt64();
+                uint64_t loser_id = (winner_id == _white_user_id ? _black_user_id : _white_user_id);
+                update_db_info(winner_id, loser_id);
+                _statu = GAME_OVER;
+            }
+        } else if(type == "chat") {
+            resp = handler_chat(req);
+        } else {
+            resp["optype"] = req["optype"].asString();
+            resp["result"] = false;
+            resp["reason"] = "位置请求类型";
+        }
+        // 将消息广播给房间中的其他玩家
+        broadcast(resp);
+    }
+
+    /*将动作响应广播给房间中的其他玩家*/
+    void broadcast(Json::Value &resp) {
+        // 将Json响应进行序列化
+        std::string body;
+        json_util::serialize(resp, body);
+        // 获取房间中的所有玩家的通信连接
+        wsserver_t::connection_ptr conn_white = _online_user->get_conn_from_room(_white_user_id);
+        wsserver_t::connection_ptr conn_black = _online_user->get_conn_from_room(_black_user_id);
+        // 如果玩家连接没有断开,则将消息广播给他
+        if(conn_white.get() != nullptr) {
+            conn_white->send(body);
+        }
+        if(conn_black.get() != nullptr) {
+            conn_black->send(body);
+        }
+    }
+public:
+    // 将部分成员变量设为public,供外部类访问
+    uint64_t _room_id;             // 房间ID
+    room_status _statu;            // 房间状态
+    int _player_count;             // 玩家数量
+    uint64_t _white_user_id;       // 白棋玩家ID
+    uint64_t _black_user_id;       // 黑棋玩家ID
+private:
+    user_table *_tb_user;          // 管理玩家数据的句柄
+    online_manager *_online_user;  // 管理玩家在线状态的句柄 
+    std::vector<std::vector<int>> _board;  // 二维棋盘
+};
+
+/*管理房间数据的智能指针*/
+using room_ptr = std::shared_ptr<room>;  
+
+/*游戏房间管理类*/
+class room_manager {
+public:
+    room_manager(user_table *tb_user, online_manager *online_user)
+    : _next_rid(1), _tb_user(tb_user), _online_user(online_user) {
+        LOG(DEBUG, "游戏房间管理模块初始化成功");
+    }
+    ~room_manager() { LOG(NORMAL, "游戏房间管理模块已被销毁"); }
+
+    /*为两个玩家创建房间,并返回房间信息*/
+    room_ptr create_room(uint64_t uid1, uint64_t uid2) {
+        // 判断两个玩家是否都处于游戏大厅中
+        if(_online_user->is_in_game_hall(uid1) == false || _online_user->is_in_game_hall(uid2) == false) {
+            LOG(DEBUG, "玩家不在游戏大厅中,匹配失败");
+            return room_ptr();
+        }
+        // 创建游戏房间,将用户信息添加到房间中
+        std::unique_lock<std::mutex> lock(_mutex);
+        room_ptr rp(new room(_next_rid, _tb_user, _online_user));
+        rp->add_white_user(uid1);
+        rp->add_black_user(uid2);
+        // 将游戏房间管理起来(建立房间id与房间信息以及玩家id与房间id的关联关系)
+        _rooms[_next_rid] = rp;
+        _users[uid1] = _next_rid;
+        _users[uid2] = _next_rid;
+        // 更新下一个房间的房间id
+        ++_next_rid;
+        // 返回房间信息
+        return rp;
+    }
+
+    /*通过房间id获取房间信息*/
+    room_ptr get_room_by_rid(uint64_t rid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        auto it = _rooms.find(rid);
+        if(it == _rooms.end()) return room_ptr();
+        return _rooms[rid];
+    }
+
+    /*通过用户id获取房间信息*/
+    room_ptr get_room_by_uid(uint64_t uid) {
+        std::unique_lock<std::mutex> lock(_mutex);
+        // 获取房间id
+        auto it1 = _users.find(uid);
+        if(it1 == _users.end()) return room_ptr();
+        uint64_t rid = _users[uid];
+        // 获取房间信息(这里不能直接调用get_room_by_rid,会造成死锁)
+        auto it2 = _rooms.find(rid);
+        if(it2 == _rooms.end()) return room_ptr();
+        return _rooms[rid];
+    }
+
+    /*通过房间id销毁房间*/
+    void remove_room(uint64_t rid) {
+        // 通过房间id获取房间信息
+        room_ptr rp = get_room_by_rid(rid);
+        if(rp.get() == nullptr) return;
+        // 通过房间信息获取房间中的玩家
+        uint64_t white_user_id = rp->_white_user_id;
+        uint64_t black_user_id = rp->_black_user_id;
+        // 移除房间管理中的玩家信息
+        std::unique_lock<std::mutex> lock(_mutex);
+        _users.erase(white_user_id);
+        _users.erase(black_user_id);
+        // 移除房间管理信息 -- 移除房间对应的shared_ptr(room_ptr)
+        _rooms.erase(rid);
+    }
+
+    /*删除房间中的指定用户,若房间中没有用户则销毁房间(用户断开websocket连接时调用)*/
+    void remove_room_user(uint64_t uid) {
+        // 通过玩家id获取房间信息
+        room_ptr rp = get_room_by_uid(uid);
+        if(rp.get() == nullptr) return;
+        // 玩家退出
+        rp->handler_exit(uid);
+        // 如果房间中没有玩家了,则移除房间
+        if(rp->_player_count == 0) remove_room(rp->_room_id);
+    }
+private:
+    uint64_t _next_rid;             //房间ID分配计数器
+    std::mutex _mutex;              
+    user_table *_tb_user;           // 管理玩家数据的句柄
+    online_manager *_online_user;   // 管理玩家在线状态的句柄
+    std::unordered_map<uint64_t, room_ptr> _rooms;  // 建立房间id与房间信息的关联关系
+    std::unordered_map<uint64_t, uint64_t> _users;  // 建立用户id与房间id的关联关系
+};
+#endif