房卡麻将分析系列 "牌局回放" 之 数据设计详解及实例

房卡麻将分析系列 "牌局回放" 之 数据设计      

最近几个月,”房卡“棋牌游戏成为了资本追逐的热点,基于微信的广大用户和社交属性,”房卡”棋牌发展迅速。红孩儿团队因为之前几年有过相关项目的经验积累,鉴于未来广阔的地方棋牌市场和”开发间“机制的发展前景,也开始转向基于”开房间“棋牌游戏的项目开发中。为了更好的与开发者进行交流学习,特开设”房卡麻将游戏分析系列“。

红孩儿团队研发的"大赢家"红中麻将

本套麻将分析基于网络上流传的“网狐”房卡麻将源码做为基础,按照功能模块分为"架设指南",”服务器框架","后台系统","胡牌算法","客户端界面",“防作弊功能”等等细节做一些分析和指导,帮助广大的棋牌游戏开发者迅速掌握“房卡”麻将的研发原理和技巧设计。也希望有兴趣的朋友多多关注。

第一次开公众号,挑个简单的下手,先来讲一讲房卡麻将中一个重要功能:“牌局回放”,我们都知道,棋牌类游戏注重公平真实不作弊,如果玩家感觉到游戏的过程有作弊,我相信他一定会对这款游戏失去兴趣。但作弊与否,玩家并不容易进行判断。这时候提供一个“牌局回放”功能给玩家进行分析就尤为重要。

“网狐”等一些长期耕耘在棋牌领域的企业,在这方面都有完整的经验和框架,通过参考,我发现它是通过下面一套流程来完成”牌局回放“功能的。

首先,在游戏服务器的房间类CTableFrameSink里需要有一个GameRecord结构,这个结构对 玩家信息,手牌以及每一步的动作都可以进行相应的记录:

struct GameRecordPlayer
{
  DWORD dwUserID;
  std::string kHead;
  std::string kNickName;
  std::vector<BYTE> cbCardData;
  void StreamValue(datastream& kData, bool bSend)
  {
    Stream_VALUE(dwUserID);
    Stream_VALUE(kHead);
    Stream_VALUE(kNickName);
    Stream_VECTOR(cbCardData);
  }
}; 

struct GameRecordOperateResult
{
  enum Type
  {
    TYPE_NULL,
    TYPE_OperateResult,
    TYPE_SendCard,
    TYPE_OutCard,
    TYPE_ChiHu,
  }; 

  GameRecordOperateResult()
  {
    cbActionType = 0;
    wOperateUser = 0;
    wProvideUser = 0;
    cbOperateCode = 0;
    cbOperateCard = 0;
  } 

  BYTE    cbActionType;
  WORD    wOperateUser;            //操作用户
  WORD    wProvideUser;            //供应用户
  BYTE    cbOperateCode;           //操作代码
  BYTE    cbOperateCard;           //操作扑克 

  void StreamValue(datastream& kData, bool bSend)
  {
    Stream_VALUE(cbActionType);
    Stream_VALUE(wOperateUser);
    Stream_VALUE(wProvideUser);
    Stream_VALUE(cbOperateCode);
    Stream_VALUE(cbOperateCard);
  }
}; 

struct GameRecord
{
  std::vector<GameRecordPlayer>   kPlayers;
  std::vector<GameRecordOperateResult> kAction; 

  void StreamValue(datastream& kData, bool bSend)
  {
    StructVecotrMember(GameRecordPlayer, kPlayers);
    StructVecotrMember(GameRecordOperateResult, kAction);
  } 

  void CleanUp()
  {
    kPlayers.clear();
    kAction.clear();
  }
};

在datastream.h中,有一套set,get数据流的宏,能够将数据放入到数据流中或从中拿出。

#define Stream_VALUE(Name) \
  if(bSend)      \
{              \
  kData.pushValue(Name);\
}\
else\
{\
  kData.popValue(Name);\
}\

好了,有了这样一个结构,在游戏开始的时候,我们就可以开始记录本局了。

//游戏开始
void CTableFrameSink::GameStart()
{
    ...
    //填充四个玩家的基础信息
  for (int i = 0; i < 4; i++)
  {
    GameRecordPlayer  tNewRecordPlayer; 

    tagUserInfo *  tpUserInfo = m_pITableFrame->GetTableUserItem(i)->GetUserInfo();
    tNewRecordPlayer.dwUserID = tpUserInfo->dwUserID;
    tNewRecordPlayer.kNickName = tpUserInfo->szNickName; 

        //取得手牌信息
    BYTE cbCardData[MAX_COUNT];
    m_GameLogic.SwitchAllToCardData(m_cbCardIndex[i], cbCardData); 

    for (int j = 0; j < MAX_COUNT ; j++)
    {
      tNewRecordPlayer.cbCardData.push_back(cbCardData[j]);
    }
        //存储到当前记录结构中的玩家信息容器。
    m_sGameRecord.kPlayers.push_back(tNewRecordPlayer);
  }
}

然后我们开始记录操作,分别在玩家出牌,以及玩家应答吃,碰,杠,胡等操作时加入记录。

//用户出牌
bool CTableFrameSink::OnUserOutCard(WORD wChairID, BYTE cbCardData)
{
     ...
  //记录动作数据
  GameRecordOperateResult  tNewRecordOperateResult;
  tNewRecordOperateResult.cbActionType =    GameRecordOperateResult::TYPE_OutCard;
  tNewRecordOperateResult.cbOperateCard = cbCardData;
  tNewRecordOperateResult.cbOperateCode = WIK_NULL;
  tNewRecordOperateResult.wOperateUser = wChairID;
  tNewRecordOperateResult.wProvideUser = wChairID;
  m_sGameRecord.kAction.push_back(tNewRecordOperateResult);
     ...
}
//用户操作
bool CTableFrameSink::OnUserOperateCard(WORD wChairID, BYTE cbOperateCode, BYTE cbOperateCard)
{
     ...
//记录动作数据
    GameRecordOperateResult  tNewRecordOperateResult;
      tNewRecordOperateResult.cbActionType = XZDDGameRecordOperateResult::TYPE_OperateResult;
    tNewRecordOperateResult.cbOperateCard = cbOperateCard;
    tNewRecordOperateResult.cbOperateCode = cbOperateCode;
    tNewRecordOperateResult.wOperateUser = wChairID;
    tNewRecordOperateResult.wProvideUser = m_wProvideUser;
    m_sGameRecord.kAction.push_back(tNewRecordOperateResult);
     ...
}

就这样,基本的操作记录也完成了。最后当牌局结束时,我们需要将记录提交到数据库中。

//游戏结束
bool CTableFrameSink::OnEventGameConclude(WORD wChairID, IServerUserItem * pIServerUserItem, BYTE cbReason)
{
  switch (cbReason)
  {
  case GER_NORMAL:    //常规结束
    {
         ...
            //将记录转化为数据流。
      datastream kDataStream;
      m_sGameRecord.StreamValue(kDataStream, true);
            //除去写分等处理,这里最后一个参数即是数据流。
            m_pITableFrame->WriteTableScore(ScoreInfoArray, CountArray(ScoreInfoArray), kDataStream); 

         ...
        }
    }
}

在私人场服务器中,会通过WriteTableScore这个函数调用PrivateTableInfo的writeSocre,它将将数的流记录下来。

并最终在牌局结束时DismissRoom(pTableInfo);发给了数据库。

数据库最终会通过一个存储过程的执行完成将数据流入库的工作。具体的代码就不再展示了,大家可以参考

CDataBaseEngineSink::OnRequestPrivateGameRecord()。

这样一套完整的回放数据流程就结束了。

好,今天的分析就到这里,红孩儿欢迎大家下次继续听课哦~

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • 详解房卡麻将分析系列 "牌局回放" 之 播放处理

    详解房卡麻将分析系列 "牌局回放" 之 播放处理 昨天红孩儿给大伙讲了讲"牌局回放"的数据记录处理,有了数据的存储,下面就是数据的显示了. 实话讲,好久没用过 SQL Server 来做数据库了, 网狐的服务器是基于WIN,IOCP,  SQL Server 这套路子.配置好后,可以在QPTreasureDB数据库中看到三个牌局相关的表. 其中dbo.PrivateGameRecord是存储当前游戏的房间及玩家,最终胜负信息的. dbo.PrivateGameRec

  • 麻将游戏算法深入解析及实现代码

    麻将游戏算法深入解析及实现代码 这两天为了工具箱的完善,整理了这些年引擎开发的一些资料,无意中发现06年写的一个麻将算法,编译运行了一下,还是有点意思的,拿出来整理一下分享给大家. 麻将是一种大家最喜爱的娱乐活动之一,相信所有人都有接触过.我写的这版算法,是可以吃,碰,杠,还有把牌摸完没有人胡时的皇庄和包听.是用控制台方式来表现的,什么?控制台? 对,因为是算法的设计,所以用控制台来表现当然最简单了. 当然,在交互时要用文字输入会有少许不便,不过这种形式的游戏可是图形游戏的鼻祖哦~ 好,废话不多

  • 房卡麻将分析系列 "牌局回放" 之 数据设计详解及实例

    房卡麻将分析系列 "牌局回放" 之 数据设计       最近几个月,"房卡"棋牌游戏成为了资本追逐的热点,基于微信的广大用户和社交属性,"房卡"棋牌发展迅速.红孩儿团队因为之前几年有过相关项目的经验积累,鉴于未来广阔的地方棋牌市场和"开发间"机制的发展前景,也开始转向基于"开房间"棋牌游戏的项目开发中.为了更好的与开发者进行交流学习,特开设"房卡麻将游戏分析系列". 红孩儿团队研发的&

  • 房卡麻将分析系列 "牌局回放" 之 数据设计详解及实例

    房卡麻将分析系列 "牌局回放" 之 数据设计       最近几个月,"房卡"棋牌游戏成为了资本追逐的热点,基于微信的广大用户和社交属性,"房卡"棋牌发展迅速.红孩儿团队因为之前几年有过相关项目的经验积累,鉴于未来广阔的地方棋牌市场和"开发间"机制的发展前景,也开始转向基于"开房间"棋牌游戏的项目开发中.为了更好的与开发者进行交流学习,特开设"房卡麻将游戏分析系列". 红孩儿团队研发的&

  • ABP(现代ASP.NET样板开发框架)系列之二、ABP入门教程详解

    ABP是"ASP.NET Boilerplate Project (ASP.NET样板项目)"的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板. ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开源项目:https://github.com/aspnetboilerplate ABP 的由来 "DRY--避免重复

  • mysql分区功能详解,以及实例分析

    一,什么是数据库分区 前段时间写过一篇关于mysql分表的 的文章,下面来说一下什么是数据库分区,以mysql为例.mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面 (可以通过my.cnf中的datadir来查看),一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,一个是myi存表 索引的.如果一张表的数据量太大的话,那么myd,myi就会变的很大,查找数据就会变的很慢,这个时候我们可以利用mysql的分区功能,在物理上将这 一张

  • Java List 用法详解及实例分析

    Java List 用法详解及实例分析 Java中可变数组的原理就是不断的创建新的数组,将原数组加到新的数组中,下文对Java List用法做了详解. List:元素是有序的(怎么存的就怎么取出来,顺序不会乱),元素可以重复(角标1上有个3,角标2上也可以有个3)因为该集合体系有索引 ArrayList:底层的数据结构使用的是数组结构(数组长度是可变的百分之五十延长)(特点是查询很快,但增删较慢)线程不同步 LinkedList:底层的数据结构是链表结构(特点是查询较慢,增删较快) Vector

  • Spring的事件机制知识点详解及实例分析

    同步事件和异步事件 同步事件: 在一个线程里,按顺序执行业务,做完一件事再去做下一件事. 异步事件: 在一个线程里,做一个事的同事,可以另起一个新的线程执行另一件事,这样两件事可以同时执行. 用一个例子来解释同步事件和异步事件的使用场景,有时候一段完整的代码逻辑,可能分为几部分,拿最常见的注册来说,假设完整流程是,1.点击注册->2.检验信息并存库->3.发送邮件通知->4.返回给用户.代码这么写是正确,但不是最好的,缺点如下: 逻辑复杂,业务耦合,我们把校验数据并存库和发送邮件写到一个

  • Go基础教程系列之defer、panic和recover详解

    defer关键字 defer关键字可以让函数或语句延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束.即便已经panic().即便函数已经return了,也都会执行defer所推迟的对象. 其实defer的本质是,当在某个函数中使用了defer关键字,则创建一个独立的defer栈帧,并将该defer语句压入栈中,同时将其使用的相关变量也拷贝到该栈帧中(显然是按值拷贝的).因为栈是LIFO方式,所以先压栈的后执行.因为是独立的栈帧,所以即使调用者函数已经返回或报错,也一样能在它

  • Paddle模型性能分析工具Profiler定位瓶颈点优化程序详解

    目录 Paddle模型性能分析Profiler 1.使用Profiler工具调试程序性能 1.1 使用cifar10数据集卷积神经网络进行图像分类 1.1.1 获取性能调试前模型正常运行的ips 1.1.2. 开启性能分析器,定位性能瓶颈点 1.1.3. 优化程序,检查优化效果 1.1.5 结果展示 2 统计表单展示 Paddle模型性能分析Profiler 定位性能瓶颈点优化程序提升性能 Paddle Profiler是飞桨框架自带的低开销性能分析器,可以对模型运行过程的性能数据进行收集.统计

  • 微信 小程序前端源码详解及实例分析

    微信小程序前端源码逻辑和工作流 看完微信小程序的前端代码真的让我热血沸腾啊,代码逻辑和设计一目了然,没有多余的东西,真的是大道至简. 废话不多说,直接分析前端代码.个人观点,难免有疏漏,仅供参考. 文件基本结构: 先看入口app.js,app(obj)注册一个小程序.接受一个 object 参数,其指定小程序的生命周期函数等.其他文件可以通过全局方法getApp()获取app实例,进而直接调用它的属性或方法,例如(getApp().globalData) //app.js App({ onLau

随机推荐