【Flutter/Dart】神経衰弱の判定処理

目次

概要

今回は事情により神経衰弱の項目を詰め込んでいる。
いくつかの部分があるのでまとめていこう。

ソースコード

アプリ実行部分

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'TrumpCard/Simple/TrumpCardViewSimple.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        useMaterial3: true,
      ),
      home: const TrumpCardViewSimple(title: 'Flutter Demo Home Page'),
    );
  }
}

トランプに関するコード

import 'TrumpMark.dart';

class TrumpCard {
  // メンバ変数
  bool isSelected = false;  // 選択されているかどうか
  bool isGetten = false;    // カードが取得されたかどうか
  int number;               // カードの番号
  TrumpMark mark;           // カードのマーク

  // コンストラクタ
  TrumpCard(this.number, this.mark);

  // タップされた時の処理
  void tap() {
    // 選択されている状態にする
    this.isSelected = !this.isSelected;
  }
}

トランプのマークに関する列挙体

enum TrumpMark {
  spade,
  heart,
  dia,
  club
}

// マーク(列挙体の値)に応じて取得する文字列を切り替える
extension TrumpMarkAdditional on TrumpMark {
  String get trumpMarkString {
    switch (this) {
      case TrumpMark.spade:
        return 'spade';
      case TrumpMark.heart:
        return 'heart';
      case TrumpMark.dia:
        return 'dia';
      case TrumpMark.club:
        return 'club';
    }
  }
}

トランプボタン

import 'package:flutter/material.dart';

import '../../Common/TrumpCard.dart';
import '../../Common/TrumpMark.dart';

class StateTrumpCard extends StatefulWidget {
  final Function() notifyParent;
  TrumpCard trumpCard;
  StateTrumpCard({super.key, required this.notifyParent, required this.trumpCard});

  @override
  State<StatefulWidget> createState() => _StateTrumpCard(notifyParent: notifyParent, trumpCard: trumpCard);
}

class _StateTrumpCard extends State<StateTrumpCard> {
  final Function() notifyParent;
  TrumpCard trumpCard;

  _StateTrumpCard({required this.notifyParent, required this.trumpCard});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 80,
        height: 120,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(0),
          image:trumpDisplay(trumpCard),
        ),
        margin: const EdgeInsets.all(5),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            borderRadius: BorderRadius.circular(0),
            onTap: () {
              notifyParent();
            },
          ),
        ),
      ),
    );
  }

  DecorationImage trumpDisplay(TrumpCard trumpCard) {
    if (trumpCard.isSelected) {
      return DecorationImage(
        image: AssetImage('images/${trumpCard.mark.trumpMarkString}_${trumpCard.number+1}.png'),
        fit: BoxFit.cover,
      );
    } else {
      return DecorationImage(
        image: AssetImage('images/card_back.png'),
        fit: BoxFit.cover,
      );
    }
  }
}

神経衰弱部分

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

import 'StateTrumpCard.dart';
import '../../Common/TrumpCard.dart';
import '../../Common/TrumpMark.dart';

class TrumpCardViewSimple extends StatefulWidget {
  const TrumpCardViewSimple({super.key, required this.title});
  final String title;

  @override
  State<TrumpCardViewSimple> createState() => _TrumpCardViewSimpleState();
}

class _TrumpCardViewSimpleState extends State<TrumpCardViewSimple> {
  // トランプカード
  List<TrumpCard> trumpCards = [];
  // 選択したトランプカード
  TrumpCard? selectedCard = null;
  // ポイント
  int point = 0;
  // 判定中
  bool isJudging = false;

  @override
  Widget build(BuildContext context) {
    final itemsPerRow = 13;

    // 52枚分のトランプのオブジェクトを生成
    initTrumpCards();
    // トランプボタンの配列を作成
    final List<StateTrumpCard> trumpButtons = createTrumpButtons();
    // トランプをマークに応じて13枚ずつ分ける
    final slicedTrumpButtons = trumpButtons.slices(itemsPerRow).toList();

    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: Stack(
            children: [
              cardArea(slicedTrumpButtons),
              Container(width: MediaQuery.of(context).size.width,
                  height: 40,
                  color: Colors.white,
                  child: Text("点数:${this.point}",
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 24
                    ))
              )
            ]
        )
    );
  }

  // トランプを縦に並べる
  Widget columnTrumpButtons(List<StateTrumpCard> trumpButtons) {
    return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(height: 40,),
          for (var trumpButton in trumpButtons)
            trumpButton,
        ]
    );
  }

  // トランプ全体を表示させる部分
  Widget cardArea(List<List<StateTrumpCard>> cardButtons) {
    return Container(
        width: double.infinity,
        height: double.infinity,
        // 横方向のスクロールビュー
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          // 縦方向のスクロールビュー
          child: SingleChildScrollView(
            scrollDirection: Axis.vertical,
            child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // 縦に並べたトランプを横に並べる
                  for (var slicedTrumpButton in cardButtons)
                    columnTrumpButtons(slicedTrumpButton),
                ]
            ),
          ),
        )
    );
  }
  
  // トランプのデータをメンバ変数trumpCardsに格納する
  void initTrumpCards() {
    List<TrumpCard> trumpCards = [];
    for(TrumpMark mark in TrumpMark.values) {
      for (int num = 0; num < 13; num ++) {
        trumpCards.add(TrumpCard(num, mark));
      }
    }
    this.trumpCards = trumpCards;
  }

  // タップすると画面の表示を変えるボタンを生成数
  List<StateTrumpCard> createTrumpButtons() {
    List<StateTrumpCard> trumpButtons = [];

    for (TrumpCard trumpCard in trumpCards) {
      var trumpButton = StateTrumpCard(notifyParent: () { tapped(trumpCard); },
        trumpCard: trumpCard,);

      trumpButtons.add(trumpButton);
      // トランプのデータをシャッフルする
      trumpButtons.shuffle();
    }
    return trumpButtons;
  }

  // タップした時の処理
  void tapped(TrumpCard trumpCard) async {
    // 既に選択されているトランプだった場合は処理を中断
    if (trumpCard.isSelected) {
      return;
    }

    // 判定中の場合は処理を中断する
    if (isJudging) {
      print("判定中");
      return;
    }

    // タップした時の処理(画面更新処理も同時に行う)
    setState(() {
      trumpCard.tap();
    });

    // 選択中のカードがない場合
    if (this.selectedCard == null) {
      // 選択中のカードを設定する
      this.selectedCard = trumpCard;
    } else {
      // 判定中のフラグを立てる
      isJudging = true;
      // 1秒間処理を止める(2枚目のカードを表示するため)
      await Future.delayed(Duration(seconds: 1));

      // カードの数字があっているかどうかの判定(画面更新処理も同時に行う)
      setState(() {
        // 1枚目と2枚目のカードの番号が同じかチェック
        bool isCorrect = (trumpCard.number == this.selectedCard!.number);
        // 1枚目と2枚目のカードの選択状態を更新する
        this.selectedCard!.isSelected = isCorrect;
        trumpCard.isSelected = isCorrect;
        // 1枚目と2枚目のカードの取得状態を更新する
        this.selectedCard!.isGetten = isCorrect;
        trumpCard.isGetten = isCorrect;

        // 1枚目と2枚目のカードの番号が同じ場合、点数を増やす
        if (isCorrect) {
          print("正解");
          this.point += 1;
        } else {
          print("不正解");
        }
      });

      // 選択中のカードを解放
      this.selectedCard = null;
      // 判定中のフラク解除
      isJudging = false;
    }
  }
}

デモ動画

詳細

項目ごとに分けていこう。
分解していくと以下のような役割だ。

  1. カードをタップする
  2. タップしたら選択中のフラグをtrue(TrumpCardのisSelectedをtrue)にする
  3. 選択中のカードを設定する(selectedCardに選択したカードのデータを格納)
  4. 次にカードをタップしたら選択中のカードの数字と、タップしたカードの数字を比べる
    • 数字が同じだった場合、二つのカードを取得した状態にし(TrumpCardのisGettenをtrueにする)、点数を増やす(this.point += 1;)
    • 数字が違う場合、選択状態を未選択の状態にする(TrumpCardのisSelectedをfalse)
    • 上記の処理が終わったら、選択中のカードのデータを何もない状態にする(selectedCardにnullを設定する)

カードをタップする

カードをタップした時の処理はここだ。
この処理の内容を理解していこう。

void tapped(TrumpCard trumpCard) async {
  // 既に選択されているトランプだった場合は処理を中断
  if (trumpCard.isSelected) {
    return;
  }

  // 判定中の場合は処理を中断する
  if (isJudging) {
    print("判定中");
    return;
  }

  // タップした時の処理(画面更新処理も同時に行う)
  setState(() {
    trumpCard.tap();
  });

  // 選択中のカードがない場合
  if (this.selectedCard == null) {
    // 選択中のカードを設定する
    this.selectedCard = trumpCard;
  } else {
    // 判定中のフラグを立てる
    isJudging = true;
    // 1秒間処理を止める(2枚目のカードを表示するため)
    await Future.delayed(Duration(seconds: 1));

    // カードの数字があっているかどうかの判定(画面更新処理も同時に行う)
    setState(() {
      // 1枚目と2枚目のカードの番号が同じかチェック
      bool isCorrect = (trumpCard.number == this.selectedCard!.number);
      // 1枚目と2枚目のカードの選択状態を更新する
      this.selectedCard!.isSelected = isCorrect;
      trumpCard.isSelected = isCorrect;
      // 1枚目と2枚目のカードの取得状態を更新する
      this.selectedCard!.isGetten = isCorrect;
      trumpCard.isGetten = isCorrect;

      // 1枚目と2枚目のカードの番号が同じ場合、点数を増やす
      if (isCorrect) {
        print("正解");
        this.point += 1;
      } else {
        print("不正解");
      }
    });

    // 選択中のカードを解放
    this.selectedCard = null;
    // 判定中のフラク解除
    isJudging = false;
  }
}

タップしたら選択中のフラグをtrue(TrumpCardのisSelectedをtrue)にする

タップしたら、まずタップしたトランプのデータを更新しよう。
まず、メソッド内でタップしたトランプのオブジェクトはtrumpCardだ。
そのトランプをタップした時の処理を行うので、trumpCardのtap()メソッドを呼ぶ。

そして、画面更新のする必要があるためsetStateを使用する

// タップした時の処理(画面更新処理も同時に行う)
setState(() {
  trumpCard.tap();
});

選択中のカードを設定する(selectedCardに選択したカードのデータを格納)

選択したトランプカードは_TrumpCardViewSimpleStateクラスで以下のように定義している。

// 選択したトランプカード
TrumpCard? selectedCard = null;

型の後に「?」がついているが、これはnullを許容するかどうかのものだ。
nullというのはわかりやすくいうと、「何も入っていない」ということ。
今までは何かしらの値が入っていた。Intなら「0」、文字列なら「””」、Boolなら「false」などなど。

しかし、nullというのはそれすら入っていない。箱の中が空の状態を指している。
何も入っていないという状態を許容することで、神経衰弱で「開いているカードがあるかないか」を表現できる。

そして、一枚目のカードをタップしたらこの「selectedCard」にタップしたトランプのオブジェクト「trumpCard」を格納する。

次にカードをタップしたら選択中のカードの数字と、タップしたカードの数字を比べる

その箇所はここだ。

bool isCorrect = (trumpCard.number == this.selectedCard!.number);

// 1枚目と2枚目のカードの選択状態を更新する
this.selectedCard!.isSelected = isCorrect;
trumpCard.isSelected = isCorrect;

// 1枚目と2枚目のカードの取得状態を更新する
this.selectedCard!.isGetten = isCorrect;
trumpCard.isGetten = isCorrect;

if (isCorrect) {
  print("正解");
  this.point += 1;
} else {
  print("不正解");
}

これはわかりやすいだろう。1枚目に選択したオブジェクトの数字(this.selectedCard!.number)と2枚目にタップしたオブジェクトの数字(trumpCard.number)を比較する。

そしてisSelectedとisGettenの値を更新して、カードが取得されいるかどうか、カードが選択された状態になっているかを更新する。

そして、数字が同じだった場合はpointを1増やす。

そして、これらの情報は値を更新するたびに画面の表示が変わるため、setStateの中に書いている。

上記の処理が終わったら、選択中のカードのデータを何もない状態にする(selectedCardにnullを設定する)

そして、最後に選択中のカードのデータを何もない状態にしよう。
こうしてあげないと、永遠に2枚目をめくっている状態とアプリ側は判断してしまう。
なぜなら、選択中のカードのデータがあるということは、2枚目のカードをめくろうとしていると判断するからだ。

判定中は選択できないようにする

これはあまりおすすめの実装方法ではない。
処理を一旦止めるというのはバグや想定外の処理の元になるからだ。
ただ、今回はわかりやすさ最重視ということで、以下の処理を行うことで一旦処理を止めている。

// 判定中のフラグを立てる
isJudging = true;
// 1秒間処理を止める(2枚目のカードを表示するため)
await Future.delayed(Duration(seconds: 1));

awaitというのは、「一旦処理を止めるよ」ということを表す。
今回の場合、secondsが1に設定されているため、1秒間だけこの行で処理が止まってくれるようにしている。

また、メソッド内でawaitを使用した場合、メソッドの「()」の後に「async」をつける決まりになっている。

void tapped(TrumpCard trumpCard) async {
  // 中略
}

参考ページ