上个星期赖斯大学的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 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的实现使用终端作为输出。它不像Gabriele Cirulli版本的JavaScript前端一样令人印象深刻,但它是可维护的,如下两个屏幕截图展示:
总体来讲,我对于这个原型还是很满意的。当然有几个可能的改进。一个分数跟踪器的添加将是微不足道的,虽然一个 GUI 将是一个更加耗时的努力。如果有立即响应键盘输入的程序,我会觉得这个很有趣。当前,每个通过 WASD的输入 需要点击回车键进行确认。如果只按一个键将触发程序执行的下一步,那么游戏玩法会加快很多。在研究这一问题时,我没有找到任何快速的解决办法。尽管Haskell库NCurses包含键盘事件。我可能会深入探究一下,如果我用ASCII 图形进行编程使之成为一个“独立”游戏。
如果你觉得这篇文章有趣,请随意看看我的 2048的 Haskell 实现的源代码。
英文原文:Implementing the game 2048 in less than 90 lines of Haskell
译文出自:http://www.oschina.net/translate/2048-in-90-lines-haskell