让Emacs俄罗斯方块变得更难的一些Advice
Did you know that Emacs comes bundled with an implementation of Tetris? Just hit M-x tetris
and there it is:
你知道吗, Emacs 与 俄罗斯方块 的实现捆绑在一起了? 只需要输入 M-x tetris
就行了。
在文本编辑器讨论中,Emacs倡导者经常提到这一点。“没错,但是那个编辑器能运行俄罗斯方块吗?” 我很好奇,这会让大家相信Emacs更优秀吗?比如,为什么有人会关心他们是否可以在文本编辑器中玩游戏呢?“是的,但是那台吸尘器能播放mp3吗?”
有人说,俄罗斯方块总是很有趣的。像Emacs中的所有东西一样,它的源代码是开放的,易于检查和修改,因此 我们可以使它变得更加有趣. 所谓更多的乐趣,我意思是更难。
让游戏变得更困难的一个最简单的方法就是“不要下一个块预览”。你无法再在知道下一个块会填满空间的情况下有意地将S/Z块放在一个危险的位置——你必须碰碰运气,希望出现最好的情况。 下面是没有预览的情况(如你所见,没有预览,我做出的某些选择带来了“可怕的后果”):
预览框由一个名为 tetris-draw-next-shape
的函数设置:
(defun tetris-draw-next-shape () (dotimes (x 4) (dotimes (y 4) (gamegrid-set-cell (+ tetris-next-x x) (+ tetris-next-y y) tetris-blank))) (dotimes (i 4) (let ((tetris-shape tetris-next-shape) (tetris-rot 0)) (gamegrid-set-cell (+ tetris-next-x (aref (tetris-get-shape-cell i) 0)) (+ tetris-next-y (aref (tetris-get-shape-cell i) 1)) tetris-shape))))
首先,我们引入一个标志,决定是否允许显示下一个预览块:
(defvar tetris-preview-next-shape nil "When non-nil, show the next block the preview box.")
现在的问题是,我们如何才能让 tetris-draw-next-shape
遵从这个标志?最明显的方法是重新定义它:
(defun tetris-draw-next-shape () (when tetris-preview-next-shape ;; existing tetris-draw-next-shape logic ))
但这不是理想的解决方案。同一个函数有两个定义,这很容易引起混淆,如果上游版本发生变化,我们必须维护修改后的定义。
一个更好的方法是使用 advice. Emacs的advice类似于 Python装饰器,但是更加灵活,因为advice可以从任何地方添加到函数中。这意味着我们可以修改函数而不影响原始的源文件。
有很多不同的方法使用Emacs advice(查看手册),但是这里我们只使用 advice-add
函数和 :around
标志。
advise函数将原始函数作为参数,原始函数可能执行也可能不执行。我们这里,我们让原始函数只有在预览标志是非空的情况下才能执行:
(defun tetris-maybe-draw-next-shape (tetris-draw-next-shape) (when tetris-preview-next-shape (funcall tetris-draw-next-shape))) (advice-add 'tetris-draw-next-shape :around #'tetris-maybe-draw-next-shape)
这段代码将修改 tetris-draw-next-shape
的行为,而且它可以存储在配置文件中,与实际的俄罗斯方块代码分离。
去掉预览框是一个简单的改变。一个更激烈的变化是, 让块随机停止在空中:
本图中,红色的I和绿色的T部分没有掉下来,它们被固定下来了。这会让游戏变得 及其难玩 ,但却很容易实现。
和前面一样,我们首先定义一个标志:
(defvar tetris-stop-midair t "If non-nil, pieces will sometimes stop in the air.")
目前, Emacs俄罗斯方块的工作方式 类似这样子: 活动部件有x和y坐标。在每个时钟滴答声中,y坐标递增(块向下移动一行),然后检查是否有与现存的块重叠。
如果检测到重叠,则将该块回退(其y坐标递减)并设置该活动块到位。为了让一个块在半空中停下来,我们所要做的就是破解检测函数 tetris-test-shape
.
这个函数内部做什么并不重要 —— 重要的是它是一个返回布尔值的无参数函数。我们需要它在正常情况下返回布尔值true(否则我们将出现奇怪的重叠情况),但在其他时候也需要它返回true。我相信有很多方法可以做到这一点,以下是我的方法的:
(defun tetris-test-shape-random (tetris-test-shape) (or (and tetris-stop-midair ;; Don't stop on the first shape. (< 1 tetris-n-shapes ) ;; Stop every INTERVAL pieces. (let ((interval 7)) (zerop (mod tetris-n-shapes interval))) ;; Don't stop too early (it makes the game unplayable). (let ((upper-limit 8)) (< upper-limit tetris-pos-y)) ;; Don't stop at the same place every time. (zerop (mod (random 7) 10))) (funcall tetris-test-shape))) (advice-add 'tetris-test-shape :around #'tetris-test-shape-random)
这里的硬编码参数使游戏变得更困难,但仍然可玩。当时我在飞机上喝醉了,所以它们可能需要进一步调整。
顺便说一下,根据我的 tetris-scores
文件,我的 最高分 是
01389 Wed Dec 5 15:32:19 2018
该文件中列出的分数默认最多为五位数,因此这个分数看起来不是很好。
给读者的练习
- 使用advice修改Emacs俄罗斯方块,使得每当方块下移动时就闪烁显示讯息“OH SHIT”。消息的大小与块堆的高度成比例(当没有块时,消息应该很小的或不存在的,当最高块接近天花板时,消息应该很大)。
- 在这里给出的
tetris-test-shape-random
版本中,每隔七格就有一个半空中停止。一个玩家有可能能计算出时间间隔,并利用它来获得优势。修改它,使间隔随机在一些合理的范围内(例如,每5到10格)。 - 另一个对使用Tetris使用advise的场景,你可以试试
autotetris-mode
。 - 想出一个有趣的方法来打乱块的旋转机制,然后使用advice来实现它。
- Emacs只有一个大的全局命名空间,因此函数和变量名通常以它们的包名作为前缀,以避免冲突。
- 很多人会告诉你,你不应该使用现有的名称空间前缀,你应该为你自己定义的任何东西保留一个名称空间前缀,例如
my/tetris-preview-next-shape
. 但这样子很丑陋,而且通常是没有意义的,所以我不这样做。