複合属性を考慮した三すくみ　の関係を　enumのキレイなコードに書き直した話 in 英語学習ソシャゲの英語物語

どうも毎日２万人弱がプレイする
[英語学習ソーシャルゲーム、英語物語](https://eigomonogatari.com/)の開発者、ゴン氏です。

英語物語では火属性・水属性・風属性、火水属性、水風属性、風火属性、火水風属性
の７つの属性があるのですが、
![AttributeRef.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130870/3b6738c4-124a-af93-5d10-af8795c37203.png)


そのダメージ計算や、属性の演算（火水パネルに反応するのは、火属性と水属性）を定義するのに、
もともと下記のような汚いコードを書いてたんですが、
一念発起してきれいなコードに書き直したのでその備忘録です。

もし、もっとCoolな方法あったら、ぜひ教えてください😊
追記：コメントでビット演算を使ったスーパーCoolな方法を教えてもらってます。
ぜひコメントまでお読み頂けると嬉しいです。

#属性を表すenum
```OneEventEnumAttribute.cs
public enum OneEventEnumAttribute
{
    Dami = 0,
    Fire = 1,
    Water = 2,
    Wind = 3,
    FireWater = 4,
    WaterWind = 5,
    WindFire = 6,
    FireWaterWind = 7,
}
```

#以前の汚いコード

```cs:OneEventAttributeDamageLogic.cs
private float OneEventAttributeDamageLogic(OneEventEnumAttribute from, OneEventEnumAttribute to)
    {
        if (from == to)
        {
            return 1;
        }

        switch (from)
        {
            case OneEventEnumAttribute.Fire:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1f;
                    case OneEventEnumAttribute.Water:
                        return 0.5f;
                    case OneEventEnumAttribute.Wind:
                        return 1.5f;
                    case OneEventEnumAttribute.FireWater:
                        return 0.75f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1f;
                    case OneEventEnumAttribute.WindFire:
                        return 1.25f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.Water:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1.5f;
                    case OneEventEnumAttribute.Water:
                        return 1f;
                    case OneEventEnumAttribute.Wind:
                        return 0.5f;
                    case OneEventEnumAttribute.FireWater:
                        return 1.25f;
                    case OneEventEnumAttribute.WaterWind:
                        return 0.75f;
                    case OneEventEnumAttribute.WindFire:
                        return 1f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.Wind:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 0.5f;
                    case OneEventEnumAttribute.Water:
                        return 1.5f;
                    case OneEventEnumAttribute.Wind:
                        return 1f;
                    case OneEventEnumAttribute.FireWater:
                        return 1f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1.25f;
                    case OneEventEnumAttribute.WindFire:
                        return 0.75f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.FireWater:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1.25f;
                    case OneEventEnumAttribute.Water:
                        return 0.75f;
                    case OneEventEnumAttribute.Wind:
                        return 1f;
                    case OneEventEnumAttribute.FireWater:
                        return 1f;
                    case OneEventEnumAttribute.WaterWind:
                        return 0.875f;
                    case OneEventEnumAttribute.WindFire:
                        return 1.125f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.WaterWind:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1f;
                    case OneEventEnumAttribute.Water:
                        return 1.25f;
                    case OneEventEnumAttribute.Wind:
                        return 0.75f;
                    case OneEventEnumAttribute.FireWater:
                        return 1.125f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1f;
                    case OneEventEnumAttribute.WindFire:
                        return 0.875f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.WindFire:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 0.75f;
                    case OneEventEnumAttribute.Water:
                        return 1f;
                    case OneEventEnumAttribute.Wind:
                        return 1.25f;
                    case OneEventEnumAttribute.FireWater:
                        return 0.875f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1.125f;
                    case OneEventEnumAttribute.WindFire:
                        return 1f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.FireWaterWind:
                return 1f;
            default:
                return 1f;
        }

        return 1f;
}
```

```cs:OneEventAttributeLogic.cs
public bool CanAttack(OneEventEnumAttribute attacker, OneEventEnumAttribute panelAttributle)
    {
        switch (panelAttributle)
        {
            case OneEventEnumAttribute.Fire:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.Water:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.Wind:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.FireWater:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.WaterWind:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.WindFire:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.FireWaterWind:
                return true;
            default:
                Debug.LogError("no attribute like " + attacker.ToString());
                break;
        }

        return false;
    }
}
```

#今回きれいにしたポイント
* enumの拡張メソッドとして定義する。
* 複合属性は、単属性に分けてから計算する。

コードの行数的にも半分以下になったし、なにより、読みやすくなりました。
きもちいぃ！

#今回きれいにしたコード
```cs:OneEventEnumAttribute.cs
public enum OneEventEnumAttribute
{
    Dami = 0,
    Fire = 1,
    Water = 2,
    Wind = 3,
    FireWater = 4,
    WaterWind = 5,
    WindFire = 6,
    FireWaterWind = 7,
}

public static partial class OneEventEnumAttributeExtend
{
    public static bool Contains(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        var thisAttributes = param.ConvertSingleAttributeArray();
        var targetAttributes = target.ConvertSingleAttributeArray();

        foreach (var targetAttribute in targetAttributes)
        {
            foreach (var thisAttribute in thisAttributes)
            {
                if (targetAttribute == thisAttribute)
                {
                    return true;
                }
            }
        }

        return false;
    }

    public static float GetDamageRate(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        var thisAttributes = param.ConvertSingleAttributeArray();
        var targetAttributes = target.ConvertSingleAttributeArray();

        int counta = 0;
        float sumRate = 0;
        foreach (var targetAttribute in targetAttributes)
        {
            foreach (var thisAttribute in thisAttributes)
            {
                sumRate += GetSingleDamageRate(thisAttribute, targetAttribute);
                counta++;
            }
        }

        return sumRate / counta;
    }

    private static float GetSingleDamageRate(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        if (param == target)
        {
            return 1f;
        }

        if (param.IsStrongFor(target))
        {
            return 1.5f;
        }

        return 0.5f;
    }

    private static bool IsStrongFor(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        if (param == target)
        {
            return false;
        }

        switch (param)
        {
            case OneEventEnumAttribute.Fire:
                return target == OneEventEnumAttribute.Wind;
            case OneEventEnumAttribute.Water:
                return target == OneEventEnumAttribute.Fire;
            case OneEventEnumAttribute.Wind:
                return target == OneEventEnumAttribute.Water;
            default:
                throw new ArgumentOutOfRangeException(nameof(param), param, null);
        }
    }

    private static OneEventEnumAttribute[] ConvertSingleAttributeArray(this OneEventEnumAttribute param)
    {
        switch (param)
        {
            case OneEventEnumAttribute.Dami:
                return new OneEventEnumAttribute[0];
            case OneEventEnumAttribute.Fire:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Fire};
            case OneEventEnumAttribute.Water:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Water};
            case OneEventEnumAttribute.Wind:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Wind};
            case OneEventEnumAttribute.FireWater:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water};
            case OneEventEnumAttribute.WaterWind:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind};
            case OneEventEnumAttribute.WindFire:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Wind, OneEventEnumAttribute.Fire};
            case OneEventEnumAttribute.FireWaterWind:
                return new OneEventEnumAttribute[3]
                    {OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind};
            default:
                throw new ArgumentOutOfRangeException(nameof(param), param, null);
        }
    }
}
```

#参考：修正したコードを検証するテストコード(NUnit)
```cs:OneEventEnumAttributeTest.cs
using NUnit.Framework;


public class OneEventEnumAttributeTest
{
    [Test]
    public void TestGetDamageRate()
    {
        OneEventEnumAttribute[] attackerAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };
        OneEventEnumAttribute[] targetAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };

        foreach (var attackerAttribute in attackerAttributes)
        {
            foreach (var targetAttribute in targetAttributes)
            {
                var expectedResult = new OneEventAttributeDamageLogic().Caluculate(attackerAttribute, targetAttribute);
                var damageRate = attackerAttribute.GetDamageRate(targetAttribute);
                Assert.AreEqual(expectedResult, damageRate, "from:" + attackerAttribute + " to:" + targetAttribute);
            }
        }
    }

    [Test]
    public void TestContains()
    {
        OneEventEnumAttribute[] attackerAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };
        OneEventEnumAttribute[] targetAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };

        foreach (var attackerAttribute in attackerAttributes)
        {
            foreach (var targetAttribute in targetAttributes)
            {
                var expectedResult = new OneEventAttributeLogic().CanAttack(attackerAttribute, targetAttribute);
                var nowResult = attackerAttribute.Contains(targetAttribute);
                Assert.AreEqual(expectedResult, nowResult, "from:" + attackerAttribute + " to:" + targetAttribute);
            }
        }
    }
}
```
#おわりに
[開発者のツイッターはこちら](https://twitter.com/Gong_nyaa)
プログラマさん。ゲーム開発者さん。仲良くしたいです。

[英語学習ゲーム　英語物語](https://eigomonogatari.com/)で遊んでみてね♪

![713ikWQGVJL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130870/804c273c-438f-e598-e57d-741a81b110d8.png)