EMACS-DOCUMENT

=============>随便,谢谢

让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 就行了。

https://nickdrozd.github.iotetris-normal.png

在文本编辑器讨论中,Emacs倡导者经常提到这一点。“没错,但是那个编辑器能运行俄罗斯方块吗?” 我很好奇,这会让大家相信Emacs更优秀吗?比如,为什么有人会关心他们是否可以在文本编辑器中玩游戏呢?“是的,但是那台吸尘器能播放mp3吗?”

有人说,俄罗斯方块总是很有趣的。像Emacs中的所有东西一样,它的源代码是开放的,易于检查和修改,因此 我们可以使它变得更加有趣. 所谓更多的乐趣,我意思是更难。

让游戏变得更困难的一个最简单的方法就是“不要下一个块预览”。你无法再在知道下一个块会填满空间的情况下有意地将S/Z块放在一个危险的位置——你必须碰碰运气,希望出现最好的情况。 下面是没有预览的情况(如你所见,没有预览,我做出的某些选择带来了“可怕的后果”):

tetris-no-preview.png

预览框由一个名为 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 的行为,而且它可以存储在配置文件中,与实际的俄罗斯方块代码分离。

去掉预览框是一个简单的改变。一个更激烈的变化是, 让块随机停止在空中:

https://nickdrozd.github.iotetris-air.png

本图中,红色的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

该文件中列出的分数默认最多为五位数,因此这个分数看起来不是很好。

给读者的练习

  1. 使用advice修改Emacs俄罗斯方块,使得每当方块下移动时就闪烁显示讯息“OH SHIT”。消息的大小与块堆的高度成比例(当没有块时,消息应该很小的或不存在的,当最高块接近天花板时,消息应该很大)。
  2. 在这里给出的 tetris-test-shape-random 版本中,每隔七格就有一个半空中停止。一个玩家有可能能计算出时间间隔,并利用它来获得优势。修改它,使间隔随机在一些合理的范围内(例如,每5到10格)。
  3. 另一个对使用Tetris使用advise的场景,你可以试试 autotetris-mode
  4. 想出一个有趣的方法来打乱块的旋转机制,然后使用advice来实现它。
  5. Emacs只有一个大的全局命名空间,因此函数和变量名通常以它们的包名作为前缀,以避免冲突。
  6. 很多人会告诉你,你不应该使用现有的名称空间前缀,你应该为你自己定义的任何东西保留一个名称空间前缀,例如 my/tetris-preview-next-shape. 但这样子很丑陋,而且通常是没有意义的,所以我不这样做。