プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(32)

今日は「Land of Lisp」のヤバイゲームを解読します…できたらいいな。

ロボットの逆襲

このゲームではプレイヤーの任務はロボットの殲滅です。
ロボットもロボットで、ひたすらプレイヤーを殺すために近寄ってきます。
ロボットとロボットが接触すると接触した2体が壊れます。スクラップにぶつかっても壊れます。
ロボットとプレイヤーが接触すればゲームオーバー、全てのロボットを破壊すればゲームクリアです。

ヤバイのはゲーム内容ではありません。実装です。

CL-USER> (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
                                               #\ ))))))
ROBOTS
CL-USER>

う〜ん…とりあえず遊んでみます。

qwe/asd/zxc to move, (t)eleport, (l)eave:w

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

PLAYER-WINS
CL-USER>

勝てました。さて、ここでもう一度関数の全貌を見てみます。

CL-USER> (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
                                               #\ ))))))
ROBOTS
CL-USER>

落ち着いてひとつずつ解読します。

(loop named ... with ... for ... then ... when ... do ...)が関数全体の屋台骨って感じがします。
namedはループに名前をつけて、return-fromでのその名前を指定すればループ全体からの脱出を可能にするのですね。
withはローカル変数を定義する役目を持っているそう。'='を使うのが特異な感じがしますね。
forはループに伴い変化する変数。おなじみですね。
thenは、ふーむ、ちょっと試してみます。

CL-USER> (loop repeat 5 for x = 10.0 then (/ x 2) collect x)
(10.0 5.0 2.5 1.25 0.625)
CL-USER> (loop repeat 5 for x = 10.0 do (/ x 2) collect x)
(10.0 10.0 10.0 10.0 10.0)
CL-USER>

最初の一回は引数(と言っていいのかな)をそのまま使い、それ以降はthenの後にある処理でxを更新しているようです。
whenは普通のwhenとそんなに変わらなさそう。その後の条件式が真なら続く処理を実行する感じですね。
doもおなじみ。ループのたびに実行される処理ですね。
よし、更に見ていきます。

最初のnamedでloop全体の名前をmainとしていますね。
続くwithでローカル変数directionを定義。これはキーと進む方向のセットのリストですね。
マスは64*16で1024。真上に行くなら-64、左なら-1…そんな感じ。
次はforでposを544と定めています。posはプレイヤーの初期位置ですかね。
thenの中でprognを使い、入力を促して、cにプレイヤーの入力を格納。
dには入力に対応するキーのリストをdirectionから探して格納します。
dがnilでなければ、posに入力に対応する移動をさせます。
tならテレポートでランダムなマスへ、lならreturn-fromでゲームを終了。
それ以外のキーならば、プレイヤーはposから移動しません。

次はforでmonstersにランダムなゲームマス上の位置を10個リストにしたものを格納。
そしてthenではまずloopを用いてmonstersの要素を取り出しmposへ入れています。
もしmposがmonstersの中に1以上あったら、つまり1マスに複数のロボットがいたらmposをそのまま返し、
そうでなければ…むむむ。…一回落ち着こう。
directionsから要素を取り出します。進む方向を得るためにdirectionsを利用するのですね。
そしてnew-posとしてmposにdirectionから得た方向を足したものを定義。
その後はnew-posからプレイヤーの位置への「マンハッタン距離」を計算、結果をnew-posとのペアにします。
sortでマンハッタン距離を比較し、ソートの結果最もプレイヤーに近い位置になるnew-posを新しいposとし、
それらでmonstersを更新するのですね。

次はまずwhenです。モンスターズのすべての要素が、他の要素と重複しているか調べ、
それはつまり全てのロボットがスクラップになっているということなので、
もしそうならプレイヤーの勝利だと示しループを終了します。

そうでなければdoです。ここのformatは「まだ気にしなくていい」と参考書にコメントされていますが気になります。
まずは改行していますね。そして縦棒を出力。ここからリスト内をループすることが示されます。
そしてブロックに入ります。縦棒、改行、縦棒の出力がありますが、昨日のようにはもう騙されません。
この後に",~65:;"がありますので、これは65文字を超えた時に初めて出力されます。
一行の終わりと新しい一行の開始の縦棒、改行、縦棒ですね。
そして"~A"で引数を整形して出力、ブロックが終了。
しかしループが続きますのでリストの要素が尽きるまで引数と縦棒と改行の出力が繰り返されます。
ループが終わったら、最後の縦棒を置いて全体の一回りが終わります。

さて、最後に解読しなくてはならないのはformatの値の引数の部分ですね。
ここもループでリストを作っています。ループ回数はマスの数の分だけ1024回、
ループが何回目かはpで示されます。ループ回数とマスは完全に対応しています。
pが0ならそれは一番左上のマスについてのループであり、63ならば一番右上についてのループです。
condでmonstersにpが含まれているか、つまりそこにロボットがいるかを調べます。
もしいるならば、更にcondを用いて、まずはプレイヤーと同じマスにいるかを調べます。
もし同じマスにいるのならば、プレイヤーの敗北を示しループ終了です。
そうでなければ、そのマスに他にロボットがいるかを調べ、もしいるのならばスクラップを描写するために
formatに渡して値引数とするための#\#をリストの要素とします。
プレイヤーもいないロボットも一体だけというならば、リストに入る要素は#\Aです。
そしてプレイヤーがいるマスであれば、#\@をリストに渡し、
何も無ければ#\ (ただのスペース)をリストに入れます。

…終わった。解読完了です。
嬉しいですが、これを自分で作るのはまだ無理です。loopとformatの力に戦慄しています。

今日は1ページ、しかも1関数だけで2時間以上使ってしまいました。
まだまだハッカーにはなれそうもないですね。

今日もまた未熟さを感じたのでもう少し勉強を続けますが、記事としてはここまでです。
皆様の健やかな週末を願って終わります。