如何讓Emacs俄羅斯方塊變得更難
你知道嗎,Emacs 捆綁了一個(gè)俄羅斯方塊的實(shí)現(xiàn)?只需要輸入 M-x tetris 就行了。
在對(duì)文本編輯器的討論中,Emacs 鼓吹者經(jīng)常提到這一點(diǎn)。“沒錯(cuò),但是你那個(gè)編輯器能運(yùn)行俄羅斯方塊嗎?”我很好奇,這會(huì)讓大家相信 Emacs 更優(yōu)秀嗎?比如,為什么有人會(huì)關(guān)心他們是否可以在文本編輯器中玩游戲呢?“沒錯(cuò),但是你那臺(tái)吸塵器能播放 mp3 嗎?”
有人說,俄羅斯方塊總是很有趣的。像 Emacs 中的所有東西一樣,它的源代碼是開放的,易于檢查和修改,因此 我們可以使它變得更加有趣。所謂更加有趣,我的意思是更難。
讓游戲變得更難的一個(gè)最簡(jiǎn)單的方法就是“隱藏下一個(gè)塊預(yù)覽”。你無(wú)法在知道下一個(gè)塊會(huì)填滿空間的情況下有意地將 S/Z 塊放在一個(gè)危險(xiǎn)的位置——你必須碰碰運(yùn)氣,希望出現(xiàn)最好的情況。下面是沒有預(yù)覽的情況(如你所見,沒有預(yù)覽,我做出的某些選擇帶來了“可怕的后果”):
預(yù)覽框由一個(gè)名為 tetris-draw-next-shape 1 的函數(shù)設(shè)置:
(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))))
首先,我們引入一個(gè)標(biāo)志,決定是否允許顯示下一個(gè)預(yù)覽塊 2:
(defvar tetris-preview-next-shape nil"When non-nil, show the next block the preview box.")
現(xiàn)在的問題是,我們?nèi)绾尾拍茏?tetris-draw-next-shape 遵從這個(gè)標(biāo)志?最明顯的方法是重新定義它:
(defun tetris-draw-next-shape ()(when tetris-preview-next-shape;; existing tetris-draw-next-shape logic))
但這不是理想的解決方案。同一個(gè)函數(shù)有兩個(gè)定義,這很容易引起混淆,如果上游版本發(fā)生變化,我們必須維護(hù)修改后的定義。
一個(gè)更好的方法是使用 advice。Emacs 的 advice 類似于 Python 裝飾器,但是更加靈活,因?yàn)?advice 可以從任何地方添加到函數(shù)中。這意味著我們可以修改函數(shù)而不影響原始的源文件。
有很多不同的方法使用 Emacs advice(查看手冊(cè)),但是這里我們只使用 advice-add 函數(shù)和 :around 標(biāo)志。advice 函數(shù)將原始函數(shù)作為參數(shù),原始函數(shù)可能執(zhí)行也可能不執(zhí)行。我們這里,我們讓原始函數(shù)只有在預(yù)覽標(biāo)志是非空的情況下才能執(zhí)行:
(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 的行為,而且它可以存儲(chǔ)在配置文件中,與實(shí)際的俄羅斯方塊代碼分離。
去掉預(yù)覽框是一個(gè)簡(jiǎn)單的改變。一個(gè)更激烈的變化是,讓塊隨機(jī)停止在空中:
本圖中,紅色的 I 和綠色的 T 部分沒有掉下來,它們被固定下來了。這會(huì)讓游戲變得 極其困難,但卻很容易實(shí)現(xiàn)。
和前面一樣,我們首先定義一個(gè)標(biāo)志:
(defvar tetris-stop-midair t"If non-nil, pieces will sometimes stop in the air.")
目前,Emacs 俄羅斯方塊的工作方式 類似這樣子:活動(dòng)部件有 x 和 y 坐標(biāo)。在每個(gè)時(shí)鐘滴答聲中,y 坐標(biāo)遞增(塊向下移動(dòng)一行),然后檢查是否有與現(xiàn)存的塊重疊。如果檢測(cè)到重疊,則將該塊回退(其 y 坐標(biāo)遞減)并設(shè)置該活動(dòng)塊到位。為了讓一個(gè)塊在半空中停下來,我們所要做的就是破解檢測(cè)函數(shù) tetris-test-shape。
這個(gè)函數(shù)內(nèi)部做什么并不重要 —— 重要的是它是一個(gè)返回布爾值的無(wú)參數(shù)函數(shù)。我們需要它在正常情況下返回布爾值 true(否則我們將出現(xiàn)奇怪的重疊情況),但在其他時(shí)候也需要它返回 true。我相信有很多方法可以做到這一點(diǎn),以下是我的方法的:
(defun tetris-test-shape-random (tetris-test-shape)(or (andtetris-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)
這里的硬編碼參數(shù)使游戲變得更困難,但仍然可玩。當(dāng)時(shí)我在飛機(jī)上喝醉了,所以它們可能需要進(jìn)一步調(diào)整。
順便說一下,根據(jù)我的 tetris-scores 文件,我的 最高分 是:
01389 Wed Dec 5 15:32:19 2018
該文件中列出的分?jǐn)?shù)默認(rèn)最多為五位數(shù),因此這個(gè)分?jǐn)?shù)看起來不是很好。
給讀者的練習(xí)
- 使用 advice 修改 Emacs 俄羅斯方塊,使得每當(dāng)方塊下移動(dòng)時(shí)就閃爍顯示訊息 “OH SHIT”。消息的大小與塊堆的高度成比例(當(dāng)沒有塊時(shí),消息應(yīng)該很小的或不存在的,當(dāng)最高塊接近天花板時(shí),消息應(yīng)該很大)。
- 在這里給出的
tetris-test-shape-random版本中,每隔七格就有一個(gè)半空中停止。一個(gè)玩家有可能能計(jì)算出時(shí)間間隔,并利用它來獲得優(yōu)勢(shì)。修改它,使間隔隨機(jī)在一些合理的范圍內(nèi)(例如,每 5 到 10 格)。 - 另一個(gè)對(duì)使用 Tetris 使用 advise 的場(chǎng)景,你可以試試 autotetris-mode。
- 想出一個(gè)有趣的方法來打亂塊的旋轉(zhuǎn)機(jī)制,然后使用 advice 來實(shí)現(xiàn)它。
-
Emacs 只有一個(gè)巨大的全局命名空間,因此函數(shù)和變量名一般以包名做前綴以避免沖突。 ↩
-
很多人會(huì)說你不應(yīng)該使用已有的命名空間前綴而且應(yīng)該將自己定義的所有東西都放在一個(gè)預(yù)留的命名空間中,比如像這樣
my/tetris-preview-next-shape,然而這樣很難看而且沒什么意義,因此我不會(huì)這么干。 ↩































