相信大家都玩过卡牌游戏 UNO,由于 FYP (Final Year Project) 的缘故,前段时间用 python 撸了一个非常粗糙的 UNO 出来。有多粗糙呢?大概就是没有 UI, 所有操作都靠命令行,并且时常一瞬间弹出一堆日志吧。。连我一个整天面对着命令行面对各种日志的程序员都觉得看得难受。写完初版一直想找个时间总结一下,最后终于决定在今天把这篇想要写了很久的博客给写出来了,希望能对大家有帮助。

(附一张游戏截图)

接下来大概会总结一些我实现的时候一些比较有感触的地方,勉强可以算作是做单人(或者很少人的)小项目的心得吧。其实都是一些非常老生常谈的原则,只是我在实践中有了更深的体会,并且决定把我的思考过程分享出来,给有需要的同学提供一些这些抽象的原则对应的一些具体的例子。

不要怂,就是干

最鸡汤,最没有技术含量的一条放在最前面哈哈哈。其实这一条想说的是,对于这样的小项目,不必太拘泥于别人已有的成果,大胆尝试自己重新设计,重新写,不要怀疑自己做不到。固然每个人的经验能力有限,但挑战独立完成一个小项目(可以少量借鉴别人的实现)对于我等萌新来说还是一个很必要的历练;做到了当然成就感爆棚,即使做不到,也是能收获不少经验的。下面讲一讲我从最初在 github 上查找别人的实现,到最终决定自己写的心路历程吧。

当时的第一步也是在 github 上搜索别人的实现,并且最初也是想着在这些代码的基础上改的。但是浏览了一下搜索结果里靠前的 repo, 发现也不是那么合适。首先之所以要写这个 UNO, 是因为 FYP 要做的是强化学习,因此一个理想的实现要满足两个要求:1)方便自定义游戏(游戏人数,AI 玩家的策略),快速地自动化地模拟游戏,还有保存游戏数据;2)方便交互:这里的交互指的是强化学习中的 agent 可以一边进行游戏,一边更新 policy (还有 value), 然后一遍采取新的 action 返回给游戏系统。当时浏览了几个 repo 后发现都不满足这些要求:有的可能有好看的 UI, 但是游戏模式(人数等)是固定的,或者难以和一个强化学习系统对接,等等。于是我就决定自己写了。这里也有一个心得:项目之初要尽可能搞清楚具体的需求,需要写出一个怎样的实现,进而决定是否沿用已有的开源实现,以及后续怎么设计等等。

至此我和 partner 也还是打算沿用 github 上一些 repo 的设计的,但后来经过反复的思考还是决定推倒自己写。一方面是很想挑战一下独立完成一个小项目,另一方面也是觉得这些 repo 有些设计得不那么好的地方,这个后面会再详细讲。

设计:拆分独立的模块(Bottom-up)

其实模块化和 bottom-up 是非常老生常谈的东西了,基本上所有的软件工程课都必定会强调。但是真正设计以及实现的时候,把这些做好却并不是一件容易的事情,往往需要经过大量的长时间的思考,才能抽象出好的架构。

我个人喜欢(并且我认为这样比较合理)把模块拆分,直到每个模块只负责一种功能。这么说可能太抽象了,举个例子吧,在这里我假设大家都知道 UNO 的规则:在我最初看的那些 repo 里,游戏的主要逻辑都是放在一个 server(或者 game 之类的)类里面实现的。这里的重点是“一个”。也就是说,当一个玩家出了一张牌之后,现在场上的颜色,数字,玩家轮转的方向,+2 / +4 的惩罚,牌堆的变化等等,都是由一个类来处理的;而且基本上都是在这一个类的一个函数的一个 while loop 里,没有再往下拆分成别的函数了。我当时就觉得,这样这个类要承担的功能太多了,逻辑很多,容易写错,读起来也混乱(当然也可能是我太菜了,一下写不来也看不来这么多逻辑),我不想这样写。

后来我思考了挺久,最后把这些逻辑拆分成三个模块来处理:

  • 一个处理当前颜色,数字等卡牌状态的模块,负责接受玩家的 action, 改变状态,暂且称之为 StateController
  • 一个处理游戏进程的模块,比如改变顺时针/逆时针方向,以及检查游戏是否结束,暂且称之为 FlowController
  • 一个处理牌堆,给玩家发牌,洗牌等,暂且称之为 DeckController

然后在这三个模块之上有一个高级的模块,称之为 ActionController,负责协调一盘游戏内这三个模块的工作。ActionController 之上有一个更高级的模块,称之为 Game, 负责一些初始化(比如最开始询问玩家游戏的模式等),还有多盘的游戏的进行(每一盘对应一个 ActionController)。

这样的话我们就可以分开写不同的逻辑了,比如当一个玩家出了一张牌之后,写卡牌进入弃牌堆我们只写 DeckController, 写场上颜色的变化我们就只写 StateController,写方向的变化我们就只写 FlowController,;对于 ActionController, 我们也只要写好核心的逻辑,剩下的细节去调用这三个模块,就好了。这样代码一下子变得好管理了很多。

这里我的心得就是:把所有逻辑拆成多个小逻辑,小模块,以避免在一个模块里写入大量的细节;可以拆分出一些底层的模块,负责具体的逻辑,然后高层模块只需要调用这些底层模块就好了(跟实习的感受很像,我们负责写代码,老板负责管理我们,不管技术细节 2333)

然后提供一些我思考的时候会用到的捷径:

  1. 遵循 MVC 原则, 先考虑好有哪些 model , 比如这里可能是 Card, Player 和 State ,这些模块只管储存数据,至于所存的数据什么时候改变,这些逻辑不用管
  2. 对应每个 model 构建一个底层 controller, 这里可能就是 DeckController 对 Card, FlowController 对 Player, StateController 对 State,这些 controller 就负责逻辑,决定在什么条件下,以何种方式改变 model 中的数据;当然多数时候 C 和底层 M 都不是严格对应的,只是一个构思的捷径
  3. 底层 controller 之上再构建高层 controller. View 这里就暂时不管了,毕竟...这个项目中目前也还没有做 V, V 也不是这个项目的重点

这是设计时的一个思考方向:从底层模块再到高层模块

设计:专注核心逻辑(Top-down)

有 bottom-up 的地方就会有 top-down, 这里也不例外。

刚接手一个项目的时候,像我一样的萌新们往往都会很懵逼,不知道该从何入手,这个时候专注于核心逻辑是一个绝佳的思考角度,就像我们写文章前会先列大纲,而后再一点点填充一样。

我最初构思的时候花了很多时间考虑细节,要划分哪些模块,模块之间什么时候会相互调用,但是收效甚微,始终没有啥好的想法。后来决定换个角度,转而思考 UNO 这个游戏每一局的核心逻辑,最后也是思考了挺久才想出来的。用文字表述就是:

  1. 首先初始化;
  2. 直到游戏结束,重复一下过程:
    1. 移动到下一个玩家,检查这个玩家是否有可以出的牌,并且是否想出

      1. 如果有且想出:询问玩家要出的牌,然后根据这个牌改变游戏的状态
      2. 如果没有或者不想出:执行摸牌等逻辑

用 pseudocode 表述就是:

game.init()
while not game.over():  # 后来由 FlowController 负责player = game.next_player()  # 后来由 FlowController 负责if player.can_play() and player.want_to_play():action = player.get_action()player.play_action(action)game.accept_action(action)  # 三个 controller 都要负责,由 ActionController 统一调度else:game.apply_no_play_logic(player)  # 后来由 StateController 和 DeckController 负责, 由 ActionController 统一调度

从这里大家可以看到我最初概括出的核心逻辑,以及这些核心逻辑是怎样演变成底层 controller 和高层 controller 的。这里我的心得就是:最初设计的时候可以不用那么在意细节,先概括出问题的核心逻辑,然后从这个核心逻辑里出发,思考每一步应该由什么模块来负责,这样可以避免漫无目的地乱想。有一点很重要的是:这个核心逻辑是要你反复验证正确的,就是如果每一步执行正确,最终产生的结果也是正确的;也没有没考虑到的情况。所以这个过程也不是一蹴而就,是需要反复地验证与修改的。

实现:从简单的开始

也是很老生常谈的方法论。需要注意的是:这里的简单,可以时指不依赖其他部分,不会调用到其他的模块,或者说,即使会调用到,初期实现的时候也完全可以用更简单的东西替代掉。这么说有些抽象,举两个例子,是我实现的时候最早写的两个部分:

  • 从抽牌堆抽牌,牌用完进入弃牌堆:虽说要用到 Card, 但是完全可以用其他东西代替 Card, 比如数字。这样一来就很简单了,分别用两个 list 表示,一个每次 pop 出第一个元素 (抽牌),一个每次在后面 append 一个新元素(弃牌)。然后测试用数字来代替 Card 就好了。并且写到这,你会开始思考 Card 要怎么定义
  • 玩家轮转:一样,虽然要用到 Player, 但是用数字代替 Player 即可。这样首先要维护一个头尾相连的双向链表来表示玩家的圈圈;然后维护一个 boolean 表示顺时针与否;然后写一个函数可以根据这个 boolean 改变当前的玩家,等等。并且写到这,你会开始思考 Player 要怎么定义

接着就可以开始写 Card 和 Player 了,然后就可以写会用到 Card 和 Player 的模块了,以此类推,就像......算法中的 BFS。这里其实 Card 和 Player 看起来是 dependency 最少的模块,其实不然,因为他们作为 class, 有很多可能需要的 attribute 因为我们还没有用到,不知道该怎么定义,如果一开始就写这两个类,可能不如放在之后写好

一些 Tips

最后分享一些我觉得好用的细节上的东西,都是和 python 相关的,希望对大家有帮助

  • Enum class, 配上 unique 装饰器,定义 CardColor 什么的简直方便得不行,比单纯用数字或者字符串好得多
  • PyCharm: IDE 最爱用 JetBrain 一家,没有之一。我常用的功能有:从(变量或函数的)引用跳到定义,或者定义跳到引用;返回光标上一个停留的位置;git 相关功能(从 UI 选择文件 commit, 查看历史等);local change 相关功能(强烈推荐这个功能,有时候 rm 误删了文件怎么办?local history 回滚!);重构;断点调试(python debug 神器!)等。不用 PyCharm 我的效率起码减半(捂脸)
  • assertion: 常加 assert 是个好习惯,一方面代码可读性更高,另一方面 assert 配合能推断类型的 IDE,比如 PyCharm, 简直是神器,IDE 可以补全至少多一倍的代码

总结

说了这么多 bottom-up, top-down 之类的原则,其实个人认为,这些原则都是内化的。我开始做这个项目的时候,也不会想:现在要 bottom-up 了,现在要 top-down 了;而是在经过了一个思考过程之后,回过来看,才发现:“咦?这不就是 Software Engineering 课上学的 bottom-up 吗?这不就是 top-down 吗?” 所以我想,这些原则即使听了无数遍,没有经过实践,也始终只是几个名词而已,只有通过实践才会对这些名词有更深刻的理解,从而内化成自己的思考习惯,自己的一套方法论。程序员的路上没有什么捷径,写代码和思考才是硬道理。

最后附上 Github 地址(目前代码都在 playground/Janzen 里,所以就不贴主目录的链接了;已经有 v1 和重构过后的 v2),欢迎大家加星:https://github.com/JanzenLiu/uno-dev/tree/master/playground/Janzen

UNO 游戏实现心得 (version 1)相关推荐

  1. 一个菜鸟程序员的游戏开发心得

    对于一个只在大学期间学习了JavaScript,实习期间学习了点HTML.CSS的菜鸟,做游戏对我而言是一件想都不敢想的事情,后有幸被公司安排参与一款HTML5游戏的开发,才使我真正第一次与游戏来了次 ...

  2. 游戏开发心得——书籍篇——《游戏引擎框架》-导论

    游戏开发心得--书籍篇--<游戏引擎框架>-导论 FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: 学习<游戏引擎框架> ...

  3. 微信h5互动小游戏制作心得

    随着近几年H5游戏营销的盛行,高质量高规格的精品H5游戏层出不穷,各大商家们也是动用不少人力财力力争打造出专属的H5营销游戏,到底如何才能打造出精品的H5互动营销小游戏呢? 杜绝换皮,至少做到微创新 ...

  4. (心得三)java俄罗斯方块小游戏编写心得

    在"java俄罗斯方块小游戏编写心得02"中,我们已经做到了方块的各种功能的实现,那么接下来我们需要一个destroyLine()方法来对方块进行消除.我们需要想一个问题,当一个方 ...

  5. 幼儿园计算机培训家长心得体会,幼儿游戏培训心得体会5

    作为一名新的老师参加了幼儿园的游戏课程角色培训,在幼儿的美术游戏活动中重结果更重过程,我们教师应给幼儿创设宽松愉快的氛围,开展具有游戏倾向的美术活动,这样才能调动幼儿的积极性.下面是小编带来的有关幼儿 ...

  6. (心得二)java俄罗斯方块小游戏编写心得

    在"java俄罗斯方块小游戏编写心得01"中已经将游戏界面完成,那么接下来就是要让方块动起来. 我们之前在写Cell类时,曾经给每个最基本小方块定义了三个方法,即左移.右移.下落, ...

  7. 微信小游戏设计心得(一)从0到开发一款小游戏教程-岩浆救援,对初学者来说有帮助

    2019年底,因为其他工作失意,我意外来到了,小游戏设计圈子,独立开发游戏,从什么都不会,到发开了一款真正的小游戏 网上很多教程,但是没有一个全面的,都是零零散散的东西,拼凑一起,需要很多时间,我决定 ...

  8. 游戏开发心得——书籍篇——《游戏引擎框架》-专业工具

    游戏开发心得--书籍篇--<游戏引擎框架>-专业工具 FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: 学习<游戏引擎框架&g ...

  9. 游戏开发心得——资料篇——游戏行业的详细职位介绍与定义分析

    游戏开发心得--资料篇--游戏行业的详细职位介绍与分析(不定期更新) FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: 本篇主要介绍一下博主所了 ...

最新文章

  1. DataTable筛选符合条件的DataRow
  2. SAP ABAP二分查找(binary search)实际问题的深度分析
  3. Linux管道用法示例
  4. C#深入解析数据类型
  5. 在Windows下快速搭建SVN服务器 VisualSVN
  6. android定时切换活动,安卓手机如何定时自动切换情景模式 (全文)
  7. 双11拼团最后的福利!老用户如何解散团并加入新团!
  8. CCF201709-2 公共钥匙盒(100分)【模拟+优先队列】
  9. 分页内存与非分页内存导致的蓝屏死机问题
  10. 抓包工具之fiddler
  11. 忘记linux密码怎么办?
  12. [病毒木马] 什么是LSP劫持
  13. 小程序获取视频缩略图
  14. 【总结】有三AI所有原创人脸相关的学习资料汇总(2022年12月)
  15. “1只老母鸡”估值180亿!老乡鸡发展20年,都做对了什么?
  16. wpa_supplicant, wpa_cli用法
  17. P1796 汤姆斯的天堂梦
  18. 摄影师拍蜗牛乘树叶花下避雨 似撑小伞泛舟
  19. Android Others部分内存泄漏 OOM分析
  20. 机器学习_深度学习毕设题目汇总——人脸A

热门文章

  1. 金融交易游戏与理财投资
  2. 2022危险化学品经营单位安全管理人员操作证考试题及答案
  3. 爬虫取中间文本_小小爬虫批量抓取微信推文里的图片
  4. BERT在美团搜索核心排序的探索和实践
  5. 最小二乘法(实例详解)
  6. pythonGUI学习——视频解析案例
  7. github的项目转移到coding包含所有提交历史记录
  8. Boxcryptor免费加密你的云盘网盘V2021云储存文件加密工具,支持坚果云/Dropbox等
  9. 作者面对面 | 95 后工程师 zhisheng 的 Flink 之路
  10. 首届杰出数据库工程师终选名单公布