用 90 行 Haskell 代碼實現 2048 游戲
上個星期賴斯大學的MOOC 計算的規則 公開課在 Coursera 上開講啦. 從第一周的材料來看,看起來它有了他們之前的課程 Python中的交互式編程介紹 所有優良的東西: 演示文稿做的很不錯,也有大量的支持可用, 而布置的作業也很有趣. 第一個作業就是編寫 2048 游戲的邏輯.
鑒于其設計中的根本性缺陷,我并不認為2048特別的有趣. 首先,你并不能在某個地方取得游戲的勝利. 其次,最有希望的游戲策略使得其玩起來相當的繁瑣,而且最大的樂趣并不是自己的游戲技能而是隨機數生成器制造的幸運連勝. 就我個人而言,更愿意選擇那種有時被稱為“理論完美”的游戲, 比如,游戲的一個屬性使得玩它的人能夠取得一個確定的勝利. 而2048的游戲結果卻沒有吸引到我,不過我也明白為什么會有人喜歡讓瓷磚四處滑動起來.
為游戲的邏輯編寫代碼是相當直接的。歸因于使用Python作為教學語言的計算原則課程, 對于在我的最初版本中的一個錯誤是由于python發生了改變,我不會感到奇怪. 我想著用Haskell寫這個東西可能會更有趣, 隨后就著手開始用這個語言編寫了2048的一個完整實現, 包括 I/O 處理. 整個代碼可以在 我的git賬號 上找到. 最終結果證明,更加完整的Haskell方案所需要的代碼比使用Python的程序邏輯要少幾行.
作為說明,如果你到這個頁面來只是為了找尋計算規則這門課程的Python作業的解決方案,那你就是在浪費時間. Haskell的實現和Python的實現很不同,使用的編程語言構造也不能在Python上用. 換言之,如果你正糾結這個作業,Haskell的源代碼將不會對你有所幫助.
在這篇文章中,我僅想著重強調游戲邏輯的核心部分,因為它很好地顯示了函數式編程的力量。首先,我定義一個數據類型,用于展示網格中的數字移動的方向,還有一個用于存放整數列表的列表的類型同義詞,用來提高類型特征的可讀性。從函數‘move’的命名可以明顯看出函數的作用;再下一步,將輸入作為一個網格的數字和移動方向,并產生新的網格。
data Move = Up | Down | Left | Right type Grid = [[Int]]
2048這個游戲是在一個4x4的棋盤上進行的。開始位置在我的實現中是固定的:
start :: Grid start = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 2], [0, 0, 0, 2]]
棋盤上可以在4個方向上對數字進行移動,意味著所有的數字的移動都會向著一個指定的方向,如果是2個數字,移動相同的方向,以彼此相臨而告終,則他們合并到一起。舉例來說,在如下所示的起始位置,移動方向為‘Up’,結果棋盤變成了下面所示:
[[0, 0, 0, 4], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
如果網格中的起始位置移動方向為向右,則不會有任何變化。如果網格變化了,則一個新的數字會在任何空的格子中產生,這個數字可能是2或者4.
我們看這種方法,問題在于其如何更有效的建模。在網格中的任何行列,都可被理解為一個列表。行和列表之間的關系是簡單明了的。列將不得不提取、 修改,或雖然再,插入。或者他們不需要?
我寫了一個函數來合并一行或一列,表示為一個列表。首先,所有的0要被移動,然后該列表將被處理,合并相鄰元素,如果它們包含相同的數字,接著如果必要的話,為結果中填充0.
merge :: [Int] -> [Int] merge xs = merged ++ padding where padding = replicate (length xs - length merged) 0 merged = combine $ filter (/= 0) xs combine (x:y:xs) | x == y = x * 2 : combine xs | otherwise = x : combine (y:xs) combine x = x
當棋盤中的移動方心為左時,這個合并函數可以立刻被應用。其他方向的移動,然而,需要進行一些考慮,如果希望代碼保持簡潔。向右移動網格是通過采取反轉它之前將它提交給函數merge的每一行完成的,然后再次反轉結果:
move :: Grid -> Move -> Grid move grid Left = map merge grid move grid Right = map (reverse . merge . reverse) grid move grid Up = transpose $ move (transpose grid) Left move grid Down = transpose $ move (transpose grid) Right
對于網格向上或者向下移動,如果你想提取出一列,對其應用合并函數,然后產生新的網格進行列的插入,這是極其痛苦的。相反,雖然一點點的線性代數知識,卻導致一個更優雅的解決方案。如果你不能立即明確如何移調導致所期望的結果,請看看下面的插圖。
input transpose move transposeUp: 0 0 0 2 2 0 2 2 2 2 0 2 2 0 0 0
Down: 2 2 2 0 0 2 0 0 0 0 2 0 0 2 2 2</pre>
我Haskell的實現使用終端作為輸出。它不像Gabriele Cirulli版本的JavaScript前端一樣令人印象深刻,但它是可維護的,如下兩個屏幕截圖展示:
總體來講,我對于這個原型還是很滿意的。當然有幾個可能的改進。一個分數跟蹤器的添加將是微不足道的,雖然一個 GUI 將是一個更加耗時的努力。如果有立即響應鍵盤輸入的程序,我會覺得這個很有趣。當前,每個通過 WASD的輸入 需要點擊回車鍵進行確認。如果只按一個鍵將觸發程序執行的下一步,那么游戲玩法會加快很多。在研究這一問題時,我沒有找到任何快速的解決辦法。盡管Haskell庫NCurses包含鍵盤事件。我可能會深入探究一下,如果我用ASCII 圖形進行編程使之成為一個“獨立”游戲。
如果你覺得這篇文章有趣,請隨意看看我的 2048的 Haskell 實現的源代碼。