ソシャゲ系にありがちな経験値計算を自作電卓に組み込んでみた


 こんなん書いてもなー、って思わなくもないが、どうでもいい狭いネタなんか過去に山のように書いてるしな。
 つーことで、自作電卓の話。

 元々、CUIな電卓の自作は好きで、DOS向けに小さいのをアセンブラで書いたりしたこともあった。
 小さいの、と言っても、実行ファイルのサイズが小さいって話で、アセンブラだからソースは無駄にでかい。内部はbigint/rationalな処理で、循環小数は循環節付きで出力するみたいな無駄に頑張った仕様だった。
 Wolfram Alphaとかがある今時なら、こんなもん作ろうとも思わなかっただろうけどさ。

 あの頃の俺はなー、ソースの可読性とか投げ捨てるもの以前で、「見易いソース」って感覚が皆無だった(←今でも怪しい)。そもそもアセンブラで何から何まで全部書いちまおうとか考えた時点で、悪い意味のバカであるな。だがそもそも当時の俺はC言語すら知らねえのだ(笑)。
 なお、TASMのIDEALモードとかいう、もう心底アレなソースだった。いや面白い仕様だったし好きだけど。でもなー。
 しかも、何も考えずに全ての箇所を最適化した。何でもないところでもcall+retをjmpに変えたり、sbbで分岐減らしたり、フラグレジスタ読んで論理演算でどーにかとか、再帰的なマクロでテーブル作って除算を高速にとかやってたような。しかもバカだから曲芸でもコメント書かねえ。
 このように典型的なクソコードであり、完成したからまだいいけど、本気で何やってるか分からんソースになったし、あの経験が特に役に立つことも無かった気はする。
 けど、ほら、アレだ、「昔オフゲで死ぬほどチートやって飽きたからもうオンラインゲームとかでわざわざチートしたいとか思わんし」って現象があるけど、同様に「昔アセンブラで死ぬほどパズルみたいなコード書いて飽きたからもう要らん最適化したいとか思わんし」みたいなのもあるかもしれん。どうだろう。

 何かどんどん狭い話をしてる気が。

 話を戻して。

 Pythonを触るようになって少し経った頃、evalでクソ簡単な電卓作れるよなー、と思った。
 いやまーPerlも入れてたから、ワンライナーで済む話ではあったのだが、そこはそれ。何か作りたくなったのだ。

from __future__ import division, print_function
import sys
expr = ' '.join(sys.argv[1:]) or 'None'
print(expr, '=', eval(expr))

こんな程度だし。

 で、普段から使うようになって、千年戦争アイギスの経験値の計算とかもやってたんだけど。
 ソシャゲ的な奴って、「育成目標は経験値2400、主要狩場で拾えるカードから得られる経験値は225と255、あと手持ちで素材にしたいカードは経験値40が2枚、70が1枚、140が3枚」みたいな状況が良くあるじゃん。
 それを、

>e game.xp(2400, 225, 255, (40,2), (70,1), (140,3))

とかで自動的に計算してくれる機能が欲しいな、と。
 面倒だからやらずにいたけど、アイギスのデイリー復刻実装でこういう計算の需要が俺的に多発したんで、試しに拡張してみた。

from __future__ import division, print_function
import sys
import collections


def clamp(v, min_v, max_v):
    return min(max_v, max(min_v, v))


class game(object):
    @staticmethod
    def xp(target, *exp_and_limits):
        RESULT_HEAD_LEN_MIN = 15
        RESULT_HEAD_LEN_MAX = 50
        RESULT_TAIL_LEN_MIN = 5
        RESULT_TAIL_LEN_MAX = 30

        exp_lims = [
            x if isinstance(x, collections.Iterable)
                else (x, float('inf'))
            for x in exp_and_limits]

        # return [[num, ...], ...]
        def inner_func(target_rest, exp_lims_rest):
            exp_unit, num_limit = exp_lims_rest[0]
            max_num = -(-target_rest // exp_unit)
              # just round up
            max_num = clamp(max_num, 0, num_limit)
            if len(exp_lims_rest) == 1:
                return [[max_num]]

            result = []
            inner_exp_lims = exp_lims_rest[1:]
            for num in range(0, max_num + 1):
                exp_product = exp_unit * num
                inner_target = target_rest - exp_product
                inner_result = inner_func(
                    inner_target, inner_exp_lims)
                for row in inner_result:
                    result.append([num] + row)
            return result

        raw_result = inner_func(target, exp_lims)
        result = []
        for row in raw_result:
            exp_total = sum(
                exp * num
                for (exp, limit), num
                in zip(exp_lims, row))
            excess = exp_total - target
            if excess < 0:
                continue
            result.append((excess, row))
        result.sort(
            key=lambda x: [-x[0], x[1:]], reverse=True)

        head_len = clamp(
            len([
                1 for row in result
                if row[0] == result[0][0]]),
            RESULT_HEAD_LEN_MIN, RESULT_HEAD_LEN_MAX)
        tail_len = clamp(
            len([
                1 for row in result
                if row[0] == result[-1][0]]),
            RESULT_TAIL_LEN_MIN, RESULT_TAIL_LEN_MAX)
        if len(result) > head_len + tail_len + 1:
            result[head_len:-tail_len] = [
                '{} sets omitted'.format(
                    len(result) - head_len - tail_len)]
        return result


expr = ' '.join(sys.argv[1:]) or 'None'
print(expr, '=', eval(expr))

こんな感じで。
 元ソースから他の俺向け機能を削除しまくってから貼ったんで、ちゃんと動くかは知らないけど、試しにさっきのコマンドを走らせてみたら、

>e game.xp(2400, 225, 255, (40,2), (70,1), (140,3))
game.xp(2400, 225, 255, (40,2), (70,1), (140,3)) = [(0, [10, 0, 2, 1, 0]), (0, [7, 1, 2, 1, 3]), (0, [5, 5, 0, 0, 0]), (0, [2, 6, 0, 0, 3]), (0, [1, 7, 1, 1, 2]), (0, [0, 8, 2, 0, 2]), (5, [8, 1, 0, 1, 2]), (5, [7, 2, 1, 0, 2]), (5, [6, 3, 2, 1, 1]), (5, [1, 8, 0, 0, 1]), (5, [0, 9, 1, 1, 0]), (10, [7, 3, 0, 1, 0]), (10, [6, 4, 1, 0, 0]), (10, [4, 4, 0, 1, 3]), (10, [3, 5, 1, 0, 3]), '109 sets omitted', (135, [4, 5, 2, 0, 2]), (150, [0, 10, 0, 0, 0]), (165, [8, 3, 0, 0, 0]), (195, [7, 4, 0, 0, 0]), (225, [6, 5, 0, 0, 0])]

って感じになったので、多分動く気はする。怪しいが。
 なお、出力の意味は、最初の等号の右の

(0, [10, 0, 2, 1, 0])

だけ説明すりゃ分かるかなーと。この場合は「超過0、225*10 + 255*0 + 40*2 + 70*1 + 140*0」ってことで。左の項目が多い方から順にソートされてるから、優先的に使いたい素材を左に指定すると便利とかそんな感じ。
 あと、項目数が10個くらいになるとメモリ使い切る勢いになってくるし、何千通りも最適解が表示されてもまともに見る気にもならないんで、大人しく7個くらいまでにしておいた方が。

 相変わらずの説明する気ねーだろ的記事であるが、まー、分かるべ。行ける行ける。
 あと、Python2.7.x用のつもりだけど、Python3.xでも動いてるようには見える。知らんけど。
 ついでにWindowsの場合、cmd.exeへのショートカットをデスクトップに置いてプロパティ開いてショートカットキーを設定しておけば更に楽に。

 たまにこういう小さいソースコード貼っただけみたいな記事を書くけど、需要無さそうだよなー本気で。ハハハ。
 まーでも、ちょっとコード書ける人なら「そーかーevalで手軽に自作電卓とか便利そうだなー」と思うかもしれないなー、というのもある。ワンライナーと違って、自分用の関数とか定数とか作れるのはかなり嬉しい。
 俺は当時最適だと思ったPythonで書いたけど、Node.jsが登場して一荒れして落ち着いて風格が出てきた今となっては、そっちで書いた方が良い気がしなくもない。電卓としてはPythonの除算演算子(新仕様)は便利だけど。いちいちintとかfloatとか考えなくて済むからなー。

 なお、これで色々な経験値素材の組み合わせを計算させてみたけど、素材を何種類か持ってれば、無駄の無い組み合わせは大抵はゴロゴロ出てくるっぽい。
 だが、そこまでして無駄をなくす意味は多分無い。


ネットワークドライブのファイルを開く時の警告を消してみた


 Windowsのファイル共有とかNASとかで、ネットワーク上のフォルダで主に作業してる人、いますよね。つーか俺もな。
 で、ファイル開く時にいちいち、

開いているファイル – セキュリティの警告
このファイルの作成者を確認できません。このファイルを開きますか?

とか、

開いているファイル – セキュリティの警告
このファイルの作成者を確認できません。このファイルを実行しますか?

とか言われること無いですかね。俺はある。あった。

このファイルはローカル ネットワークの外部の場所にあります。認識できない場所のファイルは、PC に悪影響を与える可能性があります。この場所が信頼できる場合のみ、このファイルを開いてください。

とか下の方に出てて、うるせーバカ、と思いつつも、まあ悪気は無いんだろうと思って、大人しく「開く」とか押してたさ。毎日毎日。

 でも、思うんだけど。
 これ狼少年と同じで、危なくないと分かってるのに毎回「危ないぞー」って言われてると、それはそれで何も感じなくなって危ないんだよな。
 などと、もっともらしい理由を付けてみたけど、単に面倒になった。

 ということで、止め方を調べたところ、コントロールパネル→インターネットオプション、から行くらしい。これ基本的にIEの設定が入ってるとこだよな。変なとこにまとめるなー。ExplorerとInternet Explorerを一体化しようとしてた頃の名残だろか。
 で、セキュリティタブを開いて、と。
 ここから先は、時代によって違うっぽい。最近の環境だと多分、上に並んでるアイコンの「ローカル イントラネット」を押してから、右の方の「サイト」を押す、だと思う。
 もう少し古いと「信頼済みサイト」を押してから「サイト」、かも。

 で、ローカルイントラネットのダイアログが開いたら、詳細設定ボタンを押して、「このWebサイトをゾーンに追加する」のところにフォルダーのパスを入れて追加押すだけ。「このゾーンのサイトにはすべてサーバーの確認 (https:) を必要とする」にチェックが入ってたら切るべきかも。
 フォルダーのパスと言われても意味分からんわ、って場合は、試しに検索してみたら色々なとこで親切に解説されてるようなのでそちらにお任せしたい。

 つーことで、警告無しで開くようになったついでに、ゴミ箱も使えねーかなー、と思ったので調べてみたら英語情報があって、Windows7でもそのまま行けた。基本的には適当なGUID作ってレジストリちょいと弄るだけ。
 日本語情報もあるんじゃねーかなーと思って、Explorer\BitBucket\KnownFolderとかで日本語サイトを検索したら一箇所あった。そっち見た方が楽かも。


パーマリンクを変えてみた


 極力やらない方が良い、と言われてるのは知ってたんだけどさ。
 逆に、さほどアクセス数とか気にしてないこのblogだからこそ、実際やるとどうなるか試しやすくもある訳だ。あんまり実際に体験した人もいないんじゃなかろーか。
 ということで、今更やってみた。
 とはいえ、SEOとかの類をそもそも全然真面目にやってない俺なので、大して有用な情報も書けないと思うけど。

 WordPressのパーマリンクを、デフォの?p=%post_id%形式で放置してたんだけど、何だか急にQueryStringじゃなくPathInfo型にしたくなって。
 元々、rewrite依存な仕様にするとhttpd入れ替えて遊ぶ時に面倒だなー、って理由でデフォのままだったんだけどさ。初期はLighttpdとかNginxとかちょこちょこ入れ替えてPHPもFastCGIにして、とか遊んでたし。今はApacheで気楽に生きてるけど。
 そして、最近はQueryString型だと逆に不便になってきたので。

 SEOをする人達は%post_name%を使う物らしいけど、まー、SEOはどーでもいいので、何となく/archive/%post_id%にしてみた。手間が掛かるのが嫌いだからスラッグとか決めるのもめんどくせーし、エスケープされるようなURLにもしたくなかった、って感じで。
 もちろんお勧めしませんが。

 で、基本的にはちゃんと301で飛ばされてることも確認したんだけどさ。今時のWordPressは何もしなくてもちゃんと301で飛ばしてくれるっぽい。
 ところが、数日したらGoogleウェブマスターツールから「404急増してんぜ」みたいなメールが来てて、凄い勢いでwp-trackback.php?p=%post_id%が404出てるし、タイトルも重複しまくってるし(これは放置するしか無いけど)、検索順位もアクセス数もはっきり下がった感じ。すげー。こえー。ぬるいblogで良かったな俺。
 放置しても404は直らなそうな気もしたので、robots.txtを少しだけ書いた。真面目にやるなら、いわゆるクロール最適化ってのをちゃんと書くべきなんだろーけど。パーマリンク変更のダメージがあまりにもひどかったら考えるかも。

追記:
 robots.txtwp-trackback.phpやらwp-content/plugins/やら何やらを弾いて一〜二日後、クロールエラーは元の数値以下になってた。一日のPVが二桁くらいのblogでも結構クロールに来てるんだな、すげー。


Postgreyが思ったより効いた


 Postgreyを導入して二ヶ月弱が経過したので、そろそろ簡単に触れてみる。
 こいつはメールサーバで使うspam対策の有名なソフトで、greylistingという手法が実装されている。

 Greylistingってのは大雑把に言ったら、一見さんがメールを配達に来たら「ごめん今ちょっと忙しいから後でまた持ってきて」と言いつつ、配達人たるMTA氏が誰だか記録しといて、ちゃんと後でまた届けに来たら受け取る、という仕組み。
 メール配送は、「後で来てね」と言われたら後でもう一度届けに行け、という取り決めがあるので、まともなメールなら配送がちょっと遅くなるけど届く。でも、spamの配達人となるソフトはせっかちというかキチガイなので、「後で」と言われたその場でメールを投げ捨てることがほとんどらしい。まあspamの配達なんてのは(ソフトにとって)ブラックなお仕事だしなー。そうしないとノルマが達成出来ない、みたいな。

 んでPostgreyだけど、Postfix使ってる人ならyum辺りでインストールしてちょちょいと設定するだけで使えるんで、配送が遅くなるかもなーと思いつつも試してみた。

 導入初日のspamの量はそれまでの1/10くらいになった。大体、100通/日から10通/日くらい。ほほう。でも導入直後は効果が高いかもしれんしな。
 で、Gmailとかのメジャーなところは最初から弾かれないっぽい。でも、超メジャーどころだけかも。
 普通のメールだと、初回の配送は5〜10分くらい待たされることが多いなー。良く引っ掛かるのは、会員登録とかパスワード忘れたとかの申請でメールを待つ時で、基本的に待たされるようになった。他はあんまり気にならない感じだけど。

 テンションが上がって、他に効果のありそうな技があれば併用したいかもなー、とか思ったけど、通常のメールを弾かない安全性、管理の手間に見合う効果、という条件で落ち着いて考えると、greylistingだけで良い気もしてきた。
 確実な安全が見込めない手法は、greylistingの前段に直列で繋いで「Greylistingに掛けるまでもない正当なメールを通すバイパス」の位置付けでしか使えない。Greylistingが後段にあるなら「Greylistingを抜けられない正当なメールなんざ面倒見きれんわボケ」という設計なんだから、遅配を減らす以外の効果が思い当たらないので、導入しない。
 確実に安全な手法なら導入出来るけど、「Greylistingを抜けるようなspamでも結構引っ掛かりそう」という条件を満たさないと多層防御にならない。システムが複雑になれば安全性も下がるし。

 などと言い訳を並べて、Postgrey単独で気楽に運用することに。

 導入から一ヶ月半くらいで、いきなりspamの量がもう一段階、目に見えて減った。10通/日から1通/日くらいに。何だこれ。
 うーん、「このメールアドレスうまく届かねーわ」ってなってきたのかなあ。だといいけど。そもそもうちにspamを送る意味が無い気もするから、お互いに良い話ではあると思うのだ。
 たまたま何か大規模なspam減があったのかもだが。時々あるっぽいし。

 と思いながら半月経過。Gmailの迷惑メールボックス(30日で自動削除)がいつも3000通くらい残ってたのに、今は81通。一ページに収まった。ここ半月のspamは17通だから、あと半月もこの調子が続くなら一画面で見通せそうだが、はてさて。

 まあ、あれです。ちっこい宇宙船が大戦争のとばっちりを受けないように小さくバリア張ってるイメージで。
 いやほんと、うちとかに送っても多分あんまり意味無いし勘弁してください。

追記:
 さらに一ヶ月後、Gmailに残った迷惑メールは12通に。来ない日が普通になってきた。導入前は3000通前後だったんだよな…。
 こうなると余裕も出てくるので、油断して電話番号とか載せてる詐欺メールはポリス方面に送付したくなってきたりもする。巻き込まれないように、とか言ってた割に強気である。総務省や経産省の窓口には既に送付してるけど、詐欺メールの報告ってここで良いんだろか。

 もう一つ、書き忘れてたけど、自己紹介のとこのメールアドレスをJavaScriptで適当にスクランブルしてみた。以前は画像を使ってたけど、クリッカブルじゃないのが面倒だなーと思って。
 まあ、ここからメールを送る人はいないかも、って気もするが。


IPv6対応にしてみたけど地味な問題が残る


 まー先に言うとFail2banが問題なんすけどね。
 すっかり忘れてたけど、ふと思い出したのでIPv6対応を再開してみたのだ。

 さくらのVPSは多分もう全部IPv6対応な気がするので、素直に公式のCentOS6向け解説の通りに設定してみよう。
 せっかくメモも兼ねたblogなので、主に自分用としてまとめてみる。

 まずコンパネの「仮想サーバ情報」でIPアドレス確認。「詳細」を押すとゲートウェイとネームサーバのアドレスも出る。

 /etc/sysconfig/networkに、

NETWORKING_IPV6="yes"
IPV6_DEFAULTDEV="eth0"
IPV6_DEFAULTGW="ゲートウェイのアドレス"

のように追記。

 /etc/sysconfig/network-scripts/ifcfg-eth0に、

IPV6INIT="yes"
IPV6_ROUTER="no"
IPV6ADDR="アドレス"

のように追記。アンダーバーがあったり無かったりしてうざい。

 せっかくだから/etc/resolv.confにも、

nameserver ネームサーバのアドレス

とか追記しといたけど普通に要らないと思うし、公式の手順でも特に触ってない。いいんだ、半分実験用の鯖だからな!

 大阪リージョン以外はこのまま

# service network restart

でいいみたいなんだけど、大阪リージョンだと公式の解説に「一度落としてコンパネから起動してほしいナリ」的なことがまともな文章で書いてあるので、ざっくり

# shutdown -h now

で落としてコンパネから起動した。

 で、ifconfig -aを実行して、eth0inet6 addr: アドレス Scope:Globalがあれば、インターフェース設定は良し、と。
 netstat -rnA inet6も実行して、Destination::/0Next Hopがゲートウェイのアドレスになってれば、ゲートウェイの設定も良し、と。
 最後に、ping6 ipv6.google.com辺りでテストすればおしまい。

 あとはまー、各種サービスの設定か。つっても、何もしなくてもIPv6対応のサービスばかりなのだが。
 だがしかし、Fail2banで防御しておきたいサービスは残念ながら当面IPv4専用にすることに。通したいIPv6トラフィック以外をファイアウォールで全部弾くのが一番楽そうな気がするので、そうしてみた。
 Fail2banのIPv6サポートが簡単そうに思えてなかなか来ないなーと思ってたら、そもそもIPv6だと攻撃者がいちいちアドレスを変えるとか余裕な訳で、うーむ…。もうこのアプローチは駄目なんかなあ。最近はbotnetから来るから、IPv4でもいまいち止まってない感じだし。
 とはいえ、それなりに減る、ってのも相当有難いしなあ。Postgreyとかもそうだけど。いや、Postgreyは1/10くらいに減ってるから、それなりどころじゃないが。
 Fail2banがほぼ通用しなくなるか、IPv6でSMTPとか話せるようにしないと困る時代が来たら、その時にはスパッと諦めるとして、それまでは特定のサービス以外はIPv4だけで動かすかなあ。という何だかすっきりしない結論になるのであった。うーむむむ。

 まー何となくすっきりしない。こういう時は何か問題を抱えてる可能性が低くないんだよな、面白いことに。面白がってる場合でもないが。気ーになーるぞーう。