ラベル Game の投稿を表示しています。 すべての投稿を表示
ラベル Game の投稿を表示しています。 すべての投稿を表示

2014年1月5日日曜日

Return Of イスカンダルのトーフ屋ゲーム

シリーズ(いつシリーズ化したんだ・笑)「イスカンダルのトーフ屋ゲーム」第3弾です。

まあ、今回は余談的に始めました。OOPスタイルでのSchemeでの「イスカンダルのトーフ屋ゲーム」の作成にかなり苦労したんで、フツーに関数型で書けばどうなるのか、自分で確かめてみたかったんですね。

作成自体は1日あれば終わったんですが、やっぱり慣れもあるし、ロジック的には同じの3回目だから、って事もあるんでしょうが、それでも「関数型で書く」ってのはやっぱ早いです。すぐ出来る。

加えるとテストがラクなんですよね。オブジェクトに対してメソッド呼び出して・・・ってのは、ぶっちゃけ、インタプリタでも書式がメンド臭いんで、結局「テストコード」を本体側に対して記述しないと色々とウザったい。
ある意味、テスト駆動開発ってOOP前提だよな、って気がしました。

なお、今回は原作者のページに置いてあるイスカンダルのトーフ屋ゲーム(ViViScript 版)~プロ仕様~の計算ロジックを参考にしてます。これ(と同じページに置いてあるオリジナル版)が原作者氏の書いたソースだ、って気づかなかったんですよね(笑)。実行形式が端末起動して遊べるようになってたから、まさか今公開してるのがエディタのマクロ言語で記述されてる、たぁ思いもよらなかったです(笑)。てっきりCかなんかで記述してると思い込んでました(爆)。

ってなわけで、初めましょうか。

ゲームに必要な大域変数の定義

まずはゲームに必要な大域変数を定義します。
関数型プログラミングではあまり大域変数使わない前提なのは確かなんですが、計算の過程に於いて、何らかのデータの「参照対象」が必要な場合があります。
重要なのは「参照の為に」定義するんであって、そのデータは絶対プログラム内では書き換えません。
まあ、そいつを引数で与えてやってもイイんですが、後に見ますが、スタイル的にはフツーのプログラミングに於ける「破壊的変更」を対象を新しく生成する事で避けてるんで、どうしても本体の引数が増えちゃうんですね。
ここで「大域変数」として定義した内容は、ゲーム全体に於いて使いまわされるけど、固定した値で変更する必要が生じないモノに限ってます。具体的には、


  1. トーフのプロパティ(値段、製造コスト等)
  2. Game-Overに達する目標金額
の二つです。今までの「イスカンダルのトーフ屋ゲーム」に於けるプログラミングを見てた人には、この二つはゲームを通して不変である、って事に気づくでしょう。
(ちなみに、ゲームに於いて表示される「文字列」も不変な故に大域変数対象になりますが、それは後にPrint部を作る際に定義しましょう。結局トータルでは3つの大域変数が必要となります。)
また、プログラム本体にこの手の「決まった数値」を埋め込むのは避けるべきだ、ってのも良く言われる事です。これらを関数の外側に出しておけば、ソースに手を入れて変更するのもラクですしね。
これら二つをトーフに於いては連想リスト、目標金額は単なる変数として定義します。



これで準備は万端です。ではまずはR->E->Pの順序に則って、まずはRead部から仕上げて行きましょうか。

Read (Parser 関数)

大まかなロジックは前回と全く同じなんですが、クラスに詰め込んだローカル関数を基本的には全部外に出してフツーの関数として再定義する事で、ソースは随分スッキリと読みやすくなります。
Common Lispに比べると、まあ、名前空間の衝突とか避ける為に、割にSchemeでは何でもかんでもローカル関数にしたがる傾向がありますが(実際、外人のCLerに「キミのコードはScheme臭い」って言われた事がある)、確かにローカル関数は場合によってはソースの可読性落としますね。
(ちなみに、基本的にはCLerはSchemer程ローカル関数は使わずに何でも大域的に定義したがるらしい。と言うのも「パッケージ機構」ってのがあって、名前空間の問題を仕様上回避するような仕掛けが施されている。反面そう言う大掛かりなシステムが"仕様上定義されてない"Schemeでは比較的神経質に関数を扱わざるを得ない。)
まあ、ケースバイケースなんですが、前回みたいに「ローカル関数の中にローカル関数定義」とかは多分やり過ぎなんで(笑)、そこまでするなら大域的に定義した方がスッキリするのは確かです。

なお、大まかな仕様は次の通りです。


  1. Parser関数(以降Readとする)は6つの(!)引数を受け取り、その中のphase変数の値を見て挙動を変更する。
  2. 変更される挙動とは...もういいやね、これ(笑)。
  3. 基本的には、引数をそのまま6つの多値として返すが、入力値があった場合にはそれをpnに束縛する。


ではソースです。


だいぶスッキリとして読みやすくなったのではないでしょうか。ローカル関数(letrec)も相変わらず使ってますが、やっぱ一つの関数内にネストが一個程度でやめておきたいもんですね。ローカル関数内ローカル関数は見た目には厳しすぎる。

ところで、実はここで前回までのプログラミングと大きく違う点があります。それは3つあって、


  1. 環境を作る仕組みがない。
  2. 環境は関数に引き渡される引数として表現される。
  3. 従って、ReadにもPrintにも(言わせれば)環境が引数として渡される。
前にPython版から作り出した時、「環境はEvalのモンだしなぁ」ってこだわってたんですが、実はここに来てそんなこだわりは必要がない、って事に気づきました。以前はそこにこだわっていた為、クラス変数と言うポスト大域変数の書き換え、みたいな破壊的操作が必要になってたんですね。
しかしながら、ここに来て、環境を手渡しつつRead、Eval、Printの間でグルグル回す方が良いのではないか、と気づいた。つまり、どの環境の変数に対してどの関数が手を入れるのか明確にする方が大事な気がしてきたんです(言わばバケツリレー方式ですね・笑)。
基本的には環境に対して計算結果を返すのはEvalの役目です。それは変わりません。しかしRead部はどっちにしても入力値を手渡す、って限定に於いては環境に手を入れても良いのではないか。つまり、ここでは「入力情報」がある意味「環境」になっちゃった、って事ですね。
上にあげた6つの引数のうち、 pn ってのが入力値を保持する為の引数です。つまり、何らかの入力があった際には pn は Readが扱う、と言うルールが明確になったのです。

Eval (Interp関数)

前までだとやれこのデータをクラスで定義して・・・だったんですが、今回は全く必要がありません(笑)。サクサクと進みます(笑)。
Eval部も基本的には、前回の「ローカル関数だらけ」を分離したスタイルになっています。お陰で、Eval(Interp関数)が一体何を行ってるのか、明確になる。

  1. Interp関数(Eval)は6つの引数を受け取る。
  2. シンボルで表された変数phaseを見て適する関数を呼び出し、次のphaseを束縛する。
  3. 変数 player はプレイヤーの持ち金である。加算減算が( calc 関数によって)返された場合は変数 player にその返り値を束縛する。
  4. 変数 comp は対戦相手の持ち金である。加算減算が( calc 関数によって)返された場合は変数 comp にその返り値を束縛する。
  5. 変数 wr は天気予報の値である。関数 weather-report の返り値を束縛する。
  6. 変数 pn はReadが読み込んだ入力値である。計算に必要な場合、この値を見て評価するが、pn に対しては特に何も行わない。
  7. 変数 cn は評価に於いて何らかのエクストラの情報が生じた場合(これは要するにPrintが要する情報になるが)この変数に評価結果を束縛する。
  8. 以上の6つを多値による返り値として返す。
さて、こう見てみると、一応Read->Eval->Printループなんですが、内部的にはEvalがスタート地点で、Eval->Print->Readと言うカンジで回っていくのが分かるんじゃないでしょうか。あらゆるデータに関する「評価」は文字通りEval(interp関数)が行っているんです。

ではソースの方を。



やっぱ分かりやすいと思います。Evalが必要とする個々の関数も全部小さいですし、ローカル関数で纏められすぎてるコードを読むよりゃやっぱラクですよねぇ。
一つあるトリックとしては、関数 actual-weather が多値を返してます。一つ目は一種フラグのシンボルで、このシンボルを使ってPrint関数が文字列情報をまとめた連想リストから適する表示形式を探してきます。つまり、変数 cn に束縛させるシンボルを返すと言うのが目的。二つ目はトーフデータが持つ「天気による最大売上個数」を連想リストから検索してて返してて、この結果はプレイヤーと対戦相手の「売上計算」に利用されてその後破棄されます。
このくらいですし、大したトリックでもないですね(笑)。まあ、多値利用しただけだった、ってばだけなんですが、意外と便利です(笑)。

Print 関数

さて、最後に Print 関数です。今回ははえぇなぁ(笑)。
最初に書きましたように、まずは表示に使うメッセージデータを連想リストとして纏めて大域変数としてまずは定義します。
実はこの辺、前回も指摘したんですが、SRFI-48で定義されてるFormatを使用すれば、書式の指定子込みでデータを作れるんで、もっと簡単になるでしょう。
今回もPrintが利用してる関数は、わざわざ整形演算を行う為に書いたモノが殆どで、format 前提なら殆ど本体だけで間に合う、って言っても過言じゃないと思います。


  1. Print 関数は6つの引数を取る関数である。
  2. ただし、印字を出力後、この6つの引数を多値としてそのまま返し、引数に対する操作は一切行わない(これがまさしく印字が副作用的性質である事の証明・笑)
  3. 基本的には、phase 引数を見て適した文字列を連想リストとして設計された message データから検索してくる。
  4. 出力に必要であれば、cn 変数を用いる。
これだけ、です。コードがそれにすれば分量が若干多いように見えますが、もう一度言いますが、SRFI-48でのformatとか利用すれば殆ど Print 関数本体だけで仕事をやっつけちゃえるのです。
(恐らく連想リストの設計さえも、phase と一対一対応で、理想的には phase を利用して検索して表示するだけ、までに構造的には絞り込めると思います。Lispのシンボルの有用性の証明になる気がします。)

ではソースです。



とこれで、Read、Eval、Printの全てのパーツが「関数型プログラミングを使って」組みあがりました。(もっともHaskellな人達は簡単に出力を扱える事に違和感があるでしょうが・笑)
あとはREPLを組むだけ、ですね。

Read-Eval-Print-Loop

REPLは次のように末尾再帰な関数として書いて、実行時に適切な引数をREPL関数に渡す事でゲームが動きます。


まあ、多値を6つも返す関数が3つもあるんで物凄い事になってるんですが(笑)、基本的には簡単ですね。関数REPLは末尾再帰で呼び出された際に、最終的にPrintが渡した値を引数として持って、延々と無限ループへと突入するわけです(笑)。Evalが(exit)命令を出すまでこの動きは止まりません(笑)。

なお、phase の初期値として #f を渡してるのは、初期状態で、 #f はParser 関数( Read ) 本体のcase 構文内で「else」として認識され、そのまま「何もせずに」 interp 関数に全引数を渡すからです。
続いて、interp 関数は phase = #f を見て、やはり本体の case 構文内で else として処理され、ここで初めて、phase にシンボル 'introduction が返り値の一つとして束縛されるわけです。
つまり、初期状態で phase に与える値はゲーム中で使われてないブツなら何でも良くって(あるいは意図的に「途中から」始める事も可能) 結果、伝統的で単純な #f を与えてみました。
ちなみにそうなると、最初の Read -> Print 間は全く何もしてないに等しくて、引数与えるのは「REPLをビックリさせて起動させる」に等しいです(笑)。また、先ほどにも書きましたが、内部的にはE->P->Rの順に処理が進んでいく、ってのは全くそのままの意味です。

非破壊的なゲーム構造

まあ、ちょっとしたシャレで始めたこのヴァージョンだったんですが、面白い事に気づきました。
実は、REPLで末尾再帰が呼び出される際に、与えられる引数はPrintの返り値です。
んで、実はこのプログラムの中では「一回も」普通のプログラミングで言う「代入」は行われていないんですね。つまり「直接変数を書き換える」と言う事は一切行っていない。
特にEvalの動作なんですが、


  1. 引数を参照する
  2. 基本的には「操作」は新しい値を「作る」
  3. 他のケースでは「与えられた引数を参照して」(要するに)コピーしてる
  4. 結果、返り値は元のデータではなく新しいデータである。
この「コピーして」「新しいデータを作る」ってのが言わば関数型プログラミングのキモなんですよね。じゃあ参照に使われた古い引数はどうなるのか、っつーと、参照されなくなった時点でGC(ガベージコレクタ)がやってきて回収・廃棄処分です(笑)。
これは実はReadにせよPrintにせよ、「コピーして新しいデータを作ってる」ってのは同じです。「書き換え」ではないんです。
まあ、こんなこたぁ関数型言語に詳しい人には釈迦に説法なんですが、ちと待てよ、と。こんな事が出来るなら恐らく「非破壊的に」データ生成し続けてゲームを作れるモデル、例えば理論的にはRPGなんかも作れるんじゃないか、って思ったんですね。で、多分それは可能でしょう。
以前、関数型プログラミングは机上の空論なんじゃないか、って実は失望してたんですが、ひょいとしたキッカケで実用上OKなんじゃないか、って思えるようになったのは大収穫でした。
つまり、上のREPLのような関数を設計して、参照する環境として引数(例えばRPGで言うとプレイヤー情報とか)を与えて、内部的にはコピーして元データには一切手をかけない。そうすれば「非破壊的なプログラム構造を持った」関数型による関数型の為の関数型のゲームが作成可能だ、って光明が見えたんですよね。
引き続きこのテーマを研究してみたいと思います。言い換えると、例えばOOPモデルでさえ(プログラミング言語の末尾再帰の最適化が保証されていれば)インスタンス変数を「書き換える」のではなくって「新しいインスタンスを」常に生成するモデルも可能だろう、って事ですから。

では今回の全ソースコードです。



イスカンダルのトーフ屋ゲーム Strikes Back

年末年始かけてイスカンダルのトーフ屋ゲームの実装ばっかやってて一体何をやってるんでしょうか(笑)。

さて、今回は前回と同じゲームなんですが、Schemeで実装してみました。

SICPのデータ抽象型のプログラミング示唆にもある通り、クロージャを使いまくってプログラミングすればどうなるのか、一回やってみたかったんですね。それがOOPの理解も深めるだろう、とか思って・・・。

結果は・・・サイテーです(笑)。
まあ、言語形式もあるんですが、少なくともSchemeでOOP系のプログラミングやるのは頭痛の種以外の何者でもない、ってことが分かりました(爆)。
取り合えず関数型プログラミングとOOPとどっちがマシか、って話は脇に置いておきますが、少なくともLispのようなカッコが多い、と言うかプログラミングコードがかなり深いネストを生じる言語ですと、OOPのコード書くのはキツい、かつ、読むのもキツい、って思いますね(笑)。OOPはPython等のフツーの言語、言い換えるとネストがそんなにキツくならない言語に限って「スタイル的に有効だ」ってのが今回の実験での結論です。
Schemeのスタイル(あるいはLisp一般での関数型プログラミングのスタイル)、ってのは、基本的には


  1. 暗黙のbegin(あるいはprogn)をアテにしない。
  2. 従って一つの関数には一つの処理しかさせない小さな関数をたくさん書く
  3. 小さな関数同士を組み合わせて大きな処理をさせる
って事で、極めてシンプルにプログラムを組み上げる事です。これはUNIX的プログラミングの方策としては王道ですし、かつ、これだと大してネストが深くならないので、カッコだらけでもそんなに読むのはツラくありません。
反面、OOPと言うかデータ抽象型ですと、一つのオブジェクトに如何にしてたくさんのデータと処理をまとめるか、って事になって、一つのdefineされたモノが異様に膨らんでしまいます。もうこうなると理想的なSchemeコードとは言えないでしょう。
ま、てなワケで実験としては有効でしょうが、真似しないようにしましょう(笑)。自分で書いてて「こりゃヒドいコードだ」とかアタマ痛くなってましたから(笑)。

さて、今回の方針です。

  1. 新しいロジックは採用せず、基本的に前回作ったPython版のコーディングを参考にする。
  2. Schemeデファクトスタンダードの仕様、R5RSになるたけ準じて、Racket実装依存の機能はなるべく使わない。使った場合は明示して、他のScheme処理系で動かす場合、最低限の変更でOKなモノを目指す。
  3. 足りない機能があったらSRFIから持ってくる。
の3つです。1番目は当然で、「実験」でスクラッチからロジック考えてればアタマ痛くなってきます(笑)。
2番目もこれがなかなかSchemeだと難しいんですが敢えての挑戦です。どうも個人的な意見では、Webで見つけた面白そうなSchemeコードでも、作者の処理系選択によって、あっちじゃ動くけどこっちじゃ動かん、とか結構腹が立つんですよね(笑)。特に僕が使ってるRacketの場合、Racket実装依存で書くには便利な機能がテンコ盛りなんですが、他のScheme処理系じゃ動きません、っつーのはあまりにもアレなカンジがします。
3番目も初めての挑戦ですかね。SRFIは「共通ライブラリ」とか言われたりしますが、本当は個人的にはあんま好きじゃなくって、と言うのも仕様定義だけで実装は各実装に任せる、と。要するに外部的にライブラリがあって、どのScheme処理系でもインポート出来る、ってポータブルな意味でのライブラリじゃないんで嫌なんです(笑)。まあでも、今回は、少なくともGaucheGuileの2つで最低限の変更で動かせる為にSRFIサポートをチェックしてそのようにしました(Scheme48も考えたんですが、こいつはそもそも使えるSRFIが少ない)。

では始めます。

データ抽象化: 擬似OOP

ところで。「実装依存の機能をなるたけ使わない」「なるべくR5RS内で事を済ませる」となると・・・データ抽象型プログラミングを実現するにはそれなりの機構と自分で実装せなあきまへん(苦笑)。
もちろん、SICPよろしく、全部細やかにクロージャ使って実装していくのも可能なんですが、それじゃああんまりにもメンド臭すぎます。
そこで。
Paradigms of Artificial Intelligence Programmingを参考にして、まずは擬似OOPとも言えるシステムをR5RSマクロで組んでいくのが最初に行う事です・・・ってこれがまた大変だったんですけどね(苦笑)。
ではまずそのコードを。

一応、Schemeと言うかLisp一族に明るくない人の為に解説しておきますが、簡単に言うと、Lisp/Schemeで言う「マクロ」とは、言語に新しい構文規則を付け足す機能です。つまり、Scheme/Lispでは、極端に言うと、C言語で言うdo~whileなんかの「構文」を自分で新しく作れちゃうんですね。
ここではそう言うカタチで新しく「擬似OOP」な機構を作り出してるんです。
んで基本的なコードのアイディアってのはSICP第三章で紹介されてるモノとも同じですね。

ところで、残念ながらSchemeのR5RSで策定されてるマクロはCommon Lispのマクロに比べると遥かに貧弱で、PAIPで紹介されてる実装のスペックには届きませんでした。その一つとして継承機能が組み込めなかった。
もっともPAIPで紹介されてるOOPマクロが継承可能なのは、中で使われてるmessage変数がgensymされてないんで、そこに子クラスの条件ツッコむとcase内の選択肢が増える、と言う極めて危ない実装となっています(笑)。それで考えるとSchemeのマクロが安全なんですけどね。
それから、PAIPの実装だと、クラス定義した時点でメソッドが自動で定義されるんですが、これがR5RS Scheme マクロだと不可能でした。そこでしょーがないんで、外部的にdefine-methodと言うマクロを作って、それでメソッド名をわざわざ打ち込む形式にせざるを得ませんでした。CLOSと普通のオブジェクト指向の間の子のようなスタイルになってしまいましたね。
あと、Peter Norvigによる実装ですと、クラス変数がdefunを包んでるように定義されていて(クロージャで囲まれてる)、このスタイルだとやはりそのままSchemeでは実現出来ません(Schemeだとこの場合ローカル関数扱いになってクラスが外から全く見えなくなってしまう)。そこでPythonのようなクラス変数へのアクセスも考慮して、思い切ってクラス変数は大域変数としてクラス外で連想リストとして定義して、そこへのアクセッサをget-class-var、class-var-set!として定義しています。ちと不格好なんですが、前回見たとおり、使いドコロによっては便利ですが同時に危険なクラス変数へのアクセスは明示的に不便な薫りを漂わせておくべきだ、ってのは結果悪くないんじゃないか、とは思います。
ちなみに、連想リストを使ってる+Racketの厳密なクオート適用のせいで、原作ではフツーの関数だったけど、ここではマクロにせなあかんかったのが何個かあります。


テスト実行は以下のとおりです。




と言うわけで、ちと不格好なOOPモドキではありますが、これらを駆使してScheme版「イスカンダルのトーフ屋ゲーム」を書いていこうと思います。

Parser クラス (Read)

さて、前回は結構勢いだけで作っていって、「どう言う順番で組み立てていくべきか」とかあんま考えてなかったんですよね(笑)。結構行き当たりばったりでした(笑)。
まあ、今回は二回目ですし、また参考に出来るPythonのコードもあるし、ってな事もあるんですが、しかしそれでもどうやらやはり

REPL構造はRead->Eval->Printの順序で組み立てていくべきだ

って事がちと分かりましたね。
考えてみれば当たり前なんですが、情報の流れを追った順序で組み上げていくのが原則で、逆方向に進むとか、あるいはEvalから組み上げてみる、ってのはどうやら具合が宜しくない模様です。
んで、今回もReadに関する指針は次の通りです。


  1. Perserクラスはクラス変数としてphaseを持つ。また、メソッドはinputメソッド(ここではReadと呼ぶ)のみを持つ(正確には「のみ」しか持てなかった・苦笑)。
  2. Readの挙動は「何も入力が無くてもEvalに情報を渡す」「整数の入力しか受け付けないでその情報を含めてEvalに渡す」「Yes/Noに類する入力だけ受け取ってその情報を含めてEvalに渡す」の3種類とする。
  3. 挙動の変更はゲームのステージを表すクラス変数、phaseを参照して行う。Phaseにはゲームステージがシンボルとして束縛されている。
  4. Read は2値を返す。一つ目はphaseの値をシンボルとして、二つ目は入力値、あるいは入力されない場合は #f (Schemeの偽)返す。
以上の3つです。基本的には1つ目も2つ目も前回のPython版と同じです。
違う点と言うと、まずは、phaseに束縛されてるシンボルと言うLisp族特有のデータ型なんですが、Pythonでは文字列を返した代わりに使っています。
もちろんゲームステージに対して番号ふる処理にしてもいいんですが、やっぱり組んでる人間としては「どのステージでどう言う処理を」って明示された方が分かりやすいんですよね。しかしながら、一般に文字列情報は比較等の演算で時間がかかると言う欠点があるんです。
反面、Lisp族のデータ型シンボルとは、評価過程に於いて唯一無二と言う事が保証されていて、ポインタ比較だけで何の情報が渡されたのか、とすぐ分かるんですね。理論的には文字列を受け渡ししているPython版なんかより遥かに高速に比較処理してくれます。これが大きいです。
しかもプログラム組んでる人間にもどう言う情報で・・・と言うのが一発で分かりますし、Lisp内に於いては数値比較よりも高速だったりします。至れり尽くせり、ああシンボル万歳、たぁLisp好きの合言葉ですね(笑)。
あとは、3番。前回のPython版は二つの情報をまとめた「タプル」と言うデータ型でEvalとやり取りさせてたんですが、このScheme版では本当に二つ値を返しています。これも一部のLisp(特にCommon LispとScheme)では有名な不思議な機能、多値関数と言う機能です。手続きvaluesで包んでやるとあら不思議、いくつもの値を一気に返せるんですってよ、奥様!
まあ、実際はここで使う必然性は実はあんま無かったんですが(笑)、何となくやってみたかっただけです(笑)。実際ホントの事言うと、リストとか便利過ぎて、通常あんま多値の出番がないんですよね(笑)。

まあ、てなワケでコードを見てみますか。ぶっ飛びますよ(笑)。


さあて、どうでしょうかこれは(笑)。
まずは予告してた通り、Read自体がデータ抽象として設計されてる為、一つのSchemeの手続きとしては明らかに長すぎる印象があるんじゃないでしょうか。つまり、印象的には何でもかんでも詰め込み過ぎてる関数に見える、って事ですね。
加えると、PythonのOOPの場合はselfがあるんで、メソッドをクラス内でバラバラに定義してもお互いに参照可能でした。一方、まあ、今回実装したクラス定義があまりに貧弱だ、って事もあるんですが(笑)、クラス内で定義されたメソッドにクラス内で定義したメソッドは基本的にアクセス出来ません(笑)。自分でやってて「何じゃこりゃ?」とか思ったわけですが(笑)。
詳しく説明すると、define-methodで定義したメソッドはインスタンスを通じてしかメソッド本体にアクセス出来ないんです。

メソッドの書式:
(メソッド インスタンス 引数 ...)

ところが当然、今のように「クラスで定義してる最中にメソッドへのアクセスが出来ない」、つまり内部的に参照しあえない場合は・・・そうですね、メソッド内部でローカル関数として必要な機能を定義してやるしかない、って事になります(だから上のコードはletrecだらけなんです)。
いや、今までPythonのself嫌いで文句ばっか言ってたんですが(笑)、今回初めてPythonのself機構が好きになりましたね(笑)。自分で実際OOP作ってみて分かるその偉大さ(笑)。ああ、そう言う理由でああなんだ、とか実装してみて初めて分かる事ってあるんだなぁ、ってカンジです。
もっとも、良く言われるんですが、PythonでのOOP機構は全部パブリックなんで危険だ、って話もあって、それで言うとプログラム全体で必要な機構をメソッドで外部に出して、他の内部用演算はローカル関数として外部から隠す、と言う上のような書き方の方が安全、って言えば安全かもしれません。ただし見た目の酷さを除けば、ですが(笑)。

取り合えずテストしてみましょうか。実はこれがちょっとしたトラブルがあるんですが、まあ、大まかには予定通り動きます(笑)。

とまあ、最初の方はテストが通るんですが、途中でRacketがおかしな挙動になるんですよね(苦笑)。成功する場合もあれば失敗する場合もある。なんじゃそりゃ(笑)。
実はRacketの場合、(Lisp族にしては)ゴージャスなIDE付き、って事もあって、なんか特にReadで呼び出される入力機構が立派過ぎるんです(笑)。多分その関係で負荷が高く、平たく言えばこの手の入力テストやると(current-input-port)なり(current-output-port)のflushが上手く行かないんじゃないでしょうか。PLT Schemeの頃はこんな事無かったんだけどな(苦笑)。

まあ、取り合えずテストは通った事にして先に進みますか(ヲイ

Player Class / Computer Class / Tofu Class / Weather Class

先ほども書いた通り、REPLは大まかにはR->E->Pの順序で組み上げて行った方が良い、と言いました。
実は、結構な確率で良くある「LispでLispを作る」例ではこの原則が守られてないんですよね。と言うのも、大体のケースでは「ReadとPrintはLispのそれらを流用する」ってのが前提で、SICPやPAIPなんかの本もその全精力を「Evalの構築」に注ぎます。だから実際にREPLモデルを組む際の順序、と言うのには無頓着なんです。例えばPAIPの著者、Peter Norvigが書いたこのページなんかもそう言う意味ではグチャグチャで、やはり読み込むデータをどう言う風にパーズして渡すのか、決定してからEvalを書いた方が本当は実際的なんじゃないか、って思います。もっとも、本当はそうやってても、テキストを書いた時点で順番入れ替えてるのかもしれませんが。
んで、Eval作成時時点でEvalで使うデータから作っていくわけです。基本的には評価機構がEvalである以上、扱うデータのその殆どは(Printで扱う文字列を除き)Eval絡みになるんじゃないでしょうか。
そんなわけで、まずは(Python版と違って継承が無いので)プレイヤーを表すPlayer Classと対戦相手を表すComputer Classとをそれぞれ、そしてトーフを表すTofu Class、天候を司るWeather Classを作成します。




この辺はイイですよね。ロジック的には前回のPython版とほぼ同じですし、シンプルです。フツーに構造体が使える言語でも大体近いカタチで落ち着くんじゃないでしょうか。

Environment Class

で、上記で定義したクラス群を内部に保持する為のEnvironment Classを定義します。これもシンプルに定義出来ますね。




これもロジック的には前回とほぼ同じなんでまぁ良いでしょう。ただし、各インスタンスにアクセスするアクセッサもここで定義しておきます。
テストコードの実行結果は以下の通りです。



Lispらしくテストコードでマッピングとかしてみたんですが(笑)。しかし、メソッドの性質によって、どっちがどっちにマッピングしてんだかイマイチ分かりづらいですね(笑)。構文的には逆になってるように見えます(笑)。
ちなみに、例えば(get-player e)ですとこの場合、平たく言うとPlayer Classのインスタンスである、実際のplayerにアクセスしています。要するにplayer変数にアクセスしてるんですね。そしてそれが持ってるインスタンス変数を全部表示するようにして、問題無く各値にアクセス出来る事が分かります。
と言うわけで、いよいよEvalの実装です。

Game-Master Class (Eval)

Game-Master Class(Eval)の大まかな仕様は次の通りです。


  1. インスタンス変数として環境(env: Environment Classのインスタンス)を持つ(もう一つは、YES/NO入力に対しての挙動を変える為のStrange Flagってのをフラグとして持ってますが、これはロジック的には本質的なモノじゃないです)
  2. 唯一、interpメソッドを持っている。これが普通の意味ではEval本体に当たる。
  3. interpは二つの引数を持つ。一つはphase情報、もう一つはReadから渡された入力値、あるいは#fである。
  4. interpはphase情報に従って評価を下し、また次のphase(ゲームステージ)をParserクラスのクラス変数としてセットする。
  5. interpは出力指示情報と計算結果(あれば、あるいは#f)の二つを多値として返す。

とまあ、書く分にはやっぱ簡単で(笑)、特にphaseとして渡されてきたシンボルをcaseで事業仕分け、もとい(古い・笑)適した関数呼び出せばイイんですよね。ロジックは簡単なんですが・・・。
やっぱりPython的にselfで自分のメソッドを呼び出せないんで(笑)、結果としては鬼のようなローカル関数使いまくり、って形式になっちゃってます。

ではソースです。



何でしょうね、これは(笑)。ローカル関数内にローカル関数があったりして、たまったモンじゃないです(苦笑)。だから言ったでしょ、フラット(インデントレベルが深くなり過ぎない構文を持つ)に書ける言語にはOOPは合いますが、そもそもこう言う「ネスト大好き!」な言語にはOOPは向かないんですって(笑)。書いた本人でも「何書いてたっけ」って迷子になりました。Python以上ですね(苦笑)。

ではテストコードの実行です。


コードが長い割には結果はシンプルだと言う・・・(笑)。
ま、いっか(笑)。んで、返り値の二つは次のPrint クラスへと手渡されます。

Message Class (Print)

さて、残るはPrint部分だけです。前回のPython版では、ロジック的には「整形表示」を司ってただけでいっちゃん簡単でした。
が・・・・・・。
R5RSでdisplayだけ頼りにして整形表示を自前で計算させる、って無謀な事をやっております(爆)。しまったな、SRFIからformat引っ張ってくれば良かった(とか思っても後の祭り)。
まあ、SRFIを読みこめば読み込む程実装依存度が結果高まってしまうんで(先にも書いた対応処理系自体の事情)しょーがないですか。まあ、formatさえあれば、ホントもっと短く済みます。ホントロジック的には大した事ないんです。

大まかな仕様は以下の通りです。


  1. Message クラスは data と言うインスタンス変数を持ち、それはゲーム中で使う文字列からなるデータに束縛される。より正確には、文字列データをシンボルによりラベル付けされた連想リストがインスタンス変数 data となる。
  2. Message クラスは唯一 Print メソッドを持つ。
  3. Print メソッドは2つの引数をEvalから受け取るものとする。一つ目はどの表示をするか、と言う情報をシンボルで受け取り、二つ目は Eval が出した何らかの評価結果、あるいは #f である。
  4. 評価結果を利用した表示を行う(あるいは#fが入ってきた時は使う必要がない)。

以上です。簡単ですね。
ではコードを。

ご覧の通りです。
デカくなってる理由は仕様にも書いた通り、ゲームで扱う文字列情報を連想リストとして持ってる為、また、R5RSに準じると、文字列整形用に指定子等が使えない為、ローカル関数の殆どは「整形用の演算」を行ってるのです。先に書いた用に、SRFI辺りからformatを持ってくるとかすれば殆どの処理は書かずに済むでしょう。
(例えば数値表示をケツで揃える為、頭に何個スペースを入れるか、なんて計算したりして、自分でやりながら笑ってました・笑)

ではテストコードです。


おお、やっぱ表示が出てくると一気にゲームっぽさが出てきますね。ここに到達するまでが大変で、それまではテストコードなんか打っても無味乾燥な値が返ってくるだけですしねぇ(苦笑)。
まあ、もちろん説明するべくもないでしょうが、このテストコードに引数として与えた値は、実際Evalが返すだろう事を想定した値となっています。
では、いよいよREPLを組み上げます。

REPL(Read-Eval-Print-Loop)

「組み上げる」とは言っても、ここまで来ると大した仕事は残ってないんですけどね。
以下にREPLのコードをあげておきます。



基本的には大したこたぁないです。
注意点は、今回はRead、Eval、Print、の3つとも多値(2値)を返すメソッドとして実装されてるので、SRFI-11のlet-valuesと言う機能を用いてそれら多値を束縛して次のメソッドに渡すようにしています。
まあ、一番最初の方で書きましたが、Schemeプログラミングに於いてあんま多値使う局面ってそんなに無いんで、使わないで、例えばリストなんかで結果を返しても良いんですが、一方、consは結構計算コストがあるんで、そう言う意味では多値を使う事である程度の「速度向上」は見なせる模様です。

イスカンダルのトーフ屋ゲーム (Scheme 版) のソースコード

では、全部のソースコードを改めて貼っつけておきます。


こう見てみると各クラスは大きいわ、長いわ、でサイテーですね(笑)。まあ、コメントなんかもありますが、600行近くあります。
「Schemeらしく」関数型でプログラミングすればもっと縮まるんじゃねぇの、ってんで、実は既に関数プログラミング版作ってみたんですが、案の定 2/3 程度に圧縮されました。しかももっと綺麗(笑)。破壊的操作が一切ありません。
やっぱLisp系には少なくともOOPは向かねえんじゃねぇのかなぁ。良く、グラフィックスに関しては昔はLispが優れてた、とか言いますが、OOP前提での今の世の中ですと、昔日のアドヴァンテージはあんま持てないんじゃないでしょうか。OOPで書かれたライブラリとやり取り(特に多重継承)して、CLIをGUIに簡単に変更、ってなわけにゃ行きそうもないですから。
まあ、あくまで個人的にOOPをもっと理解したい、って為の実験ですし、OOP使うならPythonで書いた方がよりシンプルっぽいですね。

ちなみに、今回も実行形式作ろうってトライしてみたんですが、Racketのコンパイラが文字コード周りにバグがあるらしくって、日本語でコンパイルすると作成されたexecutableがすぐ落ちちゃうんですね。
そんなわけで、今回は実行形式は無し、です。

2013年12月29日日曜日

イスカンダルのトーフ屋ゲーム


スクリプトとプログラムの個人的定義
良くスクリプトとプログラムって何が違うんだ、みたいな話があります。基本的にはどちらも似たようなモノなのは確かで、一般的には両方ともプログラム、です。良く

「小さい、一つの事しかしないようなものがスクリプトで、大掛かりになるとプログラム」

なんて言われますよね。
僕個人は、最近の考えだと、REPL(Read-Eval-Print-Loop)を持ってるのがプログラム、そうじゃないのがスクリプトなんじゃないか、と思ってます。
つまり、端末かなんか起動して、命令して、そしてまた端末に戻るのがスクリプト、端末で起動したら終了命令が下されるまで端末上で入力を受け取って計算して結果を出す。それをずーっと行うのがプログラムなんじゃないかな、って事です。
まあ、これは個人的な考えなんでそれはそれで良いでしょう。

GUIアプリのMVCデザインパターンとREPL

GUIアプリを書こうとするととにかくややこしいです。どうにもすぐスパゲティ化してしまいます。
このスパゲティ化を避ける為に色々デザインパターン、要するにプログラミングの指針が提案されていますが、その中にMVCと呼ばれるモノがあります。


上のWikipediaの記事を見ると気づくのが、MVCと言うのはインタプリタの構造、REPL(Read-Eval-Print-Loop)と全く同じだと言う事です。
REPLは、例えば情報工学の過程に追いて、多分、結構大学卒業年次辺りでのプログラミング言語実装課題でインタプリタを実装しましょう的なネタで使われて、平たく言うと机上の理論だし(笑)、ついでに言うとCLI(Command Line Interface)用の実装ネタでもあり、あまり実用的なネタじゃない、と思ってた(あるいは思われてる)んですが、ここに来て、インタプリタの実装経験「こそが」良いGUIアプリケーションを書く技術を養うんじゃないか、ってちょっと視点が変わってきたんですね。
つまりこう言う事です。結構プログラマ(特にUNIX系)はGUIアプリが嫌い、使うのも嫌だし作るのも嫌、って人が多いみたいなんですが、指針としては

  1. REPLをキチンと分離して備えたCLIアプリケーションを書く
  2. GUIツールを持ち込んで多重継承を使い、Read+GUI->Controllerと言う継承、Eval+GUI->Modelと言う継承、Print+GUI->Viewと言う継承を行う
と言うプロセスを踏めば、CLIのREPLを備えたプログラムを簡単にGUI化出来るのでは、と踏んだのです。
まあ、そう言うプランがあればラクなのでは、と言う予感がしてたんですが、まずは何はともあれ、CLIで「アプリケーション」と呼べる至極単純かつきちんとREPLを持つプログラムをOOPで作れないかな、ってネタを探していたわけです。

何故OOP?

個人的にはOOPが嫌い、少なくともあまり得意じゃない、知らないことも多いし、と思ってたんですが、取り合えず一個OOPで書ければ自信に繋がるかな、と言うような事は考えてました。
それはともかくとして、GUI系のツール(wxWidgetsとかQtとか)がまずはOOPで書かれているツールなんで、そいつらとのやり取りが絡む以上、基本的にはOOPで書かないとダメ、って事がまずあります。
それと、先ほどにも書きましたが、REPLを構成するRead、Eval、Printがそれぞれクラスで定義出来たら、多重継承を使ってGUIツールが提供するクラスと融合させてModel、View、Controllerクラスを作る、と。
多重継承はあまり使うべきではない、って話も聞きますが「ここ一回限り」多重継承するならそれはパワフルでシンプルにGUIアプリ作成出来る事を意味するんじゃないか。
そのためにはCLIのプログラム自体がOOPで作ったパーツから組み上げられてないとダメだろう、って事ですね。

レモネードスタンド
とまあ、アイディアはあるんだけど、じゃあ何実装してみるか、ってのが思いつかない時期が続いていたわけです(笑)。
あんまり複雑じゃなくて、でもキチンとしたアプリケーションの体裁はあって、上記のアイディアを使えるネタ、ってのが何かねぇかなぁ、と、ずーっとボーっと考えていたわけですが(笑)。
んで最近、

「あ、昔のBASICで書かれたゲームとかをPythonで実装してみりゃエエかもしんない。」

とか思いついたわけですね。これなら単純だからOOPに直しながら実装するにせよ、手間はいきなり本格的なアプリケーション書くよりはラクだろうし、遊べるし、昔は端末上で動いてたわけだからCLIのREPLにすることが出来る、って条件もクリアしています。
それでなるたけ初期のパソコン黎明期でのBASICで書かれた単純なゲームを探しました。なおかつ、どっかでソースコードが公開されてるものがイイ。
そんなんで、見つけたのは、昔のApple ][でのゲームで次のような有名なゲームです。


日本では通称レモネードって呼ばれてるんですが、パソコン黎明期では非常に人気があった模様です。
1988年出版の電視遊戯大全と言う本には次のように紹介されています。

BASIC言語で書かれた「レモネード」。
日本では「イスカンダルの豆腐屋ゲーム」
として紹介された。
ほうほうなるほど。日本で紹介されたんなら日本でソース公開されてるかもしんねぇな、とか思って探してみたんですが・・・。

イスカンダルのトーフ屋ゲームはレモネードスタンドじゃなかった!

意外とアッサリと、現在でもイスカンダルのトーフ屋ゲームを公開してる作者さんのサイトを見つける事が出来ました。
ところがここに驚くべき記述が成されてたんですね(笑)。

当時 Apple ][ にレモネード屋さんを経営するゲームがあって、トーフ屋ゲームはそれを真似したのだろうという記述を時々みかけるが、それは間違い。おいらはそれを言われるまでレモネードゲームのことを知らなかった。(今もレモネードゲームの詳細は知らないし、たぶんプレイしたこともない)
何と、そうなんだ(笑)!
確かに上にリンク貼ったレモネードスタンドのゲーム内容とイスカンダルのトーフ屋ゲームは似てはいますが全く違いますね。レモネードスタンドの方がゲームとしては遥かに複雑です。
違う点を上げると


  1. レモネードスタンドはレモネードの販売価格を自分で決定出来る。
  2. 売るための広告費をかけることが出来る。
  3. 対戦相手はいない。
等です。「経営戦略シミュレーション」と言うならレモネードスタンドの方で、一方、イスカンダルのトーフ屋ゲームはどっちかと言うとやっぱあくまで「対戦用」アプリです。
恐らく、レモネードスタンド = イスカンダルのトーフ屋、って誤解はそれこそ、上にあげた電脳遊戯大全の記述ミスから広まったものじゃないでしょうか。
ところで、どっちがプログラムとしてシンプルなのか、と言うと恐らくイスカンダルのトーフ屋ゲームの方がよりシンプルで、今回のお題には適しているようです。
と言うわけで、Python2.7で、OOPで、REPLモデルとしてイスカンダルのトーフ屋を実装してみます。
方針としては次のようにしました。

  1. オブジェクト指向をフルに使って関数等は用いない。
  2. 入力と出力は完全に評価機構から切り離す。
これはtwitterなんかでオブジェクト指向な言語(例えばC++)を使いこなしてる人達からすると甚だ極端な方針らしくって、特に本当にデータ駆動型プログラミングで入出力を完全に切り離す、と言うのはあまり直感的では無い模様です。ただまあ、一応実験だし、先にも書いた「GUIアプリへのコンバート」を考えると極端でも行けるトコまで行ってみよう、と思いました。
なお、イスカンダルのトーフ屋ゲームの作者さんのサイトの記述ですと、ソースが公開されてる、って書いてたんですが見当たらなかったんで、中の計算ロジックは、次の山之内案山子さんが作成されたソースを参考にさせて頂きました。

イスカンダルのトーフ屋ゲームとは?

では実際にプログラムしていく前にゲームの詳細(って程じゃないけど)を見てみましょう。
原版ではゲームを開始する際にゲームの説明をするかどうか訊いてくるんですが、その文章でどんな感じのプログラムになるのか、想像が付くと思います。
以下がその解説文です。

ここはイスカンダル星。あなたはここでトーフ屋を経営し、
地球への帰還費用を作り出さなくてはいけません。
でもお向かいには、コンピュータが経営するトーフ屋があります。。。
トーフの原価は1個40円、販売価格は50円です。
1日に売れる個数は天候に左右されます。
トーフは日持ちしないので、売れ残った分はすべて廃棄します。
そこで、次の日の天気予報を良く見て、何個作るか決心してください。
所持金5千円からはじめて早く3万円を超えた方が勝ちです。

とまあ、これが仕様(?)です。
でははじめましょうか。

実行: eval (GameMasterクラス)


なお、本当はこの順番で書き上げて行ったわけではなくって、行ったり来たりかなり自分で書いてるソース内で迷子になってました(笑)。OOPはデータの割り振りが難しくってアッチコッチに飛ぶんで閉口してました。基本的には、やっぱ機能(関数やらプロシージャ)で分けて書いた方がラクだと思いますね。OOPは一筋縄じゃいきまへん。
取り合えずはゲームの心臓部eval(GameMasterクラス)の定義から。


実装方針としては、

  1. 初期化時点でゲームに使う環境をセットする。(self.env)
  2. 入力自体は扱わない。仮引数xに手渡された情報を元に計算する。
  3. readから仮引数xに渡される情報は現状況と入力結果(あるいはNone)の2要素のタプルとする。
  4. 出力自体も扱わない。出力クラスに現状況と計算結果(あるいはNone)の2要素のタプルを渡せるようにEvalを設計する。
  5. 各条件節で行う計算は一つ(つまり、出力クラスに渡す情報は一つ)のみに仕上げる。とにかく短くしてすぐ出力クラスに渡す。eval内でもたつかない。
とかまあ、簡単に書いてますけど、実際は紆余曲折あって泣きそうでした(笑)。
最初は環境も大域変数にして、一種のフラグとして書き換える形式で作り出してたんですが、どうもOOPやってる割には綺麗じゃねえな、ってんで、最終的には環境(つまり、プレイヤー情報、対戦相手のコンピュータ情報、トーフの情報、ゲームオーバーになる得点)はこのGameMasterクラス内に内包する事で落ち着きました。
また、プログラミング言語のインタプリタですと環境が重要なのはevalだけ、で、基本的にはパーズは噛ますんですが、read機構は何でも受け入れてしまうわけですね。
一方、ゲームの場合、例えばこのイスカンダルのトーフ屋ゲームですが、整数値だとか、Yes/No形式の二種類の入力(と何も入力が成されない/必要ないと言う情報も合わせれば三種類)を「現在のゲームの進行状況に合わせて」切り替えないといけません。つまり、Read自体がゲームの進行状況をある程度把握してなきゃならないんです。一方、状況を把握するのは切り分けだとあくまでevalなわけで・・・。うううううむ。
ってなわけで、あとでreadのコードも見せますが、read内でクラス変数としてphaseと言うフラグをセットして、evalが実行された際に「次のフェーズはこれになるよ」と言うのをreadに教える事にしています。苦肉の策です。
なお、クラス変数ってのは一種の大域変数のようなモノで・・・各インスタンス共通の変数として大本を変更すれば全インスタンスも影響を受ける、と言う大変危険な変数です(笑)。しかし、ここが使いドコロで、ここでこれ出すしか無かった(苦笑)。あんまこう言うアクセス手段って綺麗じゃないんですけどね。しかしながら大域変数使いたくねぇ、ってのならこれしか手がない。

ちなみに、evalの各節を実行した例は次のようになっています。

何が何だか分かりませんね(笑)。取り合えず2要素のタプルを基本的に返してて、第一要素は現在のゲームの状況(フェーズ)、第二要素は何らかの計算結果になってるかあるいはNoneか、ってのを把握して頂ければ良いと思います。
(しかし、こうやってテスト実行するのがかなりメンドい辺りがやっぱOOPの欠点だと思う)

Environmentクラス

次はevalが初期化に用いる環境クラスを定義します。
ゲームも言語インタプリタと同じで、情報を環境として保持していなければなりません。環境とは、例えばこのゲームの場合、プレイヤー情報(いくらお金を持ってるか、とか)、対戦相手のコンピュータの情報、トーフの情報(製造コストはいくらか、売値はいくらか、とか)、GameOverに達するお金の額とか、ですね。
これらの情報の保持は、要するに言語インタプリタに於いて、例えば実行した変数なり計算結果なりの保持であるとか、あるいはどんな関数が使えるのか、とかの記憶と全く同じです。
ただし、幸いなのは、言語インタプリタと違って、例えばレキシカルクロージャを作るように仕掛けるとか、そう言った技術的に面倒な面は全く無いんで、そう言う意味ではラクです。
では、Environmentクラスの定義です。

初期化だけで終わるくらいに簡単な実装です。
なお、UserDictionaryクラスを継承してますが、これはEnvironmentクラスのインスタンス変数を参照する際に、辞書(ディクショナリ)みたいに[]を使って参照出来てラクだろう、ってんで使っています。
(しかし実際出来たコードは辞書だらけみてぇになってあまり見やすいとも思えない・・・ orz)

このように、Environmentクラスのインスタンスは、辞書(ディクショナリ)データと同じようにインスタンス変数にアクセス可能です。
この例では、プレイヤー、コンピュータ共に持ってる初期金額は5,000円、作ったトーフは0個、トーフの売値は50円、製造コストは40円、天候によって売れる最大個数、あとはゲームオーバーに達する目標金額は30,000円に設定されてる、って事が分かります。
で、Environmentクラスにぶち込んでいるインスタンスの製造元であるクラスを見てみましょう。

TofuSellerクラス/Playerクラス


まずはゲームの主体であるプレイヤーの振る舞いの基礎になるTofuSellerクラス、それとプレイヤーを演じるPlayerクラスから見てみます。

TofuSellerは初期状態(初期金額5,000円、製造したトーフが0個)を保持して、何個トーフを作るか命令を出すメソッドを持っています。Playerクラスは単純にそれをそのまま継承しただけです。
ちなみに、ここがOOPの難しいトコなんですが、ここではTofuSellerクラス/Playerクラスに「トーフをいくつ作るか決定する」属性を付加してるんですが、どうなんでしょうねぇ。こう言う「計算」はevalが持つべきなのか否か・・・・・・。
世の中オブジェクトだらけなんで、オブジェクトで考える方が自然だ、とOOP信奉者はいいますが、なかなかどうして。こう言うデータ駆動で、「あるメソッドを何に属させるべきか決める」ってのはそんなに自明だとは思えません。
例えば「計算機メソッド」ってのを定義して、これがゲームマスターが持つべきかプレイヤーが持つべきか、ってのはこれはどう考えれば良いのでしょうか。両者とも持ってるべき?しかし機能中心主義なら機能だけ定義すれば良いんで「属性」とか考えなくって済むんですよね。この辺OOP信者の言う事はちと理想論な気がします。
まあ、あまり考え過ぎなきゃいい話なんですが、こだわれば厳密なトコではオブジェクトが何を意味すべきか、やっぱり分からなくなっちゃうんですね(笑)。
ちなみに、ここでもインスタンス変数へのアクセスを辞書っぽく行えるようにUserDictを継承しています。

Computerクラス

次は対戦相手のComputerクラスです。こいつもTofuSellerクラスを継承して作ります。

makeTofuメソッドはオーバーライドします。と言うのもコンピュータは天気予報を見て自動的にいくつトーフを作るべきか計算するからですね。AIと言う程じゃあないんですが(笑)、こう言うロジックでいくつ豆腐を作るか計算します。

Tofuクラス

んでお次はトーフを表すTofuクラス。何かこれが一番オブジェクトっぽいな(笑)。

ここでは初期化条件として、トーフの売値、製造コスト、天候によって売れる最大数がまずはインスタンス変数として定義されていて、そしてプレイヤーの持ち金から最大いくつトーフが作れるのか返すmaximumメソッドが定義されています。
まあ、このmaximumメソッドも本当はPlayer/Computerクラスが持つべきなのかevalが持つべきなのか(ry
一応ここではトーフの最大個数はトーフが決める事にしております(強引)。

Weatherクラス

GameOverに達する金額はわざわざクラスにする必要もないんで、そのままEnvironmentクラスに突っ込んでます。ここではもうひとつ、このゲームに重要な「天候に関する情報」を司るWeatherクラスを見てみます。
Environmentクラスは初期状態ではWeatherクラスのインスタンスは持ってないんですが、ゲームが進んで行くにつれて辞書的技でWeatherクラスがGameMasterクラスのインスタンス変数(self.env['weather'])として代入されます。
んで実際問題、1ターン(全フェーズ消化)につき1回、Weatherクラスのインスタンスは代入され続けるわけです。
と言うのも、Weatherクラスは中に確率マシンを仕込んでまして、初期化に於いて一回「天気予報」を計算します。これは固定されて変更されません。つまり1ターン内には使いまわされるわけですね。
そしてもう一つの大事な機能が「実際の天気を決定する」機能です。これは予報結果を閾値として用いてまたもや確率計算して天気を決定します。
まあ、大まかにはそんなトコで、取り合えずコードを見てみましょう。

これもUserDictを継承してるんで(UserDict大好きだなぁ~・笑)、天気予報の各数値(%)に辞書的形式でアクセス出来ます。メソッドreportは天気予報情報を丸ごと辞書形式で返してくれます。また、resultメソッドは実際の天気を決定して、晴れ/くもり/雨の文字列を返すようになっています。

ちなみに、全フェーズとは、eval基準で見ると基本的には

'input-numbers' -> 'next-day' -> 'test' -> 'input-numbers'へ戻って繰り返し

と言う3フェーズの事で、Evalを延々とこの3フェーズでloopさせるわけですが、要するにこの3フェーズ内で必ず一回self.env['weather']にWeatherクラスがインスタンス変数として代入されるわけです。

構文解析: read(Parseクラス)

まあ、構文解析って程じゃないんですがね。ただ、先程も書いた通り、ゲームのread機構なんで言語インタプリタには無い特徴があります。
このゲームの場合、次の3つが要点です。


  1. ゲームの全てのフェーズに於いて入力が必要なわけではない。従って、多くの場合では呼び出された時点で自動的に何らかの返り値をevalに渡してゲーム(あるいはloopを)進めないとならない。
  2. 整数だけしか受け付けないフェーズがある。
  3. YES/NO(あるいはyes/noとか)やY/N(またはy/n)しか受け付けないフェーズがある。
さて、これが一番困ったトコなんですよね。
先ほども書きましたが、言語インタプリタの場合、パーズはしますが(その結果、例えば入力エラー等をユーザーに教える)、原則的には「どんな入力も」取り合えずは受け入れます。つまり、evalみたいに環境を参照する必要が無いわけです。
一方、このようなゲームの場合、read自体が「フェーズによって」挙動を変えます。いや、変えないといけない。ゲームが要求する入力だけを受け、違反な入力等を過度に避ける為にはそうするしかない。従って、環境を直接参照する事が無いにせよ、何らかのカタチで「現フェーズの情報」を持ってないといけないんですね。
しかしながら、フェーズ自体は原則的にevalの為のモノで、かつevalで決定されるべきモノです。果たしてevalが計算する前にreadが次のフェーズを知る事なんて出来るのか・・・・・・?

これは古典的なプログラミングに於いてはまさしく大域変数で解決すべきだろう問題です。つまりフェーズ情報を大域変数として束縛しておいて、随時それを参照する・・・・・・。
実はこの初期のプログラムだとそう言うカタチで作ってたんですが、どうもそれだとスッキリしなかったんですね。eval(GameMasterクラス)内部の定義が大域変数塗れになってどーにもこーにも汚ねぇな、と(笑)。しかもそんなプログラムだとOOPやってる意味が無い、とまでは言わなくても相当薄れるんですね。
そこでその辺を何とか解決した(あるいはしようとした)のが次のコードです。

Parserクラスでは最初にクラス変数phaseを定義しています。これがこのプログラムのキモです。
クラス変数は初期状態では'introduction'に設定されてます。このクラス変数にアクセスするとこのクラスから作られた全インスタンス内のクラス変数は全部変更されるんですね。
ちと試してみますか。

最初にParserクラスから6つ個別にインスタンスを生成しています。OOPの理屈から言うと、この6つのインスタンスは「互いに独立じゃないとならない」前提なんですが、あら不思議。クラス変数に関して言うと親元のクラスで変更すると生成された全インスタンスのクラス変数が変更されてしまうのです(と言うか、それこそがクラス変数の「機能」なんですが)。
これは見るからに「危険な機能」ですね(笑)。ある意味大域変数と同じで、そもそもOOPでクラス作成してインスタンスを生成するのは「個別のデータの独立性を高める為」な筈なんですが、このクラス変数の存在ってのはそれで言うと「綻び」です。しかし、こう言うお題の場合は役に立つ。
なお、readとeval間のフェーズの伝達は次のようになってます。


  1. evalが実行される度に、「次のフェーズ」をParserのクラス変数phaseにアクセスしてセットする。
  2. readは自らのクラス変数phaseを参照して挙動を変える
  3. readは入力された/されないに関わらず、自らのクラス変数phaseの値と入力値/Noneの2要素をタプルとしてevalに渡す
  4. evalはタプルの第一要素を見て現在のフェーズを知る
つまり、フェーズと言う情報に関してはevalとreadの間でグルグル回ってるわけです。
なお、readメソッドは次の挙動を実装しています。
  1. 何も受け取らずにフェーズ情報とNoneのタプルを即座に返す。
  2. フェーズ情報に従って整数値だけを受け取り、フェーズ情報と入力された整数値のタプルを返す。
  3. フェーズ情報に従ってYES/NO及びそれに類する文字情報だけ受け取り、フェーズ情報と真偽値のタプルを返す。
3番目の機能の実装はCommon Lispの大変アホな便利な関数、yes-or-no-pの名前を借りてきてます。これはCommon Lispよろしく、入力された文字列を一旦大文字に変換して比較するようにしてますね。これって地味な関数なんですが、Common LispみたいにPythonにもデフォであったらイイのになぁ関数であります。
Parserクラスのインスタンスの実行例は以下のような感じです。

ご覧のように、クラス変数で与えられた情報に従って挙動が変わります。入力を必要としなかったり、あるいは入力値が期待されたモノじゃない場合、新たな(正しい)入力が来るまで待ちが入ります。

Messageクラス

これで最後です。Messageクラスはゲームで表示する文章を保持し、またREPLの要素の最後の一個、Print機能を司ります。
ここは基本的に出力だけなんで、文章データを含むから長いんですが、整形出力だけなんでコーディング的には大した苦労は無いパートですね。見てみましょう。

個人的にはゲームで良くあるwait処理(Pythonだとtime.sleep)が絡むトコの処理がもっとシンプルに書けないのか悩んだんですが・・・この辺関数的に綺麗に書こう、ってのは難しいですね。やっぱ手続き的な仕事なんで。
いずれにせよ、evalからもフェーズと何らかの計算結果(あるいはNone)のタプルがPrintメソッドに届き、そのフェーズ情報に従って必要とされる文字列を探して表示するだけ、の簡単なお仕事となっております。

REPL(Read-Eval-Print-Loop)

さて、ここまでで、インタプリタ的なread、eval、printの各部品がクラスとして実装されました。あとはこの3つを組み合わせるだけで「アプリケーション」が出来上がります。Pythonスクリプト的には次のような記述で動かせるわけですね。

これで原版の「イスカンダルのトーフ屋ゲーム」を上手くエミュレート出来てるんじゃないか、って思います。
なお、Python版「イスカンダルのトーフ屋ゲーム」の全ソースは以下のようになっています。

今回はpy2exeでWindows用のexeファイルも作ってみました。実行形式のダウンロードは以下から行って下さい。

 

まあ、慣れないOOPで苦労しましたが、いずれにせよ、一応予想通り、入出力と評価部分を完全に切り分けたREPLモデルをプログラムする事は充分可能だ、って事が分かったんで、以降、GUIのプログラムを書く時が来たら、この指針を応用出来そうだ、って感触を得たのは大きいですね。

2011年5月24日火曜日

wxPythonでハングマン

GUIでずーっと苦労してるんですけれども(笑)。


今回、Qtからはじまって、GTK+、wxWidgetsと調べまわっていました。あっちこっちのブログなりWebページなり見て回ってたんですが。


そして非常に意外だったのは、その殆どのケースでは「プログラムを作り上げる」トコまで行ってないんですね(笑)。曲がりなりにもアプリケーションを一本作り上げている、殆ど唯一の例外は我らが@kaorin_linux氏のおっさんでも解るPythonくらいで、他は殆ど、Qt CreatorなりGladeなりwxGladeなりで、ウィンドウを作るだけで終わっちゃってる、つまりGUIのスケルトンを生成する事で終わっちゃってるんです。つまり、本当に知りたい、要するに「動くプログラムを作る」トコまで到達してないんです。


より正確に言うと、アプリケーションを作った例ってのはあります。ただし、それはテキストエディタだったりして。いや、テキストエディタを作ろう、ってのは悪い話じゃないです。フツーに考えれば。ただし、いまどきのGUI Buiderなんかはテキスト入力用ウィジェットくらいデフォルトで備えてるんで、これは実はウィジェットの使い方講座です。従って、ボタン貼っつけたりするのと事実上変わらないんですよ。これじゃああんま実際的なGUIのソフトウェアを書こう、と言う話までは発展しませんよね。しないんです。


今回調べてみて思ったのは、設計思想としてはLinux(あるいはフリーソフトウェア)のGUIの構築法は非常に優秀だと言う事です。まあ、Windowsの方の設計理念はあんま良く知らないんですが、少なくとも、要するにGUI部分はGUIとして独立させ、CLI部分はCLI部分で書けるようにしているって事です。恐らく背景的には、歴史的にはUNIXはCLI文化であり、またCLIのプログラムを書ける人はたくさんいるんです。そこで、そう言う人達のスクリプト的な資産を即GUIアプリに転用出来るように考えてるんでしょうね。この辺、ベースとなる、例えばXや、あるいはその上に被さるQtやGTK+が意識して作られているのかもしれません。


専門的にはメッセージパッシングって呼ぶんですけど、OOPの根幹思考法がこれ程か、って程強調されています。つまり、GUIで組み上げたスケルトンとCLIで書かれたスクリプトが相互にやり取りするモデルとなってる。あるいは通信しあう、って事ですか。これで理論的には至極簡単にGUIアプリが書けるようにはなってるんです。


理論的には、ですよ(笑)。残念ながら、アッチコッチのブログなりWebページなり見て回ったんですが、実はその「通信をどうやるのか」まで行ってないんですよ(笑)。殆ど唯一の例外って言って良いのが、@kaorin_linux氏のページなんです。


何でこんな状況なんですかねえ(苦笑)。一つ言えるのはLinux等のフリーソフトウェア/OSSのGUI Builderがショボ過ぎるって事があるんでしょう。つまりデザイン的に言うと、使ってる人がGUI側のコード構成なんて全く考えなくて良いように作られてない事があるのかもしれません。割に剥き出しです。つまりCLIでGUIのプログラムを書ける知識がいる


もちろん、OSS界隈ではハッカーな人がたくさんいるんでしょうし、CLIでコードを弄る方が大事なんで、中を弄れるようにしておくのが吉だ、と言う考え方もあるでしょう。でも間違ってますよそれ(笑)。次の三点でそれは間違ってる、と断言出来ます。


第一に、そもそもOOPってのは中身を隠蔽出来るってのが前提です。つまりユーザ(この場合プログラマですか)が気にしなくて良い部分は気にせんで良いシステムになってるのが前提なんです。GUIスケルトンがどんなコードで組み立てられてる、なんつーのは全く気にする必要が無い。まずこのOOPの基本原則に反している。第二にGUI Builderが提供するものはあくまでスケルトンであって、実際のトコ、これはいわゆるフロントエンドですよね。ぶっちゃけこの部分はプログラムじゃないって考えて良いんです。プログラム本体はあくまでCLIの部分。つまり、プログラマが全力投球して組み立てなきゃならないのはCLI部分であって、GUI部分なんつーモノに時間割くのは本末転倒なんですよ。一体何の為にGUI部分とCLI本体がメッセージパッシング出来るように設計してあるのか。仮にGUIもCLIもゴッチャになってるVB的なシームレス環境ならいざ知らず、根本的な設計思想にも反してるんです。


そして何より、GUI部分って書くのがつまらないんですよ(笑)。最終的な見栄え調整がGUIの、要するに役割なんですが、やってる事っつったらパーツ並べていくだけのバッチ処理が本質。大して創造性があるわけでもないですし、仮に創造性を下手に発揮したらUIの統一感がグチャグチャです。こんなもんにプログラマの知的な時間を取るべきだ、なんつーのは全くバカげてる。「それでもGUIのコードを見て弄れる自由が欲しい」なんつーのは単なるヘタなこだわりです。味噌汁でツラ洗って出直して来た方が良い(笑)。いや、マジで(笑)。


つまり、Linuxを中心として提供されているGUI関係のツールってのは設計思想に反して出来が悪過ぎるんですよ。本来、GUIスケルトンとCLIのプログラムがそれぞれの役割を全うしながら通信するってモデルなら、GUI Builderは(MicrosoftのVisual Studioより)もっと直感的に使えるツールであり、吐き出されたGUI部分は全く気にせんで良いくらいじゃないといけない。要するに現状見る限り、理論先行の頭でっかちな状況なんです。そう言わざるを得ない。


だから色んなWebページの作者であるとか、あるいはブログの書き手とか、恐らく元々はWinな人たちで、趣味でLinuxもやって、んで「ああ、LinuxでもGUI Builder的なモノもあるのね」と興味を持って触ってみる。で、結局、GUI Builderってのは彼らのアタマの中では当然、「簡単にGUIアプリを構築出来るモノ」じゃなきゃいけない筈なんですが、触ってみるとCLIでのGUIプログラミング知識が必要となる。何でそこまで調べなきゃアカンのだ、ああ、メンド臭くなってきた、ってなるのも分かるんですよ(笑)。だってGUI的な決まりきった定形処理する割には覚えなきゃいけないモノが多すぎるんですから。ホントメンドくせえんだもの(笑)。


加えると、海外のTutorial系も似たり寄ったりなんです。まあ、確かに情報は多い。ただし、記述がGUI寄りなんですよね。分かりますかね、この表現。つまり、GUIで作成されたコードにCLIコードを埋め込みましょう、的な展開になってる。今ここまで読んでくれた方々だった「ああ、なるほどな」って合点が行くかもしれませんが、要するにGUIとCLIを「分離する為のシステム」なのに「埋め込みが生じてる」コード紹介になってて、これはこれでおかしいんですよ(笑)。つまり、恐らくOSS系のGUIアプリのモデルとしてはそんなに規範的ではない、と考えられるのです。つまり、誰も(OSSで恐らく想定している)正しいGUIアプリの書き方を知らない。


いや、考えてみれば酷い状況でしょ(笑)?そうなんですよ。LinuxなんかのGUIアプリのモデルってのは恐らく基本的な設計思想とは別な方向にひん曲がって行ってる気がします。それもこれも提供されているGUI Builderの「やり方」があまりにもお粗末だ、ってのが絶対一要因に違いない。メチャクチャですね。少なくとも完全なRAD(Rapid Application Development)が提供されてたら、もっとLinuxのGUIアプリはWindowsに比べて良く整理されていて(つまりメインテナンスがラクで)、もっと発展してたかもしれません。基本的な設計思想に逆らうからこう言うハメになってるんじゃないかな、って思います。RADなGUI Builderはむしろ思想を実現する肝なんですよ。


とまあ、自分なりの現状分析、っつーか、要するに愚痴なんですけど(笑)。まあ、少なくとも現状に於いてはGUIのコードを書くにもCLIでのコマンドの知識が必要だ、って事で。取り敢えずは取っ付きやすそうなwxPythonを調べていたわけです。


ってなワケで、またもや極私的な備忘録。書いとかないと忘れちゃいそうですからね。上で「GUIとCLI部分の分離が本来のスタイル」って言いながらそこまでまだ出来てないんですが、それは今後の課題と言う事で。取り敢えずGUIアプリをwxPythonを使って「一本仕上げる」事にします。はい。


お題は単純な単語当てゲームです。ハングマンと呼ばれるヤツですね。このコードには元ネタがあって、ピアソンエデュケーションから出版されているPythonで学ぶプログラム作法を元にしています。原著はWebサイトLerning to Programなんですが、本と違って改訂されて来て、現時点ではWeb上で読めるハングマンのコードはありません。残念ですが。


元々書籍の方ではPythonとTcl/tkのインターフェースである組み込みのTkinterモジュールを使ってGUIで書かれていました。それをwxPythonを使って書きなおしてみれば、wxPythonに慣れるだろう、と言うのが目論見です。また、この本では不完全ながらもGUIとCLIのコードを分けていて、言い換えるとやり方によっては基本ロジック(つまりCLI側のコード)を変更する事無く、GUIのフロントエンドを取り替える事が可能だ、と言う事を示唆しています。


でははじめますか。


あ、その前に材料用意しないといけませんね。必要な材料は次の二つです。



  1. 英語の辞書データ

  2. ハングマンの画像7枚(それぞれhm0、hm1、hm2・・・と名付ける。画像形式はお好きなように。サイズは250 x 180〜190辺りが適切。)


辞書データはいいでしょう。英単語当てなんで、まずは英単語辞書が無いとお話になりません。これを別ファイルに用意しておいて、そこからランダムに問題となる単語を選び出すスタイルとします。ご自分で作成してもいいですし、Web上探せばテキトーに使えるモノが引っかかるかもしれません。あるいはPCに詳しい人なら、例えばワープロの辞書なんかから情報抜き出したりも可能でしょう。それで取り敢えず作業フォルダ内に「hangman.words」とでも名づけて保存しておく。


2番の方が厄介ですね。ハングマンと言うゲームは(少なくともここでは)プレイヤーは6回まで間違える事が出来ます。ハングマンと言うのは「絞首刑に合った男」と言う、まあ、如何にもアメリカ的なちょっと笑えないユーモアがあるゲームなんですが(笑)、要するに、単語に何のアルファベットが使われているのか。その予想が当たってたらいいんですけど、予想が外れていた場合、少しづつ「絞首刑に合った男」の画像が明らかになっていきます(通常は単純な線画で、要するに線が描き加えられて行く)。6回間違えると「絞首刑に合った男」の線画が完成し、そしてそこでゲームオーバーとなるわけです。



まあ、そういう絵を用意しとかなきゃなりません。ペインタやGimpでご自分で作成しても良いでしょう。僕は画像作成がメンド臭かったんで、もっとアクロバットなやり方で画像を用意しました(笑)。


ところで、Pythonで学ぶプログラム作法の著者であるAlan Gauld氏は割にオブジェクト指向大好きっ子みたいで(笑)、書籍は23章から成り立ってるんですが、オブジェクト指向が登場する第17章以降、これでもか、と言う程オブジェクト指向コードが蔓延していきます(笑)。そして、その抽象化の方法論に対して一家言持っている模様です。曰く、


このプログラムは、最初からオブジェクトを使って構築することにする。作成するプログラムに必要なクラスの計画を立てるときには、まず、プログラムが実際に行う処理について可能な限り抽象的に考えなければならない。今作成しようとしているのは、コンピュータが生成した何らかの「正解」(target)を当てることを目的として、いくつかの推測(guess)が提示される「ゲーム」(game)である。

まあなんともはや(笑)。ハングマン「如き」のゲームで抽象化、ってのもアレなんですけどね(笑)。まあ、ここではそう言う方針なんで、しょーがないでしょう(笑)。要するに問題の単語を出して、入力を受け取って、判定する、と言う「抽象機構」をここでは最初に作っています。そしてそれがフレームワークだと力説します(笑)。まあ、そうですね、フレームワークってばフレームワークでしょう。極めて限定的で小さい抽象クラスのカタマリですが、プログラミング初心者にOOPのパワーを見せるにはいい方針かもしれません。そうか(笑)?


まあ、ここではちょっと端折りたいので、取り敢えずコードを紹介しましょう。



これをgame.pyとします。


さあて、これ解説必要ですかね(笑)。まあ、単純ってば単純なんですが単純過ぎて・・・(笑)。抽象化に成功してる、って事なんでしょうけれども(笑)。returnばっかやん、とも見える(笑)。あんま単純過ぎても動作が全く想像出来ないっつーか・・・(笑)。


余談なんですが、こないだTwitterで@kaorin_linux氏と面白い話してたんですよ。曰く「ガンダムをOOPでプログラムする場合どうすっか?」みたいな議題でね。果たしてガンダムをプログラムする場合、ルートのスーパークラスを作成して「人型ロボット」と言うクラスを作るべきか否か。OOPの論法から言うと「作るべき」なんですよね。理論的には。


現場主義の@kaorin_linux氏は「必要ない」と言う立場でした。その代わり、Zガンダムとか∀ガンダムとか作る場合は、ガンダムと言う性質を継承して作ればいいじゃないか、と。その通りですよね。現場主義の@kaorin_linux氏的な意見です。


一方、OOPの理論ってのは突き詰めていくとある種「机上の空論」でもあるんですよ。では本当に「人型ロボット」がルートたるべきか、っつーと分かんない。別にロボットに限定する必要もなくって、その上に「人型の何か」と言うもっと抽象的なスーパークラスも考えうる。そうしたら「人型宇宙人」とか「人型へげもげ」とか色んなブツが継承出来る。そして本当は「人型」に限定する必要があるのか、とか。もっと上に「何か」があっても良いのでは・・・・・・突き詰めるとキリがありません(笑)。


意外と、OOPのルートクラス設計的な思想ってのは、それこそJavaのように「豊富なライブラリを提供する前提の」言語とその言語設計者に一番有利なモノかもしれません。彼らがその「言語内のあらゆる事を」決められる立場ですから。Common LispのOOPでもTと言うクラスを頂点にヒエラルキーが構成されています。「見事だ!」なんじゃなくって、最初からそうデザイン出来る人はそう決められる、と言う「だけの」話だと思います。僕ら一般ユーザーはそんなヒエラルキーを考えろ、っつっても、所詮他人の褌で相撲を取るようなもので、初めから限界があるような気がします。


と言うわけで、上のコードも「所与のライブラリ」と考えて単純に使った方が良いのかもしれません。


と言うわけでgame.pyのコード解読は取り敢えずメンド臭いので止めて、CLI版のハングマンのコードを見てみましょうか。これがGUIでのハングマンの「本体」、hangman.pyです。



これどうなんでしょうね〜(笑)。結局Gameクラスとか継承しててもかなりの部分がオーバーライドされてるように見えませんか(笑)?って事は実際は、殆どスクラッチから書き始めるのと変わらないような気がするんですが(笑)。


まあ、こう言うの見ると@kaorin_linux氏が言ってる事の方が現実的には正しいような気がしますね。作らなきゃいけない部分から取り敢えず手をつけてみると。もちろんライブラリ的なモノを作る必要性も出てくる事もあるでしょうけど、少なくとも個人的にはこの例はあまり上手く無い例のような気がします(だから本家サイトからネタが消えたのか・笑)。


さて、本から丸写ししてコメント記述してるんで、これも特に解説は要らないでしょう(こればっか・笑)。ただ、これをPythonインタプリタで走らせてみて、全く画像が無いけど、単語当てゲームとしては機能しているのを確認してください。つまり、ゲームとしてのロジックはほぼ実装し終わっている、と言う事です。これがCLIでのハングマンですが、このファイルに直接手を入れずに次の段階でGUIのゲームへと変身させます。


多重継承は悪か?


さて、いよいよ本題です。CLIのプログラムにGUIフロントエンドを引っ付ける。ここではPythonで学ぶプログラム作法の流儀に従って、GUIフロントエンドは中核機構となるwx.FrameとCLIゲームであるhangmanモジュール内のHangmanクラスを多重継承します。


多重継承って結構嫌われていますよね。当然で、平たく言うと、ヘタな継承の仕方によっては有向循環グラフになる危険がある。グルグルグルグルグールグル、と言うアレです。孫クラスのつもりだったのに曽祖父クラスになってしまっていた。自分で自分を継承してたりして・・・まあ、怖い話ですわな。だからインターフェースだMix-inだ、と言う代替手段が提供されてきたのでしょう。


一方、Pythonは「プログラマの良心に任せて」多重継承機能を提供しています。そしてそれが意味する事は「使いすぎは良くない」でも「使いどころさえ間違えなければ」超強力だ、と言う事です。


そして、CLIプログラムをGUIと統合する「その瞬間」こそが、その使いどきでしょう。全く異質(に見える)のアイディアであるCLIとGUIを自然に貼り付ける「接着剤」の役目としては多重継承がまさしく適していると思われます。


wxPythonの直接手書きは諦める(爆)


ところで、最初は、手書きでwxPythonのコードをゴリゴリゴリゴリ書いてくつもりだったんですよ(笑)。せっかく勉強したんだし。ところが、途中でGive Upしました(爆)。いくつか理由があります。


まずはTcl/tkバインダのTkinterのコードって手書きでも相当短く出来るんですよ。僕はTcl/tk自体は全然分からないんですけど、どうもシンプルな記述で意図的な配置に出来るらしい。正確に言うと、wxWidgetsに比べるともっとバッチ処理に近く、基本的にコードを並べた順に順序良くウィジェットを配置していけるらしい。もちろん、wxWidgetsも基本的にはそう言う部分があるんですが、配置に関して言うと強力なようで同時に弱点も内包してる気がします。平たく言うと配置に使うSizerですね(笑)。こいつは配置をラクにするように設計されてる筈なんですが、全体の画面構成を綺麗にする為にSizerを複数重ねて配置していく必要が出てくるんです。wxGladeなんかのGUI Buiderならこれはラクなんですが、マジでCLIで書くとアタマが痛くなっちゃうんです(笑)。CLI泣かせの機能ですね(笑)。


第二に習慣の問題があります。これはTkinterで書かれたGUIを如何にwxPythonに移植するか、と言う挑戦でもあるんですが、習慣的にTkinterはimport時に



ってやっちゃうんですよ(笑)。これは実は現在のPythonでのライブラリインポートでは薦められた方法じゃない。要するにライブラリ名を(ドット表記での)接頭語にしないようにする「オマジナイ」なんですが、今の流儀では殆ど



と言うようにして、明示的にライブラリ名を接頭語として用い、「ライブラリ同士で名前がぶつかったりしないように」してると思います。ただ、Tkinterの場合は歴史が古いせいか、こう言う書き方が跋扈してるんですね(笑)。


まあ、少なくとも(何故か)Pythonで学ぶプログラム作法では前者の書き方が用いられています。これは移植の時に困るんですよ(笑)。


何が困るか。要するに「見慣れないメソッドが」登場したとき、接頭語が無いお陰で果たしてTkinter内のメソッドなのかどうなのか確認が取りづらい、と言う事です。そうなるとそれを調べながらCLIでコマンド書いてく、ってのは至難の業なんですよね。少なくとも時間がかかりすぎる。


つまり、先にwxGladeとか使って画面が立ち上がるようにしてて、生成されたPythonコードと比較しつつ、対応取って行って調べた方が早いんです。そう言う理由もあります。


最後に、「何がしたいか」で調べるのが難しい。要するに逆引きですよね。wxPythonも例に漏れませんが、ある「既知の」クラスやメソッドに付いては、ある程度調べる事が可能です。逆に「これってどーやるの?」ってなった時、CLIで書いてる場合「アタリを付けて」調べるのが難しい。だって名前をそもそも知らないんですから。


例えば、今回の準備段階として、wxPythonのチュートリアルを2つ程こなしました。ただ両者とも「画像表示に対しては全く触れていない」。つまり、どう言うクラスのどう言うメソッドを使えば良いのかサッパリなんです。今回使用したのはwx.StaticBitmapと言うクラスですが、こんな名前かどうか思いつくのは至難の業です。結局、wxGladeでコードを吐き出させてクラス名を調べてからリファレンスをあたった方が効率的だったんです。


そんなわけで、一旦wxGladeで思ったデザインを作って、そいつをPythonで吐かせて、そのコードを調べながらGUIを作り上げていく、と言う方針を取りました。っつーか取らざるを得なかったのです。


画面イメージ


ボタン配置等の画面イメージは以下のようなカンジです。



GUI版ハングマンのコード


まずは作成したGUI版ハングマンのコードから。



結構wxGladeが吐き出したコードに手を入れています。本当はダメみたいなんですがね(笑)。同時に吐き出すxmlが拗ねちゃう模様で(笑)。


まず、この部分は簡単ですね。



これはお察しの通り、GUI版ハングマンのボタンの配置と対応しています。こいつをクリックして、推測した使われてる文字をGUI版ハングマンに伝えます。ただ、これはボタンそのものではなく、一種テンプレートです。二重のリストになっています。


次はGuessクラスを継承したhmGUIGuessクラスです。



ここも特に解説は要らないでしょう。


次からGUI版ハングマンの心臓部、hmGUIクラスです。こいつがwx.Frameとhangman.Handgmanを多重継承します。まずはここから。



まずは初期化メソッド__init__。ここで分かる通り、二つのクラスを継承しているので両クラスに基づいて初期化を行ってるのが第一点。


imgpathってのは本体フォルダ内の画像フォルダの位置ですね。こいつを使って、相対パスから計算して初期画像を表示する準備をします。ちなみに画像はhm6(この場合は.jpg)から辿ってhm5->hm4->...->hm0と進んで行きます。初期画像は6番ですね。その方が入力指定したアルファベットが間違えた場合の「残りの回答出来るチャンスの数」と対応させやすい。


lettersってのは実は上で見たアルファベットのボタン本体の事です。辞書型を使ってますが、初期状態だとボタンもヘッタクレもないです(笑)。こいつに後でボタンを作成した状態をぶち込んで行きます(笑)。何故辞書型なのか、はその時に。


そして、タイトルバー表示等の調整をする__set_properties()を呼び出し、displayStart()メソッドを呼び出して初期化は終了です。


__set_properties()はwxGladeによって作成されたモノです。手書きでコード書く場合はあんま必要ないです。displayStart()はサイザーが乗ったメソッドです。こっちに関しては後述します。


次は表示に関して扱うdisplay()メソッドです。こいつはCLIのdisplay()をオーバーライドした状態になってます。っつーか事実上殆ど書き換えられてますね。


実はこの辺がOOPの無駄な部分なような気がするのだが・・・。「ちょっとだけ変更する」なら有効だろうが「大幅に変更する」のならあまり意味があるとも思えない。結局コード記述量が増えるわ無駄になるわ、と言う気がしてる。


「表示用メソッド」なんで表示に関する動作が詰め込まれています。まず冒頭で、wx.StaticText(一般にラベルと言われる部分)に何を表示させるか、それが記述されていますね。これはこのメソッドの後半でif〜else文で切り替えられるネタ元です。この辺は難しくないでしょう。


まずは第一のポイントですが、



ここ。このlettersってのはさっき初期化したlettersと同じものです。ただし、このdisplay()メソッド内では「既にボタンとして成り立っている」前提になっています(笑)。あら、いつの間に(笑)。まだボタン化するコード書いてないんですがね(笑)。まあ、そう言う前提として見てください。


ここのポイントはですね。ハングマン、と言うゲームに於いては、もう一度繰り返しますが、推測したアルファベットを与えるわけですよ。んで、それが当たったにせよ、外れたにせよ、その文字は「二度と入力しない」んです。例えば'E'と示してみて、それが正しかったにせよ間違ってたにせよ、二度目の'E'の入力はあり得ない。


と言う事は。ゲームデザインとしては、「同じ文字の二度目の入力があり得ない」って事は、単純に考えると「一度押されたボタンは二度と押せないようにする」のがセオリーですよね。つまり、ここでのlettersってのは実はwx.Buttonと言うクラスのインスタンスになるわけで、Disable()と言うのは「ボタンを押せないようにする」メソッドなんです。これがまた、wxPythonの公式ページにも記述が無い程の(笑)便利機能なんですが(笑)。どないなっとんねん(笑)。


もう一つのポイントは次の二行です。



thefileと言うのは画像(hm*)ですね。ここでoutcome(ゲームの残り回答数)と適合したパスをpythonの相対パスを利用しつつ生成してます。ここはもう良いでしょう。あと5回答えられるならhm5が選ばれるし、あと1回しかなかったらhm1が選ばれる。


次はtheImgです。これも__init__で画像の初期化により初期パスは指定してるんですが、一方画像オブジェクトはいまだ作られてません(笑)。だってコード書いてないもの(笑)。それでもいきなり登場して良いのがOOPです(笑)。self付いた変数ってクラス内大域変数だからあっちゃこっちゃ飛んでてヤダ、ってぇの(笑)。これだから困るんだってばよ(笑)。


与太はさておき、こいつはwx.StaticBitmap()というクラスのインスタンスです。要するにこの時点で(まだコード書いてないけど!)画像オブジェクトだ、と言う事です。


ちょっと余談めいた注釈になりますが、例えばこのGUIハングマンで用いるクラスではwx.StaticBitmap()とかwx.StaticText()とか、良くStatic、と言う形容詞が付いています。他の人がどーだか知らないんですが、Staticって聞くと、僕なんかは「静的な」「変更出来ない」「固定された」って言うような意味に捉えちゃうんですよね。つまり一回表示されたら変更不可みたいな。


だからコンピュータ用語って困るんですよ(笑)。簡単にStaticだ、とかDynamicだ、とか言いますが、実際問題コンテクストによって変わる。そしてコンテクストを決定するのは形容詞の筈なんで、自ずとから矛盾してますよね(笑)。何じゃそりゃ、と。


ですから、僕みたいに直感的に誤解しちゃう人の為に一言。ここで言うStaticとは「ユーザー入力を受け付けない」って事です。つまりプログラム内部から変更する分には全くOKって意味なんです。ああ、紛らわしい。


今、ゲームの進み具合(つまり、あと答えられるのは何回なのか)によって表示すべき画像が変わります。画像オブジェクト、平たく言うとインスタンス化したwx.StaticBitmap()の画像パスを変更するメソッドがSetBitmapなんです。


また良く知らないんですが(笑)、多分いわゆるjpgとかpngとかはそのままじゃビットマップ画像として認識されないのでしょうか。多分変換が必要で、その為の変換クラスがwx.Bitmapなのでしょう。ホント、よう知らんけど(笑)。


wx.Bitmapの第一引数はファイル、第二引数は第一引数で与えられたファイルタイプです。つまり、例えばjpgなら本当はwx.BITMAP_TYPE_JPEGを与えるべきなんでしょうが、メンド臭いんで(笑)、何でもオーケーのwx.BITMAP_TYPE_ANYが与えられているのです。



ここは小粒ですね〜。getTarget()はまあいいですよね。Quit()は、終了ボタンが押されたら反応するメソッドでウィンドウを閉じます。要するにゲームを終了させるメソッドです。


reset()もリセットボタンを押した時にゲームを初期状態に更新するメソッドです。ただ若干複雑なんでコメントが付いててなお重要なところを。まずは



これは先ほどのdisplay()で見た、「ボタンを使用不可にする」事と逆の事を行っています。ゲームに再度挑戦する際に、ボタンが使えないとどうにもこうにもブルドッグ、だからです(謎)。相変わらずlettersはアルファベットのボタンを表しています。つまり、wx.Buttonのインスタンスに於いて、メソッドEnable()とDisable()は真逆の関係だと言う事です。


次はここですね。



一行目はさっきやったのと殆ど同じです。画像を書き換えてる(っつーかより正確に言うと参照すべきパスを変えている)。ここでは__init__()で定義されているself.firstImgを「大域変数よろしく」引っ張ってきています。


二行目は表示されるべきモノですね。実際、self.getResult()で文字情報が入ってる筈なんですが、表示されるのはそれに対応したアンダーバーだ、と言う事になります。


そして三行目。このstatusも実はインスタンスです。まだ定義されてません。クラスはwx.StaticText。これもSetLabel()と言うメソッドで表示を変えます。wx.StaticBitmapのSetBitmap()メソッドの文字版ですね。


__set_properties()はいいでしょう。wxGladeが自動で作ったメソッドですし、ここではWindow上部のタイトルバーにアプリケーション名を表示しているだけ、ですから。


さて、いよいよ最後のメソッド、displayStart()です。



まあ、大体ここまで読んできてくれた人だったら何やってるかおおまかなトコは掴めるでしょう。theImgと言う画像オブジェクトもここで一行目で設定されていますし、同じくラベルオブジェクトであるstatusもここで作成されています。


が、やっぱ見て分かると思いますが、メンド臭いのがSizerですね。全部で5種類程使っています。これ、自分でコード直接書いてたらこんがらがりますよ(爆)。よってSizerに関して言うと、あんま細かい事いいません。ただ、最後のsizer_*.Addの第二引数でSizerの比率を変えたりしています。デフォルトでは一対一に分割、って状態になってますが、それを実際画面見ながらパラメータ微調整する、ってカンジですか。この辺はプログラミングじゃあないですよね、正直なトコ。


そしてsizer_*.Addの第一引数にはそのSizerの上に「何が乗ってるか」を表しています。ボタンが乗ってる場合もあるし、Sizerの上にSizerが乗ってる場合もある。はあ、画面デザインって大変です(爆)。


さて、ここで一番重要な部分は次の部分です。ここで'A'〜'Z'のボタンを生成しています。



実はここがこのプログラム最大の山場で、かつボタンとシグナルを結びつける全テクニックが突っ込まれています。また、プログラミングに対する大変面白い考え方が伺えます。もっとも僕が考えたんじゃないんですけどね(笑)。


実はwxGladeに'A'〜'Z'のボタン生成のコードを吐かせるのも当然可能なんですけれども、それやっちゃうと27個のボタンがソースコード上並びまくる、と言う大変な状態に陥ります。かつ、Bind(あとで後述)がメンド臭くなる。しかもそれをSizer上に置くトコまで指定せねばならない。上のコードはそれをたった9行で行っちゃうスグレモノです。およそコード量が1/3、いや1/6、いや1/9に減った、と言えるでしょう。


まずは冒頭のkeysを使ってrowを、そしてrowの中からchを取り出す二重ループ構造になっていますね。そしてchが空の文字列だった場合、



が実行される。ここで代入されてるgrid_sizer.Addですけど、これはwxGlade上ではSpacerと呼ばれています。隙間を埋める部品、って事なんですけど、実はコード上ではそう言った名前は使われていません。いずれにせよ、""に対応する場所には何も置くな、と言う指示になっているのです(あるいは、30 x 30の小さな「何か」を置け、って事でしょうか)。


では空文字列じゃなかった場合です。まあ、取り敢えず最初のactionは置いておきますか。その次のコードでAlan Gauld氏の手腕が光る。珠玉のコードがたった一行で示されます。



正直な話、最初これ見た時何やってるんだかサッパリだったんですよ。しかし考えてみると非常に良く出来てる。これはこう言う事なんです。最初に__init__でlettersを辞書型にしておいたのがここで効いてくる。


つまりchがlettersと言う辞書型のキーになるんですね。そしてその「値」この場合オブジェクトwxButton()ってのがここで結び付けられるわけです。要するにハッシュであるlettersに適切なキーを渡すと必ず返り値でユニークなボタンオブジェクトが返って来る。ね?メチャクチャクールな方法でしょう(笑)。こりゃすげえや、って思いました。


しかし、ボタンを生成するだけじゃダメです。イベントとして動作と結び付けないといけません。ここで結びつける対象は今後回しにしたactionなんですが、これらを結びつけるメソッドがBindと言います。そして第一引数にはボタン用のイベントだよ、と知らせるwx.EVT_BUTTON、第二引数には結びつける対象の関数(この場合はaction)、そして第三引数にはそのボタン自体が入ります。



これはボタン用のBindの書式です。ですが、例えばプルダウンメニューなんかでも基本変わりません。第一引数がwx.EVT_MENUになったりするだけです(色々あるんで検索するのが大変でしょうが・笑)。第三引数もいいですね。ハッシュ(辞書型)にキーchを与えて返って来るwx.Buttonインスタンスとactionが結びつく。そしてそのactionは最初にラムダ式として定義されてるのです。



この場合、ラムダ式は第一引数にイベントを受け取り、第二引数にchを受け取り、第三引数がself、と言う、ちょっとPythonとしては珍しい記述に落ち着いています(っつーか引数の順番として、はね)。そして、先ほど定義した表示用メソッドdisplay()にchを渡す、と言う作業が閉じ込められています。これで各アルファベットボタン毎に何が表示されるべきか、完全に定義されるわけです。かっこいー!!!!凄いアイディアですね。


そして、最後にボタンをgrid_sizer上に行儀良く並べていけば良いわけです。



たった9行で27個のボタン生成、27個のボタンとメソッドを結びつけ、配置までやってのける、ってなあ凄すぎます。でも良く考えてみると、これって凄いだけじゃあないんです。実はあるアイディアが背後に見え隠れしています。それは一体何でしょう?


Lisp的考え方


僕も最初にお題を見た時、「27個もボタンしこしこ書きたくねえな。何とか自動生成できねえかな」って思いました。ところが問題なのは、代入するクラスの方じゃなくって、代入される変数名の方だったんです。こいつらリテラル、と呼ばれるブツは通常自動生成は出来ません。たった一つの言語を除いて。


そうですね。Lispです。Lisperだったらこう言うお題を見た場合、即刻考えるのはまずは「変数名の自動生成」でしょう。そして恐らくマクロにしちゃいます。ところがこれがPythonじゃ出来ない。こりゃ困ったな、って思いました。


しかしこれをAlan Gauld氏は「辞書型」と言うデータ型を用いて華麗に切り抜けました。確かに凄い見事なんです。ただ、この手法は実はLisp的発想なのかもな、と気づいた。Alan Gauld氏が狙ったのは、実はPython上でのシンボル型のエミュレーションだったのでは、と。それを辞書型用いて実装してみた、ってのが本当のところではないか、と。


まあ、この辺は余談なんで、Lisp知らない人にはあんま関係ないです。がLispってのは全般的に(Java的な意味ではなく)データ型で組み立てられていて、いわゆるフツーの言語で言うリテラルさえ持ってません。「地の文」を構成する「何か」が無いわけです。フツーの言語的感覚で言うとね。その代わり、式を組みたてる「字」っつーのかな、それらは特に「シンボル型」と呼びます。れっきとしたデータ型なんです。


さて、Lispではインタプリタに何か入力すると、そのシンボルは即刻シンボルテーブルと言う内部の一種データベースに登録されるわけです。これをシンボルをインターンするとかLisperは言うわけなんですけど、実はこのPythonコードのこの部分は、この「シンボルのインターン」を明示的な方法を持ってPythonで実装したんじゃないのかな、と思えるわけです。すんごくシステム的には良く似てるんですよ。


多分PythonでLispを実装する、的なネタですと、このシンボルのインターンのシステムと言うのは、Lisp-1やLisp-2に限らず、恐らく辞書型で実装するのが一番効率が良いです。ハッシュは速いですし、シンボルとしてのキーを検索するには向いている。また、キーをメソッドっつーか関数としての「値」に結び付ける発想も至極良く似ています。このシステムは、かなりSchemeなんかに近いんですね。このコードの意味が分かった時、正直「一本取られた!」って思いました。非常にLisp的な実装ですし、明らかにLispの一部(っつーか考え方)を導入しています。


もちろんAlan Gauld氏はLispも知っている。初心者プログラマがPythonを終えた後に学ぶ言語としてLispを挙げている程です。そしてこう言う風にLispのプログラミングテクニックと言うより、その「発想/構造」を持ち込めるようになる、ってのが、ひょっとしたらエリックレイモンドが言っていた「悟り」の部分なのかもしれません。


終わりに


この見事なテクニック見たあとでは何も言えませんね。基本的にボタンとイベントを結びつける基本テクニックは上のコードで見たまんまですし、あとは地道に読めば分かる範囲です。取り敢えずCLIのコードからスタートして「多重継承」を経由して、GUIのアプリケーションへ変換する事は出来ました。wxPythonを使った「アプリケーション」を一本作り終えたわけです(よね?)。


[リセット][終了]ボタンに関して言うと、例えばGTK+をそのまま使うGladeなんかはGTK+が用意してくれているボタン画像なんかを使えたんですけど、wxGladeはどうだったかな。何か見つからなかった(笑)。その辺今後の課題かもしれませんね。まあ、課題って程でもないですけど(笑)。


最後に。一応このハングマンと言うサンプルはGitHubに上げてあります。

2010年2月8日月曜日

Project Lizardry ~その1~


#!/usr/bin/env mzscheme
#lang scheme/base

(require srfi/1)

;; データ
(define *gilgamesh-tavern-data* '())

(define *party* '())

;; キャラクター基本
(define *character-base*
'(('foe . #f)
('level . #f)
('性格 . #f)
('職業 . #f)
('種族 . #f)
('E.P. . #f)
('Next . #f)
('Gold . #f)
('Marks . #f)
('Age . #f)
('A.C. . #f)
('Rip . #f)
('力 . #f)
('知恵 . #f)
('信仰心 . #f)
('生命力 . #f)
('素早さ . #f)
('運の強さ . #f)
('H.P.numerator . #f)
('H.P.denominator . #f)
('状態 . #f)
('Mage 0 0 0 0 0 0 0 0 0)
('Priest 0 0 0 0 0 0 0 0 0)
('持ち物 . '())))

(define *race* '((人間 ((力 . 8)
(知恵 . 8)
(信仰心 . 5)
(生命力 . 8)
(素早さ . 8)
(運の強さ . 9)))
(エルフ ((力 . 7)
(知恵 . 10)
(信仰心 . 10)
(生命力 . 6)
(素早さ . 9)
(運の強さ . 6)))
(ドワーフ ((力 . 10)
(知恵 . 7)
(信仰心 . 10)
(生命力 . 10)
(素早さ . 5)
(運の強さ . 6)))
(ノーム ((力 . 7)
(知恵 . 7)
(信仰心 . 10)
(生命力 . 8)
(素早さ . 10)
(運の強さ . 7)))
(ホビット ((力 . 5)
(知恵 . 7)
(信仰心 . 7)
(生命力 . 6)
(素早さ . 10)
(運の強さ . 15)))))

;; テキスト部分
(define *top-page-text*
"\t Lizardry\n\n Proving Grounds\n\t of\n the Let Over Lambda\n\n\tWelcome to\n the world of Lizardry\n\n\t1. START\n\t2. READ STORY\n")

(define *story-text*
'("昔々あるところにラムダ王国と言う平和な国があった。ラムダ王国の平和はラムダ騎士団によって守られていた。\n\nラムダ騎士団のリーダーは紫導師と呼ばれるハッカーだった。ラムダ騎士団は紫導師の指導の下、日々鍛錬を重ねていた。\n\nそんなある日、城壁のそばに転がっていた古びたSymbolicsの中に迷宮を作って住み着く邪悪なニシキヘビが、紫導師の寝室に忍び込み、熟睡中の紫導師の耳元で悪魔の言葉を大阪弁で囁いた。\n\n「今更マクロなんて流行りまへんがな。時代は豊富な組み込みライブラリでっせ、旦那。」\n\nこの言葉は呪詛となり、あろう事か紫導師はニシキヘビと共に電脳迷宮の奥深くへと消えて行った。\n\n"
"紫導師の突然の失踪は即ラムダ騎士団の弱体化に繋がった。リーダーを失ったラムダ騎士団はもはや烏合の衆と成り果て、ラムダ王国は存亡の危機へと陥った。\n\n事態を重く見たラムダ国王はラムダ騎士団の立て直しを図り、世界に散らばった逸材を発掘するため、ある召集を行った。\n\n「我らが偉大な王、ラムダ国王の召集である。皆の者、心して聞くように。元主席ハッカー紫が、ニシキヘビの陰謀に嵌り連れ去られてしまった。ニシキヘビは勝手に町外れに転がっている中古のSymbolicsの電脳世界に迷宮を掘り、モンスターを離して立てこもっておるのだ。かの悪逆非道のニシキヘビを成敗し、紫導師を救い出すのだ!さすれば、ラムダの騎士の称号とラムダ騎士団への入隊、更には多額の賞金が贈られるであろう!ポール・グレアムも実はそうやって金を稼いだのだがそれは内緒にしててね!!」\n\nこの召集に、ラムダ王国だけではなく、各地からあらゆる種族のハッカー達が集まった。金に困る者、己の腕を磨こうとする者たちが次々に名乗りをあげる。ラムダの騎士の名誉と一攫千金を夢見て・・・。\n\n"))

(define *castle-text*
"\t キャッスル:\n\n\t1. ギルガメッシュないとの酒場\n\t2. ハッカーの宿\n\t3. ボッタクル商店\n\t4. カント寺院\n\t5. 街外れに行く\n")

(define *gilgamesh-tavern-text*
"\t キャッスル:\n\n\t1. 仲間に入れる\n\t2. 仲間から外す\n\t3. 調べる\n\t4. ゴールドの山分け\n\t5. 外へ出る\n")

(define *edge-of-town-text*
"\t 街外れ:\n\n\t1. 迷宮に入る\n\t2. 冒険の再開\n\t3. 訓練場に行く\n\t4. ゲームの中断\n\t5. 城に戻る\n")

(define *leave-game-text*
"\tお疲れ様でした。\nこの状態でゲームを終了します。\n\n\tAで冒険を再開します\n")

;; マクロ
(define-syntax character-making
(syntax-rules ()
((_ name alist)
(define name
(alist-copy alist)))))

;; 実行部分

(define (top-page scenario)
(display scenario)
(newline)
(display "コマンド? >> ")
(let ((key (read)))
(case key
((1) (castle *castle-text*))
((2) (story-reader *story-text*))
(else (top-page scenario)))))

(define (story-reader scenario)
(do ((ls scenario (cdr ls))
(key #f (read)))
((null? ls) (top-page *top-page-text*))
(display (car ls))))

(define (castle scenario)
(display scenario)
(newline)
(display "コマンド? >> ")
(let ((key (read)))
(case key
((1) (gilgamesh-tavern *gilgamesh-tavern-text*))
((2) (adventurer-inn))
((3) (boltac-trading-post))
((4) (temple-of-cant))
((5) (edge-of-town *edge-of-town-text*))
(else (castle scenario)))))

(define (gilgamesh-tavern scenario)
(display scenario)
(newline)
(display "コマンド? >> ")
(let ((key (read)))
(case key
((1) (add-character))
((2) (remove-character))
((3) (inspect-character))
((4) (divvy-gold))
((5) (castle *castle-text*))
(else (gilgamesh-tavern scenario)))))

(define (add-character) (display "工事中\n"))
(define (remove-character) (display "工事中\n"))
(define (inspect-character) (display "工事中\n"))
(define (divvy-gold) (display "工事中\n"))

(define (adventurer-inn)
(display "工事中\n"))

(define (boltac-trading-post)
(display "工事中\n"))

(define (temple-of-cant)
(display "工事中"))

(define (edge-of-town scenario)
(display scenario)
(newline)
(display "コマンド? >> ")
(let ((key (read)))
(case key
((1) (maze))
((2) (restart-an-out-party))
((3) (training-grounds))
((4) (leave-game *leave-game-text*))
((5) (castle *castle-text*))
(else (edge-of-town scenario)))))

(define (maze) (display "工事中\n"))
(define (restart-an-out-party) (display "工事中\n"))
(define (training-grounds) (display "工事中\n"))

(define (leave-game scenario)
(display scenario)
(newline)
(display "コマンド? >> ")
(let ((key (read)))
(if (eqv? key 'a)
(edge-of-town *edge-of-town-text*)
(exit))))

(define (inheritance key datum)
(lambda (alist)
(let ((ls (alist-delete key (alist-copy alist))))
(alist-cons key datum alist))))

(top-page *top-page-text*)

2009年12月23日水曜日

How to make an adventure game built on Scheme vol.3

どうも、PLT Schemeの構造体のアクセサをズラズラ書き並べるのがあまり美しくないです。
make-scenarioだらけ。
やってみると、高階手続きでマッピングしながら構造体を作成する事も出来るっぽいんで、コードをそのように修正してみます。
本体はともかくとして、こっちの方がSchemeっぽいのではないでしょうか。

;; PLT Scheme 依存

(define-struct scenario
(iKind ;処理の種類(0 -> 通常文章 1-> 選択肢 2-> キー待ち)
pString ;文章
idx1 ;次のシナリオのインデックス(-1で終了)
idx2 ;次のシナリオのインデックス
))

(define *s-aScenario*
(list->vector
(map (lambda (x)
(apply make-scenario x))
'((0 "シナリオ1\n" 1 0)
(1 "選択肢1 -> 1 選択肢2 -> 2\n" 2 3)
(0 "シナリオ2\n" 9 0)
(0 "シナリオ3\n" 4 0)
(1 "選択肢1 -> 1 選択肢2 -> 2\n" 5 6)
(0 "シナリオ4\n" 9 0)
(0 "シナリオ5\n" 7 0)
(2 "Hit Any Key" 8 0)
(0 "シナリオ6\n" 9 0)
(0 "エンドシナリオ\n" -1 0)))))

(define (main)
(do ((index 0
(let ((it (vector-ref *s-aScenario* index)))
(case (scenario-iKind it)
((0) (scenario-idx1 it))
((1) (let ((getche (read)))
(if (= getche 1)
(scenario-idx1 it)
(scenario-idx2 it))))
((2) (let ((getche (read)))
(scenario-idx1 it)))))))
((< index 0))
(display (scenario-pString (vector-ref *s-aScenario* index)))))


vol.4があるかどうかは知らない。

How to make an adventure game built on Scheme vol.2

取りあえず何も考えずに上のリンクに従って実装。


;; PLT Scheme 依存

(define-struct scenario
(iKind ;処理の種類(0 -> 通常文章 1-> 選択肢 2-> キー待ち)
pString ;文章
idx1 ;次のシナリオのインデックス(-1で終了)
idx2 ;次のシナリオのインデックス
))

(define *s-aScenario*
(vector
(make-scenario 0 "シナリオ1\n" 1 0)
(make-scenario 1 "選択肢1 -> 1 選択肢2 -> 2\n" 2 3)
(make-scenario 0 "シナリオ2\n" 9 0)
(make-scenario 0 "シナリオ3\n" 4 0)
(make-scenario 1 "選択肢1 -> 1 選択肢2 -> 2\n" 5 6)
(make-scenario 0 "シナリオ4\n" 9 0)
(make-scenario 0 "シナリオ5\n" 7 0)
(make-scenario 2 "Hit Any Key" 8 0)
(make-scenario 0 "シナリオ6\n" 9 0)
(make-scenario 0 "エンドシナリオ\n" -1 0)))

(define (main)
(do ((index 0
(let ((it (vector-ref *s-aScenario* index)))
(case (scenario-iKind it)
((0) (scenario-idx1 it))
((1) (let ((getche (read)))
(if (= getche 1)
(scenario-idx1 it)
(scenario-idx2 it))))
((2) (let ((getche (read)))
(scenario-idx1 it)))))))
((< index 0))
(display (scenario-pString (vector-ref *s-aScenario* index)))))


個人的な好みで言うと、Cのコードってそんなに綺麗と思わない。重複多いし。
Schemeへの翻訳は結構大変だったりします。

vol.3があるかどうかは知らない。

How to make an adventure game built on Scheme vol.1

取りあえず何も疑問を抱かずに上のリンクの方針に従う。

(define (main)
(display "シナリオ1\n")
(display "選択肢1 -> 1 選択肢2 -> 2\n")
(let loop ()
(let ((i (read)))
(cond
((not (integer? i))
(display "間違った入力です\n")
(loop))
((= i 1)
(display "シナリオ4\n"))
(else
(display "シナリオ5\n")))))
(display "エンドシナリオ\n"))

vol.2があるかどうかは知らない。