工作环境简介
win10,PyCharm,Python3.9,pygame
声明
本人只是一名Python爱好者,单纯记录一次实践经历,不喜勿喷!
1.背景
闲来无事想自己写个玩具玩玩,前两天抽空写了写。写的差不多能玩了,迫不及待试了试,津津有味的玩了十来分钟,突然感觉自己好像眼花了,没有在意。结果玩着玩着发现不是眼花了,是真的有奇怪的现象发生,自己都给逗笑了。重玩了几次,发现每次都是玩上好一会才开始出现这种现象,一时间还反应不上来问题所在。既然有这种现象,肯定是程序有bug,只能去排查了。
2.bug展示
录了视频并在bug出现前后截了图:
视频:
记一次俄罗斯方块玩具编写出现的有趣bug
以下几幅截图出现顺序是连续的:
======>
======>
======>
3.问题排查
结合bug出现的时机是在方块下落固定之后到新方块出现的这段短暂的时间段,因此问题代码肯定在处理这段逻辑的代码段中,完整代码段就不放了。
处理逻辑概括如下:
检测不能下落==>
调用更新方块方法==>更新方块过程包含:1.调用矩阵更新方法:更新游戏矩阵,即将当前固定的方块矩阵合并到整体游戏矩阵的对应位置内,只有两三行代码,常规的矩阵遍历和元素更新操作,没什么问题;2.调用行检测和消除方法:检查更新后的矩阵是否存在可以消除的行,在这个过程中会移除全“1”行,统计移除的全“1”行个数,根据移除的全“1”行个数在矩阵顶部添加相应的全“0”空白行,最后将处理结果更新游戏矩阵,然后计算消除行的得分;问题大概率所在。3.更新方块和方块预览队列:很简单的操作,从预览队列中出队一个方块作为新的出现的方块,然后随机创建一个方块对象补充到预览队列中,一个出队一个入队操作,不涉及游戏矩阵的变动,没有问题;4.检测新的方块是否会触发碰撞检测,发生碰撞检测则无法生成新的方块,也就意味着本局game over;也不涉及矩阵的修改,碰撞检测的逻辑也应该没问题,否则在游戏前期就应该出现bug。
综上所述,问题大概率出现在更新游戏矩阵的部分,也就是行检测和行消除方法,但是每次固定方块都涉及到行检测和消除,为什么游戏前期没问题呢,一时半会还想不通,但是我99%确定问题就在这个方法内。只有短短的十几行代码,而且每一行拿出来看都是极其简单的操作。试过输出矩阵日志,也试过断点调试,但是由于游戏大概运行10分钟左右才会偶尔出现这个问题,排查很不方便,所以多次排查都没有确定精确地问题在哪一行,也可能是我先入为主,忽略了真正的问题所在。
虽然花费了很多时间,甚至有点无奈,但是只要坚持肯定是有收获的,最终幡然醒悟,还是对问题有了察觉,主要原因还是当时写这段逻辑的时候有一个地方偷懒了,就是添加空白行的处理,代码行截图:
再看看修改后正确的处理:
看着还是一行简单的代码,但实际差距很大,前者是通过 * 运算直接批量生成列表元素,后者时严格按照列表推导式逐个生成列表元素。区别在哪呢,前者生成的结果实际上是对同一个列表元素的重复引用所达到的目的,后者则是完全独立的元素。想到这里,问题已经很明显了,不过为了验证推断,我分别在游戏中和纯代码复现进行了验证。
4.问题复现和验证
先说游戏验证,就是重新玩一局游戏,当第一次批量消除行时,也就是触发了两行及以上的消除时,记录一下消除的行数,因为这是顶部添加空白行的依据。游戏矩阵规模为20*10,这里以消除两行为例,如果推测正确,那么这时候生成的有问题的空白行时顶部的前两行,那么接下来我只要继续消除最多18行,这两行问题空白行就会沉到底部,也就会出现之前遇到的问题,于是我继续游戏,这里后来也想到了可以不消除直接在同一列往上叠加,直到高度达到那两行看看是否会出现之前的问题。按照我的推测,当方块高度来到那两行时,果然出现了之前的问题。所以已经基本实锤了,接下来我又从代码层面简单复现了一下。
这是为了复现问题编写的简单代码:
可以看到我先生成可一个20*10的矩阵,但是这个矩阵是有两部分组成,前5行是用错误的逻辑生成的全“0”行,后15行是正常逻辑生成的矩阵。然后调用修改方法分别修改前5行中的(2,3)坐标和后15行的(6,7)坐标位置,最后分别打印修改前后的矩阵状态 。这是运行后两次打印的输出截图:
可以看到和我推测的一样,由于前五行通过 * 运算生成,所以其实前五行是同一行的重复引用构成,后15行的元素是列表生成式生成的相互独立的元素。那么在对前5行任意行的某个元素修改时,其他行相同位置的元素也会相应被修改,而后15行则不会出现这种问题。所以当某一次消除多行后,当新添加的一批空白行下落到底部方块群高度时,一旦有方块改变了这些空白行的某一个单元格,这些空白行的同一列就都会同步改变,这也就是为什么有时有这种情况,而不是游戏一开始就出现。
到此,问题也就排查并完美解决了。
5.总结与感想
要记住一句话:不存在绝对完美的程序和编程语言。其实在日常编程中,我们很多时候也会犯一些低级错误,有时候可能是偷个小懒,有时候可能是在不经意间埋下的隐患,但最终都会在未来的某个时间给你当头一棒,瞬间蒙圈。
一些错误可能很快就排查出来,但是有时候往往看起来十分简单的代码反而不容易立即察觉,除去一些其他的因素,主观来说更多是由于我们已经有一种先入为主的思维,觉得有些我们认为过于简单的地方不可能出问题,这往往让我们一开始与问题擦肩而过,而走很多弯路,甚至随着排查的时间累积但问题得不到解决而逐渐暴躁,怀疑自己,甚至怀疑人生。这时候越要冷静的反思,回过头来再重新好好看看,也许就有一种蓦然回首,忽然眼前一亮的直觉。即使一时半会还是解决不了,也不要轻易气馁,因为走弯路也是在逐渐接近终点,总好过原地踏步,哪怕试错也总有试对的时候。
直到问题解决了,你就会明白,一些所谓的玄学,不过是目前无法解决的问题,一旦解决了也就只是一个自己曾经不经意创造的bug,而这个bug也不过是帮我们积累经验,帮我们更好的认识自己,认识世界。到这个时候,你就应该庆幸曾经的自己没有放弃,也要为现在的自己而鼓掌欢呼了!