├── .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 |
--------------------------------------------------------------------------------