今回やること。
あるクリーチャーが、攻撃しているクリーチャーをブロックできるかを判定するプログラムを書く。使用言語は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); });