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





























