ソースコード source code

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

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

下記コードの最終ビルド日: 2023-11-15

pubspec.yaml

name: ladderlottery
description: "Ladder-style Lottery"
# 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 is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.1+2

environment:
  sdk: '>=3.3.0-48.0.dev <4.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


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6
  package_info_plus: ^4.1.0
  shared_preferences: ^2.0.17
  flutter_localizations:    #多言語ライブラリの本体    # .arbファイルを更新したら flutter gen-l10n
    sdk: flutter
  intl: ^0.18.1     #多言語やフォーマッタなどの関連ライブラリ
  google_mobile_ads: ^3.1.0
  just_audio: ^0.9.36

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.13.1    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.5     #flutter pub run flutter_native_splash:create

  # 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: ^3.0.1

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

flutter_launcher_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: '#2A01AD'
  image: 'assets/image/splash.png'
  color_dark: '#2A01AD'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#2A01AD'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#2A01AD'
    image_dark: 'assets/image/splash.png'


# The following section is specific to Flutter packages.
flutter:
  generate: true    #pub get時に多言語対応のファイルが自動生成される

  # 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
  assets:
    - assets/image/
    - assets/sound/

  # 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/ad_mob.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-02-06
///
library;

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      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();
    }
  }
  void dispose() {
    if (_isAdMob) {
      _adMobBanner.dispose();
    }
  }
  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();
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

lib/audio_play.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

import 'package:just_audio/just_audio.dart';

import 'package:ladderlottery/const_value.dart';

class AudioPlay {
  //音を重ねて連続再生できるようにインスタンスを用意しておき、順繰りに使う。
  static final List<AudioPlayer> _player01 = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _player02 = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  static final List<AudioPlayer> _player03 = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
  ];
  int _player01Ptr = 0;
  int _player02Ptr = 0;
  int _player03Ptr = 0;

  double _soundVolume = 0.0;

  //constructor
  AudioPlay() {
    constructor();
  }
  void constructor() async {
    for (int i = 0; i < _player01.length; i++) {
      await _player01[i].setVolume(0);
      await _player01[i].setAsset(ConstValue.audioSlide);
    }
    for (int i = 0; i < _player02.length; i++) {
      await _player02[i].setVolume(0);
      await _player02[i].setAsset(ConstValue.audioSet);
    }
    for (int i = 0; i < _player03.length; i++) {
      await _player03[i].setVolume(0);
      await _player03[i].setAsset(ConstValue.audioFinish);
    }
    playZero();
  }
  void dispose() {
    for (int i = 0; i < _player01.length; i++) {
      _player01[i].dispose();
    }
    for (int i = 0; i < _player02.length; i++) {
      _player02[i].dispose();
    }
    for (int i = 0; i < _player03.length; i++) {
      _player03[i].dispose();
    }
  }
  //getter
  double get soundVolume {
    return _soundVolume;
  }
  //setter
  set soundVolume(double vol) {
    _soundVolume = vol;
  }
  //最初に音が鳴らないのを回避する方法
  void playZero() async {
    AudioPlayer ap = AudioPlayer();
    await ap.setAsset(ConstValue.audioZero);
    await ap.load();
    await ap.play();
  }
  //
  void play01() async {
    if (_soundVolume == 0) {
      return;
    }
    _player01Ptr += 1;
    if (_player01Ptr >= _player01.length) {
      _player01Ptr = 0;
    }
    await _player01[_player01Ptr].setVolume(_soundVolume);
    await _player01[_player01Ptr].pause();
    await _player01[_player01Ptr].seek(Duration.zero);
    await _player01[_player01Ptr].play();
  }
  void play02() async {
    if (_soundVolume == 0) {
      return;
    }
    _player02Ptr += 1;
    if (_player02Ptr >= _player02.length) {
      _player02Ptr = 0;
    }
    await _player02[_player02Ptr].setVolume(_soundVolume);
    await _player02[_player02Ptr].pause();
    await _player02[_player02Ptr].seek(Duration.zero);
    await _player02[_player02Ptr].play();
  }
  void play03() async {
    if (_soundVolume == 0) {
      return;
    }
    _player03Ptr += 1;
    if (_player03Ptr >= _player03.length) {
      _player03Ptr = 0;
    }
    await _player03[_player03Ptr].setVolume(_soundVolume);
    await _player03[_player03Ptr].pause();
    await _player03[_player03Ptr].seek(Duration.zero);
    await _player03[_player03Ptr].play();
  }
}

lib/canvas_custom_painter.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-11-12
///
library;

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

import 'package:ladderlottery/const_value.dart';
import 'package:ladderlottery/game.dart';
import 'package:ladderlottery/game_mode.dart';

class CanvasCustomPainter extends CustomPainter {
  final Game game;
  CanvasCustomPainter({required this.game});

  @override
  void paint(Canvas canvas, Size size) async {
    const double strokeWidth = 10.0;
    //梯子の色と太さ
    final Paint paint = Paint()
      ..color = const Color.fromRGBO(150,150,230, 1)
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round
      ..blendMode = BlendMode.src;
    //アイコンサイズ
    final double iconSize = size.width * 0.08;
    //梯子の間隔
    final double gapX = size.width / (game.lotteryCount + 1);
    final double gapY = (size.height - (iconSize * 4)) / 20;
    //モードによって動作切り替え
    if (game.gameMode == GameMode.ready) {
      //梯子の上と下
      _drawHeaderFooterLine(canvas,size,paint,gapX,iconSize);
    } else if (game.gameMode == GameMode.make) {
      //梯子の縦線
      _drawVerticalLine(canvas,size,paint,gapX,iconSize);
      //梯子の横線
      _drawHorizontalLine(canvas,paint,gapX,gapY,iconSize,strokeWidth,game.ladderY);
    } else {  //GameMode.action
      //梯子の縦線
      _drawVerticalLine(canvas,size,paint,gapX,iconSize);
      //梯子の横線
      _drawHorizontalLine(canvas,paint,gapX,gapY,iconSize,strokeWidth,20);
      //アイコン
      _drawIcon(canvas,gapX,gapY,iconSize,strokeWidth);
    }
    //固定のアルファベットアイコンと数字アイコン
    _drawFixedIcon(canvas,size,iconSize);
  }
  void _drawHeaderFooterLine(Canvas canvas, Size size, Paint paint, double gapX, double iconSize) {
    for (int i = 0; i < game.lotteryCount; i++) {
      canvas.drawLine(Offset(gapX * i + gapX, iconSize), Offset(gapX * i + gapX, iconSize * 2), paint);
      canvas.drawLine(Offset(gapX * i + gapX, size.height - iconSize * 2),Offset(gapX * i + gapX, size.height - iconSize), paint);
    }
  }
  //梯子の縦線
  void _drawVerticalLine(Canvas canvas, Size size, Paint paint, double gapX, double iconSize) {
    for (int i = 0; i < game.lotteryCount; i++) {
      canvas.drawLine(Offset(gapX * i + gapX, iconSize), Offset(gapX * i + gapX, size.height - iconSize), paint);
    }
  }
  //梯子の横線
  void _drawHorizontalLine(Canvas canvas, Paint paint, double gapX, double gapY, double iconSize, double strokeWidth, int countY) {
    for (int x = 0; x < game.lotteryCount - 1; x++) {
      final double x1 = gapX * x + gapX;
      final double x2 = gapX * (x + 1) + gapX;
      for (int y = 0; y < countY; y++) {
        if (game.ladders[x][y] == 1) {
          final double y1 = gapY * y + (iconSize * 2) + (strokeWidth);
          canvas.drawLine(Offset(x1, y1), Offset(x2, y1), paint);
        }
      }
    }
  }
  void _drawIcon(Canvas canvas, double gapX, double gapY, double iconSize, double strokeWidth) {
    //アイコンのY位置
    final double y = gapY * game.iconPositionY + iconSize + (strokeWidth / 2);
    //アイコンの位置
    for (int i = 0; i < game.lotteryCount; i++) {
      //アイコンのX位置
      final double x = gapX * game.iconPositionX[i] + gapX - (iconSize / 2);
      //アイコン描画
      _imageAlphabetDraw(canvas, iconSize, x, y, game.alphabets[i]);
    }
  }
  //固定のアルファベットアイコンと数字アイコン
  void _drawFixedIcon(Canvas canvas, Size size, double iconSize) {
    for (int i = 0; i < game.lotteryCount; i++) {
      double gapX = size.width / (game.lotteryCount + 1);
      double x = gapX * i + gapX - (iconSize / 2);
      _imageAlphabetDraw(canvas, iconSize, x, iconSize * 0.2, game.alphabets[i]);
      _imageNumberDraw(canvas, iconSize, x, size.height - (iconSize * 1.2), game.numbers[i]);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; // shouldRepaintがtrueを返すと再描画されます
  }

  Future<void> _imageAlphabetDraw(Canvas canvas, double iconSize, double x, double y, int index) async {
    /* これを async/await 対応にしたもの
    final ImageStream stream = AssetImage(ConstValue.imageIconAlphabets[0]).resolve(ImageConfiguration.empty);
    stream.addListener(
      ImageStreamListener((ImageInfo info, bool synchronousCall) {
        final img = info.image;
        try {
          canvas.drawImageRect(
            img,
            Rect.fromPoints(const Offset(0, 0), Offset(img.width.toDouble(), img.height.toDouble())),
            Rect.fromPoints(const Offset(50, 50), const Offset(200, 200)),
            Paint()
          );
        } catch(e) {
          print(e);
        }
      })
    );
    */
    final Completer<void> completer = Completer<void>();
    final ImageStream stream = AssetImage(ConstValue.imageIconAlphabets[index]).resolve(ImageConfiguration.empty);
    void listener(ImageInfo info, bool synchronousCall) {
      final img = info.image;
      try {
        canvas.drawImageRect(
          img,
          Rect.fromPoints(const Offset(0, 0), Offset(img.width.toDouble(), img.height.toDouble())),
          Rect.fromPoints(Offset(x, y), Offset(x + iconSize, y + iconSize)),
          Paint(),
        );
      } catch(e) {
        //以下の警告が出る。直せないのでtry-catchで封じ込め。
        //I/flutter (14175): Bad state: A Dart object attempted to access a native peer, but the native peer has been collected (nullptr). This is usually the result of calling methods on a native-backed object when the native resources have already been disposed.
        //何もしない
      }
      // 非同期処理が完了したことを通知
      completer.complete();
    }
    // リスナーを登録
    stream.addListener(ImageStreamListener(listener));
    // 非同期処理が完了するまで待機
    await completer.future;
  }

  Future<void> _imageNumberDraw(Canvas canvas, double iconSize, double x, double y, int index) async {
    final Completer<void> completer = Completer<void>();
    final ImageStream stream = AssetImage(ConstValue.imageIconNumbers[index]).resolve(ImageConfiguration.empty);
    void listener(ImageInfo info, bool synchronousCall) {
      final img = info.image;
      try {
        canvas.drawImageRect(
          img,
          Rect.fromPoints(const Offset(0, 0), Offset(img.width.toDouble(), img.height.toDouble())),
          Rect.fromPoints(Offset(x, y), Offset(x + iconSize, y + iconSize)),
          Paint(),
        );
      } catch(e) {
        //以下の警告が出る。直せないのでtry-catchで封じ込め。
        //I/flutter (14175): Bad state: A Dart object attempted to access a native peer, but the native peer has been collected (nullptr). This is usually the result of calling methods on a native-backed object when the native resources have already been disposed.
        //何もしない
      }
      // 非同期処理が完了したことを通知
      completer.complete();
    }
    // リスナーを登録
    stream.addListener(ImageStreamListener(listener));
    // 非同期処理が完了するまで待機
    await completer.future;
  }

}

lib/const_value.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';

class ConstValue {
  //pref
  static const String prefLanguageCode = 'languageCode';
  static const String prefLotteryCount = 'lotteryCount';
  static const String prefSoundVolume = 'soundVolume';
  static const String prefBackgroundImageNumber = 'backgroundImageNumber';
  //image
  static const List<String> imageIconAlphabets = [
    'assets/image/c_a.webp',
    'assets/image/c_b.webp',
    'assets/image/c_c.webp',
    'assets/image/c_d.webp',
    'assets/image/c_e.webp',
    'assets/image/c_f.webp',
    'assets/image/c_g.webp',
    'assets/image/c_h.webp',
    'assets/image/c_i.webp',
  ];
  static const List<String> imageIconNumbers = [
    'assets/image/c_1.webp',
    'assets/image/c_2.webp',
    'assets/image/c_3.webp',
    'assets/image/c_4.webp',
    'assets/image/c_5.webp',
    'assets/image/c_6.webp',
    'assets/image/c_7.webp',
    'assets/image/c_8.webp',
    'assets/image/c_9.webp',
  ];
  static const String imageSpace1 = 'assets/image/space1.webp';
  static const List<String> imageBackGrounds = [
    'assets/image/bg1.webp',  //0 dummy
    'assets/image/bg1.webp',  //1
    'assets/image/bg2.webp',  //2
    'assets/image/bg3.webp',
    'assets/image/bg4.webp',
    'assets/image/bg5.webp',
    'assets/image/bg6.webp',
    'assets/image/bg7.webp',
    'assets/image/bg8.webp',
    'assets/image/bg9.webp',
    'assets/image/bg10.webp',
  ];
  //color
  static const Color colorButtonBack = Color.fromARGB(255, 42,1,173);
  static const Color colorButtonBackOn = Color.fromARGB(100, 42,1,173);
  static const Color colorButtonFore = Color.fromARGB(255, 255, 255, 255);
  static const Color colorHeader = Color.fromARGB(100, 42,1,173);
  static const Color colorSettingAccent = Color.fromARGB(255, 76,43,164);
  //sound
  static const String audioZero = 'assets/sound/zero.wav';    //無音1秒
  static const String audioSlide = 'assets/sound/slide.mp3';
  static const String audioSet = 'assets/sound/set.wav';
  static const String audioFinish = 'assets/sound/bigo2.wav';
  //string
  static const Map<String,String> languageCode = {
    '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': '中文',
  };

}

lib/empty.dart

lib/game.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-11-14
///
library;

import 'dart:math';

import 'package:ladderlottery/game_mode.dart';
import 'package:ladderlottery/audio_play.dart';
import 'package:ladderlottery/preferences.dart';

class Game {
  late AudioPlay _audioPlay;
  int lotteryCount = 3; //2..9
  int lastLotteryCount = 3; //記録しておき、変化が有ったら再描画
  List<int> alphabets = []; //A..Iまでで使用されるアルファベットがシャッフル
  List<int> numbers = []; //1..9までで使用される数字がシャッフル
  List<List<int>> ladders = [ //8x20 梯子の横線
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  ];
  GameMode gameMode = GameMode.ready;
  int tick = 0; //描画アニメーションのtick
  double iconPositionY = 0.0; //アイコンY移動
  List<double> iconPositionX = [0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]; //アイコンX移動
  List<int> ladderXs = [0,1,2,3,4,5,6,7,8]; //アイコンX移動したときのリファレンス
  int ladderY = -1; //アイコンY初期値

  //constructor
  Game() {
    shuffle();
  }
  void setAudioPlay(audioPlay) {
    _audioPlay = audioPlay;
  }
  //使用するアルファベットと数字をシャッフルしてゲーム進行を初期値にする
  void shuffle() {
    alphabets = [];
    numbers = [];
    for (int i = 0; i < lotteryCount; i++) {
      alphabets.add(i);
      numbers.add(i);
    }
    alphabets.shuffle();
    numbers.shuffle();
    iconPositionY = 0.0;
    iconPositionX = [0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0];
    ladderY = -1;
    ladderXs = [0,1,2,3,4,5,6,7,8];
    tick = 0;
    gameMode = GameMode.ready;
  }
  //梯子の横線を作成して開始
  void start() {
    //横線を全てクリア
    for (int x = 0; x < ladders.length; x++) {
      for (int y = 0; y < ladders[0].length; y++) {
        ladders[x][y] = 0;
      }
    }
    //横線を作成。最左は1/2の確率。他は3/4の確率
    final int seed = DateTime.now().millisecondsSinceEpoch;
    Random random = Random(seed);
    for (int x = 0; x < lotteryCount - 1; x++) {
      for (int y = 0; y < 20; y++) {
        final int rnd = (x == 0) ? random.nextInt(2) : random.nextInt(4);
        if (x == 0) {
          ladders[x][y] = (rnd == 0) ? 0 : 1;
        } else {
          if (ladders[x - 1][y] != 1) {
            ladders[x][y] = (rnd == 0) ? 0 : 1;
          }
        }
      }
    }
    //進行具合を初期化
    iconPositionY = 0.0;
    iconPositionX = [0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0];
    ladderY = -1;
    ladderXs = [0,1,2,3,4,5,6,7,8];
    tick = 0;
    gameMode = GameMode.make;
  }
  //梯子とアイコンの描画
  void periodic() {
    //準備中|終了
    if (gameMode == GameMode.ready || gameMode == GameMode.end) {
      return;
    }
    if (gameMode == GameMode.make) {
      //梯子の横線を順に描画
      ladderY = tick ~/ 20;
      if (tick % 20 == 0) {
        _audioPlay.soundVolume = Preferences.soundVolume;
        _audioPlay.play01();
      }
      if (ladderY >= 20) {
        tick = 0;
        ladderY = -1;
        gameMode = GameMode.action;
      }
    } else {  //GameMode.action
      //アイコンを動かす
      //
      if (ladderY >= 20) {
        gameMode = GameMode.end;
        _audioPlay.soundVolume = Preferences.soundVolume;
        _audioPlay.play03();
        return;
      }
      //1サイクルを60として、0-29の時はY移動、30-59の時はX移動
      if (tick % 60 < 30) {
        //0-29の時
        if (tick % 60 == 0) {
          _audioPlay.soundVolume = Preferences.soundVolume;
          _audioPlay.play01();
        }
        iconPositionY += 1 / 30; //実際の位置
        if (tick % 60 == 29) {
          ladderY += 1; //論理位置
          iconPositionY = ladderY.toDouble() + 1; //実際の位置 誤差を修正
        }
      } else {
        //30-59の時
        bool moveFlag = false;
        for (int x = 0; x < lotteryCount; x++) {
          if (ladderXs[x] < lotteryCount - 1 && ladders[ladderXs[x]][ladderY] == 1) {
            iconPositionX[x] += 1 / 30;
            moveFlag = true;
          } else if (ladderXs[x] >= 1 && ladders[ladderXs[x] - 1][ladderY] == 1) {
            iconPositionX[x] -= 1 / 30;
            moveFlag = true;
          }
        }
        if (tick % 60 == 30 && moveFlag) {
          _audioPlay.soundVolume = Preferences.soundVolume;
          _audioPlay.play02();
        }
        //数字を綺麗にする。誤差を修正
        if (tick % 60 == 59) {
          for (int x = 0; x < lotteryCount; x++) {
            iconPositionX[x] = iconPositionX[x].round().toDouble(); //実際の位置
            ladderXs[x] = iconPositionX[x].toInt(); //論理位置
          }
        }
      }
    }
    tick += 1;
  }

}

lib/game_mode.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-11-14
///
library;

enum GameMode {
  ready,  //準備中
  make,  //梯子描画中
  action,  //動作中
  end, //動作終了
}

lib/language_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

import 'package:ladderlottery/preferences.dart';

class LanguageState {

  static String _languageCode = 'en';

  //言語コードを記録
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    await Preferences.setLanguageCode(_languageCode);
  }
  //言語コードを返す
  static Future<String> getLanguageCode() async {
    _languageCode = await Preferences.getLanguageCode() ?? 'en';
    return _languageCode;
  }

}

lib/main.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-02
///
library;

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart' if (dart.library.html) 'empty.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

//自身で作成したclassを読み込む
import 'package:ladderlottery/const_value.dart';
import 'package:ladderlottery/language_state.dart';
import 'package:ladderlottery/version_state.dart';
import 'package:ladderlottery/setting.dart';
import 'package:ladderlottery/ad_mob.dart';
import 'package:ladderlottery/page_state.dart';
import 'package:ladderlottery/preferences.dart';
import 'package:ladderlottery/audio_play.dart';
import 'package:ladderlottery/canvas_custom_painter.dart';
import 'package:ladderlottery/game.dart';
import 'package:ladderlottery/game_mode.dart';

void main() {
  //MobileAds.instance.initialize(); ここに入れると進行しなかった
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {    //statefulに変更して言語変更に対応
  const MainApp({super.key});
  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  Locale localeLanguage = const Locale('en');
  @override
  Widget build(BuildContext context) {
    if (kIsWeb == false) {
      MobileAds.instance.initialize();
    }
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: AppLocalizations.localizationsDelegates,   //多言語化
      supportedLocales: AppLocalizations.supportedLocales,  //自動で言語リストを生成
      locale: localeLanguage,
      home: const MainHomePage(),
    );
  }
}

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

class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
  final AdMob _adMob = AdMob(); //広告表示
  final AudioPlay _audioPlay = AudioPlay();
  final Game _game = Game();
  late Timer _timer;
  double _screenWidth = 0;  //画面幅
  double _screenHeight = 0; //画面高さ
  double _bgImageSize = 0;  //背景画像サイズ
  double _bgImageAngle = 0; //背景画像回転角度
  int _backgroundImageNumber = 0;
  Color _buttonBackColorShuffle = ConstValue.colorButtonBack;
  Color _buttonBackColorStart = ConstValue.colorButtonBack;

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }
  //言語準備
  void _getCurrentLocale() async {
    Locale locale = Locale(await LanguageState.getLanguageCode());
    if (mounted) {  //Widgetが存在する。Widgetが存在しない時の実行によるエラーを回避する為。
      context.findAncestorStateOfType<_MainAppState>()!
        ..localeLanguage = locale
        ..setState(() {});
    }
  }
  //ページ起動開始時に一度だけ呼ばれる
  @override
  void initState() {
    super.initState();
    _getVersion();
    _getCurrentLocale();
    LanguageState.getLanguageCode();
    _adMob.load();
    _audioPlay.playZero();
    _game.setAudioPlay(_audioPlay);
    (() async {
      await Preferences.initial();
      _audioPlay.soundVolume = Preferences.soundVolume;
      _game.lotteryCount = Preferences.lotteryCount;
      _backgroundImageNumber = Preferences.backgroundImageNumber;
      _game.shuffle();
      _timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 60)), (timer) {  //速度調整はここで
        setState(() {
          _game.periodic();
          _bgImageAngle -= 0.001;
          if (_bgImageAngle < -314159265) {
            _bgImageAngle = 0;
          }
        });
      });
    })();
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    _timer.cancel();
    super.dispose();
  }
  //シャッフルボタン
  Widget _shuffleButton() {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.fromLTRB(3, 0, 0, 0),
        child: GestureDetector(
          onTap: () {
            if (_game.gameMode != GameMode.make && _game.gameMode != GameMode.action) {
              _audioPlay.soundVolume = Preferences.soundVolume;
              _audioPlay.play01();
              _game.shuffle();
              _buttonBackColorShuffle = ConstValue.colorButtonBack;
            }
          },
          onTapDown: (TapDownDetails details) {
            _buttonBackColorShuffle = ConstValue.colorButtonBackOn;
          },
          onTapUp: (TapUpDetails details) {
            _buttonBackColorShuffle = ConstValue.colorButtonBack;
          },
          child: Opacity(
            opacity: (_game.gameMode == GameMode.make || _game.gameMode == GameMode.action) ? 0.1 : 1, //動作中は薄くする
            child: Container(
              padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
              color: _buttonBackColorShuffle,
              child: Center(
                child: Text(AppLocalizations.of(context)!.shuffle,
                  style: const TextStyle(
                    color: ConstValue.colorButtonFore,
                    fontSize: 20.0,
                  )
                )
              )
            )
          )
        )
      )
    );
  }
  //抽選ボタン
  Widget _startButton() {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.fromLTRB(3,0,3,0),
        child: GestureDetector(
          onTap: () {
            if (_game.gameMode != GameMode.make && _game.gameMode != GameMode.action) {
              _audioPlay.soundVolume = Preferences.soundVolume;
              _audioPlay.play02();
              _game.start();
              _buttonBackColorStart = ConstValue.colorButtonBack;
            }
          },
          onTapDown: (TapDownDetails details) {
            _buttonBackColorStart = ConstValue.colorButtonBackOn;
          },
          onTapUp: (TapUpDetails details) {
            _buttonBackColorStart = ConstValue.colorButtonBack;
          },
          child: Opacity(
            opacity: (_game.gameMode == GameMode.make || _game.gameMode == GameMode.action) ? 0.1 : 1, //動作中は薄くする
            child: Container(
              //width: double.infinity,
              padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
              color: _buttonBackColorStart,
              child: Center(
                child: Text(AppLocalizations.of(context)!.start,
                  style: const TextStyle(
                    color: ConstValue.colorButtonFore,
                    fontSize: 20.0,
                  )
                )
              )
            )
          )
        )
      )
    );
  }
  //画面全体
  @override
  Widget build(BuildContext context) {
    _screenWidth = MediaQuery.of(context).size.width;
    _screenHeight = MediaQuery.of(context).size.height;
    _bgImageSize = max(_screenWidth,_screenHeight);
    return Container(
      decoration: _decoration(),
      child: Scaffold(
        backgroundColor: Colors.transparent,
        appBar: AppBar(
          //タイトル表示
          title: const Text('Ladder-style Lottery',
            style: TextStyle(
              color: ConstValue.colorButtonFore,
              fontSize: 15.0,
            )
          ),
          foregroundColor: const Color.fromRGBO(255,255,255,1),
          backgroundColor: ConstValue.colorHeader,
          actions: <Widget>[
            TextButton(
              onPressed: () async {
                if (_game.gameMode != GameMode.make && _game.gameMode != GameMode.action) {
                  bool? ret = await Navigator.of(context).push(
                    MaterialPageRoute<bool>(
                      builder: (context) => const SettingPage(),
                    ),
                  );
                  //awaitで呼び出しているので、settingから戻ったら以下が実行される。
                  if (ret!) { //設定で適用だった場合
                    _getCurrentLocale();
                    _game.lotteryCount = Preferences.lotteryCount;
                    if (_game.lastLotteryCount != _game.lotteryCount) {
                      _game.lastLotteryCount = _game.lotteryCount;
                      _game.shuffle();
                    }
                    _audioPlay.soundVolume = Preferences.soundVolume;
                    _backgroundImageNumber = Preferences.backgroundImageNumber;
                    setState(() {});
                  }
                }
              },
              child: Opacity(
                opacity: (_game.gameMode == GameMode.make || _game.gameMode == GameMode.action) ? 0.1 : 1, //動作中は薄くする
                child: Text(
                  AppLocalizations.of(context)!.setting,
                  style: const TextStyle(
                    color: ConstValue.colorButtonFore,
                  )
                )
              )
            )
          ]
        ),
        body: SafeArea(
          child: Stack(children:[
            _background(),
            Column(children:[
              Expanded(
                child: Column(children:[
                  CustomPaint(
                    painter: CanvasCustomPainter(game: _game),
                    size: Size(_screenWidth, _screenHeight - 280),
                  ),
                  Row(children:[
                    _shuffleButton(),
                    _startButton(),
                  ])
                ]),
              ),
              //広告
              Padding(
                padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
                child: SizedBox(
                  width: double.infinity,
                  child: _adMob.getAdBannerWidget(),
                )
              )
            ])
          ])
        )
      )
    );
  }
  Decoration _decoration() {
    if (_backgroundImageNumber == 0) {
      return const BoxDecoration(
        color: Colors.white,
      );
    } else {
      return const BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageSpace1),
          fit: BoxFit.cover,
        ),
      );
    }
  }
  Widget _background() {
    if (_backgroundImageNumber == 0) {
      return Container(
        color: Colors.white,
      );
    } else {
      return Transform.rotate(
        angle: _bgImageAngle,
        child: Transform.scale(
          scale: 2.6,
          child: Image.asset(
            ConstValue.imageBackGrounds[_backgroundImageNumber],
            width: _bgImageSize,
            height: _bgImageSize,
          ),
        ),
      );
    }
  }
}

lib/page_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-05
///
library;

//現在のページを記録。initStateでタイミングが合わない時にbuild内で一度だけ実行させるために使用。
class PageState {

  static String _currentPage = '';

  static void setCurrentPage(String str) {
    _currentPage = str;
  }

  static String getCurrentPage() {
    return _currentPage;
  }

}

lib/preferences.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-26
///
library;

import 'package:shared_preferences/shared_preferences.dart';

import 'package:ladderlottery/const_value.dart';

//デバイスに情報を保存
class Preferences {

  static bool ready = false;

  //この値は常に最新にしておく
  static String _languageCode = '';
  static int _lotteryCount = 3;
  static double _soundVolume = 1.0;
  static int _backgroundImageNumber = 0;

  static String get languageCode {
    return _languageCode;
  }
  static int get lotteryCount {
    return _lotteryCount;
  }
  static double get soundVolume {
    return _soundVolume;
  }
  static int get backgroundImageNumber {
    return _backgroundImageNumber;
  }

  static Future<void> initial() async {
    _languageCode = await getLanguageCode();
    _lotteryCount = await getLotteryCount();
    _soundVolume = await getSoundVolume();
    _backgroundImageNumber = await getBackgroundImageNumber();
    ready = true;
  }

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

  //言語コード
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefLanguageCode, str);
  }
  static Future<String> getLanguageCode() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String str = prefs.getString(ConstValue.prefLanguageCode) ?? 'en';
    return str;
  }

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

  //抽選数
  static Future<void> setLotteryCount(int num) async {
    _lotteryCount = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefLotteryCount, num);
  }
  static Future<int> getLotteryCount() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefLotteryCount) ?? 3;
    return num;
  }

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

  //効果音音量
  static Future<void> setSoundVolume(double num) async {
    _soundVolume = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefSoundVolume, num);
  }
  static Future<double> getSoundVolume() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefSoundVolume) ?? 1.0;
    return num;
  }

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

  //背景画像番号
  static Future<void> setBackgroundImageNumber(int num) async {
    _backgroundImageNumber = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefBackgroundImageNumber, num);
  }
  static Future<int> getBackgroundImageNumber() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefBackgroundImageNumber) ?? 0;
    return num;
  }

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

}

lib/setting.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:ladderlottery/const_value.dart';
import 'package:ladderlottery/preferences.dart';
import 'package:ladderlottery/language_state.dart';
import 'package:ladderlottery/version_state.dart';
import 'package:ladderlottery/ad_mob.dart';
import 'package:ladderlottery/page_state.dart';

class SettingPage extends StatefulWidget {
  const SettingPage({super.key});

  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  final AdMob _adMob = AdMob(); //広告
  //これら変数はUIへの表示や入力の為に一時的に使用される。
  String _languageKey = ''; //言語コード 'en'
  String _languageValue = '';
  int _lotteryCount = 3;
  double _soundVolume = 0.0;
  int _backgroundImageNumber = 0;

  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _adMob.load();
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    //このページが開いたときに一度だけ実行される処理を記述。initStateではタイミングが合わない為。
    if (PageState.getCurrentPage() != 'setting') {
      PageState.setCurrentPage('setting');
      (() async {
        await Preferences.initial();
        _languageKey = await LanguageState.getLanguageCode();
        _languageValue = ConstValue.languageCode[_languageKey] ?? '';
        _lotteryCount = Preferences.lotteryCount;
        _soundVolume = Preferences.soundVolume;
        _backgroundImageNumber = Preferences.backgroundImageNumber;
        setState((){});
      })();
    }
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        //設定キャンセルボタン
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false); //falseを返す
          },
        ),
        title: Text(AppLocalizations.of(context)!.setting),
        foregroundColor: const Color.fromRGBO(255,255,255,1),
        backgroundColor: ConstValue.colorSettingAccent,
        actions: [
          //設定OKボタン
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: () async {
              await LanguageState.setLanguageCode(_languageKey);
              await Preferences.setLotteryCount(_lotteryCount);
              await Preferences.setSoundVolume(_soundVolume);
              await Preferences.setBackgroundImageNumber(_backgroundImageNumber);
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);  //trueを返す
            },
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.lotteryCount,style: const TextStyle(fontSize: 16)),
                      const Spacer()
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    child: Row(children: <Widget>[
                      Text(_lotteryCount.toString()),
                      Expanded(
                        child: Slider(
                          value: _lotteryCount.toDouble(),
                          min: 2,
                          max: 9,
                          divisions: 7,
                          onChanged: (double value) {
                            setState(() {
                              _lotteryCount = value.toInt();
                            });
                          }
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.soundVolume,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    child: Row(children: <Widget>[
                      Text(_soundVolume.toString()),
                      Expanded(
                        child: Slider(
                          value: _soundVolume,
                          min: 0.0,
                          max: 1.0,
                          divisions: 10,
                          onChanged: (double value) {
                            setState(() {
                              _soundVolume = value;
                            });
                          }
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                      padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                      child: Row(children: [
                        Text(AppLocalizations.of(context)!.backgroundImageNumber,style: const TextStyle(fontSize: 16)),
                        const Spacer(),
                      ])
                  ),
                  Padding(
                      padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                      child: Row(children: <Widget>[
                        Text(_backgroundImageNumber.toString().padLeft(2, '0')),
                        Expanded(
                            child: Slider(
                                value: _backgroundImageNumber.toDouble(),
                                min: 0,
                                max: 10,
                                divisions: 10,
                                onChanged: (double value) {
                                  setState(() {
                                    _backgroundImageNumber = value.toInt();
                                  });
                                }
                            )
                        )
                      ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 0, right: 0, bottom: 0),
                    child: Row(children:[
                      const SizedBox(width:16),
                      Text(AppLocalizations.of(context)!.language,
                        style: const TextStyle(
                          fontSize: 16,
                        )
                      ),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 12, left: 0, right: 0, bottom: 18),
                    child: Table(
                      children: <TableRow>[
                        TableRow(children: <Widget>[
                          _languageTableCell(0),
                          _languageTableCell(1),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(2),
                          _languageTableCell(3),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(4),
                          _languageTableCell(5),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(6),
                          _languageTableCell(7),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(8),
                          _languageTableCell(9),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(10),
                          _languageTableCell(11),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(12),
                          _languageTableCell(13),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(14),
                          _languageTableCell(15),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(16),
                          _languageTableCell(17),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(18),
                          _languageTableCell(19),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(20),
                          _languageTableCell(21),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(22),
                          _languageTableCell(23),
                        ]),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 24),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children:[
                        Text(AppLocalizations.of(context)!.usage1),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage2),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage3),
                        const SizedBox(height:15),
                        Text(AppLocalizations.of(context)!.usage4),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
                    child: SizedBox(
                      child: Text('version  ${VersionState.versionLoad()}',
                        style: const TextStyle(
                          fontSize: 10,
                        ),
                      ),
                    ),
                  ),
                ]),
              ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
          child: SizedBox(
            width: double.infinity,
            child: _adMob.getAdBannerWidget(),
          ),
        ),
      ]),
    );
  }
  //UIの仕切り用ボーダーライン
  Widget _border() {
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }
  //言語一覧表示
  TableCell _languageTableCell(int index) {
    return TableCell(
      child: RadioListTile(
        visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity,vertical: VisualDensity.minimumDensity),
        contentPadding: EdgeInsets.zero,
        title: Text(ConstValue.languageCode.values.elementAt(index)),
        value: ConstValue.languageCode.values.elementAt(index),
        groupValue: _languageValue,
        onChanged: (String? value) {
          setState(() {
            _languageValue = value ?? '';
            _languageKey = ConstValue.languageCode.keys.elementAt(index);
          });
        },
      ),
    );
  }

}

lib/version_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

class VersionState {

  static String _version = '';

  //バージョンを記録
  static void versionSave(String str) {
    _version = str;
  }
  //バージョンを返す
  static String versionLoad() {
    return _version;
  }

}

lib/l10n/app_bg.arb

{
	"@@locale":"bg",
	"@locale": {
		"description": "ブルガリア"
	},
    "setting": "Настройка",
    "shuffle": "Разбъркайте",
    "start": "Започнете",
    "lotteryCount": "Броене на лотарията",
    "soundVolume": "Сила на звука на звуковите ефекти",
    "backgroundImageNumber": "Номер на фоновото изображение",
    "language": "език",
    "usage1": "Това е Amidakuji (лотария в стил стълба).",
    "usage2": "Тя ще бъде рандомизирана чрез бутона за разбъркване. Тегленето ще се проведе от бутона за стартиране.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_cs.arb

{
	"@@locale":"cs",
	"@locale": {
		"description": "チェコ"
	},
    "setting": "Nastavení",
    "shuffle": "Zamíchat",
    "start": "Start",
    "lotteryCount": "Počítání loterie",
    "soundVolume": "Hlasitost zvukových efektů",
    "backgroundImageNumber": "Číslo obrázku na pozadí",
    "language": "Jazyk",
    "usage1": "Toto je Amidakuji (loterie ve stylu žebříku).",
    "usage2": "Bude náhodně losováno pomocí tlačítka Zamíchat.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_da.arb

{
	"@@locale":"da",
	"@locale": {
		"description": "デンマーク"
	},
    "setting": "Indstilling",
    "shuffle": "Bland",
    "start": "Start",
    "lotteryCount": "Lotteri tæller",
    "soundVolume": "Lydstyrke for lydeffekter",
    "backgroundImageNumber": "Baggrundsbilledets nummer",
    "language": "Sprog",
    "usage1": "Dette er et Amidakuji (lotteri i stigestil).",
    "usage2": "Det vil blive tilfældigt ved hjælp af bland-knappen. Lodtrækningen vil blive gennemført med start-knappen.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_de.arb

{
	"@@locale":"de",
	"@locale": {
		"description": "ドイツ"
	},
    "setting": "Einstellung",
    "shuffle": "Mischen",
    "start": "Start",
    "lotteryCount": "Lotteriezählung",
    "soundVolume": "Lautstärke der Soundeffekte",
    "backgroundImageNumber": "Hintergrundbildnummer",
    "language": "Sprache",
    "usage1": "Dies ist eine Amidakuji-Lotterie (Leiter-Lotterie).",
    "usage2": "Die Zufallsauswahl erfolgt über die Zufallstaste. Die Auslosung erfolgt über die Starttaste.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_el.arb

{
	"@@locale":"el",
	"@locale": {
		"description": "ギリシャ"
	},
    "setting": "Σύνθεση",
    "shuffle": "Ανάμιξη",
    "start": "Αρχή",
    "lotteryCount": "Καταμέτρηση λαχείων",
    "soundVolume": "Ένταση ήχου εφέ",
    "backgroundImageNumber": "Αριθμός εικόνας φόντου",
    "language": "Γλώσσα",
    "usage1": "Πρόκειται για ένα Amidakuji (λαχείο τύπου Ladder).",
    "usage2": "Θα γίνει τυχαία από το κουμπί τυχαίας αναπαραγωγής. Η κλήρωση θα γίνει με το κουμπί έναρξης.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_en.arb

{
	"@@locale":"en",
	"@locale": {
		"description": "英語"
	},
	"setting": "Setting",
	"shuffle": "Shuffle",
	"start": "Start",
	"lotteryCount": "Lottery count",
	"soundVolume": "Sound effects volume",
	"backgroundImageNumber": "Background image",
	"language": "Language",
	"usage1": "This is an Amidakuji (Ladder-style lottery).",
	"usage2": "It will be randomized by the shuffle button. The draw will be conducted by the start button.",
	"usage3": "",
	"usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_es.arb

{
	"@@locale":"es",
	"@locale": {
		"description": "スペイン"
	},
    "setting": "Configuración",
    "shuffle": "Barajar",
    "start": "Comenzar",
    "lotteryCount": "recuento de lotería",
    "soundVolume": "Volumen de efectos de sonido",
    "backgroundImageNumber": "Número de imagen de fondo",
    "language": "Idioma",
    "usage1": "Esta es una Amidakuji (lotería estilo escalera).",
    "usage2": "Se aleatorizará mediante el botón de barajar y el sorteo se realizará mediante el botón de inicio.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_et.arb

{
	"@@locale":"et",
	"@locale": {
		"description": "エストニア"
	},
    "setting": "Seadistamine",
    "shuffle": "Segamine",
    "start": "Alusta",
    "lotteryCount": "Loterii arv",
    "soundVolume": "Heliefektide helitugevus",
    "backgroundImageNumber": "Taustapildi number",
    "language": "Keel",
    "usage1": "See on Amidakuji (redeli stiilis loterii).",
    "usage2": "See muudetakse juhuslikult segamisnupu abil. Loosimine toimub stardinupu abil.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_fi.arb

{
	"@@locale":"fi",
	"@locale": {
		"description": "フィンランド"
	},
    "setting": "Asetus",
    "shuffle": "Sekoita",
    "start": "alkaa",
    "lotteryCount": "Lottolasku",
    "soundVolume": "Äänitehosteiden äänenvoimakkuus",
    "backgroundImageNumber": "Taustakuvan numero",
    "language": "Kieli",
    "usage1": "Tämä on Amidakuji (tikkaatyylinen lotto).",
    "usage2": "Se satunnaistetaan sekoituspainikkeella.Arvonta suoritetaan aloituspainikkeella.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_fr.arb

{
	"@@locale":"fr",
	"@locale": {
		"description": "フランス"
	},
    "setting": "Paramètre",
    "shuffle": "Mélanger",
    "start": "Commencer",
    "lotteryCount": "Compte de loterie",
    "soundVolume": "Volume des effets sonores",
    "backgroundImageNumber": "Numéro de l'image d'arrière-plan",
    "language": "Langue",
    "usage1": "Il s'agit d'une loterie Amidakuji (style échelle).",
    "usage2": "Il sera randomisé par le bouton de lecture aléatoire. Le tirage au sort sera effectué par le bouton de démarrage.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_hu.arb

{
	"@@locale":"hu",
	"@locale": {
		"description": "ハンガリー"
	},
    "setting": "Beállítás",
    "shuffle": "Keverés",
    "start": "Rajt",
    "lotteryCount": "Lottószám",
    "soundVolume": "Hangeffektusok hangereje",
    "backgroundImageNumber": "Háttérkép száma",
    "language": "Nyelv",
    "usage1": "Ez egy Amidakuji (létra stílusú lottó).",
    "usage2": "A sorsolást a keverés gombbal véletlenszerűen, a start gombbal végezzük.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_it.arb

{
	"@@locale":"it",
	"@locale": {
		"description": "イタリア"
	},
    "setting": "Collocamento",
    "shuffle": "Mescola",
    "start": "Inizio",
    "lotteryCount": "Conteggio della lotteria",
    "soundVolume": "Volume degli effetti sonori",
    "backgroundImageNumber": "Numero dell'immagine di sfondo",
    "language": "Lingua",
    "usage1": "Questa è una Amidakuji (lotteria in stile Ladder).",
    "usage2": "L'estrazione verrà effettuata in modo casuale premendo il pulsante di riproduzione casuale, mentre l'estrazione verrà effettuata premendo il pulsante di avvio.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_ja.arb

{
	"@@locale":"ja",
	"@locale": {
		"description": "日本"
	},
	"setting": "設定",
    "shuffle": "シャッフル",
    "start": "スタート",
	"lotteryCount": "抽選数",
	"soundVolume": "効果音量",
	"backgroundImageNumber": "背景画像",
	"language": "言語",
	"usage1": "あみだくじです。",
	"usage2": "シャッフルボタンによりランダム化されます。スタートボタンにより抽選されます。",
	"usage3": "",
	"usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_lt.arb

{
	"@@locale":"lt",
	"@locale": {
		"description": "リトアニア"
	},
    "setting": "Nustatymas",
    "shuffle": "Maišyti",
    "start": "Pradėti",
    "lotteryCount": "Loterijų skaičiavimas",
    "soundVolume": "Garso efektų garsumas",
    "backgroundImageNumber": "Fono vaizdo numeris",
    "language": "Kalba",
    "usage1": "Tai Amidakuji (kopėčių tipo loterija).",
    "usage2": "Jis bus atsitiktinai suskirstytas maišymo mygtuku, o burtų traukimas bus vykdomas pradžios mygtuku.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_lv.arb

{
	"@@locale":"lv",
	"@locale": {
		"description": "ラトビア"
	},
    "setting": "Iestatījums",
    "shuffle": "Jaukt",
    "start": "Sākt",
    "lotteryCount": "Loterijas skaitīšana",
    "soundVolume": "Skaņas efektu skaļums",
    "backgroundImageNumber": "Fona attēla numurs",
    "language": "Valoda",
    "usage1": "Šī ir Amidakuji (kāpņu stila loterija).",
    "usage2": "Tas tiks nejauši izvēlēts ar jaukšanas pogu. Izloze tiks veikta ar sākuma pogu.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_nl.arb

{
	"@@locale":"nl",
	"@locale": {
		"description": "オランダ"
	},
    "setting": "Instelling",
    "shuffle": "Schudden",
    "start": "Begin",
    "lotteryCount": "Loterij tellen",
    "soundVolume": "Volume geluidseffecten",
    "backgroundImageNumber": "Nummer achtergrondafbeelding",
    "language": "Taal",
    "usage1": "Dit is een Amidakuji (loterij in ladderstijl).",
    "usage2": "De loting wordt willekeurig gemaakt via de shuffle-knop en de trekking vindt plaats via de startknop.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_pl.arb

{
	"@@locale":"pl",
	"@locale": {
		"description": "ポーランド"
	},
    "setting": "Ustawienie",
    "shuffle": "Człapać",
    "start": "Początek",
    "lotteryCount": "Liczba loterii",
    "soundVolume": "Głośność efektów dźwiękowych",
    "backgroundImageNumber": "Numer obrazu tła",
    "language": "Język",
    "usage1": "To jest Amidakuji (loteria drabinkowa).",
    "usage2": "Losowanie odbędzie się za pomocą przycisku losowania. Losowanie zostanie przeprowadzone za pomocą przycisku start.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_pt.arb

{
	"@@locale":"pt",
	"@locale": {
		"description": "ポルトガル"
	},
    "setting": "Contexto",
    "shuffle": "Embaralhar",
    "start": "Começar",
    "lotteryCount": "Contagem de loteria",
    "soundVolume": "Volume dos efeitos sonoros",
    "backgroundImageNumber": "Número da imagem de fundo",
    "language": "Linguagem",
    "usage1": "Esta é uma Amidakuji (loteria estilo escada).",
    "usage2": "Será randomizado pelo botão shuffle. O sorteio será realizado pelo botão iniciar.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_ro.arb

{
	"@@locale":"ro",
	"@locale": {
		"description": "ルーマニア"
	},
    "setting": "Setare",
    "shuffle": "Amesteca",
    "start": "start",
    "lotteryCount": "Numărarea loteriei",
    "soundVolume": "Volumul efectelor sonore",
    "backgroundImageNumber": "Numărul imaginii de fundal",
    "language": "Limba",
    "usage1": "Acesta este un Amidakuji (loterie în stil scară).",
    "usage2": "Acesta va fi randomizat de butonul de amestecare. Extragerea va fi efectuată de butonul de pornire.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_ru.arb

{
	"@@locale":"ru",
	"@locale": {
		"description": "ロシア"
	},
    "setting": "Параметр",
    "shuffle": "Перетасовать",
    "start": "Начинать",
    "lotteryCount": "Подсчет лотереи",
    "soundVolume": "Громкость звуковых эффектов",
    "backgroundImageNumber": "Номер фонового изображения",
    "language": "Язык",
    "usage1": "Это Amidakuji (лестничная лотерея).",
    "usage2": "Он будет рандомизирован с помощью кнопки перемешивания. Розыгрыш будет проводиться с помощью кнопки «Старт».",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_sk.arb

{
	"@@locale":"sk",
	"@locale": {
		"description": "スロバキア"
	},
    "setting": "Nastavenie",
    "shuffle": "Zamiešať",
    "start": "Štart",
    "lotteryCount": "Počet lotérií",
    "soundVolume": "Hlasitosť zvukových efektov",
    "backgroundImageNumber": "Číslo obrázka na pozadí",
    "language": "Jazyk",
    "usage1": "Toto je Amidakuji (lotéria v štýle rebríčka).",
    "usage2": "Bude náhodne vybrané tlačidlom zamiešať. Žrebovanie sa uskutoční pomocou tlačidla Štart.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_sv.arb

{
	"@@locale":"sv",
	"@locale": {
		"description": "スウェーデン"
	},
    "setting": "Miljö",
    "shuffle": "Blanda",
    "start": "Start",
    "lotteryCount": "Antal lotterier",
    "soundVolume": "Ljudeffektvolym",
    "backgroundImageNumber": "Bakgrundsbildsnummer",
    "language": "Språk",
    "usage1": "Detta är ett Amidakuji (lotteri i stegliknande stil).",
    "usage2": "Det kommer att slumpas med shuffle-knappen. Dragningen kommer att genomföras med startknappen.",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_th.arb

{
	"@@locale":"th",
	"@locale": {
		"description": "タイ"
	},
    "setting": "การตั้งค่า",
    "shuffle": "สับเปลี่ยน",
    "start": "เริ่ม",
    "lotteryCount": "การนับลอตเตอรี",
    "soundVolume": "ปริมาณเอฟเฟกต์เสียง",
    "backgroundImageNumber": "หมายเลขภาพพื้นหลัง",
    "language": "ภาษา",
    "usage1": "นี่คือ Amidakuji (ลอตเตอรีแบบบันได)",
    "usage2": "จะถูกสุ่มโดยปุ่ม shuffle การจับรางวัลจะดำเนินการโดยปุ่มเริ่มต้น",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}

lib/l10n/app_zh.arb

{
	"@@locale":"zh",
	"@locale": {
		"description": "中国"
	},
    "setting": "环境",
    "shuffle": "随机播放",
    "start": "开始",
    "lotteryCount": "彩票计数",
    "soundVolume": "音效音量",
    "backgroundImageNumber": "背景图片数量",
    "language": "语言",
    "usage1": "这是阿弥陀寺(阶梯式彩票)。",
    "usage2": "通过随机播放按钮进行随机抽奖,通过开始按钮进行抽奖。",
    "usage3": "",
    "usage4": "",

	"dummy": "dummy"
}