一応このサイトは全面的に Caty で作っているんだが、実は今ダウンロード可能な奴では機能が足りてない(+バグも残ってる)ため、まあなんというか次のバージョンで盛り込む予定の機能をいくつか実装してそれで動かしてる。要するに極めて実験的な状態でこのサイトは運用されてるわけだ。それで新たに導入した機能のうち俺がどうしてもこれがないとダメだと主張したのが、新しい型拡張の演算子で、今回はちょっとそれについて(先走り気味だが)書いてみる。
(以下は今までのスキーマ文法で書いているが、次のリリースでは新しいスキーマ文法に変更されるので注意)
まずは現在の Caty には、既に & による型と型の論理 AND 演算が実装されている。俺が手抜きしたんで object 同士にしか使えないが、次のバージョンでは当初の予定どおりあらゆる型同士で扱えるようにする。
type UserInfo = object {
"userid": string,
"password": string,
*:any, // 論理 AND にはワイルドカードに any が必須
};
type userID = string(mixLength=3);
type MyUser = UserInfo & object {
"userid": userID,
"friends": list [userID],
*: any, // 同じく
};
この型同士の論理 AND を取ると UserInfo と無名 object の間で、
といった演算がなされ、 UserInfo 型の範囲を狭めた型を定義できるようになる。これは Caty 組み込みのログイン機能を使いたいがユーザ情報に追加の値が必要な時などに使えるはずだ(それを見据えてユーザ管理機能を改良中)。
一見これだけでも十分に思えるが、この型同士の論理 AND には *:any の宣言のないオブジェクト型に使えないという特徴があるため、次節で述べるケースに使えない。そして俺は次節のケースの方が圧倒的に多くなるだろうと判断したので、新しい仕様を提案したわけだ(むしろ仕様を削るのが俺の仕事なので、それだけ必要性を感じたということだ)。
まずは簡単なログインフォームを考えてみる。
<form action="/login.cgi" method="post">
<p>Login ID: <input type="text" size="20" name="userid"/></p>
<p>Password: <input type="password" size="20" name="password"/></p>
<p><input type="submit" value="Login"/></p>
</form>
このフォームに対応するスキーマは以下のようなものだろう。
type LoginForm = object {
// *:any がない場合、 *:never が補われる
"userid": string,
"password": string(minLength=8),
};
さて、ここでログインフォームを新規のユーザ登録フォームにも使うこととし、また spam アカウントの登録のために CAPTCHA なりなぞなぞ認証なり、とにかく何か追加の情報が必要になったとする。先の例に従って次のような型を考えるかもしれないが、これはエラーになる。
type MyLoginForm = LoginForm & object {
"additional": string,
*:any
};
何故なら演算対象の二つの型のうち、 LoginForm 型はワイルドカードに never が指定されており、 never はどんな型との論理 AND を取っても never にしかならないからだ。そのため、もしも同じように新しいプロパティを追加したいなら、元の LoginForm を *:any で宣言しなければならない。だが常識的に考えればフォームの宣言を *:any で行うことはどう考えてもダメで、例えば本来不要となったプロパティがフォームに残っていても検出できず、思わぬところで型エラーになる恐れがある。そのため、原則的にフォームのスキーマは *:never つまりデフォルトの状態にしておくべきだろう。
そもそもの間違いは、最初に出した UserInfo と次に出した LoginForm ではまったく逆の事をやろうとしていたのに、それらを同じ演算子で行おうとしたことにある。最初の例は既存の型の範囲を狭める演算であり、それに対して次の例ではまったく逆に既存の型の範囲を広げる演算を行おうとしていたわけだ。そして Caty の object 型は
という仕様になっており、これらの性質はそれぞれ open-constraints 及び closed-constraints と呼ばれるらしい。確か XML の属性名は closed-constraints の例だったと思う。つまり、事前にスキーマ定義がなされていたら、そこからはみ出ることは一切不可能ということ。逆に open-constraints なものは OOPL で final などの継承不可の宣言のなされていないクラスで、この場合は継承により後から項目の追加が可能となる。これら二つの制約は一長一短であり、どちらが優れているかという類のものではない。
が、今必要になっているのは closed-constraints な型を元にそれを拡張した新たな型を作るための演算だ。果たしてそんなものが作れるのか?
……といった問題を呟いたら翌日のミーティングに答えを持ってきた檜山さんは改めて化け物としか思えん。
ここからはまだ確定ではなくて俺が先行的に実装している段階だが、多分最終的にはほぼ同じものになるだろうという仕様。まずは実例から。
type MyLoginForm = LoginForm << object {
"additional": string
};
この << が新しく定義された演算子で、これは << の右側のオペランドを型変更作用素として扱う演算子である。型変更作用素とはワイルドカードを持たず、型変数を含む事ができないという事を除き、ほぼ object と同じ文法になる。大雑把に説明するとこの演算子は二つの object 型 A, B に対する type C = A << B という式に対して
という単一代入モデルの演算を行う。 A のワイルドカードを never に限定しているのは理論上の都合で、実装的には A のワイルドカードに any を認めるのは簡単。だが、それで整合性が取れるかどうかはまだ俺には判断付かない。何しろこの先 Caty には静的検証ツールをいろいろ付け加えないといけないんだから、その基盤となる型システムがあまりに大雑把過ぎると実装者の俺が死ぬので、どうにか使い勝手を確保した上でなるべく理論的にきっちりとせねばならんわけだ。
型変更作用素を使いこなせるようになるのにどんだけの時間がかかるか不明というか、そもそもどうやってこの仕様を理解してもらおうかねぇ。そもそも JSON スキーマの Caty 方言(……だったはずだけど既にもう別物)の使い勝手自体がどんだけのものか不明なんだが、俺は Caty プロジェクトの中の人につき判断付き兼ねる。 XML スキーマを考えたら桁違いに簡単だと思うんだが。
新年早々何やってんだか自分でも良く分からんが、まあやっちゃったものは仕方がない。独自ドメイン取ってサイト全部 Caty で作るというのを去年の終わり頃から構想していて、まあ実際に作り始めたのが去年の最後の3日ぐらいというアレなスケジュールだったわけだ。
実際に作ってみた感想としては、
とまあ、 Caty の売りにしてる/しようとしてる部分は割合実現しつつあるなーといったところ。当然、まだ正式な version 1.0 にむけてやんないといけない事も多いんだが。
このサイトを作るにあたって何に悩んだかというとだな、もうこのブログのタイトルに尽きる。ってかさっそく昨日までとタイトルが違うとかなめたことになってるが、昨日までの f;g;h というのはどうしても面白いタイトルが思いつかず、しょうがねえから付けたというか。ちなみに最初は /* XXX: 怪文書 */ じゃなくて /* FIXME: 怪文書 */ だったんだが、別にコメント受け付けてないのでやめた(実はコメント機能は作ってあるが、今までコメント受け付けてなかったんで)。
一番肝心のドメイン名は特に迷わなかったというか、もう多分俺は「return 0 とかいう黒背景のサイトの人」と認識されてるだろうし、別に他に取りたいドメインがあるわけでもないしという事で、 return0.info がちょうど空いてたので取得。サイト名は普通にドメイン名付ければいいのでこれも迷うところなし。
実は去年の11月に、押切蓮介の「プピポー!」の完結編である3巻が出ていた。どうも忙しくてすっかり忘れていたようだ。
http://www.bk1.jp/product/03173632
俺は前に「主人公二人は自分の霊感体質と折り合いを付ける事を選ぶんじゃないか」と予想していたのだが、その予想は完全に外れた。まさかポーちゃんの正体が××だったなんて……。怪奇暴力漫画の「ゆうやみ特攻隊」や鬱系精神異常漫画である「ミスミソウ」を描きつつ(特にミスミソウはヤバい)、こんなハートフルな話を捻り出せる作者の存在が最大の怪奇現象だ。
http://www.bk1.jp/product/03120698
で、その「ミスミソウ」は既に完結済みなのだが、これは気軽に勧められない一品。俺はとても好きだが、救いの無さがただごとじゃない。簡単に言えば「辺鄙な田舎に引っ越してきた美少女が惨劇の引き金となる」なんだが、その惨劇のオチの付け方が凄い。というか、相場晄というキャラクターがヤバすぎる。ガチとヤオでいうと完全にガチ。
そういや大晦日に実家に戻ったときに妹たちに遅めのクリスマスプレゼント兼お年玉にいろいろ買ってやったのだが、前から妹二号が「ネタとしてほしい」とかいってた「新宿の狼」というゲームが凄かったというか酷かった。刑事が GTA な行為をしながら(ってか警察署内で発砲とかできる)犯人に説教たれるというスゲェ代物で、カプコンが発売するのを取りやめたという判断はむしろ当然といいたくなる。
ゲームとしての作りは荒いし、グラフィックもしょぼいし、ストーリーは「いい話かもしれないけど主人公のゲーム中の行動が違法行為まみれ」のためにツッコミどころ満載というかツッコミどころしかない。これ多分、一人で遊ぶよりは複数人で集まって「これはひどい」とかツッコミ入れながら遊ぶか、プレイ動画をニコニコ動画あたりにアップしてみんなでツッコミ入れるのが正しい遊び方だと思う。一人で遊んでるとツッコミきれんよ。
今年の2月25日に(主に悪い意味で)いろいろ話題になってるジャレコの WiZmans World が発売になるが、その翌週ぐらいに世界樹の迷宮IIIが発売というスケジュールである以上、こんなもん買ってる暇はない。流石に仕事がそれなりに忙しいので、1週間かそこいらでゲーム1本終わらせるとか無理。プレイ時間が30時間ぐらいだとすると、一日あたりで4時間以上進めないといけならず、それは無理だろ。そもそもそんな熱中したくなるような出来なのかという疑問もあるが。
ここ数日ほど、どうもこのブログのフィードの本文が全部 HTML 特殊文字がエスケープされた状態になっていたようだ。原因は単純、俺がフィードを吐くためのテンプレートで HTML 特殊文字のエスケープを解除するフィルタをかけわすれていただけなんだが。いや、自分で自分のブログのフィードとか読まないから気がつかんかったでござるよイヤッハッハッハッハ。
いやでもマジな話、こういうマヌケなミスをしでかすリスクと脆弱性を抱えるリスク、どっちを潰すべきかと言われると当然後者なので、これはもう必要経費として割り切るしかないよなー。
Caty の次のリリースにおける最大の売りは JSON ストレージなのだが、
それとは別に売りになる機能があるのでそれをちょっと紹介する。
これと近い将来導入される静的解析を組み合わせると、かなり開発が楽になるはずだ。
まずはこのブログの記事作成・編集の画面を出力する Caty スクリプトを晒してみる。
これは今配布してるバージョンでは動かないが、次のバージョンでは動く予定だ。
user:loggedin --userid=chikara | when {
OK => weblog:edit %entry_id? | print weblog_edit.html | secure:embed-token,
NG => redirect /login.html
}
順番に読み下していくと、
という処理になっている。ポイントは secure:embed-token で、こいつは文字列を受け取ったらそれを HTML として、
HTTP レスポンスを受け取ったらそれのボディを同様に HTML として解釈し、そこに現れるフォームのうち自サイトへ
POST してるフォームに隠しパラメータとしてトークンを埋め込むというコマンドである。
このトークンはサーバ側にリクエストトークンの集合として保存され、二重送信の防止と不正な手段での POST の防止に使える。
例えばこんなフォームがあったら:
<form action="/post.cgi" method="POST">
<p><input type="text" name="foo" /></p>
<p><input type="Submit" value="送信" /></p>
</form>
こうなる。
<form action="/post.cgi" method="POST">
<p><input type="text" name="foo" /></p>
<p><input type="Submit" value="送信" /></p>
<p><input type="hidden" name="$_catySecureToken" value="..." /></p>
</form>
ちなみにこのコマンドは well-formed な XHTML でしか使えないが、どんなに無茶苦茶なマークアップの施された HTML でも
well-formed な XHTML に変換する text:correct-html というコマンドを用意してあるので、
どうしても well-formed なものが仕上がってこないときにはこいつを使うのもアリといえばそうだ(お勧めはしない)。
そしてそのトークンを受け取る側、新規投稿/記事修正処理の Caty スクリプトはこうだ。
user:loggedin --userid=chikara | when {
OK => translate "weblog:PostedEntry << secure:TokenProperty" | when {
OK => secure:check-token | when {
OK => weblog:post-entry /note/,
NG => redirect /note/
},
NG => redirect /note/
},
NG => redirect /login.html
}
さっきよりはちょっと複雑だが、順に追えばこれまたなんということはない。
多分、 Caty スクリプトで書ける処理って大体この程度だと思う。さらにこういった処理は大抵定型処理なので、
マクロ機能を入れればさらに簡略化できるはず。まあその辺の話は置いておくとして、さらにスクリプトの解説を続ける。
最初のログインチェックはもういいとして、問題は次の translate コマンドだ。こいつは引数の型に Web からの入力を変換するのだが、
ここで "weblog:PostedEntry << secure:TokenProperty" という文字列が出てくる。
これは http://return0.info/note/0001.entry で紹介した型変更作用素を使った演算で、
平たく言えば weblog:PostedEntry に secure:TokenProperty のプロパティをくっつけた型を作る演算である。
secure:TokenProperty は先の secure:embed-token で埋め込まれたリクエストトークンのパラメータ名のみを持つ object で、
具体的な定義はこうなってる。
type TokenProperty = object {
"$_catySecureToken": string
};
じゃあこいつを付加される側の weblog:PostedEntry の定義はどうなっているかというと、
type PostedEntry = @new NewEntryForm | @update EditEntryForm;
こうなっていて、何か別の型同士のユニオンとなっている。
じゃあこの NewEntryForm と EditEntryForm の定義はどうなっているかというと、
type NewEntryForm = object {
"title": string,
"body": string,
"tags": string
};
type EditEntryForm = NewEntryForm << object {
"entry_id": string,
};
こうだ。先にこいつの解説を少しだけしておこう。
まずは普通にブログの新規記事作成・記事編集画面を考えると、
というパラメータになると思う。 NewEntryForm << object ... でやってるのはそういうことだ。
もちろんこれはベタに EditEntryForm で馬鹿正直に全部のプロパティを定義し直してもいいが、
アップデート演算(<<)を使えば将来 NewEntryForm に新しい項目が追加になったときなどに便利だ。
それになにより、似たような事を何度も書くのは面倒じゃねえか。
じゃあ PostedEntry の定義に戻る。ようするに PostedEntry は「NewEntryForm か EditEntryForm のどっちか」なのだが、
ここで Caty 特有の問題が生じる。普通に NewEntryForm | EditEntryForm とすると、どちらも object 型なので
タイプタグが排他的な関係になっておらず、ユニオンが取れない。そこで @new と @update というタグを付加し、
二つの型を @new 型と @update 型にしてユニオンを取ってるわけだ。
実際のところ、ホスト言語でプログラムを書くときも「特定のキーが辞書にあるか」よりも
タグ名で振り分けた方が気持ちよく書ける気がする(あくまで俺はだが)。
ここで "weblog:PostedEntry << secure:TokenProperty" という式に戻ろう。
ここまでの知識でこいつは次の式に展開可能だろう。
(@new weblog:NewEntryForm | @update weblog:EditEntryForm) << secure:TokenProperty
これまでの例では object << object の例しか出してこなかったので、
もしかしたら上記の式は意味不明に見えるかもしれない。
が、これはとても簡単な法則で計算可能だ。
まずアップデート演算は分配されるので、以下の形に展開される。
(@new weblog:NewEntryForm << secure:TokenProperty
| @update weblog:EditEntryForm << secure:TokenProperty)
そしてアップデート演算はタグの内部に入り込んで計算されるので、括弧を付けて演算順序を明記するとこうなる。
(@new (weblog:NewEntryForm << secure:TokenProperty))
| (@update (weblog:EditEntryForm << secure:TokenProperty))
最終的な結果を丁寧に書き下すとこうなる。
@new object {
"title": string,
"body": string,
"tags": string,
"$_catySecureToken": string
} | @update object {
"entry_id": string,
"title": string,
"body": string,
"tags": string,
"$_catySecureToken": string
};
そしてこの型に合致する値が secure:check-token コマンドに渡されると、 $_catySecureToken の値が検証されるわけだ。
ちなみに secure:check-token は今のところ面倒なので any -> @OK any | @NG null 型のプロファイルだが、
そのうちこれは _T << TokenProperty -> @OK _T | @NG null にする予定。これは型解析のフェーズで推論できるはず。
ちなみにもしも「トークンを埋め込んだけどサーバ側で secure:scheck-token を忘れた」なんて事になったら、
メイン処理(今回の例だと weblog:post-entry)が走る前に実行時型チェックでエラーになる。
なぜなら weblog:post-entry の入力は weblog:PostedEntry で、そこには $_catySecureToken という
プロパティは存在せず、予期せぬプロパティがあった場合のデフォルトの動作が問答無用でエラーになっているからだ。
というわけで、俺が下請け開発会社で冷や飯食ってたころにセキュリティ監査会社によるアプリケーションのセキュリティチェックで
指摘された項目のいくつかは、 Caty においてはほんの数文字〜数行程度のスクリプトの修正で終わる話で、
そもそも静的型解析を頑張れば「ストレージに書き込んでるのにトークンチェックしてない」みたいなのが
コンパイル時に多分全部判明するので、監査会社のツッコミ待ちすらいらなくなるというとんでもない話でした。
今回はブログの記事作成などの実処理を行う Python コードについては何も触れなかったので、
次のリリースに合わせてそっちの解説も含めた Caty でのアプリケーション開発について記事を書く予定だ。
一週間とちょっと前から遊んでた「ゼルダの伝説 大地の汽笛」をクリア。とはいえ平日はなかなか遊んでられないので、実は昨日晩飯食った後に一気に進めた部分が結構あったり。
前作と操作感覚は大体同じで、違う要素はダンジョンギミック。大雑把に分けると前作同様にファントムの出てくるダンジョンとそれ以外の二つのダンジョンに大別されるわけだが、今回はファントムにゼルダ姫を乗り移らせて仕掛けを解くというシステムがあるので、より謎解きの手が込んでる印象だ。つまり前作よりは難易度が上昇してる。あとボス戦もなかなか歯ごたえがあり、ラスボス戦の熱さは特に気に入った。この時期にネタバレするのはアレなので詳しくは書かないけど、ゲームにおける物語要素のあるべき姿が入ってる。
他のギミックとしては画面に息を吹きかける操作が随所で出てくるので、人によっては「電車の中とかでできねー」と思うかもしれない。が、別に息を吹きかけずにマイク入力のあるところを指で弾いても何とかなるケースもあった(一回電車の中で遊んだときはこれで凌いだ)ので、まあ無理ということもないだろう。
汽車による移動はちと退屈な部分がある(これは前作の船もそうか)。雰囲気自体は気に入ったしゲーム的に面白い仕掛けも出てくるので否定はしないんだが、このワープゾーンを使ってもなお冗長な移動部分はもうちょいどうにかならないものかと思う。本編を進めるだけならそこまで気にならなかったけど、細かなサイドイベントなどが結構あるのでそれ全部やってるとかなり気になるんじゃないかな。
総合すると前作の方向性でさらにやり応えのあるゲームで、なかなかの良作になってると思う。本編のボリュームが手頃で上質なアクションアドベンチャーで遊びたいなら、鉄板で進められる一品。
またちょっと更新が滞りそうな気配なのだが、まあ仕事で結構キツいところに差し掛かっているわけだ。何しろ物凄い設計上の大手術が三つあって、それぞれが関連しあっているという状態。ただまあ、これを乗り切ればかなりの改善にはなる。
どうでもいいが、仕事の合間を縫って「真・女神転生 Strange Journey」をプレイ中。面白いには面白いが、やはり俺にはこういうやたらと大量に用意されたリソースを試行錯誤してどうこうするようなゲームについては、遊び尽くす時間も体力もなくなってしまったようだ。周回プレイとか絶対に無理。
話は去年行われた「圏論デスレース米田スペシャル〜チーム C vs 檜山流〜」(正式名称「層・圏・トポス米田フェスティバル」)に遡る。その時の参加者の一人の確か [[http://d.hatena.ne.jp/DigitalGhost/|DigitalGhost]] さんあたりが中心に「;を直前の射の cod を無視して次に何でもつなげられるようにすればどうたら」という小ネタを話していた記憶があるが、おめでとうございます、多分そのまんまの仕様が Caty スクリプトの正式な機能になりそうです。
Web サイト/アプリケーションのセットアップ時には、必要なテーブルの作成や必要な管理者ユーザの登録といった処理を行う必要がある。そして実はこれは Caty スクリプトでは気持ちよく書けない処理でもある。
以下は現状の Caty スクリプトでのセットアップスクリプトの例。
[
strg:create-table user:UserInfo user, // user:UserInfo 型で user テーブル作成
strg:create-table weblog:Entry entry // 同じくテーブル作成
] | void | // 次のコマンドは全て void 入力なので不要だが一応
[
{"name": "root", "password"...} | user:add // 管理者ユーザ登録
]
自分で言うのも何だが、あまりいいとは思えん。特に Caty では配列の要素の評価順序が未定義なため、「ユーザ登録」と「テーブル作成」を同じリストの中に入れることができない。
かといって次のような書き方はどうよ。
strg:create-table user:UserInfo user | // user:UserInfo 型で user テーブル作成
strg:create-table weblog:Entry entry | // 同じくテーブル作成
{"name": "root", "password"...} | user:add |// 管理者ユーザ登録
strg:create-table も {} によるオブジェクト構築も、入力が void (入力を無視)なので問題なく動く。でもこれはどこからどう入力データが流れていくのかわかりにくい=メンテナンス上の問題になりかねない。行で区切っているから大丈夫かもしれないけど、でも読みにくいのは確か。
strg:create-table user:UserInfo user; // user:UserInfo 型で user テーブル作成
strg:create-table weblog:Entry entry; // 同じくテーブル作成
{"name": "root", "password"...} | user:add;// 管理者ユーザ登録
もう誰がどう見てもそれぞれの行の入出力は無関係だ。ここでの ; は単なる SyntacticSugar で実際には void コマンドあたりに置換される。ちなみに void コマンドの定義はこれ。
追記:いかん、今の実装は手抜きで any にしてるけど、 void コマンドは正式には多相型だった。
// command void :: any -> void; command void :: _T -> void;
入力が何であっても捨てるわけね。で、次に来るコマンドの入力が void じゃなかったらエラーになる。現状はランタイムでしか型チェックしてないが、これはコンパイル時にチェックできるようになる。
ただしこの仕様の場合、普通の Web ページの出力コマンドを次のように書いて混乱するかもしれない。
translate SomeType | print /template.html;
もっともこれもそのうち Web サーバに「コマンドの出力結果の型が WebOutput 型じゃなかったらエラー」という機能を盛り込むので、妙なエラーのスタックトレースなどに悩まされる暇なしに「型エラーなんで直せ」あるいは「多分スクリプトの最後に ; が付いてるから取れ」みたいなメッセージを出せる。はず。
次のリリースには多分入らない(次の次に向けた作業で死にかけてるんで)。まあ、次のリリースは JSON ストレージと檜山さんがここ数日のエントリで書いてる内容のお披露目リリースなので。
真・女神転生 SJ はウロボロス第二形態を倒した所なんだが、えらいこと釈然としない。何が釈然としないかって言うと、
という条件下で、ウロボロスが出してくる「災厄の輪廻」が万能属性で全体ダメージ+各種状態異常というフザケた技で、こればっかりはどうしようもない。他にも全体に特大物理ダメージを与える「断末波」が脅威なのだが(耐性がないとほぼ死ぬ)、これに対抗しようとして物理反射のテトラカーンなどを使うと「災厄の輪廻」ばかりが飛んでくる。というか、ウロボロスの攻撃への反射ができる状態にあると、「災厄の輪廻」の使用頻度が激増するようになっているとしか思えない。普通にやったら 10 ターン中に 2 回程度だとすると、 8 回ぐらい使われるようになったんで、これはかなり疑念を抱いてる。とはいえ、検証する気力はさらさらないが。
結局最後は主人公一人で回復アイテムをふんだんに使いつつチマチマ削って倒したが、本当に釈然としない。基本的に俺は将棋のように一手ずつ敵の手を叩き潰していくプレイスタイルが好きなのだが、最終的に導き出された最善手が「普通に殴り合え」というのがなんとも。
ちなみに主人公の攻撃は一発あたり 50 前後に対して、首尾良く「断末波」を返せた場合は 200 オーバー。名前忘れたけど電撃属性の技を返したときは 400 オーバーのダメージ。「災厄の輪廻」連発のパターンさえなければカウンターで終了できたんだがなあ。物理反射や電撃反射を持ってる仲魔は限られてる上に作るのが手間なんだから、こういう抜け道的な攻略方法を用意してもよかったんじゃねえかと思う。
あと全体的にボスの行動パターンが変化に乏しいから、最初に戦線整えてから全軍突撃が殆どの場合の最適解っぽい。ウロボロス第二形態だったら仲魔全員ノーガードで補助スキル使って、主人公一人で殴り合うとか。スキル継承の仕様などのために細かなスキル調整が難しいから、ある程度大雑把というか単調になるのは仕方がないところではあるんだが。
世界樹III延期かー。世界樹シリーズは確かエンジンは同じものの流用で、真・女神転生 SJ も同じとか聞いていたから、こんだけ実践で磨いてりゃコア部分のバグではないだろう。となると、イベント・シナリオのフラグ管理の問題か、単なるバランス調整か。世界樹シリーズといえばバグというぐらい毎回愉快なバグが発見されるので、不安といえば不安だが(そういや今回は下請けに発注しないで自社開発だっけ?)。
追記:DS で発売されるゲームソフトのスケジュールを確認してみたら、二月の終わりから三月頭にかけて RPG が実に不気味に固まってる。とりあえず任天堂のサイトで確認できたのは
あとドラクエ9のベスト価格も3/4で、さらに一週間後には RPG ツクールが控えてると。どこまで購買層が被っているかどうかは俺にはわからんが、パイの食い合いを避けたと見ることもできるか。
最近ちょっとしたライブラリを書いてみて、 Caty の開発に利用してる。たいした分量じゃないのでソースをそのまま全部載せとく。
class PbcObject(object):
def __init__(self):
self.__initialized = True
@property
def pbc_initialized(self):
if hasattr(self, '_PbcObject__initialized'):
return self.__initialized
return False
def preserve_properties(self):
o = {}
for n in self.__properties__:
o[n] = getattr(self, n)
return OldObject(o)
class OldObject(object):
def __init__(self, old):
self.__old = old
def __getattr__(self, name):
return self.__old[name]
class Contract(property):
def __init__(self, f):
property.__init__(self, lambda obj: lambda *args, ** kwds: self.execute(obj, *args, **kwds))
self.require = PreCondition()
self.ensure = PostCondition()
self.body = f
def execute(self, obj, *args, **kwds):
if obj.pbc_initialized:
old = obj.preserve_properties()
obj.__invariant__()
self.require.assert_condition(obj, *args, **kwds)
assert isinstance(old, OldObject)
r = self.body(obj, *args, **kwds)
if obj.pbc_initialized:
self.ensure.assert_condition(obj, r, old, *args, **kwds)
obj.__invariant__()
return r
class Condition(object):
def __init__(self):
self.__conditions = []
def __iadd__(self, func):
self.__conditions.append(func)
return self
@property
def conditions(self):
return self.__conditions
class PreCondition(Condition):
def assert_condition(self, obj, *args, **kwds):
for a in self.conditions:
a(obj, *args, **kwds)
class PostCondition(Condition):
def assert_condition(self, obj, result, old, *args, **kwds):
for a in self.conditions:
a(obj, result, old, *args, **kwds)
これは何かと言うと、 Programming By Contract つまり「契約によるプログラミング」を支援するためのライブラリ。(本当は「契約による設計」だろうけど、日本語訳のこれはわからんけど、原語の方は商標登録されててその辺面倒なんで確か商標とられてなかったはずの「契約によるプログラミング」を使う)
とりあえずこの手のサンプルとしてはお馴染みのスタックの実装で使い方を見てみる。ただし、単なるスタックではなくて容量制限付きのスタックとする。
class Stack(PbcObject):
__properties__ = [
'_stack',
'_capacity',
'size',
]
def __init__(self, capacity):
self._stack = []
self._capacity = capacity
PbcObject.__init__(self)
def push(self, v):
self._stack.append(v)
def pop(self):
return self._stack.pop(-1)
def top(self):
return self._stack[-1]
@property
def size(self):
return len(self._stack)
def __invariant__(self):
assert self._stack is not None
assert self._capacity >= 0
assert len(self._stack) <= self._capacity
def _not_empty(self, *args, **kwds):
assert self._stack != []
def _longer(self, result, old, *args, **kwds):
assert self.size > old.size
push = Contract(push)
pop = Contract(pop)
top = Contract(top)
push.ensure += _longer
push.ensure += _not_empty
pop.require += _not_empty
top.require += _not_empty
別にこれに対して解説とかイラネエだろとか思うんだが、とりあえず一つ一つの要素を解説していく。
これはオブジェクトが外部あるいはサブクラスから観測可能なプロパティの名前を列挙する特殊な属性だ。ここに列挙されたプロパティは全て後述の Contract で利用される。
というか Eiffel の feature 句のパクリだが、別にメソッド自体は列挙しないので __properties__ にしてある。メソッドも列挙するようにして、ここに property か呼び出し可能でないものが与えられたらエラーとかできなくもないけど、 Python でそこまでやる意味ないというかそこまでやるなら Eiffel 使えよ。
これはオブジェクトの不変条件を羅列する特殊メソッドだ。内部的にはひたすら assert を並べてるだけだが、まあここは assert じゃなくてもいいので、不変条件を破っていたら例外を投げれば細かい事はどうでもいい。
注意点としては、このメソッドが呼ばれるのは後述の Contract でラップされたメソッドが呼ばれたときに、メソッド本体の前後で呼ばれるのみであり、あらゆるメソッド呼び出しにフックしているわけではないということがある。もっとも、契約の設定されていないメソッドは大した事をやらない下請けメソッドで、そこで契約が破られるような操作はありえん(あるいは契約の設定された上位メソッドでチェックできる)と考えれば、別に問題ないと俺は考える。
その辺の制限があるが、要するに Eiffel の invariant のパクリだ。
メソッドに対して契約を付加するためのクラスである。上記のコードで pop = Contract(pop) などとなっていることからわかる通り、実際にはこのクラスはプロパティとして機能する。プロパティであっても property#fget の値が呼び出し可能オブジェクトならメソッドのように振る舞えることを利用して実装している。
Contract オブジェクトは require, ensure の二つのプロパティを持ち、それらは事前条件と事後条件を格納する。 push.ensure += _not_empty という記述からわかると思うが、事前条件と事後条件は複数並べることができる。
事前条件は事前条件を付加した対象のメソッドに渡されるのと同じ引数が渡され、事後条件には戻り値(result)、メソッド実行前の __properties__ で列挙されたプロパティ(old)、事前条件同様の元のメソッドに渡された引数が渡される。最後の元のメソッドの引数はもしかしたら使うかもしれないんで渡してるだけで、実際には result と old だけで十分な事が多い気がする。ちなみにこの old も Eiffel のパクリ。
当然、契約のチェックはそれなりに重い。パフォーマンス問題に発展する事も十分考えられるので、以下のようなコードにすることを検討した方がいい。
if foo.DEBUG:
push = Contract(push)
pop = Contract(pop)
top = Contract(top)
push.ensure += _longer
push.ensure += _not_empty
pop.require += _not_empty
top.require += _not_empty
適当なモジュールでデバッグモードか否かの宣言をしておいて、デバッグモードだったら契約を適用するわけだ。 class 直下に条件式が書けるという仕様は、こういうときに役に立つ。
現在 Caty のコードを大幅にリファクタリングしているんだが、その時についでにこのライブラリを使って契約を埋め込んでみてる。そしたらまあ、案の定バグとかテストコードの間違いが早速いくつか見つかっている。あと品質向上以外にもドキュメントを書きやすくなるとか、そもそも設計しやすくなるといった副次的な効果もある。
Meyer 先生凄い。
割と本気で本業の方でテンパり気味だったんだが、ようやく山を越えた感じ。まあそっちが一通り終わったら、このサイトの実装にも大きく手を入れる必要があって、そのためにこのブログに入れようとしてる機能が入れられてないんだが。何を入れようとしてるかというと月毎のアーカイブなんだが、これがちと面倒。
まずこのブログの記事データは、主に俺の手抜きによって次のような形式で保存されている(今回から新形式のスキーマで記述する。ただし Caty の次のリリースには入らないよ)。
type entry = {
"entry_id": string,
"title": string,
"body": string,
"created": string,
"modified": string
};
要するに時刻データを文字列としてそのまま持たせてるわけだ。まあ、こいつを次のような形式に直すこと自体は全然難しくない。
type entry = {
"entry_id": string,
"title": string,
"body": string,
"created": date,
"modified": date
};
type date = {
"year": integer,
"month": integer,
"day": integer,
"hour": integer?,
"minute": integer?,
"second": integer?
};
この形式になれば何年何月という括りでデータを取り出しやすくなるので(今までの奴でも出来なくもないが、文字列関数を使うので直観的ではない)、この形式にしてから月毎のアーカイブを実装しようかなと。
で、もう一個問題があって、今の JSON ストレージの実装だとデータの一部だけを取り出すという操作が基本的に下位モジュールでできない。 Caty のコマンド実装者から直接触れる API では、前述の例だと entry テーブルへのクエリの結果は常に entry 型のデータがまるまる返ってくる。月毎のアーカイブを作る場合、 entry 型のデータのうち内包されている date 型の created プロパティだけがあれば十分だろう。つまり、次のようなクエリを投げられるようになっていればいい。
@_ANY | strg:select --uniq --path="$.created" entry
Python の API なら大体こんな感じ。
result = storage("entry").select(any_(), path="$.created", uniq=True)
一応これは entry 型から created プロパティのみを抜き出すためのクエリの草案。というかこれでいいだろ。そしてこの場合、 strg:select の戻り値の型は list<date> になっていなければならないのだが、スキーマの方もデータとまったく同じく JSON パスで部分型を取得できるようにしてあるので、これで問題はないはず。
冒頭に書いた通り今回書いたネタは次のリリースには入らんのだが、それでも次のリリースでは JSON ストレージのお披露目になるので楽しみにしていてください。
真・女神転生 Strange Journey をクリアした。なのでいつも通り感想を。
本作では敵に対して殴りかかる前に話し掛け、仲魔になるように交渉したりする「悪魔会話」、
仲魔同士を合体させてより強い仲魔にする「悪魔合体」、仲魔を育成することで入手でき、悪魔合体のカスタマイズ性をさらに広める「デビルソース」、戦闘中に条件を満たしたメンバーが敵への追撃を行う「デビル CO-OP」といったシステムが絡み合い、実にパーティ編成に悩めるゲームになっている。
まず本作は戦闘バランスがなかなかシビアなので、殲滅力のあるパーティを
いかに手っ取り早く編成できるかで相当難易度が変わる。そこで役に立つのがデビル CO-OP で、
これが発動する条件は「敵の弱点を突いたとき、スタンスの同じメンバーがいる」こと。
このスタンスは Law/Neutral/Chaos の三種類で、主人公はシナリオ上の選択肢で変動、
仲魔は固定となっている。そのためデビル CO-OP を活用しようとすると仲魔のスタンスを揃える必要が出てくるが、
特定のスタンスに拘ると潰しが効かなくなり思うように弱点を突くことができなくなったり、
あるいは弱点だらけの編成になってしまう。また選択肢の選び方次第では主人公のスタンスが変わってしまい、デビル CO-OP の頭数が減ることもしばしば。
デビル CO-OP が「それに頼らずとも攻略は可能だが、できた方が間違いなく有利」というバランスなのが悩ましい。
つまり汎用性と爆発力の間にジレンマがあるわけだが、ある程度これを解消できるかもしれないのがデビルソース。これはある程度戦闘をしていくと悪魔ごとに設定された解析度がどんどん増えていき、
解析度が最大の仲魔がレベルアップすると取得できる。
デビルソースには固有のスキルが設定されており、合体時に組み込むことで元の合体で得られるもの+αのスキルを習得させられる。
これで弱点を解消したり、パーティにかけている属性の攻撃を覚えさせたり、特定のボス用にチューニングしたりといった
カスタマイズが可能となる。
基本的に仲魔はどんどん合体させていった方が簡単に強い布陣になるのだが、
合体による変化がかなり激しいため、あまり計画的に強化していくということは困難でもある。
全体的に目の前の脅威に対処するために合体させる事が多いのだが、そこでデビルソースや仲魔を消費すると
その後の編成でまた悩む事になるので、仲魔の合体で試行錯誤する時間はかなりのものがある。
基本的にかなりキツい。まず主人公が死んだらその場でゲームオーバーのくせに即死呪文を平気で唱えてくる奴が
雑魚にも散見されるし、そもそも普通のダメージレースも厳しいので見敵必殺しないと死ぬ。
なので特に序盤〜中盤は先に述べたように仲魔の編成で多いに悩めるのだが、
終盤は敵の方もかなりアタマオカシイ性能の奴がチラホラどころじゃなく湧いてくるので、
ダメージで倒すよりも即死と石化で倒した方がずっと効率が良かったりする。
というか終盤にもなれば全体に高確率で即死を与えるスキルを使える奴が一体か二体はいるし、
主人公はボス戦ではアイテム係になるので雑魚相手には即死スキルの使える銃を装備させるのが多分安牌だろう。
なので雑魚戦は最終的には相当大雑把になり易い気がする。
いや何度も戦う雑魚戦がそんな緻密過ぎてもそれはそれでアレだが、
このゲームって初見の敵は姿が不明で名前も何も分からず、戦いを重ねるに連れて弱点などが見えてくるってシステムなので、
「よくわかんねーけど即死 or 石化させた」「メギドラオンで終了」ってのは何かちょっともったいない。まあそれなりにボリュームのあるゲームなので、最後ぐらいは一気に蹂躙できてもいいのかもしれないけど。
ボス戦は全面的に「死んで覚えろ」な連中ばかりで、多分特に対策しなくても普通にボスが倒せるようなゲームばかりプレイしてた人は
かなり心が折られると思う一方、ボス戦の前には必ず回復スポットとセーブポイントがあるので試行錯誤のし易さへの配慮ならある。
とはいえ、呪文使ったらカウンターで即死など理解のあるプレイヤーでないとクソゲー扱いされるボスもいるんだが。
特にラスボスのやりすぎ感は尋常ではなく、これはもう「アトラスってそういう会社」という認識がないと投げる域に達していると思う。
もっともあまりに攻撃が苛烈なせいで、殆どのボスについて攻略手順がちょいと死ねば見えてくるレベルだったのはちょっと問題か。
特にラスボスなどは「全属性に耐性か反射を付けて、万能属性と攻撃反射で削る」ぐらいしか見えてこないのが。
全体的に、対策を考えるよりもその対策を実践するための準備に時間がかかりすぎたように思える。
ダメージや毒のトラップ、強制移動床、ダークゾーン(オートマッピング不可)、落とし穴、ワープゾーンなどと
こちらのギミックはオーソドックスだが、ダンジョンの種類とボリュームの割には捻りがないので、
途中でダンジョンがただ単に面倒なだけの代物に成り下がっているのが惜しい。
特に終盤になると延々とワープゾーンや一方通行が続くため、それへの対処の意味でも即死スキルによる
迅速な雑魚対策が必要となり、戦闘の大味さにつながっている気がする。
というかバカ正直に雑魚相手に殴り合っていたら今日中にクリアできなかったと思うぞ。
悪意に満ちたダンジョンの作り、シビアな戦闘、パーティ編成の変動の大きさなどかなりハードコアなゲームだ。
多分作ってる方も一見さんお断りな気持ちで作ったのだろう。
大まかに見れば最初から最後まで難易度曲線が上がりっぱなしで、終盤に十分過ぎる戦力が整うまではかなり緊張感が持続する。
遊び応えのあるダンジョン RPG には違いないので、ある程度マゾゲー耐性があるなら遊んでみては。
ついでにラスボス(Neutral ルート)撃破時の俺の編成を晒しておく。実はこの面子でどんだけ戦えるかのテストのつもりだったが、
倒せてしまったのでこれが最終パーティに。
風と物理にしか反射がないので、基本的にメギドラオンとモータルジハードでちまちま削り、
主人公は防御とアイテム。ケツアルカトルは素早さの関係で常に後手に回るので、メシアライザーを毎ターン使う。
あとは運良く反射で大ダメージが入ることを祈ろう。
もう少し筋良く攻略するなら、ペイルライダーの代わりに破魔と呪殺無効で反射と耐性に優れた仲魔を入れて、
デビルソースで片っ端から反射と無効を付けていくのがいいだろう。
どうでもいい Python の小ネタを一つ。 Python では super 関数を使って、規定クラス名を明示せずにメソッドやプロパティを呼び出せる。
class A(object):
@property
def foo(self):
return 1
def bar(self):
return 'bar'
class B(A):
@property
def foo(self):
return 2
def bar(self):
return 'bar!!'
b = B()
print b.foo
print b.bar()
print super(B, b).foo
print super(B, b).bar()
ここで一つ問題。とある事情で絶対に特定の基底クラスのメソッドあるいはプロパティを呼ばなければならない事態になった場合、一体どう書くのか?
ここでメソッドの場合の話は単純で、以下のように単純に基底クラスから直接メソッドへの参照を取得すればよい。クラスから直接参照したメソッドは unbound な状態なので、インスタンスを明示的に渡せる、というか明示的に渡す必要がある。
b = B() print A.bar(b) # ここでは print super(B, b).bar() と同じ
ちと横道に逸れるが、俺はこういった仕様からも Python におけるオブジェクトのインスタンスメソッド及びそのインスタンスは、第一引数を部分適用した関数とその関数の集合ぐらいに思っておけば、明示的な self とかが割とすんなり受け入れられるんじゃないかと思う。というか俺はそういう理解で Python を覚えたんだが。
まあとりあえずメソッドに付いては解決した。それじゃプロパティはどうするのかだが、これは次のように書けば良い。
b = B() print A.foo.fget(b) # ここでは print super(B, b).foo と同じ
Python のプロパティはちょっと特別扱いされてるオブジェクトなんだが、いいか悪いかはともかく fget, fset, fdel といったメンバを使ってプロパティ内部にアクセス可能になってんだよな。
こんなもんを使う機会ってそもそもあんのかよと言われると大変微妙で、多分このテクニックが必要になる場面って継承先のクラスで思わぬプロパティをオーバーライドされたぐらいしか思いつかない。で、そういう事が起こる背景には
などの問題があるとしか思えないので、要するに臭い物に蓋系の急場しのぎのテクニックなんだが。