『Land of Lisp』(Shiro:LandOfLisp) のゲームで遊ぶために いちいちCLISPを立ち上げるのが面倒、という方々のために、コードをGaucheに移植したよ!
全てのコードはこちらから。開発版HEADでチェックしてるので、0.9.3.3だとエラーが出るかも。
元のコードは原著のページ http://landoflisp.com/source.html から取れる(ただ、このページから取れるソースより本に載ってるソースの方が新しいので注意)。Conradさんが「Public Domainでいいよ」って言ったので、Gauche版もPublic Domainで。
以下、移植のノウハウを書きつつコメントしてゆく。
pure Schemeに移植するとなるとCLに比べて色々機能が足りなくて面倒だろうけれど、
GaucheはかなりCLに擦り寄ってるのでほとんど手間はかからなかった。大体は関数の
名前や引数順を直す程度。loop は大抵 SRFI:42 で書き直せる。
format のループ構文など高度な機能を使ってるやつは、Gaucheで未実装なので
仕方なくべたなコードに展開した。
ハマりどころはnilの扱いで、元コードで真偽値として見てるのか
空リストとして見ているのかを判断して扱いを変えないとならない。
ここは機械的にやるのは無理だろう。
「どっちも兼ねられるから便利」ってCLerは言うんだけど、Schemerは最初から
真偽値とリストを別物として発想するから、Schemeで書いてて困ることってあんまりないんだよね。
CLを移植するときだけ困る。
とはいえ、関数名のtypoとかが実行時まで見つけられないと面倒なので、 こんな感じで未定義名のチェックだけやるようにしといた (test.scm)。 Gaucheは静的チェックをテスト時に行うのを推奨している。
(define-macro (test-file-binding name)
`(begin
(define-module ,name)
(load ,#`"./,|name|.scm" :environment (find-module ',name))
(test-module ',name)))
(test-file-binding guess)
...
これは全くベタな移植でok。ashはGaucheでも組み込み。
これもほぼストレートな移植だったかな。よくあるシーケンス関数の置き換え:
| CL | Scheme | |
(remove-if-not pred lis)
| (filter pred lis)
| |
(remove-if pred lis)
| (remove pred lis)
| |
(find-if pred lis)
| (find pred lis)
| |
(count obj lis)
| (count (cute eq? obj <>) lis)
| srfi-1 |
(position obj lis)
| (list-index (cute eq? obj <>) lis)
| srfi-1 |
ちょっとだけトリッキーなのはgame-readで、Common Lispのreadは
後続の空白文字(改行文字含む)を消費するので、
> (game-repl)
とタイプしてRETを打った時、入力バッファは空で、game-read はすぐに
ユーザの入力待ちになるのだけれど、Gaucheのreadは後続の空白文字を消費しない。
gosh> (game-repl)
とタイプしてRETを打つと、REPLは閉じ括弧まで読んですぐに(RETを入力に残したまま)
game-replを実行する。その中から読まれる game-read は最初に
残っていたRETを見るので、最初のread-line で空の文字列が帰ってくる。
移植版ではこのケースへの対応が入っている。
(use gauche.sequence) しとけば色々な関数がリストやベクタや文字列に
使えるようになるのでCLのシーケンス関数みたいな感じになる。
substitute-if に対応するのが無いことに気づいた。
個人的にあまり使ったことはないんだけど、あってもいいかもしれない。
原書でmapc使ってる繰り返しは自分の好みでdolistにした。
原書でdolistって確か紹介してないよね。まあloop使えばいいんだけど。
CLのset-differenceやintersectionにはSRFI:1のlset-difference, lset-intersection等。 remove-duplicatesは
delete-duplicates。
これの移植の時が一番 nil でひっかかった気がする。
alistを調べて見つかったら値を取る、というコードはCLだと
(cdr (assoc key alist))
で良い。assocで見つからないとnilが返るが、(cdr nil) => nil なので。
これをSchemeでそのまま書こうとすると、assocが#fを返すケースを判断しないと
ならない。
(and-let* ([p (assoc key alist)]) (cdr p))
Gaucheなら以下を推奨。以前は(use util.list)しないとならなかったけど
今は組み込みになってる。
(assoc-ref alist key)
あと、原著の find-empty-node の実装が極めてナイーブなので、
そこだけGauche版では直してある。
本では移動するたびにブラウザでpngをリロードするように書いてあるけど、
ファイルの更新を自動検出してくれる画像ビューア (gthumbとか) を使えば
さくさくプレーできるよ。
CLのdefstructは、Gaucheだとレコード(define-record-type)にだいたい
相当する。
レコードはdefstructと同様、コンストラクタやアクセサが自動生成
されるし、定義したレコード型は独立したクラスになるのでdefine-methodによる
ディスパッチもできる。ただ、コンストラクタ引数を省略した場合の
デフォルトの初期値の指定ができない
(GaucheのはSRFI:99ベースなので。R6RSレコードだと(あんまり綺麗じゃないけど)書ける。)
define-classにすればデフォルト値を使えるけど、
こっちはアクセサとか名前をいちいち書いてやらないとならない。
define-record-typeを拡張して初期値指定できるようにしようかなあ。
gethashとhash-table-getの引数の順序に注意。
hash-table-getにsetterが無いことに気づいた。何でだったかな。
単に忘れてただけかも。
copy-recordはあった方が良いのかもしれない。とはいえ、コピーが無いことを
逆手に取って、deep copyが必要なgeneのところを直に処理している。
それから、数珠つなぎのアクセス。Cでなら
animal->gene[mutation]
と書けるところ、CLだと
(aref (animal-gene animal) mutation)
Gaucheのスタイルはこれ。
(~ animal 'gene mutation)
おまけ: 本書ではシミュレーションを十分長く走らせると種が分化するって
話が出てくるけど、実際に分化しているかどうかを確かめるのに*animals*の遺伝子を
いちいちダンプして見比べるのは面倒なので、遺伝子の違いを色で表示するバージョンを作った。
実行例。これは20万日後。最初はグレーなんだけど、緑と茶色が出てきてる。
30万日後。緑と茶色だけになった。緑がゾウ型で茶色がロバ型かな。
一度24bit colorを計算してから、ANSIエスケープシーケンスで端末に出せる色に近似してる。 フルカラー出せる端末ならもっと派手になるんじゃないかな。
loopとformatの変態構文を駆使したコード。これはかなり変えざるを得なかった。
まあでもそんなに冗長にはなってないと思うけれど。
プロンプトを出してユーザ入力を受け付ける前に(flush)を忘れないように。
これは移植ではない。Gaucheの道具を使うともっと簡単に書けるので。 基本形は『プログラミングGauche』に載せたやつだけど、 後のダイス・オブ・ドゥームで使えるようにインタフェースを『Land of Lisp』に合わせてある。
でもhttpサーバが欲しかったらこういうところから書き始めるよりは 既にあるものをいじった方が良いだろう。
あ、コミットしてあるコードはポート番号を8077にしてある。手元のマシンでたまたま 8080を使用中だったので。
arefは直接対応させるならvector-refだけど、~で済ませた。
SRFI:42大活躍。ところでCLのloopで、リストやベクタを走査しつつ、
そのインデックスをループの度に取りたい時は、for節を複数回すのがイディオムだ。
(loop for elt across #(a b c)
for pos
collect (cons pos elt))
=> ((0 . A) (1 . B) (2 . C))
srfi-42では並べて書いた繰り返し節は入れ子になるので、loopのように並列に走らせたければ こう書かないとならない。
(list-ec (:parallel (: elt '#(a b c))
(:integers pos))
(cons pos elt))
でも、欲しいのがインデックスなら、こういう構文がある。忘れがちなのでメモ。
(list-ec (: elt (index pos) '#(a b c))
(cons pos elt))
ゲーム木のcar部、passing moveの時は#fを入れている。CL版だとここがnil
なのでチェック無しでゲーム木の caar を取ったりするところがある。
ちょっとはまった。
neighbors のリストを組み立てるところ、こういうの、
CLだとwhen/unlessが条件不成立/成立時に
nilを返すことを利用して簡潔に書ける:
(append (list up down)
(unless (zerop (mod pos *board-size*))
(list (1- up) (1- pos)))
(unless (zerop (mod (1+ pos) *board-size*))
(list (1+ pos) (1+ down))))
Schemeだと#<undef>が返ってきちゃうので、ifにしていちいち
() を書いてやらないとならない。
(append (list up down)
(if (zero? (mod pos *board-size*))
'()
(list (- up 1) (- pos 1)))
(if (zero? (mod (+ pos 1) *board-size*))
'()
(list (1+ pos) (1+ down))))
Gaucheではcond-list推奨。これも最近組み込みになってる。
(cond-list [#t @ (list up down)]
[(not (zero? (mod pos *board-size*))) @ (list (- up 1) (- pos 1))]
[(not (zero? (mod (+ pos 1) *board-size*))) @ (list (+ pos 1) (+ down 1))]))
気をつけるのはformatのループ構文を展開しているとこくらいかな。
game-actionマクロは定義と式に展開される。
(progn (defun ...) (pushnew ...))
progn に相当するのはbeginだけど、Schemeのbegin は
「全部定義」か「全部式」ってことになってる。
かわりに、ローカルに関数を作って式を実行してから、作った関数を束縛すれば良い。
(define cmd
(rlet1 cmd (lambda ...))
(cmdを使った式))
pushnewに相当するのがGaucheに無いのがちと残念ではある。
このファイルは実質空っぽ。原著のlazy.lispで作ってる機能は全て
util.stream モジュールで提供されている。
組み込みのLazy sequenceでも書き直せるかもしれないけれど、セマンティクスが ちょっと異なるのでちゃんと考えてない。
ここは機械的に原著のlazy-*系をstream-*に置き換えただけだなあ。
threatened 関数で any?-ec がうまく使えた。
パラメータchosenにnilが渡ってくることがあるのを
どうしようかと思ったけどアドホックに処理してる。
これも特別なことはあまりなし。
コードについて何かあればgithubに直接pull req送ってもらってもいいです。
Tag: Game
Post a comment