通过 Land of Lisp 中的超简短字符游戏例程学习 loop 和 format

通过 Land of Lisp 中的超简短字符游戏例程学习 Common Lisp 的 loop 和 format

介绍

Land of Lisp 第11章结束处有一个使用了 loopformat 的代码超级简短的小游戏, 书中称其为single paper game, 意思是这段游戏代码可以放在一页纸上.

本文主要介绍构成这段代码的技术基础: loopformat 的各种用法.

游戏代码一览

这里是代码

(defun robots ()		
  (loop named main		
        with directions = '((q . -65) (w . -64) (e . -63) (a . -1)
                            (d .   1) (z .  63) (x .  64) (c . 65))
        for pos = 544
        then (progn (format t "~%qwe/asd/zxc to move, (t)eleport, (l)eave:")
                    (force-output)
                    (let* ((c (read))
                           (d (assoc c directions)))
                      (cond (d (+ pos (cdr d)))
                            ((eq 't c) (random 1024))
                            ((eq 'l c) (return-from main 'bye))
                            (t pos))))
        for monsters = (loop repeat 10
                             collect (random 1024))
        then (loop for mpos in monsters
                   collect (if (> (count mpos monsters) 1)
                                 mpos
                               (cdar (sort (loop for (k . d) in directions
                                                    for new-mpos = (+ mpos d)
                                                 collect (cons (+ (abs (- (mod new-mpos 64)
                                                                          (mod pos 64)))
                                                                  (abs (- (ash new-mpos -6)
                                                                          (ash pos -6))))
                                                               new-mpos))
                                           '<
                                           :key #'car))))
        when (loop for mpos in monsters
                   always (> (count mpos monsters) 1))
        return 'player-wins
        do (format t
                   "~%|~{~<|~%|~,65:;~A~>~}|"
                   (loop for p
                         below 1024
                         collect (cond ((member p monsters)
                                        (cond ((= p pos) (return-from main 'player-loses))
                                              ((> (count p monsters) 1) #\#)
                                              (t #\A)))
                                       ((= p pos)
                                        #\@)
                                       (t
                                        #\ ))))))

原书中该页截图如下: 页面截图

使用方法

加载并执行程序

把上述代码保存为一个名为 c11-robots.lisp 的文件, 然后启动 CLISP, 在 REPL 中加载该段文件, 加载成功后执行函数 (robots), 操作过程如下:

[1]> (load "c11-robots.lisp")
;; Loading file c11-robots.lisp ...
;; Loaded file c11-robots.lisp
T
[2]> (robots)

|      A                                                         |
|                                                                |
|  A                                                             |
|                      A                                         |
|                 A                                              |
|                                                                |
|                          A                                     |
|                                                                |
|     A                          @                             A |
|                                 A                              |
|                                                                |
|                                                                |
|                                          A                     |
|                                                                |
| A                                                              |
|                                                                |
qwe/asd/zxc to move, (t)eleport, (l)eave:

游戏截图:

输入图片说明

游戏说明

图中 @ 表示玩家控制的角色, A 表示机器人, 随机出现的 # 表示陷阱, 不论玩家还是机器人谁碰到都会死, 机器人会一直追逐玩家, 一旦被追上就失败, 玩家可以引诱机器人去碰陷阱

游戏操作

  • 方向键:
    • qwe: 左上,上,右上
    • a d: 左,右
    • zxc: 左下,下,右下
  • 其他键:
    • t 瞬移到随机位置,
    • l 离开游戏

代码解读

这段游戏代码主要使用了 loop 宏和 format 函数的功能, 后面我们会按功能块逐行解读这段代码, 不过在此之前, 需要了解关于 loop 宏和 format 函数相关语法的一些知识储备.

format 语法的知识储备

先说 format 的语法结构, 就以上述代码来说, 通常, 我们会这样调用 format

(format t "~%qwe/asd/zxc to move, (t)eleport, (l)eave:")

这里, t 表示标准输出, 双引号内的 ~% 是一个控制字符串, 表示换行, 波浪号~所代表的就是控制字符串, 有些控制字符用来控制输出的格式, 有些控制字符则作为占位符使用, 双引号内其他的文本内容表示直接输出的字符串, 直接输出即可.

常用的一些控制字符串:

  • ~a
  • ~b 显示二进制数, 例子如下:
[4]> (format t "十进制数字 12 的二进制形式为 ~b." 12)
十进制数字 12 的二进制形式为 1100.
NIL
[5]>
  • ~c 作为字符的占位符, 由后面参数提供, 会显示一个小写字符, 例子如下:
[18]> (format t "输出字符 ~c, 它的具体值由后面的参数决定." #\a)
输出字符 a, 它的具体值由后面的参数决定.
NIL
[19]>

format 的嵌套语法, 按照 Land of Lisp 作者所说, 这个语法很疯狂, 它以一种非常简洁的形式表现出比较复杂的形式, 因为它具备一种循环结构可以解析处理嵌套的列表. ~{~} 控制序列加一个列表, 会依次循环输出列表中的元素, 这里我们先定义一个用 loop 生成的数字列表, 然后再用 format"~{ ~a ~}" (1 2 3 4) 形式来循环输出数字列表中的数字:

[6]> (defparameter *numbers* (loop repeat 10 collect (random 100)))
*NUMBERS*
[7]> *numbers*
(37 27 8 47 42 13 32 86 7 73)
[8]> (format t "~{ I see a number: ~a! ~}" *numbers*)
 I see a number: 37!  I see a number: 27!  I see a number: 8!  I see a number: 47!  I see a number: 42!  I see a number: 13!  I see a number: 32!  I see a number: 86!  I see a number: 7!  I see a number: 73! 
NIL
[9]> 

增加一个换行控制序列 ~%, 注意, 因为要换行的是每次输出的内容, 所以 ~% 要写在 ~{~} 内, 如下:

[9]> (format t "~{ I see a number: ~a!~% ~}" *numbers*)
 I see a number: 37!
  I see a number: 27!
  I see a number: 8!
  I see a number: 47!
  I see a number: 42!
  I see a number: 13!
  I see a number: 32!
  I see a number: 86!
  I see a number: 7!
  I see a number: 73!
 
NIL
[10]>

接下来我们见识一下什么是疯狂的 format, 用一行代码输出格式规整的数字表:

[12]> (format t "|~{~<|~%|~,33:;~2d ~>~}|" (loop for x below 100 collect x))
| 0  1  2  3  4  5  6  7  8  9 |
|10 11 12 13 14 15 16 17 18 19 |
|20 21 22 23 24 25 26 27 28 29 |
|30 31 32 33 34 35 36 37 38 39 |
|40 41 42 43 44 45 46 47 48 49 |
|50 51 52 53 54 55 56 57 58 59 |
|60 61 62 63 64 65 66 67 68 69 |
|70 71 72 73 74 75 76 77 78 79 |
|80 81 82 83 84 85 86 87 88 89 |
|90 91 92 93 94 95 96 97 98 99 |
NIL
[13]> 

我们希望能在表格上方跟下方增加分割线条, 类似这样的: ------------, 那么我们单独试验一下:

[18]> (format t "~{~a~}~%" (loop repeat 32 collect #\-))
--------------------------------
NIL
[19]> 

很好, 符合预期, 那就跟前面的表格语句合并, 先试验上方, 如下:

[17]> (format t "~{~a~}~%|~{~<|~%|~,33:;~2d ~>~}|" (loop repeat 32 collect #\-) (loop for x below 100 collect x))
--------------------------------
| 0  1  2  3  4  5  6  7  8  9 |
|10 11 12 13 14 15 16 17 18 19 |
|20 21 22 23 24 25 26 27 28 29 |
|30 31 32 33 34 35 36 37 38 39 |
|40 41 42 43 44 45 46 47 48 49 |
|50 51 52 53 54 55 56 57 58 59 |
|60 61 62 63 64 65 66 67 68 69 |
|70 71 72 73 74 75 76 77 78 79 |
|80 81 82 83 84 85 86 87 88 89 |
|90 91 92 93 94 95 96 97 98 99 |
NIL
[18]>

再把下方的加进去:

[20]> (format t "~{~a~}~%|~{~<|~%|~,33:;~2d ~>~}|~%~{~a~}~%" (loop repeat 32 collect #\-) (loop for x below 100 collect x) (loop repeat 32 collect #\-))
--------------------------------
| 0  1  2  3  4  5  6  7  8  9 |
|10 11 12 13 14 15 16 17 18 19 |
|20 21 22 23 24 25 26 27 28 29 |
|30 31 32 33 34 35 36 37 38 39 |
|40 41 42 43 44 45 46 47 48 49 |
|50 51 52 53 54 55 56 57 58 59 |
|60 61 62 63 64 65 66 67 68 69 |
|70 71 72 73 74 75 76 77 78 79 |
|80 81 82 83 84 85 86 87 88 89 |
|90 91 92 93 94 95 96 97 98 99 |
--------------------------------
NIL
[21]> 

控制好换行 ~% 的位置, 就可以输出上述的表格.

实际上这几个例子已经用到了部分 loop, 接下来我们探讨一下 loop 的一些用法.

loop 语法的知识储备

loop 的语法相当复杂, 好在我们这里只用到其中一一小部分, 在本文例程中用到的的 loop 形式有这么几种, 为方便理解, 全部用实际例子代替解说, 自己多试几遍应该就掌握了, 如下:

  • with 用于定义一个局部变量
[48]> (loop with x = (+ 1 2) repeat 5 do (print x))                                                                                                           

3 
3 
3 
3 
3 
NIL
[49]> 
  • repeat 最简单的循环
[29]> (defparameter o (loop repeat 10 collect 2))
O
[30]> o
(2 2 2 2 2 2 2 2 2 2)
[31]> 
  • for 用于设置循环变量

(for x below 10 ...)

[37]> (defparameter o (loop for x below 10 collect x))
O
[38]> o
(0 1 2 3 4 5 6 7 8 9)
[39]> 

(for x from 0 do ...)

[51]> (loop for i from 0 do (print i) when (= i 5) return 'oK)

0 
1 
2 
3 
4 
5 
OK
[52]>

(for x in list ...)

[73]> (loop for i in '(100 25 35) sum i)
160
[74]> 
  • then 不知道该怎么描述
[43]> (loop repeat 10 for x = 10.0 then (/ x 2) collect x)
(10.0 5.0 2.5 1.25 0.625 0.3125 0.15625 0.078125 0.0390625 0.01953125)
[44]> 
  • if 用于设置条件
[44]> (loop for i below 10 if (oddp i) do (print i) (print "OK"))

1 
"OK" 
3 
"OK" 
5 
"OK" 
7 
"OK" 
9 
"OK" 
NIL
[45]> 
  • when 用于设置条件
[45]> (loop for i below 10 when (oddp i) do (print i) (print "OK"))

1 
"OK" 
3 
"OK" 
5 
"OK" 
7 
"OK" 
9 
"OK" 
NIL
[46]>
  • unless 用于设置条件
[77]> (loop for i below 10 unless (oddp i) do (print i))                                                                                                      

0 
2 
4 
6 
8 
NIL
[78]>
  • named 相当于一个标签, 用于设置返回点
[72]> (loop named outer
for i below 10 
do
(progn 
(print "outer")
(loop named inner 
for x below i 
do (print "**inner") 
when (= x 2)
do
(return-from outer 'kicked-out-of-all-the-way))))

"outer" 
"outer" 
"**inner" 
"outer" 
"**inner" 
"**inner" 
"outer" 
"**inner" 
"**inner" 
"**inner" 
KICKED-OUT-OF-ALL-THE-WAY
[73]> 
  • return-from 返回到由 named 定义的标签处

参见 named 的例程

  • collect 收集单个元素, 返回由单个元素组成的列表
[79]> (loop for i below 5 collect (list i))
((0) (1) (2) (3) (4))
[80]> (loop for i below 5 collect i)
(0 1 2 3 4)
[81]>
  • append 收集单个元素追加到列表中, 返回列表
[78]> (loop for i below 5 append (list 'z i))
(Z 0 Z 1 Z 2 Z 3 Z 4)
[79]>

忽然发现理解了上面例子中的这几种用法, 那段游戏代码也就理解了. :)

不过单色的字符有些单调, 我们打算把它们修改为彩色字符, 比如, 机器人用一种颜色, 玩家控制的角色用一种颜色, 陷阱用一种颜色. 这部分修改改天再写, 先上一张彩色字符的截图: 彩色字符

--结束

转载于:https://my.oschina.net/freeblues/blog/601453

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值