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

   敬请关注微信共众号:红孩儿的游戏开发之路


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

                                                                   

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



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


       

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


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


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


        首先,在游戏服务器的房间类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()。

            

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

      

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




扫一扫,关注红孩儿共众号,每天获取更多房卡麻将技术文章

                                        

原创文章 197 获赞 526 访问量 140万+

猜你喜欢

转载自blog.csdn.net/honghaier/article/details/60572594