你知道嗎,Emacs 綑綁了一個俄羅斯方塊的實現?只需要輸入 M-x tetris
就行了。
在對文字編輯器的討論中,Emacs 鼓吹者經常提到這一點。“沒錯,但是你那個編輯器能執行俄羅斯方塊嗎?”我很好奇,這會讓大家相信 Emacs 更優秀嗎?比如,為什麼有人會關心他們是否可以在文字編輯器中玩遊戲呢?“沒錯,但是你那台吸塵器能播放 mp3 嗎?”
有人說,俄羅斯方塊總是很有趣的。像 Emacs 中的所有東西一樣,它的原始碼是開放的,易於檢查和修改,因此 我們可以使它變得更加有趣。所謂更加有趣,我的意思是更難。
讓遊戲變得更難的一個最簡單的方法就是“隱藏下一個塊預覽”。你無法在知道下一個塊會填滿空間的情況下有意地將 S/Z 塊放在一個危險的位置——你必須碰碰運氣,希望出現最好的情況。下面是沒有預覽的情況(如你所見,沒有預覽,我做出的某些選擇帶來了“可怕的後果”):
預覽框由一個名為 tetris-draw-next-shape
1 的函數設定:
(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))))
首先,我們引入一個標誌,決定是否允許顯示下一個預覽塊 2:
(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
標誌。advice 函數將原始函數作為引數,原始函數可能執行也可能不執行。我們這裡,我們讓原始函數只有在預覽標誌是非空的情況下才能執行:
(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
該檔案中列出的分數預設最多為五位數,因此這個分數看起來不是很好。
tetris-test-shape-random
版本中,每隔七格就有一個半空中停止。一個玩家有可能能計算出時間間隔,並利用它來獲得優勢。修改它,使間隔隨機在一些合理的範圍內(例如,每 5 到 10 格)。Emacs 只有一個巨大的全域性名稱空間,因此函數和變數名一般以包名做字首以避免衝突。 ?
很多人會說你不應該使用已有的名稱空間字首而且應該將自己定義的所有東西都放在一個預留的名稱空間中,比如像這樣 my/tetris-preview-next-shape
,然而這樣很難看而且沒什麼意義,因此我不會這麼乾。 ?