com.android.applicationとcom.android.libraryを取り違えると.BuildTypeAttr 'debug' and found compatible value 'debug'.で死ぬ

またやってしまった…

Androidでライブラリのモジュールを作るにはbuild.gradleで頭の

apply plugin: 'com.android.application'

apply plugin: 'com.android.library'

に変えればいいのだが、

適当にプラグインをコピペするとapplicationに戻ったりしてこんな感じでビルドが失敗する

Error:Unable to resolve dependency for ':fehbs@debug/compileClasspath': Could not resolve project :reposroom.
<a href="openFile:C:/ols/gdx/fehs/fehbs/build.gradle">Open File</a><br><a href="Unable to resolve dependency for &#39;:fehbs@debug/compileClasspath&#39;: Could not resolve project :reposroom.">Show Details</a>

 

Show Detailsの中身:

Unable to resolve dependency for ':fehbs@debug/compileClasspath': Could not resolve project :reposroom.

Could not resolve project :reposroom.
Required by:
project :fehbs
> Unable to find a matching configuration of project :reposroom:
- Configuration 'debugApiElements':
- Required com.android.build.api.attributes.BuildTypeAttr 'debug' and found compatible value 'debug'.

 

今日はもう寝よう…

Kotlinで委譲:enumに委譲することでenumの機能を拡張する

今日も忘れないうちにQiitaへ投稿した記事の転載。いいねは一個もつかなかったな…みんな値オブジェクト嫌いなのかなあ。便利なんだけどなあ。

アカンならアカンで罵倒くらい欲しいな…

 

Kotlinでは実装を他のクラスへ委譲(Class Delegation)することができます。
これは大変便利です。が、コードを委譲先へ共通化するとかクラスの継承を避けるために使われることが多いのではないでしょうか?
この記事では目先を変えて、委譲を使ってEnumの機能を拡張してみます。

※この記事は[ファイアーエムブレムヒーローズの戦闘結果計算ツールをKotlinでDDD的に作ってみた](https://qiita.com/turanukimaru/items/4176da313f370b0007f9)の補足でもあります。

Enum

Kotlinに限らずJavaでもEnumは大変に便利です。
初期化したりメソッドを追加したりできます。

```
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
```

ですが、初期化できるだけなので新しいパラメータを与えることはできませんし、新しい値のEnumを作ることもできません。よって「種類は同じだが程度が違うEnum」は作れません。ですが、ゲームでは「特定のスキルをある程度のレベルで持つ」事がよくあります。そこで、(若干安全性に欠けますが)柔軟なEnumを作ってみます。

```
enum class Skill(val level: Int) {
//鬼神の一撃(level)の一つで済ませたい
鬼神の一撃_1(1),
鬼神の一撃_2(2),
鬼神の一撃_3(3),
}
```

公式での委譲の説明

公式では"指定されたオブジェクトへの public メソッドのすべてを委譲することができます。"として説明しています。

```
//ほぼ公式のコード
interface Base {
fun print()
}

class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

class EnumTest {
@Test
fun mainTest() {
val b = BaseImpl(10)
Derived(b).print() // 出力:10
}
}
```


Enumにする

BaseImplをEnumにするとこうなります。

```
interface Base2 {
val x: Int get() = 0
fun printX() {
print(x)
}
}

enum class BaseImpl2(override val x: Int) : Base2 {
BaseImpl2A(10);
}

class Derived2(b: Base2) : Base2 by b

class EnumTest2 {
@Test
fun mainTest() {
val b = BaseImpl2.BaseImpl2A
b.printX() // 出力:10
Derived2(b).printX() // 出力:10
}
}
```
無事Enumに委譲されました。
ここで、委譲元であるDerived2でプロパティをOverrideして任意の数字を出力させるようにします。

```
//期待通りに動作しないコード
interface Base2 {
val x: Int get() = 0
fun printX() {
print(x)
}
}

enum class BaseImpl2(override val x: Int) : Base2 {
BaseImpl2A(10);
}

class Derived2(b: Base2,override val x: Int) : Base2 by b

class Derived2B(b: Base2,override val x: Int) : Base2

class EnumTest2 {
@Test
fun mainTest() {
val b = BaseImpl2.BaseImpl2A
b.printX() // 出力:10
Derived2(b,30).printX() // 出力:30ではなく10
Derived2B(b,30).printX() // 出力:30
}
}


```
Derived2のプロパティが無視されてしまいました。そもそも継承で作ったDerived2Bと挙動が異なります。
良く継承の代わりに委譲を使え、と言いますが継承と委譲は完全に別のものです。委譲先はあくまで別のオブジェクトなので委譲元と委譲先のプロパティは異なります。ついでに言うと、委譲ではTemplate Methodパターンとかも書き方が変わります。

そこで、Interfaceの関数にデフォルト引数としてプロパティを追加します。

```
interface Base3 {
val x: Int get() = 0
fun printX(i:Int = x) {
print(i)
}
}

enum class BaseImpl3(override val x: Int) : Base3 {
BaseImpleA(10);
}

class Derived3(b: Base3,override val x: Int) : Base3 by b

class EnumTest3 {
@Test
fun mainTest() {
val b = BaseImpl3.BaseImpl3A
b.printX() // 出力:10
Derived3(b,30).printX() // 出力:30
}
}
```
Interfaceの関数はpublicなので委譲経路の一番外側、つまりDerived3のプロパティを参照します。これにより、期待通り後から与えられたパラメータを使う事が出来ました!

冒頭に書いたアプリでは実際にこんな感じでスキルごとに能力、つまりメソッドを与え、そこにレベルを後から追加しています。

```
interface Base4 {
val x: Int get() = 0
fun printX(i: Int = x) {
print(i)
}
}

enum class BaseImpl4(override val x: Int) : Base4 {
鬼神の一撃(10) {
override fun printX(i: Int) {
print("攻撃+${i * 2}")
}
},
飛燕の一撃(10) {
override fun printX(i: Int) {
print("速さ+${i * 2}")
}
},
}

class Derived4(b: Base4, override val x: Int) : Base4 by b

class EnumTest4 {
@Test
fun mainTest() {
val b = BaseImpl4.鬼神の一撃
Derived4(b, 3).printX() // 出力:攻撃+6
Derived4(BaseImpl4.飛燕の一撃, 3).printX() // 出力:速さ+6
}
}
```
これでとても柔軟なEnumになりました。ただしDerived4のequalsはenumとレベルも見るように書き直す必要があります。とはいえ、このDerived4は不変であるEnumと不変であるIntのxがともに等しければ同じであり、良く正確であるか問題となるequalsも簡単に書くことができます。

```
class Derived5(val b: Base5, override val x: Int) : Base5 by b {
override fun equals(other: Any?): Boolean = other is Derived5 && other.b == this.b && other.x == this.x
}
```

Enumの嬉しいところ

さて、このEnumですが何が嬉しいのでしょうか?実を言うとEnumであること自体が嬉しかったりします。具体的に言えば既に生成されているのでクラスに言及する必要がありません。

```
class EnumTes5 {
@Test
fun mainTest() {
val b = BaseImpl5.valueOf("鬼神の一撃")
Derived5(b, 3).printX() // 出力:攻撃+6
Derived5(BaseImpl5.valueOf("飛燕の一撃"), 3).printX() // 出力:速さ+6
println(Derived5(b, 3) == Derived5(BaseImpl5.valueOf("鬼神の一撃"), 3))//true
print(Derived5(b, 1) == Derived5(BaseImpl5.valueOf("鬼神の一撃"), 3))//false
print(Derived5(b, 1) == Derived5(BaseImpl5.valueOf("飛燕の一撃"), 3))//false
}
}
```
選択したスキルを取得したりする場合、スキルを生成する必要があります。つまり、ロジック内であれファクトリに隔離するであれ、どこかにクラス名を書かなければなりません。
どこかにクラス名を書くという事は、アップデート時にスキルが増えたりしたらそこに書き足す必要があるという事です。
ですがEnumならvalueOf()だけでOKです。スキルが増えたら単にEnumを増やせば済みます。スキルのリストもわざわざ作る必要はなく、BaseImpl5.values()で得たリストをそのまま使っても良し、職業によってフィルタしても良しとすこぶる使い勝手がよくなります。

冒頭で紹介したアプリでは、全てのスキルをEnumにして画面で選択するときやリポジトリに保存するときは単にnameに変換し、スキルに戻すときはvalueOf()で戻しています。変換ロジックはミスが入りやすいところであると同時にドメインロジックが漏れやすいところでもあるのでそれを防ぐことができるのは大きなメリットです。

 

まとめ

委譲は便利
Enumも便利
委譲とEnumを組み合わせるととても便利
ただし委譲と継承は別の物なので同じように使ってはいけない
Enumの安全性が一部失われるので気を付けて使う必要がある

ファイアーエムブレムヒーローズの戦闘結果計算ツールをKotlinでDDD的に作ってみた

個人開発 Advent Calendar 2017 の三日目の記事からアプリの説明を転載。自分で書いた奴だから良いよね?

qiita.com

この記事は筆者が初めて作ったAndroidアプリの設計を解説します。
何分初めてなものなので問題があったら指摘をいただければ記事(とアプリ)を修正します。
ソースは https://github.com/turanukimaru/fehs に公開していますがリファクタリング途中なので汚いですしGitHubも初めてなので正直よくわかっていません。

概要


このアプリはゲームの戦闘結果計算ツールです。
https://fire-emblem-heroes.com/ja/
任天堂の出しているファイアーエムブレムヒーローズというゲームがあります。
このゲームは将棋盤上の画面で駒を動かすSLGの一種なのですが、
戦闘に乱数が絡まないので計算結果は一定である
という特徴があります。
駒同士の計算結果を事前に知ることができると便利なので計算してくれるツールを作ります。

#DDD(ドメイン駆動設計)で作る
DDDが良いと聞きますが具体的なコードはあまり見たことが無いし業務で設計する機会もあまりないのでこの機会にDDDっぽく作ります。上記の「戦闘に乱数が絡まないので計算結果は一定である」および戦闘中に人の手が絡まないというのはモデルの例として最適なのもポイントです。

##DDDとしての構造
DDDの構造についてはオニオンアーキテクチャなどが知られていると思います。
それを実現するためにモデルをモジュールに分割し、強制的に他のコードへ依存できなくします。
![modelmodules.png](https://qiita-image-store.s3.amazonaws.com/0/151783/45fb3081-11ed-dbf4-3f3e-68b8b276f786.png)

android/core/desktop はLibGDXで将棋盤上の画面を作成するためのものなので今回は説明しませんというかまだできてません。
今回は
-fehbs:シミュレータアプリ。android依存部
-fehsbattlemodel:戦闘ロジックが格納されるドメインモデル部
-repos:戦闘するキャラクターを編集して保存するためのリポジトリ
の三つに大きく分けます。
依存関係は android依存部->リポジトリ->戦闘ロジック への一方通行です。

 

Android依存部


画面の説明はしません(良くわかってません)。
ただしRepositoryにRealmを使い、RealmはAndroidに依存しているためここで準備します。準備と言っても初期化してリポジトリに直接インジェクションするだけですが。
ここでArmedHeroRepositoryとRealmArmedHeroContentはともにシングルトンのオブジェクトです。
通常AndroidでDBをどう扱うのか知りませんが、オブジェクトにしておくとどのアクティビティからも同じデータソースとして使えるので安心感がありますし、Androidでない環境で動かすときにはRealmの代わりになるものを突っ込めばいいだけになります。~~というかゲームで遊んでる最中に計算ツールを起動すると画面が隠れるので最初からデスクトップかWebアプリとして作るべきでしたね。~~

```
class BattleSimApp : Application() {
    override fun onCreate() {
    super.onCreate()
    Realm.init(this)
    val realmConfig =      RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build()
    Realm.setDefaultConfiguration(realmConfig)
    ArmedHeroRepository.repo = RealmArmedHeroContent
    }
}
```

リポジトリ


リポジトリはRealmのサンプルそのままです。リポジトリ内でRealmオブジェクトを作成し、モデルのオブジェクトに変換して外へ出します。
Realmのライブオブジェクトを完全に捨てることになりますが、ライブオブジェクトは別スレッドから参照するだけで安全性のために死んでしまうのでActivityをまたいだ時などに信用できません。Activityをまたいだ時に死ぬという事はサブドメイン、例えばシミュレータではなく実際にゲームを作ったときに死ぬという事なのでもう捨ててモデルのオブジェクトに一本化するべきでしょう。

```
    object RealmArmedHeroContent : ModelObjectRepository<ArmedHero>() {

    /** realmのkotlin用ハンドラ */
    private var realm: Realm by Delegates.notNull()

    /** 初期化ブロック。テーブル変更時などはここでマイグレーションすることになる*/
    init {
    // Open the realm for the UI thread.
    realm = Realm.getDefaultInstance()
    }

    fun allItems(): List<ArmedHero> {
        return realm.where(RealmArmedHero::class.java).findAll().map { e -> e.toModelObject() }
}
```
なおリポジトリでは基準値のキャラクターと編集したキャラクターを別に扱っています。が出てくるときは同じです。
リポジトリにRealmへのアクセスrepoを入れた場合、repoはnullableなので直接触るとコードが!!だらけになり汚いのですがリポジトリに閉じ込めると!!もリポジトリ内に閉じ込められていい感じです。reposモジュールへ提供するインタフェースは適当です。どうせ必要になったら増やすものですし。

```
object ArmedHeroRepository {
var repo: ModelObjectRepository<ArmedHero>? = null

fun getById(id: String): ArmedHero? = if (StandardBaseHero.containsKey(id)) ArmedHero(StandardBaseHero.get(id)!!) else repo!!.getById(id)

fun allItems(includeDb: Boolean = false): List<ArmedHero> = if (includeDb) StandardBaseHero.allItems().map { e -> ArmedHero(e) }.union(repo!!.allItems()).toList() else StandardBaseHero.allItems().map { e -> ArmedHero(e) }

fun isStandardBattleClass(id: String): Boolean = StandardBaseHero.containsKey(id)

fun createItem(battleHero: ArmedHero) = repo!!.createOrUpdate(battleHero)
}

interface ModelObjectRepository<T> {
fun createOrUpdate(item: T): T
fun deleteById(id: String): Int
fun allItems(): List<T>
fun getById(id: String): T?
}
```


ドメインモデル


ここから本題のドメインモデルの話ですがその前にアクセスする画面側のコードです。
switchは攻撃する側/される側の切り替え、battleUnitは計算対象のキャラクター、filteredUnitsは対戦相手のリストです。
キャラクターから戦闘ユニットを作成してHP初期化・バフや地形効果を計算し、全ユニットと戦闘した結果をモデルから得ます。外から見えるのはキャラクターの能力値とそのステータスであるBattleUnitと戦闘結果だけです。なおバフ計算はActivity内にあり画面入力した情報をセットしていますが結構長いので拡張関数にして見やすくしています。

```
val resultList = filteredUnits.map({ e ->
if (switch) BattleUnit(e, e.maxHp).buff().fightAndAfterEffect(battleUnit)
else battleUnit.fightAndAfterEffect(BattleUnit(e, e.maxHp).buff())
})
```

クラス

後で後悔する例


武器は武器クラスを作りたくなりますが我慢します。

```
//後で後悔する例
class Weapon(val name:String, val atk:Int){

    fun attack(heroAtk:Int, target:BattleUnit){target.damage(heroAtk + atk)}

}

class Falchion (val name:String="ファルシオン", val atk:Int=16):Weapon{

    fun attack(heroAtk:Int, target:BattleUnit){
    if(target.isDragon){target.specialDamage(heroAtk + atk)
    }else...

}

val SilverSword = Weapon("銀の剣",15)
val falchion = Falchion()
```
この方法だと武器ごとに個性的な特徴をつけられますが、攻撃に関係する要素が増えたときに引数が増えます。

```
//後悔した例
class Weapon(val name:String, val atk:Int){

    fun attack(heroAtk:Int, target:BattleUnit, lastAttacker:BattleUnit){target.damage(heroAtk + atk, lastAttacker)}

}
```
「連続攻撃を軽減するスキル」なるものが実際に追加されました。攻撃を受ける側は最後に攻撃した側がどちらかも受け取り、スキルがあれば軽減します。もちろんこれだけなら置換するだけでなんとかなりますが、問題は影響が全てのクラスに及ぶことと、それが予想できない事です。

 

全てのスキルを「キャラクターを強化するもの」と抽象化する

-BaseHero:マルスとかシーダとかのキャラクター.基準能力値と初期スキルを持つ。リポジトリ管理ではない。
-ArmedHero:個体値や装備の変更されたキャラクター。リポジトリに保存される。
-BattleUnit:HPやバフなどの戦闘時に変動するステータスと戦闘用ロジックを持つ。モデルのファサードでもある。
-Skill:スキルのインタフェース。起動タイミングでの(何もしない)動作とスキル効果関数を実装している。
-Skillを継承したEnum群:武器・奥義・A~Cのスキル・聖印・その後追加される諸々

として、全てのスキルを「同じもの」として扱います。これにより、何が追加されても新スキルが追加されたこととして処理することができるようになります。

 

戦闘ロジック

全てのスキルはキャラクターを強化するものなので「大量の起動タイミングを持ち、合致する起動条件で引数として渡されるキャラクターを強化する」オブジェクトとします。

```
interface Skill {
val level: Int get() = 0
val type: SkillType get() = SkillType.NONE

/** 戦闘時の効果 */
fun bothEffect(battleUnit: BattleUnit, lv: Int = level): BattleUnit {
if (type == SkillType.BOW && battleUnit.enemy!!.armedHero.baseHero.moveType == MoveType.FLIER) battleUnit.effectiveAgainst = EffectiveAgainst.FLIER
return battleUnit
}

/** 攻撃時の効果 */
fun attackEffect(battleUnit: BattleUnit, lv: Int = level): BattleUnit {return battleUnit}

/** 反撃時の効果 */
fun counterEffect(battleUnit: BattleUnit, lv: Int = level): BattleUnit {return battleUnit}

```
KotlinはSkillに実装を書けるので、デフォルトの計算や空実装を全部書きます。プロパティも書けるので、全て空文字や0にします。また、「能力値上昇量など関数内で使うものは全て外側で定義して関数の引数にする」ことを心がけます。
ただしデフォルトの計算はsuper()を呼び忘れるバグを生みますので慎重に使いましょう。実際に特殊効果のある弓に特効が発生しないバグを作り込みました。たぶん私だけじゃないです。ゲーム本編であるGBAのFEでもミュルグレに飛行特効が無いという仕様(?)があります。

 

全てのスキルをEnumにする


スキルは使用者の戦闘力をあげるもの、ということは武器含めスキルは状態を持たないとも言えます。よって全てのスキルをEnumとして書きます。
この際に必要なプロパティを上書きします。また、武器の特殊効果を武器ごとに書きます。実を言うととても面倒なので表にしたりできないかな?とか思いましたがスキルはそれぞれが違いすぎててまったく統一できませんでした。表を書くより覚悟して書くのが早いです。表が必要になったらオブジェクトから表を作るのが早いというのもあります。

```
enum class Weapon(override val jp: String, override val type: Skill.SkillType, override val level: Int = 0, override val preSkill: Skill = Skill.NONE, val refineSkillType: RefineSkill.RefineType = RefineSkill.RefineType.NONE) : Skill {

    IronSword("鉄の剣", Skill.SkillType.SWORD, 6),
    SteelSword("鋼の剣", Skill.SkillType.SWORD, 8, IronSword),

    BraveSword("勇者の剣", Skill.SkillType.SWORD, 5, SteelSword) {
        override fun equip(armedHero: ArmedHero, lv: Int): ArmedHero =    equipBrave(armedHero, lv)
        override fun attackEffect(battleUnit: BattleUnit, lv: Int): BattleUnit = doubleAttack(battleUnit)
},

Falchion("ファルシオン", Skill.SkillType.SWORD, 16, SilverSword) {
    override fun bothEffect(battleUnit: BattleUnit, lv: Int): BattleUnit =     effectiveAgainst(WeaponType.DRAGON, battleUnit)
},

```
パッシブももちろんEnumにします。なお、スキルの効果は全てSkillインタフェースに書くと武器とスキルで同じ効果を使えてよいです。とはいっても二度とないような武器効果などを共通化する必要もないでしょう。一回は直接書いて、2回目になったらそれをSkill側に移動してEnum側はそれを呼び出すようにするのが良いかと思われます。

```
enum class SkillA(override val jp: String, override val type: Skill.SkillType, override val level: Int = 0, override val preSkill: Skill = Skill.Companion.NONE, override val maxLevel: Int = 3) : Skill {

Hp("HP", Skill.SkillType.A) {
override fun equip(armedHero: ArmedHero, lv: Int): ArmedHero = equipHp(armedHero, lv+2)
},

Furry("獅子奮迅", Skill.SkillType.A) {
override fun equip(armedHero: ArmedHero, lv: Int): ArmedHero = furry(armedHero, lv)
override fun bothEffect(battleUnit: BattleUnit, lv: Int): BattleUnit = attackHpLoss(battleUnit, lv * 2)
},
```
武器もそうですがEnumとしてでなくクラスで普通に書いてもいいのですがどうせ生成するのと「スキルはアイデンティティと状態を持つか?」を考えたときに恐らくアイデンティティも状態もない、ということになるのでアイデンティティと状態のないものはシングルトンにするのが形而上学的に正しい態度です。武器が状態を持つようになったら…例えば、戦闘中に誰かの武器を強化したうえで他の人に渡せる、みたいなゲームになったら武器はアイデンティティと状態を持ちますのでそのときはシングルトンでない普通のオブジェクトにするべきでしょう。

 

Enumに委譲するレベルを変えられるクラスを作る


スキルはレベルを持ちますがEnumは状態を持てません。よって真面目に実装するとスキルLevel1,スキルLevel2,スキル...みたいなことになってキリがありません。良くあるのはPair(スキル,レベル)としてもつものですがKotlinなので委譲を使います

```
class LappedSkill(val skill: Skill, override val level: Int) : Skill by skill {
override fun equals(other:Any?)=other is LappedSkill && other.skill == this.skill && other.level == this.level
}

enum class SkillA(/* 省略 */): Skill {
fun lv(lv: Int) = if (level == lv) this else LappedSkill(this, lv)
}

val furry3 = Furry.lv(3)
```

ここでわざわざインタフェースにプロパティを書いた甲斐が出てきました。Skillの空実装とEnumのスキル効果に委譲されるためこれ以上何もする必要がありません。このクラスから作られるオブジェクトは完全にスキルとして振舞いますし、任意のレベルを与えることができます。Kotlinでは==はequalsなので===を使われない限りはenumと区別もつきません。そして不変のままです。lv()を呼び出す前のEnumはレベルのないスキルかレベル0のスキルか?という問題は発生しますが今回はレベル0のスキルが無いので関係ないでしょう。

これで全てのスキル効果が再現されました!フィールド上で効果があるスキルは名前を書いただけですが。
最後に念のためダメージ計算式もスキルとして分離します。

```
fun damage(battleUnit: BattleUnit, target: BattleUnit, results: List<AttackResult>, skill: Skill? = null): Pair<Int, Skill?> {
    val damage = battleUnit.halfByStaff(target.preventByDefResTerrain(battleUnit.colorAttack(), battleUnit.armedHero.weapon.type))
    return Pair(if (damage > 0) damage else 0, skill)
}

enum class Special(/* 略 */) : Skill {

    Glimmer("凶星", Skill.SkillType.SPECIAL_A, 2) {
        override fun damage(battleUnit: BattleUnit, target: BattleUnit, results: List<AttackResult>, skill: Skill?): Pair<Int, Skill?> {
        return Pair(super.damage(battleUnit, target, results, this).first * 15 / 10, this)
    }
},

```
奥義のダメージ計算式が怪しいので、変な奥義が追加されたら計算式をそっくり取り換えられるようにします。このダメージを防御側では奥義で軽減したりなんやかんやしますがそれは防御側や防御側の奥義発動場所に書きます。

これでダメージの計算ができるようになりましたので、あとは実際に攻撃してダメージを計算するだけです。
書き忘れましたがキャラクターの能力を強化するだけではなく攻撃順を変更するスキルもありますが同じように単に攻撃順を入れ替えてます。
この際、ダメージの累計を使うかユニットのHPを減らしていくかは難しい問題ですがHPに依存する奥義もあるのでHPでいいでしょう。そのままHPを減らすのではなく遂次コピーしてそのコピーのHPを減らすようにし、経過を含めて全ての攻撃結果のリストを返すようにすると、全ての計算を適用した後にアニメーション表示したくなった時に役に立ちます。
なお、スキルを計算するときは単に自分を引数にして所持スキルを全部呼ぶだけです。

```
data class ArmedHero(
    val baseHero: BaseHero,
    var weapon: Skill = Skill.NONE,
    var aSkill: Skill = Skill.NONE,
    /* 略 */
){
    val skills get() = listOfNotNull(weapon, assist, special, aSkill, bSkill, cSkill, seal)

    fun bothEffect(battleUnit: BattleUnit): BattleUnit {
        return skills.fold(battleUnit, { b, skill -> skill.bothEffect(b) })
    }

    fun attackEffect(battleUnit: BattleUnit): BattleUnit {
        return skills.fold(battleUnit, { b, skill -> skill.attackEffect(b) })
    }

```

 

大型アップデートきたる

プレイヤーならわかるでしょうがリリースから半年たった時点でインフレブレムが始まってしまいました。シナリオ第二部の開始に合わせて待ちに待った旧キャラのテコ入れとなる大型アップデートです。
第二部と新キャラはこのツールに関係ないので旧キャラのテコ入れとなる武器錬成だけ対応します。

 

武器錬成の効果

一部の武器を(消費アイテムを使って)強化することができる。
強化の種類は武器によって4または特殊能力が追加できるならば5種類である。
化内容は
-近武器共通:HP+3,Atk+1
-遠武器共通:特になし
-近武器選択:特殊能力追加,HP+2&ATK+2,HP+2&SPD+3,HP+2&DEF+4,HP+2&RES+4,から一種類
-遠武器選択:特殊能力追加,HP+2&ATK+1,HP+2&SPD+2,HP+2&DEF+3,HP+2&RES+3,から一種類
-銀装備など一部の武器はAtk+1
-暗器など一部の武器はAtk+2/3/4と武器によって異なる強化
-季節ものなど一部の武器は特殊効果が変化する
-マムクートのブレスは射程武器の敵に対してDEFとRESの低いほうを適用するようになる
-風神弓は特殊能力を選んだ時のみ新しい特殊能力を得るとともに今までの能力がなくなる
※錬成していない武器の特殊効果は現在のまま

( ^ω^)・・・
上四つは共通ルールとして簡単に実装できそうに思えますが残りが問題です。
一部であるため一律に適用できないうえ、現在の効果を変更できません。そこで、新武器として追加したくなりますがこれは下手に全て新武器とすると今後強化対象が追加されるたびに新武器が追加されることになり手間です。
そこで「錬成をスキルの一種にする」事にします。

 

新スキルを追加する

 

```
enum class RefineSkill(val us: String = "", override val jp: String, val hp: Int, val atk: Int, val spd: Int, val def: Int, val res: Int, val refineSkillType: RefineSkill.RefineType = RefineSkill.RefineType.NONE, override val preSkill: Skill = Skill.NONE, override val level: Int = 0, override val type: Skill.SkillType = Skill.SkillType.REFINERY) : Skill {
//基本ルール
    Range1Atk("Atk(melee)", "攻撃(近)", 5, 2, 0, 0, 0, RefineType.Range1),
    Range1Spd("Spd(melee)", "速さ(近)", 5, 0, 3, 0, 0, RefineType.Range1),
    Range1Def("Def(melee)", "守備(近)", 5, 0, 0, 4, 0, RefineType.Range1),
    Range1Res("Res(melee)", "魔防(近)", 5, 0, 0, 0, 4, RefineType.Range1),


    Range2Atk("Atk(Ranged)", "攻撃(遠)", 2, 1, 0, 0, 0, RefineType.Range2),
    Range2Spd("Spd(Ranged)", "速さ(遠)", 2, 0, 2, 0, 0, RefineType.Range2),
    Range2Def("Def(Ranged)", "守備(遠)", 2, 0, 0, 3, 0, RefineType.Range2),
    Range2Res("Res(Ranged)", "魔防(遠)", 2, 0, 0, 0, 3, RefineType.Range2),

    //武器特有能力
    WrathfulStaff("WrathfulStaff", "神罰", 0, 0, 0, 0, 0, RefineType.Staff){
        override fun bothEffect(battleUnit: BattleUnit, lv: Int): BattleUnit = wrathfulStaff(battleUnit, 3)
    },

    SolKatti("BrashAssault", "差し違え", 3, 0, 0, 0, 0, RefineType.DependWeapon,     Weapon.SolKatti) {
        override fun attackEffect(battleUnit: BattleUnit, lv: Int): BattleUnit =     brashAssault(battleUnit, 75)
        override fun attackPlan(fightPlan: FightPlan, lv: Int): FightPlan = desperation(fightPlan, lv)
},

    //武器自体を置き換えるもの
    SilverSword2("", "", 0, 1, 0, 0, 0, RefineType.ReplaceWeapon, Weapon.SilverSword2, 15, Skill.SkillType.SWORD),

    CarrotLance2("", "", 0, 1, 0, 0, 0, RefineType.ReplaceWeapon, Weapon.CarrotLance2, 13, Skill.SkillType.LANCE) {
        override fun bothEffect(battleUnit: BattleUnit, lv: Int): BattleUnit = attackHeal(battleUnit, 4)
    },
}

enum class Weapon(/* 略 */){
    //錬成のタイプのみ旧武器に追加する
    SilverSword2("銀の剣+", Skill.SkillType.SWORD, 15, SilverSword,     RefineSkill.RefineType.Range1),

```

錬成を使う側も残念ながら修正が必要になります。といってもスキルが増えるのと、錬成時に武器が置き換わるものであるならば置き換えるだけですが。なお、weaponを直接置き換えたくなりますが、直接置き換えると「錬成武器を持った人がさらに別の練成しようとしたとき」のことを考える必要が出てきてしまいます。錬成前の武器と錬成後に置き換えた武器は同じに扱うことができないから別に扱います。別のものは別に扱うのも設計のうちです。

```
data class ArmedHero(/* 略 */
    var baseWeapon: Skill = Skill.NONE,
    var refinedWeapon: Skill = Skill.NONE,
){
    val weapon get()= if (refinedWeapon != Skill.NONE)     RefineSkill.valueOfWeapon(baseWeapon) ?: baseWeapon else baseWeapon

    val skills get() = listOfNotNull(weapon, refinedWeapon, assist, special, aSkill, bSkill, cSkill, seal)

```
なんということでしょう!対処に困る大型アップデートでしたが「新スキル追加」「錬成できる武器に錬成できるマーク」「スキルを使う側に数行修正」だけで済んでしまいました!
リポジトリは1カラム増えただけ、登録画面もそれを表示/選択できるようにしただけ、シミュレータ実行画面に至っては修正なしです!

~~実際はマムクートのブレスとかうっかり実装し忘れていた神罰を実装しなおすとか色々ありましたが。あと簡単に書いてますが実際には一番シンプルな方法を模索して徹夜しています。~~

なお、私が作ったわけではありませんが同じようなというか一部ソースをパク…参考にしたJavascriptのシミュレータがあります。[https://github.com/Andu2/FEH-Mass-Simulator] こちらはほぼ手続きで書かれているのでソースを見比べると設計思想の違いが良くわかると思います。なお、アップデート対応はとても早かったので継承だの移譲だのよりif連打のほうが早いのかもしれません。ついでに言うと私の書いたコードはスキルが凄い回数呼び出されるのでかなり遅いです。

 

まとめ

DDD的に作るとモデルが外に漏れません。正しくモデルを作ればアップデート時の対応がシンプルになります(簡単とは言ってない)。Javaでは色々大変なこともKotlinでは自然にできます。ていうか今無職ですがKotlinの仕事したいですね。

DDD的にはここから更にドメインを考えていくことになります。具体的には自分で操作できるゲーム画面も作った場合は同じドメインサブドメインです。逆にゲームを作る側から考えると、ゲームがドメインでありこういったシミュレータはサブドメインになります。ゲームエンジンアーキテクチャにも記載がありましたが、ゲームを作る最中にゲームの戦闘モデルを直接シミュレートできれば開発が捗るでしょうし、キャラクターの8割をワンパンするような壊れキャラを作ってしまうことも減るでしょう。

画面をドメインとした場合、画面を将棋盤やボードゲームの舞台として考えた場合にはボードゲームドメイン言語が使えます。実際にどうなるかの話もしたいところですが私はボードゲーム作家どころかどんなゲーム屋でもないので説得力のある話ができないのが残念です。

明日…はあいて明後日は@dala00氏です。

 

おまけ

ゲームのツールなんだから解析されてる資料を基にすれば楽勝、仕様ミスなんてない。と思ってた時期が私にもありました。
実装されてるけどまだ使えないエフラムの武器錬成ですが

日本語:自分のHPが90%以上で、自分から攻撃した時、絶対追撃
英語:Unit makes a guaranteed follow-up attack when HP ≥ 90% and attacking a foe that can counter.

おわかりいただけるだろうか?

運営には先日メールしましたが日本語表記だと弓魔導士ワンパン祭りになるし多分実際の挙動である英語表記にすると弱体化となります。~~こりゃホンマ詫び石もんやでえ~~

基本的な構成

UI(android依存部)

f:id:turanukimaru:20171127000256p:plain

UIは実質画面のみ。シミュレータ画面とキャラ登録画面。登録画面は後で。

シミュレータ画面では選択したキャラと他のキャラをRepositoryに出してもらって戦いを指示するだけ。戦うロジックは全てビジネスロジック部に入っている。

f:id:turanukimaru:20171126235653p:plain

ロジック部

f:id:turanukimaru:20171127000330p:plain

キャラクターを構成する部分と戦いの経過・結果を格納する部分に分かれる。キャラクターを構成する部分は下図のように

BaseHero:ベースとなるキャラ

ArmedHero:装備を変更・個体差を持ったキャラ

BattleUnit:HPなど戦闘中に変動する部分

で構成される。BaseHeroはStandardBaseHeroに登録されていてRepository経由で取り出すが扱うのは基本的にArmedHero。装備や個体値が変更されてない相手のときのみBaseHeroの値をコピーして使う形になる。なおスキルは全てのメソッドが空実装されていて、効果を発揮しないタイミングでは空メソッドが実行され何も起きない。

f:id:turanukimaru:20171127002439p:plain

 

肝心の戦闘ロジックは以下。スキル効果は全てSkillに入っているので順番に起動するだけ。主にスキルの持ち主のステータスを変動させたり追撃の追加や制限を登録する、攻撃順の変更を行う。攻撃開始指示時に追撃が発生しない条件で追撃を取り除いて両ユニットに攻撃指示をする。

f:id:turanukimaru:20171127002449p:plain

リポジトリ

f:id:turanukimaru:20171127003029p:plain

気が変わってリポジトリも別モジュールにした。これはAndroidに依存しているがUIに依存していないため、モジュールにすることで複数アプリから共通して使うことができる。またこれによりRealmへの依存もモジュール内で完結する。なお、見てわかるようにクラスではなくシングルトンオブジェクトであり、ApplicationでビジネスロジックのRepositoryに手動インジェクションしている。これによりリポジトリが複数できてしまうことを防げる。

Realmの“ライブオブジェクト”に別スレッドからアクセスをするとデータの一貫性を維持するために落ちる。本来ならばライブオブジェクトを有効活用するべきなのだろうが私はデータストアは明示的にアクセスしたいしリポジトリという概念と相性が悪い。Hibernateも嫌いでデータストアとしてしか使っていない。

各モジュール間の関係は以上のようになっており、作って使う事はできるがそれ以上の操作ができないように隔離されている。ビルドの都合上内部のロジックから外部の画面などは見えない。こうすることでロジックは漏れなくなるというか漏らせなくなる。

 

次回は各モジュールの内部の話。とはいっても画面やリポジトリについて話すことは皆無なので戦闘ロジックの話になる。

ドメイン駆動設計(DDD)でAndroidアプリを開発する実例を作る

最近よく見るでも前からあるけれど実践するにはどうすればいいかさっぱり分からないドメイン駆動設計(DDD)でFEH Battle Simulator(以下FEHBS)を作ってみます。個人開発でないと実践するチャンスが少ないですからね。

ドメイン駆動設計 - Wikipedia

なんにでも使えるように抽象的な言葉を使っているので今ひとつピンときませんのでFEHBSのドメインに適用した例を日本語で書いてみます。

 

FEHBSはファイアーエムブレムヒーローズ任天堂。以下FEH)のシミュレータである。

FEHは将棋盤上のステージでヒーローを操作して敵を撃破するゲームである。

ヒーローが敵ヒーローを攻撃した場合、能力値やスキルによってダメージや撃破判定が発生する。これには攻撃順やヒーロー同士の距離によるスキル効果は関係するが、将棋盤上の操作とは関係が無い。

つまり、FEHBSは将棋盤上の操作とヒーロー同士の戦闘判定の二つのサブドメインに分割することができる。

 

・エンティティ:ヒーローはFEH内のオブジェクトであり(属性=能力値やスキルではなく)連続性と識別性により定義される。

・値オブジェクト:ヒーローは個体差のある能力値やスキルを持つが、スキルは誰が持っても同じ効果を持ち、LVはあるが状態として持つわけではない。誰かの獅子奮迅スキルのレベルが上がっても他の人の獅子奮迅スキルのレベルも上昇したりはしない。よってスキルは読み出し専用のオブジェクトであり、各ヒーローが共有する。

・サービス・リポジトリ・ファクトリ:FEHBSでは適宜使用するがAndroidプラットフォーム上で動作するため、ドメインに存在する「定義」とAndroidに存在する「実装」に分かれる。

 

このドメインの分析により、まずサブドメインに分割してモジュール化します。サブドメインは「将棋盤上の操作」「ヒーロー同士の戦闘」の二つです。

f:id:turanukimaru:20171123224229p:plain

android:LibGDXで動かす画面のAndroid依存部分。リポジトリ実装であるRealmの準備と戦闘画面の起動を行う。開発中。

core:LibGDXで動かす画面のAndroid非依存部分であり、将棋盤上の操作をモデル化したもの。FEH操作モデル。LibGDXに依存。

desktop:LibGDXで動かす画面のデスクトップ依存部分。詳しくはないがデスクトップJavaベースなのだろう。現在は開発スコープに入っていない。

fehbs:操作とは無関係にヒーロー同士の戦闘を計算する仕組み、を駆動するAndroidアプリ。現在GooglePlayで公開している部分のUIを担当。Realmの準備も行っている。

fehsbattlemodel:操作とは無関係にヒーロー同士の戦闘を計算する仕組み、のモデル部分。fehbsからアクセスされるが、何にも依存していない。

 

f:id:turanukimaru:20171123225150j:plain

クリーンアーキテクチャに適用した場合、

中心のEntitiesはfehsbattlemodelとcoreモジュール内のアプリケーション非依存部

UseCasesはcoreモジュール内のアプリケーション依存部

Controllersとその外側がandroidとfehbsモジュール

となります。ビルドのDependencyの都合により、Entitiesはその外側に依存することは不可能になります。これによりモデルの独立性を保ちます。

 

以降はandroid部は未完成であるために公開してるアプリであるfehbsと戦闘モデルの関係を作っていきます。PlantUML勉強してはてなブログで直接書けるようにならないと…。

Google Playにてリリース※ただし戦闘結果予測ツールのみ

Google playでリリースしました。と言っても、FEH Mass Duel Simulatorのようにユニットを指定して全ユニットとの戦闘結果を予測して出すだけですが。

 

play.google.com

 

しばらくオープンベータにしていたけど反応が無いというかオープンベータ画面へのアクセス方法もわからない。

というわけで普通にリリースして動作確認。

手元の端末でも動く…けど自分が正しい手順を踏んでいるか分からないというのはどうも落ち着かない。登録したサイトもGitHub直だし。

ソースをGitHubに公開する準備※ただしGit使うの初めて

構成かえて作り直したら2か月経ってる…だと…

とはいえ戦闘結果を出すシミュレータはできたのでそのうちにAndroidで公開する予定

どうせ公開するならソースも公開しようと思うので初めてGitHubを使ってみる。

まずはGitをインストール。設定は全部デフォルトにした。

Git - Downloading Package

 

f:id:turanukimaru:20171120233727p:plain

AndroidStudioでVCSを有効にする。SVNとか色々あるからGitを選択

 

リポジトリが自動的にできる。スクショ取り忘れたので別プロジェクトで代用

 f:id:turanukimaru:20171120234442p:plain

Gitになったけど使い方が分からない。

 

f:id:turanukimaru:20171120234048p:plain

複数のモジュールがあるときは最初は全て出ないみたいなのでbrowseから追加

f:id:turanukimaru:20171120235027p:plain

f:id:turanukimaru:20171120235152p:plain

f:id:turanukimaru:20171120235344p:plain

 

コードをチェックしてくれるんだけどライブラリが見えてないらしくてエラー吐きまくる。どうなってんの…?

f:id:turanukimaru:20171121000043p:plain

Share Project on GitHubが用意されてるので押してみる

f:id:turanukimaru:20171121000322p:plain

デフォルトはトークンだけど使い方が分からないから今回は普通にログイン

f:id:turanukimaru:20171121000512p:plain

Remote nameって何…?

f:id:turanukimaru:20171121000850p:plain

できた。

github.com

でもコミットメッセージが日本語だわ…いいのかなこれ…?

とりあえず明日別PCで取り込んでみよう。説明とかも書こう。