【GameTips】ダメージ計算

どうもです、タドスケです。

ゲーム開発で使えそうな部品を提供するこのシリーズ。

今回はバトル系ゲームには必須のダメージ計算機能を作ってみました。

この記事内で公開しているコードは全て自作なので、個人/商用問わずご自由に流用いただいて構いません。
ただしコードを利用して損害が出た場合でも、その責任は負いかねます。
目次

実行例

今回のプログラムを実行すると、以下のような出力が得られます。

威力10、物理攻撃 → 物理防御力8
  2 のダメージ!
威力10、火属性魔法攻撃 → 魔法防御力4、耐性なし
  6 のダメージ!
威力10、火属性魔法攻撃 → 魔法防御力4、火耐性50%
  3 のダメージ!
威力10、物理攻撃、毒特攻2倍 → 物理防御力5、毒
  10 のダメージ!
威力10、水属性魔法攻撃、眠り特攻1.5倍 → 魔法防御力2、水耐性75%、眠り
  3 のダメージ!

攻撃側は威力、種別(物理/魔法)、属性(火水土風)、状態異常特攻(毒・眠り)の情報を持っています。

防御側は物理/魔法防御力、属性耐性、状態異常の情報を持っています。

両者を利用してダメージ計算した結果が出力されます。

実装方法

実行例のような動作をするためにどのようなプログラムを書けばよいでしょうか。

クラスごとに説明します。

AttackInfo クラス

class AttackInfo:
    """攻撃情報.

    :param power: 威力
    :param type_: 攻撃タイプ
    :param attr: 属性

    :param cond_mag_dict: 状態異常特攻の辞書(状態異常種別:倍率)
    """

    def __init__(
            self,
            power: int,
            type_: AttackType = AttackType.PHYSICS,
            attr: Attribute = Attribute.NONE,
            cond_mag_dict: ConditionMagnificationDictType = None) -> None:
        if power < 0:
            raise ValueError

        self._power = power
        self._type = type_
        self._attr = attr
        if cond_mag_dict is not None:
            self._cond_mag_dict = cond_mag_dict
        else:
            self._cond_mag_dict = {}

    @property
    def power(self) -> int:
        """威力."""

        return self._power

    @property
    def attribute(self) -> Attribute:
        """属性."""

        return self._attr

    @property
    def condition_magnifications(self) -> ConditionMagnificationDictType:
        """状態異常特攻の辞書."""

        return self._cond_mag_dict

    def is_physics(self) -> bool:
        """物理攻撃か?"""

        return self._type == AttackType.PHYSICS

    def is_magic(self) -> bool:
        """魔法攻撃か?"""

        return self._type == AttackType.MAGIC

    def get_condition_magnification(self, cond: Condition) -> float:
        """指定した状態異常特攻の倍率を得ます."""

        mag = self._cond_mag_dict.get(cond)
        if mag is None:
            return 1.0
        return mag

攻撃情報を表すクラスです。

威力、種別、属性、状態異常特攻の辞書を持っています。

種別(AttackType)はそのままプロパティとして公開せず、is~ 関数を通して判定するようにしています。
こうすることで、type == AttackType.PHYSICS というような生々しい判定文をあちこちに書かなくて済み、プログラムの見通しが良くなります。

状態異常特攻の辞書についても同じようにしたかったのですが、こちらはダメージ計算時に複数の状態異常をまとめて処理する必要があるため、そのまま公開しました。

DefenceInfo クラス

class DefenceInfo:
    """防御情報.

    :param physics: 物理防御力
    :param magic: 魔法防御力
    :param res_dict: 属性抵抗率の辞書(属性→抵抗率)
    :param conditions: 状態異常。正常時は None
    """

    def __init__(
            self,
            physics: int,
            magic: int,
            res_dict: AttributeResistanceDictType = None,
            conditions: Condition = None) -> None:
        if physics < 0:
            raise ValueError
        if magic < 0:
            raise ValueError

        self._physics = physics
        self._magic = magic
        if res_dict is None:
            self._res_dict = {}
        else:
            self._res_dict = res_dict
        self._conditions = conditions

    @property
    def physical_power(self) -> int:
        """物理防御力."""

        return self._physics

    @property
    def magical_power(self) -> int:
        """魔法防御力."""

        return self._magic

    def get_regist(self, attr: Attribute) -> float:
        """属性抵抗率を得ます.

        :param attr: 属性
        :return: 属性抵抗率。見つからない場合は 1.0
        """

        res = self._res_dict.get(attr)
        if res is None:
            return 1.0
        return res

    def has_condition(self) -> bool:
        """状態異常を持っているか?"""

        return self._conditions is not None

    def is_condition(self, condition: Condition) -> bool:
        """指定の状態異常を持っているか?"""

        if self._conditions is None:
            return False
        
        return bool(self._conditions & condition)

防御情報を表すクラスです。

物理防御力、魔法防御力、属性抵抗率の辞書、状態異常を持っています。

攻撃側はあらかじめどんな攻撃をするか分かっているのに対し、防御側は攻撃されるまでどんな攻撃が来るか分からないので、使う可能性のある情報を全て持っています。

Damage クラス

class Damage:
    """ダメージ.

    :param attack: 攻撃情報
    :param defence: 防御情報
    """

    def __init__(
            self,
            attack: AttackInfo,
            defence: DefenceInfo) -> None:
        self._attack = attack
        self._defence = defence

    def calc(self) -> int:
        """ダメージ値を計算します.

        丸め誤差を少なくするため、ギリギリまで float で計算し、最後に int で丸めた値を返します。

        :return: ダメージ値
        """

        # 基本ダメージ
        val = self._calc_basic()
        # 状態異常特攻
        val = self._calc_cond_mag(val)
        # 属性抵抗
        val = self._calc_regist(val)

        return int(val)

    def _calc_basic(self) -> float:
        """基本ダメージ値を計算します.

        :return: 基本ダメージ値
        """

        at_pow = self._attack.power

        if self._attack.is_physics():
            # 物理ダメージ
            df_pow = self._defence.physical_power
        elif self._attack.is_magic():
            # 魔法ダメージ
            df_pow = self._defence.magical_power
        else:
            raise NotImplementedError

        return float(max(0, at_pow - df_pow))

    def _calc_cond_mag(self, val: float) -> float:
        """異状態異常特攻を適用したダメージ値を計算します.

        複数の状態異常と一致する場合は、足し合わせた倍率が適用されます.
        倍率 1.5 の特攻が3つある場合、2.5倍になります.

        :param val: ダメージ値
        :return: 適用後のダメージ値
        """

        matches = []
        for cond, mag in self._attack.condition_magnifications.items():
            if self._defence.is_condition(cond):
                matches.append(mag)

        sum_mag = 1.0
        for mag in matches:
            sum_mag += (mag - 1.0)

        return val * sum_mag

    def _calc_regist(self, val: float) -> float:
        """属性抵抗を適用したダメージ値を計算します.

        :param val: ダメージ値
        :return: 適用後のダメージ値
        """

        attr = self._attack.attribute
        ratio = self._defence.get_regist(attr)
        return val * ratio

ダメージ計算を行うクラスです。

コンストラクタ(init)で攻撃情報と防御情報を受け取り、calc 関数で計算結果のダメージを返します。

まずは威力と物理/魔法防御力から基本ダメージを計算し、状態異常特攻→属性抵抗の順に倍率をかけ合わせていきます。

わざわざ関数を分けているのは、関数ごとのテストを書きやすくするためです。

例えば属性抵抗の計算を行う _calc_regist 関数については、それ単体で見れば与えられた float 値に倍率をかけて返す関数とみなせます。

_calc_regist 関数のテストを書く際には、前後のダメージ計算のことを考える必要はありません。

適当に value=10 とかを与えて、意図した倍率がかかった値が返ってくるかだけを見れば十分です。

同じように基本ダメージ、状態異常特攻についても個別でテストを行い、それぞれがどんな入力値でも正しく動くことが個別に確認できれば、それらを組み合わせた一連のダメージ計算処理も正しいとみなせます。

calc 関数にありとあらゆる値の組み合わせを与えてテストするよりも、ずっと簡潔にテストが書けます。

コード

コード全文は以下の場所にアップしてあります。

実装

GitHub
gametips/src/damage/damage.py at master · tadosuke/gametips ゲームで使えそうな処理. Contribute to tadosuke/gametips development by creating an account on GitHub.

テスト

GitHub
gametips/tests/damage/test_damage.py at master · tadosuke/gametips ゲームで使えそうな処理. Contribute to tadosuke/gametips development by creating an account on GitHub.

拡張のアイデア

ターンバトルシステムと組み合わせる

あわせて読みたい
【GameTips】ドラクエ風ターンバトルシステム どうもです、タドスケです。 Python で何か作りたいけど、大きなゲームをまるっと作れるほどの余力がない現状。 いろいろ考えてみたところ、 ゲームそのものじゃなくて...

先日公開したターンバトルシステムでは、以下のようにダメージ計算を行っています。

def attack(self, target: Character) -> int:
    """攻撃する.
    :param target: 攻撃相手
    :return: 与えたダメージ
    """
    point = target.damage(self._atk)
    return point

def damage(self, atk) -> int:
    """ダメージを受ける.
    :param atk: 攻撃力
    :return: 受けたダメージ
    """
    point = max(int(atk - self._def / 2), 0)
    self._hp = max(self._hp - point, 0)
    return point

攻撃力、防御力には生の int 値を使っていますが、これを AttackInfo、DefenceInfoクラスに置き換えることで、属性なども考慮したダメージ計算処理に置き換えることができます。

また、AttackInfo に入れる威力値をキャラクターの力・武器・スキルなどから計算して入れるようにすれば、よりゲームらしくなります。

状態異常の付与/回復

AttackInfo 側に状態異常の付与率、DefenceInfo 側に防御率を入れれば、「毒攻撃を毒耐性アクセサリで防御する」ということもできます。

ダメージを受けた際にランダムで眠り状態が回復するようにしても良いかもしれません。

状態異常が付与された/回復したかどうかは、calc 関数の戻り値をタプルや Result クラスにして含める方法や、ダメージ計算とは別のクラスで行う方法が考えられます。
(Damage.calc で計算→ダメージが1以上なら、状態異常判定関数を呼ぶ、など)

ランダム要素

ターンバトルシステムの時にも触れましたが、ダメージ値に ±20% の範囲で変動させたり、3%の確率でクリティカルヒット(防御無視、ダメージ○倍)になったりすると、ゲームらしくなります。

以上、ダメージ計算機能の説明でした!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (2件)

コメントする

目次