詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Angband(4.x系)を日本語化している話

こんにちは。この記事ははてなエンジニアAdvent Calendarの24日目の記事です。昨日はid:todays_mitsuiさん、明日はid:d-haruさんです。いいタイミングで風邪を引いてしまったので、今年も趣味の話です。

ローグライクといえば最近は定義がけっこう広がってきている印象がありますが、いわゆるRogueライクなコンピュータゲームの系譜で、Nethackと並んで重要なのがAngbandですね。自分も高校生の頃だったか、指輪物語を読んであまりに面白かったので何かゲームをやりたい! と思って何故かAngbandのフォークのフォークである変愚蛮怒を始めたような覚えがあります。いや順序が逆だったかもしれねェ……。何も覚えていない……。ともあれこういうテキストベースのゲームですね。トルネコとかシレンとか。

思うところあって今年はこのAngbandの日本語化を休み休みやっていたので、その紹介をしようと思います。ちなみにまだまだ途上で、かつ自分のメイン環境であるmacOS対応しかできてないです……。

Angbandは派生バリアントが星の数ほどあって、まとめて*bandと呼ばれたりもします。*bandも歴史が長く、日本語化も何度かなされているようです。

国際化の戦略

Angbandの日本語化というか、国際化について考えるべきなのはおそらくテキストだけ。タイムゾーンのようなものは存在しないので、そこは楽ですね。そしてAngbandに含まれるテキストにソースコード中のメッセージと、別ファイルとして存在しているゲームデータの二種類がある。ゲームデータには多数の敵モンスターの名前やアイテムなどの名前や挙動がテキストで記されている。

上で紹介した先駆者たちは、#ifdef JP のような分岐をソースコード中に入れてメッセージの表示を出し分けていたけど、翻訳データを外出しすることで翻訳自体の進捗がわかりやすくなるだろうという見込みのもと、今回はlibintl (gettext) の仕組みで対応してみることにした。ゲームプレイ中に言語を切り替えることを想定する必要もないので、メッセージは静的に決定できていいわけだけど、まあ後からどうとでもできるかなという想定。

過去に日本語化が行われたときはAngbandのバージョンは2.xや3.xだったけど、今見てみるとメジャーバージョンは4となっていて、内部的な改善も進んでいる模様。Unicodeへの対応などはすでになされていて、そこはすんなりいったのでよかった。

日本語化

とはいえ完全に英語圏に向けて作られたものなので、日本語に対応するのはそれなりに骨が折れる。スクリーンショットを見ると分かるとおりテキストベースのゲームなので(グラフィックを有効にすることもできる)、日本語の表示幅がASCIIの倍であるところがやはり罠。strlen や数字の 1 が使われているところで、いま注目しているのが文字数なのか画面幅なのかを気にしてやる必要がある。

まあこれはわかりやすい。難しいのはメッセージ文字列をプログラムで組み立てているところだ。こういう箇所は綺麗には解決できない。たとえば次に続く数字が8か80台だったら、不定冠詞として"a"を出力したあとに"n"も出力するとか。これは日本語化のときには完全に不要なので、 #ifdef とかで消す対応とした。

英語と日本語との文法の体系の違いで困る部分もある。例えば敵の行動の説明に「These monsters bite, claw and terrify.」とあった場合、日本語なら「このモンスターは噛みつき、引っ掻き、恐怖させる。」などと動詞を活用させたくなる。かといって、これらの説明文はゲームデータから構成されていて、同じ単語が別の位置で登場しうるので、biteをそのまま「噛みつき」に翻訳することはできない。ここは翻訳の工夫でなんとかしようとしているがあまりすっきりしない。変愚蛮怒などではこの活用を実装していた覚えがある。そもそも自然な文章の形に落とそうとするのが無理な話なのかもしれない。

あと面白かったのが、アイテムを拾ったときに You have {count} {item}s. と表示するのでこれをそのまま「{count}個の{item}を持っている。」と翻訳してしまうと、アイテムをすべて捨てたときに「no more個の{item}を持っている。」になってしまうこと。英語だとYou have no more {item}. で素直に通ることを利用していておもしろい。

まあだいたいの場所では、文字列リテラルを国際化することでなんとかなるパターンが多い。sprintfが使われる場面では、%sにはめたい引数の場所が日英で違うことがおおいのでPOSIX拡張の %1$s のようなものを使うことにしている。

進捗の可視化

まあこれはライフワーク的にのんびりやろうと思っているので、ときどき思い出したら手を出せるよう、国際化および日本語への翻訳の進捗をトラッキングするようにしている。後者は.poファイルの msgtxt エントリが埋まっているかをカウントすればよいだけなので簡単。前者はソースコード中の文字列リテラルが _() で囲まれているかを調べたいのでちょっと難しい。これをなんとかしたい動機で生まれたのがclang-tidyのカスタムチェックでした。

このへんは https://github.com/motemen/angband/issues/4https://github.com/motemen/angband/issues/3 で元気に動いていそうです。

また、tokuhiromさんがIMEを作っている話で紹介されていた、GitHubのDiscussionsに開発日誌を書く話がよかったので真似している

というわけでリポジトリはこちら。生暖かく応援してくださいね。書いてて思ったけどまずはWindows版の対応かな……。

GitHub - motemen/angband: A free, single-player roguelike dungeon exploration game

Goのテンプレートを静的型チェックする

Goでちょっとしたウェブアプリケーションを書く際、依存を減らしたい気持ちでGoのhtml/templateを使う場面もないわけではない。とはいえ、HTMLを組み立てるのも最近はReact/TypeScriptに慣れきってしまっているので、実行してみるまでテンプレートが正しいかどうかわからない、なんてのは不安を誘う状況だ。

しかし、テンプレートの解析時に {{template ...}} で呼び出されるテンプレートを指定しないとエラーとなることからもわかるとおり、Goのテンプレートは事前にしっかり解析されているらしい。そしてありがたいことに、Goのテンプレートはそのパーザを別パッケージとして公開してくれている(text/template/parse)。これを使って、Goのテンプレートを静的型チェックするツールを書いた。

GitHub - motemen/go-template-statictools

というわけでさっそくインストール。

$ go install github.com/motemen/go-template-statictools/cmd/gotmplcheck@latest

使い方は簡単。チェックしたいテンプレートファイルを引数に渡すだけ。

$ gotmplcheck template.html

とはいえこれだけだと構文解析が通るかどうかだけであまり意味がない。型情報が渡されない場合はいわゆるanyのように扱われて、すべての型チェックが通ってしまう。 実際には、テンプレートに渡すデータの型と、それに対するフィールドやメソッドの呼び出しが正しいかどうかを知りたい。-dot でこの型を指定できる。exampleディレクトリの例だと、

$ gotmplcheck -dot github.com/motemen/go-template-statictools/example.Data example/example.tmpl

という感じ。するとこのようにエラーが表示される。やったね!

example/example.tmpl:4:11: in .Title_with_typo: can't evaluate field Title_with_typo in type github.com/motemen/go-template-statictools/example.Meta
example/example.tmpl:10:9: in .NoSuchMethod: can't evaluate field NoSuchMethod in type github.com/motemen/go-template-statictools/example.Item
example/example.tmpl:12:7: in $v.NoSuchField: can't evaluate field NoSuchField in type string

データの型の指定はテンプレート中に {{/* @type github.com/motemen/go-template-statictools/example.Data */}} などと書くことでも可能。

レイアウトファイルのようなものを用意してテンプレートを複数分けている場合は、それらをまとめて指定する。最初のファイルがエントリポイントになる。

$ gotmplcheck example/layout_layout.tmpl example/layout_content.tmpl

ユーザ定義の関数(FuncMap)がグローバル変数として定義されていれば、それをオプションで指定することも可能。これはちょっと使いにくそう。

$ gotmplcheck -funcmap github.com/motemen/go-template-statictools/example.funcs example/example.tmpl

ユーザ定義の関数や呼び出すテンプレートが存在しない場合もエラーにしたくない場合は -soft を指定することで無視できる。

とまあ書いてみて、それなりに使えるものにはなった。実装漏れなどで途中で型がわからなくなった場合は、それ以降の型チェックを無言で通してしまうので、完全な型チェックを行えるようなクオリティではないけど、まあまあ便利かな。

ISUCON13に参戦した(カラアゲネイティブ、105,751点、15位) #isucon

追記: 15位でした

ISUCON13 まとめ

もう何度目か忘れたけど、チーム「カラアゲネイティブ」(Songmutoricls)でISUON13に参戦。

リポジトリはこちら。

GitHub - motemen/isucon13

toriclsくんが午前中不在であることがわかっていたので、開始前に集まってGitHubやSlackの会場の相談をしてからスタート。実装はGo。

Songmuさんと問題を読みながら方針を検討。DNSをやるのは面白そうだけど、全然触ったことがないし意図もまだつかめなかったので、まずはmysqldがボトルネックになっていることを確認して、愚直にpt-query-digestでクエリの改善を進めていった。今回はじめてquery-digesterを使ったけど、便利でした。一方でGoアプリケーション自体の改善やアクセスログからのチューニングはまったくやらなかった。

最終的にはPowerDNSに手を入れることはせずに、PowerDNS用のDBとisupipe用のDBを分けるくらいの対応だった。

  • isu11: nginx, pdns, isupipe
  • isu12: isupipe, redis
  • isu13: mysql

という感じ。

あとは思い出せる範囲でざっくりと。ほんとにpt-query-digestの結果を上から潰していくことしかしてない。

  • 初期実行: 3,862
  • livestream_tagsにインデックスを張る: 4,629
  • ADMIN PREPARE が支配的だったのでinterpolateParamsを設定: 4,977
  • 投げ銭、リアクション数の統計情報の一部をRedis化: 6,335
    • 統計情報については他にもまとめて対応できそうな部分も横目に見ていたが、主要ではなさそうだったのでこの時点では後回しにした。
  • 予約枠をstarted_at、end_atで見てるところはidベースで見るようにできそうだったので変更: 7,517
  • このへんでtoriclsくんが合流。とくに説明せずにキャッチアップしてくれたので助かった。
  • Songmuさんがiconsのファイル化をしてくれてだいぶ伸びた。それからtagsのN+1解消、themeのRedis化など: 18,098
  • DBとアプリケーションを分ける: 27,976
  • goccy/json化、ng_wordsにインデックス、アプリケーションを2台にわける: 34,688
  • 統計情報のその他のクエリが支配的になっていたのでRedis化: 36,603
  • iconsをredis化: 57,313
  • 複数台構成をさらに調整: 74,129
    • ユーザ登録時にpdnsutilを呼んでるのを思い出せてよかった。ユーザ登録処理をpdns同居のアプリケーションのみで行うように変更(したはず)
  • 最後にダメ押しで getReactionsHandler, getLivecommentsHandler のN+1を解消して2万点ほど上乗せして: 105,751

追試に失敗してなければこれが最終スコア。水責め対応をいくらかでもできればもう少し伸ばせたかもしれないけど、明確にボトルネックなようにも見えなかった&具体的な手を思いつけていなかったので手が出せず。

運営の皆さんおつかれさまでした!!!

はてなで一緒に働きませんか?