游戏互动是淘宝内容化建设的重要一环,其实自研的淘宝斗地主满足了部分人群的简单娱乐需求。本文主要介绍了淘宝斗地主新推出的残局玩法从0到1是如何实现的,笔者针对游戏链路到设计方案和可能出现的问题做了比较细致的介绍,供大家参考交流。
项目介绍
当前互联网应用很重要的一点就是内容建设,通过内容吸引用户。对我们来说就是成为吸引用户每天点开淘宝的动力。期望用户能够每天点开淘宝看看——我现在没事,那打开淘宝领取一下淘金币,去淘宝人生看看,打个斗地主,玩个麻将,看看淘宝乐园的小游戏等等。
笔者本次拿斗地主举例,其作为棋牌游戏面向人群广泛,即时满足性强,两分钟一局,通勤、食堂、洗手间等碎片时间来一局,此类互动类内容建设为淘宝在用户增长和用户使用时长方面带来了一定的收益。
相信每个人都知道斗地主的玩法和规则,这里就不花笔墨叙述了,淘宝斗地主目前有经典玩法、炸弹房、实物大奖赛、好友对战等游戏模式,也形成了任务中心、钻石一分兑、道具商城的运营体系。但是游戏要想持续吸引住玩家,必须要不断推陈出新。
因此在经典玩法基础上我们需要探索出的新的玩法,就像9月份爆火的《羊了个羊》游戏一样,诸如此类的解密闯关类,我们做了斗地主的残局玩法,你可以想象和象棋的残局一样,双方能够看到对方的手牌,并且其中玩家方的胜利路线是唯一的。玩家从复杂的敌我手牌中,面对永不出错的AI,在重重险阻中找到获胜的途径,获得战胜AI的成就感。残局作为承接斗地主部分玩家的双十一活动,为玩家尤其是高玩提供了新颖的游戏体验,后期作为外投的“钩子”,期望吸引到更多感兴趣的玩家,拉新并转化。
残局目前作为活动投放,每天的关卡都是不同的,欢迎大家每天都来挑战~
项目分析
经典模式的斗地主分了以下几个阶段:
匹配->发牌->叫抢地主->加倍阶段->打牌阶段->结算
功能上分为几大模块:实时在线匹配,牌局推进与更新、托管与守护、消息的推拉结合。由于本文主要讲述的是残局,对于经典玩法是如何实现的就不展开了,主要想对比一下残局玩法和经典玩法的区别:
- 经典玩法中,玩家的出牌使用HTTP请求,而牌局的推送使用socket推送到三个玩家的端上,但是残局玩法实际上是一个用户与AI进行1对1的出牌场景,按斗地主的出牌规则进行出牌;
- 残局玩法是玩家与AI对战,不涉及到多方玩家的匹配;
- 同样的,由于是单人游戏,不涉及异步的请求或牌局的推送问题,因此不需要长链接,可以不使用socket去推送牌局消息,玩家每一次出牌使用HTTP请求服务端即可;
- 经典玩法中,托管的机器人使用的是通过发送延时消息来推进牌局的,残局没有托管和提示,对算法的请求也没必要采用消息的方式来推进;
- 残局没有叫抢地主、加倍等阶段,业务逻辑较经典玩法会简单一些,系统要保证的是牌局能够按预期推进,尤其是有时候会出现各种各样的超时或是依赖下游的抖动,不能够阻塞玩家。
业务思考
首先是定义我们的斗地主残局是什么,以及如何保证对局的挑战性和趣味性。
▐流程概述
- 触发活动,获得当日牌局
根据运营策略,AB、人群、来源等判断是否触发活动,再请求当日牌局信息。
牌局要求:玩家必有获胜机会,但是仅有一种出牌方式能获胜;要求生产的牌局一定要保证某一步是唯一解,既不能出现用户随便出都能赢AI,也不能出现无论怎么出都无法赢AI的牌局。
- 进入对局
进入对局后,只有玩家与AI对手两个角色,并且双方手牌都直接展示。
- 用户出牌
开局后直接进入出牌环节,玩家为先出的一方,对局过程中,玩家侧不设定倒计时,每一步没有超时时间,可无限等待下去。
- AI和用户轮流出牌
每当玩家出牌,AI方将有5S的倒计时出牌时间,AI的每次出牌会随机在1~5S间隔
- AI出牌策略
当玩家出牌并在“获胜”的路径上时,AI需要尽量确保后续的出牌也只有1种策略【AI不能认为要输了就摆烂】
当玩家出牌并非“获胜”路径时,AI需要使用合理的牌序策略将玩家击败【只要玩家出错就必败】
- 有一方出完则进入结算
直到双方有一方将手牌打光,即为牌局结束,AI出完用户失败,反之则胜利。
当用户击败AI后,可以获得相应关卡的奖励(一般是斗地主的道具或豆子钻石),吸引用户在其他对局中使用,进而拉长用户的游戏时长
- 牌局保留逻辑
玩家的牌局缓存会保存一定时长,中途退出后,牌局不会被自动托管,现场被保存下来,用户当天再次挑战会从上次结束地方继续,除非用户手动点击重开或本局进入到结算。
- 牌局选取与更新原则
要求用户短时间(30天)内不能遇到重复的牌局;
半夜十二点玩游戏用户很多,因此将每日关卡改到凌晨5点刷新
▐功能上的挑战
- 牌局自动生成:这种大量残局不可能由人工生产,需要训练生成,并且残局要求牌面实力相当,用户存在少量必赢路径(1~2条),尤其是大规模高质量残局生成难度大
- 用户请求当日关卡如何做到不重复,并且有一定容错能力
- 牌局信息存储的选型和可扩展设计,并尽量减少缓存占用
- 高度依赖稳定的AI出牌,尤其是较高的调用量要保证高可用高性能:a
- .高准确率:准确率100%;当用户走必赢路径时能合理出牌;当用户走错时AI能100%反杀;b
- .高性能:AI出牌低延迟;c.兜底能力:当AI不可用时,服务端要有兜底的能力,不能阻塞用户
- 设计时考虑投放和AB测试能力,以及加入多种降级和紧急下线的开关与兜底工作
- 数据表的设计,数据分析能力,监控报警,数据看板以及可灰度、可观测、可回滚的能力等等
技术方案及思考
▐技术模块拆解1、用户触发活动 2、获得今日关卡 3、用户和AI对局 4、结算和发奖
▐系统链路图▐技术模块拆解
- 牌局数据存储
一般的存储方式:
- 单机内存、分布式数据库、分布式缓存
由于牌局更新频繁,而且在线玩家越多,并发越大,对RT和性能就越敏感,而牌局的具体过程系统不需要详细记录下来,也不需要使用永久性的存储方式。综合考虑之后基于Redis的缓存是当前比较合适的方式。
残局场景涉及到两个缓存信息:
一、十局牌局信息同时也作为触发标志
存储包括挑战进度,十局残局对应的信息(gameId;用户手牌;AI手牌)
二、用户当日正在挑战局的牌局信息
- 用户触发活动逻辑
用户在进入斗地主主页的时候,会去校验该用户是否能够触发活动,如果在触发活动范围内,会判断疲劳度决定是否出现活动弹窗,引导用户进入残局玩法。下面展示的是当用户进入首页时,校验触发逻辑:
这里在设计的时候用了两个开关,一是总的活动开关,二是新用户触发开关;考虑当出问题的时候能够提供一键降级的能力,另外给运营提供了关闭新用户触发的入口。
- 进入游戏请求牌局
每一个用户每日十局挑战的关卡并不相同,用户点击开始挑战会进入当天的挑战进度中,此时会判断当前是否有未完成且是当日的牌局,如果有的话,直接恢复至玩家上次离开时的状态。如果发现没有一局缓存或此关卡已结束,那么利用十局缓存中的数据和进度信息初始化牌局,并写入到一局缓存中,用户开始本关卡挑战。
值得注意的是请求用户今日的十关挑战是放在每天第一次进入第一关挑战时,避免在前一步判断触发时调用算法而造成大量无用请求;为什么在这里统一请求,而不是每一关都请求算法:是为了能够分发不同的牌局策略,算法侧的同学会根据不同的策略筛选和分发牌局,例如A桶投放前5关简单,B桶投放奇数关简单等;并且会尽量将牌局打散以获得更多真实用户挑战记录,以供后期数据分析。
前文提到的如何让用户一个月内不会请求到同样的牌局,一开始想的解决方案是通过布隆过滤器实现,后来简化了一个方案,将所有牌局分成31个区,每个自然天请求不同分区内的牌局即可。
- 出牌和跳过
下方的框图展示了玩家的出牌逻辑,在调用算法接口获得AI出牌信息之前,会对用户出牌进行前置判断。由于斗地主涉及到多种异常信息,因此需要做很多对异常信息的判断和处理,哪一些异常需要透给用户,例如:牌型不符合要求(不是单张、对子、三带、顺子、炸弹等),是否能够压过上家牌,是否能跳过(对面不要时玩家不可跳过),出的牌是否是玩家手牌(防止直接刷接口) 等等。
在检验没有问题之后,会将玩家本轮出的牌、玩家剩余手牌、AI剩的牌组装,并封装请求,RPC请求算法获得AI此轮出牌,拿到AI出牌后更新牌局信息,再判断是否走到结算,如果此时有一方手牌已为空,则本关结算;否则更新牌局信息缓存,玩家继续下一轮次的出牌动作。
结算逻辑:
▐设计上的考虑
- 针对算法不稳定的backup和兜底方案,减少依赖
由于残局对算法侧依赖很大,如果请求算法出现问题,拿不到今日牌局,或者玩家出牌后拿不到AI出牌信息等,会导致牌局无法正常进行,阻塞玩家流程。实际联调时发现调用时候会出现算法侧返回报错或者RPC超时等问题的,另外也本着不信任依赖的原则,需要有兜底。面对这些问题,设计了以下解决方案:
一、配置监控报警,监控调用算法时长和成功率的指标,当出现抖动时及时报警
二、设计兜底方案:
- 第一层backup:将出牌的可能情况写在图数据库中,当没有拿到算法侧返回的AI出牌信息时,会构建查询key直接查图数据库,这样能够减少一部分查询算法失败直接走到兜底的情况第二层兜底:当出现数据库出问题或者查询不到的时候,此时系统的业务逻辑自己会做一层兜底,根据当前双方手牌和出牌信息出牌,但是这个策略是自己实现的,所以出的牌并不保证百分百符合机器人胜利原则。最终兜底:当前面都出问题的时候,选最简单的方式,如果此时玩家出牌了,那么机器人就不出牌,如果此时玩家跳过,此时机器人不能跳过,选择自己手牌第一张。【此时认为本局对玩家是送分局】
- 针对请求可能出现的多种异常做鲁棒设计
斗地主此类游戏场景和一般的请求有不同,要求端上渲染信息和服务端存储的信息必须一致,但是在交互过程中有可能出现的异常有:
端上网络问题:
- 前端请求服务端失败、
- 前端请求成功,服务端做出了响应但是网络问题前端没有拿到结果
服务端依赖的下游出现问题
- 牌局缓存更新失败:
- 牌局缓存更新超时返回错误,但是实际上已经更新成功
上面说的问题会有一个表现:用户出了一手牌之后,前端没有拿到这次请求的结果,或者拿到了包装的异常,此时本轮出牌渲染出现问题。
如果说此次请求服务端真的失败,其实倒也没什么问题,用户重新出牌就好了;但是麻烦的是服务端走了业务逻辑,更新了缓存,但是端上因为各种各样的原因没能正常渲染,此时玩家再出牌时候就会抛本张手牌已出的异常了,此时是会阻塞用户的。因此设计了以下解决方案:
- 参考CAS的思想,在用户每一次请求参数中加入order字段:a.这个order字段实际上就是牌局进行的轮次,当请求order和Redis中version不一致时,直接将最新的牌局缓存返回,端上重新渲染,玩家拿到最新牌局信息;b.在更新缓存的时候,会对比version字段,只有符合当前轮次时,才会更新缓存;c.当每一次更新缓存后,缓存的version会自动 1,将此version即作为order字段,不用手动维护。
- 对缓存操作考虑到多并发的情况,update时不会使用参数0强制更新,会带上version字段来更新。
- 前端做了状态锁防止连点,以及出现特定异常时直接拉取最新牌局的逻辑
- 两个缓存状态一致性问题
这里涉及了两个缓存,理论上这两个缓存要求数据一致性,但是Redis并没有传统说的事务特性,想要做到两者状态强一致性几乎是不可能的,因此笔者在设计上考虑以下方案保证玩家不会因为数据不一致而阻塞或损失权益:
- 使用了基于消息队列的重试方案,当更新缓存出现异常时,会发送MQ消息,利用消息重试的机制来保证缓存更新尽量成功;
- 配置监控报警,在进入对局时检查用户两者状态不一致会打错误日志
- 如果出现本局缓存更新失败的情况,实际上这时候已经向上抛出了异常,不会再更新十局缓存中的进度信息。此时在用户测的表现:最后一次出牌失败,那么重新出最后一手牌即可,会重新结算;
- 如果出现本局缓存更新成功,但是十局缓存中的对局进度更新失败情况,此时在用户侧表现是:本次结算失败,玩家手动重试,重新走到结算逻辑;
- 奖励的发放构造了幂等,保证了用户当天完成同一场对局奖励不会重复发放,不会发生资损的问题;
- 在数据库中记录每一局的状态,可以通过定时扫表方式做对账,检查是否有问题。
- 数据信息沉淀,衡量牌局难度和投放策略
当一局牌局结束的时候(结算或用户选择重开),会将本局玩家和AI出牌的记录、玩家挑战次数、本关挑战时长、进度等等信息打印到日志中,并通过日志同步到数据平台中,联合算法侧对不同玩家的发牌策略表join,来判断什么样的分发策略能够提升用户时长和留存,也用来分析不同的残局难度应该如何衡量,为之后固定关卡的牌局选择沉淀信息。
总结
残局玩法是淘宝斗地主上线的全新玩法,用户的状态流转和设计思路上倒并不多难,主要集中在业务逻辑上判断比较多,针对各种异常链路要有完整的思考理解。残局玩法上线后获得了不少用户的积极反馈,同时作为双十一活动承接了部分用户,对斗地主用户游戏时长有一定提升。我们会设计一些简单而巧妙的牌局,看起来很容易赢,但是怎么出都会被AI击败,往往需要某一妙手比如拆一个对子才能获胜。除此以外我们已经在迭代通过做任务获得胜利路径提示等功能,进一步提升玩法的完整性,希望能尽快够给大家呈上一个更完美的残局,更丝滑的斗地主。
作者:刘家成(悟学)
来源:大淘宝技术
出处:https://mp.weixin.qq.com/s/JRgXV-yakS5c71Dxoluaeg
,