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


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

 元々、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とか考えなくて済むからなー。

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

(Visited 74 times, 1 visits today)

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください