いよいよ実践的なコーディングを行っていく。REPLにバシバシ打ち込むぞ〜〜!
第5章 テキストゲームのエンジンを作る
ここではテキストを扱う方法を勉強する。課題はテキストアドベンチャーゲームエンジン。
この章を読み進めるに当たって、テキストの扱いはコンピュータにとって得意ではないということを意識すること。
テキストは必要悪であって、触らずに済むならそれに越したことはない。
5.1 魔法使いのアドベンチャー
このゲームは第17章で完成する。その暁にはパズルを解いて魔法のドーナツを手に入れられるだろう。
あなたは魔法使いの弟子になって魔法使いの館を探索するのだ。
このゲームの世界
このゲームが進行する世界はこの画像のように、魔法使いの館だ。家には居間と屋根裏部屋がある。外には庭があり、井戸がある。合計3つの場所だ。また、各場所には色んなオブジェクト(アイテムだな)がある。

各場所を移動する経路は下記のようになる。

基本的な要求仕様
ゲームにおいて可能なユーザーの操作は下記。
- 周囲を見渡す
- 別の場所へ移動する
- オブジェクトを拾う
- 拾ったオブジェクトで何かをする
此の章では最初の3つの機能を実装する。完成は17章だ。
連想リストを使って景色を描写する
ゲーム世界を見渡すということは、各場所において、以下の3種類のものを「見る」ことができるということだ。
- 基本となる景色
- 1つ以上の、他の場所へつながる道
- 手にとって操作できるオブジェクト
グローバルな連想リストにゲーム世界の様子を登録しておこう。
(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 front of you.)) ;;面前に井戸がある
(attic (you are in the attic. ;;屋根裏
there is a giant welding torch in the corner.)))) ;;大きな溶接トーチがある
*nodes*は連想リストとして定義されている。キーは場所名、データはその場所の風景を描写するテキストだ。此のような構造は連想リスト(association list)、あるいはalistと呼ぶ。詳細は7章で。
ここに文字列の定義が無いことに注意。シンボルとリストだけで出来ている。その訳は、此の後の処理において元となるデータを出力形式に縛られない形で持っておくためだ。Lispで一番操作しやすいのが、シンボルとリストだからだ。確かに古めかしいスタイルとは言えるが^^
情景を描写する
assoc関数で*nodes*連想リストから情報を取り出す、describe-location関数を定義する。
> (defun describe-location (location nodes)
(cadr (assoc location nodes)))
> (describe-location 'living-room *nodes*)
(YOU ARE IN THE LIVING_ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH.)
‘living-roomをキーとして対応する情景描写が取り出された。
関数型プログラミングの例として噛み締めて。詳しくは14章だ。
5.2 通り道を描写する
各場所の情景描写ができるようになったので、ある場所から他の場所へ移動する通り道の情報を定義しよう。連想リスト*edges*を作る。
(defparameter *edges* '((living-room (garden west door)
(attic upstairs ladder))
(garden (living-room east door))
(attic (living-room downstairs ladder))))
次に*edges*をアクセスする関数を作る。まずは1個だけ求めるやつ。
(defun describe-path (edge)
`(ther is a ,(caddr edge) going ,(cadr edge) from here.)) ;;準クオート使用中
> (describe-path '(garden west door))
(THERE IS A DOOR GOING WEST FROM HERE.) ;;DOORとWESTが埋め込まれた
準クオートの仕組み
ここで準クオートと言う手法が使われている。データの埋め込みというやつだ。「,(caddr edge)」と言う文字列を組み込んでおくと、コードモードで読み込んで実行した結果を文字列として置き換えてくれる。どこかで見たやつだよね。コンマをつけることでクオートでデータモードにしてある所をコードモードに切り替える(アンクオートする)わけだ。絵を切り貼る。

複数の通り道を一度に描写する
次に全ての通り道を求めるやつ。
(defun describe-paths (location edges)
(apply #'append (mapchar #'describe-path (cdr (assoc location edges)))))
> (describe-paths 'living-room *edges*)
(THER IS A DOOR GOING WEST FROM HERE. THER IS A LADDER GOING UPSTAIRS FROM HERE.)
describe-paths関数は次のように実行される。
- 関係するエッジを求める
- エッジをその描写へと変換する
- 得られた描写同士をくっつける
関係するエッジを見つける
一番内側のコードを見てみよう。このように結果が戻ってくるんだ。
> (cdr (assoc 'living-room *edges*))
((GARDEN WEST DOOR) (ATTIC UPSTAIRS LADDER))
エッジをその描写へと変換する
見やすいように文字列の編集を行う。
> (mapcar #'describe-path '((GARDEN WEST DOOR) (ATTIC UPSTAIRS LADDER)))
((THER IS A DOOR WEST FROM HERE.)
(THERE IS A LADDER GOING UPSTAIRS FROM HERE.))
mapcar関数はある関数とその引数を受け取って実行させる機能を持つ。そして引数は複数のセットが許される。bashにもあるやつ。
> (mapcar #'sqrt '(1 2 3 4 5))
(1 1.4142135 1.7320508 2 2.236068) ;;引数の順番に実行されて結果がリストで帰っている
> (let ((car "Honda Civic")) ;;ローカル関数carを定義
(mapcar #'car '((foo bar) (baz qux)))) ;;mapcarにcar関数を渡している
(foo baz) ;;この場合は#'という記号が付いているので混乱しない
Common Lispは変数の名前空間と関数の名前区間を別々に管理しているのだ。
一方Schemeでは同じブロック内で、関数と同じ名前を変数につけることは出来ない。
SchemeはLisp-1、Common LispはLisp-2。
描写を統合する
mapcarによって得られた全ての通り道の記述のリストをまとめなければならない。
applyでバラしてappendでまとめるのだ。
> (append '(mary had) '(a) '(little lamb))
(MARY HAD A LITTLE LAMB)
> (apply #'append '((mary had) (a) (little lamb)))
(MARY HAD A LITTLE LAMB)
これでdescribe-pathsが返す大きなリストから、実際の描写のリストを作ることができる。
(apply #'append '((THER IS A DOOR GOING WEST FROM HERE.)
(THERE IS A LADDER GOING UPSTAIRS FROM HERE.)))
(THER IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE.)
5.3 特定の場所にあるオブジェクトを描写する
ゲームの世界を病者するために最後の要素は、夫々の場所に置いてある、プレーヤーが手にとって使うことができるオブジェクトの記述だ。
目に見えるオブジェクトをリストする
オブジェクトのリストを作る。
(defparameter *objects* '(whiskey bucket frog chain))

次にオブジェクトの場所を管理する連想リスト*object-locations*と見えるオブジェクトのリストを返す関数object-atを作る。
(defparameter *object-locations* '((whiskey living-room)
(bucket living-room)
(chain garden)
(frog garden))
(defun objects-at (loc objs obj-loc)
(labels ((at-loc-p (obj)
(eq (cadr (assoc obj obj-loc)) loc)))
(remove-if-not #'at-loc-p objs)))
見えるオブジェクトを描写する
(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 onj-loc)))))
5.4 全てを描写する
ここまで作った関数を全部使って、look関数を作り、自分の周囲を見渡す際のコマンドにしよう。
現在地は*location*で保持することにする。
(defparameter *location* 'living-room)
(defun look ()
(append (describe-location *location* *nodes*)
(describe-paths *locayion* *edges*)
(describe-objects *location* *object-locations*)))
look関数はグローバル変数を参照しているので、関数型プログラミングスタイルではない。
関数型プログラミングでは引数と関数内で宣言された変数しか使わない建前なのだ。
5.5 ゲーム世界を動き回る
ゲームの世界を動き回ってみよう。
(defun walk (direction)
(let (next (find direction ;;directionを持つcadrがあればnextへ出す
(cdr (assoc *location* *edges*))
:key #'cadr)))
(if next
(progn (setf *location* (car next))
(look))
'(you cannot go that way.))))
walk関数はdirection(方向)を引数にして現在地からの移動を実現させる。その歩行に道がなければエラーが帰って動けない。:keyはfind関数への引数で、探す際のキーを指定している。
5.6 オブジェクトを手に取る
(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とassocを使って、以前の値を残したまま、alistの値が変更されたかのように見せることができる。
5.7 持っているものを調べる
最後にプレーヤーが現在持っているものを見られる関数を作ろう。
(defun inventory()
(cons 'item- (objects-at 'body *objects* *object-locations*))) ;;'bodyがキーだ
5.8 本章で学んだこと
- 此の章ではテキストアドベンチャーゲームのための簡単なエンジンを組み立てた。
- ゲームの世界はプレーヤーが行くことができる場所をノードとし、場所間を行き来する経路をエッジとする数学的なグラフで表現できる。
- 此のグラフはm’変数*nodes*に連想リスト(alist)の形で持っておくことができる。これによってノード(場所)の名前からその場所の属性を引ける。此のゲームでは、属性として各ノード(場所)の描写を格納しておいた。
- assoc関数により、キー(ここでは場所の名前)を使ってalistからデータを引き出すことができる。
- 準クオートを使えば、大きなデータの中に、その一部分を計算するためのコードを埋め込むことができる。
- Lispの関数には、保管の関数を引数として受け取るものがある。これらは高階関数と呼ばれる。mapcarはCommon Lispで最もよく使われる高階関数だ。
- alist中の値を置き換えr長ければ、新しい要素をリストにpushするだけでいい。assocは最も新しい値だけを返すからだ。
コメント