通过 Land of Lisp 中的超简短字符游戏例程学习 Common Lisp 的 loop 和 format
介绍
在 Land of Lisp
第11章结束处有一个使用了 loop
和 format
的代码超级简短的小游戏, 书中称其为single paper game
, 意思是这段游戏代码可以放在一页纸上.
本文主要介绍构成这段代码的技术基础: loop
和 format
的各种用法.
游戏代码一览
这里是代码
(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.lispT[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 outerfor 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]>
忽然发现理解了上面例子中的这几种用法, 那段游戏代码也就理解了. :)
不过单色的字符有些单调, 我们打算把它们修改为彩色字符, 比如, 机器人用一种颜色, 玩家控制的角色用一种颜色, 陷阱用一种颜色. 这部分修改改天再写, 先上一张彩色字符的截图:
--结束