Google Hackathon for OpenSocial in Kyoto に参加

先日、近所の京都リサーチパークHackathonがあるということで参加してきた。

2/20(事前ミーティング/Ideathon), 2/21(Hackathon)という2日間。結果から言うと、前日のIdeathonでは見事優勝させて頂いたものの、本番では意外な所で躓き思ったように作業が進まず惨敗であった。

今回作ろうと考えたのは、MySpaceをターゲットにした、音を使ったFlashアプリケーションで、タイトルもまんま「Social Sounds」。簡単に説明すると、まず、ユーザの属性値をパラメータとしてユーザー毎に固有の「音」が生成されるようにする。誰かと友達になるとその友達の音を自分の音コレクションに追加出来るようになる。で、集めた音を使い、簡単なシーケンサーもどきでトラックを作って聴いて遊べる、というもの。

アプリケーションのコアな部分は全てFlashで実装し、jsでの実装部分はOpenSocialコンテナとFlashアプリ間のブリッジに徹するのが良さそうだ、ということで、基本的にFlashとjsに分担して作業することにした。二人チームで、相方となった方がCS使いということだったので、Flash制作は彼にお願いし、js/Flash間のインターフェースを取り決め、自分はFlexを使用してテスト用のアプリケーションを作成。Flex <-> js <-> コンテナがやりとりするコードを書いていった。

しかし自分がCSを使った事が無く、FlexとCSで如何にコラボレートしていくべきかのノウハウが全く無かったのが痛かった。結局、テスト用Flexアプリに対しては問題無く動作していたjsとのインターフェースが、CSで作成したFlashとは上手く動作しない部分があり、ハマりまくっているうちに時間切れ、と非常に中途半端な状態で終了。

まあ、元々そんなに作業が進むとは期待していなかったものの、流石にこのままでは悔しいし勿体ないので、今後も開発を継続していくことにした。ついでにFlash CS4 Professionalの試用版をダウンロードし、CSの使い方を探りはじめてみたり。

で、現状は、元々MySpaceのみを対象にしていたのを、Orkutの方が開発し易いかも?ということでOrkut上で色々試しはじめているのだけど、Flashの埋め込み方法に違いがあってハマったので以下にメモしておく。

MySpaceの場合

MySpaceの場合、普通にタグを書いて問題ない。

<Content type="html">
<![CDATA[
<object
  id="flashobject"
  classid="clsid:1111111-2222-333-4444-xxxxxxxxxx"
  codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=10,0,0,0"
  width="550" height="400" align="middle">
<param name="movie" value="path_to_swf_file/foo.swf" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<param name="allowScriptAccess" value="always" />
<param name="allowNetworking" value="all" />
<param name="allowFullScreen" value="false" />
<embed
  name="flashobject"
  src="path_to_swf_file/foo.swf"
  width="550" height="400" align="middle"
  quality="high" bgcolor="#ffffff"
  allowScriptAccess="always" allowNetworking="all" allowFullScreen="false"
  type="application/x-shockwave-flash"
  pluginspage="http://www.adobe.com/go/getflashplayer_jp" />
</object>
]]>
</Content>

Orkutの場合

まず、MySpaceと同じようにタグを書いてもロード出来ない。また、OpenSocialのドキュメントによれば(gadgets.flash - OpenSocial - Google Code)、gadgets.flash.embedFlashを使えば問題ないはずだが、下記の様に記述してもきちんと動作しない。もう少し正確に言うと、Flashは読み込まれるがallowScriptAccess等のパラメータが渡らないらしく、Flash内からjsへアクセス出来ない為、事実上使い物にならない。これはgadgets.flash.embedFlashのバグだと思われるが、詳しい事は不明(この辺に情報あり)。

<div id="flashcontainer"></div>

<script type="text/javascript">
gadgets.flash.embedFlash(
  "path_to_swf_file/foo.swf",
  "flashcontainer", {
  swf_version: 10,
  id: "flashobject",
  allowScriptAccess: "always",
  allowNetworking: "all",
  wmode: "transparent"});
</script>

ではどうするか?答はSWFObjectを使用すること。Download SWFObject 1.5を適当に配置し下記の様に記述すると、問題無く動作した。まだ試していないが、多分Orkut以外もこの方法で大丈夫なのではと思われる。

<div id="flashcontainer"></div>

<script type="text/javascript" src="path_to_swfobject/swfobject.js"></script>
<script type="text/javascript">
var so = new SWFObject(
  "path_to_swf_file/foo.swf",
  "flashobject", "550", "400", "10", "#ffffff");
so.addParam("allowScriptAccess","always");
so.addParam("allowNetworking","all");
so.addParam("wmode","transparent");
so.addParam("menu", "false");
so.write("flashcontainer");
</script>

という訳で、

Hackathonはとても楽しいイベントだった。Hackathonというと、コードばりばり書けるハッカーばかりが集まりそうな感じがして敷居が高そう(だし、実際ハッカー度の高いHackathonも多そう)だけど、Google Hackathonは色んなレベルの人が参加し易い良いイベントだと思う。このようなイベントを開催してくれたGoogleの方や京都のスタッフの方には感謝したい。懇親会で他の開発者の方々やGooglerの皆さんに色々と興味深い話が聞けるのも楽しいし、ハッカーが集まってコード書きまくるなんてなんか怖い、とか思ってる人がいるとしたら、それは誤解だと言っておきたい。

尚、今回参加の目的として、他の人の開発手法やどんな感じでコード書いてるのか等のテクニックを盗みたい、というのもあったが、とてもそんなことやってる場合じゃない、という位、自分の事で精一杯だったのは残念。

下駄記号

文字列同士の類似度を調べるコードを見ていて、下駄記号(〓)というのを初めて知った。へえ。

と、それはどうでもいいんだけど、仕事場の近くにふたば書房っていう極々普通の町の小さな書店があるのだが、久しぶりに立ち読みに入ったら、BGMが延々Animal Collectiveで異様だった上に、何故だか音楽やら建築やらのコーナーがやたらと増えている。一体何処へ向かってるんだ。小さい店は色々大変なんだろうな。

で、そのよく分からない頑張りに敬意を表し、見つけた「ナイロン100パーセント」を買ってみた。

この本、ページ左下がディスクガイドになっていて1ページに1タイトル紹介されているのだけど、これが僕が好きなのばっかで嬉しくなる。というか、ニューウェーブ、レコメン系、クラウトロック等なんだけど、そういうのを僕が好きだということの源流がここら辺にあるのだろうな・・・。殆ど持ってる(or 持っていた)ので、ここで紹介されていた中でまだ聴いてないやつは今後全て入手し聴くべきだ、と思った。

以上、ここへきて初めて音楽絡みのことを書いてみた。

ATOKのキーバインドをEmacs風にする

ATOKダイレクト API for Perl/Rubyが面白そうだったので、これを機会に初めてATOKを使ってみた。

で、早速ruby_evalを試してみら、確かに便利なんだけど、残念ながらATOKの変換後の文字列って、改行などを含むことが出来ないのね。便利さ半減。

それはそうと、ATOKって滅茶苦茶キーバインドをカスタマイズ出来るんだな。知らなかった・・・。即、以下のような感じでEmacs風にしてみたら、非常に日本語入力が快適になった!

変換/次候補 Ctrl-N 追加
変換/前候補 Ctrl-P 追加
全角無変換(後)変換 Ctrl-Y Ctrl-Pから変更
部分確定 - Ctrl-Nを削除
変換取消 Ctrl-G Ctrl-Hから変更
全文字削除 Ctrl-G 追加(入力中のみ)
1文字削除 Ctrl-D Ctrl-Gから変更
カーソル前移動 Ctrl-B Ctrl-Kから変更
カーソル後移動 Ctrl-F Ctrl-Lから変更
カーソル先頭移動 Ctrl-A 追加
カーソル末尾移動 Ctrl-E 追加
文節区切り伸張 Ctrl-V Ctrl-Lから変更
文節区切り収縮 Ctrl-6 Ctrl-Kから変更
文節前移動 Ctrl-B 追加
文節後移動 Ctrl-F 追加
文節先頭移動 Ctrl-A 追加
文節最終移動 Ctrl-E 追加

惜しむらくは、修飾キーにAltが使えないことだな。それが出来たら、文節区切りの変更にM-F/M-Bを使うのだが。

ともかくこれでまた、矢印キーに指を伸ばさねばならない場面が減った。というか、今までMS-IME使い続けててかなり損してたかも。変換精度の良さとかはまだあまり体感出来てないけど、キーバインド変え放題というだけで使う価値大だな。本物のEmacs使いはSKK使うんだろうけど。

これで、残る大きな矢印キー問題は、eclipseのインテリセンスで候補を選択する時位になったな。

KOF 2008での「はてな流大規模データ処理」by id:naoyaさんの発表に関して質問したときのメモ

KOF 2008 の発表資料 - naoyaのはてなダイアリー

こちらの発表に関して、後日質問した時のメモ。

  • 全文検索のインデックス生成等の為、MySQLのデータをバッチ処理でdumpしている、という点について。
    • データが多く、dump処理からして重い/時間が掛かるのでは?
      • そこまで即時性を求めないので、10日とか用途毎に適当な間隔で。
      • パーティショニングされたテーブルなら、各パーティションそれぞれ担当分のdumpとなり軽くなる。
      • また、master-master構成で片方をユーザーのアクセス用、もう片方を完全にバッチ用にしたり。
        • はてなは殆どMyISAM。テーブルロックになるのでユーザーの利用でアクセスするマシンでdumpすると、その間のレスポンスがえらいことに。
          • ちなみにMyISAMって結構よく壊れて、気付いたら滅茶苦茶リソース食って暴走してたりする。
          • でもファイルコピペするだけで他のDBにテーブル移動したりできて取り扱いは楽。
          • はてなの場合、トランザクション基本的に必要ないのでinnoDB使ってない。
    • 用途毎に特化したデータを出力している?
      • テキストになっていればHadoopに突っ込んで割とどうとでもなるので、そうでもない。
  • 全文検索について。Sedueを使っているが、既にLuceneなどもあるが?
    • Senna -> 以前試したが、segvしてMySQLごと落ちたりして使えなかった。今は良くなってると思うけど。インデックスがでかくなってメモリに載せられなかったり。
    • Lucene -> 試したが、目的の用途では遅くて使えなかった。融通も効きにくい。それにJavaだし。
      • その時一緒に話していた方より、Luceneは億単位のデータを扱っている例もあるとのこと。(ここでその方がSenのコミッタである事が判明。驚!!)
      • 日本語の扱いが…。またn-gramだとキーに紐付くドキュメント数が膨大になって、計算量が多くなりがち。
    • HyperEstraier -> 試してみていない模様?mixiとかでも使ってるのでいい感じなのかな、とのこと。
    • Sedue -> Suffix Arrayならではの弱点もある。(具体的な話は忘れた。)
    • 検索は自分で実装するのが良いよ!
  • Hadoop使いまくりですか?
    • 今年使い始めて、今8台のHadoopクラスタ
    • 最初はログデータの解析が追い付かないので使い始めたけど、その後クローラに使ったり、結構色々出来て便利。
  • memcachedは結構使ってる?
    • 勿論使ってるけど、そこまで使いまくってるというほどでもない。
    • 格納するデータはケースバイケースで。あんまり凝ったことをしようとし過ぎるとまたバッドノウハウの世界に…。
    • DBアクセスも、OSのキャッシュにデータがちゃんと載ってれば全然速い。


以上、特に公表してマズそうな内容も無さそうだし、もしかすると何か参考になる人がいるかも知れないので公開しておく。憶えている内容をそのまま書いただけで、間違ったことを書いている可能性もあるので注意。

ちなみに、id:kazuhookuさんのコメントで「〜OS のバッファの話からすると、myisam なん?」とあったが、例えばinnoDBだとOSのバッファが有効でないという事なんだろうか。実践ハイパフォーマンスMySQL読めば分るかな?

しかしJOINもトランザクションも要らないなら、もうそこはRDBMS捨てても良さそうな気もするんですが、本当に完全にJOIN使わないという訳でもなく、色々あってなかなか捨てられないもんなんですね。

PHP関西 勉強会に参加

11/7、KOF初日の帰りにPHPの勉強会に初参加。関西でPHPの勉強会というのは久しぶりらしい。とりあえず、全参加者のうち、かなりの割合がCakePHPを使っていて驚いた。以下感想。

PHPUnitのこんな機能知ってる? by id:kunitさん

PHPUnitというかSeleniumの話がメイン。なぜPHPUnitというタイトルかというと、話の中で、kunitさんが今作っている、SeleniumIDEの吐くテストのHTMLを、SeleniumRC使ったPHPUnitのコードに変換するツールについて触れようと思ってたら、Seleniumの話が面白くなってきてしまって、それメインになった、とのこと。

SeleniumIDEは便利だけど、HTMLだと柔軟性に欠けるので、結局プログラム書いてテストしたくなるよね?なのでそのHTMLをコードに変換し、それを改変してテスト書くと楽じゃないかという話。確かに便利そうだ。個人的にはSeleniumIDEの方は使ったこと無くて、最初からRCだけ使っていて十分便利だと思っているのだが。ちなみにそのRCも、テスト用に使ってるのではなく、ブラウザを自動操作するMechanize的なツールとして使っている。(Mechanizeでは上手く行かないところがあったので。)SeleniumRCはテスト用途で無くとも便利な道具だと思う。

PHPUnitについては、最近は単なるxUnit系のテスティングフレームワークという枠を超えて、RSpecみたいなBDDの要素を取り込んだりして面白い事になっているという(詳しくは今回の発表の範囲外)。自分は何故だったかSimpleTestをずっと使っている為PHPUnitは全然チェックしていなかったが、ちょっと気になった。

CakePHPとか by shin1x1さん

どの様な経緯でCakePHPを使うようになったかという話から、ちょっとしたTIPS、実際の構築例まで。自分はCakePHPは使っていないのだが、こういった経験談というのは参考になるし面白い。Cakeを選んだ理由として、PHP4/5対応、ドキュメント充実、などの点を挙げていたが、ここら辺が自分がsymfonyを選んだ理由と対照的で興味深かった。自分の場合、CakeはPHP4対応という所で、オブジェクト指向的にイマイチなんじゃないかと思ったのがまずあったし、またドキュメントという点では、当時の時点ではsymfonyの方がしっかりしていたように感じられた、ということもあるので。まあでも、今の時点で選ぶならCakeにするかもなーとか思ったりして。それにしても、発表途中で出てきたCake内のSetとかの配列関係のライブラリは、どんだけarray好きやねん!という感じでちょっと引いた。XPath的に多次元配列の要素にアクセスできるような機能とか、かなりキモい。便利そうだけど。

PHPScraping2008 by させざきさん

Web::Scraper的に使えるPHPライブラリ、Diggin_Scraperの作者であるid:sasezakiさんによる発表。Diggin_Scraper知らなかったけどなかなかいい感じ!sfBrowser使ったテストとかでDOMXPath->query()とかチマチマやってassertしたりしてたんだが、試しに使ってみて大丈夫そうなら今後はこれを使おうと思った。発表後半では、HTTPアクセスして結果をスクレイプするようなコードを書く時に、HTTPリクエスト周りの抽象化にZend_Http_Client使ってやると、テストするのにテストアダプタ使えていいよ、といった話もあり、Zendフレームワームのライブラリって完全にスルーしてたけど実は色々便利なものが埋もれてるのか、という気付きがあった。

懇親会

一度会ってみたかったkunitさんですが、やはり熱い方で、飲みまくりながら色々語ってくれました。少し前の設計勉強会絡みの話から、開発中だというMapleの次期バージョンの設計思想まで。既にsymfonyやらCakePHPなどが出ている中で、フルスタックのフレームワークを作る気はもう無いという話やら、レイヤーアーキテクチャ大好きだから勿論そういう作りになるのを意識したフレームワークであるとか、複数のリソースが複合的に表示されたビューに対応するRESTfulなインターフェースの話とか、色々と。最近RESTやテスト駆動に傾倒されているようなので、その辺を強力にサポートするフレームワークになるのだろう。テスト可能性を重視するならやはりDIコンテナがあると良いのだけど。個人的にsymfony2で入る予定だというDIコンテナにもかなり期待してるので。それにしてもkunitさんはこの懇親会後、次の日KOFで発表があるにも関わらず、更に朝4時まで飲んでいたという。凄い。

その他、ずっと不思議な存在として気になっているrhacoについて、やはり何だか凄いものなのだということが色んな人の話からちょっと分ったり。というか、作者のtokushimaさんという方が非常に面白い人のようだ。あんまり使ってもらうことを前提としてないとか、色々自由過ぎる話に衝撃を受けた。

WindowsでGitのGUIを使えるようにする

先日の日記に、よく調べもせずGitのeclipseプラグイン出ないかな、などと書いたら、親切にもid:secondlifeさんよりeclipseプラグインならありますよ。とのコメントをもらったので、試してみた。

教えてもらったEclipse Git、egit - 実用にビルドしたものが置いてあったのを、とりあえずそのままインストールしてみる。が、設定の「チーム」にGitの項目が増えたものの、特に何の操作も出来ず…。

残念。

まあeclipseプラグインは置いといて、Windows(XP)でGit使えるようにしとくか、と思い、msysgitをインストール。普通にインストールして、環境変数PATHに C:\Program Files\Git\bin を追加してやったら、コマンドラインからGitの各コマンドが使えるようになった。

インストールされたフォルダを見てみると、git-guiが一緒にインストールされているようだったので、これが動けばいいなあと、コマンドラインからsh.exe git-gui等と実行してみると、" *** Couldn't reserve space for cygwin's heap."というエラー。

cygwinもインストールしてあったので、何かが干渉しててダメなのかと暫くハマっていたが、ちょっと調べると、Gitのインストール時に入った C:\Program Files\Git\bin\msys-1.0.dll がダメな模様。WinAVR and Windows Vista - MadWizard.orgで、msys-rebased.zipを取ってきて解凍したもので上書きしたら、sh.exe自体は動くようになった。が、しかしgit-guiはやはり起動せず。正確には、wishのウィンドウが開くのだが、そこで固まる。


結局GUI使えないのか…と思いつつ、githubにGitのGUIのプロジェクトが登録されてないかと検索してみたら、git-colaなるものを発見したので、次はこれを試す。

基本的にREADMEに書いてある通りインストールしていけば良いようだが、リンク切れがあったり、使ったことのないpython回りのインストールを行ったので、その過程をメモっておく。ちなみに、cygwinもインストールしてある環境では、PATHを追加する際にcygwin関係よりも前に追加した方が良いと思われる。

  • msysgitをインストール

先にインストール済み。

Download Python SoftwareからPython2.5.2 Windows installerを取ってきて、C:\Python25にインストール。C:\Python25 をPATHに追加。

  • PyQt4をインストール

Riverbank | Software | PyQt | PyQt4 Downloadより、PyQt-win-gpl-4.4.2.zipをダウンロードしてインストール。C:\Python25\Scripts;C:\Python25\PyQt4\bin をPATHに追加。

  • MinGWをインストール

この後インストールするsimplejsonをコンパイルするのに必要なので、先にMinGW - Minimalist GNU for WindowsよりMinGW 5.1.4を C:\MinGW へインストール。C:\MinGW\bin をPATHに追加。

  • simplejsonをインストール

Python Package Index : simplejson 1.9.2をダウンロードして解凍。コマンドラインより、解凍後のフォルダ内で

python setup.py build -c mingw32 install 

としてインストール。(最初easy_installでインストールを試したが、上手く行かなかったので。)

  • fileコマンドをインストール

File for Windowsから、バイナリを取ってきてインストール。解凍して C:\Program Files に入れ、C:\Program Files\file-4.25-1-bin\bin をPATHに追加。

  • colaをインストール

git-colaから最新版をダウンロードし、C:\Python25\Lib\site-packages\cola に解凍。

  • ショートカット作成

下記内容のファイルをcola.batとして保存。

C:\Python25\python.exe C:\Python25\Lib\site-packages\cola\bin\git-cola


作ったcola.batを実行すればGUIのウィンドウが開く…と思ったら、そう甘くは無く、既にgit-initなどで作成済みの既存レポジトリで実行しないとダメでした。なので、ショートカットをGitのレポジトリーにコピーしておく。これで一応使えるようになった。

使い方は、適当に弄ってみた限りでは、変更のあったファイルを一覧表示して、addするかどうかを指定し、ログを書いてコミットしたりといった作業が可能なようだ。まあ何も無いよりはいいかも知れない。

と、ここまでやってみたものの、Git入門 - Windows環境での利用によれば「Windowsファイルシステムでは、Gitの特徴をうまく生かせず、Linux環境のようには高速に動作しないそうです。」といったこともあるようなので、結局Linux上でコマンドラインでのみ使用することになりそう。


第28回 Ruby/Rails勉強会@関西に参加

日本Rubyの会 公式Wiki - 第28回 Ruby/Rails勉強会@関西
http://jp.rubyist.net/?KansaiWorkshop28


自転車で京女の前の坂を汗だくになって登った上、ぎりぎりの到着でばたばたしてしまい、エラトステネスの嵐はちゃんと聴けなかった。

id:secondlifeさんのGitの解説は、これは使ってみたくなるなあ、という強力なもので、この日一気に信者を増やしたんじゃないだろうか。

branch切り替えとか異様に高速だったんですが、ファイルが大量にあってもやはり切り替え速いんだろうか。Subversionの場合、特にeclipseSubclipse使って作業したりすると逐一時間がかかって苛々しまくるのだが。それからTrac+Subversionの連携は便利だけど、Trac+Gitも良い感じに組み合わせられるのかな?とか、windowsで問題なく使えるようにGUIクライアントとかeclipseプラグインとか出ないと辛いな、とか、良く考えるとGitをメインに使うには色々なハードルが。

ちなみにGitは「ギット」なのか「ジット」なのかどっちなんだ?と思っていたんだけど、皆さん普通にギットと発音していたのでそうなのか?なんとなくジットかと思っていたのだけど。


初心者レッスンはirbの説明。irbをカスタマイズして使おうとか考えたことがなかったので、なるほどという感じで楽しめた。okkezさんのブログのエントリに設定ファイルが載っているので、とりあえずこれをそのまま頂いて使おう。

演習では、irbの結果表示を消す方法が分からずに苦労してしまった。一応conf.return_format = ""で消えたけど、こんなんでいいのか。あとはirb_context.echo = falseとか。

以下は計算100もどきの回答。なんかこの前の石取りゲームと似た感じになってしまうので、ステートはlambda使ってみたりした。rubyだったら、State/Strategyといったパターンは、クラス使う替わりにlambdaでやるのも良いかも。コード長いけど、表示とロジックは分離してないと気持ち悪くて仕方ないので、個人的にはこれで良し。include Observableするだけで一発でObserverが書けるrubyは最高。

require "observer"

QUESTION_NUMBER = 100

class UI
  def initialize(hundred_calc)
    @hundred_calc = hundred_calc
    @hundred_calc.add_observer(self)
    @response = Response.new(@hundred_calc)
    @hundred_calc.update(nil)
  end
  
  def update
    print @response.render
    exit if @hundred_calc.state.nil?
    @hundred_calc.update(gets.strip)
  end
end

class Response
  def initialize(hundred_calc)
    @calc = hundred_calc
  end

  def render
    send(@calc.result.to_s)
  end

  def greeting
    "計算 100 開始\n" + select_level
  end

  def select_level
    "難易度を選択してください。1:かんたん 2:むずかしい >"
  end

  def level
    (@calc.level == 1 ? "かんたん" : "むずかしい") +
      " を開始します。Press Enter >"
  end

  def first_question
    question
  end

  def correct_continue
    correct + question
  end

  def error_continue
    error + question
  end

  def correct_check_point
    correct + check_point + question
  end

  def error_check_point
    error + check_point + question
  end

  def correct_finish
    correct + summary
  end

  def error_finish
    error + summary
  end

  def correct
    "正解\n"
  end

  def error
    "不正解 (正解: #{@calc.answer(@calc.question_count - 1)})\n"
  end

  def question
    "#{@calc.question(@calc.question_count)} = "
  end

  def check_point
    "#{@calc.question_count} 問突破\n"
  end

  def summary
    m, s = (@calc.end_time.to_i - @calc.start_time.to_i).divmod(60)
    check_point +
      "#{QUESTION_NUMBER} 問終了しました\n" +
      "正解   : #{@calc.correct_count}\n" +
      "不正解 : #{@calc.error_count}\n" +
      "タイム : #{m}#{s}\n"
  end
end

class HundredCalc
  include Observable

  LEVELS = {1 => :easy, 2 => :difficult}

  attr_accessor :state, :input, :result, 
    :level, :correct_count, :error_count, :question_count, 
    :start_time, :end_time, :questions

  def initialize
    @level = nil
    @correct_count = 0
    @error_count = 0
    @question_count = 0
    @start_time = nil
    @end_time = nil
    @questions = []
    initialize_states
    @state = @initial_state
  end

  def update(input)
    @input = input
    @state.call
    changed
    notify_observers
  end

  def correct?(question_count, answer)
    @questions[question_count].correct?(answer)
  end
  
  def question(question_count)
    @questions[question_count].expression
  end

  def answer(question_count)
    @questions[question_count].answer
  end

private

  def create_question(question_count)
    @questions[question_count] = Question.new
  end

  def initialize_states
    @initial_state = lambda do
      @result = :greeting
      @state = @select_level_state
    end

    @select_level_state = lambda do
      if LEVELS.include?(@input.to_i)
        @level = @input.to_i
        Question.send(LEVELS[@level].to_s)
        @result = :level
        @state = @start_state
      else
        @result = :select_level
        @state = @select_level_state
      end
    end

    @start_state = lambda do
      (0...QUESTION_NUMBER).each {|i| create_question(i) }
      @question_count = 0
      @start_time = Time.now
      @result = :first_question
      @state = @question_state
    end

    @question_state = lambda do
      correct = @input =~ /\A-?\d+$/ && correct?(@question_count, @input.to_i)
      if correct
        @correct_count += 1
      else
        @error_count += 1
      end
      @question_count += 1
      if QUESTION_NUMBER <= @question_count
        @end_time = Time.now
        @result = correct ? :correct_finish : :error_finish
        @state = nil
      else
        if @question_count % 10 == 0
          @result = correct ? :correct_check_point : :error_check_point
        else
          @result = correct ? :correct_continue : :error_continue
        end
        @state = @question_state
      end
    end
  end
end

class Question
  @@expression_builder = nil

  def self.easy
    @@expression_builder = lambda do
      op = %w[+ - *][rand(3)]
      left = rand(10)
      right = op == "/" ? rand(9) + 1 : rand(10)
      "#{left} #{op} #{right}"
    end
  end

  def self.difficult
    @@expression_builder = lambda do
      op = %w[+ - * /][rand(4)]
      if op == "/"
        #除数が大きいとつまらないので
        right = rand < 0.9 ? rand(20) + 1 : rand(99) + 1
        left = right * (rand(100 / right) + 1)
      else
        right = rand(100)
        left = rand(100)
      end
      "#{left} #{op} #{right}"
    end
  end

  attr_reader :expression, :answer

  def initialize
    @expression = @@expression_builder.call
    @answer = eval(@expression)
  end

  def correct?(answer)
    @answer == answer
  end  
end

if $0 == __FILE__
  UI.new(HundredCalc.new)
end