├── .gitignore ├── .metadata ├── .travis.yml ├── README.md ├── android ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── peacepan │ │ │ │ └── flutter_minesweeper │ │ │ │ └── MainActivity.java │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── peacepan │ │ │ │ └── flutter_play_a_game │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── key.properties.enc ├── settings.gradle └── store.jks.enc ├── assets └── tetris │ ├── gameover.mp3 │ └── theme.mp3 ├── docs ├── index.html └── main.dart.js ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── configs.dart ├── frame_scheduler.dart ├── games │ ├── mineaweeper │ │ └── minesweeper.dart │ ├── tetris │ │ ├── game_pad.dart │ │ ├── sound.dart │ │ ├── tetris.dart │ │ ├── tetris_data.dart │ │ └── tetris_renderder.dart │ └── tic_tac_toe │ │ └── tic_tac_toe.dart ├── layout.dart ├── main.dart ├── screens │ ├── home.dart │ └── settings.dart ├── utlis.dart └── widgets │ ├── bonuce_icon.dart │ ├── loading.dart │ └── swipeable.dart ├── pubspec.lock ├── pubspec.yaml ├── test └── minesweeper_test.dart └── web ├── .gitignore ├── analysis_options.yaml ├── lib ├── configs.dart ├── frame_scheduler.dart ├── games │ ├── mineaweeper │ │ └── minesweeper.dart │ ├── tetris │ │ ├── game_pad.dart │ │ ├── tetris.dart │ │ ├── tetris_data.dart │ │ └── tetris_renderder.dart │ └── tic_tac_toe │ │ └── tic_tac_toe.dart ├── layout.dart ├── main.dart ├── screens │ ├── home.dart │ └── settings.dart ├── utlis.dart └── widgets │ ├── bonuce_icon.dart │ ├── loading.dart │ └── swipeable.dart ├── pubspec.yaml └── web ├── assets └── FontManifest.json ├── index.html └── main.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/key.properties 38 | **/android/store.jks 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | git: 5 | depth: false 6 | cache: 7 | bundler: true 8 | directories: 9 | # flutter usage 10 | - "$HOME/.pub-cache" 11 | jobs: 12 | include: 13 | - stage: test 14 | os: linux 15 | language: generic 16 | sudo: false 17 | # 設置 flutter 環境 18 | addons: 19 | apt: 20 | sources: 21 | - ubuntu-toolchain-r-test 22 | packages: 23 | - libstdc++6 24 | - fonts-droid 25 | # 設置 flutter 環境 26 | before_script: 27 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1 28 | script: 29 | # 執行 flutter 測試 30 | - "./flutter/bin/flutter test" 31 | - stage: deploy_apk 32 | os: linux 33 | language: android 34 | # 安装 android sdk 35 | licenses: 36 | - android-sdk-preview-license-.+ 37 | - android-sdk-license-.+ 38 | - google-gdk-license-.+ 39 | android: 40 | components: 41 | - tools 42 | - platform-tools 43 | - build-tools-28.0.3 44 | - android-28 45 | - sys-img-armeabi-v7a-google_apis-25 46 | - extra-android-m2repository 47 | - extra-google-m2repository 48 | - extra-google-android-support 49 | jdk: oraclejdk8 50 | sudo: false 51 | # 安装 android sdk 52 | # =========== 53 | # 設置 flutter 環境 54 | env: APK_OUTPUT=build/app/outputs/apk/release/app-release.apk 55 | addons: 56 | apt: 57 | sources: 58 | - ubuntu-toolchain-r-test 59 | packages: 60 | - libstdc++6 61 | - fonts-droid 62 | # 設置 flutter 環境 63 | before_script: 64 | # 解碼 keystore 65 | - openssl enc -aes-256-cbc -d -k $ANDROID_ENCRYPTED_KEY -in android/key.properties.enc -out android/key.properties 66 | - openssl enc -aes-256-cbc -d -k $ANDROID_ENCRYPTED_KEY -in android/store.jks.enc -out android/store.jks 67 | # 安裝 flutter 命令列執行檔 68 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1 69 | script: 70 | - "./flutter/bin/flutter upgrade" 71 | - "./flutter/bin/flutter -v build apk --release" 72 | # deploy: 73 | # provider: releases 74 | # skip_cleanup: true 75 | # # 用你的 api_key 替代 76 | # api_key: 77 | # secure: uDRE0d3gZ5JYhl/jBiDp5z... 78 | # file: $APK_OUTPUT 79 | # on: 80 | # tags: true 81 | - stage: deploy_ipa 82 | os: osx 83 | language: objective-c 84 | osx_image: xcode10.2 85 | before_script: 86 | # ===== 安裝 xcode ===== 87 | - pip2 install six 88 | - brew update 89 | - brew install libimobiledevice 90 | - brew install ideviceinstaller 91 | - brew install ios-deploy 92 | # ===== 安裝 xcode ===== 93 | # 安裝 flutter 命令列執行檔 94 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1 95 | # 檢查 flutter 編譯環境 96 | - "./flutter/bin/flutter doctor -v" 97 | # 抓取使用到的 flutter 套件 98 | - "./flutter/bin/flutter packages get" 99 | script: 100 | - "./flutter/bin/flutter upgrade" 101 | - gem install cocoapods 102 | - pod setup 103 | - "./flutter/bin/flutter -v build ios --release --no-codesign" 104 | - mkdir Runner 105 | - mkdir Runner/Payload 106 | - cp -r build/ios/iphoneos/Runner.app Runner/Payload/Runner.app 107 | - cd Runner 108 | - zip -r Runner.ipa Payload 109 | # deploy: 110 | # provider: releases 111 | # skip_cleanup: true 112 | # # 跟 android 的 api_key 一致 113 | # api_key: 114 | # secure: uDRE0d3gZ5JYhl/jBiDp5zv32fH... 115 | # file: Runner.ipa 116 | # on: 117 | # tags: true 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 用 Flutter 寫個遊戲 2 | 3 | - 踩地雷 4 | - 井字遊戲 5 | - 俄羅斯方塊 6 | 7 | [Web Demo](https://peacepan.github.io/flutter_play_a_game/) -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | def keystoreProperties = new Properties() 28 | def keystorePropertiesFile = rootProject.file('key.properties') 29 | if (keystorePropertiesFile.exists()) { 30 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 31 | } 32 | 33 | android { 34 | compileSdkVersion 28 35 | 36 | lintOptions { 37 | disable 'InvalidPackage' 38 | } 39 | 40 | defaultConfig { 41 | applicationId "com.peacepan.flutter_play_a_game" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 47 | } 48 | signingConfigs { 49 | release { 50 | keyAlias keystoreProperties['keyAlias'] 51 | keyPassword keystoreProperties['keyPassword'] 52 | storeFile file("${rootDir}/store.jks") 53 | storePassword keystoreProperties['storePassword'] 54 | } 55 | } 56 | buildTypes { 57 | release { 58 | signingConfig signingConfigs.release 59 | minifyEnabled true 60 | useProguard true 61 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 62 | // https://github.com/flutter/flutter/issues/28979#issuecomment-476426976 63 | dependencies { 64 | implementation 'com.android.support:support-fragment:28.0.0' 65 | } 66 | } 67 | } 68 | } 69 | 70 | flutter { 71 | source '../..' 72 | } 73 | 74 | dependencies { 75 | testImplementation 'junit:junit:4.12' 76 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 77 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 78 | } 79 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Flutter wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/peacepan/flutter_minesweeper/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.peacepan.flutter_play_a_game; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/peacepan/flutter_play_a_game/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.peacepan.flutter_play_a_game 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/key.properties.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/key.properties.enc -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /android/store.jks.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/android/store.jks.enc -------------------------------------------------------------------------------- /assets/tetris/gameover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/assets/tetris/gameover.mp3 -------------------------------------------------------------------------------- /assets/tetris/theme.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/assets/tetris/theme.mp3 -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | 38 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 39 | # referring to absolute paths on developers' machines. 40 | system('rm -rf .symlinks') 41 | system('mkdir -p .symlinks/plugins') 42 | 43 | # Flutter Pods 44 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 45 | if generated_xcode_build_settings.empty? 46 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 47 | end 48 | generated_xcode_build_settings.map { |p| 49 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 50 | symlink = File.join('.symlinks', 'flutter') 51 | File.symlink(File.dirname(p[:path]), symlink) 52 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 53 | end 54 | } 55 | 56 | # Plugin Pods 57 | plugin_pods = parse_KV_file('../.flutter-plugins') 58 | plugin_pods.map { |p| 59 | symlink = File.join('.symlinks', 'plugins', p[:name]) 60 | File.symlink(p[:path], symlink) 61 | pod p[:name], :path => File.join(symlink, 'ios') 62 | } 63 | end 64 | 65 | post_install do |installer| 66 | installer.pods_project.targets.each do |target| 67 | target.build_configurations.each do |config| 68 | config.build_settings['ENABLE_BITCODE'] = 'NO' 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audioplayer (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - path_provider (0.0.1): 6 | - Flutter 7 | - shared_preferences (0.0.1): 8 | - Flutter 9 | 10 | DEPENDENCIES: 11 | - audioplayer (from `.symlinks/plugins/audioplayer/ios`) 12 | - Flutter (from `.symlinks/flutter/ios`) 13 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 14 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 15 | 16 | EXTERNAL SOURCES: 17 | audioplayer: 18 | :path: ".symlinks/plugins/audioplayer/ios" 19 | Flutter: 20 | :path: ".symlinks/flutter/ios" 21 | path_provider: 22 | :path: ".symlinks/plugins/path_provider/ios" 23 | shared_preferences: 24 | :path: ".symlinks/plugins/shared_preferences/ios" 25 | 26 | SPEC CHECKSUMS: 27 | audioplayer: f4462b84216b9c55f02bbbdc7ab60eec7427b2d4 28 | Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a 29 | path_provider: f96fff6166a8867510d2c25fdcc346327cc4b259 30 | shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523 31 | 32 | PODFILE CHECKSUM: ebd43b443038e611b86ede96e613bd6033c49497 33 | 34 | COCOAPODS: 1.7.1 35 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeacePan/flutter_play_a_game/e68e18de4a411a423ef41ce3d2730a59713c1995/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | 玩個遊戲 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_play_a_game 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /lib/configs.dart: -------------------------------------------------------------------------------- 1 | /// 難度的型別宣告 2 | enum Level { 3 | easy, 4 | medium, 5 | difficult 6 | } 7 | /// 對應難度的顯示文字 8 | const LevelText = { 9 | Level.easy: '簡單', 10 | Level.medium: '中等', 11 | Level.difficult: '困難', 12 | }; 13 | /// 遊戲設定 14 | class GameConfigs { 15 | Level mineweeperLevel; 16 | GameConfigs({ 17 | this.mineweeperLevel, 18 | }); 19 | } -------------------------------------------------------------------------------- /lib/frame_scheduler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | 4 | /// 向系統註冊監聽幀渲染的回調 5 | /// 由於 [addPersistentFrameCallback] 註冊後無法解除 6 | /// 因此只會執行一次 7 | void registerFrameScheduler() { 8 | if (FrameScheduler.isRegistered) { 9 | print('FrameScheduler is registered.'); 10 | return; 11 | } 12 | FrameScheduler.isRegistered = true; 13 | FrameScheduler.scheduler = SchedulerBinding.instance; 14 | FrameScheduler.scheduler.addPersistentFrameCallback(FrameScheduler.frameCallback); 15 | print('FrameScheduler is registered.'); 16 | } 17 | /// 提供新增或解除監聽幀的類別 18 | class FrameScheduler { 19 | /// 是否已向系統註冊全域的幀監聽回調 20 | static bool isRegistered = false; 21 | /// 系統的幀排程實體 22 | static SchedulerBinding scheduler; 23 | /// 自行定義的幀監聽器 24 | static Map frameListeners = Map(); 25 | /// 掛載到全域的幀監聽回調 26 | static FrameCallback frameCallback = (Duration timestamp) { 27 | if (FrameScheduler.frameListeners.length == 0) return; 28 | FrameScheduler.frameListeners.values.forEach((listener) { listener(timestamp); }); 29 | // 請 UI 層繼續執行渲染 30 | FrameScheduler.scheduler.scheduleFrame(); 31 | }; 32 | /// 新增一筆幀監聽回調,回傳該回調的鍵值 33 | static Key addFrameListener(FrameCallback frameListener) { 34 | Key listenerKey = UniqueKey(); 35 | FrameScheduler.frameListeners[listenerKey] = frameListener; 36 | return listenerKey; 37 | } 38 | /// 輸入回調的鍵值,移除該幀監聽回調 39 | static bool removeFrameListener(Key listenerKey) { 40 | bool isContainsKey = FrameScheduler.frameListeners.containsKey(listenerKey); 41 | if (isContainsKey) FrameScheduler.frameListeners.remove(listenerKey); 42 | return isContainsKey; 43 | } 44 | /// 移除所有的幀監聽回調 45 | static void removeAllFrameListeners() { 46 | FrameScheduler.frameListeners.clear(); 47 | } 48 | } -------------------------------------------------------------------------------- /lib/games/mineaweeper/minesweeper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'package:flutter/material.dart'; 4 | import '../../configs.dart'; 5 | import '../../main.dart'; 6 | import '../../utlis.dart'; 7 | import '../../widgets/bonuce_icon.dart'; 8 | 9 | const int ROWS = 14; 10 | const int COLUMNS = 12; 11 | const int TOTAL_GRIDS = ROWS * COLUMNS; 12 | /// 周圍炸彈數的數字顏色 13 | const BOMB_COLORS = [ 14 | Colors.transparent, 15 | Colors.blue, 16 | Colors.green, 17 | Colors.red, 18 | Colors.indigo, 19 | Colors.pink, 20 | Colors.lightGreen, 21 | Colors.grey, 22 | Colors.black, 23 | ]; 24 | /// 1 秒鐘的定義 25 | const ONE_SEC = const Duration(seconds: 1); 26 | /// 隨機產生器種子 27 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 28 | 29 | class Minesweeper extends StatefulWidget { 30 | Minesweeper({ Key key, }) : super(key: key); 31 | @override 32 | _MinesweeperState createState() => _MinesweeperState(); 33 | } 34 | 35 | class _MinesweeperState extends State { 36 | /// 炸彈旗標設置 (座標對應布林值) 37 | final Map> flags = Map(); 38 | /// 每個格子單位 39 | final List> grids = List(); 40 | /// 全部的炸彈數量 41 | int _totalBombs; 42 | /// 是否踩到炸彈遊戲結束 43 | bool _isGameOver = false; 44 | /// 所有格子全搜尋,設置旗子數等於所有炸彈數 45 | bool _isGameWon = false; 46 | /// 計數遊戲時間 47 | Timer _timer; 48 | /// 一場遊戲開始時間 49 | DateTime gameStart; 50 | /// 一場遊戲結束時間 51 | DateTime gameEnd; 52 | /// 取得目前設置的旗子數 53 | int get flagCount { 54 | int _flagCount = 0; 55 | flags.forEach((index, row) { 56 | _flagCount += row.entries.length; 57 | }); 58 | return _flagCount; 59 | } 60 | void resetGrids(bool isInit) { 61 | flags.clear(); 62 | grids.clear(); 63 | for (int r = 0; r < ROWS; r++) { 64 | grids.add(List.generate(COLUMNS, (x) => GameGrid(isSearched: isInit))); 65 | } 66 | } 67 | /// 建立新遊戲 68 | void createGame() { 69 | final Level level = App.of(context).configs.mineweeperLevel; 70 | int bombAmount; 71 | if (level == Level.difficult) { 72 | bombAmount = 73 | (TOTAL_GRIDS * 0.5).round() + 74 | randomGenerator.nextInt(10) - 75 | randomGenerator.nextInt(10); 76 | } else if (level == Level.medium) { 77 | bombAmount = 78 | (TOTAL_GRIDS * 0.25).round() + 79 | randomGenerator.nextInt(10) - 80 | randomGenerator.nextInt(10); 81 | } else { 82 | bombAmount = 83 | (TOTAL_GRIDS * 0.1).round() + 84 | randomGenerator.nextInt(10) - 85 | randomGenerator.nextInt(10); 86 | } 87 | bombAmount = min(bombAmount, TOTAL_GRIDS); 88 | int total = bombAmount; 89 | resetGrids(false); 90 | while (bombAmount > 0) { 91 | final rY = randomGenerator.nextInt(ROWS); 92 | final rX = randomGenerator.nextInt(COLUMNS); 93 | if (!grids[rY][rX].hasBomb) { 94 | grids[rY][rX].hasBomb = true; 95 | bombAmount--; 96 | } 97 | } 98 | setState(() { 99 | _totalBombs = total; 100 | _isGameOver = _isGameWon = false; 101 | _timer?.cancel(); 102 | _timer = Timer.periodic(ONE_SEC, (Timer timer) { 103 | if (_isGameOver || _isGameWon) { 104 | timer.cancel(); 105 | return; 106 | } 107 | setState(() { 108 | gameEnd = DateTime.now(); 109 | }); 110 | }, 111 | ); 112 | gameStart = gameEnd = DateTime.now(); 113 | }); 114 | } 115 | /// 搜尋周圍 8 格,若目標周圍的炸彈數為 0 116 | /// 則遞迴搜尋周圍 8 格 117 | void searchBomb(int x, int y) { 118 | if (x < 0 || y < 0 || x >= COLUMNS || y >= ROWS) return; 119 | if (grids[y][x].isSearched) return; 120 | int bombs = 0; 121 | for (int dy = -1; dy <= 1; dy++) { 122 | for (int dx = -1; dx <= 1; dx++) { 123 | if (dy == 0 && dx == 0) continue; 124 | int ay = y + dy; 125 | int ax = x + dx; 126 | if (ax < 0 || ay < 0 || ax >= COLUMNS || ay >= ROWS) continue; 127 | final grid = grids[ay][ax]; 128 | if (grid.hasBomb) { 129 | bombs += 1; 130 | } 131 | } 132 | } 133 | grids[y][x].aroundBombs = bombs; 134 | grids[y][x].isSearched = true; 135 | if (bombs == 0) { 136 | for (int dy = -1; dy <= 1; dy++) { 137 | for (int dx = -1; dx <= 1; dx++) { 138 | if (dy == 0 && dx == 0) continue; 139 | int ay = y + dy; 140 | int ax = x + dx; 141 | if (ax < 0 || ay < 0 || ax >= COLUMNS || ay >= ROWS) continue; 142 | searchBomb(ax, ay); 143 | } 144 | } 145 | } 146 | } 147 | /// 檢查目前狀態是否已經獲勝 148 | /// 設置旗子數等於所有炸彈數且所有格子全已搜尋 149 | bool checkWin() { 150 | if (_totalBombs != flagCount) { 151 | return false; 152 | } 153 | int searchedCount = 0; 154 | grids.forEach((row) { 155 | row.forEach((col) { 156 | if (col.isSearched) searchedCount++; 157 | }); 158 | }); 159 | return searchedCount + flagCount == TOTAL_GRIDS; 160 | } 161 | @override 162 | void initState() { 163 | super.initState(); 164 | resetGrids(true); 165 | } 166 | @override 167 | void dispose() { 168 | _timer?.cancel(); 169 | super.dispose(); 170 | } 171 | @override 172 | Widget build(BuildContext context) { 173 | String timeString = '00:00'; 174 | int remainBombs = 0; 175 | if (gameStart != null && gameEnd != null) { 176 | Duration gameTime = gameEnd.difference(gameStart); 177 | int minutes = gameTime.inMinutes; 178 | int seconds = (gameTime.inSeconds - (minutes * 60)); 179 | timeString = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; 180 | remainBombs = _totalBombs - flagCount; 181 | } 182 | return ListView( 183 | children: [ 184 | Row( 185 | mainAxisAlignment: MainAxisAlignment.spaceAround, 186 | children: [ 187 | Container( 188 | color: Colors.black, 189 | width: 100, 190 | alignment: Alignment.center, 191 | padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), 192 | child: Text( 193 | remainBombs.toString().padLeft(3, '0'), 194 | style: TextStyle( 195 | fontSize: 32, 196 | color: Colors.red, 197 | ), 198 | ), 199 | ), 200 | Container( 201 | margin: EdgeInsets.symmetric(vertical: 8), 202 | decoration: BoxDecoration( 203 | color: Colors.grey, 204 | borderRadius: BorderRadius.all(Radius.circular(48)), 205 | ), 206 | child: IconButton( 207 | icon: Icon( 208 | _isGameOver 209 | ? Icons.sentiment_very_dissatisfied 210 | : Icons.mood 211 | ), 212 | iconSize: 48.0, 213 | color: Colors.yellow[200], 214 | onPressed: () { 215 | if (_isGameOver == false && _isGameWon == false) { 216 | alertMessage( 217 | context: context, 218 | title: '建立新遊戲?', 219 | okText: '確定', 220 | cancelText: '取消', 221 | onOK: () { 222 | Navigator.pop(context); 223 | createGame(); 224 | }, 225 | onCancel: () { 226 | Navigator.pop(context); 227 | }, 228 | ); 229 | return; 230 | } 231 | createGame(); 232 | }, 233 | ), 234 | ), 235 | Container( 236 | color: Colors.black, 237 | width: 100, 238 | alignment: Alignment.center, 239 | padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), 240 | child: Text( 241 | timeString, 242 | style: TextStyle( 243 | fontSize: 32, 244 | color: Colors.red, 245 | ), 246 | ), 247 | ), 248 | ], 249 | ), 250 | GridView.builder( 251 | physics: NeverScrollableScrollPhysics(), 252 | shrinkWrap: true, 253 | scrollDirection: Axis.vertical, 254 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 255 | crossAxisCount: COLUMNS, 256 | mainAxisSpacing: 2, 257 | crossAxisSpacing: 2, 258 | ), 259 | padding: EdgeInsets.all(4), 260 | itemCount: ROWS * COLUMNS, 261 | itemBuilder: (BuildContext context, int index) { 262 | int y = (index / COLUMNS).floor(); 263 | int x = index - (COLUMNS * y); 264 | final grid = grids[y][x]; 265 | Widget widget; 266 | if (grid.isSearched) { 267 | if (grid.hasBomb) { 268 | widget = Icon(Icons.new_releases); 269 | } else { 270 | widget = Text( 271 | grid.aroundBombs > 0 272 | ? grid.aroundBombs.toString() 273 | : '', 274 | style: TextStyle( 275 | fontSize: 24, 276 | color: BOMB_COLORS[grid.aroundBombs], 277 | ), 278 | ); 279 | } 280 | } else if ( 281 | flags[y] is Map && 282 | flags[y][x] == true 283 | ) { 284 | widget = BonuceIcon( 285 | Icons.assistant_photo, 286 | color: Colors.red, 287 | size: 20, 288 | ); 289 | } else { 290 | widget = Text(''); 291 | } 292 | return IgnorePointer( 293 | ignoring: _isGameOver || _isGameWon, 294 | child: InkWell( 295 | child: Container( 296 | alignment: Alignment.center, 297 | decoration: BoxDecoration( 298 | color: grid.isSearched && grid.hasBomb 299 | ? Colors.red 300 | : _isGameOver 301 | ? Colors.black12 302 | : Colors.black26, 303 | border: grid.isSearched 304 | ? null 305 | : Border( 306 | left: BorderSide(color: Colors.grey[300], width: 4), 307 | top: BorderSide(color: Colors.grey[300], width: 4), 308 | right: BorderSide(color: Colors.black26, width: 4), 309 | bottom: BorderSide(color: Colors.black26, width: 4), 310 | ), 311 | ), 312 | child: widget, 313 | ), 314 | onTap: () { 315 | if (!grid.isSearched && grid.hasBomb) { 316 | /// 踩到炸彈遊戲結束 317 | setState(() { 318 | grid.isSearched = true; 319 | _isGameOver = true; 320 | }); 321 | return; 322 | } 323 | /// 如果採的位置有設置的旗子, 324 | /// 但沒有炸彈則解除旗子的設置 325 | if ( 326 | !grid.hasBomb && 327 | flags[y] is Map && 328 | flags[y][x] == true 329 | ) { 330 | flags[y].remove(x); 331 | } 332 | setState(() { 333 | searchBomb(x, y); 334 | _isGameWon = checkWin(); 335 | if (_isGameWon == true) { 336 | /// 已達成獲勝條件 337 | alertMessage( 338 | context: context, 339 | title: '厲害唷!', 340 | okText: '新遊戲', 341 | onOK: () { 342 | Navigator.pop(context); 343 | createGame(); 344 | } 345 | ); 346 | } 347 | }); 348 | }, 349 | onLongPress: () { 350 | setState(() { 351 | flags[y] ??= Map(); 352 | if (flags[y][x] == true) { 353 | flags[y].remove(x); 354 | } else { 355 | flags[y][x] = true; 356 | } 357 | _isGameWon = checkWin(); 358 | }); 359 | }, 360 | ), 361 | ); 362 | }, 363 | ), 364 | ], 365 | ); 366 | } 367 | } 368 | 369 | class GameGrid { 370 | bool hasBomb; 371 | bool isSearched; 372 | int aroundBombs; 373 | GameGrid({ 374 | this.isSearched = false, 375 | this.hasBomb = false, 376 | this.aroundBombs = 0, 377 | }); 378 | } 379 | -------------------------------------------------------------------------------- /lib/games/tetris/game_pad.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | const MAX_OFFSET = 10; 6 | 7 | /// 偵測手勢動作判斷魔術方塊操作行為 8 | class GamePad extends StatefulWidget { 9 | final Widget child; 10 | final VoidCallback onUp; 11 | final VoidCallback onDown; 12 | final VoidCallback onLeft; 13 | final VoidCallback onRight; 14 | final VoidCallback onSwipeDown; 15 | final VoidCallback onTap; 16 | GamePad({ 17 | @required this.child, 18 | this.onUp, 19 | this.onDown, 20 | this.onLeft, 21 | this.onRight, 22 | this.onSwipeDown, 23 | this.onTap, 24 | }); 25 | @override 26 | State createState() => _GamePadState(); 27 | } 28 | 29 | class _GamePadState extends State { 30 | /// 方向移動時,自動發出控制指令 31 | Timer _autoFirer; 32 | /// 玩家目前操控的方向 33 | CtrlDirection _currentDirention = CtrlDirection.none; 34 | // /// 玩家首次觸碰螢幕時在螢幕上的 X 軸位置 35 | // double _sx; 36 | // /// 玩家首次觸碰螢幕時在螢幕上的 Y 軸位置 37 | // double _sy; 38 | // /// 玩家觸碰螢幕後當前觸碰的 X 軸位置 39 | // double _cx; 40 | // /// 玩家觸碰螢幕後當前觸碰的 Y 軸位置 41 | // double _cy; 42 | /// 玩家觸碰螢幕後與首次觸碰螢幕的 X 軸位置的總位移量 43 | double _tdx; 44 | /// 玩家觸碰螢幕後與首次觸碰螢幕的 Y 軸位置的總位移量 45 | double _tdy; 46 | /// 重置數據 47 | void _reset() { 48 | // _sx = _sy = _cx = _cy = _tdx = _tdy = null; 49 | _tdx = _tdy = null; 50 | _currentDirention = CtrlDirection.none; 51 | _autoFirer?.cancel(); 52 | _autoFirer = null; 53 | } 54 | void _onKey(RawKeyEvent ev) { 55 | print(ev); 56 | if (ev is RawKeyUpEvent) { 57 | return; 58 | } 59 | final key = ev.data.physicalKey; 60 | if (key == PhysicalKeyboardKey.arrowLeft) { 61 | widget.onLeft(); 62 | } else if (key == PhysicalKeyboardKey.arrowRight) { 63 | widget.onRight(); 64 | } else if (key == PhysicalKeyboardKey.arrowUp) { 65 | widget.onUp(); 66 | } else if (key == PhysicalKeyboardKey.arrowDown) { 67 | widget.onSwipeDown(); 68 | } 69 | } 70 | @override 71 | void initState() { 72 | super.initState(); 73 | RawKeyboard.instance.addListener(_onKey); 74 | } 75 | @override 76 | void dispose() { 77 | RawKeyboard.instance.removeListener(_onKey); 78 | _autoFirer?.cancel(); 79 | super.dispose(); 80 | } 81 | @override 82 | Widget build(BuildContext context) { 83 | return GestureDetector( 84 | child: widget.child, 85 | onTap: () { 86 | if (widget.onTap == null) return; 87 | widget.onTap(); 88 | }, 89 | onPanDown: (DragDownDetails details) { 90 | // _sx = _cx = details.globalPosition.dx; 91 | // _sy = _cy = details.globalPosition.dy; 92 | _tdx = _tdy = 0; 93 | }, 94 | onPanUpdate: (DragUpdateDetails details) { 95 | // 一次下滑的距離超過設定值時即判斷執行下滑 96 | if (details.delta.dy > 10 && widget.onSwipeDown != null) { 97 | widget.onSwipeDown(); 98 | } 99 | // _cx = details.globalPosition.dx; 100 | // _cy = details.globalPosition.dy; 101 | _tdx += details.delta.dx; 102 | _tdy += details.delta.dy; 103 | if (_tdx.abs() >= MAX_OFFSET || _tdy.abs() >= MAX_OFFSET) { 104 | _currentDirention = _getDirection(_tdx, _tdy); 105 | _tdx = _tdy = 0; 106 | if (_currentDirention == CtrlDirection.left && widget.onLeft != null) { 107 | widget.onLeft(); 108 | } else if (_currentDirention == CtrlDirection.right && widget.onRight != null) { 109 | widget.onRight(); 110 | } else if (_currentDirention == CtrlDirection.up && widget.onUp != null) { 111 | widget.onUp(); 112 | } else if (_currentDirention == CtrlDirection.down && widget.onDown != null) { 113 | widget.onDown(); 114 | } 115 | } 116 | }, 117 | onPanEnd: (DragEndDetails details) { _reset(); }, 118 | onPanCancel: _reset, 119 | ); 120 | } 121 | } 122 | /// 根據輸入的座標位移量 [tx], [ty] 判斷移動的方向 123 | CtrlDirection _getDirection(double tx, double ty) { 124 | CtrlDirection direction = CtrlDirection.none; 125 | if (tx.abs() >= ty.abs()) { 126 | if (tx > 0) { 127 | direction = CtrlDirection.right; 128 | } else if (tx < 0) { 129 | direction = CtrlDirection.left; 130 | } else { 131 | direction = CtrlDirection.none; 132 | } 133 | } else { 134 | if (ty > 0) { 135 | direction = CtrlDirection.down; 136 | } else if (ty < 0) { 137 | direction = CtrlDirection.up; 138 | } else { 139 | direction = CtrlDirection.none; 140 | } 141 | } 142 | return direction; 143 | } 144 | /// 手勢移動時的控制方向定義 145 | enum CtrlDirection { 146 | none, 147 | up, 148 | down, 149 | left, 150 | right, 151 | } 152 | -------------------------------------------------------------------------------- /lib/games/tetris/sound.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:audioplayer/audioplayer.dart'; 4 | import 'package:path_provider/path_provider.dart' as pathProvider; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | 8 | typedef void StateChangeCallback(AudioPlayerState currentState); 9 | 10 | class Sound { 11 | final String url; 12 | final bool loop; 13 | final VoidCallback onComplete; 14 | final StateChangeCallback onStateChange; 15 | 16 | AudioPlayer _player; 17 | StreamSubscription _stateSubscription; 18 | 19 | AudioPlayerState get playerState => _player.state; 20 | Duration get duration => _player.duration; 21 | 22 | static Future playFromAsset( 23 | String assetDirPath, String fileName, { 24 | bool loop, 25 | VoidCallback onComplete, 26 | StateChangeCallback onStateChange, 27 | } 28 | ) async { 29 | Directory temporaryDir = await pathProvider.getTemporaryDirectory(); 30 | File temporaryFile = File('${temporaryDir.path}/$fileName'); 31 | if (await temporaryFile.exists()) { 32 | print('file exists, filePath=${temporaryFile.path}'); 33 | Sound sound = Sound( 34 | url: temporaryFile.path, 35 | loop: loop, 36 | onComplete: onComplete, 37 | onStateChange: onStateChange, 38 | ); 39 | sound.play(); 40 | return sound; 41 | } 42 | final ByteData soundData = await rootBundle.load('$assetDirPath$fileName'); 43 | final bytes = soundData.buffer.asUint8List(); 44 | await temporaryFile.writeAsBytes(bytes, flush: true); 45 | print('finished loading, filePath=${temporaryFile.path}'); 46 | Sound sound = Sound( 47 | url: temporaryFile.path, 48 | loop: loop, 49 | onComplete: onComplete, 50 | onStateChange: onStateChange, 51 | ); 52 | sound.play(); 53 | return sound; 54 | } 55 | 56 | Sound({ 57 | @required this.url, 58 | this.loop = false, 59 | this.onComplete, 60 | this.onStateChange, 61 | }) { 62 | _player = AudioPlayer(); 63 | } 64 | Future play() async { 65 | _player?.stop(); 66 | await _player.play(url, isLocal: true); 67 | _stateSubscription?.cancel(); 68 | _stateSubscription = _player.onPlayerStateChanged.listen((AudioPlayerState currentState) { 69 | if (onStateChange != null) onStateChange(currentState); 70 | if (currentState == AudioPlayerState.COMPLETED) { 71 | if (loop == true) play(); 72 | if (onComplete != null) onComplete(); 73 | } 74 | }); 75 | } 76 | Future pause() async { 77 | await _player.pause(); 78 | } 79 | Future stop() async { 80 | await _player.stop(); 81 | _stateSubscription?.cancel(); 82 | _stateSubscription = null; 83 | } 84 | } -------------------------------------------------------------------------------- /lib/games/tetris/tetris.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import './game_pad.dart'; 4 | import './sound.dart'; 5 | import './tetris_data.dart'; 6 | import './tetris_renderder.dart'; 7 | 8 | /// 可堆疊的總行數 9 | const ROWS = 20; 10 | /// 一行最多擺放的寬度 11 | const COLS = 10; 12 | /// 設定的遊戲等級,一共六級 13 | /// 用毫秒數來代表墜落速度 14 | const LEVELS = const [ 15 | Duration(milliseconds: 600), 16 | Duration(milliseconds: 500), 17 | Duration(milliseconds: 400), 18 | Duration(milliseconds: 300), 19 | Duration(milliseconds: 200), 20 | Duration(milliseconds: 100), 21 | ]; 22 | /// 分數升級門檻 23 | const LEVEL_UP = const [ 24 | 1000, 25 | 5000, 26 | 10000, 27 | 20000, 28 | 50000 29 | ]; 30 | const double RIGHT_PANEL_WIDTH = 100.0; 31 | const TextStyle INFO_TEXT_STYLE = TextStyle( 32 | color: Colors.white, 33 | fontSize: 20, 34 | ); 35 | 36 | class Tetris extends StatefulWidget { 37 | Tetris({ Key key, }) : super(key: key); 38 | static TetrisData dataOf(BuildContext context) { 39 | final _TetrisStateContainer widgetInstance = 40 | context.inheritFromWidgetOfExactType(_TetrisStateContainer); 41 | return widgetInstance.data; 42 | } 43 | @override 44 | TetrisState createState() => TetrisState(); 45 | } 46 | 47 | class TetrisState extends State with WidgetsBindingObserver { 48 | /// 魔術方塊面板資料 49 | TetrisData data; 50 | /// 魔術方塊下降間隔的計時器 51 | Timer _fallTimer; 52 | /// 魔術方塊到底時短暫停頓的計時器 53 | Timer _restTimer; 54 | /// 禁止移動 55 | bool _freezeMove; 56 | /// 禁止移動 57 | bool _gameOver; 58 | /// 目前等級 59 | int _level; 60 | /// 遊戲總得分數 61 | int _score; 62 | /// App 目前的狀態 63 | AppLifecycleState _appLifecycleState; 64 | /// 魔術方塊的主題音樂 65 | Sound _themeMusic; 66 | /// 遊戲結束的音樂 67 | Sound _gameOverMusic; 68 | /// 遊戲是否暫停中 69 | bool get _isPause => _fallTimer == null && _restTimer == null; 70 | TetrisState() { 71 | this.data = TetrisData(rows: ROWS, cols: COLS); 72 | this._freezeMove = false; 73 | this._score = this._level = 0; 74 | } 75 | void init() { 76 | _score = _level = 0; 77 | _freezeMove = _gameOver = false; 78 | data.reset(); 79 | putInShape(); 80 | _themeMusic?.stop(); 81 | // Sound.playFromAsset( 82 | // 'assets/tetris/', 'theme.mp3', 83 | // loop: true, 84 | // ).then((Sound sound) { 85 | // if (!mounted) return; 86 | // _themeMusic = sound; 87 | // }); 88 | } 89 | void putInShape() { 90 | data.putInShape(); 91 | _toggleFallTimer(true); 92 | setState(() {}); 93 | } 94 | @override 95 | void didChangeAppLifecycleState(AppLifecycleState lifecycleState) { 96 | _appLifecycleState = lifecycleState; 97 | switch (_appLifecycleState) { 98 | case AppLifecycleState.resumed: 99 | _toggleFallTimer(true); 100 | if (!_gameOver) _themeMusic.play(); 101 | print('!!!!! 恢復遊戲 !!!!!'); 102 | break; 103 | case AppLifecycleState.inactive: 104 | case AppLifecycleState.paused: 105 | default: 106 | _toggleFallTimer(false); 107 | _toggleRestTimer(false); 108 | _themeMusic.stop(); 109 | _gameOverMusic.stop(); 110 | print('!!!!! 遊戲暫停 !!!!!'); 111 | break; 112 | } 113 | } 114 | @override 115 | void initState() { 116 | super.initState(); 117 | WidgetsBinding.instance.addObserver(this); 118 | init(); 119 | } 120 | @override 121 | void dispose() { 122 | WidgetsBinding.instance.removeObserver(this); 123 | _fallTimer?.cancel(); 124 | _restTimer?.cancel(); 125 | _themeMusic?.stop(); 126 | _gameOverMusic?.stop(); 127 | super.dispose(); 128 | } 129 | void _toggleFallTimer(bool shouldEnable) { 130 | if (!shouldEnable && _fallTimer != null) { 131 | _fallTimer.cancel(); 132 | _fallTimer = null; 133 | } else if (shouldEnable) { 134 | _fallTimer?.cancel(); 135 | _fallTimer = Timer.periodic(LEVELS[_level], _execMoveDown); 136 | } 137 | } 138 | void _toggleRestTimer(bool shouldEnable) { 139 | if (!shouldEnable && _restTimer != null) { 140 | _restTimer.cancel(); 141 | _restTimer = null; 142 | } else if (shouldEnable) { 143 | _restTimer?.cancel(); 144 | _restTimer = Timer(LEVELS[_level], _afterRest); 145 | } 146 | } 147 | /// 到底時會有一個方塊要置底的休息時間 148 | void _afterRest() { 149 | if (data.canMoveDown) { 150 | _execMoveDown(_restTimer); 151 | _toggleRestTimer(true); 152 | return; 153 | } 154 | data.mergeShapeToPanel(); 155 | _score += (data.cleanLines() * (_level + 1)); 156 | if (_level < LEVELS.length && _score >= LEVEL_UP[_level]) { 157 | _level++; 158 | } 159 | if (data.isGameOver) { 160 | print('!!!!! 遊戲結束 !!!!!'); 161 | _gameOver = true; 162 | _toggleFallTimer(false); 163 | _toggleRestTimer(false); 164 | // Sound.playFromAsset( 165 | // 'assets/tetris/', 'gameover.mp3', 166 | // onComplete: () { _gameOverMusic = null; } 167 | // ).then((Sound sound) { 168 | // if (!mounted) return; 169 | // _gameOverMusic = sound; 170 | // }); 171 | } else { 172 | data.putInShape(); 173 | _toggleFallTimer(true); 174 | _freezeMove = false; 175 | } 176 | setState(() {}); 177 | } 178 | /// 直接執行讓方塊直接落下 179 | void _execFallingDown() { 180 | _toggleFallTimer(false); 181 | _freezeMove = true; 182 | data.fallingDown(); 183 | _toggleRestTimer(true); 184 | setState(() {}); 185 | } 186 | /// 執行方塊落下一格的處理 187 | void _execMoveDown(Timer _timer) { 188 | if (_gameOver || _isPause) return; 189 | if (!data.canMoveDown) { 190 | print('到底了, bottom: ${data.currentBottom}'); 191 | _toggleFallTimer(false); 192 | _toggleRestTimer(true); 193 | return; 194 | } 195 | data.moveCurrentShapeDown(); 196 | setState(() {}); 197 | } 198 | /// 執行往左移動 199 | void _execMoveLeft() { 200 | if (_freezeMove || _gameOver || _isPause) return; 201 | if (data.moveCurrentShapeLeft()) setState(() {}); 202 | } 203 | /// 執行往右移動 204 | void _execMoveRight() { 205 | if (_freezeMove || _gameOver || _isPause) return; 206 | if (data.moveCurrentShapeRight()) setState(() {}); 207 | } 208 | /// 執行方塊旋轉 209 | void _execRotate() { 210 | if (_gameOver || _isPause) return; 211 | if (data.rotateCurrentShape()) setState(() {}); 212 | } 213 | /// 暫停/復原遊戲 214 | void _togglePause() { 215 | setState(() { 216 | if (_isPause) { 217 | _toggleFallTimer(true); 218 | } else { 219 | _toggleFallTimer(false); 220 | _toggleRestTimer(false); 221 | } 222 | }); 223 | } 224 | @override 225 | Widget build(BuildContext context) { 226 | final Size size = MediaQuery.of(context).size; 227 | double boxWidth = size.width; 228 | double boxHeight = size.height - kBottomNavigationBarHeight - kToolbarHeight; 229 | return _TetrisStateContainer( 230 | data: this.data, 231 | child: Container( 232 | width: boxWidth, 233 | height: boxHeight, 234 | color: Colors.black, 235 | child: GamePad( 236 | child: Row( 237 | children: [ 238 | TertisRenderder(size: Size(boxWidth - RIGHT_PANEL_WIDTH, boxHeight)), 239 | Container( 240 | width: RIGHT_PANEL_WIDTH, 241 | decoration: BoxDecoration( 242 | border: Border( 243 | left: BorderSide(color: Colors.white, width: 1.0), 244 | ), 245 | ), 246 | child: Column( 247 | mainAxisAlignment: MainAxisAlignment.spaceAround, 248 | children: [ 249 | NextShapeRenderder(size: Size(RIGHT_PANEL_WIDTH, RIGHT_PANEL_WIDTH)), 250 | Container( 251 | padding: EdgeInsets.symmetric(vertical: 16), 252 | child: Column( 253 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 254 | children: [ 255 | Text('等級', 256 | textAlign: TextAlign.center, 257 | style: INFO_TEXT_STYLE, 258 | ), 259 | Text('${_level + 1}', 260 | textAlign: TextAlign.center, 261 | style: INFO_TEXT_STYLE, 262 | ), 263 | ], 264 | ), 265 | ), 266 | Container( 267 | padding: EdgeInsets.only(bottom: 16), 268 | child: Column( 269 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 270 | children: [ 271 | Text('分數', 272 | textAlign: TextAlign.center, 273 | style: INFO_TEXT_STYLE, 274 | ), 275 | Text('$_score', 276 | textAlign: TextAlign.center, 277 | style: INFO_TEXT_STYLE, 278 | ), 279 | ], 280 | ), 281 | ), 282 | Container( 283 | margin: EdgeInsets.only(top: 16, bottom: 16), 284 | child: IgnorePointer( 285 | ignoring: _gameOver, 286 | child: IconButton( 287 | icon: Icon( 288 | _isPause ? Icons.play_arrow : Icons.pause 289 | ), 290 | iconSize: 32, 291 | color: Colors.white, 292 | onPressed: _togglePause, 293 | ), 294 | ), 295 | ), 296 | Container( 297 | margin: EdgeInsets.only(top: 16, bottom: 24), 298 | child: IconButton( 299 | icon: Icon(Icons.sync), 300 | iconSize: 32, 301 | color: Colors.white, 302 | onPressed: init, 303 | ), 304 | ), 305 | ], 306 | ), 307 | ), 308 | ], 309 | ), 310 | onTap: Feedback.wrapForTap(_execRotate, context), 311 | onUp: _execRotate, 312 | onLeft: _execMoveLeft, 313 | onRight: _execMoveRight, 314 | onSwipeDown: Feedback.wrapForTap(_execFallingDown, context), 315 | ), 316 | ), 317 | ); 318 | } 319 | } 320 | 321 | class _TetrisStateContainer extends InheritedWidget { 322 | final TetrisData data; 323 | _TetrisStateContainer({ 324 | @required this.data, 325 | @required Widget child, 326 | }) : super(child: child); 327 | @override 328 | bool updateShouldNotify(_TetrisStateContainer old) => true; 329 | } 330 | -------------------------------------------------------------------------------- /lib/games/tetris/tetris_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// 隨機產生器種子 5 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 6 | 7 | class TetrisData { 8 | final int rows; 9 | final int cols; 10 | int get totalGrids => rows * cols; 11 | /// 魔術方塊的面板格子資料,紀錄每個格子資料 12 | List> panel; 13 | /// 下一個要落下的方塊 14 | Shape nextShape; 15 | /// 目前落下的魔術方塊 16 | Shape currentShape; 17 | /// 目前落下的方塊位置 18 | Offset _currentOffset; 19 | /// 面板上所有的魔術方塊 20 | List _shapes; 21 | /// 落下方塊所在的 X 座標 22 | int get currentX { 23 | if (_currentOffset == null) return -1; 24 | return _currentOffset.dx.toInt(); 25 | } 26 | /// 落下方塊的所在的 Y 座標 27 | int get currentY { 28 | if (_currentOffset == null) return -1; 29 | return _currentOffset.dy.toInt(); 30 | } 31 | /// 落下方塊右側的位置 32 | int get currentRight { 33 | if (_currentOffset == null) return -1; 34 | return currentX + currentShape.width; 35 | } 36 | /// 落下方塊底部的位置 37 | int get currentBottom { 38 | if (_currentOffset == null) return -1; 39 | return currentY + currentShape.height; 40 | } 41 | /// 判斷遊戲是否結束 42 | bool get isGameOver => currentY < 0; 43 | /// 是否可往下移動 44 | bool get canMoveDown => currentY + 1 <= findFallingDownY(); 45 | TetrisData({ 46 | @required this.rows, 47 | @required this.cols, 48 | }) { 49 | this.panel = List.generate(rows, (y) => List.generate(cols, (x) => 0)); 50 | this._shapes = []; 51 | this.nextShape = Shape.random(); 52 | } 53 | /// 重置資料,將所有的資料回歸原始狀態 54 | void reset() { 55 | for (int y = 0; y < rows; y++) { 56 | for (int x = 0; x < cols; x++) { 57 | panel[y][x] = 0; 58 | } 59 | } 60 | _shapes.clear(); 61 | currentShape = _currentOffset = null; 62 | nextShape = Shape.random(); 63 | int rotateCount = randomGenerator.nextInt(4); 64 | while (--rotateCount >= 0) { nextShape.rotate(); } 65 | } 66 | /// 將下一個方塊放進遊戲面板裡,並同時產生下一個魔術方塊 67 | void putInShape() { 68 | currentShape = nextShape; 69 | _currentOffset = Offset( 70 | // 初始 X 軸位置置中 71 | ((cols / 2) - (currentShape.width / 2)).roundToDouble(), 72 | // 初始 Y 軸位置完全隱藏方塊 73 | -currentShape.height.toDouble(), 74 | ); 75 | nextShape = Shape.random(); 76 | int rotateCount = randomGenerator.nextInt(4); 77 | while (--rotateCount >= 0) { nextShape.rotate(); } 78 | } 79 | /// 往左移動目前的魔術方塊 80 | bool moveCurrentShapeLeft() { 81 | if (currentShape == null) return false; 82 | int nextX = currentX - 1; 83 | // 如果要往左移動的位置,目前面板已有方塊,則不處理移動 84 | if ( 85 | nextX >= 0 && 86 | _canMoveToX(currentX, nextX) 87 | ) { 88 | _currentOffset = Offset( 89 | nextX.toDouble(), 90 | currentY.toDouble(), 91 | ); 92 | return true; 93 | } 94 | return false; 95 | } 96 | /// 往右移動目前的魔術方塊 97 | bool moveCurrentShapeRight() { 98 | if (currentShape == null) return false; 99 | int nextX = currentX + 1; 100 | int nextRight = nextX + currentShape.width; 101 | // 如果要往右移動的位置,目前面板已有方塊,則不處理移動 102 | if ( 103 | nextRight <= cols && 104 | _canMoveToX(currentRight - 1, nextRight - 1) 105 | ) { 106 | _currentOffset = Offset( 107 | nextX.toDouble(), 108 | currentY.toDouble(), 109 | ); 110 | return true; 111 | } 112 | return false; 113 | } 114 | /// 往下移動目前的魔術方塊 115 | bool moveCurrentShapeDown() { 116 | if (!(currentShape != null && canMoveDown)) return false; 117 | _currentOffset = Offset( 118 | currentX.toDouble(), 119 | (currentY + 1).toDouble(), 120 | ); 121 | return true; 122 | } 123 | /// 將目前的方塊直接落下 124 | void fallingDown() { 125 | _currentOffset = Offset( 126 | currentX.toDouble(), 127 | findFallingDownY().toDouble(), 128 | ); 129 | } 130 | /// 旋轉目前的魔術方塊 131 | bool rotateCurrentShape() { 132 | if (currentShape == null) return false; 133 | bool canRotate = true; 134 | /// 先判斷旋轉後的方塊是否合法,合法時才能旋轉 135 | Shape rotatedShape = Shape( 136 | patterns: currentShape.patterns, 137 | patternIndex: currentShape.patternIndex, 138 | colorIndex: currentShape.colorIndex, 139 | ); 140 | rotatedShape.rotate(); 141 | rotatedShape.forEachBlock((value, x, y) { 142 | if (currentX + x < 0) { 143 | _currentOffset = Offset( 144 | 0, 145 | currentY.toDouble(), 146 | ); 147 | } else if (currentX + x > cols - rotatedShape.width) { 148 | _currentOffset = Offset( 149 | (cols - rotatedShape.width).toDouble(), 150 | currentY.toDouble(), 151 | ); 152 | } 153 | int ty = currentY + y; 154 | int tx = currentX + x; 155 | if ( 156 | ty >= 0 && ty < rows && 157 | tx >= 0 && tx < cols && 158 | panel[ty][tx] > 0 && value > 0 159 | ) { 160 | canRotate = false; 161 | } 162 | }, reverse: true); 163 | if (canRotate) { 164 | currentShape = rotatedShape; 165 | } 166 | return canRotate; 167 | } 168 | /// 把目前的方塊固定到面板上 169 | void mergeShapeToPanel() { 170 | final block = currentShape.block; 171 | for (int y = currentShape.height - 1; y >= 0; y--) { 172 | for (int x = 0; x < currentShape.width; x++) { 173 | if (block[y][x] > 0) { 174 | int ty = currentY + y; 175 | int tx = currentX + x; 176 | if (ty < 0) break; 177 | panel[ty][tx] = block[y][x]; 178 | } 179 | } 180 | } 181 | _shapes.add(currentShape); 182 | currentShape = null; 183 | } 184 | /// 檢查是否有填滿,回傳得到的分數 185 | int cleanLines() { 186 | int score = 0; 187 | int bonus = 0; 188 | int y = rows - 1; 189 | while (y >= 0) { 190 | bool shouldClean = true; 191 | for (int x = 0; x < cols; x++) { 192 | if (panel[y][x] == 0) { 193 | shouldClean = false; 194 | break; 195 | } 196 | } 197 | if (shouldClean) { 198 | score += 100 + bonus; 199 | // 每多一行疊加 100 分 200 | bonus += 100; 201 | // 將目標清空格的上方空格都往下移 202 | for (int dy = y; dy >= 0; dy--) { 203 | for (int x = 0; x < cols; x++) { 204 | panel[dy][x] = dy - 1 >= 0 ? panel[dy - 1][x] : 0; 205 | } 206 | } 207 | } else { 208 | y--; 209 | } 210 | } 211 | return score; 212 | } 213 | /// 找到方塊能直接落下的 Y 軸位移量 214 | int findFallingDownY() { 215 | for (int fY = currentY + 1; fY <= rows - currentShape.height; fY++) { 216 | bool blocked = false; 217 | currentShape.forEachBlock((value, x, y) { 218 | if (fY + y < 0) return; 219 | if (panel[fY + y][currentX + x] > 0) { 220 | blocked = true; 221 | } 222 | }, reverse: true); 223 | if (blocked) { 224 | return fY - 1; 225 | } 226 | } 227 | return rows - currentShape.height; 228 | } 229 | /// 檢查目前的方塊是否可移動至目標 X 軸位置 230 | bool _canMoveToX(int fromX, int toX) { 231 | if (currentShape == null) return false; 232 | final block = currentShape.block; 233 | int blockX = fromX - toX >= 0 ? 0 : currentShape.width - 1; 234 | // 檢查目前方塊的垂直軸是否都能移動過去 235 | for (int y = currentShape.height - 1; y >= 0; y--) { 236 | if (currentY + y < 0) continue; 237 | int blockValue = block[y][blockX]; 238 | if (blockValue == 0) { 239 | blockValue = block[y][fromX - toX >= 0 ? blockX + 1 : blockX - 1]; 240 | } 241 | // 只要有一個位置衝突就不能移動過去 242 | if (panel[currentY + y][toX] > 0 && blockValue > 0) { 243 | return false; 244 | } 245 | } 246 | return true; 247 | } 248 | } 249 | typedef void ShapeForEachCallback(int value, int x, int y); 250 | class Shape { 251 | /// 顯示顏色的編號 252 | final int colorIndex; 253 | /// 4 * 4 方塊模板 254 | final List> patterns; 255 | /// 目前方塊模板的位置(樣板包含旋轉) 256 | int patternIndex; 257 | /// 4 * 4 的方塊 258 | List> block; 259 | /// 方塊所佔的尺寸 260 | Size _size; 261 | /// 方塊目前的所佔最大寬度 262 | int get width => _size.width.toInt(); 263 | /// 方塊目前的所佔最大高度 264 | int get height => _size.height.toInt(); 265 | Shape({ 266 | @required this.patterns, 267 | @required this.patternIndex, 268 | @required this.colorIndex, 269 | }) { 270 | this.block = List.generate(PATTERN_SIZE, (y) => List.generate(PATTERN_SIZE, (x) => 0)); 271 | List pattern = patterns[patternIndex]; 272 | forEachBlock((value, x, y) { 273 | int i = PATTERN_SIZE * y + x; 274 | block[y][x] = pattern[i] == 1 ? colorIndex : 0; 275 | }, ignoreZero: false); 276 | updateSize(); 277 | } 278 | void forEachBlock(ShapeForEachCallback callback, { 279 | ignoreZero = true, 280 | reverse = false, 281 | }) { 282 | if (reverse) { 283 | for (int y = PATTERN_SIZE - 1; y >= 0; y--) { 284 | for (int x = PATTERN_SIZE - 1; x >= 0; x--) { 285 | if (ignoreZero && block[y][x] == 0) continue; 286 | callback(block[y][x], x, y); 287 | } 288 | } 289 | } else { 290 | for (int y = 0; y < PATTERN_SIZE; y++) { 291 | for (int x = 0; x < PATTERN_SIZE; x++) { 292 | if (ignoreZero && block[y][x] == 0) continue; 293 | callback(block[y][x], x, y); 294 | } 295 | } 296 | } 297 | } 298 | /// 旋轉方塊 299 | void rotate() { 300 | patternIndex = patternIndex + 1 >= patterns.length ? 0 : patternIndex + 1; 301 | List pattern = patterns[patternIndex]; 302 | forEachBlock((value, x, y) { 303 | int i = PATTERN_SIZE * y + x; 304 | block[y][x] = pattern[i] == 1 ? colorIndex : 0; 305 | }, ignoreZero: false); 306 | updateSize(); 307 | } 308 | /// 更新方塊的寬高資訊 309 | void updateSize() { 310 | double _offsetX = 0; 311 | double _offsetY = 0; 312 | List widths = List.filled(PATTERN_SIZE, 0.0); 313 | List heights = List.filled(PATTERN_SIZE, 0.0); 314 | for (int x = 0; x < PATTERN_SIZE; x++) { 315 | for (int y = 0; y < PATTERN_SIZE; y++) { 316 | if (block[y][x] > 0) { 317 | widths[x] = heights[y] = 1; 318 | } 319 | if (heights[y] == 1 && _offsetY == 0) _offsetY = y.toDouble(); 320 | } 321 | if (widths[x] == 1 && _offsetX == 0) _offsetX = x.toDouble(); 322 | } 323 | double width = widths.reduce((val, elem) => val + elem); 324 | double height = heights.reduce((val, elem) => val + elem); 325 | _size = Size(width, height); 326 | } 327 | /// 隨機產生一個方塊形狀 328 | static Shape random() { 329 | int patternsIndex = randomGenerator.nextInt(SHAPES.length); 330 | final patterns = SHAPES[patternsIndex]; 331 | int patternIndex = randomGenerator.nextInt(patterns.length); 332 | final shape = Shape( 333 | patterns: patterns, 334 | patternIndex: patternIndex, 335 | colorIndex: 1 + randomGenerator.nextInt(TETRIS_COLORS.length - 1), 336 | ); 337 | return shape; 338 | } 339 | } 340 | 341 | /// 顯示的方塊顏色 342 | const List TETRIS_COLORS = [ 343 | Colors.black, 344 | Colors.cyan, 345 | Colors.orange, 346 | Colors.blue, 347 | Colors.yellow, 348 | Colors.red, 349 | Colors.green, 350 | Colors.purple 351 | ]; 352 | /// 每個魔術方塊樣板尺寸 353 | const PATTERN_SIZE = 4; 354 | /// 所有可能出現的魔術方塊樣板 355 | const SHAPES = [ 356 | [ 357 | [ 1, 1, 1, 1, 358 | 0, 0, 0, 0, 359 | 0, 0, 0, 0, 360 | 0, 0, 0, 0 ], 361 | [ 1, 0, 0, 0, 362 | 1, 0, 0, 0, 363 | 1, 0, 0, 0, 364 | 1, 0, 0, 0 ], 365 | ], 366 | [ 367 | [ 1, 1, 1, 0, 368 | 1, 0, 0, 0, 369 | 0, 0, 0, 0, 370 | 0, 0, 0, 0 ], 371 | [ 1, 0, 0, 0, 372 | 1, 0, 0, 0, 373 | 1, 1, 0, 0, 374 | 0, 0, 0, 0 ], 375 | [ 0, 0, 1, 0, 376 | 1, 1, 1, 0, 377 | 0, 0, 0, 0, 378 | 0, 0, 0, 0 ], 379 | [ 1, 1, 0, 0, 380 | 0, 1, 0, 0, 381 | 0, 1, 0, 0, 382 | 0, 0, 0, 0 ], 383 | ], 384 | [ 385 | [ 1, 1, 1, 0, 386 | 0, 0, 1, 0, 387 | 0, 0, 0, 0, 388 | 0, 0, 0, 0 ], 389 | [ 0, 1, 0, 0, 390 | 0, 1, 0, 0, 391 | 1, 1, 0, 0, 392 | 0, 0, 0, 0 ], 393 | [ 1, 0, 0, 0, 394 | 1, 1, 1, 0, 395 | 0, 0, 0, 0, 396 | 0, 0, 0, 0 ], 397 | [ 1, 1, 0, 0, 398 | 1, 0, 0, 0, 399 | 1, 0, 0, 0, 400 | 0, 0, 0, 0 ], 401 | ], 402 | [ 403 | [ 1, 1, 0, 0, 404 | 1, 1, 0, 0, 405 | 0, 0, 0, 0, 406 | 0, 0, 0, 0, ], 407 | ], 408 | [ 409 | [ 1, 1, 0, 0, 410 | 0, 1, 1, 0, 411 | 0, 0, 0, 0, 412 | 0, 0, 0, 0 ], 413 | [ 0, 1, 0, 0, 414 | 1, 1, 0, 0, 415 | 1, 0, 0, 0, 416 | 0, 0, 0, 0 ], 417 | ], 418 | [ 419 | [ 0, 1, 1, 0, 420 | 1, 1, 0, 0, 421 | 0, 0, 0, 0, 422 | 0, 0, 0, 0 ], 423 | [ 1, 0, 0, 0, 424 | 1, 1, 0, 0, 425 | 0, 1, 0, 0, 426 | 0, 0, 0, 0 ], 427 | ], 428 | [ 429 | [ 0, 1, 0, 0, 430 | 1, 1, 1, 0, 431 | 0, 0, 0, 0, 432 | 0, 0, 0, 0 ], 433 | [ 1, 0, 0, 0, 434 | 1, 1, 0, 0, 435 | 1, 0, 0, 0, 436 | 0, 0, 0, 0 ], 437 | [ 1, 1, 1, 0, 438 | 0, 1, 0, 0, 439 | 0, 0, 0, 0, 440 | 0, 0, 0, 0 ], 441 | [ 0, 1, 0, 0, 442 | 1, 1, 0, 0, 443 | 0, 1, 0, 0, 444 | 0, 0, 0, 0 ], 445 | ], 446 | ]; 447 | -------------------------------------------------------------------------------- /lib/games/tetris/tetris_renderder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import './tetris_data.dart'; 3 | import './tetris.dart'; 4 | 5 | class NextShapeRenderder extends StatelessWidget { 6 | final Size size; 7 | NextShapeRenderder({ 8 | @required this.size, 9 | }); 10 | @override 11 | Widget build(BuildContext context) { 12 | return CustomPaint( 13 | painter: _NextShapePainter( 14 | data: Tetris.dataOf(context), 15 | ), 16 | size: this.size, 17 | ); 18 | } 19 | } 20 | class _NextShapePainter extends CustomPainter { 21 | final TetrisData data; 22 | _NextShapePainter({ 23 | @required this.data, 24 | }); 25 | @override 26 | void paint(Canvas canvas, Size size) { 27 | final nextShape = data.nextShape; 28 | if (nextShape == null) return; 29 | Size blockSize = Size(size.width / 5, size.height / 5); 30 | Size blockWithBorderSize = Size(blockSize.width - 1, blockSize.height - 1); 31 | Paint paint = Paint() 32 | ..color = Colors.white 33 | ..strokeJoin = StrokeJoin.round 34 | ..strokeCap = StrokeCap.round 35 | ..strokeWidth = 1; 36 | double centerX = size.width / 2; 37 | double blockWidthHalf = blockSize.width / 2; 38 | double left = centerX - blockWidthHalf - (nextShape.width / 2.5 * blockSize.width); 39 | for (int y = 0; y < nextShape.height; y++) { 40 | for (int x = 0; x < nextShape.width; x++) { 41 | int colorIndex = nextShape.block[y][x]; 42 | if (colorIndex == 0) continue; 43 | paint.color = TETRIS_COLORS[colorIndex]; 44 | _drawBlock( 45 | canvas, paint, 46 | offset: Offset(left + (blockSize.width * x), blockSize.height * y), 47 | size: blockWithBorderSize, 48 | ); 49 | } 50 | } 51 | } 52 | @override 53 | bool shouldRepaint(CustomPainter oldDelegate) => true; 54 | } 55 | 56 | class TertisRenderder extends StatefulWidget { 57 | final Size size; 58 | TertisRenderder({ 59 | @required this.size, 60 | }); 61 | @override 62 | _TertisRenderderState createState() => _TertisRenderderState(); 63 | } 64 | 65 | class _TertisRenderderState extends State { 66 | @override 67 | void initState() { 68 | super.initState(); 69 | } 70 | @override 71 | void dispose() { 72 | super.dispose(); 73 | } 74 | @override 75 | Widget build(BuildContext context) { 76 | return CustomPaint( 77 | foregroundPainter: _TetrisPainter( 78 | data: Tetris.dataOf(context), 79 | ), 80 | painter: TetrisBGPainter(), 81 | size: widget.size, 82 | ); 83 | } 84 | } 85 | 86 | /// 處理魔術方塊的畫面渲染 87 | class _TetrisPainter extends CustomPainter { 88 | final TetrisData data; 89 | _TetrisPainter({ 90 | @required this.data, 91 | }); 92 | @override 93 | void paint(Canvas canvas, Size size) { 94 | List> panel = data.panel; 95 | Shape shape = data.currentShape; 96 | Size blockSize = Size(size.width / data.cols, size.height / data.rows); 97 | Size blockWithBorderSize = Size(blockSize.width - 1, blockSize.height - 1); 98 | Paint paint = Paint() 99 | ..color = Colors.white 100 | ..strokeJoin = StrokeJoin.round 101 | ..strokeCap = StrokeCap.round 102 | ..strokeWidth = 1; 103 | // 繪製目前整個面板上的方塊內容 104 | for (int y = 0; y < data.rows; y++) { 105 | for (int x = 0; x < data.cols; x++) { 106 | int colorIndex = panel[y][x]; 107 | if (colorIndex > 0) { 108 | paint.color = TETRIS_COLORS[colorIndex]; 109 | _drawBlock( 110 | canvas, paint, 111 | offset: Offset(blockSize.width * x, blockSize.height * y), 112 | size: blockWithBorderSize, 113 | ); 114 | } 115 | } 116 | } 117 | if (shape != null) { 118 | int fallingDownY = data.findFallingDownY(); 119 | shape.forEachBlock((value, x, y) { 120 | int colorIndex = shape.block[y][x]; 121 | if (colorIndex > 0) { 122 | // 繪製目前落下的魔術方塊 123 | paint.color = TETRIS_COLORS[colorIndex]; 124 | _drawBlock( 125 | canvas, paint, 126 | offset: Offset( 127 | (data.currentX + x) * blockSize.width, 128 | (data.currentY + y) * blockSize.height, 129 | ), 130 | size: blockWithBorderSize, 131 | ); 132 | // 繪製落下位置的預覽 133 | paint.color = Colors.white.withOpacity(0.33); 134 | _drawBlock( 135 | canvas, paint, 136 | offset: Offset( 137 | (data.currentX + x) * blockSize.width, 138 | (fallingDownY + y) * blockSize.height, 139 | ), 140 | size: blockWithBorderSize, 141 | ); 142 | } 143 | }); 144 | // paint.color = Colors.white; 145 | // canvas.drawLine( 146 | // Offset(panel.currentX * blockWidth, 0), 147 | // Offset(panel.currentX * blockWidth, size.height), 148 | // paint, 149 | // ); 150 | // canvas.drawLine( 151 | // Offset(panel.currentRight * blockWidth, 0), 152 | // Offset(panel.currentRight * blockWidth, size.height), 153 | // paint, 154 | // ); 155 | // canvas.drawLine( 156 | // Offset(0, panel.currentBottom * blockHeight), 157 | // Offset(size.width, panel.currentBottom * blockHeight), 158 | // paint, 159 | // ); 160 | } 161 | } 162 | @override 163 | bool shouldRepaint(_TetrisPainter oldDelegate) => true; 164 | } 165 | 166 | class TetrisBGPainter extends CustomPainter { 167 | @override 168 | void paint(Canvas canvas, Size size) { 169 | Rect rect = Offset.zero & size; 170 | Paint paint = Paint() 171 | ..strokeJoin = StrokeJoin.round 172 | ..strokeCap = StrokeCap.round 173 | ..strokeWidth = 1 174 | ..shader = LinearGradient( 175 | colors: [ 176 | Colors.purple, 177 | Colors.black, 178 | Colors.purple, 179 | ], 180 | stops: [ 181 | 0.0, 182 | 0.5, 183 | 1.0, 184 | ], 185 | ).createShader(rect); 186 | canvas.drawRect(rect, paint); 187 | } 188 | @override 189 | bool shouldRepaint(TetrisBGPainter oldDelegate) => true; 190 | } 191 | 192 | void _drawBlock(Canvas canvas, Paint paint, { 193 | @required Offset offset, 194 | @required Size size, 195 | }) { 196 | Rect rect = offset & size; 197 | paint.style = PaintingStyle.fill; 198 | canvas.drawRect(rect, paint); 199 | 200 | paint.shader = LinearGradient( 201 | colors: [ 202 | Colors.white.withOpacity(0.75), 203 | Colors.white.withOpacity(0.3), 204 | paint.color, 205 | ], 206 | stops: [ 207 | 0.0, 208 | 0.75, 209 | 1.0, 210 | ], 211 | ).createShader(rect); 212 | canvas.drawRect(rect, paint); 213 | paint.shader = null; 214 | // paint.style = PaintingStyle.stroke; 215 | // paint.color = Colors.white; 216 | // canvas.drawRect(rect, paint); 217 | } -------------------------------------------------------------------------------- /lib/games/tic_tac_toe/tic_tac_toe.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../../utlis.dart'; 5 | 6 | const int GRIDS = 9; 7 | /// 隨機產生器種子 8 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 9 | 10 | class TicTacToe extends StatefulWidget { 11 | TicTacToe({ Key key, }) : super(key: key); 12 | @override 13 | _TicTacToeState createState() => _TicTacToeState(); 14 | } 15 | 16 | class _TicTacToeState extends State { 17 | int turn; 18 | int remainStep; 19 | int winner; 20 | List grids; 21 | void reset() { 22 | setState(() { 23 | turn = randomGenerator.nextInt(2); 24 | remainStep = GRIDS; 25 | winner = null; 26 | grids = List.filled(GRIDS, null); 27 | }); 28 | } 29 | bool hasWin(int turn) { 30 | return ( 31 | (grids[0] == turn && grids[0] == grids[1] && grids[0] == grids[2]) || 32 | (grids[0] == turn && grids[0] == grids[3] && grids[0] == grids[6]) || 33 | (grids[0] == turn && grids[0] == grids[4] && grids[0] == grids[8]) || 34 | (grids[1] == turn && grids[1] == grids[4] && grids[1] == grids[7]) || 35 | (grids[2] == turn && grids[2] == grids[5] && grids[2] == grids[8]) || 36 | (grids[3] == turn && grids[3] == grids[4] && grids[3] == grids[5]) || 37 | (grids[6] == turn && grids[6] == grids[7] && grids[6] == grids[8]) || 38 | (grids[2] == turn && grids[2] == grids[4] && grids[2] == grids[6]) 39 | ); 40 | } 41 | @override 42 | void initState() { 43 | super.initState(); 44 | reset(); 45 | } 46 | @override 47 | Widget build(BuildContext context) { 48 | return Column( 49 | children: [ 50 | GridView.builder( 51 | physics: NeverScrollableScrollPhysics(), 52 | shrinkWrap: true, 53 | scrollDirection: Axis.vertical, 54 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 55 | crossAxisCount: 3, 56 | mainAxisSpacing: 8, 57 | crossAxisSpacing: 8, 58 | ), 59 | padding: EdgeInsets.all(8), 60 | itemCount: GRIDS, 61 | itemBuilder: (BuildContext context, int index) { 62 | int grid = grids[index]; 63 | Widget widget; 64 | switch (grid) { 65 | case 0: 66 | widget = Icon(Icons.close); break; 67 | case 1: 68 | widget = Icon(Icons.radio_button_unchecked); break; 69 | default: 70 | widget = Text(''); break; 71 | } 72 | return IgnorePointer( 73 | ignoring: winner != null, 74 | child: InkWell( 75 | child: Container( 76 | height: 240, 77 | alignment: Alignment.center, 78 | color: winner == null ? Colors.black26 : Colors.black12, 79 | child: widget, 80 | ), 81 | onTap: () { 82 | if (grids[index] != null) { 83 | alertMessage( 84 | context: context, 85 | title: '此格已下過', 86 | okText: '知道了', 87 | onOK: () { Navigator.pop(context); } 88 | ); 89 | return; 90 | } 91 | setState(() { 92 | grids[index] = turn; 93 | if (hasWin(turn)) { 94 | winner = turn; 95 | alertMessage( 96 | context: context, 97 | titlePrefix: Icon(winner == 0 ? Icons.close : Icons.radio_button_unchecked), 98 | title: ' 獲勝了', 99 | okText: '再玩一次', 100 | onOK: () { 101 | Navigator.pop(context); 102 | reset(); 103 | } 104 | ); 105 | } else if (grids.fold(true, (isFlat, grid) => isFlat && grid != null)) { 106 | winner = -1; 107 | alertMessage( 108 | context: context, 109 | title: '平手', 110 | okText: '再玩一次', 111 | onOK: () { 112 | Navigator.pop(context); 113 | reset(); 114 | } 115 | ); 116 | } 117 | turn = turn == 1 ? 0 : 1; 118 | }); 119 | }, 120 | ), 121 | ); 122 | }, 123 | ), 124 | Row( 125 | mainAxisAlignment: MainAxisAlignment.center, 126 | children: [ 127 | Text('輪到 '), 128 | Icon(turn == 0 ? Icons.close : Icons.radio_button_unchecked), 129 | ], 130 | ), 131 | Container( 132 | alignment: Alignment.center, 133 | child: RaisedButton( 134 | child: Text('重來'), 135 | onPressed: reset, 136 | ), 137 | ), 138 | ], 139 | ); 140 | } 141 | } -------------------------------------------------------------------------------- /lib/layout.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'main.dart'; 4 | import 'screens/settings.dart'; 5 | 6 | const ICON_TITLES = [ 7 | '踩地雷', 8 | '井字遊戲', 9 | '俄羅斯方塊', 10 | ]; 11 | 12 | class Layout extends StatefulWidget { 13 | final LayoutState initialState; 14 | final List children; 15 | Layout({ 16 | this.initialState, 17 | @required this.children, 18 | }); 19 | @override 20 | LayoutState createState() => LayoutState( 21 | title: this.initialState?.title, 22 | currentBottomNavIndex: this.initialState?.currentBottomNavIndex, 23 | ); 24 | } 25 | 26 | class LayoutState extends State { 27 | final bool hideBottomNav; 28 | String title; 29 | int currentBottomNavIndex; 30 | LayoutState({ 31 | this.title = '', 32 | this.currentBottomNavIndex = 0, 33 | this.hideBottomNav = false, 34 | }); 35 | void updateTitle(String newTitle) { 36 | setState(() { 37 | title = newTitle; 38 | }); 39 | } 40 | void updateBottomNavIndex(int newIndex) { 41 | setState(() { 42 | currentBottomNavIndex = newIndex; 43 | title = ICON_TITLES[newIndex]; 44 | }); 45 | } 46 | @override 47 | void initState() { 48 | super.initState(); 49 | if (!hideBottomNav) { 50 | updateTitle(ICON_TITLES[currentBottomNavIndex]); 51 | } 52 | } 53 | @override 54 | Widget build(BuildContext context) { 55 | return Scaffold( 56 | appBar: AppBar( 57 | title: Text(title), 58 | actions: [ 59 | IconButton( 60 | icon: Icon(Icons.settings), 61 | onPressed: onPressSettings, 62 | ) 63 | ], 64 | ), 65 | drawer: Drawer( 66 | child: ListView( 67 | padding: EdgeInsets.zero, 68 | children: [ 69 | DrawerHeader( 70 | child: Text( 71 | '玩個遊戲', 72 | style: TextStyle( 73 | color: Colors.white, 74 | fontSize: 24, 75 | ), 76 | ), 77 | decoration: BoxDecoration( 78 | color: Colors.blue, 79 | ), 80 | ), 81 | ListTile( 82 | title: Text('關閉應用程式'), 83 | onTap: closeApp, 84 | ), 85 | ], 86 | ), 87 | ), 88 | body: widget.children[currentBottomNavIndex], 89 | bottomNavigationBar: hideBottomNav 90 | ? null 91 | : BottomNavigationBar( 92 | type: BottomNavigationBarType.fixed, 93 | currentIndex: currentBottomNavIndex, 94 | onTap: updateBottomNavIndex, 95 | items: [ 96 | BottomNavigationBarItem( 97 | icon: Icon(Icons.brightness_high), 98 | title: Text(ICON_TITLES[0]), 99 | ), 100 | BottomNavigationBarItem( 101 | icon: Icon(Icons.grid_on), 102 | title: Text(ICON_TITLES[1]), 103 | ), 104 | BottomNavigationBarItem( 105 | icon: Icon(Icons.gamepad), 106 | title: Text(ICON_TITLES[2]), 107 | ), 108 | ], 109 | ), 110 | ); 111 | } 112 | void closeApp() { 113 | exit(0); 114 | } 115 | void onPressSettings() { 116 | Navigator.push(context, MaterialPageRoute( 117 | builder: (BuildContext context) { 118 | final state = App.of(context); 119 | return SettingsScreen( 120 | configs: state.configs, 121 | configUpdater: state.updateConfig, 122 | ); 123 | }, 124 | fullscreenDialog: true, 125 | )); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'configs.dart'; 4 | import 'screens/home.dart'; 5 | 6 | void main() => runApp(App()); 7 | 8 | class App extends StatefulWidget { 9 | App({ Key key }) : super(key: key); 10 | static AppState of(BuildContext context) { 11 | final _AppStateContainer widgetInstance = context.inheritFromWidgetOfExactType(_AppStateContainer); 12 | return widgetInstance.state; 13 | } 14 | @override 15 | AppState createState() => AppState(); 16 | } 17 | 18 | class AppState extends State { 19 | GameConfigs configs = GameConfigs(mineweeperLevel: Level.easy); 20 | Future updateConfig(GameConfigs newConfigs) async { 21 | setState(() { configs = newConfigs; }); 22 | SharedPreferences prefs = await SharedPreferences.getInstance(); 23 | prefs.setInt('mineweeperLevel', newConfigs.mineweeperLevel.index); 24 | } 25 | @override 26 | void initState() { 27 | super.initState(); 28 | } 29 | @override 30 | void dispose() { 31 | super.dispose(); 32 | // App 關閉 33 | } 34 | @override 35 | Widget build(BuildContext context) { 36 | return _AppStateContainer( 37 | state: this, 38 | child: MaterialApp( 39 | title: 'Play a Game', 40 | theme: ThemeData( 41 | primarySwatch: Colors.purple, 42 | accentColor: Colors.orangeAccent[400], 43 | ), 44 | home: HomeScreen(), 45 | ), 46 | ); 47 | } 48 | } 49 | 50 | class _AppStateContainer extends InheritedWidget { 51 | final AppState state; 52 | _AppStateContainer({ 53 | @required this.state, 54 | @required Widget child, 55 | }) : super(child: child); 56 | @override 57 | bool updateShouldNotify(_AppStateContainer old) => true; 58 | } 59 | -------------------------------------------------------------------------------- /lib/screens/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import '../games/mineaweeper/minesweeper.dart'; 6 | // import '../games/number_2048/number_2048.dart'; 7 | import '../games/tetris/tetris.dart'; 8 | import '../games/tic_tac_toe/tic_tac_toe.dart'; 9 | import '../widgets/loading.dart'; 10 | import '../configs.dart'; 11 | import '../layout.dart'; 12 | import '../main.dart'; 13 | 14 | class HomeScreen extends StatefulWidget { 15 | HomeScreen({ Key key, }) : super(key: key); 16 | @override 17 | _HomeScreenState createState() => _HomeScreenState(); 18 | } 19 | 20 | class _HomeScreenState extends State { 21 | bool _inited = false; 22 | void _initData() async { 23 | try { 24 | SharedPreferences prefs = await SharedPreferences.getInstance(); 25 | int levelIndex = prefs.getInt('mineweeperLevel'); 26 | Level mineweeperLevel = levelIndex != null 27 | ? Level.values[levelIndex] 28 | : Level.easy; 29 | AppState appState = App.of(context); 30 | appState.configs.mineweeperLevel = mineweeperLevel; 31 | await appState.updateConfig(appState.configs); 32 | } catch (ex) { 33 | print(ex); 34 | } finally { 35 | setState(() { _inited = true; }); 36 | } 37 | } 38 | @override 39 | void initState() { 40 | super.initState(); 41 | _initData(); 42 | } 43 | @override 44 | Widget build(BuildContext context) { 45 | if (!_inited) return Loading(); 46 | return Layout( 47 | initialState: LayoutState( 48 | currentBottomNavIndex: 0, 49 | ), 50 | children: [ 51 | Minesweeper(), 52 | TicTacToe(), 53 | Tetris(), 54 | // Number2048(), 55 | ], 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /lib/screens/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import '../configs.dart'; 4 | 5 | typedef void ConfigUpdater(GameConfigs configs); 6 | 7 | class SettingsScreen extends StatefulWidget { 8 | final GameConfigs configs; 9 | final ConfigUpdater configUpdater; 10 | SettingsScreen({ 11 | Key key, 12 | @required this.configs, 13 | @required this.configUpdater, 14 | }) : super(key: key); 15 | @override 16 | _SettingsScreenState createState() => _SettingsScreenState(); 17 | } 18 | 19 | class _SettingsScreenState extends State { 20 | Level currentMineweeperLevel; 21 | @override 22 | void initState() { 23 | super.initState(); 24 | currentMineweeperLevel = widget.configs.mineweeperLevel; 25 | } 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: Text('設定'), 31 | ), 32 | backgroundColor: Colors.white, 33 | body: ListView( 34 | children: [ 35 | Container( 36 | padding: EdgeInsets.only(left: 16, right: 16, top: 16), 37 | child: Text('踩地雷'), 38 | ), 39 | ListTile( 40 | title: Text('難度'), 41 | trailing: Text(LevelText[currentMineweeperLevel]), 42 | onTap: () async { 43 | showCupertinoModalPopup( 44 | context: context, 45 | builder: (BuildContext popupContext) { 46 | void setLevel(Level level) { 47 | widget.configs.mineweeperLevel = level; 48 | widget.configUpdater(widget.configs); 49 | Navigator.pop(popupContext); 50 | setState(() { 51 | currentMineweeperLevel = level; 52 | }); 53 | } 54 | return CupertinoActionSheet( 55 | title: Text('設定難度'), 56 | actions: [ 57 | CupertinoButton( 58 | child: Text(LevelText[Level.easy]), 59 | onPressed: () => setLevel(Level.easy), 60 | ), 61 | CupertinoButton( 62 | child: Text(LevelText[Level.medium]), 63 | onPressed: () => setLevel(Level.medium), 64 | ), 65 | CupertinoButton( 66 | child: Text(LevelText[Level.difficult]), 67 | onPressed: () => setLevel(Level.difficult), 68 | ), 69 | ], 70 | cancelButton: CupertinoButton( 71 | child: Text('取消'), 72 | onPressed: () { 73 | Navigator.pop(popupContext); 74 | }, 75 | ), 76 | ); 77 | }, 78 | ); 79 | }, 80 | ), 81 | ], 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/utlis.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Future alertMessage({ 4 | @required BuildContext context, 5 | @required String title, 6 | @required String okText, 7 | Widget titlePrefix, 8 | String cancelText, 9 | VoidCallback onOK, 10 | VoidCallback onCancel, 11 | }) { 12 | return showDialog( 13 | context: context, 14 | builder: (BuildContext context) { 15 | final titleWidgets = [ 16 | Text(title, style: TextStyle(fontSize: 32)), 17 | ]; 18 | if (titlePrefix != null) { 19 | titleWidgets.insert(0, titlePrefix); 20 | } 21 | final actionWidgets = [ 22 | SimpleDialogOption( 23 | child: Text(okText), 24 | onPressed: onOK, 25 | ), 26 | ]; 27 | if (cancelText != null) { 28 | actionWidgets.add( 29 | SimpleDialogOption( 30 | child: Text(cancelText), 31 | onPressed: onCancel, 32 | ) 33 | ); 34 | } 35 | return SimpleDialog( 36 | title: Row( 37 | mainAxisAlignment: MainAxisAlignment.start, 38 | children: titleWidgets, 39 | ), 40 | children: actionWidgets, 41 | ); 42 | }, 43 | ); 44 | } -------------------------------------------------------------------------------- /lib/widgets/bonuce_icon.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class BonuceIcon extends StatefulWidget { 5 | final IconData icon; 6 | final double size; 7 | final Color color; 8 | 9 | BonuceIcon( 10 | this.icon, { 11 | Key key, 12 | this.size = 24.0, 13 | this.color, 14 | }) : super(key: key); 15 | @override 16 | State createState() => _BonuceIconState(); 17 | } 18 | 19 | class _BonuceIconState extends State with SingleTickerProviderStateMixin { 20 | final double dx = 4.0; 21 | AnimationController controller; 22 | Animation animation; 23 | 24 | @override 25 | initState() { 26 | super.initState(); 27 | controller = AnimationController( 28 | duration: Duration(milliseconds: 300), vsync: this); 29 | animation = Tween(begin: widget.size, end: widget.size + dx) 30 | .animate(controller); 31 | 32 | animation.addStatusListener((status) { 33 | if (status == AnimationStatus.completed) { 34 | controller.reverse(); 35 | } 36 | // else if (status == AnimationStatus.dismissed) { 37 | // Future.delayed(Duration(seconds: 2), () { 38 | // if (!mounted) return; 39 | // controller?.forward(); 40 | // }); 41 | // } 42 | }); 43 | controller.forward(); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | controller.dispose(); 49 | super.dispose(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return _Animator( 55 | icon: widget.icon, 56 | animation: animation, 57 | color: widget.color, 58 | size: widget.size + dx, 59 | ); 60 | } 61 | } 62 | 63 | class _Animator extends AnimatedWidget { 64 | final double size; 65 | final IconData icon; 66 | final Color color; 67 | _Animator({ 68 | Key key, 69 | this.icon, 70 | this.size, 71 | this.color, 72 | Animation animation, 73 | }) : super(key: key, listenable: animation); 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | final Animation animation = listenable; 78 | return Container( 79 | width: size, 80 | height: size, 81 | child: Center( 82 | child: Icon( 83 | icon, 84 | size: animation.value, 85 | color: color, 86 | ), 87 | ), 88 | ); 89 | } 90 | } -------------------------------------------------------------------------------- /lib/widgets/loading.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | class Loading extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | final child = Platform.isIOS 9 | ? CupertinoActivityIndicator() 10 | : CircularProgressIndicator( 11 | backgroundColor: Colors.transparent, 12 | ); 13 | return Container( 14 | alignment: Alignment.center, 15 | color: Colors.white, 16 | width: double.infinity, 17 | height: double.infinity, 18 | child: child, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/widgets/swipeable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef void SwipeCallback(SwipeDirection direction); 4 | 5 | class Swipeable extends StatefulWidget { 6 | static double swipeLimit = 20; 7 | final Widget child; 8 | final SwipeCallback onSwipe; 9 | Swipeable({ 10 | Key key, 11 | @required this.child, 12 | @required this.onSwipe, 13 | }) : super(key: key); 14 | @override 15 | _SwipeableState createState() => _SwipeableState(); 16 | } 17 | 18 | class _SwipeableState extends State { 19 | double dx; 20 | double dy; 21 | @override 22 | Widget build(BuildContext context) { 23 | return GestureDetector( 24 | child: widget.child, 25 | onPanDown: (details) { 26 | dx = dy = 0; 27 | }, 28 | onPanUpdate: (details) { 29 | if (dx == null || dy == null) return; 30 | dx += details.delta.dx; 31 | dy += details.delta.dy; 32 | SwipeDirection direction; 33 | if (dx > Swipeable.swipeLimit) { 34 | direction = SwipeDirection.right; 35 | dx = null; 36 | } else if (dx < -Swipeable.swipeLimit) { 37 | direction = SwipeDirection.left; 38 | dx = null; 39 | } 40 | if (dy > Swipeable.swipeLimit) { 41 | if (direction == SwipeDirection.right) { 42 | direction = SwipeDirection.downRight; 43 | } else if (direction == SwipeDirection.left) { 44 | direction = SwipeDirection.downLeft; 45 | } else { 46 | direction = SwipeDirection.down; 47 | } 48 | dy = null; 49 | } else if (dy < -Swipeable.swipeLimit) { 50 | if (direction == SwipeDirection.right) { 51 | direction = SwipeDirection.upRight; 52 | } else if (direction == SwipeDirection.left) { 53 | direction = SwipeDirection.upLeft; 54 | } else { 55 | direction = SwipeDirection.up; 56 | } 57 | dy = null; 58 | } 59 | if (direction == null) return; 60 | widget.onSwipe(direction); 61 | }, 62 | ); 63 | } 64 | } 65 | 66 | enum SwipeDirection { 67 | up, 68 | upLeft, 69 | upRight, 70 | right, 71 | downRight, 72 | down, 73 | downLeft, 74 | left, 75 | } 76 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.2.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.0.4" 18 | charcode: 19 | dependency: transitive 20 | description: 21 | name: charcode 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.2" 25 | collection: 26 | dependency: transitive 27 | description: 28 | name: collection 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.14.11" 32 | cupertino_icons: 33 | dependency: "direct main" 34 | description: 35 | name: cupertino_icons 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "0.1.2" 39 | flutter: 40 | dependency: "direct main" 41 | description: flutter 42 | source: sdk 43 | version: "0.0.0" 44 | flutter_test: 45 | dependency: "direct dev" 46 | description: flutter 47 | source: sdk 48 | version: "0.0.0" 49 | matcher: 50 | dependency: transitive 51 | description: 52 | name: matcher 53 | url: "https://pub.dartlang.org" 54 | source: hosted 55 | version: "0.12.5" 56 | meta: 57 | dependency: transitive 58 | description: 59 | name: meta 60 | url: "https://pub.dartlang.org" 61 | source: hosted 62 | version: "1.1.6" 63 | path: 64 | dependency: transitive 65 | description: 66 | name: path 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "1.6.2" 70 | pedantic: 71 | dependency: transitive 72 | description: 73 | name: pedantic 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.7.0" 77 | quiver: 78 | dependency: transitive 79 | description: 80 | name: quiver 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "2.0.3" 84 | shared_preferences: 85 | dependency: "direct main" 86 | description: 87 | name: shared_preferences 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "0.5.3+1" 91 | sky_engine: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.99" 96 | source_span: 97 | dependency: transitive 98 | description: 99 | name: source_span 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.5.5" 103 | stack_trace: 104 | dependency: transitive 105 | description: 106 | name: stack_trace 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.9.3" 110 | stream_channel: 111 | dependency: transitive 112 | description: 113 | name: stream_channel 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "2.0.0" 117 | string_scanner: 118 | dependency: transitive 119 | description: 120 | name: string_scanner 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.0.4" 124 | term_glyph: 125 | dependency: transitive 126 | description: 127 | name: term_glyph 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.0" 131 | test_api: 132 | dependency: transitive 133 | description: 134 | name: test_api 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "0.2.5" 138 | typed_data: 139 | dependency: transitive 140 | description: 141 | name: typed_data 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.1.6" 145 | vector_math: 146 | dependency: transitive 147 | description: 148 | name: vector_math 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.0.8" 152 | sdks: 153 | dart: ">=2.2.2 <3.0.0" 154 | flutter: ">=1.5.0 <2.0.0" 155 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_play_a_game 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+4 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | cupertino_icons: ^0.1.2 21 | audioplayer: ^0.5.2 22 | shared_preferences: ^0.5.3+1 23 | path_provider: ^1.1.0 24 | flutter: 25 | sdk: flutter 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://www.dartlang.org/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | assets: 42 | - assets/tetris/gameover.mp3 43 | - assets/tetris/theme.mp3 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /test/minesweeper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_play_a_game/main.dart'; 4 | 5 | void main() { 6 | testWidgets('踩地雷 - 開始新遊戲', (WidgetTester tester) async { 7 | await tester.pumpWidget(App()); 8 | 9 | expect(find.text('000'), findsOneWidget); 10 | expect(find.byIcon(Icons.mood), findsOneWidget); 11 | 12 | await tester.tap(find.byIcon(Icons.mood)); 13 | await tester.pump(); 14 | 15 | // expect(find.text('000'), findsNothing); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /web/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | exclude: [build/**] 14 | -------------------------------------------------------------------------------- /web/lib/configs.dart: -------------------------------------------------------------------------------- 1 | /// 難度的型別宣告 2 | enum Level { 3 | easy, 4 | medium, 5 | difficult 6 | } 7 | /// 對應難度的顯示文字 8 | const LevelText = { 9 | Level.easy: '簡單', 10 | Level.medium: '中等', 11 | Level.difficult: '困難', 12 | }; 13 | /// 遊戲設定 14 | class GameConfigs { 15 | Level mineweeperLevel; 16 | GameConfigs({ 17 | this.mineweeperLevel, 18 | }); 19 | } -------------------------------------------------------------------------------- /web/lib/frame_scheduler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | import 'package:flutter_web/scheduler.dart'; 3 | 4 | /// 向系統註冊監聽幀渲染的回調 5 | /// 由於 [addPersistentFrameCallback] 註冊後無法解除 6 | /// 因此只會執行一次 7 | void registerFrameScheduler() { 8 | if (FrameScheduler.isRegistered) { 9 | print('FrameScheduler is registered.'); 10 | return; 11 | } 12 | FrameScheduler.isRegistered = true; 13 | FrameScheduler.scheduler = SchedulerBinding.instance; 14 | FrameScheduler.scheduler.addPersistentFrameCallback(FrameScheduler.frameCallback); 15 | print('FrameScheduler is registered.'); 16 | } 17 | /// 提供新增或解除監聽幀的類別 18 | class FrameScheduler { 19 | /// 是否已向系統註冊全域的幀監聽回調 20 | static bool isRegistered = false; 21 | /// 系統的幀排程實體 22 | static SchedulerBinding scheduler; 23 | /// 自行定義的幀監聽器 24 | static Map frameListeners = Map(); 25 | /// 掛載到全域的幀監聽回調 26 | static FrameCallback frameCallback = (Duration timestamp) { 27 | if (FrameScheduler.frameListeners.isEmpty) return; 28 | FrameScheduler.frameListeners.values.forEach((listener) { listener(timestamp); }); 29 | // 請 UI 層繼續執行渲染 30 | FrameScheduler.scheduler.scheduleFrame(); 31 | }; 32 | /// 新增一筆幀監聽回調,回傳該回調的鍵值 33 | static Key addFrameListener(FrameCallback frameListener) { 34 | Key listenerKey = UniqueKey(); 35 | FrameScheduler.frameListeners[listenerKey] = frameListener; 36 | return listenerKey; 37 | } 38 | /// 輸入回調的鍵值,移除該幀監聽回調 39 | static bool removeFrameListener(Key listenerKey) { 40 | bool isContainsKey = FrameScheduler.frameListeners.containsKey(listenerKey); 41 | if (isContainsKey) FrameScheduler.frameListeners.remove(listenerKey); 42 | return isContainsKey; 43 | } 44 | /// 移除所有的幀監聽回調 45 | static void removeAllFrameListeners() { 46 | FrameScheduler.frameListeners.clear(); 47 | } 48 | } -------------------------------------------------------------------------------- /web/lib/games/mineaweeper/minesweeper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'package:flutter_web/material.dart'; 4 | import '../../configs.dart'; 5 | import '../../main.dart'; 6 | import '../../utlis.dart'; 7 | import '../../widgets/bonuce_icon.dart'; 8 | 9 | const int ROWS = 14; 10 | const int COLUMNS = 12; 11 | const int TOTAL_GRIDS = ROWS * COLUMNS; 12 | /// 周圍炸彈數的數字顏色 13 | const BOMB_COLORS = [ 14 | Colors.transparent, 15 | Colors.blue, 16 | Colors.green, 17 | Colors.red, 18 | Colors.indigo, 19 | Colors.pink, 20 | Colors.lightGreen, 21 | Colors.grey, 22 | Colors.black, 23 | ]; 24 | /// 1 秒鐘的定義 25 | const ONE_SEC = Duration(seconds: 1); 26 | /// 隨機產生器種子 27 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 28 | 29 | class Minesweeper extends StatefulWidget { 30 | Minesweeper({ Key key, }) : super(key: key); 31 | @override 32 | _MinesweeperState createState() => _MinesweeperState(); 33 | } 34 | 35 | class _MinesweeperState extends State { 36 | /// 炸彈旗標設置 (座標對應布林值) 37 | final Map> flags = Map(); 38 | /// 每個格子單位 39 | final List> grids = List(); 40 | /// 全部的炸彈數量 41 | int _totalBombs; 42 | /// 是否踩到炸彈遊戲結束 43 | bool _isGameOver = false; 44 | /// 所有格子全搜尋,設置旗子數等於所有炸彈數 45 | bool _isGameWon = false; 46 | /// 計數遊戲時間 47 | Timer _timer; 48 | /// 一場遊戲開始時間 49 | DateTime gameStart; 50 | /// 一場遊戲結束時間 51 | DateTime gameEnd; 52 | /// 取得目前設置的旗子數 53 | int get flagCount { 54 | int _flagCount = 0; 55 | flags.forEach((index, row) { 56 | _flagCount += row.entries.length; 57 | }); 58 | return _flagCount; 59 | } 60 | void resetGrids(bool isInit) { 61 | flags.clear(); 62 | grids.clear(); 63 | for (int r = 0; r < ROWS; r++) { 64 | grids.add(List.generate(COLUMNS, (x) => GameGrid(isSearched: isInit))); 65 | } 66 | } 67 | /// 建立新遊戲 68 | void createGame() { 69 | final Level level = App.of(context).configs.mineweeperLevel; 70 | int bombAmount; 71 | if (level == Level.difficult) { 72 | bombAmount = 73 | (TOTAL_GRIDS * 0.5).round() + 74 | randomGenerator.nextInt(10) - 75 | randomGenerator.nextInt(10); 76 | } else if (level == Level.medium) { 77 | bombAmount = 78 | (TOTAL_GRIDS * 0.25).round() + 79 | randomGenerator.nextInt(10) - 80 | randomGenerator.nextInt(10); 81 | } else { 82 | bombAmount = 83 | (TOTAL_GRIDS * 0.1).round() + 84 | randomGenerator.nextInt(10) - 85 | randomGenerator.nextInt(10); 86 | } 87 | bombAmount = min(bombAmount, TOTAL_GRIDS); 88 | int total = bombAmount; 89 | resetGrids(false); 90 | while (bombAmount > 0) { 91 | final rY = randomGenerator.nextInt(ROWS); 92 | final rX = randomGenerator.nextInt(COLUMNS); 93 | if (!grids[rY][rX].hasBomb) { 94 | grids[rY][rX].hasBomb = true; 95 | bombAmount--; 96 | } 97 | } 98 | setState(() { 99 | _totalBombs = total; 100 | _isGameOver = _isGameWon = false; 101 | _timer?.cancel(); 102 | _timer = Timer.periodic(ONE_SEC, (Timer timer) { 103 | if (_isGameOver || _isGameWon) { 104 | timer.cancel(); 105 | return; 106 | } 107 | setState(() { 108 | gameEnd = DateTime.now(); 109 | }); 110 | }, 111 | ); 112 | gameStart = gameEnd = DateTime.now(); 113 | }); 114 | } 115 | /// 搜尋周圍 8 格,若目標周圍的炸彈數為 0 116 | /// 則遞迴搜尋周圍 8 格 117 | void searchBomb(int x, int y) { 118 | if (x < 0 || y < 0 || x >= COLUMNS || y >= ROWS) return; 119 | if (grids[y][x].isSearched) return; 120 | int bombs = 0; 121 | for (int dy = -1; dy <= 1; dy++) { 122 | for (int dx = -1; dx <= 1; dx++) { 123 | if (dy == 0 && dx == 0) continue; 124 | int ay = y + dy; 125 | int ax = x + dx; 126 | if (ax < 0 || ay < 0 || ax >= COLUMNS || ay >= ROWS) continue; 127 | final grid = grids[ay][ax]; 128 | if (grid.hasBomb) { 129 | bombs += 1; 130 | } 131 | } 132 | } 133 | grids[y][x].aroundBombs = bombs; 134 | grids[y][x].isSearched = true; 135 | if (bombs == 0) { 136 | for (int dy = -1; dy <= 1; dy++) { 137 | for (int dx = -1; dx <= 1; dx++) { 138 | if (dy == 0 && dx == 0) continue; 139 | int ay = y + dy; 140 | int ax = x + dx; 141 | if (ax < 0 || ay < 0 || ax >= COLUMNS || ay >= ROWS) continue; 142 | searchBomb(ax, ay); 143 | } 144 | } 145 | } 146 | } 147 | /// 檢查目前狀態是否已經獲勝 148 | /// 設置旗子數等於所有炸彈數且所有格子全已搜尋 149 | bool checkWin() { 150 | if (_totalBombs != flagCount) { 151 | return false; 152 | } 153 | int searchedCount = 0; 154 | grids.forEach((row) { 155 | row.forEach((col) { 156 | if (col.isSearched) searchedCount++; 157 | }); 158 | }); 159 | return searchedCount + flagCount == TOTAL_GRIDS; 160 | } 161 | @override 162 | void initState() { 163 | super.initState(); 164 | resetGrids(true); 165 | } 166 | @override 167 | void dispose() { 168 | _timer?.cancel(); 169 | super.dispose(); 170 | } 171 | @override 172 | Widget build(BuildContext context) { 173 | String timeString = '00:00'; 174 | int remainBombs = 0; 175 | if (gameStart != null && gameEnd != null) { 176 | Duration gameTime = gameEnd.difference(gameStart); 177 | int minutes = gameTime.inMinutes; 178 | int seconds = (gameTime.inSeconds - (minutes * 60)); 179 | timeString = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; 180 | remainBombs = _totalBombs - flagCount; 181 | } 182 | return ListView( 183 | children: [ 184 | Row( 185 | mainAxisAlignment: MainAxisAlignment.spaceAround, 186 | children: [ 187 | Container( 188 | color: Colors.black, 189 | width: 100, 190 | alignment: Alignment.center, 191 | padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), 192 | child: Text( 193 | remainBombs.toString().padLeft(3, '0'), 194 | style: TextStyle( 195 | fontSize: 32, 196 | color: Colors.red, 197 | ), 198 | ), 199 | ), 200 | Container( 201 | margin: EdgeInsets.symmetric(vertical: 8), 202 | decoration: BoxDecoration( 203 | color: Colors.grey, 204 | borderRadius: BorderRadius.all(Radius.circular(48)), 205 | ), 206 | child: IconButton( 207 | icon: Icon( 208 | _isGameOver 209 | ? Icons.sentiment_very_dissatisfied 210 | : Icons.mood 211 | ), 212 | iconSize: 48.0, 213 | color: Colors.yellow[200], 214 | onPressed: () { 215 | if (_isGameOver == false && _isGameWon == false) { 216 | alertMessage( 217 | context: context, 218 | title: '建立新遊戲?', 219 | okText: '確定', 220 | cancelText: '取消', 221 | onOK: () { 222 | Navigator.pop(context); 223 | createGame(); 224 | }, 225 | onCancel: () { 226 | Navigator.pop(context); 227 | }, 228 | ); 229 | return; 230 | } 231 | createGame(); 232 | }, 233 | ), 234 | ), 235 | Container( 236 | color: Colors.black, 237 | width: 100, 238 | alignment: Alignment.center, 239 | padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), 240 | child: Text( 241 | timeString, 242 | style: TextStyle( 243 | fontSize: 32, 244 | color: Colors.red, 245 | ), 246 | ), 247 | ), 248 | ], 249 | ), 250 | GridView.builder( 251 | physics: NeverScrollableScrollPhysics(), 252 | shrinkWrap: true, 253 | scrollDirection: Axis.vertical, 254 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 255 | crossAxisCount: COLUMNS, 256 | mainAxisSpacing: 2, 257 | crossAxisSpacing: 2, 258 | ), 259 | padding: EdgeInsets.all(4), 260 | itemCount: ROWS * COLUMNS, 261 | itemBuilder: (BuildContext context, int index) { 262 | int y = (index / COLUMNS).floor(); 263 | int x = index - (COLUMNS * y); 264 | final grid = grids[y][x]; 265 | Widget widget; 266 | if (grid.isSearched) { 267 | if (grid.hasBomb) { 268 | widget = Icon(Icons.new_releases); 269 | } else { 270 | widget = Text( 271 | grid.aroundBombs > 0 272 | ? grid.aroundBombs.toString() 273 | : '', 274 | style: TextStyle( 275 | fontSize: 24, 276 | color: BOMB_COLORS[grid.aroundBombs], 277 | ), 278 | ); 279 | } 280 | } else if ( 281 | flags[y] is Map && 282 | flags[y][x] == true 283 | ) { 284 | widget = BonuceIcon( 285 | Icons.assistant_photo, 286 | color: Colors.red, 287 | size: 20, 288 | ); 289 | } else { 290 | widget = Text(''); 291 | } 292 | return IgnorePointer( 293 | ignoring: _isGameOver || _isGameWon, 294 | child: InkWell( 295 | child: Container( 296 | alignment: Alignment.center, 297 | decoration: BoxDecoration( 298 | color: grid.isSearched && grid.hasBomb 299 | ? Colors.red 300 | : _isGameOver 301 | ? Colors.black12 302 | : Colors.black26, 303 | border: grid.isSearched 304 | ? null 305 | : Border( 306 | left: BorderSide(color: Colors.grey[300], width: 4), 307 | top: BorderSide(color: Colors.grey[300], width: 4), 308 | right: BorderSide(color: Colors.black26, width: 4), 309 | bottom: BorderSide(color: Colors.black26, width: 4), 310 | ), 311 | ), 312 | child: widget, 313 | ), 314 | onTap: () { 315 | if (!grid.isSearched && grid.hasBomb) { 316 | /// 踩到炸彈遊戲結束 317 | setState(() { 318 | grid.isSearched = true; 319 | _isGameOver = true; 320 | }); 321 | return; 322 | } 323 | /// 如果採的位置有設置的旗子, 324 | /// 但沒有炸彈則解除旗子的設置 325 | if ( 326 | !grid.hasBomb && 327 | flags[y] is Map && 328 | flags[y][x] == true 329 | ) { 330 | flags[y].remove(x); 331 | } 332 | setState(() { 333 | searchBomb(x, y); 334 | _isGameWon = checkWin(); 335 | if (_isGameWon == true) { 336 | /// 已達成獲勝條件 337 | alertMessage( 338 | context: context, 339 | title: '厲害唷!', 340 | okText: '新遊戲', 341 | onOK: () { 342 | Navigator.pop(context); 343 | createGame(); 344 | } 345 | ); 346 | } 347 | }); 348 | }, 349 | onLongPress: () { 350 | setState(() { 351 | flags[y] ??= Map(); 352 | if (flags[y][x] == true) { 353 | flags[y].remove(x); 354 | } else { 355 | flags[y][x] = true; 356 | } 357 | _isGameWon = checkWin(); 358 | }); 359 | }, 360 | ), 361 | ); 362 | }, 363 | ), 364 | ], 365 | ); 366 | } 367 | } 368 | 369 | class GameGrid { 370 | bool hasBomb; 371 | bool isSearched; 372 | int aroundBombs; 373 | GameGrid({ 374 | this.isSearched = false, 375 | this.hasBomb = false, 376 | this.aroundBombs = 0, 377 | }); 378 | } 379 | -------------------------------------------------------------------------------- /web/lib/games/tetris/game_pad.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'package:flutter_web/material.dart'; 4 | import 'package:flutter_web/services.dart'; 5 | 6 | const MAX_OFFSET = 10; 7 | 8 | /// 偵測手勢動作判斷魔術方塊操作行為 9 | class GamePad extends StatefulWidget { 10 | final Widget child; 11 | final VoidCallback onUp; 12 | final VoidCallback onDown; 13 | final VoidCallback onLeft; 14 | final VoidCallback onRight; 15 | final VoidCallback onSwipeDown; 16 | final VoidCallback onTap; 17 | GamePad({ 18 | @required this.child, 19 | this.onUp, 20 | this.onDown, 21 | this.onLeft, 22 | this.onRight, 23 | this.onSwipeDown, 24 | this.onTap, 25 | }); 26 | @override 27 | State createState() => _GamePadState(); 28 | } 29 | 30 | class _GamePadState extends State { 31 | /// 方向移動時,自動發出控制指令 32 | Timer _autoFirer; 33 | /// 玩家目前操控的方向 34 | CtrlDirection _currentDirention = CtrlDirection.none; 35 | // /// 玩家首次觸碰螢幕時在螢幕上的 X 軸位置 36 | // double _sx; 37 | // /// 玩家首次觸碰螢幕時在螢幕上的 Y 軸位置 38 | // double _sy; 39 | // /// 玩家觸碰螢幕後當前觸碰的 X 軸位置 40 | // double _cx; 41 | // /// 玩家觸碰螢幕後當前觸碰的 Y 軸位置 42 | // double _cy; 43 | /// 玩家觸碰螢幕後與首次觸碰螢幕的 X 軸位置的總位移量 44 | double _tdx; 45 | /// 玩家觸碰螢幕後與首次觸碰螢幕的 Y 軸位置的總位移量 46 | double _tdy; 47 | /// 重置數據 48 | void _reset() { 49 | // _sx = _sy = _cx = _cy = _tdx = _tdy = null; 50 | _tdx = _tdy = null; 51 | _currentDirention = CtrlDirection.none; 52 | _autoFirer?.cancel(); 53 | _autoFirer = null; 54 | } 55 | void _onKey(RawKeyEvent ev) { 56 | if (ev is RawKeyUpEvent) { 57 | return; 58 | } 59 | final RawKeyEventDataAndroid data = ev.data; 60 | if (data.keyCode == 37) { 61 | widget.onLeft(); 62 | } else if (data.keyCode == 39) { 63 | widget.onRight(); 64 | } else if (data.keyCode == 38) { 65 | widget.onUp(); 66 | } else if (data.keyCode == 40) { 67 | widget.onSwipeDown(); 68 | } 69 | } 70 | @override 71 | void initState() { 72 | super.initState(); 73 | RawKeyboard.instance.addListener(_onKey); 74 | } 75 | @override 76 | void dispose() { 77 | RawKeyboard.instance.removeListener(_onKey); 78 | _autoFirer?.cancel(); 79 | super.dispose(); 80 | } 81 | @override 82 | Widget build(BuildContext context) { 83 | return GestureDetector( 84 | child: widget.child, 85 | onTap: () { 86 | if (widget.onTap == null) return; 87 | widget.onTap(); 88 | }, 89 | onPanDown: (DragDownDetails details) { 90 | // _sx = _cx = details.globalPosition.dx; 91 | // _sy = _cy = details.globalPosition.dy; 92 | _tdx = _tdy = 0; 93 | }, 94 | onPanUpdate: (DragUpdateDetails details) { 95 | // 一次下滑的距離超過設定值時即判斷執行下滑 96 | if (details.delta.dy > 10 && widget.onSwipeDown != null) { 97 | widget.onSwipeDown(); 98 | } 99 | // _cx = details.globalPosition.dx; 100 | // _cy = details.globalPosition.dy; 101 | _tdx += details.delta.dx; 102 | _tdy += details.delta.dy; 103 | if (_tdx.abs() >= MAX_OFFSET || _tdy.abs() >= MAX_OFFSET) { 104 | _currentDirention = _getDirection(_tdx, _tdy); 105 | _tdx = _tdy = 0; 106 | if (_currentDirention == CtrlDirection.left && widget.onLeft != null) { 107 | widget.onLeft(); 108 | } else if (_currentDirention == CtrlDirection.right && widget.onRight != null) { 109 | widget.onRight(); 110 | } else if (_currentDirention == CtrlDirection.up && widget.onUp != null) { 111 | widget.onUp(); 112 | } else if (_currentDirention == CtrlDirection.down && widget.onDown != null) { 113 | widget.onDown(); 114 | } 115 | } 116 | }, 117 | onPanEnd: (DragEndDetails details) { _reset(); }, 118 | onPanCancel: _reset, 119 | ); 120 | } 121 | } 122 | /// 根據輸入的座標位移量 [tx], [ty] 判斷移動的方向 123 | CtrlDirection _getDirection(double tx, double ty) { 124 | CtrlDirection direction = CtrlDirection.none; 125 | if (tx.abs() >= ty.abs()) { 126 | if (tx > 0) { 127 | direction = CtrlDirection.right; 128 | } else if (tx < 0) { 129 | direction = CtrlDirection.left; 130 | } else { 131 | direction = CtrlDirection.none; 132 | } 133 | } else { 134 | if (ty > 0) { 135 | direction = CtrlDirection.down; 136 | } else if (ty < 0) { 137 | direction = CtrlDirection.up; 138 | } else { 139 | direction = CtrlDirection.none; 140 | } 141 | } 142 | return direction; 143 | } 144 | /// 手勢移動時的控制方向定義 145 | enum CtrlDirection { 146 | none, 147 | up, 148 | down, 149 | left, 150 | right, 151 | } 152 | -------------------------------------------------------------------------------- /web/lib/games/tetris/tetris.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter_web/material.dart'; 3 | import './game_pad.dart'; 4 | import './tetris_data.dart'; 5 | import './tetris_renderder.dart'; 6 | 7 | /// 可堆疊的總行數 8 | const ROWS = 20; 9 | /// 一行最多擺放的寬度 10 | const COLS = 10; 11 | /// 設定的遊戲等級,一共六級 12 | /// 用毫秒數來代表墜落速度 13 | const LEVELS = [ 14 | Duration(milliseconds: 600), 15 | Duration(milliseconds: 500), 16 | Duration(milliseconds: 400), 17 | Duration(milliseconds: 300), 18 | Duration(milliseconds: 200), 19 | Duration(milliseconds: 100), 20 | ]; 21 | /// 分數升級門檻 22 | const LEVEL_UP = [ 23 | 1000, 24 | 5000, 25 | 10000, 26 | 20000, 27 | 50000 28 | ]; 29 | const double RIGHT_PANEL_WIDTH = 100.0; 30 | const TextStyle INFO_TEXT_STYLE = TextStyle( 31 | color: Colors.white, 32 | fontSize: 20, 33 | ); 34 | 35 | class Tetris extends StatefulWidget { 36 | Tetris({ Key key, }) : super(key: key); 37 | static TetrisData dataOf(BuildContext context) { 38 | final _TetrisStateContainer widgetInstance = 39 | context.inheritFromWidgetOfExactType(_TetrisStateContainer); 40 | return widgetInstance.data; 41 | } 42 | @override 43 | TetrisState createState() => TetrisState(); 44 | } 45 | 46 | class TetrisState extends State with WidgetsBindingObserver { 47 | /// 魔術方塊面板資料 48 | TetrisData data; 49 | /// 魔術方塊下降間隔的計時器 50 | Timer _fallTimer; 51 | /// 魔術方塊到底時短暫停頓的計時器 52 | Timer _restTimer; 53 | /// 禁止移動 54 | bool _freezeMove; 55 | /// 禁止移動 56 | bool _gameOver; 57 | /// 目前等級 58 | int _level; 59 | /// 遊戲總得分數 60 | int _score; 61 | /// App 目前的狀態 62 | AppLifecycleState _appLifecycleState; 63 | /// 遊戲是否暫停中 64 | bool get _isPause => _fallTimer == null && _restTimer == null; 65 | TetrisState() { 66 | this.data = TetrisData(rows: ROWS, cols: COLS); 67 | this._freezeMove = false; 68 | this._score = this._level = 0; 69 | } 70 | void init() { 71 | _score = _level = 0; 72 | _freezeMove = _gameOver = false; 73 | data.reset(); 74 | putInShape(); 75 | // Sound.playFromAsset( 76 | // 'assets/tetris/', 'theme.mp3', 77 | // loop: true, 78 | // ).then((Sound sound) { 79 | // if (!mounted) return; 80 | // _themeMusic = sound; 81 | // }); 82 | } 83 | void putInShape() { 84 | data.putInShape(); 85 | _toggleFallTimer(true); 86 | setState(() {}); 87 | } 88 | @override 89 | void didChangeAppLifecycleState(AppLifecycleState lifecycleState) { 90 | _appLifecycleState = lifecycleState; 91 | switch (_appLifecycleState) { 92 | case AppLifecycleState.resumed: 93 | _toggleFallTimer(true); 94 | print('!!!!! 恢復遊戲 !!!!!'); 95 | break; 96 | case AppLifecycleState.inactive: 97 | case AppLifecycleState.paused: 98 | default: 99 | _toggleFallTimer(false); 100 | _toggleRestTimer(false); 101 | print('!!!!! 遊戲暫停 !!!!!'); 102 | break; 103 | } 104 | } 105 | @override 106 | void initState() { 107 | super.initState(); 108 | WidgetsBinding.instance.addObserver(this); 109 | init(); 110 | } 111 | @override 112 | void dispose() { 113 | WidgetsBinding.instance.removeObserver(this); 114 | _fallTimer?.cancel(); 115 | _restTimer?.cancel(); 116 | super.dispose(); 117 | } 118 | void _toggleFallTimer(bool shouldEnable) { 119 | if (!shouldEnable && _fallTimer != null) { 120 | _fallTimer.cancel(); 121 | _fallTimer = null; 122 | } else if (shouldEnable) { 123 | _fallTimer?.cancel(); 124 | _fallTimer = Timer.periodic(LEVELS[_level], _execMoveDown); 125 | } 126 | } 127 | void _toggleRestTimer(bool shouldEnable) { 128 | if (!shouldEnable && _restTimer != null) { 129 | _restTimer.cancel(); 130 | _restTimer = null; 131 | } else if (shouldEnable) { 132 | _restTimer?.cancel(); 133 | _restTimer = Timer(LEVELS[_level], _afterRest); 134 | } 135 | } 136 | /// 到底時會有一個方塊要置底的休息時間 137 | void _afterRest() { 138 | if (data.canMoveDown) { 139 | _execMoveDown(_restTimer); 140 | _toggleRestTimer(true); 141 | return; 142 | } 143 | data.mergeShapeToPanel(); 144 | _score += (data.cleanLines() * (_level + 1)); 145 | if (_level < LEVELS.length && _score >= LEVEL_UP[_level]) { 146 | _level++; 147 | } 148 | if (data.isGameOver) { 149 | print('!!!!! 遊戲結束 !!!!!'); 150 | _gameOver = true; 151 | _toggleFallTimer(false); 152 | _toggleRestTimer(false); 153 | // Sound.playFromAsset( 154 | // 'assets/tetris/', 'gameover.mp3', 155 | // onComplete: () { _gameOverMusic = null; } 156 | // ).then((Sound sound) { 157 | // if (!mounted) return; 158 | // _gameOverMusic = sound; 159 | // }); 160 | } else { 161 | data.putInShape(); 162 | _toggleFallTimer(true); 163 | _freezeMove = false; 164 | } 165 | setState(() {}); 166 | } 167 | /// 直接執行讓方塊直接落下 168 | void _execFallingDown() { 169 | _toggleFallTimer(false); 170 | _freezeMove = true; 171 | data.fallingDown(); 172 | _toggleRestTimer(true); 173 | setState(() {}); 174 | } 175 | /// 執行方塊落下一格的處理 176 | void _execMoveDown(Timer _timer) { 177 | if (_gameOver || _isPause) return; 178 | if (!data.canMoveDown) { 179 | print('到底了, bottom: ${data.currentBottom}'); 180 | _toggleFallTimer(false); 181 | _toggleRestTimer(true); 182 | return; 183 | } 184 | data.moveCurrentShapeDown(); 185 | setState(() {}); 186 | } 187 | /// 執行往左移動 188 | void _execMoveLeft() { 189 | if (_freezeMove || _gameOver || _isPause) return; 190 | if (data.moveCurrentShapeLeft()) setState(() {}); 191 | } 192 | /// 執行往右移動 193 | void _execMoveRight() { 194 | if (_freezeMove || _gameOver || _isPause) return; 195 | if (data.moveCurrentShapeRight()) setState(() {}); 196 | } 197 | /// 執行方塊旋轉 198 | void _execRotate() { 199 | if (_gameOver || _isPause) return; 200 | if (data.rotateCurrentShape()) setState(() {}); 201 | } 202 | /// 暫停/復原遊戲 203 | void _togglePause() { 204 | setState(() { 205 | if (_isPause) { 206 | _toggleFallTimer(true); 207 | } else { 208 | _toggleFallTimer(false); 209 | _toggleRestTimer(false); 210 | } 211 | }); 212 | } 213 | @override 214 | Widget build(BuildContext context) { 215 | return LayoutBuilder( 216 | builder: (context, constraints) { 217 | double boxWidth = constraints.maxWidth; 218 | double boxHeight = constraints.maxHeight - kBottomNavigationBarHeight - kToolbarHeight; 219 | return _TetrisStateContainer( 220 | data: this.data, 221 | child: Container( 222 | width: boxWidth, 223 | height: boxHeight, 224 | color: Colors.black, 225 | child: GamePad( 226 | child: Row( 227 | children: [ 228 | TertisRenderder(size: Size(boxWidth - RIGHT_PANEL_WIDTH, boxHeight)), 229 | Container( 230 | width: RIGHT_PANEL_WIDTH, 231 | decoration: BoxDecoration( 232 | border: Border( 233 | left: BorderSide(color: Colors.white, width: 1.0), 234 | ), 235 | ), 236 | child: Column( 237 | mainAxisAlignment: MainAxisAlignment.spaceAround, 238 | children: [ 239 | NextShapeRenderder(size: Size(RIGHT_PANEL_WIDTH, RIGHT_PANEL_WIDTH)), 240 | Container( 241 | padding: EdgeInsets.symmetric(vertical: 16), 242 | child: Column( 243 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 244 | children: [ 245 | Text('等級', 246 | textAlign: TextAlign.center, 247 | style: INFO_TEXT_STYLE, 248 | ), 249 | Text('${_level + 1}', 250 | textAlign: TextAlign.center, 251 | style: INFO_TEXT_STYLE, 252 | ), 253 | ], 254 | ), 255 | ), 256 | Container( 257 | padding: EdgeInsets.only(bottom: 16), 258 | child: Column( 259 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 260 | children: [ 261 | Text('分數', 262 | textAlign: TextAlign.center, 263 | style: INFO_TEXT_STYLE, 264 | ), 265 | Text('$_score', 266 | textAlign: TextAlign.center, 267 | style: INFO_TEXT_STYLE, 268 | ), 269 | ], 270 | ), 271 | ), 272 | Container( 273 | margin: EdgeInsets.only(top: 16, bottom: 16), 274 | child: IgnorePointer( 275 | ignoring: _gameOver, 276 | child: IconButton( 277 | icon: Icon( 278 | _isPause ? Icons.play_arrow : Icons.pause 279 | ), 280 | iconSize: 32, 281 | color: Colors.white, 282 | onPressed: _togglePause, 283 | ), 284 | ), 285 | ), 286 | Container( 287 | margin: EdgeInsets.only(top: 16, bottom: 24), 288 | child: IconButton( 289 | icon: Icon(Icons.sync), 290 | iconSize: 32, 291 | color: Colors.white, 292 | onPressed: init, 293 | ), 294 | ), 295 | ], 296 | ), 297 | ), 298 | ], 299 | ), 300 | onTap: Feedback.wrapForTap(_execRotate, context), 301 | onUp: _execRotate, 302 | onLeft: _execMoveLeft, 303 | onRight: _execMoveRight, 304 | onSwipeDown: Feedback.wrapForTap(_execFallingDown, context), 305 | ), 306 | ), 307 | ); 308 | }, 309 | ); 310 | } 311 | } 312 | 313 | class _TetrisStateContainer extends InheritedWidget { 314 | final TetrisData data; 315 | _TetrisStateContainer({ 316 | @required this.data, 317 | @required Widget child, 318 | }) : super(child: child); 319 | @override 320 | bool updateShouldNotify(_TetrisStateContainer old) => true; 321 | } 322 | -------------------------------------------------------------------------------- /web/lib/games/tetris/tetris_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter_web/material.dart'; 3 | 4 | /// 隨機產生器種子 5 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 6 | 7 | class TetrisData { 8 | final int rows; 9 | final int cols; 10 | int get totalGrids => rows * cols; 11 | /// 魔術方塊的面板格子資料,紀錄每個格子資料 12 | List> panel; 13 | /// 下一個要落下的方塊 14 | Shape nextShape; 15 | /// 目前落下的魔術方塊 16 | Shape currentShape; 17 | /// 目前落下的方塊位置 18 | Offset _currentOffset; 19 | /// 面板上所有的魔術方塊 20 | List _shapes; 21 | /// 落下方塊所在的 X 座標 22 | int get currentX { 23 | if (_currentOffset == null) return -1; 24 | return _currentOffset.dx.toInt(); 25 | } 26 | /// 落下方塊的所在的 Y 座標 27 | int get currentY { 28 | if (_currentOffset == null) return -1; 29 | return _currentOffset.dy.toInt(); 30 | } 31 | /// 落下方塊右側的位置 32 | int get currentRight { 33 | if (_currentOffset == null) return -1; 34 | return currentX + currentShape.width; 35 | } 36 | /// 落下方塊底部的位置 37 | int get currentBottom { 38 | if (_currentOffset == null) return -1; 39 | return currentY + currentShape.height; 40 | } 41 | /// 判斷遊戲是否結束 42 | bool get isGameOver => currentY < 0; 43 | /// 是否可往下移動 44 | bool get canMoveDown => currentY + 1 <= findFallingDownY(); 45 | TetrisData({ 46 | @required this.rows, 47 | @required this.cols, 48 | }) { 49 | this.panel = List.generate(rows, (y) => List.generate(cols, (x) => 0)); 50 | this._shapes = []; 51 | this.nextShape = Shape.random(); 52 | } 53 | /// 重置資料,將所有的資料回歸原始狀態 54 | void reset() { 55 | for (int y = 0; y < rows; y++) { 56 | for (int x = 0; x < cols; x++) { 57 | panel[y][x] = 0; 58 | } 59 | } 60 | _shapes.clear(); 61 | currentShape = _currentOffset = null; 62 | nextShape = Shape.random(); 63 | int rotateCount = randomGenerator.nextInt(4); 64 | while (--rotateCount >= 0) { nextShape.rotate(); } 65 | } 66 | /// 將下一個方塊放進遊戲面板裡,並同時產生下一個魔術方塊 67 | void putInShape() { 68 | currentShape = nextShape; 69 | _currentOffset = Offset( 70 | // 初始 X 軸位置置中 71 | ((cols / 2) - (currentShape.width / 2)).roundToDouble(), 72 | // 初始 Y 軸位置完全隱藏方塊 73 | -currentShape.height.toDouble(), 74 | ); 75 | nextShape = Shape.random(); 76 | int rotateCount = randomGenerator.nextInt(4); 77 | while (--rotateCount >= 0) { nextShape.rotate(); } 78 | } 79 | /// 往左移動目前的魔術方塊 80 | bool moveCurrentShapeLeft() { 81 | if (currentShape == null) return false; 82 | int nextX = currentX - 1; 83 | // 如果要往左移動的位置,目前面板已有方塊,則不處理移動 84 | if ( 85 | nextX >= 0 && 86 | _canMoveToX(currentX, nextX) 87 | ) { 88 | _currentOffset = Offset( 89 | nextX.toDouble(), 90 | currentY.toDouble(), 91 | ); 92 | return true; 93 | } 94 | return false; 95 | } 96 | /// 往右移動目前的魔術方塊 97 | bool moveCurrentShapeRight() { 98 | if (currentShape == null) return false; 99 | int nextX = currentX + 1; 100 | int nextRight = nextX + currentShape.width; 101 | // 如果要往右移動的位置,目前面板已有方塊,則不處理移動 102 | if ( 103 | nextRight <= cols && 104 | _canMoveToX(currentRight - 1, nextRight - 1) 105 | ) { 106 | _currentOffset = Offset( 107 | nextX.toDouble(), 108 | currentY.toDouble(), 109 | ); 110 | return true; 111 | } 112 | return false; 113 | } 114 | /// 往下移動目前的魔術方塊 115 | bool moveCurrentShapeDown() { 116 | if (!(currentShape != null && canMoveDown)) return false; 117 | _currentOffset = Offset( 118 | currentX.toDouble(), 119 | (currentY + 1).toDouble(), 120 | ); 121 | return true; 122 | } 123 | /// 將目前的方塊直接落下 124 | void fallingDown() { 125 | _currentOffset = Offset( 126 | currentX.toDouble(), 127 | findFallingDownY().toDouble(), 128 | ); 129 | } 130 | /// 旋轉目前的魔術方塊 131 | bool rotateCurrentShape() { 132 | if (currentShape == null) return false; 133 | bool canRotate = true; 134 | /// 先判斷旋轉後的方塊是否合法,合法時才能旋轉 135 | Shape rotatedShape = Shape( 136 | patterns: currentShape.patterns, 137 | patternIndex: currentShape.patternIndex, 138 | colorIndex: currentShape.colorIndex, 139 | ); 140 | rotatedShape.rotate(); 141 | rotatedShape.forEachBlock((value, x, y) { 142 | if (currentX + x < 0) { 143 | _currentOffset = Offset( 144 | 0, 145 | currentY.toDouble(), 146 | ); 147 | } else if (currentX + x > cols - rotatedShape.width) { 148 | _currentOffset = Offset( 149 | (cols - rotatedShape.width).toDouble(), 150 | currentY.toDouble(), 151 | ); 152 | } 153 | int ty = currentY + y; 154 | int tx = currentX + x; 155 | if ( 156 | ty >= 0 && ty < rows && 157 | tx >= 0 && tx < cols && 158 | panel[ty][tx] > 0 && value > 0 159 | ) { 160 | canRotate = false; 161 | } 162 | }, reverse: true); 163 | if (canRotate) { 164 | currentShape = rotatedShape; 165 | } 166 | return canRotate; 167 | } 168 | /// 把目前的方塊固定到面板上 169 | void mergeShapeToPanel() { 170 | final block = currentShape.block; 171 | for (int y = currentShape.height - 1; y >= 0; y--) { 172 | for (int x = 0; x < currentShape.width; x++) { 173 | if (block[y][x] > 0) { 174 | int ty = currentY + y; 175 | int tx = currentX + x; 176 | if (ty < 0) break; 177 | panel[ty][tx] = block[y][x]; 178 | } 179 | } 180 | } 181 | _shapes.add(currentShape); 182 | currentShape = null; 183 | } 184 | /// 檢查是否有填滿,回傳得到的分數 185 | int cleanLines() { 186 | int score = 0; 187 | int bonus = 0; 188 | int y = rows - 1; 189 | while (y >= 0) { 190 | bool shouldClean = true; 191 | for (int x = 0; x < cols; x++) { 192 | if (panel[y][x] == 0) { 193 | shouldClean = false; 194 | break; 195 | } 196 | } 197 | if (shouldClean) { 198 | score += 100 + bonus; 199 | // 每多一行疊加 100 分 200 | bonus += 100; 201 | // 將目標清空格的上方空格都往下移 202 | for (int dy = y; dy >= 0; dy--) { 203 | for (int x = 0; x < cols; x++) { 204 | panel[dy][x] = dy - 1 >= 0 ? panel[dy - 1][x] : 0; 205 | } 206 | } 207 | } else { 208 | y--; 209 | } 210 | } 211 | return score; 212 | } 213 | /// 找到方塊能直接落下的 Y 軸位移量 214 | int findFallingDownY() { 215 | for (int fY = currentY + 1; fY <= rows - currentShape.height; fY++) { 216 | bool blocked = false; 217 | currentShape.forEachBlock((value, x, y) { 218 | if (fY + y < 0) return; 219 | if (panel[fY + y][currentX + x] > 0) { 220 | blocked = true; 221 | } 222 | }, reverse: true); 223 | if (blocked) { 224 | return fY - 1; 225 | } 226 | } 227 | return rows - currentShape.height; 228 | } 229 | /// 檢查目前的方塊是否可移動至目標 X 軸位置 230 | bool _canMoveToX(int fromX, int toX) { 231 | if (currentShape == null) return false; 232 | final block = currentShape.block; 233 | int blockX = fromX - toX >= 0 ? 0 : currentShape.width - 1; 234 | // 檢查目前方塊的垂直軸是否都能移動過去 235 | for (int y = currentShape.height - 1; y >= 0; y--) { 236 | if (currentY + y < 0) continue; 237 | int blockValue = block[y][blockX]; 238 | if (blockValue == 0) { 239 | blockValue = block[y][fromX - toX >= 0 ? blockX + 1 : blockX - 1]; 240 | } 241 | // 只要有一個位置衝突就不能移動過去 242 | if (panel[currentY + y][toX] > 0 && blockValue > 0) { 243 | return false; 244 | } 245 | } 246 | return true; 247 | } 248 | } 249 | typedef void ShapeForEachCallback(int value, int x, int y); 250 | class Shape { 251 | /// 顯示顏色的編號 252 | final int colorIndex; 253 | /// 4 * 4 方塊模板 254 | final List> patterns; 255 | /// 目前方塊模板的位置(樣板包含旋轉) 256 | int patternIndex; 257 | /// 4 * 4 的方塊 258 | List> block; 259 | /// 方塊所佔的尺寸 260 | Size _size; 261 | /// 方塊目前的所佔最大寬度 262 | int get width => _size.width.toInt(); 263 | /// 方塊目前的所佔最大高度 264 | int get height => _size.height.toInt(); 265 | Shape({ 266 | @required this.patterns, 267 | @required this.patternIndex, 268 | @required this.colorIndex, 269 | }) { 270 | this.block = List.generate(PATTERN_SIZE, (y) => List.generate(PATTERN_SIZE, (x) => 0)); 271 | List pattern = patterns[patternIndex]; 272 | forEachBlock((value, x, y) { 273 | int i = PATTERN_SIZE * y + x; 274 | block[y][x] = pattern[i] == 1 ? colorIndex : 0; 275 | }, ignoreZero: false); 276 | updateSize(); 277 | } 278 | void forEachBlock(ShapeForEachCallback callback, { 279 | ignoreZero = true, 280 | reverse = false, 281 | }) { 282 | if (reverse) { 283 | for (int y = PATTERN_SIZE - 1; y >= 0; y--) { 284 | for (int x = PATTERN_SIZE - 1; x >= 0; x--) { 285 | if (ignoreZero && block[y][x] == 0) continue; 286 | callback(block[y][x], x, y); 287 | } 288 | } 289 | } else { 290 | for (int y = 0; y < PATTERN_SIZE; y++) { 291 | for (int x = 0; x < PATTERN_SIZE; x++) { 292 | if (ignoreZero && block[y][x] == 0) continue; 293 | callback(block[y][x], x, y); 294 | } 295 | } 296 | } 297 | } 298 | /// 旋轉方塊 299 | void rotate() { 300 | patternIndex = patternIndex + 1 >= patterns.length ? 0 : patternIndex + 1; 301 | List pattern = patterns[patternIndex]; 302 | forEachBlock((value, x, y) { 303 | int i = PATTERN_SIZE * y + x; 304 | block[y][x] = pattern[i] == 1 ? colorIndex : 0; 305 | }, ignoreZero: false); 306 | updateSize(); 307 | } 308 | /// 更新方塊的寬高資訊 309 | void updateSize() { 310 | double _offsetX = 0; 311 | double _offsetY = 0; 312 | List widths = List.filled(PATTERN_SIZE, 0.0); 313 | List heights = List.filled(PATTERN_SIZE, 0.0); 314 | for (int x = 0; x < PATTERN_SIZE; x++) { 315 | for (int y = 0; y < PATTERN_SIZE; y++) { 316 | if (block[y][x] > 0) { 317 | widths[x] = heights[y] = 1; 318 | } 319 | if (heights[y] == 1 && _offsetY == 0) _offsetY = y.toDouble(); 320 | } 321 | if (widths[x] == 1 && _offsetX == 0) _offsetX = x.toDouble(); 322 | } 323 | double width = widths.reduce((val, elem) => val + elem); 324 | double height = heights.reduce((val, elem) => val + elem); 325 | _size = Size(width, height); 326 | } 327 | /// 隨機產生一個方塊形狀 328 | static Shape random() { 329 | int patternsIndex = randomGenerator.nextInt(SHAPES.length); 330 | final patterns = SHAPES[patternsIndex]; 331 | int patternIndex = randomGenerator.nextInt(patterns.length); 332 | final shape = Shape( 333 | patterns: patterns, 334 | patternIndex: patternIndex, 335 | colorIndex: 1 + randomGenerator.nextInt(TETRIS_COLORS.length - 1), 336 | ); 337 | return shape; 338 | } 339 | } 340 | 341 | /// 顯示的方塊顏色 342 | const List TETRIS_COLORS = [ 343 | Colors.black, 344 | Colors.cyan, 345 | Colors.orange, 346 | Colors.blue, 347 | Colors.yellow, 348 | Colors.red, 349 | Colors.green, 350 | Colors.purple 351 | ]; 352 | /// 每個魔術方塊樣板尺寸 353 | const PATTERN_SIZE = 4; 354 | /// 所有可能出現的魔術方塊樣板 355 | const SHAPES = [ 356 | [ 357 | [ 1, 1, 1, 1, 358 | 0, 0, 0, 0, 359 | 0, 0, 0, 0, 360 | 0, 0, 0, 0 ], 361 | [ 1, 0, 0, 0, 362 | 1, 0, 0, 0, 363 | 1, 0, 0, 0, 364 | 1, 0, 0, 0 ], 365 | ], 366 | [ 367 | [ 1, 1, 1, 0, 368 | 1, 0, 0, 0, 369 | 0, 0, 0, 0, 370 | 0, 0, 0, 0 ], 371 | [ 1, 0, 0, 0, 372 | 1, 0, 0, 0, 373 | 1, 1, 0, 0, 374 | 0, 0, 0, 0 ], 375 | [ 0, 0, 1, 0, 376 | 1, 1, 1, 0, 377 | 0, 0, 0, 0, 378 | 0, 0, 0, 0 ], 379 | [ 1, 1, 0, 0, 380 | 0, 1, 0, 0, 381 | 0, 1, 0, 0, 382 | 0, 0, 0, 0 ], 383 | ], 384 | [ 385 | [ 1, 1, 1, 0, 386 | 0, 0, 1, 0, 387 | 0, 0, 0, 0, 388 | 0, 0, 0, 0 ], 389 | [ 0, 1, 0, 0, 390 | 0, 1, 0, 0, 391 | 1, 1, 0, 0, 392 | 0, 0, 0, 0 ], 393 | [ 1, 0, 0, 0, 394 | 1, 1, 1, 0, 395 | 0, 0, 0, 0, 396 | 0, 0, 0, 0 ], 397 | [ 1, 1, 0, 0, 398 | 1, 0, 0, 0, 399 | 1, 0, 0, 0, 400 | 0, 0, 0, 0 ], 401 | ], 402 | [ 403 | [ 1, 1, 0, 0, 404 | 1, 1, 0, 0, 405 | 0, 0, 0, 0, 406 | 0, 0, 0, 0, ], 407 | ], 408 | [ 409 | [ 1, 1, 0, 0, 410 | 0, 1, 1, 0, 411 | 0, 0, 0, 0, 412 | 0, 0, 0, 0 ], 413 | [ 0, 1, 0, 0, 414 | 1, 1, 0, 0, 415 | 1, 0, 0, 0, 416 | 0, 0, 0, 0 ], 417 | ], 418 | [ 419 | [ 0, 1, 1, 0, 420 | 1, 1, 0, 0, 421 | 0, 0, 0, 0, 422 | 0, 0, 0, 0 ], 423 | [ 1, 0, 0, 0, 424 | 1, 1, 0, 0, 425 | 0, 1, 0, 0, 426 | 0, 0, 0, 0 ], 427 | ], 428 | [ 429 | [ 0, 1, 0, 0, 430 | 1, 1, 1, 0, 431 | 0, 0, 0, 0, 432 | 0, 0, 0, 0 ], 433 | [ 1, 0, 0, 0, 434 | 1, 1, 0, 0, 435 | 1, 0, 0, 0, 436 | 0, 0, 0, 0 ], 437 | [ 1, 1, 1, 0, 438 | 0, 1, 0, 0, 439 | 0, 0, 0, 0, 440 | 0, 0, 0, 0 ], 441 | [ 0, 1, 0, 0, 442 | 1, 1, 0, 0, 443 | 0, 1, 0, 0, 444 | 0, 0, 0, 0 ], 445 | ], 446 | ]; 447 | -------------------------------------------------------------------------------- /web/lib/games/tetris/tetris_renderder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | import './tetris_data.dart'; 3 | import './tetris.dart'; 4 | 5 | class NextShapeRenderder extends StatelessWidget { 6 | final Size size; 7 | NextShapeRenderder({ 8 | @required this.size, 9 | }); 10 | @override 11 | Widget build(BuildContext context) { 12 | return CustomPaint( 13 | painter: _NextShapePainter( 14 | data: Tetris.dataOf(context), 15 | ), 16 | size: this.size, 17 | ); 18 | } 19 | } 20 | class _NextShapePainter extends CustomPainter { 21 | final TetrisData data; 22 | _NextShapePainter({ 23 | @required this.data, 24 | }); 25 | @override 26 | void paint(Canvas canvas, Size size) { 27 | final nextShape = data.nextShape; 28 | if (nextShape == null) return; 29 | Size blockSize = Size(size.width / 5, size.height / 5); 30 | Size blockWithBorderSize = Size(blockSize.width - 1, blockSize.height - 1); 31 | Paint paint = Paint() 32 | ..color = Colors.white 33 | ..strokeJoin = StrokeJoin.round 34 | ..strokeCap = StrokeCap.round 35 | ..strokeWidth = 1; 36 | double centerX = size.width / 2; 37 | double blockWidthHalf = blockSize.width / 2; 38 | double left = centerX - blockWidthHalf - (nextShape.width / 2.5 * blockSize.width); 39 | for (int y = 0; y < nextShape.height; y++) { 40 | for (int x = 0; x < nextShape.width; x++) { 41 | int colorIndex = nextShape.block[y][x]; 42 | if (colorIndex == 0) continue; 43 | paint.color = TETRIS_COLORS[colorIndex]; 44 | _drawBlock( 45 | canvas, paint, 46 | offset: Offset(left + (blockSize.width * x), blockSize.height * y), 47 | size: blockWithBorderSize, 48 | ); 49 | } 50 | } 51 | } 52 | @override 53 | bool shouldRepaint(CustomPainter oldDelegate) => true; 54 | } 55 | 56 | class TertisRenderder extends StatefulWidget { 57 | final Size size; 58 | TertisRenderder({ 59 | @required this.size, 60 | }); 61 | @override 62 | _TertisRenderderState createState() => _TertisRenderderState(); 63 | } 64 | 65 | class _TertisRenderderState extends State { 66 | @override 67 | void initState() { 68 | super.initState(); 69 | } 70 | @override 71 | void dispose() { 72 | super.dispose(); 73 | } 74 | @override 75 | Widget build(BuildContext context) { 76 | return CustomPaint( 77 | foregroundPainter: _TetrisPainter( 78 | data: Tetris.dataOf(context), 79 | ), 80 | painter: TetrisBGPainter(), 81 | size: widget.size, 82 | ); 83 | } 84 | } 85 | 86 | /// 處理魔術方塊的畫面渲染 87 | class _TetrisPainter extends CustomPainter { 88 | final TetrisData data; 89 | _TetrisPainter({ 90 | @required this.data, 91 | }); 92 | @override 93 | void paint(Canvas canvas, Size size) { 94 | List> panel = data.panel; 95 | Shape shape = data.currentShape; 96 | Size blockSize = Size(size.width / data.cols, size.height / data.rows); 97 | Size blockWithBorderSize = Size(blockSize.width - 1, blockSize.height - 1); 98 | Paint paint = Paint() 99 | ..color = Colors.white 100 | ..strokeJoin = StrokeJoin.round 101 | ..strokeCap = StrokeCap.round 102 | ..strokeWidth = 1; 103 | // 繪製目前整個面板上的方塊內容 104 | for (int y = 0; y < data.rows; y++) { 105 | for (int x = 0; x < data.cols; x++) { 106 | int colorIndex = panel[y][x]; 107 | if (colorIndex > 0) { 108 | paint.color = TETRIS_COLORS[colorIndex]; 109 | _drawBlock( 110 | canvas, paint, 111 | offset: Offset(blockSize.width * x, blockSize.height * y), 112 | size: blockWithBorderSize, 113 | ); 114 | } 115 | } 116 | } 117 | if (shape != null) { 118 | int fallingDownY = data.findFallingDownY(); 119 | shape.forEachBlock((value, x, y) { 120 | int colorIndex = shape.block[y][x]; 121 | if (colorIndex > 0) { 122 | // 繪製目前落下的魔術方塊 123 | paint.color = TETRIS_COLORS[colorIndex]; 124 | _drawBlock( 125 | canvas, paint, 126 | offset: Offset( 127 | (data.currentX + x) * blockSize.width, 128 | (data.currentY + y) * blockSize.height, 129 | ), 130 | size: blockWithBorderSize, 131 | ); 132 | // 繪製落下位置的預覽 133 | paint.color = Colors.white.withOpacity(0.33); 134 | _drawBlock( 135 | canvas, paint, 136 | offset: Offset( 137 | (data.currentX + x) * blockSize.width, 138 | (fallingDownY + y) * blockSize.height, 139 | ), 140 | size: blockWithBorderSize, 141 | ); 142 | } 143 | }); 144 | // paint.color = Colors.white; 145 | // canvas.drawLine( 146 | // Offset(panel.currentX * blockWidth, 0), 147 | // Offset(panel.currentX * blockWidth, size.height), 148 | // paint, 149 | // ); 150 | // canvas.drawLine( 151 | // Offset(panel.currentRight * blockWidth, 0), 152 | // Offset(panel.currentRight * blockWidth, size.height), 153 | // paint, 154 | // ); 155 | // canvas.drawLine( 156 | // Offset(0, panel.currentBottom * blockHeight), 157 | // Offset(size.width, panel.currentBottom * blockHeight), 158 | // paint, 159 | // ); 160 | } 161 | } 162 | @override 163 | bool shouldRepaint(_TetrisPainter oldDelegate) => true; 164 | } 165 | 166 | class TetrisBGPainter extends CustomPainter { 167 | @override 168 | void paint(Canvas canvas, Size size) { 169 | Rect rect = Offset.zero & size; 170 | Paint paint = Paint() 171 | ..strokeJoin = StrokeJoin.round 172 | ..strokeCap = StrokeCap.round 173 | ..strokeWidth = 1 174 | ..shader = LinearGradient( 175 | colors: [ 176 | Colors.purple, 177 | Colors.black, 178 | Colors.purple, 179 | ], 180 | stops: [ 181 | 0.0, 182 | 0.5, 183 | 1.0, 184 | ], 185 | ).createShader(rect); 186 | canvas.drawRect(rect, paint); 187 | } 188 | @override 189 | bool shouldRepaint(TetrisBGPainter oldDelegate) => true; 190 | } 191 | 192 | void _drawBlock(Canvas canvas, Paint paint, { 193 | @required Offset offset, 194 | @required Size size, 195 | }) { 196 | Rect rect = offset & size; 197 | paint.style = PaintingStyle.fill; 198 | canvas.drawRect(rect, paint); 199 | 200 | paint.shader = LinearGradient( 201 | colors: [ 202 | Colors.white.withOpacity(0.75), 203 | Colors.white.withOpacity(0.3), 204 | paint.color, 205 | ], 206 | stops: [ 207 | 0.0, 208 | 0.75, 209 | 1.0, 210 | ], 211 | ).createShader(rect); 212 | canvas.drawRect(rect, paint); 213 | paint.shader = null; 214 | // paint.style = PaintingStyle.stroke; 215 | // paint.color = Colors.white; 216 | // canvas.drawRect(rect, paint); 217 | } -------------------------------------------------------------------------------- /web/lib/games/tic_tac_toe/tic_tac_toe.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter_web/cupertino.dart'; 3 | import 'package:flutter_web/material.dart'; 4 | import '../../utlis.dart'; 5 | 6 | const int GRIDS = 9; 7 | /// 隨機產生器種子 8 | final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); 9 | 10 | class TicTacToe extends StatefulWidget { 11 | TicTacToe({ Key key, }) : super(key: key); 12 | @override 13 | _TicTacToeState createState() => _TicTacToeState(); 14 | } 15 | 16 | class _TicTacToeState extends State { 17 | int turn; 18 | int remainStep; 19 | int winner; 20 | List grids; 21 | void reset() { 22 | setState(() { 23 | turn = randomGenerator.nextInt(2); 24 | remainStep = GRIDS; 25 | winner = null; 26 | grids = List.filled(GRIDS, null); 27 | }); 28 | } 29 | bool hasWin(int turn) { 30 | return ( 31 | (grids[0] == turn && grids[0] == grids[1] && grids[0] == grids[2]) || 32 | (grids[0] == turn && grids[0] == grids[3] && grids[0] == grids[6]) || 33 | (grids[0] == turn && grids[0] == grids[4] && grids[0] == grids[8]) || 34 | (grids[1] == turn && grids[1] == grids[4] && grids[1] == grids[7]) || 35 | (grids[2] == turn && grids[2] == grids[5] && grids[2] == grids[8]) || 36 | (grids[3] == turn && grids[3] == grids[4] && grids[3] == grids[5]) || 37 | (grids[6] == turn && grids[6] == grids[7] && grids[6] == grids[8]) || 38 | (grids[2] == turn && grids[2] == grids[4] && grids[2] == grids[6]) 39 | ); 40 | } 41 | @override 42 | void initState() { 43 | super.initState(); 44 | reset(); 45 | } 46 | @override 47 | Widget build(BuildContext context) { 48 | return Column( 49 | children: [ 50 | GridView.builder( 51 | physics: NeverScrollableScrollPhysics(), 52 | shrinkWrap: true, 53 | scrollDirection: Axis.vertical, 54 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 55 | crossAxisCount: 3, 56 | mainAxisSpacing: 8, 57 | crossAxisSpacing: 8, 58 | ), 59 | padding: EdgeInsets.all(8), 60 | itemCount: GRIDS, 61 | itemBuilder: (BuildContext context, int index) { 62 | int grid = grids[index]; 63 | Widget widget; 64 | switch (grid) { 65 | case 0: 66 | widget = Icon(Icons.close); break; 67 | case 1: 68 | widget = Icon(Icons.radio_button_unchecked); break; 69 | default: 70 | widget = Text(''); break; 71 | } 72 | return IgnorePointer( 73 | ignoring: winner != null, 74 | child: InkWell( 75 | child: Container( 76 | height: 240, 77 | alignment: Alignment.center, 78 | color: winner == null ? Colors.black26 : Colors.black12, 79 | child: widget, 80 | ), 81 | onTap: () { 82 | if (grids[index] != null) { 83 | alertMessage( 84 | context: context, 85 | title: '此格已下過', 86 | okText: '知道了', 87 | onOK: () { Navigator.pop(context); } 88 | ); 89 | return; 90 | } 91 | setState(() { 92 | grids[index] = turn; 93 | if (hasWin(turn)) { 94 | winner = turn; 95 | alertMessage( 96 | context: context, 97 | titlePrefix: Icon(winner == 0 ? Icons.close : Icons.radio_button_unchecked), 98 | title: ' 獲勝了', 99 | okText: '再玩一次', 100 | onOK: () { 101 | Navigator.pop(context); 102 | reset(); 103 | } 104 | ); 105 | } else if (grids.fold(true, (isFlat, grid) => isFlat && grid != null)) { 106 | winner = -1; 107 | alertMessage( 108 | context: context, 109 | title: '平手', 110 | okText: '再玩一次', 111 | onOK: () { 112 | Navigator.pop(context); 113 | reset(); 114 | } 115 | ); 116 | } 117 | turn = turn == 1 ? 0 : 1; 118 | }); 119 | }, 120 | ), 121 | ); 122 | }, 123 | ), 124 | Row( 125 | mainAxisAlignment: MainAxisAlignment.center, 126 | children: [ 127 | Text('輪到 '), 128 | Icon(turn == 0 ? Icons.close : Icons.radio_button_unchecked), 129 | ], 130 | ), 131 | Container( 132 | alignment: Alignment.center, 133 | child: RaisedButton( 134 | child: Text('重來'), 135 | onPressed: reset, 136 | ), 137 | ), 138 | ], 139 | ); 140 | } 141 | } -------------------------------------------------------------------------------- /web/lib/layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | import 'main.dart'; 3 | import 'screens/settings.dart'; 4 | 5 | const ICON_TITLES = [ 6 | '俄羅斯方塊', 7 | '踩地雷', 8 | '井字遊戲', 9 | ]; 10 | 11 | class Layout extends StatefulWidget { 12 | final LayoutState initialState; 13 | final List children; 14 | Layout({ 15 | this.initialState, 16 | @required this.children, 17 | }); 18 | @override 19 | LayoutState createState() => LayoutState( 20 | title: this.initialState?.title, 21 | currentBottomNavIndex: this.initialState?.currentBottomNavIndex, 22 | ); 23 | } 24 | 25 | class LayoutState extends State { 26 | final bool hideBottomNav; 27 | String title; 28 | int currentBottomNavIndex; 29 | LayoutState({ 30 | this.title = '', 31 | this.currentBottomNavIndex = 0, 32 | this.hideBottomNav = false, 33 | }); 34 | void updateTitle(String newTitle) { 35 | setState(() { 36 | title = newTitle; 37 | }); 38 | } 39 | void updateBottomNavIndex(int newIndex) { 40 | setState(() { 41 | currentBottomNavIndex = newIndex; 42 | title = ICON_TITLES[newIndex]; 43 | }); 44 | } 45 | @override 46 | void initState() { 47 | super.initState(); 48 | if (!hideBottomNav) { 49 | updateTitle(ICON_TITLES[currentBottomNavIndex]); 50 | } 51 | } 52 | @override 53 | Widget build(BuildContext context) { 54 | return Scaffold( 55 | appBar: AppBar( 56 | title: Text(title), 57 | actions: [ 58 | IconButton( 59 | icon: Icon(Icons.settings), 60 | onPressed: onPressSettings, 61 | ) 62 | ], 63 | ), 64 | drawer: Drawer( 65 | child: ListView( 66 | padding: EdgeInsets.zero, 67 | children: [ 68 | DrawerHeader( 69 | child: Text( 70 | '玩個遊戲', 71 | style: TextStyle( 72 | color: Colors.white, 73 | fontSize: 24, 74 | ), 75 | ), 76 | decoration: BoxDecoration( 77 | color: Colors.blue, 78 | ), 79 | ), 80 | ], 81 | ), 82 | ), 83 | body: Center( 84 | child: Container( 85 | alignment: Alignment.topCenter, 86 | constraints: BoxConstraints( 87 | maxWidth: 375, 88 | maxHeight: 667, 89 | ), 90 | child: widget.children[currentBottomNavIndex], 91 | ), 92 | ), 93 | bottomNavigationBar: hideBottomNav 94 | ? null 95 | : BottomNavigationBar( 96 | type: BottomNavigationBarType.fixed, 97 | currentIndex: currentBottomNavIndex, 98 | onTap: updateBottomNavIndex, 99 | items: [ 100 | BottomNavigationBarItem( 101 | icon: Icon(Icons.gamepad), 102 | title: Text(ICON_TITLES[0]), 103 | ), 104 | BottomNavigationBarItem( 105 | icon: Icon(Icons.brightness_high), 106 | title: Text(ICON_TITLES[1]), 107 | ), 108 | BottomNavigationBarItem( 109 | icon: Icon(Icons.grid_on), 110 | title: Text(ICON_TITLES[2]), 111 | ), 112 | ], 113 | ), 114 | ); 115 | } 116 | void onPressSettings() { 117 | Navigator.push(context, MaterialPageRoute( 118 | builder: (BuildContext context) { 119 | final state = App.of(context); 120 | return SettingsScreen( 121 | configs: state.configs, 122 | configUpdater: state.updateConfig, 123 | ); 124 | }, 125 | fullscreenDialog: true, 126 | )); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /web/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | import 'configs.dart'; 3 | import 'screens/home.dart'; 4 | 5 | void main() => runApp(App()); 6 | 7 | class App extends StatefulWidget { 8 | App({ Key key }) : super(key: key); 9 | static AppState of(BuildContext context) { 10 | final _AppStateContainer widgetInstance = context.inheritFromWidgetOfExactType(_AppStateContainer); 11 | return widgetInstance.state; 12 | } 13 | @override 14 | AppState createState() => AppState(); 15 | } 16 | 17 | class AppState extends State { 18 | GameConfigs configs = GameConfigs(mineweeperLevel: Level.easy); 19 | Future updateConfig(GameConfigs newConfigs) async { 20 | setState(() { configs = newConfigs; }); 21 | } 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | @override 27 | void dispose() { 28 | super.dispose(); 29 | // App 關閉 30 | } 31 | @override 32 | Widget build(BuildContext context) { 33 | return _AppStateContainer( 34 | state: this, 35 | child: MaterialApp( 36 | title: 'Play a Game', 37 | theme: ThemeData( 38 | primarySwatch: Colors.purple, 39 | accentColor: Colors.orangeAccent[400], 40 | ), 41 | home: HomeScreen(), 42 | ), 43 | ); 44 | } 45 | } 46 | 47 | class _AppStateContainer extends InheritedWidget { 48 | final AppState state; 49 | _AppStateContainer({ 50 | @required this.state, 51 | @required Widget child, 52 | }) : super(child: child); 53 | @override 54 | bool updateShouldNotify(_AppStateContainer old) => true; 55 | } 56 | -------------------------------------------------------------------------------- /web/lib/screens/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/cupertino.dart'; 2 | import 'package:flutter_web/material.dart'; 3 | import 'package:flutter_web/widgets.dart'; 4 | import '../games/mineaweeper/minesweeper.dart'; 5 | // import '../games/number_2048/number_2048.dart'; 6 | import '../games/tetris/tetris.dart'; 7 | import '../games/tic_tac_toe/tic_tac_toe.dart'; 8 | import '../widgets/loading.dart'; 9 | import '../configs.dart'; 10 | import '../layout.dart'; 11 | import '../main.dart'; 12 | 13 | class HomeScreen extends StatefulWidget { 14 | HomeScreen({ Key key, }) : super(key: key); 15 | @override 16 | _HomeScreenState createState() => _HomeScreenState(); 17 | } 18 | 19 | class _HomeScreenState extends State { 20 | bool _inited = false; 21 | void _initData() async { 22 | try { 23 | Level mineweeperLevel = Level.easy; 24 | AppState appState = App.of(context); 25 | appState.configs.mineweeperLevel = mineweeperLevel; 26 | await appState.updateConfig(appState.configs); 27 | } catch (ex) { 28 | print(ex); 29 | } finally { 30 | setState(() { _inited = true; }); 31 | } 32 | } 33 | @override 34 | void initState() { 35 | super.initState(); 36 | _initData(); 37 | } 38 | @override 39 | Widget build(BuildContext context) { 40 | if (!_inited) return Loading(); 41 | return Layout( 42 | initialState: LayoutState( 43 | currentBottomNavIndex: 0, 44 | ), 45 | children: [ 46 | Tetris(), 47 | Minesweeper(), 48 | TicTacToe(), 49 | // Number2048(), 50 | ], 51 | ); 52 | } 53 | } -------------------------------------------------------------------------------- /web/lib/screens/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/cupertino.dart'; 2 | import 'package:flutter_web/material.dart'; 3 | import '../configs.dart'; 4 | 5 | typedef void ConfigUpdater(GameConfigs configs); 6 | 7 | class SettingsScreen extends StatefulWidget { 8 | final GameConfigs configs; 9 | final ConfigUpdater configUpdater; 10 | SettingsScreen({ 11 | Key key, 12 | @required this.configs, 13 | @required this.configUpdater, 14 | }) : super(key: key); 15 | @override 16 | _SettingsScreenState createState() => _SettingsScreenState(); 17 | } 18 | 19 | class _SettingsScreenState extends State { 20 | Level currentMineweeperLevel; 21 | @override 22 | void initState() { 23 | super.initState(); 24 | currentMineweeperLevel = widget.configs.mineweeperLevel; 25 | } 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: Text('設定'), 31 | ), 32 | backgroundColor: Colors.white, 33 | body: ListView( 34 | children: [ 35 | Container( 36 | padding: EdgeInsets.only(left: 16, right: 16, top: 16), 37 | child: Text('踩地雷'), 38 | ), 39 | ListTile( 40 | title: Text('難度'), 41 | trailing: Text(LevelText[currentMineweeperLevel]), 42 | onTap: () async { 43 | showCupertinoModalPopup( 44 | context: context, 45 | builder: (BuildContext popupContext) { 46 | void setLevel(Level level) { 47 | widget.configs.mineweeperLevel = level; 48 | widget.configUpdater(widget.configs); 49 | Navigator.pop(popupContext); 50 | setState(() { 51 | currentMineweeperLevel = level; 52 | }); 53 | } 54 | return CupertinoActionSheet( 55 | title: Text('設定難度'), 56 | actions: [ 57 | CupertinoButton( 58 | child: Text(LevelText[Level.easy]), 59 | onPressed: () => setLevel(Level.easy), 60 | ), 61 | CupertinoButton( 62 | child: Text(LevelText[Level.medium]), 63 | onPressed: () => setLevel(Level.medium), 64 | ), 65 | CupertinoButton( 66 | child: Text(LevelText[Level.difficult]), 67 | onPressed: () => setLevel(Level.difficult), 68 | ), 69 | ], 70 | cancelButton: CupertinoButton( 71 | child: Text('取消'), 72 | onPressed: () { 73 | Navigator.pop(popupContext); 74 | }, 75 | ), 76 | ); 77 | }, 78 | ); 79 | }, 80 | ), 81 | ], 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /web/lib/utlis.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | 3 | Future alertMessage({ 4 | @required BuildContext context, 5 | @required String title, 6 | @required String okText, 7 | Widget titlePrefix, 8 | String cancelText, 9 | VoidCallback onOK, 10 | VoidCallback onCancel, 11 | }) { 12 | return showDialog( 13 | context: context, 14 | builder: (BuildContext context) { 15 | final titleWidgets = [ 16 | Text(title, style: TextStyle(fontSize: 32)), 17 | ]; 18 | if (titlePrefix != null) { 19 | titleWidgets.insert(0, titlePrefix); 20 | } 21 | final actionWidgets = [ 22 | SimpleDialogOption( 23 | child: Text(okText), 24 | onPressed: onOK, 25 | ), 26 | ]; 27 | if (cancelText != null) { 28 | actionWidgets.add( 29 | SimpleDialogOption( 30 | child: Text(cancelText), 31 | onPressed: onCancel, 32 | ) 33 | ); 34 | } 35 | return SimpleDialog( 36 | title: Row( 37 | mainAxisAlignment: MainAxisAlignment.start, 38 | children: titleWidgets, 39 | ), 40 | children: actionWidgets, 41 | ); 42 | }, 43 | ); 44 | } -------------------------------------------------------------------------------- /web/lib/widgets/bonuce_icon.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:async'; 2 | import 'package:flutter_web/material.dart'; 3 | 4 | class BonuceIcon extends StatefulWidget { 5 | final IconData icon; 6 | final double size; 7 | final Color color; 8 | 9 | BonuceIcon( 10 | this.icon, { 11 | Key key, 12 | this.size = 24.0, 13 | this.color, 14 | }) : super(key: key); 15 | @override 16 | State createState() => _BonuceIconState(); 17 | } 18 | 19 | class _BonuceIconState extends State with SingleTickerProviderStateMixin { 20 | final double dx = 4.0; 21 | AnimationController controller; 22 | Animation animation; 23 | 24 | @override 25 | initState() { 26 | super.initState(); 27 | controller = AnimationController( 28 | duration: Duration(milliseconds: 300), vsync: this); 29 | animation = Tween(begin: widget.size, end: widget.size + dx) 30 | .animate(controller); 31 | 32 | animation.addStatusListener((status) { 33 | if (status == AnimationStatus.completed) { 34 | controller.reverse(); 35 | } 36 | // else if (status == AnimationStatus.dismissed) { 37 | // Future.delayed(Duration(seconds: 2), () { 38 | // if (!mounted) return; 39 | // controller?.forward(); 40 | // }); 41 | // } 42 | }); 43 | controller.forward(); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | controller.dispose(); 49 | super.dispose(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return _Animator( 55 | icon: widget.icon, 56 | animation: animation, 57 | color: widget.color, 58 | size: widget.size + dx, 59 | ); 60 | } 61 | } 62 | 63 | class _Animator extends AnimatedWidget { 64 | final double size; 65 | final IconData icon; 66 | final Color color; 67 | _Animator({ 68 | Key key, 69 | this.icon, 70 | this.size, 71 | this.color, 72 | Animation animation, 73 | }) : super(key: key, listenable: animation); 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | final Animation animation = listenable; 78 | return Container( 79 | width: size, 80 | height: size, 81 | child: Center( 82 | child: Icon( 83 | icon, 84 | size: animation.value, 85 | color: color, 86 | ), 87 | ), 88 | ); 89 | } 90 | } -------------------------------------------------------------------------------- /web/lib/widgets/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | import 'package:flutter_web/cupertino.dart'; 3 | 4 | class Loading extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Container( 8 | alignment: Alignment.center, 9 | color: Colors.white, 10 | width: double.infinity, 11 | height: double.infinity, 12 | child: CircularProgressIndicator( 13 | backgroundColor: Colors.transparent, 14 | ), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/lib/widgets/swipeable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_web/material.dart'; 2 | 3 | typedef void SwipeCallback(SwipeDirection direction); 4 | 5 | class Swipeable extends StatefulWidget { 6 | static double swipeLimit = 20; 7 | final Widget child; 8 | final SwipeCallback onSwipe; 9 | Swipeable({ 10 | Key key, 11 | @required this.child, 12 | @required this.onSwipe, 13 | }) : super(key: key); 14 | @override 15 | _SwipeableState createState() => _SwipeableState(); 16 | } 17 | 18 | class _SwipeableState extends State { 19 | double dx; 20 | double dy; 21 | @override 22 | Widget build(BuildContext context) { 23 | return GestureDetector( 24 | child: widget.child, 25 | onPanDown: (details) { 26 | dx = dy = 0; 27 | }, 28 | onPanUpdate: (details) { 29 | if (dx == null || dy == null) return; 30 | dx += details.delta.dx; 31 | dy += details.delta.dy; 32 | SwipeDirection direction; 33 | if (dx > Swipeable.swipeLimit) { 34 | direction = SwipeDirection.right; 35 | dx = null; 36 | } else if (dx < -Swipeable.swipeLimit) { 37 | direction = SwipeDirection.left; 38 | dx = null; 39 | } 40 | if (dy > Swipeable.swipeLimit) { 41 | if (direction == SwipeDirection.right) { 42 | direction = SwipeDirection.downRight; 43 | } else if (direction == SwipeDirection.left) { 44 | direction = SwipeDirection.downLeft; 45 | } else { 46 | direction = SwipeDirection.down; 47 | } 48 | dy = null; 49 | } else if (dy < -Swipeable.swipeLimit) { 50 | if (direction == SwipeDirection.right) { 51 | direction = SwipeDirection.upRight; 52 | } else if (direction == SwipeDirection.left) { 53 | direction = SwipeDirection.upLeft; 54 | } else { 55 | direction = SwipeDirection.up; 56 | } 57 | dy = null; 58 | } 59 | if (direction == null) return; 60 | widget.onSwipe(direction); 61 | }, 62 | ); 63 | } 64 | } 65 | 66 | enum SwipeDirection { 67 | up, 68 | upLeft, 69 | upRight, 70 | right, 71 | downRight, 72 | down, 73 | downLeft, 74 | left, 75 | } 76 | -------------------------------------------------------------------------------- /web/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: web 2 | description: An app built using Flutter for web 3 | 4 | environment: 5 | # You must be using Flutter >=1.5.0 or Dart >=2.3.0 6 | sdk: '>=2.3.0 <3.0.0' 7 | 8 | dependencies: 9 | flutter_web: any 10 | flutter_web_ui: any 11 | 12 | dev_dependencies: 13 | build_daemon: '>=1.0.0 <2.0.0' 14 | build_runner: ^1.5.0 15 | build_web_compilers: ^2.1.0 16 | pedantic: ^1.7.0 17 | 18 | dependency_overrides: 19 | flutter_web: 20 | git: 21 | url: https://github.com/flutter/flutter_web 22 | path: packages/flutter_web 23 | flutter_web_ui: 24 | git: 25 | url: https://github.com/flutter/flutter_web 26 | path: packages/flutter_web_ui 27 | flutter: 28 | uses-material-design: true -------------------------------------------------------------------------------- /web/web/assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "family": "MaterialIcons", 4 | "fonts": [ 5 | { 6 | "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" 7 | } 8 | ] 9 | } 10 | ] -------------------------------------------------------------------------------- /web/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/web/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | import 'package:flutter_web_ui/ui.dart' as ui; 5 | import 'package:web/main.dart' as app; 6 | 7 | main() async { 8 | await ui.webOnlyInitializePlatform(); 9 | app.main(); 10 | } 11 | --------------------------------------------------------------------------------