ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2022-06-26

pubspec.yaml

name: singlepathpuzzle
description: SinglePathPuzzle

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.8+9

environment:
  sdk: ">=2.17.3 <3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.15
  just_audio: ^0.9.25
  flutter_svg: ^1.1.0
  google_mobile_ads: ^1.3.0
  flutter_localizations:    #多言語ライブラリの本体
    sdk: flutter
  intl: ^0.17.0   #多言語やフォーマッタなどの関連ライブラリ
  package_info_plus: ^1.4.2

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.5

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.9.3
  flutter_native_splash: ^2.2.3+1

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.1

flutter_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"
  adaptive_icon_background: "assets/icon/icon_back.png"
  adaptive_icon_foreground: "assets/icon/icon_fore.png"

flutter_native_splash:
  color: '#ffcd40'
  image: 'assets/image/splash.png'
  color_dark: '#ffcd40'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#ffcd40'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#ffcd40'
    image_dark: 'assets/image/splash.png'


# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:
  assets:
    - assets/sound/
    - assets/icon/
    - assets/image/
  generate: true    #自動生成フラグの有効化

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

lib/main.dart

///
/// single path puzzle
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2022-06-20
/// @date 2022-06-26
///

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:just_audio/just_audio.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'dart:math';
import 'dart:async';
import 'dart:io';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp, //縦固定
  ]);
  MobileAds.instance.initialize();
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});
  @override
  State<MainApp> createState() => _MainAppState();
}

class Pref {
  static const String keyDifficulty = "keyDifficulty";
  static const String keySound = "keySound";
  static const String keyLanguage = "keyLanguage";
}

class LanguageChoice {
  final Map<String,String> langMap = {
    'en': 'English',
    'bg': 'български език',
    'cs': 'Čeština',
    'da': 'dansk',
    'de': 'Deutsch',
    'el': 'Ελληνικά',
    'es': 'Español',
    'et': 'eesti keel',
    'fi': 'Suomen kieli',
    'fr': 'Français',
    'hu': 'magyar nyelv',
    'it': 'Italiano',
    'ja': '日本語',
    'lt': 'lietuvių kalba',
    'lv': 'Latviešu',
    'nl': 'Nederlands',
    'pl': 'Polski',
    'pt': 'Português',
    'ro': 'limba română',
    'ru': 'русский',
    'sk': 'Slovenčina',
    'sv': 'svenska',
    'th': 'ภาษาไทย',
    'zh': '中文',
  };
  List<String> languageList() {
    List<String> languages = [];
    langMap.forEach((key, value) {
      languages.add(key);
    });
    return languages;
  }
  String getLanguageName(String str) {
    if (langMap.containsKey(str)) {
      return langMap[str]!;
    }
    return '';
  }
  List<Locale> localeList() {
    List<Locale> locales = [];
    langMap.forEach((key, value) {
      locales.add(Locale(key));
    });
    return locales;
  }
}

class PieceWay {
  static const int nothing = -1;
  static const int plain = 0;
  static const int center = 1;
  static const int up = 2;
  static const int down = 3;
  static const int left = 4;
  static const int right = 5;
  static const int upDown = 6;
  static const int leftRight = 7;
  static const int upLeft = 8;
  static const int upRight = 9;
  static const int downLeft = 10;
  static const int downRight = 11;
}

class DifficultyLevel {
  static const List<int> levels = [4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
}

class ColorDesign {
  static const Color appBackground = Color.fromARGB(255, 12, 20, 94);
  static const Color appHeader = Color.fromARGB(255, 21, 38, 168);
  static const Color appHeaderLine = Color.fromARGB(255, 53, 80, 209);
  static const Color appHeaderText = Color.fromARGB(255, 255, 255, 255);
  static const Color appBody = Color.fromARGB(255, 255, 255, 255);
  static const Color canvasClose = Color.fromARGB(255, 247, 247, 247);
  static const Color canvasOpen = Color.fromARGB(255, 16, 157, 62);
  static const Color pieceClose = Color.fromARGB(255, 32, 220, 95);
  static const Color pieceOpen = Color.fromARGB(255, 32, 220, 95);
  static const Color pieceLine = Color.fromARGB(255, 77, 51, 22);
  static const Color pieceLineSoil = Color.fromARGB(255, 114, 68, 26);
}

class RandomDouble {
  static final List<double> staticNum = [
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
    Random().nextDouble(),
  ];
}

class AudioPlay {
  static const int soundOff = 1;
  static const int soundOn = 2;
  final String _audioStart = 'assets/sound/start.mp3';
  final String _audioRetry = 'assets/sound/retry.mp3';
  final String _audioComplete = 'assets/sound/complete.mp3';
  final String _audioBack = 'assets/sound/back.mp3';
  final List<String> _audioForward = [
    'assets/sound/forward0.mp3',
    'assets/sound/forward1.mp3',
    'assets/sound/forward2.mp3',
    'assets/sound/forward3.mp3',
    'assets/sound/forward4.mp3',
    'assets/sound/forward5.mp3',
    'assets/sound/forward6.mp3',
    'assets/sound/forward7.mp3',
    'assets/sound/forward8.mp3',
    'assets/sound/forward9.mp3',
  ];
  static final List<AudioPlayer> _audioPlayerStart = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _audioPlayerRetry = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _audioPlayerComplete = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _audioPlayerBack = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _audioPlayerForward = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  int _sound = soundOn;
  int _audioPlayerStartPtr = 0;
  int _audioPlayerRetryPtr = 0;
  int _audioPlayerCompletePtr = 0;
  int _audioPlayerBackPtr = 0;
  int _audioPlayerForwardPtr = 0;
  AudioPlay() { //constructor
    constructor();
  }
  void constructor() async {
    for (int i = 0; i < _audioPlayerStart.length; i++) {
      await _audioPlayerStart[i].setAsset(_audioStart);
    }
    for (int i = 0; i < _audioPlayerRetry.length; i++) {
      await _audioPlayerRetry[i].setAsset(_audioRetry);
    }
    for (int i = 0; i < _audioPlayerComplete.length; i++) {
      await _audioPlayerComplete[i].setAsset(_audioComplete);
    }
    for (int i = 0; i < _audioPlayerBack.length; i++) {
      await _audioPlayerBack[i].setAsset(_audioBack);
    }
    for (int i = 0; i < _audioPlayerForward.length; i++) {
      await _audioPlayerForward[i].setAsset(_audioForward[i % 10]);
    }
  }
  void dispose() {
    for (int i = 0; i < _audioPlayerStart.length; i++) {
      _audioPlayerStart[i].dispose();
    }
    for (int i = 0; i < _audioPlayerRetry.length; i++) {
      _audioPlayerRetry[i].dispose();
    }
    for (int i = 0; i < _audioPlayerComplete.length; i++) {
      _audioPlayerComplete[i].dispose();
    }
    for (int i = 0; i < _audioPlayerBack.length; i++) {
      _audioPlayerBack[i].dispose();
    }
    for (int i = 0; i < _audioPlayerForward.length; i++) {
      _audioPlayerForward[i].dispose();
    }
  }
  void setSoundMode(int soundMode) {
    _sound = soundMode;
  }
  int getSoundMode() {
    return _sound;
  }
  String getSoundModeStr(dynamic context) {
    if (_sound == soundOff) {
      return AppLocalizations.of(context)!.off;
    } else if (_sound == soundOn) {
        return AppLocalizations.of(context)!.on;
    }
    return '';
  }
  void playSoundStart() async {
    if (_sound != soundOn) {
      return;
    }
    _audioPlayerStartPtr += 1;
    if (_audioPlayerStartPtr >= _audioPlayerStart.length) {
      _audioPlayerStartPtr = 0;
    }
    await _audioPlayerStart[_audioPlayerStartPtr].pause();
    await _audioPlayerStart[_audioPlayerStartPtr].seek(Duration.zero);
    await _audioPlayerStart[_audioPlayerStartPtr].play();
  }
  void playSoundRetry() async {
    if (_sound != soundOn) {
      return;
    }
    _audioPlayerRetryPtr += 1;
    if (_audioPlayerRetryPtr >= _audioPlayerRetry.length) {
      _audioPlayerRetryPtr = 0;
    }
    await _audioPlayerRetry[_audioPlayerRetryPtr].pause();
    await _audioPlayerRetry[_audioPlayerRetryPtr].seek(Duration.zero);
    await _audioPlayerRetry[_audioPlayerRetryPtr].play();
  }
  void playSoundComplete() async {
    if (_sound != soundOn) {
      return;
    }
    _audioPlayerCompletePtr += 1;
    if (_audioPlayerCompletePtr >= _audioPlayerComplete.length) {
      _audioPlayerCompletePtr = 0;
    }
    await _audioPlayerComplete[_audioPlayerCompletePtr].pause();
    await _audioPlayerComplete[_audioPlayerCompletePtr].seek(Duration.zero);
    await _audioPlayerComplete[_audioPlayerCompletePtr].play();
  }
  void playSoundBack() async {
    if (_sound != soundOn) {
      return;
    }
    _audioPlayerBackPtr += 1;
    if (_audioPlayerBackPtr >= _audioPlayerBack.length) {
      _audioPlayerBackPtr = 0;
    }
    await _audioPlayerBack[_audioPlayerBackPtr].pause();
    await _audioPlayerBack[_audioPlayerBackPtr].seek(Duration.zero);
    await _audioPlayerBack[_audioPlayerBackPtr].play();
  }
  void playSoundForward() async {
    if (_sound != soundOn) {
      return;
    }
    _audioPlayerForwardPtr += 1;
    if (_audioPlayerForwardPtr >= _audioPlayerForward.length) {
      _audioPlayerForwardPtr = 0;
    }
    await _audioPlayerForward[_audioPlayerForwardPtr].pause();
    await _audioPlayerForward[_audioPlayerForwardPtr].seek(Duration.zero);
    await _audioPlayerForward[_audioPlayerForwardPtr].play();
  }
}

class AppCondition {
  static const int ready = 0;
  static const int playing = 1;
  static const int complete = 2;
  static const int answering = 3;
  double _canvasWidth = 0;
  int _difficulty = 0;
  int _gameCondition = 0;
  AppCondition() { //constructor
    _gameCondition = ready;
  }
  double getCanvasWidth() {
    return _canvasWidth;
  }
  void setCanvasWidth(double w) {
    _canvasWidth = w;
  }
  int getDifficulty() {
    return _difficulty;
  }
  void setDifficulty(int n) {
    _difficulty = n;
  }
  int getGameCondition() {
    return _gameCondition;
  }
  void setGameCondition(int cond) {
    _gameCondition = cond;
  }
  Color stageColor() {
    if (_gameCondition == ready) {
      return ColorDesign.appBody;
    } else if (_gameCondition == playing) {
      return ColorDesign.canvasClose;
    } else if (_gameCondition == complete) {
      return ColorDesign.canvasOpen;
    }
    return ColorDesign.appBody;
  }
  Color bgColor() {
    if (_gameCondition == ready) {
      return ColorDesign.appBody;
    } else if (_gameCondition == playing) {
      return ColorDesign.appBody;
    } else if (_gameCondition == complete) {
      return ColorDesign.canvasOpen;
    }
    return ColorDesign.appBody;
  }
}

class MascotImage {
  static const String mole = 'assets/image/mole.svg';
  static const String left = 'assets/image/mole_left.svg';
  static const String right = 'assets/image/mole_right.svg';
  static const String up = 'assets/image/mole_up.svg';
  static const String down = 'assets/image/mole_down.svg';
  String getImageStr(int currentX, int currentY, int beforeX, int beforeY) {
    if (currentY < beforeY) {
      return up;
    } else if (currentY > beforeY) {
        return down;
    } else if (currentX > beforeX) {
      return right;
    } else if (currentX < beforeX) {
      return left;
    }
    return left;
  }
}

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    }
    if (_isAdMob) {
      _adMobBanner = BannerAd(
        adUnitId: adBannerUnitId,
        size: AdSize.banner,
        request: const AdRequest(),
        listener: const BannerAdListener(),
      );
    }
  }
  void load() {
    if (_isAdMob) {
      _adMobBanner.load();
    }
  }
  Widget getAdBannerWidget() {
    if (_isAdMob) {
      return Container(
        alignment: Alignment.center,
        width: _adMobBanner.size.width.toDouble(),
        height: _adMobBanner.size.height.toDouble(),
        child: AdWidget(ad: _adMobBanner),
      );
    } else {
      return Container(height: 150);
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

//-------------------------------------------------

class _MainAppState extends State<MainApp> {
  Locale locale = AppLocalizations.supportedLocales.first;
  @override
  Widget build(BuildContext context) {
    final LanguageChoice languageChoice = LanguageChoice();
    final List<Locale> localeList = languageChoice.localeList();
    return MaterialApp(
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: localeList,
      locale: locale,
      home: const MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);
  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  final GlobalKey<ScaffoldState> _key = GlobalKey<ScaffoldState>();
  String _version = '';
  List<int> _pieceState = []; //ピースの状態
  List<int> _pieceRoute = []; //問題制作時の道筋(回答例)
  List<int> _userRoute = [];  //ユーザー操作
  int _retryCount = 10; //リトライ数
  final AppCondition appCondition = AppCondition();
  final AdMob adMob = AdMob();
  final AudioPlay audioPlay = AudioPlay();
  final MascotImage mascotImage = MascotImage();
  @override
  void initState() {
    super.initState();
    getVersion();
    adMob.load();
  }
  @override
  void dispose() {
    audioPlay.dispose();
    super.dispose();
  }
  Future getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      _version = packageInfo.version;
    });
  }
  void getAppCanvasWidth() {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      final double w = _key.currentContext!.size!.width - 16;
      final double h = _key.currentContext!.size!.height - 16 - 56 - adMob.getAdBannerHeight(); //56はheader高さ
      final double appWidth = (w < h) ? w : h;
      setState(() {
        appCondition.setCanvasWidth(appWidth);
      });
    });
  }
  void _clearPieceState() {
    _pieceState = [];
    for (int i = 0; i < appCondition.getDifficulty() * appCondition.getDifficulty(); i++) {
      _pieceState.add(PieceWay.nothing);
    }
    _userRoute = [];
  }
  void _loadLanguage() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String lang = prefs.getString(Pref.keyLanguage) ?? 'en';
    if (!mounted) {
      return;
    }
    context.findAncestorStateOfType<_MainAppState>()!
      ..locale = Locale(lang)
      ..setState(() {});
  }
  void _saveLanguage(String lang) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setString(Pref.keyLanguage, lang);
  }
  void _loadDifficulty() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      appCondition.setDifficulty(prefs.getInt(Pref.keyDifficulty) ?? 5);
    });
    _clearPieceState();
  }
  void _saveDifficulty() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      prefs.setInt(Pref.keyDifficulty, appCondition.getDifficulty());
    });
    _clearPieceState();
  }
  void _loadSound() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      audioPlay.setSoundMode(prefs.getInt(Pref.keySound) ?? AudioPlay.soundOn);
    });
  }
  void _saveSound() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      prefs.setInt(Pref.keySound, audioPlay.getSoundMode());
    });
  }
  void _onClickStartButton() {
    audioPlay.playSoundStart();
    appCondition.setGameCondition(AppCondition.playing);
    setState(() {
      _retryCount = 10;
      _clearPieceState();
      _makeWay();
    });
  }
  void _onClickRetryButton() {
    audioPlay.playSoundRetry();
    appCondition.setGameCondition(AppCondition.playing);
    final int startPos = _userRoute[0];
    _userRoute = [];
    _userRoute.add(startPos);
    for (int i = 0; i < _pieceState.length; i++) {
      if (_pieceState[i] != PieceWay.nothing) {
        _pieceState[i] = PieceWay.plain;
      }
    }
    _pieceState[startPos] = PieceWay.center;
    setState(() {
      _retryCount -= 1;
      if (_retryCount < 0) {
        _retryCount = 0;
      }
    });
  }
  void _onClickAnswerButton() {
    audioPlay.playSoundRetry();
    appCondition.setGameCondition(AppCondition.playing);
    final int startPos = _userRoute[0];
    _userRoute = [];
    _userRoute.add(startPos);
    for (int i = 0; i < _pieceState.length; i++) {
      if (_pieceState[i] != PieceWay.nothing) {
        _pieceState[i] = PieceWay.plain;
      }
    }
    _pieceState[startPos] = PieceWay.center;
    setState(() {});
    Timer(const Duration(milliseconds: 600), (){  //既に実行されているAnswerが停止するのを待つ
      appCondition.setGameCondition(AppCondition.answering);
      _answerRecursion(0);
    });
  }
  void _answerRecursion(int pieceRoutePtr) async {
    if (appCondition.getGameCondition() != AppCondition.answering) {
      return;
    }
    _pieceTouchAction(_pieceRoute[pieceRoutePtr]);
    if (pieceRoutePtr < _pieceRoute.length - 1) {
      Timer(const Duration(milliseconds: 500), (){_answerRecursion(pieceRoutePtr + 1);});
    }
  }

  //-----------------------------------------------

  void _makeWay() {
    final int difficulty = appCondition.getDifficulty();
    List<int> maxPieceRoute = [];
    for (int j = 0; j < 300; j++) {
      int currentPosition = Random().nextInt(difficulty * difficulty);
      _pieceRoute = [];
      _pieceRoute.add(currentPosition);
      for (int i = 0; i < 999; i++) {
        currentPosition = _nextPosition(currentPosition);
        if (currentPosition == -1) {
          break; //動けなくなった
        }
        _pieceRoute.add(currentPosition);
      }
      if (maxPieceRoute.length < _pieceRoute.length) {
        maxPieceRoute = [..._pieceRoute]; //最大長さを更新
      }
      if (maxPieceRoute.length > (difficulty * difficulty * 0.7)) {
        break;  //目標の長さを超えた
      }
    }
    _pieceRoute = [...maxPieceRoute]; //最大長さを採用
    for (int num in _pieceRoute) {
      _pieceState[num] = PieceWay.plain;
    }
    _userRoute.add(_pieceRoute[0]); //スタート地点
    _pieceState[_pieceRoute[0]] = PieceWay.center; //スタート地点
  }

  int _nextPosition(int currentPosition) {
    final int difficulty = appCondition.getDifficulty();
    List<int> movePossible = [];
    if (currentPosition - difficulty > 0 && _pieceRoute.contains(currentPosition - difficulty) == false) {
      movePossible.add(currentPosition - difficulty);  //上が可能
    }
    if (currentPosition + difficulty < difficulty * difficulty && _pieceRoute.contains(currentPosition + difficulty) == false) {
      movePossible.add(currentPosition + difficulty);  //下が可能
    }
    if (currentPosition % difficulty != 0 && _pieceRoute.contains(currentPosition - 1) == false) {
      movePossible.add(currentPosition - 1);  //左が可能
    }
    if ((currentPosition + 1) % difficulty != 0 && _pieceRoute.contains(currentPosition + 1) == false) {
      movePossible.add(currentPosition + 1);  //右が可能
    }
    if (movePossible.isEmpty) {
      return -1;  //end
    }
    movePossible.shuffle();
    return movePossible[0];
  }

  void _pieceTouchAction(int touchPos) {
    final int difficulty = appCondition.getDifficulty();
    if (_userRoute.length >= 2 && touchPos == _userRoute[_userRoute.length - 2]) {  //戻る
      setState(() {
        audioPlay.playSoundBack();
        _pieceState[_userRoute[_userRoute.length - 1]] = PieceWay.plain;
        _userRoute.removeLast();
      });
      return;
    }
    if (_pieceState[touchPos] != PieceWay.plain) {  //ルートが移動可能ではない
      return;
    }
    bool continuousFlag = false;
    if (_userRoute[_userRoute.length - 1] - difficulty == touchPos) {  //上へ移動
      continuousFlag = true;
    } else if (_userRoute[_userRoute.length - 1] + difficulty == touchPos) {  //下へ移動
      continuousFlag = true;
    } else if (_userRoute[_userRoute.length - 1] - 1 == touchPos) {  //左へ移動
      continuousFlag = true;
    } else if (_userRoute[_userRoute.length - 1] + 1 == touchPos) {  //右へ移動
      continuousFlag = true;
    }
    if (continuousFlag == false) {  //ルートの連続性が無い
      return;
    }
    final int lastTouchPos = _userRoute[_userRoute.length - 1];
    int last2TouchPos = -1;
    if (_userRoute.length >= 2) {
      last2TouchPos = _userRoute[_userRoute.length - 2];
    }
    _userRoute.add(touchPos);
    audioPlay.playSoundForward();
    setState(() {
      if (lastTouchPos - difficulty == touchPos) { //上に移動
        if (last2TouchPos == -1) {  //start位置
          _pieceState[lastTouchPos] = PieceWay.up;
        } else if (last2TouchPos - difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upDown;
        } else if (last2TouchPos - 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upRight;
        } else if (last2TouchPos + 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upLeft;
        }
        _pieceState[touchPos] = PieceWay.down;
      } else if (lastTouchPos + difficulty == touchPos) { //下に移動
        if (last2TouchPos == -1) {  //start位置
          _pieceState[lastTouchPos] = PieceWay.down;
        } else if (last2TouchPos + difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upDown;
        } else if (last2TouchPos - 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.downRight;
        } else if (last2TouchPos + 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.downLeft;
        }
        _pieceState[touchPos] = PieceWay.up;
      } else if (lastTouchPos - 1 == touchPos) { //左に移動
        if (last2TouchPos == -1) {  //start位置
          _pieceState[lastTouchPos] = PieceWay.left;
        } else if (last2TouchPos - 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.leftRight;
        } else if (last2TouchPos + difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upLeft;
        } else if (last2TouchPos - difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.downLeft;
        }
        _pieceState[touchPos] = PieceWay.right;
      } else if (lastTouchPos + 1 == touchPos) { //右に移動
        if (last2TouchPos == -1) {  //start位置
          _pieceState[lastTouchPos] = PieceWay.right;
        } else if (last2TouchPos + 1 == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.leftRight;
        } else if (last2TouchPos + difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.upRight;
        } else if (last2TouchPos - difficulty == lastTouchPos) {
          _pieceState[lastTouchPos] = PieceWay.downRight;
        }
        _pieceState[touchPos] = PieceWay.left;
      }
    });
    if (_userRoute.length == _pieceRoute.length) {
      audioPlay.playSoundComplete();
      appCondition.setGameCondition(AppCondition.complete);
    }
  }

  //-----------------------------------------------

  Widget _startButton() {
    return ElevatedButton(
      onPressed: _onClickStartButton,
      style: ElevatedButton.styleFrom(
        elevation: 0,
        primary: ColorDesign.appHeader,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
        side: const BorderSide(width: 0.5, color: ColorDesign.appHeaderLine),
        padding: const EdgeInsets.all(8.0),
      ),
      child: Text(AppLocalizations.of(context)!.start, style: const TextStyle(color: ColorDesign.appHeaderText)),
    );
  }
  Widget _retryButton() {
    if (appCondition.getGameCondition() == AppCondition.ready) {
      return Container();
    } else {
      final String retryString = _retryCount > 0 ? '${AppLocalizations.of(context)!.retry}: $_retryCount' : AppLocalizations.of(context)!.retry;
      return ElevatedButton(
        onPressed: _onClickRetryButton,
        style: ElevatedButton.styleFrom(
          elevation: 0,
          primary: ColorDesign.appHeader,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
          side: const BorderSide(width: 0.5, color: ColorDesign.appHeaderLine),
          padding: const EdgeInsets.all(8.0),
        ),
        child: Text(retryString,
          style: const TextStyle(color: ColorDesign.appHeaderText),
        ),
      );
    }
  }
  Widget _answerButton() {
    if (_retryCount > 0) {
      return Container();
    } else {
      return ElevatedButton(
        onPressed: _onClickAnswerButton,
        style: ElevatedButton.styleFrom(
          elevation: 0,
          primary: ColorDesign.appHeader,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
          side: const BorderSide(width: 0.5, color: ColorDesign.appHeaderLine),
          padding: const EdgeInsets.all(8.0),
        ),
        child: Text(AppLocalizations.of(context)!.answer,
          style: const TextStyle(
            color: ColorDesign.appHeaderText,
          ),
        ),
      );
    }
  }
  Widget _dialogDifficulty() {
    final int difficulty = appCondition.getDifficulty();
    return SimpleDialog(
      title: Text('${AppLocalizations.of(context)!.difficulty}: $difficulty'),
      children: List.generate(DifficultyLevel.levels.length,(index) {
        return SimpleDialogOption(
          child: Text(DifficultyLevel.levels[index].toString()),
          onPressed: () {
            setState(() {
              appCondition.setDifficulty(DifficultyLevel.levels[index]);
              appCondition.setGameCondition(AppCondition.ready);
            });
            _saveDifficulty();
            Navigator.pop(context);
          }
        );
      }),
    );
  }
  Widget _dialogSound() {
    return SimpleDialog(
      title: Text('${AppLocalizations.of(context)!.sound}: ${audioPlay.getSoundModeStr(context)}'),
      children: <Widget>[
        SimpleDialogOption(
            child: Text(AppLocalizations.of(context)!.off),
            onPressed: () {
              setState(() {
                audioPlay.setSoundMode(AudioPlay.soundOff);
              });
              _saveSound();
              Navigator.pop(context);
            }
        ),
        SimpleDialogOption(
            child: Text(AppLocalizations.of(context)!.on),
            onPressed: () {
              setState(() {
                audioPlay.setSoundMode(AudioPlay.soundOn);
              });
              _saveSound();
              Navigator.pop(context);
            }
        ),
      ],
    );
  }
  Widget _dialogLanguage() {
    final LanguageChoice languageChoice = LanguageChoice();
    final List<String> langList = languageChoice.languageList();
    return SimpleDialog(
      title: Text(AppLocalizations.of(context)!.language),
      children: List.generate(langList.length,(index) {
        return SimpleDialogOption(
          child: Row(
              children:[
                SizedBox(
                  width: 25,
                  child: Text(langList[index])
                ),
                Text('- ${languageChoice.getLanguageName(langList[index])}')
              ]
          ),
          onPressed: () {
            _saveLanguage(langList[index]);
            context.findAncestorStateOfType<_MainAppState>()!
              ..locale = Locale(langList[index])
              ..setState((){});
            Navigator.pop(context);
          }
        );
      }),
    );
  }
  Widget _hamburgerMenu() {
    final int difficulty = appCondition.getDifficulty();
    return Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          SizedBox(
            height: 190,
            child: DrawerHeader(
              decoration: const BoxDecoration(color: Colors.blue),
              child: Column(children:[
                Row(children:[
                  SvgPicture.asset(MascotImage.mole, width: 80, height: 80),
                  const SizedBox(width: 8),
                  Flexible(child: Text(AppLocalizations.of(context)!.appName,style: const TextStyle(color: Colors.white))),
                ]),
                Text(AppLocalizations.of(context)!.usage,style: const TextStyle(fontSize: 11,color: Colors.white)),
              ]),
            )
          ),
          ListTile(
            leading: const CircleAvatar(
              child: Icon(
                Icons.flag,
              ),
            ),
            title: Text('${AppLocalizations.of(context)!.difficulty}: $difficulty'),
            trailing: const Icon(
              Icons.arrow_circle_right_outlined,
            ),
            onTap: () {
              showDialog(
                  context: context,
                  barrierDismissible: true,
                  builder: (_) {
                    return _dialogDifficulty();
                  }
              );
            },
          ),
          Container(
            padding: const EdgeInsets.only(left: 15,right: 15,bottom: 20),
            child: Text(AppLocalizations.of(context)!.difficulty1,style: const TextStyle(fontSize: 11,color: Colors.black54)),
          ),
          ListTile(
            leading: const CircleAvatar(
              child: Icon(
                Icons.audiotrack,
              ),
            ),
            title: Text('${AppLocalizations.of(context)!.sound}: ${audioPlay.getSoundModeStr(context)}'),
            trailing: const Icon(
              Icons.arrow_circle_right_outlined,
            ),
            onTap: () {
              showDialog(
                  context: context,
                  barrierDismissible: true,
                  builder: (_) {
                    return _dialogSound();
                  }
              );
            },
          ),
          Container(
            padding: const EdgeInsets.only(left: 15,right: 15,bottom: 20),
            child: Text(AppLocalizations.of(context)!.sound1,style: const TextStyle(fontSize: 11,color: Colors.black54)),
          ),
          ListTile(
            leading: const CircleAvatar(
              child: Icon(
                Icons.language,
              ),
            ),
            title: Text(AppLocalizations.of(context)!.language),
            trailing: const Icon(
              Icons.arrow_circle_right_outlined,
            ),
            onTap: () {
              showDialog(
                  context: context,
                  barrierDismissible: true,
                  builder: (_) {
                    return _dialogLanguage();
                  }
              );
            },
          ),
          Container(
            padding: const EdgeInsets.only(left: 15,right: 15,top: 50),
            child: Text('v $_version',style: const TextStyle(fontSize: 11,color: Colors.black45)),
          ),
        ],
      ),
    );
  }
  Widget _settingButton() {
    return IconButton(
      icon: const Icon(Icons.menu,color: ColorDesign.appHeaderText),
      tooltip: AppLocalizations.of(context)!.setting,
      onPressed: () {
        _key.currentState?.openDrawer();
      },
    );
  }
  Widget _menuBar() {
    final int difficulty = appCondition.getDifficulty();
    if (difficulty == 0) {  //アプリ起動初回のみ
      _loadDifficulty();
      _loadSound();
      _loadLanguage();
    }
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: const BoxDecoration(color: ColorDesign.appHeader),
      child: Row(
        children: <Widget> [
          _startButton(),
          const SizedBox(width: 8),
          _retryButton(),
          const SizedBox(width: 8),
          _answerButton(),
          const Expanded(
            child: SizedBox(width: 8),
          ),
          _settingButton(),
        ],
      ),
    );
  }
  Widget _boxCanvas() {
    final int difficulty = appCondition.getDifficulty();
    final double canvasWidth = appCondition.getCanvasWidth();
    if (appCondition.getGameCondition() == AppCondition.ready) {
      return InkWell(
        onTap: () {
          _onClickStartButton();
        },
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              SvgPicture.asset(MascotImage.mole, width: 200, height: 200),
            ]
          )
        )
      );
    }
    List<Widget> pieceWidget = <Widget>[];
    for (int y = 0; y < difficulty; y++) {
      for (int x = 0; x < difficulty; x++) {
        final int pieceStatePtr = y * difficulty + x;
        pieceWidget.add(
          CustomPaint(
            painter: _PiecePainter(canvasWidth, difficulty, x, y, _pieceState[pieceStatePtr]),
            child: Container(),
          )
        );
      }
    }
    void _pieceTouch(localPosition) {
      final int x = (localPosition.dx / (canvasWidth / difficulty)).floor();
      final int y = (localPosition.dy / (canvasWidth / difficulty)).floor();
      if (x < 0 || y < 0 || x >= difficulty || y >= difficulty) {
        return;
      }
      _pieceTouchAction(y * difficulty + x);
    }
    return GestureDetector(
      onTapDown: (details) {
        _pieceTouch(details.localPosition);
      },
      onPanUpdate: (details) {
        _pieceTouch(details.localPosition);
      },
      child: Stack(
          children: [
            Stack(children: pieceWidget),
            _mascot(),
          ]
      ),
    );
  }
  Widget _mascot() {
    final double canvasWidth = appCondition.getCanvasWidth();
    final int difficulty = appCondition.getDifficulty();
    final int currentRoutePosition = _userRoute.isEmpty ? _pieceRoute[0] : _userRoute[_userRoute.length - 1];
    final int currentRoutePositionX = currentRoutePosition % difficulty;
    final int currentRoutePositionY = (currentRoutePosition / difficulty).floor();
    final int beforeRoutePosition = (_userRoute.length < 2) ? _pieceRoute[0] : _userRoute[_userRoute.length - 2];
    final int beforeRoutePositionX = beforeRoutePosition % difficulty;
    final int beforeRoutePositionY = (beforeRoutePosition / difficulty).floor();
    final String mascotImgStr = mascotImage.getImageStr(currentRoutePositionX,currentRoutePositionY,beforeRoutePositionX,beforeRoutePositionY);
    final double imageW = canvasWidth / difficulty * 0.8;
    final double imageH = canvasWidth / difficulty * 0.8;
    final double marginLeft = (canvasWidth / difficulty) * currentRoutePositionX + (canvasWidth / difficulty * 0.1);
    final double marginTop = (canvasWidth / difficulty) * currentRoutePositionY + (canvasWidth / difficulty * 0.1);
    return Container(
      margin: EdgeInsets.only(left: marginLeft, top: marginTop),
      child: SvgPicture.asset(
        mascotImgStr,
        semanticsLabel: 'mascot',
        width: imageW,
        height: imageH,
      ),
    );
  }
  @override
  Widget build(BuildContext context) {
    getAppCanvasWidth();
    return Scaffold(
      key: _key,
      appBar: null,
      drawer: _hamburgerMenu(),
      backgroundColor: ColorDesign.appBackground,
      body: SafeArea(
        child: Center(
          child: Column(
            children: [
              _menuBar(),
              Expanded(
                child: Container(
                  color: appCondition.bgColor(),
                  padding: const EdgeInsets.symmetric(horizontal: 8.0,vertical: 8.0),
                  child: Center(
                    child: AspectRatio(
                      aspectRatio: 1 / 1,
                      child: AnimatedContainer(
                        duration: const Duration(milliseconds: 1000),
                        width: double.infinity,
                        height: double.infinity,
                        color: appCondition.stageColor(),
                        child: _boxCanvas(),
                      ),
                    ),
                  ),
                ),
              ),
              Container(
                width: double.infinity,
                color: appCondition.bgColor(),
                child: adMob.getAdBannerWidget(),
              ),
            ]
          ),
        ),
      ),
    );
  }
}

class _PiecePainter extends CustomPainter {
  final double canvasWidth;
  final int difficulty;
  final int x;
  final int y;
  final int direction;
  const _PiecePainter(this.canvasWidth,this.difficulty,this.x,this.y,this.direction);
  @override
  void paint(Canvas canvas, Size size) {
    if (direction == PieceWay.nothing) {
      return;
    }
    final double pieceWidth = canvasWidth / difficulty;
    final double pieceWidthHalf = pieceWidth / 2;
    const int pieceGap = 1;
    const int pieceGapDouble = pieceGap * 2;
    final double pieceLineWidth = pieceWidth / 3;
    final double pieceLineWidthHalf = pieceLineWidth / 2;
    final double piecePosX = x * pieceWidth;
    final double piecePosY = y * pieceWidth;
    if (direction == PieceWay.plain) {
      final paint = Paint()..color = ColorDesign.pieceClose;
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceGap, pieceWidth - pieceGapDouble , pieceWidth - pieceGapDouble), paint);
      return;
    }
    final paintPiece = Paint()..color = ColorDesign.pieceOpen;
    final paintLine = Paint()..color = ColorDesign.pieceLine;
    final paintLineSoil = Paint()..color = ColorDesign.pieceLineSoil;
    canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceGap, pieceWidth - pieceGapDouble , pieceWidth - pieceGapDouble), paintPiece);
    if (direction == PieceWay.center) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
    } else if (direction == PieceWay.up) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceGap, pieceLineWidth , pieceWidthHalf - pieceGap), paintLine);
    } else if (direction == PieceWay.down) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceWidthHalf, pieceLineWidth, pieceWidthHalf - pieceGap), paintLine);
    } else if (direction == PieceWay.left) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.right) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.upDown) {
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceGap, pieceLineWidth , pieceWidth - pieceGapDouble), paintLine);
    } else if (direction == PieceWay.leftRight) {
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidth - pieceGapDouble, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.upLeft) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceGap, pieceLineWidth , pieceWidthHalf - pieceGap), paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.upRight) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceGap, pieceLineWidth , pieceWidthHalf - pieceGap), paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.downLeft) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceWidthHalf, pieceLineWidth, pieceWidthHalf - pieceGap), paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceGap, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    } else if (direction == PieceWay.downRight) {
      canvas.drawCircle(Offset(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf), pieceLineWidthHalf, paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf - pieceLineWidthHalf, piecePosY + pieceWidthHalf, pieceLineWidth, pieceWidthHalf - pieceGap), paintLine);
      canvas.drawRect(Rect.fromLTWH(piecePosX + pieceWidthHalf, piecePosY + pieceWidthHalf - pieceLineWidthHalf, pieceWidthHalf - pieceGap, pieceLineWidth), paintLine);
    }
    for (int i = 0; i < 3; i++) {
      final int pos = y * difficulty + x;
      final double rndNum1 = RandomDouble.staticNum[(i + pos) % RandomDouble.staticNum.length];
      final double rndNum2 = RandomDouble.staticNum[(i + pos + 1) % RandomDouble.staticNum.length];
      final double rndNum3 = RandomDouble.staticNum[(i + pos + 2) % RandomDouble.staticNum.length];
      final double soilX = piecePosX + pieceWidthHalf + ((rndNum1 - 0.5) * pieceWidth * 0.8);
      final double soilY = piecePosY + pieceWidthHalf + ((rndNum2 - 0.5) * pieceWidth * 0.8);
      final double soilR = (rndNum3 + 0.4) * pieceWidth * 0.03;
      canvas.drawCircle(Offset(soilX, soilY), soilR, paintLineSoil);
    }
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}