スクリプトとプログラムの個人的定義
良くスクリプトとプログラムって何が違うんだ、みたいな話があります。基本的にはどちらも似たようなモノなのは確かで、一般的には両方ともプログラム、です。良く
「小さい、一つの事しかしないようなものがスクリプトで、大掛かりになるとプログラム」
なんて言われますよね。
僕個人は、最近の考えだと、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アプリが嫌い、使うのも嫌だし作るのも嫌、って人が多いみたいなんですが、指針としては
- REPLをキチンと分離して備えたCLIアプリケーションを書く
- 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で作ったパーツから組み上げられてないとダメだろう、って事ですね。
レモネードスタンド
とまあ、アイディアはあるんだけど、じゃあ何実装してみるか、ってのが思いつかない時期が続いていたわけです(笑)。
あんまり複雑じゃなくて、でもキチンとしたアプリケーションの体裁はあって、上記のアイディアを使えるネタ、ってのが何かねぇかなぁ、と、ずーっとボーっと考えていたわけですが(笑)。
んで最近、
とか思いついたわけですね。これなら単純だからOOPに直しながら実装するにせよ、手間はいきなり本格的なアプリケーション書くよりはラクだろうし、遊べるし、昔は端末上で動いてたわけだからCLIのREPLにすることが出来る、って条件もクリアしています。
それでなるたけ初期のパソコン黎明期でのBASICで書かれた単純なゲームを探しました。なおかつ、どっかでソースコードが公開されてるものがイイ。
そんなんで、見つけたのは、昔のApple ][でのゲームで次のような有名なゲームです。
んで最近、
「あ、昔のBASICで書かれたゲームとかをPythonで実装してみりゃエエかもしんない。」
とか思いついたわけですね。これなら単純だからOOPに直しながら実装するにせよ、手間はいきなり本格的なアプリケーション書くよりはラクだろうし、遊べるし、昔は端末上で動いてたわけだからCLIのREPLにすることが出来る、って条件もクリアしています。
それでなるたけ初期のパソコン黎明期でのBASICで書かれた単純なゲームを探しました。なおかつ、どっかでソースコードが公開されてるものがイイ。
そんなんで、見つけたのは、昔のApple ][でのゲームで次のような有名なゲームです。
日本では通称レモネードって呼ばれてるんですが、パソコン黎明期では非常に人気があった模様です。
1988年出版の電視遊戯大全と言う本には次のように紹介されています。
BASIC言語で書かれた「レモネード」。ほうほうなるほど。日本で紹介されたんなら日本でソース公開されてるかもしんねぇな、とか思って探してみたんですが・・・。
日本では「イスカンダルの豆腐屋ゲーム」
として紹介された。
イスカンダルのトーフ屋ゲームはレモネードスタンドじゃなかった!
意外とアッサリと、現在でもイスカンダルのトーフ屋ゲームを公開してる作者さんのサイトを見つける事が出来ました。
ところがここに驚くべき記述が成されてたんですね(笑)。
当時 Apple ][ にレモネード屋さんを経営するゲームがあって、トーフ屋ゲームはそれを真似したのだろうという記述を時々みかけるが、それは間違い。おいらはそれを言われるまでレモネードゲームのことを知らなかった。(今もレモネードゲームの詳細は知らないし、たぶんプレイしたこともない)何と、そうなんだ(笑)!
確かに上にリンク貼ったレモネードスタンドのゲーム内容とイスカンダルのトーフ屋ゲームは似てはいますが全く違いますね。レモネードスタンドの方がゲームとしては遥かに複雑です。
違う点を上げると
- レモネードスタンドはレモネードの販売価格を自分で決定出来る。
- 売るための広告費をかけることが出来る。
- 対戦相手はいない。
等です。「経営戦略シミュレーション」と言うならレモネードスタンドの方で、一方、イスカンダルのトーフ屋ゲームはどっちかと言うとやっぱあくまで「対戦用」アプリです。
恐らく、レモネードスタンド = イスカンダルのトーフ屋、って誤解はそれこそ、上にあげた電脳遊戯大全の記述ミスから広まったものじゃないでしょうか。
ところで、どっちがプログラムとしてシンプルなのか、と言うと恐らくイスカンダルのトーフ屋ゲームの方がよりシンプルで、今回のお題には適しているようです。
と言うわけで、Python2.7で、OOPで、REPLモデルとしてイスカンダルのトーフ屋を実装してみます。
方針としては次のようにしました。
- オブジェクト指向をフルに使って関数等は用いない。
- 入力と出力は完全に評価機構から切り離す。
これはtwitterなんかでオブジェクト指向な言語(例えばC++)を使いこなしてる人達からすると甚だ極端な方針らしくって、特に本当にデータ駆動型プログラミングで入出力を完全に切り離す、と言うのはあまり直感的では無い模様です。ただまあ、一応実験だし、先にも書いた「GUIアプリへのコンバート」を考えると極端でも行けるトコまで行ってみよう、と思いました。
なお、イスカンダルのトーフ屋ゲームの作者さんのサイトの記述ですと、ソースが公開されてる、って書いてたんですが見当たらなかったんで、中の計算ロジックは、次の山之内案山子さんが作成されたソースを参考にさせて頂きました。
イスカンダルのトーフ屋ゲームとは?
では実際にプログラムしていく前にゲームの詳細(って程じゃないけど)を見てみましょう。
原版ではゲームを開始する際にゲームの説明をするかどうか訊いてくるんですが、その文章でどんな感じのプログラムになるのか、想像が付くと思います。
以下がその解説文です。
とまあ、これが仕様(?)です。
でははじめましょうか。
イスカンダルのトーフ屋ゲームとは?
では実際にプログラムしていく前にゲームの詳細(って程じゃないけど)を見てみましょう。
原版ではゲームを開始する際にゲームの説明をするかどうか訊いてくるんですが、その文章でどんな感じのプログラムになるのか、想像が付くと思います。
以下がその解説文です。
ここはイスカンダル星。あなたはここでトーフ屋を経営し、
地球への帰還費用を作り出さなくてはいけません。
でもお向かいには、コンピュータが経営するトーフ屋があります。。。
トーフの原価は1個40円、販売価格は50円です。
1日に売れる個数は天候に左右されます。
トーフは日持ちしないので、売れ残った分はすべて廃棄します。
そこで、次の日の天気予報を良く見て、何個作るか決心してください。
所持金5千円からはじめて早く3万円を超えた方が勝ちです。
とまあ、これが仕様(?)です。
でははじめましょうか。
実行: eval (GameMasterクラス)
なお、本当はこの順番で書き上げて行ったわけではなくって、行ったり来たりかなり自分で書いてるソース内で迷子になってました(笑)。OOPはデータの割り振りが難しくってアッチコッチに飛ぶんで閉口してました。基本的には、やっぱ機能(関数やらプロシージャ)で分けて書いた方がラクだと思いますね。OOPは一筋縄じゃいきまへん。
取り合えずはゲームの心臓部eval(GameMasterクラス)の定義から。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class GameMaster: | |
u"""ゲームの評価システム | |
REPLのEval部分を司る""" | |
def __init__(self): | |
self.env = Environment(Player(), Computer()) | |
self.strangeFlag = True | |
def Eval(self, x): | |
u"""評価マシン""" | |
if x[0] == 'introduction': | |
Parser.phase = 'instruction' | |
return (x[0], None) | |
elif x[0] == 'instruction': | |
return self.__instruction(x[1], self.env) | |
elif x[0] == 'input-numbers': | |
Parser.phase = 'next-day' | |
self.env['player'].makeTofu(x[1], self.env['tofu']) | |
num = self.env['computer'].makeTofu(self.env) | |
return ('opponent-turn', num) | |
elif x[0] == 'next-day': | |
Parser.phase = 'test' | |
fact = self.env['weather'].result() | |
[self.__calculation(i, fact, self.env) for i in\ | |
[self.env['player'], self.env['computer']]] | |
return (x[0], fact) | |
elif x[0] == 'test': | |
return self.__testWhoIsWinner(self.env) | |
elif x[0] == 'play-again?': | |
return self.__playAgain_p(x[1], self.env) | |
def __instruction(self, x, env): | |
u"""ゲームの解説を見るか見ないか尋ねる""" | |
if x is self.strangeFlag: | |
self.strangeFlag = False | |
return ('instruction', None) | |
else: | |
Parser.phase = 'input-numbers' | |
self.env['weather'] = Weather() | |
return ('show-data', env) | |
def __calculation(self, x, fact, env): | |
u"""トーフ屋の日割り決算""" | |
if x['tofu'] > env['tofu'][fact]: | |
sold = env['tofu'][fact] | |
else: | |
sold = x['tofu'] | |
x['money'] = x['money'] + sold * env['tofu']['price'] -\ | |
x['tofu'] * env['tofu']['cost'] | |
def __testWhoIsWinner(self, env): | |
u"""ゲームオーバーの条件を調べ勝者敗者を決定する""" | |
if self.__test(env): | |
Parser.phase = 'play-again?' | |
return ('who-is-winner?', self.__whoIsWinner(env)) | |
else: | |
Parser.phase = 'input-numbers' | |
self.env['weather'] = Weather() | |
return ('show-data', env) | |
def __test(self, env): | |
u"""ゲームを続けられる状態か否かの調査""" | |
if env['player']['money'] >= env['game-over'] or\ | |
env['computer']['money'] >= env['game-over']: | |
return True | |
elif env['player']['money'] < env['tofu']['cost'] or\ | |
env['computer']['money'] < env['tofu']['cost']: | |
return True | |
else: | |
return False | |
def __whoIsWinner(self, env): | |
u"""ゲームの勝者判定""" | |
if env['player']['money'] > env['computer']['money']: | |
return 'you-win' | |
elif env['player']['money'] < env['computer']['money']: | |
return 'you-lose' | |
else: | |
return 'even' | |
def __playAgain_p(self, x, env): | |
u"""コンティニュー?""" | |
if x: | |
Parser.phase, self.env['player'], \ | |
self.env['computer'], \ | |
self.env['weather'] =\ | |
'input-numbers', Player(), \ | |
Computer(), Weather() | |
return ('show-data', env) | |
else: | |
sys.exit() |
実装方針としては、
- 初期化時点でゲームに使う環境をセットする。(self.env)
- 入力自体は扱わない。仮引数xに手渡された情報を元に計算する。
- readから仮引数xに渡される情報は現状況と入力結果(あるいはNone)の2要素のタプルとする。
- 出力自体も扱わない。出力クラスに現状況と計算結果(あるいはNone)の2要素のタプルを渡せるようにEvalを設計する。
- 各条件節で行う計算は一つ(つまり、出力クラスに渡す情報は一つ)のみに仕上げる。とにかく短くしてすぐ出力クラスに渡す。eval内でもたつかない。
とかまあ、簡単に書いてますけど、実際は紆余曲折あって泣きそうでした(笑)。
最初は環境も大域変数にして、一種のフラグとして書き換える形式で作り出してたんですが、どうもOOPやってる割には綺麗じゃねえな、ってんで、最終的には環境(つまり、プレイヤー情報、対戦相手のコンピュータ情報、トーフの情報、ゲームオーバーになる得点)はこのGameMasterクラス内に内包する事で落ち着きました。
また、プログラミング言語のインタプリタですと環境が重要なのはevalだけ、で、基本的にはパーズは噛ますんですが、read機構は何でも受け入れてしまうわけですね。
一方、ゲームの場合、例えばこのイスカンダルのトーフ屋ゲームですが、整数値だとか、Yes/No形式の二種類の入力(と何も入力が成されない/必要ないと言う情報も合わせれば三種類)を「現在のゲームの進行状況に合わせて」切り替えないといけません。つまり、Read自体がゲームの進行状況をある程度把握してなきゃならないんです。一方、状況を把握するのは切り分けだとあくまでevalなわけで・・・。うううううむ。
ってなわけで、あとでreadのコードも見せますが、read内でクラス変数としてphaseと言うフラグをセットして、evalが実行された際に「次のフェーズはこれになるよ」と言うのをreadに教える事にしています。苦肉の策です。
なお、クラス変数ってのは一種の大域変数のようなモノで・・・各インスタンス共通の変数として大本を変更すれば全インスタンスも影響を受ける、と言う大変危険な変数です(笑)。しかし、ここが使いドコロで、ここでこれ出すしか無かった(苦笑)。あんまこう言うアクセス手段って綺麗じゃないんですけどね。しかしながら大域変数使いたくねぇ、ってのならこれしか手がない。
ちなみに、evalの各節を実行した例は次のようになっています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> g = GameMaster() | |
>>> g.Eval(('introduction', None)) | |
('introduction', None) | |
>>> g.Eval(('instruction', None)) | |
('show-data', {'game-over': 30000, 'player': {'money': 5000, 'tofu': 0}, 'computer': {'money': 5000, 'tofu': 0}, 'tofu': {'price': 50, 'cost': 40, 'sunny': 500, 'rainy': 100, 'cloudy': 300}, 'weather': {'rainy': 68, 'sunny': 0, 'cloudy': 32}}) | |
>>> g.Eval(('input-numbers', 125)) | |
('opponent-turn', 100) | |
>>> g.Eval(('next-day', None)) | |
('next-day', 'rainy') | |
>>> g.Eval(('test', None)) | |
('show-data', {'game-over': 30000, 'player': {'money': 5000, 'tofu': 125}, 'computer': {'money': 6000, 'tofu': 100}, 'tofu': {'price': 50, 'cost': 40, 'sunny': 500, 'rainy': 100, 'cloudy': 300}, 'weather': {'rainy': 30, 'sunny': 15, 'cloudy': 55}}) | |
>>> g.Eval(('play-again', True)) | |
>>> g.Eval(('play-again', False)) | |
>>> |
何が何だか分かりませんね(笑)。取り合えず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つが要点です。
これは古典的なプログラミングに於いてはまさしく大域変数で解決すべきだろう問題です。つまりフェーズ情報を大域変数として束縛しておいて、随時それを参照する・・・・・・。
実はこの初期のプログラムだとそう言うカタチで作ってたんですが、どうもそれだとスッキリしなかったんですね。eval(GameMasterクラス)内部の定義が大域変数塗れになってどーにもこーにも汚ねぇな、と(笑)。しかもそんなプログラムだとOOPやってる意味が無い、とまでは言わなくても相当薄れるんですね。
そこでその辺を何とか解決した(あるいはしようとした)のが次のコードです。
Parserクラスでは最初にクラス変数phaseを定義しています。これがこのプログラムのキモです。
クラス変数は初期状態では'introduction'に設定されてます。このクラス変数にアクセスするとこのクラスから作られた全インスタンス内のクラス変数は全部変更されるんですね。
ちと試してみますか。
最初にParserクラスから6つ個別にインスタンスを生成しています。OOPの理屈から言うと、この6つのインスタンスは「互いに独立じゃないとならない」前提なんですが、あら不思議。クラス変数に関して言うと親元のクラスで変更すると生成された全インスタンスのクラス変数が変更されてしまうのです(と言うか、それこそがクラス変数の「機能」なんですが)。
これは見るからに「危険な機能」ですね(笑)。ある意味大域変数と同じで、そもそもOOPでクラス作成してインスタンスを生成するのは「個別のデータの独立性を高める為」な筈なんですが、このクラス変数の存在ってのはそれで言うと「綻び」です。しかし、こう言うお題の場合は役に立つ。
なお、readとeval間のフェーズの伝達は次のようになってます。
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のプログラムを書く時が来たら、この指針を応用出来そうだ、って感触を得たのは大きいですね。
(しかし、こうやってテスト実行するのがかなりメンドい辺りがやっぱOOPの欠点だと思う)
Environmentクラス
次はevalが初期化に用いる環境クラスを定義します。
ゲームも言語インタプリタと同じで、情報を環境として保持していなければなりません。環境とは、例えばこのゲームの場合、プレイヤー情報(いくらお金を持ってるか、とか)、対戦相手のコンピュータの情報、トーフの情報(製造コストはいくらか、売値はいくらか、とか)、GameOverに達するお金の額とか、ですね。
これらの情報の保持は、要するに言語インタプリタに於いて、例えば実行した変数なり計算結果なりの保持であるとか、あるいはどんな関数が使えるのか、とかの記憶と全く同じです。
ただし、幸いなのは、言語インタプリタと違って、例えばレキシカルクロージャを作るように仕掛けるとか、そう言った技術的に面倒な面は全く無いんで、そう言う意味ではラクです。
では、Environmentクラスの定義です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Environment(UserDict): | |
u"""プレイヤーとコンピュータの情報を保持する""" | |
def __init__(self, player1, player2,\ | |
tofu = Tofu(), gameover = 30000): | |
UserDict.__init__(self) | |
self.data = {'player' : player1,\ | |
'computer' : player2,\ | |
'tofu' : tofu,\ | |
'game-over' : gameover} |
なお、UserDictionaryクラスを継承してますが、これはEnvironmentクラスのインスタンス変数を参照する際に、辞書(ディクショナリ)みたいに[]を使って参照出来てラクだろう、ってんで使っています。
(しかし実際出来たコードは辞書だらけみてぇになってあまり見やすいとも思えない・・・ orz)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> e = Environment(Player(), Computer()) | |
>>> e['player'] | |
{'money': 5000, 'tofu': 0} | |
>>> e['computer'] | |
{'money': 5000, 'tofu': 0} | |
>>> e['tofu'] | |
{'price': 50, 'cost': 40, 'sunny': 500, 'rainy': 100, 'cloudy': 300} | |
>>> e['game-over'] | |
30000 | |
>>> |
この例では、プレイヤー、コンピュータ共に持ってる初期金額は5,000円、作ったトーフは0個、トーフの売値は50円、製造コストは40円、天候によって売れる最大個数、あとはゲームオーバーに達する目標金額は30,000円に設定されてる、って事が分かります。
で、Environmentクラスにぶち込んでいるインスタンスの製造元であるクラスを見てみましょう。
TofuSellerクラス/Playerクラス
まずはゲームの主体であるプレイヤーの振る舞いの基礎になるTofuSellerクラス、それとプレイヤーを演じるPlayerクラスから見てみます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class TofuSeller(UserDict): | |
u"""トーフ屋のルートクラス""" | |
def __init__ (self): | |
UserDict.__init__(self) | |
self.data = {'money' : 5000,\ | |
'tofu' : 0} | |
def makeTofu(self, number, tofu): | |
u"""トーフをいくつ作るか計算""" | |
if number > tofu.maximum(self): | |
return tofu.maximum(self) | |
else: | |
self['tofu'] = number | |
return self['tofu'] | |
class Player(TofuSeller): | |
u"""トーフ屋クラスを継承したプレイヤークラス""" | |
def __init__(self): | |
TofuSeller.__init__(self) |
ちなみに、ここがOOPの難しいトコなんですが、ここではTofuSellerクラス/Playerクラスに「トーフをいくつ作るか決定する」属性を付加してるんですが、どうなんでしょうねぇ。こう言う「計算」はevalが持つべきなのか否か・・・・・・。
世の中オブジェクトだらけなんで、オブジェクトで考える方が自然だ、とOOP信奉者はいいますが、なかなかどうして。こう言うデータ駆動で、「あるメソッドを何に属させるべきか決める」ってのはそんなに自明だとは思えません。
例えば「計算機メソッド」ってのを定義して、これがゲームマスターが持つべきかプレイヤーが持つべきか、ってのはこれはどう考えれば良いのでしょうか。両者とも持ってるべき?しかし機能中心主義なら機能だけ定義すれば良いんで「属性」とか考えなくって済むんですよね。この辺OOP信者の言う事はちと理想論な気がします。
まあ、あまり考え過ぎなきゃいい話なんですが、こだわれば厳密なトコではオブジェクトが何を意味すべきか、やっぱり分からなくなっちゃうんですね(笑)。
ちなみに、ここでもインスタンス変数へのアクセスを辞書っぽく行えるようにUserDictを継承しています。
Computerクラス
次は対戦相手のComputerクラスです。こいつもTofuSellerクラスを継承して作ります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Computer(TofuSeller): | |
u"""トーフ屋クラスを継承したコンピュータクラス""" | |
def __init__(self): | |
TofuSeller.__init__(self) | |
def makeTofu(self, env): | |
u"""トーフを何個作るか自動計算""" | |
def calc(money): | |
u"""持ち金とのバランスを見る""" | |
maximum = 1.0*self['money']/env['tofu']['cost'] | |
if money >= maximum: | |
return int(maximum) | |
else: | |
return money | |
cost = env['tofu']['cost'] | |
if env['weather']['rainy'] > 30: | |
number = env['tofu']['rainy'] | |
self['tofu'] = number | |
elif env['weather']['sunny'] > 49: | |
number = calc(env['tofu']['sunny']) | |
self['tofu'] = number | |
else: | |
number = calc(env['tofu']['cloudy']) | |
self['tofu'] = number | |
return self['tofu'] |
Tofuクラス
んでお次はトーフを表すTofuクラス。何かこれが一番オブジェクトっぽいな(笑)。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Tofu(UserDict): | |
u"""トーフに関する情報""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.data = {'cost' : 40, 'price' : 50,\ | |
'sunny' : 500, 'cloudy' : 300, 'rainy' : 100} | |
def maximum(self, player): | |
u"""プレイヤーの資金でいくつトーフが作れるのか情報を返す""" | |
return player['money'] / self['cost'] |
まあ、このmaximumメソッドも本当はPlayer/Computerクラスが持つべきなのかevalが持つべきなのか(ry
一応ここではトーフの最大個数はトーフが決める事にしております(強引)。
Weatherクラス
GameOverに達する金額はわざわざクラスにする必要もないんで、そのままEnvironmentクラスに突っ込んでます。ここではもうひとつ、このゲームに重要な「天候に関する情報」を司るWeatherクラスを見てみます。
Environmentクラスは初期状態ではWeatherクラスのインスタンスは持ってないんですが、ゲームが進んで行くにつれて辞書的技でWeatherクラスがGameMasterクラスのインスタンス変数(self.env['weather'])として代入されます。
んで実際問題、1ターン(全フェーズ消化)につき1回、Weatherクラスのインスタンスは代入され続けるわけです。
と言うのも、Weatherクラスは中に確率マシンを仕込んでまして、初期化に於いて一回「天気予報」を計算します。これは固定されて変更されません。つまり1ターン内には使いまわされるわけですね。
そしてもう一つの大事な機能が「実際の天気を決定する」機能です。これは予報結果を閾値として用いてまたもや確率計算して天気を決定します。
まあ、大まかにはそんなトコで、取り合えずコードを見てみましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Weather(UserDict): | |
u"""天候に関する情報をコントロールする""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.prob = (randint(0, 100), randint(0, 100)) | |
self.data = {'sunny': 0, | |
'cloudy': 0, | |
'rainy' : 0} | |
if self.prob[0] >= self.prob[1]: | |
self['sunny'] = 100 - self.prob[0] | |
self['rainy'] = self.prob[1] | |
else: | |
self['sunny'] = 100 - self.prob[1] | |
self['rainy'] = self.prob[0] | |
self['cloudy'] = 100 - self['sunny'] - self['rainy'] | |
def report(self): | |
u"""天気予報の情報を返す""" | |
return self | |
def result(self): | |
u"""実際の天気が何なのか返す""" | |
r = randint(0, 100) | |
if r <= self['rainy']: | |
return 'rainy' | |
elif r <= self['rainy'] + self['cloudy']: | |
return 'cloudy' | |
else: | |
return 'sunny' | |
ちなみに、全フェーズとは、eval基準で見ると基本的には
'input-numbers' -> 'next-day' -> 'test' -> 'input-numbers'へ戻って繰り返し
と言う3フェーズの事で、Evalを延々とこの3フェーズでloopさせるわけですが、要するにこの3フェーズ内で必ず一回self.env['weather']にWeatherクラスがインスタンス変数として代入されるわけです。
構文解析: read(Parseクラス)
まあ、構文解析って程じゃないんですがね。ただ、先程も書いた通り、ゲームのread機構なんで言語インタプリタには無い特徴があります。
このゲームの場合、次の3つが要点です。
- ゲームの全てのフェーズに於いて入力が必要なわけではない。従って、多くの場合では呼び出された時点で自動的に何らかの返り値をevalに渡してゲーム(あるいはloopを)進めないとならない。
- 整数だけしか受け付けないフェーズがある。
- YES/NO(あるいはyes/noとか)やY/N(またはy/n)しか受け付けないフェーズがある。
さて、これが一番困ったトコなんですよね。
先ほども書きましたが、言語インタプリタの場合、パーズはしますが(その結果、例えば入力エラー等をユーザーに教える)、原則的には「どんな入力も」取り合えずは受け入れます。つまり、evalみたいに環境を参照する必要が無いわけです。
一方、このようなゲームの場合、read自体が「フェーズによって」挙動を変えます。いや、変えないといけない。ゲームが要求する入力だけを受け、違反な入力等を過度に避ける為にはそうするしかない。従って、環境を直接参照する事が無いにせよ、何らかのカタチで「現フェーズの情報」を持ってないといけないんですね。
しかしながら、フェーズ自体は原則的にevalの為のモノで、かつevalで決定されるべきモノです。果たしてevalが計算する前にreadが次のフェーズを知る事なんて出来るのか・・・・・・?
これは古典的なプログラミングに於いてはまさしく大域変数で解決すべきだろう問題です。つまりフェーズ情報を大域変数として束縛しておいて、随時それを参照する・・・・・・。
実はこの初期のプログラムだとそう言うカタチで作ってたんですが、どうもそれだとスッキリしなかったんですね。eval(GameMasterクラス)内部の定義が大域変数塗れになってどーにもこーにも汚ねぇな、と(笑)。しかもそんなプログラムだとOOPやってる意味が無い、とまでは言わなくても相当薄れるんですね。
そこでその辺を何とか解決した(あるいはしようとした)のが次のコードです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Parser: | |
u"""REPLのRead部分を司る""" | |
phase = 'introduction' | |
def __init__(self): | |
pass | |
def read(self): | |
u"""入力関数 | |
phaseの情報によって動作が変わる""" | |
if self.phase == 'input-numbers': | |
return (self.phase, self.__integer()) | |
elif self.phase == 'play-again?' or\ | |
self.phase == 'instruction': | |
return (self.phase, self.__yes_or_no_p()) | |
else: | |
return (self.phase, None) | |
def __integer(self): | |
u"""整数の入力をコントロール""" | |
var = raw_input() | |
try: return int(var) | |
except ValueError: | |
return self.__integer() | |
def __yes_or_no_p(self): | |
u"""入力がYesかNoか判定して真偽値を返す""" | |
def y_or_n_p(string): | |
if string.upper() in ('Y', 'YES'): | |
return True | |
else: | |
return False | |
t = ('Y', 'YES', 'N', 'NO') | |
i = raw_input() | |
if i.upper() in t: | |
return y_or_n_p(i) | |
else: | |
return self.__yes_or_no_p() |
クラス変数は初期状態では'introduction'に設定されてます。このクラス変数にアクセスするとこのクラスから作られた全インスタンス内のクラス変数は全部変更されるんですね。
ちと試してみますか。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> p, q, r, s, t, u = Parser(), Parser(), Parser(), Parser(), Parser(), Parser() | |
>>> p.phase | |
'introduction' | |
>>> q.phase | |
'introduction' | |
>>> r.phase | |
'introduction' | |
>>> s.phase | |
'introduction' | |
>>> t.phase | |
'introduction' | |
>>> u.phase | |
'introduction' | |
>>> Parser.phase = 'instruction' | |
>>> p.phase | |
'instruction' | |
>>> q.phase | |
'instruction' | |
>>> r.phase | |
'instruction' | |
>>> s.phase | |
'instruction' | |
>>> t.phase | |
'instruction' | |
>>> u.phase | |
'instruction' | |
>>> |
これは見るからに「危険な機能」ですね(笑)。ある意味大域変数と同じで、そもそもOOPでクラス作成してインスタンスを生成するのは「個別のデータの独立性を高める為」な筈なんですが、このクラス変数の存在ってのはそれで言うと「綻び」です。しかし、こう言うお題の場合は役に立つ。
なお、readとeval間のフェーズの伝達は次のようになってます。
- evalが実行される度に、「次のフェーズ」をParserのクラス変数phaseにアクセスしてセットする。
- readは自らのクラス変数phaseを参照して挙動を変える
- readは入力された/されないに関わらず、自らのクラス変数phaseの値と入力値/Noneの2要素をタプルとしてevalに渡す
- evalはタプルの第一要素を見て現在のフェーズを知る
つまり、フェーズと言う情報に関してはevalとreadの間でグルグル回ってるわけです。
なお、readメソッドは次の挙動を実装しています。
- 何も受け取らずにフェーズ情報とNoneのタプルを即座に返す。
- フェーズ情報に従って整数値だけを受け取り、フェーズ情報と入力された整数値のタプルを返す。
- フェーズ情報に従ってYES/NO及びそれに類する文字情報だけ受け取り、フェーズ情報と真偽値のタプルを返す。
3番目の機能の実装はCommon Lispの大変アホな便利な関数、yes-or-no-pの名前を借りてきてます。これはCommon Lispよろしく、入力された文字列を一旦大文字に変換して比較するようにしてますね。これって地味な関数なんですが、Common LispみたいにPythonにもデフォであったらイイのになぁ関数であります。
Parserクラスのインスタンスの実行例は以下のような感じです。
ご覧のように、クラス変数で与えられた情報に従って挙動が変わります。入力を必要としなかったり、あるいは入力値が期待されたモノじゃない場合、新たな(正しい)入力が来るまで待ちが入ります。
Parserクラスのインスタンスの実行例は以下のような感じです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
>>> p = Parser() | |
>>> p.read() | |
('introduction', None) | |
>>> Parser.phase = 'input-numbers' | |
>>> p.read() | |
hoge | |
35 | |
('input-numbers', 35) | |
>>> Parser.phase = 'play-again?' | |
>>> p.read() | |
hoge | |
yes | |
('play-again?', True) | |
>>> Parser.phase = 'instruction' | |
>>> p.read() | |
fuga | |
no | |
('instruction', False) | |
>>> Parser.phase = 'next-day' | |
>>> p.read() | |
('next-day', None) | |
>>> Parser.phase = 'test' | |
>>> p.read() | |
('test', None) | |
>>> |
Messageクラス
これで最後です。Messageクラスはゲームで表示する文章を保持し、またREPLの要素の最後の一個、Print機能を司ります。
ここは基本的に出力だけなんで、文章データを含むから長いんですが、整形出力だけなんでコーディング的には大した苦労は無いパートですね。見てみましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Messages(UserDict): | |
u"""ゲームで扱うメッセージ集 | |
REPLのPrint部分を司る""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.data = {'introduction': u'イスカンダルのトーフ屋ゲーム (python版)\n\ | |
Copyright (C) 1978-2012 by N.Tsuda\n\ | |
ルール説明しますか?[y/n]', | |
'instruction' : u'ここはイスカンダル星。あなたはここでトーフ屋を経営し、\n\ | |
地球への帰還費用を作り出さなくてはいけません。\n\ | |
でもお向かいには、コンピュータが経営するトーフ屋があります。。。\n\ | |
\n\ | |
トーフの原価は1個40円、販売価格は50円です。\n\ | |
1日に売れる個数は天候に左右されます。\n\ | |
トーフは日持ちしないので、売れ残った分はすべて廃棄します。\n\ | |
そこで、次の日の天気予報を良く見て、何個作るか決心してください。\n\ | |
所持金5千円からはじめて早く3万円を超えた方が勝ちです。\n\ | |
\n\ | |
いいですか?[y/n]', | |
'money' : u'\n所持金: \n\ | |
あなた %5d円 %s%s\n\ | |
わたし %5d円 %s%s\n\n', | |
'1000-yen' : u'■', | |
'empty-yen' : u'□', | |
'weather-report' : u'明日の天気予報: 晴れ%3d%% くもり %3d%% 雨 %3d%%\n\ | |
%s%s%s\n\n', | |
'howmany-tofus' : u'トーフを何個作りますか? (1~%3d)', | |
'computer-reply' : u'わたしは%3d個作ります。\n', | |
'next-day' : u'***** 次の日 *****\n', | |
'weather-is' : u'今日の天気は', | |
'result' : u'%s です。\n', | |
'sunny' : (u'◎', u'晴れ \\(^o^)/ '), | |
'cloudy' : (u'・', u'くもり (~_~) '), | |
'rainy' : (u'●', u'雨 (;_;) '), | |
'you-win' : u'あなたの勝ちです。\n\n', | |
'even' : u'引き分けです。', | |
'you-lose' : u'コンピュータの勝ちです。\n\n', | |
'play-again?' : u'play again? [y/n]'} | |
def Print(self, x): | |
u"""出力用メソッド""" | |
if x[0] == 'show-data': | |
time.sleep(0.5) | |
print self.__showData(x[1]), | |
elif x[0] == 'opponent-turn': | |
print self.__showComputerReply(x[1]) | |
elif x[0] == 'next-day': | |
time.sleep(0.5) | |
print self.__showNextDay() | |
print self.__showWeatherIs(), | |
time.sleep(0.5) | |
for i in range(3): | |
print '.', | |
time.sleep(0.5) | |
print self.__showResult(x[1]) | |
elif x[0] == 'who-is-winner?': | |
print self[x[1]] + self['play-again?'], | |
else: | |
print self[x[0]], | |
def __showData(self, env): | |
return self.__showMoney(env) +\ | |
self.__showWeatherReport(env) +\ | |
self.__showHowmanyTofus(env) | |
def __showMoney(self, env): | |
u"""プレイヤーと敵の所持金額表示""" | |
def calc(player): | |
u"""グラフィック用の小物の為の演算処理""" | |
x = player['money'] | |
y = x/1000 | |
return [x, self['1000-yen']*y, self['empty-yen']*(30-y)] | |
return self['money'] % tuple(calc(env['player']) + calc(env['computer'])) | |
def __showWeatherReport(self, env): | |
u"""天気予報に関する処理""" | |
def calc(weather): | |
u"""グラフィック用の小物の為の演算処理""" | |
wr = weather.report() | |
keys = ('sunny', 'cloudy', 'rainy') | |
table = dict([(k, int(round(40.0*wr[k]/100))) for k in keys]) | |
return [wr[k] for k in keys] + \ | |
[self[k][0] * table[k] for k in keys] | |
return self['weather-report'] % tuple(calc(env['weather'])) | |
def __showHowmanyTofus(self, env): | |
u"""豆腐をいくつ作るか質問""" | |
return self['howmany-tofus'] % env['tofu'].maximum(env['player']) | |
def __showComputerReply(self, num): | |
u"""コンピュータの反応表示""" | |
return self['computer-reply'] % num | |
def __showNextDay(self): | |
u"""翌日表示""" | |
return self['next-day'] | |
def __showWeatherIs(self): | |
u'''今日の天気は''' | |
return self['weather-is'] | |
def __showResult(self, result): | |
u"""翌日の天候表示""" | |
return self['result'] % self[result][1] |
いずれにせよ、evalからもフェーズと何らかの計算結果(あるいはNone)のタプルがPrintメソッドに届き、そのフェーズ情報に従って必要とされる文字列を探して表示するだけ、の簡単なお仕事となっております。
REPL(Read-Eval-Print-Loop)
さて、ここまでで、インタプリタ的なread、eval、printの各部品がクラスとして実装されました。あとはこの3つを組み合わせるだけで「アプリケーション」が出来上がります。Pythonスクリプト的には次のような記述で動かせるわけですね。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if __name__ == '__main__': | |
r = Parser() | |
e = GameMaster() | |
p = Messages() | |
while True: | |
p.Print(e.Eval(r.read())) |
なお、Python版「イスカンダルのトーフ屋ゲーム」の全ソースは以下のようになっています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! c:/Python27/python.exe | |
# -*- coding: utf-8 -*- | |
from random import randint | |
from UserDict import UserDict | |
import math, sys, time | |
class Messages(UserDict): | |
u"""ゲームで扱うメッセージ集 | |
REPLのPrint部分を司る""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.data = {'introduction': u'イスカンダルのトーフ屋ゲーム (python版)\n\ | |
Copyright (C) 1978-2012 by N.Tsuda\n\ | |
ルール説明しますか?[y/n]', | |
'instruction' : u'ここはイスカンダル星。あなたはここでトーフ屋を経営し、\n\ | |
地球への帰還費用を作り出さなくてはいけません。\n\ | |
でもお向かいには、コンピュータが経営するトーフ屋があります。。。\n\ | |
\n\ | |
トーフの原価は1個40円、販売価格は50円です。\n\ | |
1日に売れる個数は天候に左右されます。\n\ | |
トーフは日持ちしないので、売れ残った分はすべて廃棄します。\n\ | |
そこで、次の日の天気予報を良く見て、何個作るか決心してください。\n\ | |
所持金5千円からはじめて早く3万円を超えた方が勝ちです。\n\ | |
\n\ | |
いいですか?[y/n]', | |
'money' : u'\n所持金: \n\ | |
あなた %5d円 %s%s\n\ | |
わたし %5d円 %s%s\n\n', | |
'1000-yen' : u'■', | |
'empty-yen' : u'□', | |
'weather-report' : u'明日の天気予報: 晴れ%3d%% くもり %3d%% 雨 %3d%%\n\ | |
%s%s%s\n\n', | |
'howmany-tofus' : u'トーフを何個作りますか? (1~%3d)', | |
'computer-reply' : u'わたしは%3d個作ります。\n', | |
'next-day' : u'***** 次の日 *****\n', | |
'weather-is' : u'今日の天気は', | |
'result' : u'%s です。\n', | |
'sunny' : (u'◎', u'晴れ \\(^o^)/ '), | |
'cloudy' : (u'・', u'くもり (~_~) '), | |
'rainy' : (u'●', u'雨 (;_;) '), | |
'you-win' : u'あなたの勝ちです。\n\n', | |
'even' : u'引き分けです。', | |
'you-lose' : u'コンピュータの勝ちです。\n\n', | |
'play-again?' : u'play again? [y/n]'} | |
def Print(self, x): | |
u"""出力用メソッド""" | |
if x[0] == 'show-data': | |
time.sleep(0.5) | |
print self.__showData(x[1]), | |
elif x[0] == 'opponent-turn': | |
print self.__showComputerReply(x[1]) | |
elif x[0] == 'next-day': | |
time.sleep(0.5) | |
print self.__showNextDay() | |
print self.__showWeatherIs(), | |
time.sleep(0.5) | |
for i in range(3): | |
print '.', | |
time.sleep(0.5) | |
print self.__showResult(x[1]) | |
elif x[0] == 'who-is-winner?': | |
print self[x[1]] + self['play-again?'], | |
else: | |
print self[x[0]], | |
def __showData(self, env): | |
return self.__showMoney(env) +\ | |
self.__showWeatherReport(env) +\ | |
self.__showHowmanyTofus(env) | |
def __showMoney(self, env): | |
u"""プレイヤーと敵の所持金額表示""" | |
def calc(player): | |
u"""グラフィック用の小物の為の演算処理""" | |
x = player['money'] | |
y = x/1000 | |
return [x, self['1000-yen']*y, self['empty-yen']*(30-y)] | |
return self['money'] % tuple(calc(env['player']) + calc(env['computer'])) | |
def __showWeatherReport(self, env): | |
u"""天気予報に関する処理""" | |
def calc(weather): | |
u"""グラフィック用の小物の為の演算処理""" | |
wr = weather.report() | |
keys = ('sunny', 'cloudy', 'rainy') | |
table = dict([(k, int(round(40.0*wr[k]/100))) for k in keys]) | |
return [wr[k] for k in keys] + \ | |
[self[k][0] * table[k] for k in keys] | |
return self['weather-report'] % tuple(calc(env['weather'])) | |
def __showHowmanyTofus(self, env): | |
u"""豆腐をいくつ作るか質問""" | |
return self['howmany-tofus'] % env['tofu'].maximum(env['player']) | |
def __showComputerReply(self, num): | |
u"""コンピュータの反応表示""" | |
return self['computer-reply'] % num | |
def __showNextDay(self): | |
u"""翌日表示""" | |
return self['next-day'] | |
def __showWeatherIs(self): | |
u'''今日の天気は''' | |
return self['weather-is'] | |
def __showResult(self, result): | |
u"""翌日の天候表示""" | |
return self['result'] % self[result][1] | |
class Weather(UserDict): | |
u"""天候に関する情報をコントロールする""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.prob = (randint(0, 100), randint(0, 100)) | |
self.data = {'sunny': 0, | |
'cloudy': 0, | |
'rainy' : 0} | |
if self.prob[0] >= self.prob[1]: | |
self['sunny'] = 100 - self.prob[0] | |
self['rainy'] = self.prob[1] | |
else: | |
self['sunny'] = 100 - self.prob[1] | |
self['rainy'] = self.prob[0] | |
self['cloudy'] = 100 - self['sunny'] - self['rainy'] | |
def report(self): | |
u"""天気予報の情報を返す""" | |
return self | |
def result(self): | |
u"""実際の天気が何なのか返す""" | |
r = randint(0, 100) | |
if r <= self['rainy']: | |
return 'rainy' | |
elif r <= self['rainy'] + self['cloudy']: | |
return 'cloudy' | |
else: | |
return 'sunny' | |
class TofuSeller(UserDict): | |
u"""トーフ屋のルートクラス""" | |
def __init__ (self): | |
UserDict.__init__(self) | |
self.data = {'money' : 5000,\ | |
'tofu' : 0} | |
def makeTofu(self, number, tofu): | |
u"""トーフをいくつ作るか計算""" | |
if number > tofu.maximum(self): | |
return tofu.maximum(self) | |
else: | |
self['tofu'] = number | |
return self['tofu'] | |
class Player(TofuSeller): | |
u"""トーフ屋クラスを継承したプレイヤークラス""" | |
def __init__(self): | |
TofuSeller.__init__(self) | |
class Computer(TofuSeller): | |
u"""トーフ屋クラスを継承したコンピュータクラス""" | |
def __init__(self): | |
TofuSeller.__init__(self) | |
def makeTofu(self, env): | |
u"""トーフを何個作るか自動計算""" | |
def calc(money): | |
u"""持ち金とのバランスを見る""" | |
maximum = 1.0*self['money']/env['tofu']['cost'] | |
if money >= maximum: | |
return int(maximum) | |
else: | |
return money | |
cost = env['tofu']['cost'] | |
if env['weather']['rainy'] > 30: | |
number = env['tofu']['rainy'] | |
self['tofu'] = number | |
elif env['weather']['sunny'] > 49: | |
number = calc(env['tofu']['sunny']) | |
self['tofu'] = number | |
else: | |
number = calc(env['tofu']['cloudy']) | |
self['tofu'] = number | |
return self['tofu'] | |
class Tofu(UserDict): | |
u"""トーフに関する情報""" | |
def __init__(self): | |
UserDict.__init__(self) | |
self.data = {'cost' : 40, 'price' : 50,\ | |
'sunny' : 500, 'cloudy' : 300, 'rainy' : 100} | |
def maximum(self, player): | |
u"""プレイヤーの資金でいくつトーフが作れるのか情報を返す""" | |
return player['money'] / self['cost'] | |
class Environment(UserDict): | |
u"""プレイヤーとコンピュータの情報を保持する""" | |
def __init__(self, player1, player2,\ | |
tofu = Tofu(), gameover = 30000): | |
UserDict.__init__(self) | |
self.data = {'player' : player1,\ | |
'computer' : player2,\ | |
'tofu' : tofu,\ | |
'game-over' : gameover} | |
class GameMaster: | |
u"""ゲームの評価システム | |
REPLのEval部分を司る""" | |
def __init__(self): | |
self.env = Environment(Player(), Computer()) | |
self.strangeFlag = True | |
def Eval(self, x): | |
u"""評価マシン""" | |
if x[0] == 'introduction': | |
Parser.phase = 'instruction' | |
return (x[0], None) | |
elif x[0] == 'instruction': | |
return self.__instruction(x[1], self.env) | |
elif x[0] == 'input-numbers': | |
Parser.phase = 'next-day' | |
self.env['player'].makeTofu(x[1], self.env['tofu']) | |
num = self.env['computer'].makeTofu(self.env) | |
return ('opponent-turn', num) | |
elif x[0] == 'next-day': | |
Parser.phase = 'test' | |
fact = self.env['weather'].result() | |
[self.__calculation(i, fact, self.env) for i in\ | |
[self.env['player'], self.env['computer']]] | |
return (x[0], fact) | |
elif x[0] == 'test': | |
return self.__testWhoIsWinner(self.env) | |
elif x[0] == 'play-again?': | |
return self.__playAgain_p(x[1], self.env) | |
def __instruction(self, x, env): | |
u"""ゲームの解説を見るか見ないか尋ねる""" | |
if x is self.strangeFlag: | |
self.strangeFlag = False | |
return ('instruction', None) | |
else: | |
Parser.phase = 'input-numbers' | |
self.env['weather'] = Weather() | |
return ('show-data', env) | |
def __calculation(self, x, fact, env): | |
u"""トーフ屋の日割り決算""" | |
if x['tofu'] > env['tofu'][fact]: | |
sold = env['tofu'][fact] | |
else: | |
sold = x['tofu'] | |
x['money'] = x['money'] + sold * env['tofu']['price'] -\ | |
x['tofu'] * env['tofu']['cost'] | |
def __testWhoIsWinner(self, env): | |
u"""ゲームオーバーの条件を調べ勝者敗者を決定する""" | |
if self.__test(env): | |
Parser.phase = 'play-again?' | |
return ('who-is-winner?', self.__whoIsWinner(env)) | |
else: | |
Parser.phase = 'input-numbers' | |
self.env['weather'] = Weather() | |
return ('show-data', env) | |
def __test(self, env): | |
u"""ゲームを続けられる状態か否かの調査""" | |
if env['player']['money'] >= env['game-over'] or\ | |
env['computer']['money'] >= env['game-over']: | |
return True | |
elif env['player']['money'] < env['tofu']['cost'] or\ | |
env['computer']['money'] < env['tofu']['cost']: | |
return True | |
else: | |
return False | |
def __whoIsWinner(self, env): | |
u"""ゲームの勝者判定""" | |
if env['player']['money'] > env['computer']['money']: | |
return 'you-win' | |
elif env['player']['money'] < env['computer']['money']: | |
return 'you-lose' | |
else: | |
return 'even' | |
def __playAgain_p(self, x, env): | |
u"""コンティニュー?""" | |
if x: | |
Parser.phase, self.env['player'], \ | |
self.env['computer'], \ | |
self.env['weather'] =\ | |
'input-numbers', Player(), \ | |
Computer(), Weather() | |
return ('show-data', env) | |
else: | |
sys.exit() | |
class Parser: | |
u"""REPLのRead部分を司る""" | |
phase = 'introduction' | |
def __init__(self): | |
pass | |
def read(self): | |
u"""入力関数 | |
phaseの情報によって動作が変わる""" | |
if self.phase == 'input-numbers': | |
return (self.phase, self.__integer()) | |
elif self.phase == 'play-again?' or\ | |
self.phase == 'instruction': | |
return (self.phase, self.__yes_or_no_p()) | |
else: | |
return (self.phase, None) | |
def __integer(self): | |
u"""整数の入力をコントロール""" | |
var = raw_input() | |
try: return int(var) | |
except ValueError: | |
return self.__integer() | |
def __yes_or_no_p(self): | |
u"""入力がYesかNoか判定して真偽値を返す""" | |
def y_or_n_p(string): | |
if string.upper() in ('Y', 'YES'): | |
return True | |
else: | |
return False | |
t = ('Y', 'YES', 'N', 'NO') | |
i = raw_input() | |
if i.upper() in t: | |
return y_or_n_p(i) | |
else: | |
return self.__yes_or_no_p() | |
if __name__ == '__main__': | |
r = Parser() | |
e = GameMaster() | |
p = Messages() | |
while True: | |
p.Print(e.Eval(r.read())) |
まあ、慣れないOOPで苦労しましたが、いずれにせよ、一応予想通り、入出力と評価部分を完全に切り分けたREPLモデルをプログラムする事は充分可能だ、って事が分かったんで、以降、GUIのプログラムを書く時が来たら、この指針を応用出来そうだ、って感触を得たのは大きいですね。