白嶺桃源郷のブログ

いろいろ書いていきます

MTGの能力でインターフェースを学習

今回やること。

あるクリーチャーが、攻撃しているクリーチャーをブロックできるかを判定するプログラムを書く。使用言語はTypeScriptとする。

今回の記事はある程度MTGのルールに詳しいことを前提として書くことにする。

設定する能力とクリーチャー

設定する能力は以下の通りである。

能力名 効果 備考
警戒 攻撃してもタップしない。 今回のプログラミングでは特に意味のない能力。
飛行 飛行到達を持つクリーチャーにブロックされない。
到達 飛行を持つクリーチャーをブロックできる。) 飛行に参照されるだけの能力*1
威嚇 アーティファクト・クリーチャーや自身の色を含まないクリーチャーにブロックされない。
Unblockable このクリーチャーはブロックできない。 「ブロックされない」ではない。
HighFlying このクリーチャーは飛行を持たないクリーチャーをブロックできない。 俗語。
到達は無関係。

以上の能力を持たせたクリーチャーとして以下を設定する。

クリーチャー 能力 その他重要な特性
人間 警戒
天使 飛行警戒
ドレイク 飛行HighFlying
ゾンビ Unblockable
ホラー 威嚇
デーモン 飛行威嚇
エレメンタル (なし)
ゴブリン 威嚇Unblockable
ドラゴン 飛行
蜘蛛 到達
スフィンクス 飛行 白青黒
恐竜 警戒到達 赤緑白
多相の戦士 威嚇到達 白青黒赤緑
構築物 到達 (無色) アーティファクトでもある

レッツプログラミング

今回のプログラミングでは継承を使わず、すべてインターフェースの実装のみで済ませる。

各クリーチャーと各能力をクラスとして定義する。パーマネントとして持つ特性はパーマネントの実装PermanentImplに委譲する。

interface Permanent {
    permanent : PermanentImpl;
    creature : CreatureImpl;
    abilities : Ability[];
}

class PermanentImpl {
    get name() { return this._name; }
    get cardTypes() { return this._cardTypes; }
    get colors() { return this._colors; }
    constructor(
        private _name : string,
        private _cardTypes : CardType[],
        private _colors : Color[],
    ) { }
}

interface CardType { get name() : string; }
class Artifact implements CardType { get name() { return "アーティファクト"; } }
class Creature implements CardType { get name() { return "クリーチャー"; } }

interface Color { get name() : string; }
class White implements Color { get name() { return "白"; } }
class Blue implements Color { get name() { return "青"; } }
class Black implements Color { get name() { return "黒"; } }
class Red implements Color { get name() { return "赤"; } }
class Green implements Color { get name() { return "緑"; } }

能力は、攻撃クリーチャーがブロックできるか否かに関係するCanBeBlockedと、防御クリーチャーがブロックできるか否かに関係するCanBlockを実装することができる。警戒のようにまったく関係ない能力ならば、何も実装しないこともできる。到達も何もしない能力だが、一応CanBlockを実装しておく。

また、インターフェースの判別のためにisObject isCanBeBlocked isCanBlockを用意する*2

const isObject = (arg : unknown) : boolean => { return typeof arg === typeof Object() && arg !== null; }

interface Ability {
    ability : AbilityImpl;
}
class AbilityImpl {
    get text() { return this._text; }
    constructor(
        private _text : string,
    ) { }
}
interface CanBlock {
    canBlock(blocker : Permanent, attacker : Permanent) : boolean;
}
const isCanBlock = (arg : unknown) : arg is CanBlock => {
    return isObject(arg) && typeof (arg as CanBlock).canBlock === typeof function(){};
}
interface CanBeBlocked {
    canBeBlocked(attacker : Permanent, blocker : Permanent) : boolean;
}
const isCanBeBlocked = (arg : unknown) : arg is CanBeBlocked => {
    return isObject(arg) && typeof (arg as CanBeBlocked).canBeBlocked === typeof function(){};
}

class Vigilance implements Ability {
    ability = new AbilityImpl("警戒");
}
class Flying implements Ability, CanBeBlocked {
    ability = new AbilityImpl("飛行");
    canBeBlocked(attacker: Permanent, blocker: Permanent): boolean {
        return blocker.abilities.some(a => { return a instanceof Flying || a instanceof Reach; });
    }
}
class Reach implements Ability, CanBlock {
    ability = new AbilityImpl("到達");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return true;    // 飛行を参照
    }
}
class Intimidate implements Ability, CanBeBlocked {
    ability = new AbilityImpl("威嚇");
    canBeBlocked(attacker: Permanent, blocker: Permanent): boolean {
        return blocker.permanent.cardTypes.some(e => { return e instanceof Artifact; })
            || attacker.permanent.colors.some(a => { return blocker.permanent.colors.some(b => { return a.name == b.name; }) })
    }
}
class Unblockable implements Ability, CanBlock {
    ability = new AbilityImpl("このクリーチャーはブロックできない。");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return false;
    }
}
class HighFlying implements Ability, CanBlock {
    ability = new AbilityImpl("このクリーチャーは飛行を持たないクリーチャーをブロックできない。");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return attacker.abilities.some(a => { return a instanceof Flying; });
    }
}

クリーチャーの実装CreatureImplに、攻撃クリーチャーattackerが防御クリーチャーblockerをブロックできるかを判定する関数canBeBlockedを実装する。その逆であるcanBlockについてはcanBeBlockedを使いまわす。

canBeBlocked関数については、攻撃クリーチャーが持っている能力attacker.abilitiesの中で、CanBeBlockedを実装しているものと、防御クリーチャーが持っている能力blocker.abilitiesの中で、CanBlockを実装しているものを参照する。それをTypeScriptで実現するために関数filterCastInterface isCanBeBlocked isCanBlockを定義した。

マジックの黄金律『「できない」は「できる」に勝つ』のため、能力を適用した結果1つでもブロックできなくなったらそれはブロックできないこととする。それを達成するためにevery関数を用いる。

const filterCastInterface = <From, To>(list : From[], judge : (e : From) => boolean) : (From & To)[] => {
    return list.filter(e => { return judge(e); }).map(e => { return ((e as unknown) as From & To) });
}

class CreatureImpl {
    canBeBlocked(attacker : Permanent, blocker : Permanent) : boolean {
        const attackerAbilityList = filterCastInterface<Ability, CanBeBlocked>(attacker.abilities, isCanBeBlocked);
        const blockerAbilityList = filterCastInterface<Ability, CanBlock>(blocker.abilities, isCanBlock);
        return blocker.permanent.cardTypes.includes(e => { return e instanceof Creature })
            && attackerAbilityList.every(a => { return a.canBeBlocked(attacker, blocker); })
            && blockerAbilityList.every(a => { return a.canBlock(blocker, attacker); });
    }
    canBlock(blocker : Permanent, attacker : Permanent) : boolean {
        return this.canBeBlocked(attacker, blocker);
    }
}

クリーチャーを実装し、実際にどのクリーチャーがどのクリーチャーにブロックされるかをコンソール上に表示してみる。

class Human implements Permanent {
    permanent = new PermanentImpl("人間", [ new Creature() ], [ new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Vigilance() ];
}
class Angel implements Permanent {
    permanent = new PermanentImpl("天使", [ new Creature() ], [ new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new Vigilance() ];
}
class Drake implements Permanent {
    permanent = new PermanentImpl("ドレイク", [ new Creature() ], [ new Blue() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new HighFlying() ];
}
class Zombie implements Permanent {
    permanent = new PermanentImpl("ゾンビ", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Unblockable() ];
}
class Horror implements Permanent {
    permanent = new PermanentImpl("ホラー", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate() ];
}
class Demon implements Permanent {
    permanent = new PermanentImpl("デーモン", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new Intimidate() ];
}
class Elemental implements Permanent {
    permanent = new PermanentImpl("エレメンタル", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ ];
}
class Goblin implements Permanent {
    permanent = new PermanentImpl("ゴブリン", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate(), new Unblockable() ];
}
class Dragon implements Permanent {
    permanent = new PermanentImpl("ドラゴン", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying() ];
}
class Spider implements Permanent {
    permanent = new PermanentImpl("蜘蛛", [ new Creature() ], [ new Green() ]);
    creature = new CreatureImpl();
    abilities = [ new Reach() ];
}
class Sphinx implements Permanent {
    permanent = new PermanentImpl("スフィンクス", [ new Creature() ], [ new White(), new Blue(), new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying() ];
}
class Dinosaur implements Permanent {
    permanent = new PermanentImpl("恐竜", [ new Creature() ], [ new Red(), new Green(), new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Vigilance(), new Reach() ];
}
class Shapeshifter implements Permanent {
    permanent = new PermanentImpl("多相の戦士", [ new Creature() ], [ new White(), new Blue(), new Black(), new Red(), new Green() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate(), new Reach() ];
}
class Construct implements Permanent {
    permanent = new PermanentImpl("構築物", [ new Artifact(), new Creature() ], [ ]);
    creature = new CreatureImpl();
    abilities = [ new Reach() ];
}

const getCreatures = () => { return [ new Human(), new Angel(), new Drake(), new Zombie(), new Horror(), new Demon(),
    new Elemental(), new Goblin(), new Dragon(), new Spider(), new Sphinx(), new Dinosaur(), new Shapeshifter(), new Construct() ]; };
getCreatures().forEach(attacker => {
    let list = getCreatures().filter(blocker => { return !attacker.creature.canBeBlocked(attacker, blocker); } )
        .map(e => e.permanent.name).join(", ");
    if (list == "") { list = "全員にブロックされる"; } else { list += " にブロックされない"; }
    console.log(attacker.permanent.name + " は " + list);
});
getCreatures().forEach(blocker => {
    let list = getCreatures().filter(attacker => { return blocker.creature.canBlock(blocker, attacker); } )
        .map(e => e.permanent.name).join(", ");
    if (list == "") { list = "誰もブロックできない"; } else { list += " をブロックできる"; }
    console.log(blocker.permanent.name + " は " + list);
});

出力結果

> 人間 は ドレイク, ゾンビ, ゴブリン にブロックされない
> 天使 は 人間, ゾンビ, ホラー, エレメンタル, ゴブリン にブロックされない
> ドレイク は 人間, ゾンビ, ホラー, エレメンタル, ゴブリン にブロックされない
> ゾンビ は ドレイク, ゾンビ, ゴブリン にブロックされない
> ホラー は 人間, 天使, ドレイク, ゾンビ, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, 恐竜 にブロックされない
> デーモン は 人間, 天使, ドレイク, ゾンビ, ホラー, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, 恐竜 にブロックされない
> エレメンタル は ドレイク, ゾンビ, ゴブリン にブロックされない
> ゴブリン は 人間, 天使, ドレイク, ゾンビ, ホラー, デーモン, ゴブリン, 蜘蛛, スフィンクス にブロックされない
> ドラゴン は 人間, ゾンビ, ホラー, エレメンタル, ゴブリン にブロックされない
> 蜘蛛 は ドレイク, ゾンビ, ゴブリン にブロックされない
> スフィンクス は 人間, ゾンビ, ホラー, エレメンタル, ゴブリン にブロックされない
> 恐竜 は ドレイク, ゾンビ, ゴブリン にブロックされない
> 多相の戦士 は ドレイク, ゾンビ, ゴブリン にブロックされない
> 構築物 は ドレイク, ゾンビ, ゴブリン にブロックされない
> 人間 は 人間, ゾンビ, エレメンタル, 蜘蛛, 恐竜, 多相の戦士, 構築物 をブロックできる
> 天使 は 人間, 天使, ドレイク, ゾンビ, エレメンタル, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> ドレイク は 天使, ドレイク, ドラゴン, スフィンクス をブロックできる
> ゾンビ は 誰もブロックできない
> ホラー は 人間, ゾンビ, ホラー, エレメンタル, 蜘蛛, 恐竜, 多相の戦士, 構築物 をブロックできる
> デーモン は 人間, 天使, ドレイク, ゾンビ, ホラー, デーモン, エレメンタル, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> エレメンタル は 人間, ゾンビ, エレメンタル, ゴブリン, 蜘蛛, 恐竜, 多相の戦士, 構築物 をブロックできる
> ゴブリン は 誰もブロックできない
> ドラゴン は 人間, 天使, ドレイク, ゾンビ, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> 蜘蛛 は 人間, 天使, ドレイク, ゾンビ, エレメンタル, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> スフィンクス は 人間, 天使, ドレイク, ゾンビ, ホラー, デーモン, エレメンタル, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> 恐竜 は 人間, 天使, ドレイク, ゾンビ, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> 多相の戦士 は 人間, 天使, ドレイク, ゾンビ, ホラー, デーモン, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる
> 構築物 は 人間, 天使, ドレイク, ゾンビ, ホラー, デーモン, エレメンタル, ゴブリン, ドラゴン, 蜘蛛, スフィンクス, 恐竜, 多相の戦士, 構築物 をブロックできる

感想

継承を使わないって結構面倒だと思った。でもやってできないこともないので、慣れていきたいと思う。

ソースコード再掲

const isObject = (arg : unknown) : boolean => { return typeof arg === typeof Object() && arg !== null; }
const filterCastInterface = <From, To>(list : From[], judge : (e : From) => boolean) : (From & To)[] => {
    return list.filter(e => { return judge(e); }).map(e => { return ((e as unknown) as From & To) });
}

interface Permanent {
    permanent : PermanentImpl;
    creature : CreatureImpl;
    abilities : Ability[];
}

interface CardType { get name() : string; }
class Artifact implements CardType { get name() { return "アーティファクト"; } }
class Creature implements CardType { get name() { return "クリーチャー"; } }

interface Color { get name() : string; }
class White implements Color { get name() { return "白"; } }
class Blue implements Color { get name() { return "青"; } }
class Black implements Color { get name() { return "黒"; } }
class Red implements Color { get name() { return "赤"; } }
class Green implements Color { get name() { return "緑"; } }

class PermanentImpl {
    get name() { return this._name; }
    get cardTypes() { return this._cardTypes; }
    get colors() { return this._colors; }
    constructor(
        private _name : string,
        private _cardTypes : CardType[],
        private _colors : Color[],
    ) { }
}
class CreatureImpl {
    canBeBlocked(attacker : Permanent, blocker : Permanent) : boolean {
        const attackerAbilityList = filterCastInterface<Ability, CanBeBlocked>(attacker.abilities, isCanBeBlocked);
        const blockerAbilityList = filterCastInterface<Ability, CanBlock>(blocker.abilities, isCanBlock);
        return blocker.permanent.cardTypes.includes(e => { return e instanceof Creature })
            && attackerAbilityList.every(a => { return a.canBeBlocked(attacker, blocker); })
            && blockerAbilityList.every(a => { return a.canBlock(blocker, attacker); });
    }
    canBlock(blocker : Permanent, attacker : Permanent) : boolean {
        return this.canBeBlocked(attacker, blocker);
    }
}
interface Ability {
    ability : AbilityImpl;
}
class AbilityImpl {
    get text() { return this._text; }
    constructor(
        private _text : string,
    ) { }
}
interface CanBlock {
    canBlock(blocker : Permanent, attacker : Permanent) : boolean;
}
const isCanBlock = (arg : unknown) : arg is CanBlock => {
    return isObject(arg) && typeof (arg as CanBlock).canBlock === typeof function(){};
}
interface CanBeBlocked {
    canBeBlocked(attacker : Permanent, blocker : Permanent) : boolean;
}
const isCanBeBlocked = (arg : unknown) : arg is CanBeBlocked => {
    return isObject(arg) && typeof (arg as CanBeBlocked).canBeBlocked === typeof function(){};
}

class Vigilance implements Ability {
    ability = new AbilityImpl("警戒");
}
class Flying implements Ability, CanBeBlocked {
    ability = new AbilityImpl("飛行");
    canBeBlocked(attacker: Permanent, blocker: Permanent): boolean {
        return blocker.abilities.some(a => { return a instanceof Flying || a instanceof Reach; });
    }
}
class Reach implements Ability, CanBlock {
    ability = new AbilityImpl("到達");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return true;    // 飛行を参照
    }
}
class Intimidate implements Ability, CanBeBlocked {
    ability = new AbilityImpl("威嚇");
    canBeBlocked(attacker: Permanent, blocker: Permanent): boolean {
        return blocker.permanent.cardTypes.some(e => { return e instanceof Artifact; })
            || attacker.permanent.colors.some(a => { return blocker.permanent.colors.some(b => { return a.name == b.name; }) })
    }
}
class Unblockable implements Ability, CanBlock {
    ability = new AbilityImpl("このクリーチャーはブロックできない。");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return false;
    }
}
class HighFlying implements Ability, CanBlock {
    ability = new AbilityImpl("このクリーチャーは飛行を持たないクリーチャーをブロックできない。");
    canBlock(blocker: Permanent, attacker : Permanent): boolean {
        return attacker.abilities.some(a => { return a instanceof Flying; });
    }
}

class Human implements Permanent {
    permanent = new PermanentImpl("人間", [ new Creature() ], [ new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Vigilance() ];
}
class Angel implements Permanent {
    permanent = new PermanentImpl("天使", [ new Creature() ], [ new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new Vigilance() ];
}
class Drake implements Permanent {
    permanent = new PermanentImpl("ドレイク", [ new Creature() ], [ new Blue() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new HighFlying() ];
}
class Zombie implements Permanent {
    permanent = new PermanentImpl("ゾンビ", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Unblockable() ];
}
class Horror implements Permanent {
    permanent = new PermanentImpl("ホラー", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate() ];
}
class Demon implements Permanent {
    permanent = new PermanentImpl("デーモン", [ new Creature() ], [ new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying(), new Intimidate() ];
}
class Elemental implements Permanent {
    permanent = new PermanentImpl("エレメンタル", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ ];
}
class Goblin implements Permanent {
    permanent = new PermanentImpl("ゴブリン", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate(), new Unblockable() ];
}
class Dragon implements Permanent {
    permanent = new PermanentImpl("ドラゴン", [ new Creature() ], [ new Red() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying() ];
}
class Spider implements Permanent {
    permanent = new PermanentImpl("蜘蛛", [ new Creature() ], [ new Green() ]);
    creature = new CreatureImpl();
    abilities = [ new Reach() ];
}
class Sphinx implements Permanent {
    permanent = new PermanentImpl("スフィンクス", [ new Creature() ], [ new White(), new Blue(), new Black() ]);
    creature = new CreatureImpl();
    abilities = [ new Flying() ];
}
class Dinosaur implements Permanent {
    permanent = new PermanentImpl("恐竜", [ new Creature() ], [ new Red(), new Green(), new White() ]);
    creature = new CreatureImpl();
    abilities = [ new Vigilance(), new Reach() ];
}
class Shapeshifter implements Permanent {
    permanent = new PermanentImpl("多相の戦士", [ new Creature() ], [ new White(), new Blue(), new Black(), new Red(), new Green() ]);
    creature = new CreatureImpl();
    abilities = [ new Intimidate(), new Reach() ];
}
class Construct implements Permanent {
    permanent = new PermanentImpl("構築物", [ new Artifact(), new Creature() ], [ ]);
    creature = new CreatureImpl();
    abilities = [ new Reach() ];
}

const getCreatures = () => { return [ new Human(), new Angel(), new Drake(), new Zombie(), new Horror(), new Demon(),
    new Elemental(), new Goblin(), new Dragon(), new Spider(), new Sphinx(), new Dinosaur(), new Shapeshifter(), new Construct() ]; };
getCreatures().forEach(attacker => {
    let list = getCreatures().filter(blocker => { return !attacker.creature.canBeBlocked(attacker, blocker); } )
        .map(e => e.permanent.name).join(", ");
    if (list == "") { list = "全員にブロックされる"; } else { list += " にブロックされない"; }
    console.log(attacker.permanent.name + " は " + list);
});
getCreatures().forEach(blocker => {
    let list = getCreatures().filter(attacker => { return blocker.creature.canBlock(blocker, attacker); } )
        .map(e => e.permanent.name).join(", ");
    if (list == "") { list = "誰もブロックできない"; } else { list += " をブロックできる"; }
    console.log(blocker.permanent.name + " は " + list);
});

*1:「到達を持つクリーチャーが飛行を持つクリーチャーをブロックできる」のではなく、「飛行を持つクリーチャーが(飛行や)到達を持つクリーチャーにブロックされない」能力である。前者だとマジックの黄金律『「できない」は「できる」に勝つ』により、(飛行が到達を参照しなければ)意味のない能力になってしまう。

*2:TypeScriptはインターフェースの判別ができないため、インターフェースが実装されるための条件であるメソッド/プロパティが存在しているか否かで判別しなければならない。詳細は「ガード節」で検索。