[プログラミング]いいコード……
それから、自分がいっている『いいコード』というのは、インデントがどうと か、そういう意味じゃないんです。十分にリファクタリングされていて、プロ グラマの意図がコードに十分に表現されていて、ほとんどコメントがいらない ようなコードです。
これはオレが目標にしていた「いいコード」のイメージそのもの。生産性とかそんな話じゃない、と言うのを聞いて少し違うものを想像してしまったようだ。
前にも書いたことがありますが、プログラムにとって一番の価値基準は、いい コードで書かれたかどうかでなく、人にどれだけ喜ばれるかということです。 ただ、これは自分が受注といった仕事をしているせいか、価値が比較的軽くな ります。芸術作品ほどの感動は求められていないといったところです。
芸術作品ほどの感動というのは大げさな気がしたが、このあたりの自分の考えはあいまいだ。もしかしたらプログラミングとプログラミングに関係ない事を混同しているのかもしれない。
自分が過去に作ったプログラムは CD に焼いてあとは売るだけという使い捨てのものが多かった。マスターを上げればそのコードは 2 度と見ずに済むことも多いので、「いいコード」に対する意識はたいへん低かった。まぁ見ずに済むと言ってしまうところが、 2 度と見たくないコードを書いていということでもあるな。
で、この『いいスタイル』と『いいプログラム』というのがやっぱり今の自分の活動に必要なものだと思う。
あれ、てことはオレが目指すべきは XP なのか。
[アイデンティティ]いいコード
いいコードを書くことと高い生産性とは別の話でしょう。いや、いいコードを書けば生産性が高くはなるんでしょうけどね。
でも、自分は生産性云々でいいコードを書きたいと思ってるわけじゃないんです。悪いコードに我慢ならないからいいコードを書きたいんです。悪いコード を見ると無性に書き直したくなるんです。それはもう生産性とかそんな話じゃ ないんです。
悪いコードに我慢ならないからいいコードを書きたい、というのはあまり感じない。いいアプリを書きたい衝動の方がはるかに大きく、いつも自分やユーザーがそのアプリを使う姿ばかり頭に浮かんでくる。無意識にエンドユーザの反応をずっと想像している。
たまに他人のソースを見て、ほんとに几帳面だなあと感じることがよくある。 オレはああいうのは書けない。
全部が全部を生産性に結びつけるのはプログラミングを味気ないものにしちゃ うんじゃないでしょうか。
全部が全部かはわからないけど、 オレにとってのプログラミングはコードを作ることではなくて、ソフトを作ることだから生産性はとても重要だ。
いいコードが書きたいという衝動はそれほどでもないということか。
[開発ログ]ログイン画面完成
ユーザー作成とユーザーログインの WEB ページができた。
これが Jewel-mmo サービスの最小構成か。
--
テンプレートはこうした。テンプレート内に含めるロジックをファイルの先頭に移動。これならこのままでもやっていけそうだ。
<% name = @cgi.params['name'].first pass = @cgi.params['pass'].first sid = nil error = nil if name begin require 'command/login.rb' raise Core_Error unless (com = Login.new(nil, name, pass)).result sid = com.result @cgi_header["cookie"] = CGI::Cookie::new({"name" => "sid", "value" => [sid]}) rescue Core_Error error = $!.inspect end end %> <html lang="ja"> <head> <meta http-equiv="Content-type" content="text/html; charset=EUC-JP"> <meta http-equiv="Content-script-type" content="text/javascript"> <title>ログイン</title> </head> <body> <h1>ログイン</h1> <% if sid %> <head><meta http-equiv="refresh" content="5;url=<%= jmp_url('usermain') %>"></head> <p>Wait or <a href="<%= jmp_url('usermain') %>">Click here!</a></p> <% else %> <% if error %> <p>Error : <%= h(error) %></p> <% end %> <form action="<%= jmp_url %>" method="post" class="login_form"> <input type="hidden" name="tmpl" value="<%= h(@tmpl) %>" /> user name : <input type="text" size="10" name="name" value="<%= h(name) %>"/> password : <input type="pass" size="10" name="pass" value="<%= h(pass) %>"/> <input type="submit" value="login" /> </form> <p><a href="<%= jmp_url('useradd') %>">ユーザー登録画面</a></p> <% end %> </body> </html>
現状のランチャーは以下。
#!/usr/bin/env ruby require 'cgi' require 'erb' include ERB::Util def jmp_url(tmpl=nil, params=nil) url = $0 url += "?tmpl=#{tmpl}" if tmpl h(url) end @cgi_header = {} @cgi = CGI.new("html3") begin $:.unshift '../core/' require 'jewelcore.rb' include JewelCore @sid = @cgi.cookies['sid'].empty? ? nil : @cgi.cookies['sid'].value.first @tmpl = @cgi.params['tmpl'].first raise @tmpl if @tmpl =~ /[^\w_-]/ html = ERB.new(File.read("./#{@tmpl}.htm")).result(binding) rescue Exception html = "<H2>Error</H2>\n#{h($!.inspect)}" html += '<PRE>' + $@.map{|s| h(s) }.join("\n") + '</PRE>' end @cgi.out(@cgi_header) { html }
[開発ログ]WEB インターフェース作成中
こんな感じの erb スクリプト。 ロジックは core コマンドになっているのでそれを使うコードになる。
<html lang="ja"> <head> <meta http-equiv="Content-type" content="text/html; charset=EUC-JP"> <meta http-equiv="Content-script-type" content="text/javascript"> <title>ログイン</title> </head> <body> <% name = @cgi.params['name'].first pass = @cgi.params['pass'].first sid = nil error = nil if name begin require 'command/login.rb' raise Core_Error unless (com = Login.new(nil, name, pass)).result sid = com.result rescue Core_Error error = $!.inspect end end %> <h1>ログイン</h1> <% if sid %> <p><%= h(name) %> : ログインしました。</p> <p>sid : <%= h(sid) %></p> <% else %> <% if error %> <p>Error : <%= h(error) %></p> <% end %> <form action="<%= h(@action_script) %>" method="post" class="login_form"> <input type="hidden" name="tmpl" value="<%= h(@tmpl) %>" /> user name : <input type="text" size="10" name="name" value="<%= h(name) %>"/> password : <input type="pass" size="10" name="pass" value="<%= h(pass) %>"/> <input type="submit" value="login" /> </form> <% end %> <p><a href="<%= h(@action_script) %>?tmpl=useradd">ユーザー登録画面</a></p> </body> </html>
前はテンプレートとプログラムを別ファイルに分けていたのだが、面倒なのでひとつにまとめた。これくらいならデザイナも理解してくれる。規模によっては切り分けが必要か。
でも、これをデザイナに渡すと複雑な HTML になって返ってくるわけで、その中の Ruby スクリプトを編集するのも面倒な気もする。難しいところか。
[開発ログ]回復コマンドを書いてみたが
手探りで使っている Acrive Record の使い方やその他どうもすっきりしない。
class Heal < CoreCommand def exec doll_id = @options[0] doll = @session.user.dolls.find(doll_id) return nil unless doll.temp_states.find_all("expiration_time > now()").empty? doll.temp_states.find_all("state = 'action_wait'").each{|s|s.destroy} wait = 10 doll.temp_states.create(:state => "action_wait", :expiration_time => Time.now + wait) d = 1 old_hp = doll.hp doll.hp = old_hp + d doll.hp = doll.state.hpmax if doll.hp > doll.state.hpmax doll.save doll.hp - old_hp end end
そもそもローカルの時間と DB の時間のズレが問題になりそうだし。後半のロジックももっと短く書けないものだろうか。 ツッコミお願いします。
[アイデンティティ]インプットとアウトプットのバランス
<URL:http://rucila.s43.xrea.com/memo/?date=20050421#p06>
オレの場合は圧倒的にインが少なすぎる。非常にバランスが悪い。 というのに気づいてここ一年は意識して勉強しているのだが、それでもまだまだインが少ないと思う。
問題は書くのは楽しいけど読むのは面倒と感じてしまう根本の性格か。 と言うわけで読書会に参加したり、面白い本がないかを聞いたりしているわけだ。
[アイデア]エリアマスターと守衛
エリアマスターが不在のエリアで、人形にエリアマスターのカードを使うとユーザーはそのエリアのエリアマスターとなることができる。
カードの対象となった人形は戦闘能力値が約 10倍 にパワーアップし、そのエリアの守衛として最強のキャラクターになる。エリアマスターであれば他の人形に守衛のカードを使って守衛の数を増やすことが可能。
エリアマスターのカードや、守衛のカードによって守衛と化した人形は強力な戦闘能力と引き替えに、いくつかの制限が課せられる。
- 守衛は成長しない
- 守衛はトレードその他のいくつかのアクションを実行出来ない
- エリアマスターとなったユーザー(とその人形達)はそのエリアをでることができない。
町には通常治安を維持するためにエリアマスターと守衛が存在する。 たいてい守衛は 24 時間 bot によってコントロールされ無条件に Evil のキャラクタを攻撃するか、 Evil キャラクタのアクションを厳しく監視する。
そう言えば人形の性格による攻撃ルールをデジタル化していない。メモ噴出する前にアップしておかねば。
[アイデア]チャットサーバーのイメージ
チャットサーバーについて。 今日も思いついたことを深く考えずにただそのまま書いてみる。
チャットサーバーでできること。
- NPC との会話 …… Core コマンド
- プレイヤー同士の会話 …… 通常の irc によるチャット
- プレイヤー bot との会話 …… ピリカ → irc → ピリカ
- 自分の bot との会話 …… クライアント〜ピリカ間の通信
- エリア URL の問い合わせ …… クライアント → アイリ → クライアント
- ショップ URL の問い合わせ …… 同上
- エフェクト情報の取得 …… Core コマンド処理 → アイリ → ピリカ → クライアント
プレイヤー bot とのトレード
- トレード条件の問い合わせ …… ask name
- 条件表示 …… 「trade0: 相手 : やくそう あなた : 10J 」
- 「交換しますか?はい/いいえ」 …… 「はい」 → trade name 0
[開発ログ]ircd
- <URL:http://isaji.cheap.jp/wiki/pukiwiki.php?%C6%FC%B5%AD%2F2005-03-17%2FFedora%A4%C7ircd-hybrid>
- <URL:http://vdr.jp/d/20050109.html>
- <URL:http://hp.vector.co.jp/authors/VA012345/laboratory/freeBSD/IRCdFreeBSD.html>
ircd-hybrid がいいかなと思い、
emerge -p ircd-hybrid
したら、
Calculating dependencies !!! All ebuilds that could satisfy "ircd-hybrid" have been masked. !!! One of the following masked packages is required to complete your request: - net-irc/ircd-hybrid-7.0.3 (masked by: ~x86 keyword)
うーむ。
[開発ログ]アイリ
Nadoka をサーバーにもクライアントにも使うので、サーバー上の irc チャンネル内のユーザー管理を行う Nadoka をアイリと名付けだ。ちなみにクライアントの方はピリカ。
アイリのコードは Nadoka の設定ファイルと bot になる。
サーバー側の bot を使う処理は NPC の制御や、お店などいろいろある気がしていたのだが、よく考えるとそれらは一ユーザーと同じ条件下で動かす bot でもよい。ただそれだとパフォーマンスが問題になるかもしれないが。
アイリの本分はエリア毎に存在するチャンネルの管理だ。 オペーレータがどのような操作が可能なのかよく知らないので、まず irc を調べないといけない。
--
http://irc.nahi.to/command.html
なるほど。
join 制限は +sn に加えて、
/mode ch +i
しておけばよさそうだ。もし、アイリがオペ権限を失った場合は、そのチャンネルは破棄して新しいチャンネルを作ればよい。
エリア移動の時
クライアントがエリア移動コマンド実行 → 移動認証(成功ならアイリは invite を実行) → クライアントはエリアのチャンネル名を受け取る → ピリカによる自動 part と join
アイリ起動時
エリア数の分のチャンネル開設 (join) 。
ピリカ起動時
キャラが wlogin しているなら、エリアログインだろうか。
[アイデア]ゲームの目的と富
ゲームの目的を考えるととりあえず 2 つ思いつく。
ひとつは自分のプレイスタイルを作ること。それは 8 体のキャラクタに好きな名前を付けるところから始まって、好きな絵を描いて、 bot を駆使して好きに振る舞わせるということ。あとは自分にだけ見える世界観そのものをカスタマイズすることも可能。
もう一つはゲームの中で優位な地位を築くこと。富を作ることだ。 で、このゲームワールドでの富とはなんなのか。 やりたくないのは、モンスターを倒すとお金が手に入り、それを集めることが富になるというもの。モンスターがアイテムを落としても同じ事。 じゃあ、どうやって富を生み出すのかというと、基本は土地に種をまけば自然と食料が育つので、それを栽培して収穫することが富になる。自分が食べる分はそれでたやすくまかなえるが、強力なアイテムを作るにはいろいろな地方で生産されたたくさんの物質を合成しなければならない。お金は最初から一定の量がワールドに存在している。お店もユーザ主体のぶつぶつ交換なので物価は自然に決まる。決して生産出来ない宝石をお金のかわりにしてもいいかもしれない。
これが基本。実際はいちいちめんどくさくて栽培なんてやってられない。冒険にも出かけないといけない。でも、そうやって時間と暇さえあれば作れる食べ物にも自然と価値が生まれていく。
[開発ログ]Google Maps
遅ればせながら時代は WEB だということに気づき(例によって妻に遅すぎと突っ込まれつつ)調査を始める。
http://maps.google.com/
wema を見たときにこういうことができそうだとは思った。ただ、この使いやすさと速度と安定度は実際に見ると衝撃的だ。
つまり何でもありだ。ページのリロードなしでいくらでもゲーム画面を更新できる。この技術は将来的なフロントエンドの基盤となりえる。
自分のプログラマとしての得意分野を考えると Ajax に今から注力するのもありかと思うが、やっぱり今は Ruby でのコア実装に集中すべき時期だ。ゴージャスな GUI に手を出すのは早すぎる最適化だろう。
[開発ログ] 次に何を実装するか考えよう
コマンドのランチャーとテスト環境のフレームワークができたので、次の段階へ進むことができる。
次の作業として考えられるのは
- WEB インターフェースに関する考察
- Nodoka でアイリ&ピリカを実装
あたりか。
WEB インターフェースについては、エンドユーザから見れば
- 基本が WEB でその他はおまけの便利(マニアック)なツール
だが、実装から見れば
- 基本はコマンドによる各種処理で WEB はおまけ
であるから、 WEB は後回しでもよさそうだ。 ただしゲームデザインは WEB のインターフェースを考慮する必要がある。
このあたりをもっと考えてみよう。
--
考えがなかなか進まない。 WEB ページで MMORPG をプレイする姿がなかなかイメージできない。しかしそれは Ajax でなくたって可能なはず。既存のモデルに思考が縛られているだけだ。
プレイの流れを考えてみる。
- ユーザー登録
- キャラ作成
- ワールドへ放り出される
- 地理の把握
- 移動
- 戦闘が発生する
- お店
- 会話
- 装備、アイテムを使う
ワールドの時間はリアルタイムに進行しているはずなので、その動的な世界観をどう表現するか。チャットウィンドウにリアルタイムでログが流れればそれは十分に時間軸を表現できる。
[開発ログ] config/environment.rb
開発とテストの DB を分けたかったので rails の config/environment.rb をまねて yaml でそれぞれの DB 設定を記述する方式を導入。
config/database.yml
development: adapter: mysql database: jewel host: localhost username: root password: test: adapter: mysql database: jewel_test host: localhost username: root password: production: adapter: mysql database: jewel_production host: localhost username: root password:
ActiveSupport を知らないのでその辺は全部削除。
[アイデア]最大 HP が低いほど強い
いままで HP と呼んでいたものは仮に耐久力と呼ぶことにする。
新しい HP は 1 〜 10 の数値になる。 レベルアップ等によってこの HP パラメータは成長しない。 そしてダメージの計算は……うーむこれはコードで書いたほうが早い。
hp_max = 5 hp = hp_max old_hp_max = 75
として、ダメージ計算は
hp -= damage / (old_hp_max / hp_max)
とする。
hp_max が低いほうが切り捨てるダメージ量が多くなるので有利。 hp_max が 1 なら従来の最大 HP 以上のダメージを一度に与えない限りダメージは 0 になるわけだ。
たとえばボス戦などではいかにダメージ 1 を与えるかが重要になる(ボスの HP が 10 しかないのだから。魔法剣のサンダガでためる× 10 とバイキルトした最強のジョブアビを 200% 連携でヒットさせてやっとダメージが 0 から 1 になるわけだ。もちろんこの連携を成功させるまでの準備時間がすごく長いので、最強レベルのアーマーナイトを先頭に配置し、魔法使い達はそいつに何重ものバリアを被せてボスの攻撃をしのぎ続けなければならないのだ……)。
昔から キャラの最大 HP は 10 くらいでいいんじゃないかと思っていたので、これは非常にいいアイデアだ。
[開発ログ]テストに挑戦
よく知らないが見よう見まねでテストを書いてみる。
require 'jewelcore.rb' require 'arconf.rb' module JewelCore class Useradd < CoreCommand class Core_name_exists < Core_Error ; end class Core_name_is_too_short < Core_Error ; end class Core_password_is_too_short < Core_Error ; end class Core_invalid_user_name < Core_Error ; end def find_session sid end def exec name = @options[0] pass = @options[1] raise Core_invalid_user_name, name if name =~ /[^0-9A-Za-z_-]/ raise Core_name_exists, name if User.find_by_name(name) raise Core_name_is_too_short, name.size.to_s if name.size < 3 raise Core_password_is_too_short, pass.size.to_s if pass.size < 3 user = User.create(:name => name, :password => pass) true end end end if defined? Test::Unit class Test_CoreCommands < Test::Unit::TestCase def test_useradd [ {:name => 'username', :password => 'pass'}, {:name => '000', :password => 'pass'}, {:name => '-00', :password => 'pass'}, {:name => 'aa_', :password => 'pass'}, {:name => '0123456789abc', :password => 'pass'}, ].each do |v| user = OpenStruct.new v assert Useradd.new(nil, user.name, user.password).result assert_raises Useradd::Core_name_exists do Useradd.new(nil, user.name, user.password) end end end def test_useradd_faile ['aa/','aa*','00"','00あ'].each do|name| assert_raises Useradd::Core_invalid_user_name do Useradd.new(nil, name, 'pass') end end ['','u','uu','0'].each do|name| assert_raises Useradd::Core_name_is_too_short do Useradd.new(nil, name, 'pass') end end ['','u','uu','0'].each do|pass| assert_raises Useradd::Core_password_is_too_short do Useradd.new(nil, 'user', pass) end end end end end
コードそのものとテストコードを離したくなかったので同じファイルに書いている。テストを書くのはそれほど面倒ではない。逆に端末でいちいちコマンドを実行しながらデバッグするより楽かもしれない。これくらいだと自然にテストファーストができる。
例外クラスを作るのは初めてかもしれない。こんな使い方でいいのだろうか。
パスワードはノーチェックで DB 行きで大丈夫だろうか。
上記テストコードを呼び出すスクリプトはこんな感じ。
#!/usr/bin/env ruby require 'test/unit' Dir.glob('command/*.rb').each{|s| require s} class Test_CoreCommands < Test::Unit::TestCase include JewelCore def setup_user(user) User.destroy_all Useradd.new(nil, user.name, user.password) user.sid = Login.new(nil, user.name, user.password).result user.db = CoreCommand.new(user.sid).session.user user end def setup require 'ostruct' @user = setup_user OpenStruct.new(:name => 'testman00', :password => 'pass') @target = OpenStruct.new :name => 'target00', :password => 'pass' #setup_user @target end def teardown end def create_doll(user, num) Filldolls.new(user.sid, num) end def test_ end end
テストは重要な気がするのでちゃんと勉強したい。
[プログラミング] YARV で HSP となでしこ
そうそう、
<URL:http://www.namikilab.tuat.ac.jp/%7Esasada/diary/200504.html#d1>
これは 4/1 ネタじゃないとのこと。 オレは前から話を聞いていたからもともとネタとは思っていなかったのだけど。
とても楽しみ。 HSP のライブラリが Ruby から使えればすごく楽しいことになるはず。
[むらさまプレイ日記] ょ。君の成績
ょ。君の総合成績
- ALL 100勝82敗74分 勝率.390
- Grade1 9勝24敗17分 勝率.180 優勝0回
- Grade2 35勝36敗36分 勝率.327 優勝1回 ★
- Grade3 49勝16敗20分 勝率.576 優勝6回 ★★★★★★
- Grade4 7勝6敗1分 勝率.500 優勝1回 ★
よかった。普通はこうなるはず。
あれ、 G1 未勝利じゃないですか。頑張ってください。
オレは G1 の時しかやる気がでない模様。というか育成にしても下位にいるときから G1 での戦いを焦点にしいる。他の人の成績も知りたい。
[開発ログ]とりあえず攻撃コマンド
PC がログインできるようになったので、とりあえず攻撃がしたい。実装の注意点はこんなところか。
- 攻撃の種類には直接、魔法、アイテムがある
- 回復アクションも攻撃と同じ処理がよい
- 相手の HP を 0 にしたときは経験値やアイテムの取得がある
- ただ今回は一時的な経験値システムでレベルアップ処理を別にできる
あと NPC の扱いをどうするか。どのようにワールドにログインさせるか。
--
とりあえず攻撃できるようになった。次はエフェクト情報をクライアントにばら撒くチャットサーバー(アイリ)について考えるか。
--
ActiveRecord の使い方が甘いので後々修正が必要だろう。
<URL:http://api.rubyonrails.com/?u=ar.rubyonrails.com>
昨日と今日の成果。 ActiveRecord 効果でコマンドのコードが短くなっている。
39 ./www/cmdlauncher.cgi 39 .cgi total 10 ./conf.rb 5 ./lcmdlauncher.rb 56 ./arconf.rb 18 ./jewelcore.rb 15 ./command/users.rb 11 ./command/dlist.rb 35 ./command/login.rb 12 ./command/logout.rb 19 ./command/filldolls.rb 12 ./command/wlogin.rb 40 ./command/attack.rb 12 ./command/wlogout.rb 245 .rb total 59 ./db/tables.sql 59 .sql total 8 ./db/uses.txt 8 .txt total 0 .html total total 351 lines .cgi 39 .rb 245 .sql 59 .txt 8 .html 0
[アイデンティティ]夢のよう?
この土日はこれといった予定もなく家族とのんびり過ごした。
暇なので面白いゲームでもやりたいなと思うものの、面白いゲーム知らないし、ムラサマも足掛け2年以上遊んでてさすがに飽き気味だし(でもいまだに毎日やってたりする)はやく自分で新しいゲーム作らないといけないなと思う。
というわけでデータベースの設計に着手しようとしているわけだが、ふとこんなことを思い出した。小学生のころからRPGのシステムとかパラメータとかをノートに何度も書いていた。プログラミングのスキルはなかったのでルールを書いては、弟を集めてサイコロでゲームを進行させていた。当時からすれば今はプログラミングのスキルが身につき、というか既に何本ものゲームソフトを世に送り出し、自分が主軸となって開発したソフトを数十万の人が買ってくれたりもしているわけで、まるで夢のような状態にあるわけだ。 Ruby もあるし、今なら少しがんばれば自分の作りたいゲームが自分の手で作れる。
がんばろう。