🃏

MTG一人回しシミュレーターをflutterで作っていたので概要を説明する

に公開

はじめに

マジック:ザ・ギャザリング(MTG)にはまっていたときに、デッキを一人回しするアプリを作りたいなと思って作っていたのでここで供養します。
最新のルールについていけていないのでもはや使えないと思います。

以下Google Play
https://play.google.com/store/apps/details?id=com.soyukkeproducts.myapp&pli=1

実装方針

デッキの一人回しを実際に行うときと同じ感覚を再現したいと考えた。そうすると、実装に必要な動きは概ね以下の通りだと思う。

  • デッキ構築
  • 山札シャッフル
  • ドロー(カードを山札から引く)
  • 土地領域、戦場領域、墓地領域、除外領域にカードを置ける
  • カードをスタックできる
  • カードをタップ(横に向ける)できる

flutterでの状態管理

デッキインポート

mtg arenaでデッキインポートするときに使われるテキストの形式は以下のように、枚数、名前というシンプルな形式なので、input要素に入力されたテキストをparseするように実装しました。
デッキ情報はsqfliteライブラリを使用してsqliteを使って扱っています。

デッキ
2 沼 (ONE) 264
...

サイドボード
2 強迫 (STA) 29
...

領域 Zone

カードを置くことができる領域ウィジェットを必要な分だけ用意している。

final ZONE_LIST = ["Hand", "Battle Field", "Land", "Graveyard", "Exile"];

カードのスタック StackableCards

Zoneに置くことができるウィジェットの最小単位はスタック可能なCardとすることで,すべてスタック可能となっている。

ソリティアのように、スタックされたカードをドラッグ&ドロップして、別のStackableCardsウィジェットに移動することができるように実装している。

/// 束の基本単位
/// Drag, Drop先でもある。
/// 同じ場所にドロップされたら削除してから追加する
class StackableCards extends ConsumerWidget {
  String id;
  List<Widget> cards;
  StackableCards(Key? key, this.id, this.cards) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return DragTarget(
      builder: (context, accepted, rejected) =>
          builder(context, accepted, rejected, ref),
      onAccept: (DraggingData data) => {
        ref.read(deckStateProvide.notifier).state = true,
        print("Stackagble dropped"),
        handleAccept(data, ref)
      },
    );
  }
  
  ...

タップ TappableCard

StackableCardsではカードウィジェットのリストを持つ。MTGシミュレーターに必須な操作として、カードのタップ(90°回転させる)がある。
そのため、StackableCardsの一つ一つのカードはすべてドラッグ&ドロップできるて、タップもできるように実装している。

カードがタップされているかどうかの状態管理用変数を用意し、カードidごとに保持している。

final isTappedProvider = StateProviderFamily<bool, String>((ref, id) => false);

class TappableCard extends ConsumerWidget {
  MyCard card;
  TappableCard(Key? key, this.card) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final myTapped = ref.watch(isTappedProvider(card.id));
    return InkWell(
        onTap: () {
          toggleState(ref);
        },
        child: IgnorePointer(
            child: Container(
          child: RotatedBox(
            quarterTurns: myTapped ? 1 : 0,
            child: card,
          ),
        )));
  }

  void toggleState(WidgetRef ref) {
    final myTapped = ref.watch(isTappedProvider(card.id));
    print(
        "tap:;toggleState::(id, name, value) = (${card.id}, ${card.name}, $myTapped)");
    if (myTapped) {
      ref.read(isTappedProvider(card.id).notifier).state = false;
    } else {
      ref.read(isTappedProvider(card.id).notifier).state = true;
    }
  }
}

Discussion