人生は勉強ブログ

https://github.com/dooooooooinggggg

Common Lispの基礎的な文法2

あくまで自分用のメモ。

今後の実装の時に、簡単に振り返るための記事。

進めている本は、Land Of Lisp

Land of Lisp

Land of Lisp

前回の記事の続き。

www.ishikawa.tech

今回は、5章のテキストゲームのエンジンを作る。

場所の描画

;; トップレベル変数で、場所の描画
(defparameter *nodes* '(
    (living-room (you are in the living room.
        a wizard is snoring loudly on the couch.))
    (garden (you are in a beautiful garden.
        there is a well in vront of you))
    (attic (you are in the attic.
        there is a giant welding torch in the corner))
))

ここで、

(living-room (you are in the living room.
        a wizard is snoring loudly on the couch.))

となっている部分の表現方法は、alistという構造で、馴染み深い日本語でいうと、連想リスト。

また、alistからキーを元に値を取り出すassoc関数もある。

(assoc 'garden *nodes*)
;; ->(GARDEN (YOU ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN VRONT OF YOU))

この、assoc関数を使うと、場所を描画するdescribe-location関数は簡単に書ける。

(defun describe-location(location nodes)
    (cadr (assoc location nodes)))

これを使うには、以下のようにコマンドを叩く。

(describe-location 'living-room *nodes*)

ここで、describe関数が、関数の中で直接*nodes*を参照していないのには理由がある。

これは、関数型プログラミングスタイルで書かれているからである。

外の世界をみないで、引数と、関数内で定義したもののみを見る。

いわゆる、副作用のない関数というものか。

(defparameter *nodes* '(
    (living-room (you are in the living room.
        a wizard is snoring loudly on the couch.))
    (garden (you are in a beautiful garden.
        there is a well in vront of you))
    (attic (you are in the attic.
        there is a giant welding torch in the corner))
))

(defun describe-location(location nodes)
    (cadr (assoc location nodes)))

;; これを使うためのコードは、
(describe-location 'living-room *nodes*)

通り道の描画

(defparameter *edges* '(
    (living-room
        (gardern west door)
        (attic upstairs ladder)
    )
    (garden (living-room east door))
    (attic (living-room downstairs ladder))
))

(defun describe-path (edge)
    `(there is a ,(caddr edge) going ,(cadr edge) from here.))
;; これを使うためのコードは、
(describe-path '(garden west door))

準クォート

準クォートを使うには、これまでコードモードからデータモードに切り替えに使っていた'の代わりに、バッククォートを使う。

バッククォートを使った準クォートの中では、カンマを使うことで、一部分だけをコードモードに戻せる。

通り道の描画の拡張

(defun describe-paths (location edges)
    (apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))

これは、一般的なプログラミング言語では、for文に値するものである。 describe-pathsを実行するには以下の方法をとる。 (describe-paths 'living-room edges)

次のように実行される。 1. 関係するエッジを見つける。 1. エッジをその描写へと変換する 1. 得られた描画同士をくっつける

describe-pathの一番内側を見てみる。

(cdr (assoc 'location *edges*))
;; ((GARDERN WEST DOOR) (ATTIC UPSTAIRS LADDER))

ここで得られた結果を、その描写へと変換。

(mapcar #'describe-path '((GARDERN WEST DOOR) (ATTIC UPSTAIRS LADDER)))
;; ((THERE IS A DOOR GOING WEST FROM HERE.) (THERE IS A LADDER GOING UPSTAIRS FROM HERE.))

mapcarは、よく使われる関数で、引数に他の関数とリストを受け取って、リストの要素の数だけ関数を呼び出す。

このように、他の関数を引数として受け取る関数を高階関数と呼ぶ。

また、#' -> #'car -> (function car)

このような内部的な変換が行われる。

common lispでは、関数を値として扱う場合には、functionオペレータを明示しなければならない。

(apply #'append (mapcar #'describe-path '((GARDERN WEST DOOR) (ATTIC UPSTAIRS LADDER)))))
;; 全て分解するとこうなる。↑

append関数と、describe-pathがそれぞれapplyとmapcarに渡されている。

この二つは、最初に引数に、関数を受け取るように設計されている。

appendにはくっつけたいっリストを一つずつ別々の引数として、渡す必要がある。

が、いくつ結果が返ってくるかなどわからない。

そこで、applyを使う。

applyでは、関数とリストを渡すと、あたかもそのリストの各要素を引数として関数を呼び出したかのような挙動をする。

目に見えるオブジェクトをリストする。

(defparameter *objects* '(whiskey bucket frog chain))

;; オブジェクトの場所を管理する変数
;; オブジェクトとその場所をalistで管理する
(defparameter *object-locations* '(
    (whiskey living-room)
    (bucket living-room)
    (chain garden)
    (frog garden)
))

;; 与えられた場所から見えるもののリストを返す関数
(defun objects-at (loc objs obj-locs)
    (labels (
        ;; labelsを使用して、ローカル関数を定義
        (at-loc-p (obj)
            (eq (cadr (assoc obj obj-locs)) loc)
        ))
        (remove if-not #'at-loc-p objs)
    )
)

(objects-at 'living-room *objects* *object-locations*)
;; (WHISKEY BUCKET)

;; これらを使い、ある場所で見えるオブジェクトを描写する関数が書ける
(defun describe-objects (loc objs obj-loc)
    (labels (
        (describe-obj (obj)
            `(you see a ,obj on the floor.)
        )
        (apply #'append (mapcar #'describe-obj (objects-at loc objs obj-loc)))
    ))
)

(describe-objects 'living-room *objects* *object-locations*)

動き系

今までに作った3つの描写関数をまとめて、lockというコマンドで簡単に呼び出せるようにする。

まずは現在地を保持するグローバル変数を作る

デフォルトはliving-room

(defparameter *location* 'living-room)

(defun lock ()
    (append
        (describe-location *location* *nodes*)
        (describe-paths *location* *edges*)
        (describe-objects *location* *objects* *object-locations*)))

;; (lock)で呼び出せる

;; 歩き回るコードも書く。
(defun walk (direction)
    (let (
            (next (find direction
                    (cdr (assoc *location* *edges*))
                    :key #'cadr)))
            (if next
                (progn (setf *location* (car next))
                    (lock))
                '(you cannot go that way.))))

このコードはまず、現在地から進める道を、edgeから調べている。

その結果をfind関数に渡している。

findは、リストから、与えた要素を探す関数。方角は、cadrに格納されているため、それをとってくる。

もしnextがあれば、Tを返す。

ifが真の時、locationに値をセットすることで、移動が完了する。

findのあとの、key というところで、direction(の値)という要素をcadrに持つような最初の要素をリストから見つけてくる。

;; オブジェクトを手にとる
(defun pickup (object)
    (cond ((member object
                (objects-at *location* *objects* *object-locations*))
            (push (list object 'body) *object-locations*)
            `(you are now carrying the ,object))
        (t '(you cannot get that.))))
;; pushに関して。
(push (list object 'body) *object-locations*)
;; これは、下のようにも置き換えられる。
(push (list object 'body) *object-locations*)
;; 持っているものを調べる
(defun inventory ()
    (cons 'items- (objects-at 'body *objects* *object-locations*)))

このような感じで、ゲームエンジンの部分は完了。

その後、コードの書き方(インデントなど)を修正したものが、以下。

現在、gardenに行くと、NILが帰ってくる不具合があるので要修正。

https://github.com/dooooooooinggggg/LandOfLisp/blob/663af4e7c47466e0fe2e18541bb03a6afa06d912/text_game/game.lisp

次は6章。

www.ishikawa.tech