200行,写个2048游戏

最近断断续续地在学racket [1],同时也在把学习过程中的心得汇总成一本使用 scribble [2] 撰写的电子书 [3]。有几个读者看了之后,在公众号里不约而同地留言:

学这么一门小众的语言,除了了解下Lisp的能力(魔力)外,有什么实际的用途?

如果「实际的用途」是指用其找工作,那么的确没有,在可预见的未来(3-5年)也不太会有,您可以点左上角的返回按钮退出本文;但如果「实际的用途」指写点有意义的代码,而不是翻来覆去地写求阶乘的算法,快排的算法,那么可以继续。本文讲讲如何用racket写曾经风靡的2048游戏。

2048游戏的规则是这样:

1) 开始时棋盘上随机有两个棋子,2或4都有可能,其它为空

2) 玩家可以用方向键移动棋子。移动时所有棋子一起整体移动到用户按下的方向,直到不能移动为止

3) 在移动方向上,相邻的两个数字如果相同,则合并为一个,合并后的结果为两个数字之和(即乘以2)

4) 每移动一次,棋盘上空闲的位置会随机出现2或者4,出现2的几率(90%)要远大于4(10%)

5) 当棋子布满棋盘,四个方向移动时又无法进行合并,则游戏结束

我们知道,做这样一个小游戏,最核心的就是找到其内部状态的表示方式,然后将这种状态投射到漂亮的UI上。而2048的内部状态,最好的表述方式就是一个矩阵(0表示空闲的位置)。

'((0 2 4 8)
  (2 4 8 16)
  (4 4 0 16)
  (0 0 0 4))

矩阵是一个二维数组,也可以看作是数组的数组(或者说列表的列表)。因此,对整个矩阵的移动,可以看作是每行(每个数组)的单独移动后的集合。比如向右移动(注意:以下文字小屏幕手机请自行横屏观看 ^_^):

'((0 2 4 8)          '((0 2 4 8)
  (4 4 0 4)    --\     (0 0 8 4)
  (4 4 0 16)   --/     (0 0 8 16)
  (0 0 0 4))           (0 *2* 0 4))

注意按照游戏的规范,每次移动完成后,会在空闲的位置上随机出现2或4。所以这里在最后一行,第二个位置,出现了一个2。我们再看如何向上移动。

'((0 2 4 8)          '((0 4 4  8)
  (0 0 8 4)    --\     (0 0 16 4)
  (0 0 8 16)   --/     (*2* 0 0  16)
  (0 2 0 4))           (0 0 0  4))

问题来了,在大部分编程语言中,由于矩阵是数组的数组,所以横向的运算的算法无法直接应用到纵向的运算上,尽管算法上完全一致。作为一个讨厌重复的程序员,能不能找到两全其美的方法?

答案是矩阵的转置:

'((0 2 4 8)          '((0 0 0  0)
  (0 0 8 4)    --\     (2 0 0  2)
  (0 0 8 16)   --/     (4 8 8  0)
  (0 2 0 4))           (8 4 16 4))

向左移动:

'((0 0 0  0)          '((0 0  0 0)
  (2 0 0  2)   --\      (4 0  0 0)
  (4 8 8  0)   --/      (4 16 0 0)
  (8 4 16 4))           (8 4 16 4))

再转置回去(在相同的位置添加2):

'((0 0  0 0)          '((0 4 4  8)
  (4 0  0 0)   --\      (0 0 16 4)
  (4 16 0 0)   --/      (*2* 0 0 16)
  (8 4 16 4))           (0 0 0  4))

假定一张棋盘的所有状态用 board 记录,合并一行上面的元素的函数是 merge,转置的函数是 transpose,在棋盘上的任意空闲位置添加一个2/4的函数是 put-random-piece,那么对于一行的移动的代码是:

(define (move-row row v left?)
    (let* ([n (length row)]  ; 获取长度 '(4 4 0 2) -> 4
           [l (merge (filter (λ (x) (not (zero? x))) row))] ; '(4 4 0 2) -> '(4 4 2) -> '(8 2)
           [padding (make-list (- n (length l)) v)]) ; '(0 0)
      (if left?
          (append l padding) ; '(8 2 0 0)
          (append padding l)))) '(0 0 8 2)

移动整张棋盘的函数 move,在 move-row 基础上 map 一下就好。然后就可以定义四个方向上的移动了,比如说 move-leftmove-up

(define (move-left lst)
    (put-random-piece (move lst 0 #t)))

(define (move-up lst)
    ((compose1 transpose move-left transpose) lst))

这样把大的思路理顺后我们再回过头来看 merge 算法,其实就是一个递归:

(define (merge row)
    (cond [(<= (length row) 1) row]  ; row长度小于等于1直接返回
          [(= (first row) (second row)) ; 判断头两个值是否相等,然后生成新的列表
           (cons (* 2 (first row)) (merge (drop row 2)))]
          [else (cons (first row) (merge (rest row)))]))

基本的思路有了之后,我们看如何把状态投射到UI上。racket里提供 2htdp/image 可以很方便地绘出一个个cell,进而绘出一张棋盘:

然后便可以使用 big-bang 来开始游戏。big-bang 接受事件,然后进行处理。我们可以在一开始画一张空棋盘,然后每次键盘方向键按下,就进行状态调整,根据最新的状态重绘棋盘。

整个游戏制作下来,不到200行代码。就这么简单。如果你有兴趣,点击「阅读原文」看看详细的解说和代码吧。


1. 一门从scheme基础上发展起来的Lisp方言,见:http://racket-lang.org

2. racket下一个用来撰写文档的工具

3. 见:http://racket.tchen.me

原文发布于微信公众号 - 程序人生(programmer_life)

原文发表时间:2014-12-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java 成神之路

高亮标红

30580
来自专栏coolblog.xyz技术专栏

科普:String hashCode 方法为什么选择数字31作为乘子

某天,我在写代码的时候,无意中点开了 String hashCode 方法。然后大致看了一下 hashCode 的实现,发现并不是很复杂。但是我从源码中发现了一...

701190
来自专栏数据结构与算法

01:谁考了第k名 个人博客:doubleq.win

个人博客:doubleq.win 01:谁考了第k名 查看 提交 统计 提问 总时间限制: 1000ms 内存限制: 65536kB描述 在一次考试中,每个学...

36650
来自专栏小詹同学

Leetcode打卡 | No.011 盛最多水的容器

欢迎和小詹一起定期刷leetcode,每周一和周五更新一题,每一题都吃透,欢迎一题多解,寻找最优解!这个记录帖哪怕只有一个读者,小詹也会坚持刷下去的!

17920
来自专栏技术翻译

Python中的NLP

自然语言处理(NLP)是数据科学中最有趣的子领域之一,数据科学家越来越期望能够制定涉及利用非结构化文本数据的解决方案。尽管如此,许多应用数据科学家(来自STEM...

39150
来自专栏新智元

【看图识算法】这是你见过最简单的 “算法说明书”

【新智元导读】像阅读宜家的安装说明书一样学习算法,是怎样的体验?不伦瑞克工业大学的三名研究者制作了这份“算法说明书”,简明传神地解释了一些基本算法,一起来看图说...

38080
来自专栏从流域到海域

《笨办法学Python》 第27课手记

《笨办法学Python》 第27课手记 本节课讲逻辑运算(即布尔运算),对于学过数字电路或者离散数学的人来说非常简单,甚至不需要去刻意记忆真值表。 逻辑运算只有...

227100
来自专栏申龙斌的程序人生

参加steemit数学x程式大赛(第八回)

前一段时间参加了Steemit社区的两个活动,比如“接龙”创作大赛,五个人根据几张图片素材编出一篇小说,事先没有任何沟通,人员报名之后,顺序是随机指定的,我第一...

31460
来自专栏机器学习和数学

[数据结构和算法]《算法导论》动态规划笔记(1)

动态规划是求解最优化问题的方法,这类问题有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。我们称这个解为问题的一个最优解,而不是最优解,因为可能有多个...

402100
来自专栏mathor

搜索(7)

 这道题迷宫中多了一些花样。一是迷宫中有陷阱,由X表示。除非处于无敌状态,否则不能经过陷阱。二是有些位置到达后会自动获得无敌状态,持续K步  我们可以看...

11420

扫码关注云+社区

领取腾讯云代金券