『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