Typescriptでドグマブレードシミュレータ開発(遊戯王1人回しゲーム)

#### 追記
折角なので[ジャンクブレードシミュレータ](https://tsd0313.github.io/ygo-JunkBlade/dist/)も公開しました。

# 概要
先日公開した[ドグマブレードシミュレータ](https://tsd0313.github.io/ygo-DogmaBlade/dist/
)(遊戯王一人回しゲーム)についての解説です。
開発背景、実装アルゴリズム、今後の課題等をまとめました。
遊戯王の基本的なルール知識がある人用に書いているので意味不明な部分は適宜読み飛ばしてください。

>ドグマブレードとは、先行1ターン目に自らのデッキをフル回転させて墓地に魔法カードを溜め、相手のターン開始時に《マジカル・エクスプロージョン》で相手のライフを削り切るワンターンキルデッキです。
フィニッシュの補助輪として《D-HERO ドグマガイ》を使用するほか、手札コストとして《神剣-フェニックスブレード》が多用されることからこの名がつけられました。
引用:ドグマブレード｜ンマルギルドーニ #note https://note.com/gninallman3/n/nf330a71d5446 

# 背景
常々04環境や旋風BFのミラーマッチツールを作りたい！という思いがあり、ぼんやりと開発手法を模索していましたが
初のゲーム開発でいきなりPvPは難易度が高いので、まずは一人回しシステムの構築から始めることとしました。

当初は極僅かながら経験のあるPythonで書いていたものの

 - UI表現の幅が狭い
 - 専用アプリケーションとして公開してもプレイのハードルが高い

という観点からJavaScriptでの開発に転換。
それを見たフロントエンドエンジニアの姉から「TypeScriptにしろ」とのお達しがあったので仰せの通りに。
この時点でTypeScriptどころかJSもHTML/CSSも一切触ったことがなく（「へぇ～JavaとJavaScriptって別物なんだ」とか言ってた）
どんな挙動で何が可能なのかも不明な為、とりあえず手を動かして分からなかったら調べよう！の精神で設計を固めないまま着手しました。

[#KSUweb開発部](https://twitter.com/search?q=%23KSUweb%E9%96%8B%E7%99%BA%E9%83%A8&src=typed_query)で開発途中のあれこれが見られます。


# 目的
 - 遊戯王ルール再現アルゴリズムの土台を構築する。
 - Webアプリケーションの基本的な構造と動作を学ぶ。
 - あわよくば、何故か大量に持っているディスクガイ(旧テキスト)の買取値を上げる。
<img width="600" alt="IMG_6294.JPG]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/0a0a0522-9c70-7e4d-1b3e-4b69dc78d3a4.jpeg">



# スペック

<dl>
 <dt>言語</dt>
 <dd>TypeScript 3.7.2</dd>
</dl>
<dl>
 <dt>HTML5 Canvasフレームワーク</dt>
 <dd>CreateJS 1.0.1</dd>
</dl>
<dl>
 <dt>開発環境</dt>
 <dd>macOS Catalina 10.15.5</dd>
</dl>
<dl>
 <dt>本番環境</dt>
 <dd>GitHub Pages</dd>
</dl>

# 機能
<img width="600" alt="スクリーンショット 2020-06-15 19.29.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/226817d2-1814-7264-13c9-e375531e838b.png">


 - 2008年当時の遊戯王ルールを再現

プレイヤーは使用するカードを選択するだけで、カードの領域移動や効果処理はすべて自動で行われます。

 - カードテキストの表示

カードにマウスオーバーで拡大画像と左側にステータス・テキストが表示されます。
昔のカードに詳しくない方でも安心。

 - リザルト共有

ゲームの勝敗、初期手札、与ダメージ合計をツイート。

# アルゴリズム設計

### 実装クラス
プレイヤーの操作に合わせて各クラスオブジェクトのプロパティを更新し、盤面を描画することでゲームが進行します。

**1. Game**
各領域のカード、召喚権、チェーンブロック等 ゲーム全体に関わるステータス。

```typescript:ClassGame.tsc
class Game{
    defaultDeck : Card[];
    FIELD : Card[];
    MO : Card[];
    ST : Card[];
    GY : Card[];
    DD : Card[];
    EXTRA : Card[];
    DECK : Card[];
    HAND : Card[];
    nowTime : Time;
    timeArray : Time[];
    myLifePoint : number;
    enemyLifePoint : number;
    normalSummon : boolean;
    countNS : number;
    //以下略
}
```


**2. Card**
種族属性レベル分類、保持する効果、所在地、「破壊された」情報、装備カード等 カード毎のステータス。
MonsterCard,SpellCard,TrapCardの3つの子クラスに派生します。

```typescript:ClassCard.tsc
class Card  {
    frontImg : Bitmap;  cardBackImg : Bitmap;
    imageFileName : string;  cardBackImageFileName : string;
    ID : Number;
    cardName : string;
    cardNameJP : string;
    location : "MO"|"ST"|"FIELD"|"DECK"|"HAND"|"GY"|"DD";
    imgContainer : Container;
    cardType : "Monster"|"Spell"|"Trap";
    face : "UP"|"DOWN" ;
    text : string;
    effect : effect[];
    //以下略
}
```

**3. Effect**
発動条件、適用条件、発動元カード、発動時/処理時の動作、対象カード等 カード効果毎のステータス。
エフェクトオブジェクトはカードオブジェクトのeffectプロパティに格納されます。

```typescript:ClassEffect.tsc
class effect {
    card : Card;
    effType : "CardActived"|"Ignition"|"Trigger"|"Continuous"|"Quick"|"Rule";
    spellSpeed : 1|2|3;
    range : ("MO"|"ST"|"FIELD"|"DECK"|"HAND"|"GY"|"DD")[];
    whetherToActivate : "Any"|"Forced";
    costCard : Card[];
    targetCard : Card[];
    lifeCost : number;
    actionPossible :(time:Time) => boolean;
    whenActive : (eff?: effect) => Promise<any>;
    whenResolve : (eff?: effect) => Promise<any>;
    //以下略
}
```


**4. Time**
誘発効果・永続効果の適用条件に関わるタイミング情報のステータス。

```typescript:ClassTime.tsc
class Time{
    summon?:{
            card : MonsterCard;
            type : "NS"|"SS";
            position : "ATK"|"DEF";
            face : "UP"|"DOWN";
            from? : "MO"|"ST"|"FIELD"|"DECK"|"HAND"|"GY"|"DD";
    }[];
    move?:{
        card : Card;
        from? : "MO"|"ST"|"FIELD"|"BOARD"|"DECK"|"HAND"|"GY"|"DD";
        to? : "MO"|"ST"|"FIELD"|"DECK"|"HAND"|"GY"|"DD";
    }[];
    leaveBoard?:{
        card : Card;
    }[];
    discard?:{
        card : Card;
    }[];
    destroy?:{
        card : Card;
        by? : "BATTLE"|"EFFECT"|"RULE";
    }[];
    //以下略
}
```

### チェーンブロックとタイミング
#### タイミング情報の扱い
前述のTimeクラスはタイミング分類とその詳細情報のオブジェクト配列を持ち
Game.nowTimeのValueとしてタイミング情報を記録します。

[《E・HEROエアーマン》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=6784&request_locale=ja)を通常召喚した場合、
Timeインスタンスのsummonプロパティに情報オブジェクトが格納されます。

```typescript:TimeAirmanNSed.tsc
{
    summon:[{
            card : AirmanCardObject,
            type : "NS",
            position : "ATK",
            face : "UP",
            from : "HAND"
    }];
}
```
このTimeインスタンスをgame.timeArrayに蓄積し、誘発効果発動・永続/残存/ルール効果適用の判定に利用します。
エアーマンEffectの誘発条件「自身が」「(表側表示で)召喚または特殊召喚に成功した時」とTimeインスタンスが適合し
発動可能と判定されるわけです。

#### 同時タイミング
複数の処理が連続した場合、それらが同時扱いか否かは明確に区別しなければなりません。
「タイミングを逃す」ルールや永続効果適用判定に大きく関わり、
ここが綯い交ぜになると全く異なる挙動をとってしまう為です。

1つのTimeインスタンスには複数のタイミング情報を持たせることができ、それらは全て「同時」として扱われます。

**例1. [《次元融合》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=5906&request_locale=ja)で複数のモンスターを特殊召喚した**

```typescript:SSatTheSameTime.tsc
{
  summon : [{MonsterA},{MonsterB}] 
};
```
**例2. [《光帝クライス》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=7659&request_locale=ja)の効果 破壊とドロー**

```typescript:DestroyDrawAtTheSameTime.tsc
{
  destroy : [{MonsterA},{MonsterB}] ,
  draw : [{drawA},{drawB}]
};
```
**例3. 発動とコスト ([《デステニー・ドロー》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=6950&request_locale=ja)の場合)**

```typescript:ActivateDdraw.tsc
{
  activate : [{Ddraw}] ,
  discard : [{Diskguy}]
};
```

同時でないタイミング情報は、其々独立したTimeインスタンスを作らなければいけせん。
ここで注意しなければいけないのは、1つのカード効果による一連の処理だったとしても
それが**同時扱いであるとは限らない**ということです。

[《サイバー・ヴァリー》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=7413&request_locale=ja)のドロー効果は
**1.** 自身と他のモンスターを除外
**2.** 2枚ドロー
を一連の効果処理として行いますが、これは[《光帝クライス》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=7659&request_locale=ja)の「破壊とドロー」とは異なり**同時に行われる処理ではありません。**
その為、「除外されたタイミング」と「ドローしたタイミング」 2つの独立したTimeインスタンスを作る必要があります。

```typescript:TimeVaryEffectActivated.tsc
{
  vanish : [{CyberVary},{MonsterA}] 
};

{
  draw : [{drawA},{drawB}]
};
```

#### 効果発動確認フローチャート
1. 一連の同時扱い処理のタイミング情報game.nowTimeをgame.timeArrayにpush。
1. game.nowTimeを新たなTimeインスタンスで上書きし、次の処理情報を記録。
1. 全ての処理が終了したら、game.timeArrayから発動可能な効果を探して発動、チェーンに乗せる。
1. 発動可能な効果が無ければチェーン解決。
1. チェーン処理後さらにその処理内容のタイミング情報から発動可能な効果を…

というループを発動可能な効果がなくなるまで繰り返します。


![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/648bddbf-03e3-e4d4-13f4-0aaa0355d0d2.png)

かなり端折っていますが大まかな流れはこの通りです。
誘発発動優先度、永続効果とその他チェーンに乗らない処理の違い、墓地での情報保持、「フィールドから離れた時」とは…他
ややこしい要素は山程ありますが、遊戯王ルール講座だけで長さが10倍になりそうなので割愛。

#公開してみて
6/12(金)夜に公開。
「身内がたまに遊んでくれるくらいかな」という予想を大きく上回る反響があり、
翌朝にはTwitterトレンド一位になっていました。
<img width="400" alt="IMG_6288.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/1af46524-5dba-178e-11fe-034c2eaafc7b.jpeg">
アクセス解析をつけていないので正確な数は不明ですが、
リザルトツイート数から見積もるに公開後1週間で数万回はプレイされていると思います。

その影響か幾つかのカードショップでドグマブレードパーツの高価買取が開始。
二次創作ゲームというグレーな物を公開した負い目はあったので、現実のカード需要に少しでも貢献できた事はとても嬉しく思います。
ディスクガイ売ります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">《遊戯王買取情報です！》<br>ｻｲﾊﾞｰｳﾞｧﾘｰ（ｽｰﾊﾟｰ） ￥100<br>D-HEROﾄﾞｸﾞﾏｶﾞｲ（ｽｰﾊﾟｰ） ￥200<br>D-HEROﾄﾞｸﾞﾏｶﾞｲ（ｳﾙﾄﾗ） ￥300<a href="https://twitter.com/hashtag/%E9%81%8A%E6%88%AF%E7%8E%8B?src=hash&amp;ref_src=twsrc%5Etfw">#遊戯王</a> <a href="https://twitter.com/hashtag/%E8%B2%B7%E5%8F%96?src=hash&amp;ref_src=twsrc%5Etfw">#買取</a> <a href="https://twitter.com/hashtag/%E3%82%B7%E3%83%BC%E3%82%AC%E3%83%AB?src=hash&amp;ref_src=twsrc%5Etfw">#シーガル</a> <a href="https://t.co/Y9JhXzEP4D">pic.twitter.com/Y9JhXzEP4D</a></p>&mdash; カードゲームのシーガル仙台駅前店 (@seagull_sendai) <a href="https://twitter.com/seagull_sendai/status/1271632086202384384?ref_src=twsrc%5Etfw">June 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


開発相談に乗ってくれた方、プレイしてくれた方、拡散してくれた方、バグ報告を送ってくれた方
皆様本当にありがとうございました。心よりお礼申し上げます。

#課題点
###セレクトウインドウを開いた状態で場や墓地の確認ができない
特定のイベントのみONOFFを切り替える方法が分からず、
効果の発動と処理が終わるまでボード上のカードマウスイベントを纏めて無効にしています。
明確な欠陥UIなのでなんとか改善したいところ第一位。
###手札が溢れる
手札20枚を超えたあたりからカードが画面外に溢れ表示されなくなります。
遊戯王のルールでは（自分のターン内であれば）手札枚数に上限はありません。
ことドグマブレードにおいては《混沌の黒魔術師》[《サイバー・ヴァリー》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=7413&request_locale=ja)[《次元融合》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=5906&request_locale=ja)[《魔力倹約術》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=6051&request_locale=ja)のループが搭載されており
早期に成立すれば手札が30枚を超えることも有り得ます。
レアケースな上にそこまで手札を溜め込む事にあまり意味は無いのですが
それがルール上可能なプレイである以上はどうにかして対応したいところです。
###スマホでの操作性が悪い
元々PCブラウザ環境でのプレイを想定して公開したものですが、
Twitterに上げられているスクショを見るにスマホプレイの人もかなり多いんですね。
現状スマホ環境でも操作自体は可能なものの、ボタンが小さく誤タップが頻発したり
カードを一つ一つタップしないとテキスト確認ができなかったりと不便な点が目立ちます。
###ルール再現が不十分
限られたカードプールでは顕在化しないだけで、本来のルールに沿わない処理が多数あります。
・[《光帝クライス》](https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&cid=7659&request_locale=ja)の効果で対象を破壊できなくてもドローできてしまう。
・《混沌の黒魔術師》の効果が無効になっていると場を離れても除外されない。
他諸々、上辺だけのハリボテ状態で裏はボロボロ。
より複雑化した現代ルールで万を超えるカードをバグなく実装している公式ゲームの凄まじさを痛感します。
中身覗いてみたい。
[遊戯王レガシー・オブ・ザ・デュエリスト](https://www.konami.com/yugioh/lotd_le/jp/ja/) Switch/PS4/Xbox/Steamにて好評配信中。
###コーディングが乱雑
無学のまま具体的な設計無しに始めた為、非常に読みづらくメンテナンス性も悪いコードになっています。
1ファイルに思いつくまま書き散らかした結果5000行弱にまでなり、書いた本人も見直しに苦労する有様。
次からは事前に設計を固めて、整頓されたコードにしようと誓いました。
###バージョン管理してない
Gitの扱いでハマってしまい、一切のバージョン管理をしていません。
GitHubにはブラウザから手動でアップロードしています。エンジニアの友人に正気を疑われました。
趣味の個人開発とはいえ、このまま続けるととんでもない事態に陥ることは明らかなので
次のプロジェクトに着手する前にしっかり勉強し直そうと思います。

#今後

以上の課題を踏まえた上で対戦ツール開発に繋げていこうと思います。まずは04環境から。

対戦を実現するには当然バトルフェイズや相手ターンでの優先権管理が要件に挙がり、サーバーサイドの学習も必要になってきます。
今回手を付けなかったVue,Reactも触ってみたいですね。
やること山積みで先は見えませんが暖かく見守っていただけると幸いです。
あとフロントエンドエンジニア転職したいので募集してるとこあったらお声がけいただけるともっと幸いです。

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>