├── functions ├── .gitignore ├── requirements.txt └── main.py ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── LaunchImage.imageset │ │ │ ├── Logo.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Logo-ios.png │ │ │ └── Contents.json │ ├── Runner.entitlements │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift ├── .gitignore └── Podfile ├── web ├── favicon.png └── manifest.json ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── Logo.png ├── ios │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── Logo.png ├── Feature.jpg └── Feature_resized.jpg ├── assets ├── images │ ├── ai.png │ └── user.png └── audio │ ├── click.wav │ ├── click1.ogg │ ├── click2.mp3 │ ├── click2.ogg │ └── bg_music.mp3 ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── values │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── princeappstudio │ │ │ │ │ └── tic_tac_toe │ │ │ │ │ └── MainActivity.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── lib ├── components │ ├── my_spacer.dart │ ├── icon_button.dart │ ├── player_card.dart │ ├── button.dart │ └── pop_up.dart ├── helper │ ├── navigation.dart │ ├── random_gen.dart │ ├── screenshot_board.dart │ ├── audio_controller.dart │ ├── board_desgin.dart │ ├── animation_widget.dart │ ├── show_interstitial_ad.dart │ ├── check_win.dart │ ├── show_banner_ad.dart │ └── game.dart ├── model │ ├── symbol.dart │ ├── player.dart │ └── room.dart ├── provider │ ├── audio_provider.dart │ ├── theme_provider.dart │ ├── room_provider.dart │ ├── game_provider.dart │ ├── login_provider.dart │ └── single_mode_provider.dart ├── constants.dart ├── main.dart └── screen │ ├── home.dart │ ├── settings.dart │ ├── single_mode.dart │ ├── room.dart │ ├── lobby.dart │ └── game.dart ├── .gitignore ├── analysis_options.yaml ├── .metadata ├── pubspec.yaml ├── README.md └── privacy_policy.md /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ./venv -------------------------------------------------------------------------------- /functions/requirements.txt: -------------------------------------------------------------------------------- 1 | firebase_functions~=0.1.0 -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/web/favicon.png -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/8.png -------------------------------------------------------------------------------- /assets/images/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/images/ai.png -------------------------------------------------------------------------------- /screenshots/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/Logo.png -------------------------------------------------------------------------------- /screenshots/ios/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/1.png -------------------------------------------------------------------------------- /screenshots/ios/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/2.png -------------------------------------------------------------------------------- /screenshots/ios/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/3.png -------------------------------------------------------------------------------- /screenshots/ios/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/4.png -------------------------------------------------------------------------------- /screenshots/ios/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/5.png -------------------------------------------------------------------------------- /assets/audio/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/audio/click.wav -------------------------------------------------------------------------------- /assets/audio/click1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/audio/click1.ogg -------------------------------------------------------------------------------- /assets/audio/click2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/audio/click2.mp3 -------------------------------------------------------------------------------- /assets/audio/click2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/audio/click2.ogg -------------------------------------------------------------------------------- /assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/images/user.png -------------------------------------------------------------------------------- /screenshots/Feature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/Feature.jpg -------------------------------------------------------------------------------- /assets/audio/bg_music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/assets/audio/bg_music.mp3 -------------------------------------------------------------------------------- /screenshots/ios/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/ios/Logo.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /screenshots/Feature_resized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/screenshots/Feature_resized.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/Logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Logo-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Logo-ios.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princesanjivy/tic-tac-toe/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/princeappstudio/tic_tac_toe/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.princeappstudio.tic_tac_toe; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Logo-ios.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tic Tac Toe - Online", 3 | "short_name": "tic_tac_toe", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Tic Tac Toe - Play online, offline with friends.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/components/my_spacer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class VerticalSpacer extends StatelessWidget { 4 | final double space; 5 | 6 | const VerticalSpacer(this.space, {Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return SizedBox( 11 | height: space, 12 | ); 13 | } 14 | } 15 | 16 | class HorizontalSpacer extends StatelessWidget { 17 | final double space; 18 | 19 | const HorizontalSpacer(this.space, {Key? key}) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return SizedBox( 24 | width: space, 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/helper/navigation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:page_transition/page_transition.dart'; 3 | 4 | class Navigation { 5 | late final NavigatorState _navigatorState; 6 | 7 | Navigation(this._navigatorState); 8 | 9 | Future changeScreenReplacement(Widget screen, Widget currentScreen) { 10 | return _navigatorState.pushReplacement( 11 | PageTransition( 12 | child: screen, 13 | type: PageTransitionType.rightToLeftWithFade, 14 | childCurrent: currentScreen, 15 | duration: const Duration(milliseconds: 400), 16 | reverseDuration: const Duration(milliseconds: 400), 17 | ), 18 | ); 19 | } 20 | 21 | void goBack(BuildContext context) { 22 | return Navigator.pop(context); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | plugins { 14 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 15 | } 16 | } 17 | 18 | include ":app" 19 | 20 | apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" 21 | -------------------------------------------------------------------------------- /lib/helper/random_gen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:tic_tac_toe/model/symbol.dart'; 4 | 5 | int generateRandomRoomCode() { 6 | Random random = Random(); 7 | int min = 100000; // Minimum 6-digit number 8 | int max = 999999; // Maximum 6-digit number 9 | return min + random.nextInt(max - min); 10 | } 11 | 12 | int generateRandomBoardSize() { 13 | Random random = Random(); 14 | int min = 3; 15 | int max = 5; 16 | 17 | int number = min + random.nextInt(max - min + 1); 18 | int size = pow(number, 2) as int; 19 | 20 | return size; 21 | } 22 | 23 | String generateRandomPlaySymbol() { 24 | Random random = Random(); 25 | int randomNumber = random.nextInt(2) + 1; 26 | 27 | if (randomNumber == PlaySymbol.xInt) { 28 | return PlaySymbol.x; 29 | } 30 | 31 | return PlaySymbol.o; 32 | } 33 | -------------------------------------------------------------------------------- /lib/model/symbol.dart: -------------------------------------------------------------------------------- 1 | class PlaySymbol { 2 | static const String _symbolX = "X"; 3 | static const String _symbolO = "O"; 4 | static const String _symbolDraw = "Draw"; 5 | 6 | static const int _symbolXInt = 1; 7 | static const int _symbolOInt = 2; 8 | 9 | static get x { 10 | return _symbolX; 11 | } 12 | 13 | static get o { 14 | return _symbolO; 15 | } 16 | 17 | static get draw { 18 | return _symbolDraw; 19 | } 20 | 21 | static get xInt { 22 | return _symbolXInt; 23 | } 24 | 25 | static get oInt { 26 | return _symbolOInt; 27 | } 28 | 29 | static int inNum(String symbol) { 30 | // can be rewritten using map 31 | if (symbol == _symbolX) { 32 | return _symbolXInt; 33 | } 34 | if (symbol == _symbolO) { 35 | return _symbolOInt; 36 | } 37 | 38 | return 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | // END: FlutterFire Configuration 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | rootProject.buildDir = '../build' 25 | subprojects { 26 | project.buildDir = "${rootProject.buildDir}/${project.name}" 27 | } 28 | subprojects { 29 | project.evaluationDependsOn(':app') 30 | } 31 | 32 | tasks.register("clean", Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/helper/screenshot_board.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'package:share_plus/share_plus.dart'; 7 | 8 | Future screenshotBoard(GlobalKey globalKey) async { 9 | final RenderRepaintBoundary boundary = 10 | globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; 11 | final ui.Image image = await boundary.toImage(pixelRatio: 3); 12 | final ByteData? byteData = 13 | await image.toByteData(format: ui.ImageByteFormat.png); 14 | final Uint8List pngBytes = byteData!.buffer.asUint8List(); 15 | 16 | String tempName = DateTime 17 | .now() 18 | .microsecondsSinceEpoch 19 | .toString(); 20 | 21 | final XFile fileData = XFile.fromData( 22 | pngBytes, 23 | mimeType: "image/png", 24 | name: tempName, 25 | ); 26 | 27 | return fileData; 28 | } 29 | -------------------------------------------------------------------------------- /lib/helper/audio_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:audioplayers/audioplayers.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:tic_tac_toe/provider/audio_provider.dart'; 5 | 6 | class AudioController { 7 | static final AudioController _audioController = AudioController._internal(); 8 | 9 | // bool playAudio = true; 10 | late AssetSource source; 11 | AudioPlayer player = AudioPlayer(); 12 | 13 | factory AudioController() { 14 | return _audioController; 15 | } 16 | 17 | AudioController._internal() { 18 | init(); 19 | } 20 | 21 | void init() async { 22 | source = AssetSource("audio/click2.mp3"); 23 | } 24 | 25 | void buttonClick(BuildContext context) async { 26 | AudioProvider audioProvider = 27 | Provider.of(context, listen: false); 28 | if (audioProvider.canPlayAudio) { 29 | await player.play(source); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/helper/board_desgin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tic_tac_toe/helper/game.dart' as helper; 3 | 4 | class BoardDesign { 5 | List corners = []; 6 | Map borders = {}; 7 | 8 | BoardDesign(this.corners, this.borders); 9 | } 10 | 11 | BoardDesign boardDesign(List board) { 12 | Map borders = {}; 13 | 14 | List borderRadius = [ 15 | const BorderRadius.only( 16 | topLeft: Radius.circular(16), 17 | ), 18 | const BorderRadius.only( 19 | topRight: Radius.circular(16), 20 | ), 21 | const BorderRadius.only( 22 | bottomLeft: Radius.circular(16), 23 | ), 24 | const BorderRadius.only( 25 | bottomRight: Radius.circular(16), 26 | ), 27 | ]; 28 | List corners = helper.findCornerPositions(board.length); 29 | for (int i = 0; i < corners.length; i++) { 30 | borders.addAll({corners[i]: borderRadius[i]}); 31 | } 32 | 33 | return BoardDesign(corners, borders); 34 | } 35 | -------------------------------------------------------------------------------- /lib/model/player.dart: -------------------------------------------------------------------------------- 1 | class Player { 2 | final String name; 3 | final String playerId; 4 | final String displayPicture; 5 | String chose = ""; 6 | int winCount = 0; 7 | 8 | Player( 9 | this.name, 10 | this.playerId, 11 | this.displayPicture, 12 | ); 13 | 14 | Player.fromRoomDataJson(json) 15 | : chose = json!["chose"], 16 | playerId = json!["id"], 17 | winCount = json!["winCount"], 18 | displayPicture = "", 19 | 20 | /// temp 21 | name = ""; 22 | 23 | /// temp 24 | 25 | Player.fromJson(json) 26 | : name = json!["name"], 27 | displayPicture = json!["displayPicture"], 28 | playerId = json!["playerId"]; 29 | 30 | Map toJson() => { 31 | "id": playerId, 32 | "chose": chose, 33 | "winCount": winCount, 34 | }; 35 | 36 | Map toDbJson() => { 37 | "name": name, 38 | "playerId": playerId, 39 | "displayPicture": displayPicture, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /functions/main.py: -------------------------------------------------------------------------------- 1 | from firebase_functions import scheduler_fn 2 | from firebase_admin import initialize_app, db 3 | from datetime import datetime 4 | 5 | initialize_app() 6 | 7 | 8 | @scheduler_fn.on_schedule(schedule="0 * * * *") 9 | def on_every_hour(event: scheduler_fn.ScheduledEvent) -> None: 10 | date_format = "%Y-%m-%d %H:%M:%S.%f" # eg: 2023-07-26 21:43:50.019983 11 | path = db.reference("room/") 12 | data = path.get() 13 | 14 | print(data) 15 | 16 | if data: 17 | for room in data: 18 | created_at_str = data[room]["createdAt"] 19 | created_at = datetime.strptime(created_at_str, date_format) 20 | current_time = datetime.now() 21 | 22 | # print(created_at) 23 | # print(current_time) 24 | 25 | time_diff = current_time - created_at 26 | 27 | if time_diff.total_seconds() > 3600: 28 | print("Time difference is greater than 1 hour") 29 | print(f"Deleting... room => {room}") 30 | 31 | path.child(room).delete() 32 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | /lib/firebase_options.dart 49 | /android/app/google-services.json 50 | 51 | pubspec.lock 52 | 53 | /web/index.html 54 | /web/sw.js 55 | 56 | # functions venv 57 | /functions/venv/ 58 | /cloud-functions-js/ 59 | 60 | .firebase 61 | firebase.json 62 | .firebaserc 63 | 64 | android/flutter-tic-tac-toe-1f1db-7d46c91ea2c7.json 65 | 66 | web/adsterra_banner_ad.html 67 | 68 | # ios 69 | /ios/Runner/GoogleService-info.plist 70 | -------------------------------------------------------------------------------- /lib/provider/audio_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:tic_tac_toe/helper/audio_controller.dart'; 4 | 5 | class AudioProvider with ChangeNotifier { 6 | AudioController audioController = AudioController(); 7 | 8 | bool _canPlayAudio = true; 9 | 10 | AudioProvider.init() { 11 | // showLoading = true; 12 | // notifyListeners(); 13 | 14 | getPrefs(); 15 | } 16 | 17 | void getPrefs() async { 18 | SharedPreferences preferences = await SharedPreferences.getInstance(); 19 | bool? val = preferences.getBool("isLight"); 20 | 21 | if (val == null) { 22 | _canPlayAudio = true; 23 | } else { 24 | _canPlayAudio = val; 25 | } 26 | 27 | print("Can playAudio: $_canPlayAudio"); 28 | // showLoading = false; 29 | notifyListeners(); 30 | } 31 | 32 | void setPrefs(bool value) async { 33 | SharedPreferences preferences = await SharedPreferences.getInstance(); 34 | preferences.setBool("canPlayAudio", value); 35 | } 36 | 37 | bool get canPlayAudio { 38 | return _canPlayAudio; 39 | } 40 | 41 | void setPlayAudio() { 42 | _canPlayAudio = !_canPlayAudio; 43 | 44 | setPrefs(_canPlayAudio); 45 | // audioController.playAudio = _canPlayAudio; 46 | notifyListeners(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/helper/animation_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 3 | 4 | class AnimationOnWidget extends StatefulWidget { 5 | final Widget child; 6 | final int? msDelay; 7 | final bool useIncomingEffect, hasRestEffect; 8 | final bool? doStateChange; 9 | final WidgetTransitionEffects? incomingEffect; 10 | 11 | const AnimationOnWidget({ 12 | super.key, 13 | required this.child, 14 | this.msDelay, 15 | this.useIncomingEffect = false, 16 | this.hasRestEffect = false, 17 | this.doStateChange = false, 18 | this.incomingEffect, 19 | }); 20 | 21 | @override 22 | State createState() => _AnimationOnWidgetState(); 23 | } 24 | 25 | class _AnimationOnWidgetState extends State { 26 | @override 27 | Widget build(BuildContext context) { 28 | return WidgetAnimator( 29 | incomingEffect: widget.useIncomingEffect 30 | ? widget.incomingEffect 31 | : WidgetTransitionEffects.incomingScaleUp( 32 | delay: Duration(milliseconds: widget.msDelay!), 33 | curve: Curves.easeInOut, 34 | ), 35 | atRestEffect: widget.hasRestEffect ? WidgetRestingEffects.wave() : null, 36 | doStateChange: widget.doStateChange, 37 | child: widget.child, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Color primaryColor = const Color(0xFF404B47); 4 | // Color secondaryColor = const Color(0xFFCB974E); 5 | // Color bgColor = Colors.white; 6 | 7 | // Color primaryColor = const Color(0xFFB95300); 8 | // Color secondaryColor = const Color(0xFFF39C11); 9 | // Color bgColor = const Color(0xFFFBE5A9); 10 | // Color winColor = const Color(0xFF00FF7F); 11 | 12 | double defaultTextSize = 18; 13 | double borderRadius = 12; 14 | 15 | int msAnimationDelay = 150; 16 | 17 | String imageUrl = 18 | "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80"; 19 | String roomPath = "/room/"; 20 | String gameLinkAndroid = 21 | "https://play.google.com/store/apps/details?id=com.princeappstudio.tic_tac_toe", 22 | gameLinkWeb = "https://tictactoe.princeappstudio.in"; 23 | String gameLinkIos = "https://apps.apple.com/us/app/tic-tac-toe-online-2player/id6740833110"; 24 | 25 | List shadow = [ 26 | BoxShadow( 27 | color: Colors.grey.withOpacity(0.8), 28 | blurRadius: 8, 29 | offset: const Offset(0, 2), 30 | // blurStyle: BlurStyle.outer, 31 | ), 32 | ]; 33 | 34 | /// Test Ad ids 35 | const String bannerId1 = "ca-app-pub-3940256099942544/6300978111"; 36 | const String bannerId2 = "ca-app-pub-3940256099942544/6300978111"; 37 | const String interstitialId1 = "ca-app-pub-3940256099942544/1033173712"; 38 | const String interstitialId2 = "ca-app-pub-3940256099942544/1033173712"; 39 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.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 flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/model/room.dart: -------------------------------------------------------------------------------- 1 | import 'package:tic_tac_toe/model/player.dart'; 2 | 3 | class RoomData { 4 | final int code; 5 | final String roomOwnerId; 6 | final String turn; 7 | final DateTime createdAt; 8 | bool isStarted = false; 9 | 10 | final List players; 11 | final List board; 12 | 13 | final int round; 14 | 15 | RoomData( 16 | this.code, 17 | this.roomOwnerId, 18 | this.turn, 19 | this.players, 20 | this.board, 21 | this.round, 22 | this.createdAt, 23 | ); 24 | 25 | factory RoomData.fromJson(json, int code) { 26 | List players = []; 27 | List.from(json!["players"]).forEach((element) { 28 | players.add(Player.fromRoomDataJson(element)); 29 | }); 30 | 31 | RoomData roomData = RoomData( 32 | code, 33 | json!["roomOwnerId"], 34 | json!["turn"], 35 | players, 36 | json!["board"], 37 | json!["round"], 38 | DateTime.parse( 39 | json!["createdAt"], 40 | ), 41 | ); 42 | roomData.isStarted = json!["isStarted"]; 43 | 44 | return roomData; 45 | } 46 | 47 | List> playersToJson(List p) { 48 | List> temp = []; 49 | for (Player element in p) { 50 | temp.add(element.toJson()); 51 | } 52 | 53 | return temp; 54 | } 55 | 56 | Map toJson() => { 57 | "roomOwnerId": roomOwnerId, 58 | "turn": turn, 59 | "isStarted": isStarted, 60 | "players": playersToJson(players), 61 | "board": board, 62 | "round": round, 63 | "createdAt": createdAt.toString(), 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /lib/helper/show_interstitial_ad.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 4 | import 'package:tic_tac_toe/constants.dart'; 5 | 6 | class FullScreenAd { 7 | static final FullScreenAd object = FullScreenAd._internal(); 8 | 9 | InterstitialAd? interstitialAd; 10 | int maxFailedLoadAttempts = 3; 11 | int numInterstitialLoadAttempts = 0; 12 | 13 | FullScreenAd._internal() { 14 | _createInterstitialAd(); 15 | } 16 | 17 | factory FullScreenAd() { 18 | return object; 19 | } 20 | 21 | void _createInterstitialAd() { 22 | final ids = [interstitialId1, interstitialId2]; 23 | final random = Random(); 24 | 25 | InterstitialAd.load( 26 | adUnitId: ids[random.nextInt(ids.length)], 27 | request: const AdRequest(), 28 | adLoadCallback: InterstitialAdLoadCallback( 29 | onAdLoaded: (ad) { 30 | interstitialAd = ad; 31 | numInterstitialLoadAttempts = 0; 32 | }, 33 | onAdFailedToLoad: (error) { 34 | print("Ad Failed to load"); 35 | numInterstitialLoadAttempts += 1; 36 | interstitialAd = null; 37 | if (numInterstitialLoadAttempts <= maxFailedLoadAttempts) { 38 | _createInterstitialAd(); 39 | } 40 | }, 41 | ), 42 | ); 43 | } 44 | 45 | void show() { 46 | if (interstitialAd == null) { 47 | return; 48 | } 49 | interstitialAd!.fullScreenContentCallback = FullScreenContentCallback( 50 | onAdDismissedFullScreenContent: (InterstitialAd ad) { 51 | ad.dispose(); 52 | _createInterstitialAd(); 53 | }, 54 | onAdFailedToShowFullScreenContent: (InterstitialAd ad, AdError error) { 55 | ad.dispose(); 56 | _createInterstitialAd(); 57 | }, 58 | ); 59 | 60 | interstitialAd!.show(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.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: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 17 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 18 | - platform: android 19 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 20 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 21 | - platform: ios 22 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 23 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 24 | - platform: linux 25 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 26 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 27 | - platform: macos 28 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 29 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 30 | - platform: web 31 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 32 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 33 | - platform: windows 34 | create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 35 | base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Tic Tac Toe 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | tic_tac_toe 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLSchemes 31 | 32 | com.googleusercontent.apps.1052229586554-g7tm8tqpjq242hmphomb1hn9bejqa26r 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(FLUTTER_BUILD_NUMBER) 38 | GADApplicationIdentifier 39 | ca-app-pub-5164932036098856~6363578508 40 | LSRequiresIPhoneOS 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiresFullScreen 49 | 50 | UIStatusBarHidden 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tic_tac_toe 2 | description: Play offline, with friends, or online worldwide. Customize boards & themes. 3 | 4 | version: 1.0.4+5 5 | 6 | environment: 7 | sdk: '>=3.1.0-149.0.dev <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | firebase_database: ^11.0.4 14 | firebase_core: ^3.3.0 15 | audioplayers: ^6.1.0 16 | google_fonts: ^5.1.0 17 | provider: ^6.0.5 18 | widget_and_text_animator: 19 | git: 20 | url: https://github.com/princesanjivy/widget_and_text_animator.git 21 | ref: main 22 | vibration: ^1.8.1 23 | shared_preferences: ^2.1.2 24 | page_transition: ^2.0.9 25 | share_plus: ^7.0.2 26 | firebase_auth: ^5.4.1 27 | google_sign_in: ^6.2.2 28 | fluttertoast: ^8.0.8 29 | cloud_firestore: ^5.6.2 30 | flutter_svg: ^2.0.7 31 | clipboard: ^0.1.3 32 | url_launcher: ^6.1.14 33 | google_mobile_ads: ^5.2.0 34 | sign_in_with_apple: ^6.1.4 35 | 36 | dependency_overrides: 37 | http: ^1.0.0 38 | 39 | dev_dependencies: 40 | flutter_test: 41 | sdk: flutter 42 | 43 | # The "flutter_lints" package below contains a set of recommended lints to 44 | # encourage good coding practices. The lint set provided by the package is 45 | # activated in the `analysis_options.yaml` file located at the root of your 46 | # package. See that file for information about deactivating specific lint 47 | # rules and activating additional ones. 48 | flutter_lints: ^2.0.0 49 | 50 | # For information on the generic Dart part of this file, see the 51 | # following page: https://dart.dev/tools/pub/pubspec 52 | 53 | # The following section is specific to Flutter packages. 54 | flutter: 55 | 56 | # The following line ensures that the Material Icons font is 57 | # included with your application, so that you can use the icons in 58 | # the material Icons class. 59 | uses-material-design: true 60 | 61 | # To add assets to your application, add an assets section, like this: 62 | assets: 63 | - assets/audio/ 64 | - assets/images/ 65 | 66 | -------------------------------------------------------------------------------- /lib/helper/check_win.dart: -------------------------------------------------------------------------------- 1 | class Result { 2 | bool hasWon; 3 | List positions; 4 | 5 | Result(this.hasWon, this.positions); 6 | } 7 | 8 | Result checkWin(List board, int player, int size) { 9 | // int size = 10 | // (board.length / 5).round(); // Calculate the size of the square board 11 | 12 | // Check rows 13 | for (int i = 0; i < size; i++) { 14 | int rowStart = i * size; 15 | List rowPositions = []; 16 | if (board 17 | .sublist(rowStart, rowStart + size) 18 | .every((cell) => cell == player)) { 19 | for (int j = 0; j < size; j++) { 20 | rowPositions.add(rowStart + j); 21 | } 22 | return Result(true, rowPositions); 23 | } 24 | } 25 | 26 | // Check columns 27 | for (int i = 0; i < size; i++) { 28 | int colStart = i; 29 | List colPositions = []; 30 | if (List.generate(size, (j) => board[colStart + j * size]) 31 | .every((cell) => cell == player)) { 32 | for (int j = 0; j < size; j++) { 33 | colPositions.add(colStart + j * size); 34 | } 35 | return Result(true, colPositions); 36 | } 37 | } 38 | 39 | // Check diagonals 40 | List diagonalPositions1 = []; 41 | bool isDiagonal1Win = true; 42 | for (int i = 0; i < size; i++) { 43 | int position = i * size + i; 44 | diagonalPositions1.add(position); 45 | if (board[position] != player) { 46 | isDiagonal1Win = false; 47 | break; 48 | } 49 | } 50 | 51 | if (isDiagonal1Win) { 52 | return Result(true, diagonalPositions1); 53 | } 54 | 55 | List diagonalPositions2 = []; 56 | bool isDiagonal2Win = true; 57 | for (int i = 0; i < size; i++) { 58 | int position = (i + 1) * size - 1 - i; 59 | diagonalPositions2.add(position); 60 | if (board[position] != player) { 61 | isDiagonal2Win = false; 62 | break; 63 | } 64 | } 65 | 66 | if (isDiagonal2Win) { 67 | return Result(true, diagonalPositions2); 68 | } 69 | 70 | // No winning condition found 71 | return Result(false, []); 72 | } 73 | -------------------------------------------------------------------------------- /lib/provider/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class ThemeProvider with ChangeNotifier { 5 | final Color _primaryColorLight = const Color(0xFF404B47); 6 | final Color _secondaryColorLight = const Color(0xFFCB974E); 7 | final _bgColorLight = Colors.white; 8 | final Color _winColorLight = const Color(0xFF00FF7F); 9 | 10 | final Color _primaryColorDark = const Color(0xFFB95300); 11 | final Color _secondaryColorDark = const Color(0xFFF39C11); 12 | final Color _bgColorDark = const Color(0xFFFBE5A9); 13 | final Color _winColorDark = const Color(0xFF00FF7F); 14 | 15 | bool _isLight = true; 16 | bool showLoading = true; 17 | 18 | ThemeProvider.init() { 19 | showLoading = true; 20 | notifyListeners(); 21 | 22 | getPrefs(); 23 | } 24 | 25 | void getPrefs() async { 26 | SharedPreferences preferences = await SharedPreferences.getInstance(); 27 | bool? val = preferences.getBool("isLight"); 28 | 29 | if (val == null) { 30 | _isLight = true; 31 | } else { 32 | _isLight = val; 33 | } 34 | 35 | print("Theme isLight: $_isLight"); 36 | showLoading = false; 37 | notifyListeners(); 38 | } 39 | 40 | void setPrefs(bool value) async { 41 | SharedPreferences preferences = await SharedPreferences.getInstance(); 42 | preferences.setBool("isLight", value); 43 | } 44 | 45 | Color get primaryColor { 46 | return _isLight ? _primaryColorLight : _primaryColorDark; 47 | } 48 | 49 | Color get secondaryColor { 50 | return _isLight ? _secondaryColorLight : _secondaryColorDark; 51 | } 52 | 53 | Color get bgColor { 54 | return _isLight ? _bgColorLight : _bgColorDark; 55 | } 56 | 57 | Color get winColor { 58 | return _isLight ? _winColorLight : _winColorDark; 59 | } 60 | 61 | bool get isLightTheme { 62 | return _isLight; 63 | } 64 | 65 | void changeTheme() { 66 | _isLight = !_isLight; 67 | setPrefs(_isLight); 68 | notifyListeners(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/helper/show_banner_ad.dart: -------------------------------------------------------------------------------- 1 | // ignore: avoid_web_libraries_in_flutter 2 | // import 'dart:html'; 3 | import 'dart:math'; 4 | import 'dart:ui' as ui; 5 | 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 9 | import 'package:tic_tac_toe/constants.dart'; 10 | 11 | class BottomBannerAd { 12 | late BannerAd _bannerAd; 13 | 14 | BottomBannerAd() { 15 | if (!kIsWeb) { 16 | _initBannerAd(); 17 | } 18 | } 19 | 20 | void _initBannerAd() { 21 | final ids = [bannerId1, bannerId2]; 22 | final random = Random(); 23 | 24 | _bannerAd = BannerAd( 25 | adUnitId: ids[random.nextInt(ids.length)], 26 | size: AdSize.banner, 27 | request: const AdRequest(), 28 | listener: const BannerAdListener( 29 | // onAdLoaded: (ad) { 30 | // _isLoaded = true; 31 | // }, 32 | ), 33 | ); 34 | 35 | _bannerAd.load(); 36 | } 37 | 38 | // Widget bannerForWeb() { 39 | // // ignore: undefined_prefixed_name 40 | // ui.platformViewRegistry.registerViewFactory( 41 | // "bannerAd", 42 | // (int viewID) => IFrameElement() 43 | // ..width = "468" 44 | // ..height = "60" 45 | // ..src = "adsterra_banner_ad.html" 46 | // ..style.border = "none"); 47 | 48 | // return const SizedBox( 49 | // height: 60, 50 | // width: 468, 51 | // child: HtmlElementView( 52 | // viewType: "bannerAd", 53 | // ), 54 | // ); 55 | // } 56 | 57 | Widget showBanner() { 58 | return kIsWeb 59 | ? Container( 60 | alignment: Alignment.center, 61 | padding: EdgeInsets.all(2), 62 | height: 26, 63 | child: const Text( 64 | "Copyright © princeappstudio.in", 65 | style: TextStyle( 66 | color: Colors.cyan, 67 | ), 68 | ), 69 | ) 70 | // ? bannerForWeb() 71 | : Container( 72 | alignment: Alignment.center, 73 | width: _bannerAd.size.width.toDouble(), 74 | height: _bannerAd.size.height.toDouble(), 75 | child: AdWidget( 76 | ad: _bannerAd, 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/components/icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:tic_tac_toe/constants.dart'; 5 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 6 | import 'package:tic_tac_toe/helper/audio_controller.dart'; 7 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 8 | import 'package:vibration/vibration.dart'; 9 | 10 | class MyIconButton extends StatelessWidget { 11 | const MyIconButton({ 12 | super.key, 13 | required this.onPressed, 14 | required this.msDelay, 15 | required this.iconData, 16 | }); 17 | 18 | final Function onPressed; 19 | final int msDelay; 20 | final IconData iconData; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Positioned( 25 | top: 32, 26 | right: 32, 27 | child: AnimationOnWidget( 28 | msDelay: 1200, // need to change \ get as arg 29 | doStateChange: true, 30 | child: ElevatedButton( 31 | onPressed: () async { 32 | if (!kIsWeb) { 33 | Vibration.vibrate(duration: 80, amplitude: 120); 34 | } 35 | AudioController audioController = AudioController(); 36 | audioController.buttonClick(context); 37 | await Future.delayed(Duration(milliseconds: msAnimationDelay)); 38 | onPressed(); 39 | }, 40 | style: ButtonStyle( 41 | minimumSize: MaterialStateProperty.all( 42 | const Size(48, 48), 43 | ), 44 | elevation: MaterialStateProperty.all(4), 45 | shape: MaterialStateProperty.all( 46 | RoundedRectangleBorder( 47 | borderRadius: BorderRadius.circular(12), 48 | ), 49 | ), 50 | padding: 51 | MaterialStateProperty.all(const EdgeInsets.all(0)), 52 | backgroundColor: MaterialStateProperty.all( 53 | Provider.of(context, listen: true).primaryColor), 54 | ), 55 | child: Icon( 56 | iconData, 57 | color: Provider.of(context, listen: true).bgColor, 58 | ), 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | // START: FlutterFire Configuration 4 | id 'com.google.gms.google-services' 5 | // END: FlutterFire Configuration 6 | id "kotlin-android" 7 | id "dev.flutter.flutter-gradle-plugin" 8 | } 9 | 10 | def localProperties = new Properties() 11 | def localPropertiesFile = rootProject.file('local.properties') 12 | if (localPropertiesFile.exists()) { 13 | localPropertiesFile.withReader('UTF-8') { reader -> 14 | localProperties.load(reader) 15 | } 16 | } 17 | 18 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 19 | if (flutterVersionCode == null) { 20 | flutterVersionCode = '1' 21 | } 22 | 23 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 24 | if (flutterVersionName == null) { 25 | flutterVersionName = '1.0' 26 | } 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | namespace "com.princeappstudio.tic_tac_toe" 36 | compileSdkVersion 33 37 | ndkVersion flutter.ndkVersion 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.princeappstudio.tic_tac_toe" 47 | // You can update the following values to match your application needs. 48 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 49 | minSdkVersion 28 50 | targetSdkVersion 33 51 | versionCode flutterVersionCode.toInteger() 52 | versionName flutterVersionName 53 | } 54 | 55 | signingConfigs { 56 | release { 57 | keyAlias keystoreProperties['keyAlias'] 58 | keyPassword keystoreProperties['keyPassword'] 59 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 60 | storePassword keystoreProperties['storePassword'] 61 | } 62 | } 63 | 64 | buildTypes { 65 | release { 66 | signingConfig signingConfigs.release 67 | } 68 | } 69 | } 70 | 71 | flutter { 72 | source '../..' 73 | } 74 | 75 | dependencies { 76 | implementation 'com.google.firebase:firebase-analytics:21.3.0' 77 | implementation "com.google.android.gms:play-services-games-v2:17.0.0" 78 | } 79 | -------------------------------------------------------------------------------- /lib/components/player_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tic_tac_toe/components/my_spacer.dart'; 4 | import 'package:tic_tac_toe/constants.dart'; 5 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 6 | 7 | class PlayerCard extends StatelessWidget { 8 | const PlayerCard({ 9 | super.key, 10 | required this.imageUrl, 11 | required this.name, 12 | this.showScore = false, 13 | this.isAsset = false, 14 | this.scoreValue = 0, 15 | }); 16 | 17 | final String imageUrl; 18 | final String name; 19 | final bool showScore; 20 | final bool isAsset; 21 | final int scoreValue; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Column( 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | children: [ 28 | Container( 29 | decoration: BoxDecoration( 30 | color: Provider.of(context, listen: true).bgColor, 31 | borderRadius: BorderRadius.circular(100), 32 | boxShadow: shadow, 33 | ), 34 | child: ClipRRect( 35 | borderRadius: BorderRadius.circular(100), 36 | child: isAsset 37 | ? Padding( 38 | padding: const EdgeInsets.all(25), 39 | child: Image.asset( 40 | imageUrl, 41 | width: 40, 42 | height: 40, 43 | fit: BoxFit.cover, 44 | ), 45 | ) 46 | : Image.network( 47 | imageUrl, 48 | width: 100, 49 | height: 100, 50 | fit: BoxFit.cover, 51 | ), 52 | ), 53 | ), 54 | const VerticalSpacer(16), 55 | Text( 56 | name, 57 | style: TextStyle( 58 | fontSize: defaultTextSize, 59 | color: Provider.of(context, listen: true) 60 | .secondaryColor, 61 | ), 62 | ), 63 | const VerticalSpacer(12), 64 | showScore 65 | ? Container( 66 | padding: 67 | const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 68 | decoration: BoxDecoration( 69 | color: Provider.of(context, listen: true) 70 | .secondaryColor, 71 | borderRadius: BorderRadius.circular(12), 72 | ), 73 | child: Text( 74 | "Won: $scoreValue", 75 | style: TextStyle( 76 | fontSize: 14, 77 | color: Provider.of(context, listen: true) 78 | .bgColor, 79 | ), 80 | ), 81 | ) 82 | : Container(), 83 | ], 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/provider/room_provider.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: use_build_context_synchronously 2 | 3 | import 'package:firebase_database/firebase_database.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:tic_tac_toe/constants.dart'; 6 | import 'package:tic_tac_toe/helper/random_gen.dart'; 7 | import 'package:tic_tac_toe/model/player.dart'; 8 | import 'package:tic_tac_toe/model/room.dart'; 9 | import 'package:tic_tac_toe/model/symbol.dart'; 10 | 11 | class RoomProvider with ChangeNotifier { 12 | bool _showLoading = false; 13 | bool showJoinLoading = false; 14 | 15 | bool get loading { 16 | return _showLoading; 17 | } 18 | 19 | set loading(bool v) { 20 | _showLoading = v; 21 | notifyListeners(); 22 | } 23 | 24 | Future createRoom(Player player, Widget widget) async { 25 | loading = true; 26 | 27 | int code = generateRandomRoomCode(); 28 | int round = 1; 29 | // int code = 123465; // for testing; 30 | // List board = List.generate(generateRandomBoardSize(), (index) => 0); 31 | List board = List.generate(9, (index) => 0); 32 | 33 | player.chose = PlaySymbol.x; 34 | List players = []; 35 | players.add(player); 36 | 37 | RoomData roomData = RoomData( 38 | code, 39 | player.playerId, 40 | PlaySymbol.x, 41 | players, 42 | board, 43 | round, 44 | DateTime.now(), 45 | ); 46 | 47 | await FirebaseDatabase.instance 48 | .ref("$roomPath$code") 49 | .set(roomData.toJson()); 50 | 51 | loading = false; 52 | 53 | return code; 54 | // SchedulerBinding.instance.addPostFrameCallback((_) { 55 | // }); 56 | } 57 | 58 | Future joinRoom(Player player, int roomCode, Widget widget) async { 59 | showJoinLoading = true; 60 | notifyListeners(); 61 | 62 | String path = "$roomPath$roomCode/players"; 63 | 64 | // DataSnapshot data = 65 | // await FirebaseDatabase.instance.ref(path).get(); 66 | 67 | player.chose = PlaySymbol.o; 68 | await FirebaseDatabase.instance.ref(path).update({"1": player.toJson()}); 69 | 70 | showJoinLoading = false; 71 | notifyListeners(); 72 | } 73 | 74 | void isStarted(bool v, int roomCode) async { 75 | await FirebaseDatabase.instance.ref("$roomPath$roomCode/").update( 76 | {"isStarted": true}, 77 | ); 78 | } 79 | 80 | Future leaveRoom(int roomCode, bool isRoomOwner) async { 81 | String path = "$roomPath$roomCode/players/"; 82 | if (isRoomOwner) { 83 | await FirebaseDatabase.instance.ref("$roomPath$roomCode").remove(); 84 | } else { 85 | path += "1"; 86 | await FirebaseDatabase.instance.ref(path).remove(); 87 | } 88 | } 89 | 90 | Future isRoomExist(int roomCode) async { 91 | showJoinLoading = true; 92 | notifyListeners(); 93 | 94 | String path = "$roomPath$roomCode"; 95 | DatabaseEvent databaseEvent = 96 | await FirebaseDatabase.instance.ref(path).once(); 97 | 98 | showJoinLoading = false; 99 | notifyListeners(); 100 | 101 | return databaseEvent.snapshot.value != null; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/components/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:tic_tac_toe/constants.dart'; 5 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 6 | import 'package:tic_tac_toe/helper/audio_controller.dart'; 7 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 8 | import 'package:vibration/vibration.dart'; 9 | 10 | class MyButton extends StatelessWidget { 11 | const MyButton({ 12 | super.key, 13 | required this.text, 14 | required this.onPressed, 15 | this.showLoading = false, 16 | this.invertColor = false, 17 | this.canAnimate = true, 18 | this.doStateChange = false, 19 | this.hasRestEffect = false, 20 | this.msDelay, 21 | }); 22 | 23 | final String text; 24 | final Function onPressed; 25 | final bool showLoading; 26 | final bool invertColor; 27 | final bool canAnimate; 28 | final int? msDelay; 29 | final bool? doStateChange; 30 | final bool? hasRestEffect; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | double width = kIsWeb ? 400 : MediaQuery.of(context).size.width; 35 | 36 | Widget buttonChild = ElevatedButton( 37 | onPressed: showLoading 38 | ? null 39 | : () async { 40 | if (!kIsWeb) { 41 | Vibration.vibrate(duration: 80, amplitude: 120); 42 | } 43 | AudioController audioController = AudioController(); 44 | audioController.buttonClick(context); 45 | await Future.delayed(Duration(milliseconds: msAnimationDelay)); 46 | onPressed(); 47 | }, 48 | style: ButtonStyle( 49 | minimumSize: MaterialStateProperty.all( 50 | Size(width / 1.6, 48), 51 | ), 52 | elevation: MaterialStateProperty.all(4), 53 | shape: MaterialStateProperty.all( 54 | RoundedRectangleBorder( 55 | borderRadius: BorderRadius.circular(12), 56 | ), 57 | ), 58 | backgroundColor: MaterialStateProperty.all(!invertColor 59 | ? Provider.of(context, listen: true).primaryColor 60 | : Provider.of(context, listen: true).bgColor), 61 | ), 62 | child: showLoading 63 | ? CircularProgressIndicator( 64 | color: Provider.of(context, listen: true).bgColor, 65 | strokeWidth: 2, 66 | ) 67 | : Text( 68 | text, 69 | style: TextStyle( 70 | fontSize: defaultTextSize, 71 | color: !invertColor 72 | ? Provider.of(context, listen: true).bgColor 73 | : Provider.of(context, listen: true) 74 | .primaryColor, 75 | letterSpacing: 1, 76 | ), 77 | ), 78 | ); 79 | 80 | return canAnimate 81 | ? AnimationOnWidget( 82 | msDelay: msDelay, 83 | doStateChange: doStateChange, 84 | hasRestEffect: hasRestEffect!, 85 | child: buttonChild, 86 | ) 87 | : buttonChild; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/provider/game_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_database/firebase_database.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:tic_tac_toe/constants.dart'; 4 | import 'package:tic_tac_toe/helper/check_win.dart'; 5 | import 'package:tic_tac_toe/helper/game.dart'; 6 | import 'package:tic_tac_toe/helper/random_gen.dart'; 7 | import 'package:tic_tac_toe/model/room.dart'; 8 | import 'package:tic_tac_toe/model/symbol.dart'; 9 | 10 | class GameProvider with ChangeNotifier { 11 | bool showLoading = false; 12 | 13 | late Result _result; 14 | 15 | List corners = []; 16 | Map borders = {}; 17 | 18 | String button1Text = "Yes"; 19 | 20 | Result get result { 21 | return _result; 22 | } 23 | 24 | set updateResult(Result r) { 25 | _result = r; 26 | notifyListeners(); 27 | } 28 | 29 | void resetBoard(String refPath, RoomData roomData, int player, 30 | bool isRoomOwner, BuildContext context) async { 31 | await Future.delayed( 32 | const Duration(seconds: 5), 33 | () async { 34 | late List board; 35 | if (isRoomOwner) { 36 | board = List.generate(generateRandomBoardSize(), (index) => 0); 37 | 38 | FirebaseDatabase.instance.ref(refPath).update({ 39 | "board": board, 40 | "round": roomData.round + 1, 41 | }); 42 | 43 | FirebaseDatabase.instance 44 | .ref( 45 | "$roomPath${roomData.code}/players/${player == PlaySymbol.xInt ? 0 : 1}") 46 | .update({ 47 | "winCount": 48 | roomData.players[player == PlaySymbol.xInt ? 0 : 1].winCount + 49 | 1, 50 | }); 51 | designBoard(board); 52 | 53 | Navigator.pop(context); 54 | } else { 55 | // TODO: change designBoard to const to avoid this call to fb. 56 | DatabaseEvent databaseEvent = 57 | await FirebaseDatabase.instance.ref("$refPath/board").once(); 58 | board = databaseEvent.snapshot.value as List; 59 | print("Reset board: $board"); 60 | designBoard( 61 | board); // TODO: remove this to get rid of state error during build 62 | 63 | // ignore: use_build_context_synchronously 64 | Navigator.pop(context); 65 | } 66 | }, 67 | ); 68 | } 69 | 70 | void designBoard(List board) { 71 | // showLoading = true; 72 | // notifyListeners(); 73 | 74 | List borderRadius = [ 75 | const BorderRadius.only( 76 | topLeft: Radius.circular(16), 77 | ), 78 | const BorderRadius.only( 79 | topRight: Radius.circular(16), 80 | ), 81 | const BorderRadius.only( 82 | bottomLeft: Radius.circular(16), 83 | ), 84 | const BorderRadius.only( 85 | bottomRight: Radius.circular(16), 86 | ), 87 | ]; 88 | corners = findCornerPositions(board.length); 89 | for (int i = 0; i < corners.length; i++) { 90 | borders.addAll({corners[i]: borderRadius[i]}); 91 | } 92 | 93 | // showLoading = false; 94 | notifyListeners(); 95 | } 96 | 97 | String get popUpButtonText { 98 | return button1Text; 99 | } 100 | 101 | set popUpButtonTextChange(String val) { 102 | button1Text = val; 103 | notifyListeners(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/components/pop_up.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tic_tac_toe/components/button.dart'; 4 | import 'package:tic_tac_toe/components/my_spacer.dart'; 5 | import 'package:tic_tac_toe/constants.dart'; 6 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 7 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 8 | 9 | class MyPopUp extends StatelessWidget { 10 | const MyPopUp({ 11 | super.key, 12 | required this.title, 13 | required this.description, 14 | required this.button1Text, 15 | required this.button2Text, 16 | required this.button1OnPressed, 17 | required this.button2OnPressed, 18 | }); 19 | 20 | final String title, description, button1Text, button2Text; 21 | final VoidCallback button1OnPressed, button2OnPressed; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return WidgetAnimator( 26 | incomingEffect: WidgetTransitionEffects.incomingScaleUp( 27 | delay: const Duration(microseconds: 500), 28 | curve: Curves.easeInOut, 29 | ), 30 | child: Consumer( 31 | builder: (context, themeProvider, _) { 32 | return AlertDialog( 33 | backgroundColor: themeProvider.bgColor, 34 | shape: RoundedRectangleBorder( 35 | borderRadius: BorderRadius.circular(12), 36 | ), 37 | title: Text( 38 | title, 39 | style: TextStyle( 40 | fontSize: defaultTextSize + 6, 41 | color: themeProvider.primaryColor, 42 | ), 43 | ), 44 | content: Column( 45 | mainAxisSize: MainAxisSize.min, 46 | children: [ 47 | Text( 48 | description, 49 | style: TextStyle( 50 | fontSize: defaultTextSize + 3, 51 | color: themeProvider.secondaryColor, 52 | ), 53 | ), 54 | const VerticalSpacer(16), 55 | MyButton( 56 | canAnimate: false, 57 | text: button1Text, 58 | onPressed: button1OnPressed, 59 | ), 60 | const VerticalSpacer(8), 61 | MyButton( 62 | canAnimate: false, 63 | text: button2Text, 64 | onPressed: button2OnPressed, 65 | invertColor: true, 66 | ), 67 | ], 68 | ), 69 | ); 70 | }, 71 | ), 72 | ); 73 | } 74 | } 75 | 76 | class PopUp { 77 | final String title; 78 | final String description; 79 | final String button1Text; 80 | final String button2Text; 81 | final bool barrierDismissible; 82 | final VoidCallback button1OnPressed, button2OnPressed; 83 | 84 | PopUp.show( 85 | BuildContext context, { 86 | required this.title, 87 | required this.description, 88 | required this.button1Text, 89 | required this.button2Text, 90 | required this.barrierDismissible, 91 | required this.button1OnPressed, 92 | required this.button2OnPressed, 93 | }) { 94 | showDialog( 95 | context: context, 96 | barrierDismissible: barrierDismissible, 97 | builder: (context) => MyPopUp( 98 | title: title, 99 | description: description, 100 | button1Text: button1Text, 101 | button2Text: button2Text, 102 | button1OnPressed: button1OnPressed, 103 | button2OnPressed: button2OnPressed, 104 | ), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tic Tac Toe - Online 2 | 3 | A minimal twist on the classic game featuring both online and offline modes. Built with the power of **Flutter** and **Firebase**, this app runs seamlessly on **iOS**, **Android**, and **Web** platforms. 4 | 5 | ## Features 6 | 7 | ### 🔥 Online Multiplayer 8 | - **2-Player Mode**: Play in real-time with a friend by creating a room. 9 | - **Login Options**: Use **Google** or **Apple** authentication for a seamless login experience. 10 | - **Firebase Real-Time Database**: Ensures smooth and instantaneous game updates. 11 | 12 | ### 🎮 Offline Modes 13 | - **2-Player Local Mode**: Play offline with a friend on the same device. 14 | - **AI Mode**: Challenge yourself against an AI opponent. 15 | 16 | ### 🕹️ Game Boards 17 | - Multiple board sizes available: 18 | - **3x3** (Classic) 19 | - **4x4** (Intermediate) 20 | - **5x5** (Advanced) 21 | 22 | ### 🎨 Customization 23 | - **UI Themes**: Choose between two themes to personalize your experience. 24 | - **Minimal Design**: The app is designed with simplicity and ease of use in mind. 25 | 26 | ## Platforms 27 | - [**Android**](https://play.google.com/store/apps/details?id=com.princeappstudio.tic_tac_toe) 28 | - [**iOS**](https://apps.apple.com/us/app/tic-tac-toe-online-2player/id6740833110?platform=iphone) 29 | - [**Web**](https://tictactoe.princeappstudio.in) 30 | - [**F-Droid**](https://f-droid.org/en/packages/com.princeappstudio.tic_tac_toe/) 31 | 32 | ## Technologies Used 33 | - **Flutter**: For building the cross-platform mobile and web application. 34 | - **Firebase**: For real-time database and authentication. 35 | - **Firebase Authentication**: Supports Google and Apple login. 36 | - **Firebase Realtime Database**: Handles online gameplay. 37 | 38 | ## How to Play 39 | ### Online Mode 40 | 1. Log in using Google or Apple authentication. 41 | 2. Choose online mode and either join a room using the 6 digit code or create a room. 42 | 3. Start playing in real time and after each rounds the board size gets updated randomly. 43 | 44 | ### Offline Mode 45 | 1. Choose either **2-Player Local** or **AI Mode**. 46 | 2. Play with a friend on the same device or against AI. 47 | 48 | ## Installation 49 | 1. Clone this repository: 50 | ```bash 51 | git clone https://github.com/princesanjivy/tic-tac-toe.git 52 | ``` 53 | 2. Navigate to the project directory: 54 | ```bash 55 | cd tic-tac-toe 56 | ``` 57 | 3. Install dependencies: 58 | ```bash 59 | flutter pub get 60 | ``` 61 | 4. Run the app: 62 | ```bash 63 | flutter run 64 | ``` 65 | 5. You need to setup Firebase and need to generater the `firebase-options.dart` 66 | 6. For building the **F-Droid** version use `fdroid-submission` branch. 67 | 68 | ## Screenshots 69 | ### Home Screen 70 | Home Screen 71 | 72 | ### Game Board 73 | Game Board 74 | 75 | ### Settings 76 | Settings 77 | 78 | 79 | ## Roadmap 80 | - [ ] Add support for more board sizes (e.g., 6x6, 7x7). 81 | - [ ] Introduce leaderboards for online players. 82 | - [ ] Add more theme options. 83 | 84 | ## Contributing 85 | Contributions are welcome! Please follow these steps: 86 | 1. Fork the repository. 87 | 2. Create a new branch: 88 | ```bash 89 | git checkout -b feature-name 90 | ``` 91 | 3. Make your changes and commit: 92 | ```bash 93 | git commit -m "Add feature name" 94 | ``` 95 | 4. Push to your branch: 96 | ```bash 97 | git push origin feature-name 98 | ``` 99 | 5. Submit a pull request. 100 | 101 | Enjoy playing Tic Tac Toe and challenge your friends to see who's the ultimate champion! 102 | 103 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/provider/login_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | import 'package:sign_in_with_apple/sign_in_with_apple.dart'; 7 | import 'package:tic_tac_toe/constants.dart'; 8 | import 'package:tic_tac_toe/model/player.dart'; 9 | 10 | class LoginProvider with ChangeNotifier { 11 | bool _showLoading = false; 12 | 13 | bool get loading { 14 | return _showLoading; 15 | } 16 | 17 | set loading(bool v) { 18 | _showLoading = v; 19 | notifyListeners(); 20 | } 21 | 22 | Future loginWithApple() async { 23 | loading = true; 24 | 25 | try { 26 | final appleCredential = await SignInWithApple.getAppleIDCredential( 27 | scopes: [ 28 | AppleIDAuthorizationScopes.email, 29 | AppleIDAuthorizationScopes.fullName, 30 | ], 31 | ); 32 | final oauthCredential = OAuthProvider("apple.com").credential( 33 | idToken: appleCredential.identityToken, 34 | accessToken: appleCredential.authorizationCode, 35 | ); 36 | await FirebaseAuth.instance.signInWithCredential(oauthCredential); 37 | Future.delayed(const Duration(milliseconds: 100)); 38 | // add user to firestore database 39 | Player player = getUserData; 40 | FirebaseFirestore.instance 41 | .collection("users") 42 | .doc(player.playerId) 43 | .set(player.toDbJson()); 44 | } finally { 45 | loading = false; 46 | } 47 | } 48 | 49 | Future loginWithGoogle() async { 50 | late UserCredential userCredential; 51 | loading = true; 52 | 53 | try { 54 | final GoogleSignInAccount? googleUser = await GoogleSignIn( 55 | clientId: Platform.isIOS 56 | ? "1052229586554-g7tm8tqpjq242hmphomb1hn9bejqa26r.apps.googleusercontent.com" 57 | : "1052229586554-sfgsc4r16hsce5l5f4d4rrrc2cu24lqg.apps.googleusercontent.com") 58 | .signIn(); 59 | final GoogleSignInAuthentication? googleAuth = 60 | await googleUser?.authentication; 61 | 62 | // Create a new credential 63 | final credential = GoogleAuthProvider.credential( 64 | accessToken: googleAuth?.accessToken, 65 | idToken: googleAuth?.idToken, 66 | ); 67 | 68 | userCredential = 69 | await FirebaseAuth.instance.signInWithCredential(credential); 70 | 71 | Future.delayed(const Duration(milliseconds: 100)); 72 | // add user to firestore database 73 | Player player = getUserData; 74 | FirebaseFirestore.instance 75 | .collection("users") 76 | .doc(player.playerId) 77 | .set(player.toDbJson()); 78 | } finally { 79 | loading = false; 80 | } 81 | 82 | return userCredential; 83 | } 84 | 85 | logout() async { 86 | loading = true; 87 | await FirebaseAuth.instance.signOut(); 88 | loading = false; 89 | } 90 | 91 | bool get isLoggedIn { 92 | return FirebaseAuth.instance.currentUser != null; 93 | } 94 | 95 | Player get getUserData { 96 | Player player; 97 | final user = FirebaseAuth.instance.currentUser; 98 | if (user != null) { 99 | final String emailName = user.email!.split("@")[0]; 100 | player = Player( 101 | user.displayName ?? emailName, 102 | user.uid, 103 | user.photoURL ?? 104 | "https://placehold.co/400x400/orange/white/png?text=${emailName[0]}", 105 | ); 106 | } else { 107 | player = Player( 108 | "John Doe", 109 | "123456789", 110 | imageUrl, 111 | ); 112 | } 113 | 114 | return player; 115 | } 116 | 117 | Future getUserById(String id) async { 118 | QuerySnapshot data = await FirebaseFirestore.instance 119 | .collection("users") 120 | .where("playerId", isEqualTo: id) 121 | .get(); 122 | Player player = Player.fromJson(data.docs.first.data()); 123 | 124 | return player; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /privacy_policy.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | princeappstudio built the Tic Tac Toe Online app as a Free app. This SERVICE is provided by 4 | princeappstudio at no cost and is intended for use as is. 5 | 6 | This page is used to inform visitors regarding our policies with the collection, use, and disclosure 7 | of Personal Information if anyone decided to use our Service. 8 | 9 | If you choose to use our Service, then you agree to the collection and use of information in 10 | relation to this policy. The Personal Information that we collect is used for providing and 11 | improving the Service. We will not use or share your information with anyone except as described in 12 | this Privacy Policy. 13 | 14 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which 15 | are accessible at Tic Tac Toe Online unless otherwise defined in this Privacy Policy. 16 | 17 | **Information Collection and Use** 18 | 19 | For a better experience, while using our Service, we may require you to provide us with certain 20 | personally identifiable information. The information that we request will be retained by us and used 21 | as described in this privacy policy. 22 | 23 | The app does use third-party services that may collect information used to identify you. 24 | 25 | Link to the privacy policy of third-party service providers used by the app 26 | 27 | * [Google Play Services](https://www.google.com/policies/privacy/) 28 | * [AdMob](https://support.google.com/admob/answer/6128543?hl=en) 29 | * [Firebase Crashlytics](https://firebase.google.com/support/privacy/) 30 | 31 | **Log Data** 32 | 33 | We want to inform you that whenever you use our Service, in a case of an error in the app we collect 34 | data and information (through third-party products) on your phone called Log Data. This Log Data may 35 | include information such as your device Internet Protocol (“IP”) address, device name, operating 36 | system version, the configuration of the app when utilizing our Service, the time and date of your 37 | use of the Service, and other statistics. 38 | 39 | **Cookies** 40 | 41 | Cookies are files with a small amount of data that are commonly used as anonymous unique 42 | identifiers. These are sent to your browser from the websites that you visit and are stored on your 43 | device's internal memory. 44 | 45 | This Service does not use these “cookies” explicitly. However, the app may use third-party code and 46 | libraries that use “cookies” to collect information and improve their services. You have the option 47 | to either accept or refuse these cookies and know when a cookie is being sent to your device. If you 48 | choose to refuse our cookies, you may not be able to use some portions of this Service. 49 | 50 | **Service Providers** 51 | 52 | We may employ third-party companies and individuals due to the following reasons: 53 | 54 | * To facilitate our Service; 55 | * To provide the Service on our behalf; 56 | * To perform Service-related services; or 57 | * To assist us in analyzing how our Service is used. 58 | 59 | We want to inform users of this Service that these third parties have access to their Personal 60 | Information. The reason is to perform the tasks assigned to them on our behalf. However, they are 61 | obligated not to disclose or use the information for any other purpose. 62 | 63 | **Security** 64 | 65 | We value your trust in providing us your Personal Information, thus we are striving to use 66 | commercially acceptable means of protecting it. But remember that no method of transmission over the 67 | internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its 68 | absolute security. 69 | 70 | **Links to Other Sites** 71 | 72 | This Service may contain links to other sites. If you click on a third-party link, you will be 73 | directed to that site. Note that these external sites are not operated by us. Therefore, we strongly 74 | advise you to review the Privacy Policy of these websites. We have no control over and assume no 75 | responsibility for the content, privacy policies, or practices of any third-party sites or services. 76 | 77 | **Children’s Privacy** 78 | 79 | These Services do not address anyone under the age of 13. We do not knowingly collect personally 80 | identifiable information from children under 13 years of age. In the case we discover that a child 81 | under 13 has provided us with personal information, we immediately delete this from our servers. If 82 | you are a parent or guardian and you are aware that your child has provided us with personal 83 | information, please contact us so that we will be able to do the necessary actions. 84 | 85 | **Changes to This Privacy Policy** 86 | 87 | We may update our Privacy Policy from time to time. Thus, you are advised to review this page 88 | periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on 89 | this page. 90 | 91 | This policy is effective as of 2023-09-10 92 | 93 | **Contact Us** 94 | 95 | If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us at 96 | sanjivy@princeappstudio.in 97 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:google_fonts/google_fonts.dart'; 7 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:tic_tac_toe/firebase_options.dart'; 10 | import 'package:tic_tac_toe/helper/audio_controller.dart'; 11 | import 'package:tic_tac_toe/helper/show_interstitial_ad.dart'; 12 | import 'package:tic_tac_toe/provider/audio_provider.dart'; 13 | import 'package:tic_tac_toe/provider/game_provider.dart'; 14 | import 'package:tic_tac_toe/provider/login_provider.dart'; 15 | import 'package:tic_tac_toe/provider/room_provider.dart'; 16 | import 'package:tic_tac_toe/provider/single_mode_provider.dart'; 17 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 18 | import 'package:tic_tac_toe/screen/home.dart'; 19 | 20 | void main() async { 21 | WidgetsFlutterBinding.ensureInitialized(); 22 | await Firebase.initializeApp( 23 | options: DefaultFirebaseOptions.currentPlatform, 24 | ); 25 | if (!kIsWeb) { 26 | MobileAds.instance.initialize(); 27 | MobileAds.instance.updateRequestConfiguration( 28 | RequestConfiguration( 29 | testDeviceIds: ["44C21CA2B517E5E19D6F9D510C330CA2"], 30 | ), 31 | ); 32 | } 33 | SystemChrome.setEnabledSystemUIMode( 34 | SystemUiMode.manual, 35 | overlays: [], 36 | ); 37 | SystemChrome.setSystemUIOverlayStyle( 38 | const SystemUiOverlayStyle(statusBarColor: Colors.white), 39 | ); 40 | 41 | // create empty object to call init() 42 | AudioController audioController = AudioController(); 43 | FullScreenAd object = FullScreenAd(); 44 | 45 | runApp( 46 | MultiProvider( 47 | providers: [ 48 | ChangeNotifierProvider( 49 | create: (context) => LoginProvider(), 50 | ), 51 | ChangeNotifierProvider( 52 | create: (context) => RoomProvider(), 53 | ), 54 | ChangeNotifierProvider( 55 | create: (context) => GameProvider(), 56 | ), 57 | ChangeNotifierProvider( 58 | create: (context) => ThemeProvider.init(), 59 | ), 60 | ChangeNotifierProvider( 61 | create: (context) => AudioProvider.init(), 62 | ), 63 | ChangeNotifierProvider( 64 | create: (context) => SingleModeProvider(), 65 | ), 66 | ], 67 | child: const MyApp(), 68 | ), 69 | ); 70 | } 71 | 72 | class MyApp extends StatefulWidget { 73 | const MyApp({super.key}); 74 | 75 | @override 76 | State createState() => _MyAppState(); 77 | } 78 | 79 | class _MyAppState extends State { 80 | @override 81 | Widget build(BuildContext context) { 82 | return MaterialApp( 83 | title: "Tic Tac Toe", 84 | theme: ThemeData( 85 | useMaterial3: false, 86 | textTheme: GoogleFonts.judsonTextTheme().copyWith( 87 | // bodyMedium: GoogleFonts.judson( 88 | // color: secondaryColor, 89 | // fontSize: defaultTextSize, 90 | // ), 91 | ), 92 | colorScheme: ColorScheme.fromSeed( 93 | seedColor: 94 | Provider.of(context, listen: true).primaryColor, 95 | ), 96 | ), 97 | scrollBehavior: NoThumbScrollBehavior().copyWith(scrollbars: false), 98 | home: const ScreenController(), 99 | ); 100 | } 101 | } 102 | 103 | class ScreenController extends StatelessWidget { 104 | const ScreenController({super.key}); 105 | 106 | @override 107 | Widget build(BuildContext context) { 108 | return Consumer( 109 | builder: (context, theme, _) { 110 | return theme.showLoading 111 | ? const Scaffold( 112 | body: Center( 113 | child: Column( 114 | mainAxisAlignment: MainAxisAlignment.center, 115 | crossAxisAlignment: CrossAxisAlignment.center, 116 | children: [ 117 | // Center( 118 | // child: CircularProgressIndicator(), 119 | // ), 120 | Text( 121 | "princeappstudio\npresents", 122 | style: TextStyle( 123 | color: Colors.cyan, 124 | fontSize: 22, 125 | ), 126 | textAlign: TextAlign.center, 127 | ), 128 | ], 129 | ), 130 | ), 131 | ) 132 | : const HomeScreen(); 133 | }, 134 | ); 135 | } 136 | } 137 | 138 | class NoThumbScrollBehavior extends ScrollBehavior { 139 | @override 140 | Set get dragDevices => { 141 | PointerDeviceKind.touch, 142 | PointerDeviceKind.mouse, 143 | PointerDeviceKind.stylus, 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /lib/provider/single_mode_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:tic_tac_toe/components/pop_up.dart'; 4 | import 'package:tic_tac_toe/constants.dart'; 5 | import 'package:tic_tac_toe/helper/board_desgin.dart' as helper; 6 | import 'package:tic_tac_toe/helper/check_win.dart'; 7 | import 'package:tic_tac_toe/helper/game.dart'; 8 | import 'package:tic_tac_toe/helper/navigation.dart'; 9 | import 'package:tic_tac_toe/helper/random_gen.dart'; 10 | import 'package:tic_tac_toe/helper/show_interstitial_ad.dart'; 11 | import 'package:tic_tac_toe/model/symbol.dart'; 12 | import 'package:tic_tac_toe/screen/home.dart'; 13 | import 'package:url_launcher/url_launcher.dart'; 14 | 15 | class SingleModeProvider with ChangeNotifier { 16 | List board = List.generate(9, (index) => 0); // Always start with 3*3 board 17 | 18 | Map scores = {PlaySymbol.xInt: 0, PlaySymbol.oInt: 0}; 19 | int round = 1; 20 | 21 | String turn = PlaySymbol.x; 22 | String playerChose = ""; 23 | String aiChose = ""; 24 | 25 | Result result = Result(false, []); 26 | 27 | late BuildContext _context; 28 | late Widget _widget; 29 | late bool _twoPlayerMode; 30 | 31 | bool doStateChange = false; 32 | 33 | List corners = []; 34 | Map borders = {}; 35 | 36 | late Navigation navigation; 37 | 38 | void init(BuildContext c, Widget w, bool b) { 39 | _context = c; 40 | _widget = w; 41 | _twoPlayerMode = b; 42 | 43 | playerChose = PlaySymbol.x; 44 | if (playerChose == PlaySymbol.x) { 45 | aiChose = PlaySymbol.o; 46 | } else { 47 | aiChose = PlaySymbol.x; 48 | aiMove(aiChose); 49 | } 50 | design(); 51 | navigation = Navigation(Navigator.of(c)); 52 | } 53 | 54 | void validate() async { 55 | Result resultX = checkWin(board, PlaySymbol.xInt, getBoardSize(board)); 56 | Result resultO = checkWin(board, PlaySymbol.oInt, getBoardSize(board)); 57 | 58 | if (resultX.hasWon) { 59 | print("Player 1 (X) wins!"); 60 | result = resultX; 61 | scores[PlaySymbol.xInt] = scores[PlaySymbol.xInt]! + 1; 62 | displayTwoPlayerModeResult(PlaySymbol.x); 63 | } else if (resultO.hasWon) { 64 | print("Player 2 (O) wins!"); 65 | result = resultO; 66 | scores[PlaySymbol.oInt] = scores[PlaySymbol.oInt]! + 1; 67 | displayTwoPlayerModeResult(PlaySymbol.o); 68 | } else { 69 | if (!board.contains(0)) { 70 | displayTwoPlayerModeResult(PlaySymbol.draw); 71 | } 72 | } 73 | 74 | doStateChange = true; 75 | notifyListeners(); 76 | } 77 | 78 | void displayTwoPlayerModeResult(String winner) async { 79 | round += 1; 80 | 81 | PopUp.show( 82 | _context, 83 | title: winner == PlaySymbol.draw 84 | ? "Game draw" 85 | : winner == PlaySymbol.x 86 | ? "X won" 87 | : "O won", 88 | description: "New game restarting in 3 seconds...", 89 | button1Text: "Quit", 90 | button2Text: "Rate game", 91 | barrierDismissible: false, 92 | button1OnPressed: () { 93 | FullScreenAd.object.show(); 94 | 95 | navigation.goBack(_context); 96 | navigation.changeScreenReplacement( 97 | const HomeScreen(), 98 | _widget, 99 | ); 100 | }, 101 | button2OnPressed: () { 102 | launchUrl( 103 | Uri.parse(Platform.isIOS ? gameLinkIos: gameLinkAndroid), 104 | ); 105 | }, 106 | ); 107 | await Future.delayed(const Duration(seconds: 3), () { 108 | board = List.generate(generateRandomBoardSize(), (index) => 0); 109 | 110 | result = Result(false, []); 111 | turn = (winner == PlaySymbol.x ? PlaySymbol.o : PlaySymbol.x); 112 | 113 | navigation.goBack(_context); 114 | doStateChange = true; 115 | design(); 116 | 117 | notifyListeners(); 118 | }); 119 | } 120 | 121 | void aiMove(String chose) async { 122 | doStateChange = true; 123 | notifyListeners(); 124 | 125 | await Future.delayed(const Duration(milliseconds: 600), () { 126 | int index = 127 | findBestMove(board, PlaySymbol.inNum(chose), getBoardSize(board)); 128 | bool skipCheck = false; 129 | { 130 | if (index == -1) { 131 | printOverMsg(chose); 132 | skipCheck = true; 133 | } else { 134 | board[index] = PlaySymbol.inNum(chose); 135 | turn = (chose == PlaySymbol.x ? PlaySymbol.o : PlaySymbol.x); 136 | 137 | doStateChange = true; 138 | notifyListeners(); 139 | } 140 | } 141 | 142 | { 143 | if (!skipCheck && isGameOver(board)) { 144 | printOverMsg(chose); 145 | } 146 | } 147 | }); 148 | } 149 | 150 | void printOverMsg(String chose) async { 151 | Result resultX = checkWin(board, PlaySymbol.xInt, getBoardSize(board)); 152 | Result resultO = checkWin(board, PlaySymbol.oInt, getBoardSize(board)); 153 | 154 | if (resultX.hasWon) { 155 | print("Player 1 (X) wins!"); 156 | result = resultX; 157 | scores[PlaySymbol.xInt] = scores[PlaySymbol.xInt]! + 1; 158 | } else if (resultO.hasWon) { 159 | print("Player 2 (O) wins!"); 160 | result = resultO; 161 | scores[PlaySymbol.oInt] = scores[PlaySymbol.oInt]! + 1; 162 | } else { 163 | print("It's a draw!"); 164 | } 165 | 166 | round += 1; 167 | 168 | doStateChange = true; 169 | notifyListeners(); 170 | 171 | PopUp.show( 172 | _context, 173 | title: "Game over", 174 | description: "New game restarting in 5 seconds...", 175 | button1Text: "Quit", 176 | button2Text: "Rate game", 177 | barrierDismissible: false, 178 | button1OnPressed: () { 179 | FullScreenAd.object.show(); 180 | 181 | navigation.goBack(_context); 182 | navigation.changeScreenReplacement( 183 | const HomeScreen(), 184 | _widget, 185 | ); 186 | }, 187 | button2OnPressed: () { 188 | launchUrl( 189 | Uri.parse(Platform.isIOS ? gameLinkIos: gameLinkAndroid), 190 | ); 191 | }, 192 | ); 193 | await Future.delayed(const Duration(seconds: 5), () { 194 | board = List.generate(generateRandomBoardSize(), (index) => 0); 195 | 196 | result = Result(false, []); 197 | turn = (chose == PlaySymbol.x ? PlaySymbol.o : PlaySymbol.x); 198 | 199 | navigation.goBack(_context); 200 | doStateChange = true; 201 | design(); 202 | 203 | notifyListeners(); 204 | }); 205 | } 206 | 207 | void design() { 208 | helper.BoardDesign bd = helper.boardDesign(board); 209 | corners = bd.corners; 210 | borders = bd.borders; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/helper/game.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:tic_tac_toe/helper/check_win.dart'; 4 | import 'package:tic_tac_toe/model/symbol.dart'; 5 | 6 | int getBoardSize(List board) { 7 | return sqrt(board.length).toInt(); 8 | } 9 | 10 | List findCornerPositions(int length) { 11 | List array = List.generate(length, (index) => index); 12 | int size = getBoardSize(array); 13 | 14 | int topLeft = array[0]; 15 | int topRight = array[size - 1]; 16 | int bottomLeft = array[size * (size - 1)]; 17 | int bottomRight = array[size * size - 1]; 18 | 19 | List corners = [topLeft, topRight, bottomLeft, bottomRight]; 20 | return corners; 21 | } 22 | 23 | int findBestMove(List board, int turn, int boardSize) { 24 | int aiPlayer = turn; 25 | int opponent = (turn == 1) ? 2 : 1; 26 | 27 | if (isGameOver(board)) { 28 | return -1; // Return an invalid index indicating the end of the game 29 | } 30 | 31 | int bestScore = -9999; 32 | int bestMove = -1; 33 | 34 | for (int i = 0; i < board.length; i++) { 35 | if (board[i] == 0) { 36 | board[i] = aiPlayer; 37 | int score = 38 | minimax(board, 0, false, aiPlayer, opponent, -9999, 9999, boardSize); 39 | board[i] = 0; 40 | 41 | if (score > bestScore) { 42 | bestScore = score; 43 | bestMove = i; 44 | } 45 | } 46 | } 47 | 48 | return bestMove; 49 | } 50 | 51 | int minimax(List board, int depth, bool isMaximizingPlayer, int aiPlayer, 52 | int opponent, int alpha, int beta, int boardSize) { 53 | if (isGameOver(board) || depth == 6) { 54 | return evaluate(board, aiPlayer, opponent, boardSize); 55 | } 56 | 57 | if (isMaximizingPlayer) { 58 | int maxScore = -9999; 59 | 60 | for (int i = 0; i < board.length; i++) { 61 | if (board[i] == 0) { 62 | board[i] = aiPlayer; 63 | int score = minimax(board, depth + 1, false, aiPlayer, opponent, alpha, 64 | beta, boardSize); 65 | board[i] = 0; 66 | maxScore = max(maxScore, score); 67 | alpha = max(alpha, score); 68 | if (beta <= alpha) { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return maxScore; 75 | } else { 76 | int minScore = 9999; 77 | 78 | for (int i = 0; i < board.length; i++) { 79 | if (board[i] == 0) { 80 | board[i] = opponent; 81 | int score = minimax( 82 | board, depth + 1, true, aiPlayer, opponent, alpha, beta, boardSize); 83 | board[i] = 0; 84 | minScore = min(minScore, score); 85 | beta = min(beta, score); 86 | if (beta <= alpha) { 87 | break; 88 | } 89 | } 90 | } 91 | 92 | return minScore; 93 | } 94 | } 95 | 96 | int evaluate(List board, int aiPlayer, int opponent, int boardSize) { 97 | List> winningConditions = []; 98 | 99 | // Check rows 100 | for (int i = 0; i < boardSize; i++) { 101 | List condition = []; 102 | for (int j = 0; j < boardSize; j++) { 103 | condition.add(i * boardSize + j); 104 | } 105 | winningConditions.add(condition); 106 | } 107 | 108 | // Check columns 109 | for (int i = 0; i < boardSize; i++) { 110 | List condition = []; 111 | for (int j = 0; j < boardSize; j++) { 112 | condition.add(j * boardSize + i); 113 | } 114 | winningConditions.add(condition); 115 | } 116 | 117 | // Check diagonals 118 | List diagonal1 = []; 119 | List diagonal2 = []; 120 | for (int i = 0; i < boardSize; i++) { 121 | diagonal1.add(i * boardSize + i); 122 | diagonal2.add(i * boardSize + (boardSize - 1 - i)); 123 | } 124 | winningConditions.add(diagonal1); 125 | winningConditions.add(diagonal2); 126 | 127 | for (var condition in winningConditions) { 128 | int aiCount = 0; 129 | int opponentCount = 0; 130 | 131 | for (var cell in condition) { 132 | if (board[cell] == aiPlayer) { 133 | aiCount++; 134 | } else if (board[cell] == opponent) { 135 | opponentCount++; 136 | } 137 | } 138 | 139 | if (aiCount == boardSize) { 140 | return 100; // AI player wins 141 | } else if (opponentCount == boardSize) { 142 | return -100; // Opponent wins 143 | } 144 | } 145 | 146 | return 0; // Draw or no immediate win/loss 147 | } 148 | 149 | // int evaluate(List board, int aiPlayer, int opponent) { 150 | // // List> winningConditions = [ 151 | // // [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows 152 | // // [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns 153 | // // [0, 4, 8], [2, 4, 6] // Diagonals 154 | // // ]; 155 | // 156 | // List> winningConditions = [ 157 | // [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15], // Rows 158 | // [0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15], // Columns 159 | // [0, 5, 10, 15], [3, 6, 9, 12] // Diagonals 160 | // ]; 161 | // 162 | // for (var condition in winningConditions) { 163 | // int cell1 = board[condition[0]]; 164 | // int cell2 = board[condition[1]]; 165 | // int cell3 = board[condition[2]]; 166 | // 167 | // if (cell1 == cell2 && cell2 == cell3) { 168 | // if (cell1 == aiPlayer) { 169 | // return 10; 170 | // } else if (cell1 == opponent) { 171 | // return -10; 172 | // } 173 | // } 174 | // } 175 | // 176 | // return 0; 177 | // } 178 | 179 | bool isGameOver(List board) { 180 | return (checkWin(board, PlaySymbol.xInt, getBoardSize(board)).hasWon || 181 | checkWin(board, PlaySymbol.oInt, getBoardSize(board)).hasWon || 182 | !board.contains(0)); 183 | } 184 | 185 | // bool checkWin(List board, int player) { 186 | // List> winningConditions = [ 187 | // [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows 188 | // [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns 189 | // [0, 4, 8], [2, 4, 6] // Diagonals 190 | // ]; 191 | // 192 | // for (var condition in winningConditions) { 193 | // if (board[condition[0]] == player && 194 | // board[condition[1]] == player && 195 | // board[condition[2]] == player) { 196 | // return true; 197 | // } 198 | // } 199 | // 200 | // return false; 201 | // } 202 | 203 | // credits: chat.openai.com 204 | // the depth of the minimax algorithm has been increased to 6, allowing the 205 | // AI to look ahead and make better-informed decisions. The evaluate function 206 | // now assigns a score of 10 for a winning move by the AI player and a score 207 | // of -10 for a winning move by the opponent. The findBestMove function then selects 208 | // the move that leads to the highest score for the AI player. 209 | // These adjustments should make the AI more challenging to play against. 210 | // Feel free to modify the depth or further enhance the heuristic 211 | // evaluation function based on your preferences and requirements. 212 | // 213 | -------------------------------------------------------------------------------- /lib/screen/home.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:audioplayers/audioplayers.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:google_fonts/google_fonts.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:tic_tac_toe/components/button.dart'; 9 | import 'package:tic_tac_toe/components/icon_button.dart'; 10 | import 'package:tic_tac_toe/components/my_spacer.dart'; 11 | import 'package:tic_tac_toe/components/pop_up.dart'; 12 | import 'package:tic_tac_toe/constants.dart'; 13 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 14 | import 'package:tic_tac_toe/helper/navigation.dart'; 15 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 16 | import 'package:tic_tac_toe/provider/login_provider.dart'; 17 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 18 | import 'package:tic_tac_toe/screen/room.dart'; 19 | import 'package:tic_tac_toe/screen/settings.dart'; 20 | import 'package:tic_tac_toe/screen/single_mode.dart'; 21 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 22 | 23 | class HomeScreen extends StatefulWidget { 24 | const HomeScreen({super.key}); 25 | 26 | @override 27 | State createState() => _HomeScreenState(); 28 | } 29 | 30 | class _HomeScreenState extends State { 31 | final AudioPlayer player = AudioPlayer(); 32 | final AudioPlayer buttonClickPlayer = AudioPlayer(); 33 | 34 | late Navigation navigation; 35 | BottomBannerAd ad = BottomBannerAd(); 36 | 37 | @override 38 | void didChangeDependencies() { 39 | super.didChangeDependencies(); 40 | 41 | navigation = Navigation(Navigator.of(context)); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Consumer( 47 | builder: (context, themeProvider, _) { 48 | return WillPopScope( 49 | onWillPop: () async { 50 | return false; 51 | }, 52 | child: Scaffold( 53 | backgroundColor: themeProvider.bgColor, 54 | body: Stack( 55 | alignment: Alignment.topRight, 56 | children: [ 57 | Center( 58 | child: Column( 59 | crossAxisAlignment: CrossAxisAlignment.center, 60 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 61 | children: [ 62 | // Text( 63 | // "Tic Tac Toe", 64 | // style: GoogleFonts.hennyPenny( 65 | // fontSize: 58, 66 | // color: themeProvider.primaryColor, 67 | // ), 68 | // ), 69 | TextAnimator( 70 | "Tic Tac Toe", 71 | style: GoogleFonts.hennyPenny( 72 | fontSize: 58, 73 | color: themeProvider.primaryColor, 74 | ), 75 | // characterDelay: const Duration(milliseconds: 100), 76 | incomingEffect: 77 | WidgetTransitionEffects.incomingSlideInFromBottom(), 78 | // atRestEffect: WidgetRestingEffects.wave(), 79 | ), 80 | Column( 81 | crossAxisAlignment: CrossAxisAlignment.center, 82 | mainAxisAlignment: MainAxisAlignment.center, 83 | children: [ 84 | const VerticalSpacer(48), 85 | AnimationOnWidget( 86 | hasRestEffect: true, 87 | msDelay: 1600, 88 | child: Text( 89 | "Select mode", 90 | style: TextStyle( 91 | color: themeProvider.secondaryColor, 92 | fontSize: defaultTextSize, 93 | ), 94 | ), 95 | ), 96 | const VerticalSpacer(32), 97 | MyButton( 98 | msDelay: 800, 99 | doStateChange: true, 100 | onPressed: () { 101 | PopUp.show( 102 | context, 103 | title: "Select mode", 104 | description: 105 | "Do you want to play against AI or with another Player?", 106 | button1Text: "AI", 107 | button2Text: "Player", 108 | barrierDismissible: true, 109 | button1OnPressed: () { 110 | navigation.changeScreenReplacement( 111 | const SingleModeScreen( 112 | twoPlayerMode: false, 113 | ), 114 | widget, 115 | ); 116 | }, 117 | button2OnPressed: () { 118 | navigation.changeScreenReplacement( 119 | const SingleModeScreen( 120 | twoPlayerMode: true, 121 | ), 122 | widget, 123 | ); 124 | }, 125 | ); 126 | }, 127 | text: "Single", 128 | ), 129 | const VerticalSpacer(16), 130 | Consumer( 131 | builder: (context, loginProvider, _) { 132 | return MyButton( 133 | doStateChange: true, 134 | msDelay: 1200, 135 | onPressed: () async { 136 | User? user = 137 | FirebaseAuth.instance.currentUser; 138 | if (user != null) { 139 | print("user is available"); 140 | navigation.changeScreenReplacement( 141 | const RoomScreen(), 142 | widget, 143 | ); 144 | } else { 145 | if (Platform.isIOS) { 146 | PopUp.show( 147 | context, 148 | title: "Online Mode", 149 | description: 150 | "SignIn to play in Online mode", 151 | button1Text: "Sign in with Apple", 152 | button2Text: "Cancel", 153 | barrierDismissible: true, 154 | button1OnPressed: () async { 155 | print("sign in using appleId"); 156 | await loginProvider.loginWithApple(); 157 | 158 | Fluttertoast.showToast( 159 | msg: "Welcome", 160 | toastLength: Toast.LENGTH_LONG, 161 | gravity: ToastGravity.CENTER, 162 | ); 163 | Navigator.pop(context); 164 | // Continue to next screen 165 | print("user is available"); 166 | navigation.changeScreenReplacement( 167 | const RoomScreen(), 168 | widget, 169 | ); 170 | }, 171 | button2OnPressed: () { 172 | Navigator.pop(context); 173 | }, 174 | ); 175 | } else { 176 | print("sign in"); 177 | UserCredential userCred = 178 | await loginProvider.loginWithGoogle(); 179 | 180 | Fluttertoast.showToast( 181 | msg: 182 | "Welcome ${userCred.user!.displayName!}", 183 | toastLength: Toast.LENGTH_LONG, 184 | gravity: ToastGravity.CENTER, 185 | ); 186 | } 187 | } 188 | }, 189 | text: "Online", 190 | showLoading: loginProvider.loading, 191 | ); 192 | }, 193 | ), 194 | ], 195 | ), 196 | ], 197 | ), 198 | ), 199 | MyIconButton( 200 | msDelay: 1600, 201 | iconData: Icons.settings, 202 | onPressed: () { 203 | navigation.changeScreenReplacement( 204 | const SettingsPage(), 205 | widget, 206 | ); 207 | }, 208 | ) 209 | ], 210 | ), 211 | bottomNavigationBar: ad.showBanner(), 212 | ), 213 | ); 214 | }, 215 | ); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /lib/screen/settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:tic_tac_toe/components/button.dart'; 6 | import 'package:tic_tac_toe/components/icon_button.dart'; 7 | import 'package:tic_tac_toe/components/my_spacer.dart'; 8 | import 'package:tic_tac_toe/components/pop_up.dart'; 9 | import 'package:tic_tac_toe/constants.dart'; 10 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 11 | import 'package:tic_tac_toe/helper/navigation.dart'; 12 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 13 | import 'package:tic_tac_toe/helper/show_interstitial_ad.dart'; 14 | import 'package:tic_tac_toe/provider/audio_provider.dart'; 15 | import 'package:tic_tac_toe/provider/login_provider.dart'; 16 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 17 | import 'package:tic_tac_toe/screen/home.dart'; 18 | import 'package:url_launcher/url_launcher.dart'; 19 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 20 | 21 | class SettingsPage extends StatefulWidget { 22 | const SettingsPage({super.key}); 23 | 24 | @override 25 | State createState() => _SettingsPageState(); 26 | } 27 | 28 | class _SettingsPageState extends State { 29 | late Navigation navigation; 30 | 31 | BottomBannerAd ad = BottomBannerAd(); 32 | 33 | @override 34 | void didChangeDependencies() { 35 | super.didChangeDependencies(); 36 | 37 | navigation = Navigation(Navigator.of(context)); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Consumer3( 43 | builder: (context, themeProvider, audioProvider, loginProvider, _) { 44 | return WillPopScope( 45 | onWillPop: () async { 46 | return false; 47 | }, 48 | child: Scaffold( 49 | backgroundColor: themeProvider.bgColor, 50 | body: Stack( 51 | alignment: Alignment.topRight, 52 | children: [ 53 | Center( 54 | child: Padding( 55 | padding: const EdgeInsets.only(left: 48), 56 | child: Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | mainAxisAlignment: MainAxisAlignment.center, 59 | children: [ 60 | AnimationOnWidget( 61 | useIncomingEffect: true, 62 | incomingEffect: 63 | WidgetTransitionEffects.incomingSlideInFromTop( 64 | delay: const Duration(milliseconds: 400), 65 | curve: Curves.fastOutSlowIn, 66 | ), 67 | doStateChange: true, 68 | child: Text( 69 | "Settings", 70 | style: TextStyle( 71 | fontSize: 48, 72 | color: themeProvider.primaryColor, 73 | fontWeight: FontWeight.w400, 74 | ), 75 | ), 76 | ), 77 | const VerticalSpacer(36), 78 | AnimationOnWidget( 79 | msDelay: 600, 80 | doStateChange: true, 81 | child: Row( 82 | mainAxisAlignment: MainAxisAlignment.start, 83 | children: [ 84 | Text( 85 | "Disable Game audio", 86 | style: TextStyle( 87 | fontSize: defaultTextSize + 2, 88 | color: themeProvider.secondaryColor, 89 | ), 90 | ), 91 | const HorizontalSpacer(8), 92 | Switch( 93 | activeColor: themeProvider.primaryColor, 94 | activeTrackColor: 95 | themeProvider.primaryColor.withOpacity(0.5), 96 | inactiveThumbColor: themeProvider.primaryColor, 97 | inactiveTrackColor: 98 | themeProvider.primaryColor.withOpacity(0.5), 99 | value: !audioProvider.canPlayAudio, 100 | onChanged: (value) { 101 | audioProvider.setPlayAudio(); 102 | }, 103 | ), 104 | ], 105 | ), 106 | ), 107 | AnimationOnWidget( 108 | msDelay: 800, 109 | doStateChange: true, 110 | child: Row( 111 | mainAxisAlignment: MainAxisAlignment.start, 112 | children: [ 113 | Text( 114 | "Set dark theme", 115 | style: TextStyle( 116 | fontSize: defaultTextSize + 2, 117 | color: themeProvider.secondaryColor, 118 | ), 119 | ), 120 | const HorizontalSpacer(8), 121 | Switch( 122 | activeColor: themeProvider.primaryColor, 123 | activeTrackColor: 124 | themeProvider.primaryColor.withOpacity(0.5), 125 | inactiveThumbColor: themeProvider.primaryColor, 126 | inactiveTrackColor: 127 | themeProvider.primaryColor.withOpacity(0.5), 128 | value: !themeProvider.isLightTheme, 129 | onChanged: (value) { 130 | themeProvider.changeTheme(); 131 | }, 132 | ), 133 | ], 134 | ), 135 | ), 136 | const VerticalSpacer(16), 137 | MyButton( 138 | text: Platform.isIOS 139 | ? "More on App Store" 140 | : "More on PlayStore", 141 | msDelay: 1000, 142 | doStateChange: true, 143 | onPressed: () { 144 | String url = ""; 145 | if (Platform.isIOS) { 146 | url = 147 | "https://apps.apple.com/us/developer/sanjivy-kumaravel/id1741498828"; 148 | } else { 149 | url = 150 | "https://play.google.com/store/apps/dev?id=6439925551269057866"; 151 | } 152 | launchUrl(Uri.parse(url)); 153 | }, 154 | ), 155 | const VerticalSpacer(26), 156 | MyButton( 157 | text: "Game credits", 158 | msDelay: 1200, 159 | doStateChange: true, 160 | invertColor: true, 161 | onPressed: () { 162 | PopUp.show( 163 | context, 164 | title: "Game credits", 165 | description: 166 | "Designed & developed by Sanjivy for \nprinceappstudio.in", 167 | button1Text: "Know more", 168 | button2Text: "Close", 169 | barrierDismissible: false, 170 | button1OnPressed: () async { 171 | launchUrl(Uri.parse( 172 | "https://linktr.ee/princesanjivy")); 173 | }, 174 | button2OnPressed: () { 175 | Navigator.pop(context); 176 | }, 177 | ); 178 | }, 179 | ), 180 | const VerticalSpacer(56), 181 | loginProvider.isLoggedIn 182 | ? AnimationOnWidget( 183 | msDelay: 1400, 184 | doStateChange: true, 185 | child: Text( 186 | "Logged in as: ${loginProvider.getUserData.name}", 187 | style: TextStyle( 188 | fontSize: defaultTextSize + 2, 189 | color: themeProvider.secondaryColor, 190 | ), 191 | ), 192 | ) 193 | : Container(), 194 | const VerticalSpacer(4), 195 | loginProvider.isLoggedIn 196 | ? MyButton( 197 | text: "Logout", 198 | msDelay: 1400, 199 | doStateChange: true, 200 | onPressed: () { 201 | loginProvider.logout(); 202 | 203 | Fluttertoast.showToast( 204 | msg: "Logged out successfully", 205 | toastLength: Toast.LENGTH_LONG, 206 | gravity: ToastGravity.CENTER, 207 | ); 208 | }, 209 | ) 210 | : Container(), 211 | ], 212 | ), 213 | ), 214 | ), 215 | MyIconButton( 216 | msDelay: 1400, 217 | iconData: Icons.arrow_back_ios_new_rounded, 218 | onPressed: () { 219 | FullScreenAd.object.show(); 220 | 221 | navigation.changeScreenReplacement( 222 | const HomeScreen(), 223 | widget, 224 | ); 225 | }, 226 | ) 227 | ], 228 | ), 229 | bottomNavigationBar: ad.showBanner(), 230 | ), 231 | ); 232 | }, 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/screen/single_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:tic_tac_toe/components/icon_button.dart'; 6 | import 'package:tic_tac_toe/components/my_spacer.dart'; 7 | import 'package:tic_tac_toe/components/player_card.dart'; 8 | import 'package:tic_tac_toe/constants.dart'; 9 | import 'package:tic_tac_toe/helper/game.dart'; 10 | import 'package:tic_tac_toe/helper/navigation.dart'; 11 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 12 | import 'package:tic_tac_toe/model/symbol.dart'; 13 | import 'package:tic_tac_toe/provider/single_mode_provider.dart'; 14 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 15 | import 'package:tic_tac_toe/screen/home.dart'; 16 | import 'package:vibration/vibration.dart'; 17 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 18 | 19 | class SingleModeScreen extends StatefulWidget { 20 | const SingleModeScreen({super.key, required this.twoPlayerMode}); 21 | 22 | final bool twoPlayerMode; 23 | 24 | @override 25 | State createState() => SingleModeScreenState(); 26 | } 27 | 28 | class SingleModeScreenState extends State { 29 | late Navigation navigation; 30 | BottomBannerAd ad = BottomBannerAd(); 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | 36 | initProvider(); 37 | } 38 | 39 | void initProvider() { 40 | Provider.of(context, listen: false) 41 | .init(context, widget, widget.twoPlayerMode); 42 | } 43 | 44 | @override 45 | void didChangeDependencies() { 46 | super.didChangeDependencies(); 47 | 48 | navigation = Navigation(Navigator.of(context)); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | SingleModeProvider provider = 54 | Provider.of(context, listen: true); 55 | 56 | return Consumer(builder: (context, themeProvider, _) { 57 | return Scaffold( 58 | backgroundColor: themeProvider.bgColor, 59 | body: Stack( 60 | children: [ 61 | Center( 62 | child: Column( 63 | crossAxisAlignment: CrossAxisAlignment.center, 64 | mainAxisAlignment: MainAxisAlignment.center, 65 | children: [ 66 | Row( 67 | crossAxisAlignment: CrossAxisAlignment.center, 68 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 69 | children: [ 70 | PlayAnimationOnWidget( 71 | msDelay: 400, 72 | child: PlayerCard( 73 | imageUrl: "assets/images/user.png", 74 | name: "You (${provider.playerChose})", 75 | isAsset: true, 76 | showScore: true, 77 | scoreValue: provider 78 | .scores[PlaySymbol.inNum(provider.playerChose)]!, 79 | ), 80 | ), 81 | PlayAnimationOnWidget( 82 | msDelay: 1200, 83 | child: Text( 84 | "Round\n${provider.round}", 85 | style: GoogleFonts.hennyPenny( 86 | fontSize: defaultTextSize - 2, 87 | color: themeProvider.primaryColor, 88 | ), 89 | textAlign: TextAlign.center, 90 | ), 91 | ), 92 | PlayAnimationOnWidget( 93 | msDelay: 400, 94 | child: PlayerCard( 95 | imageUrl: widget.twoPlayerMode 96 | ? "assets/images/user.png" 97 | : "assets/images/ai.png", 98 | name: 99 | "${widget.twoPlayerMode ? "Player" : "AI"} (${provider.aiChose})", 100 | isAsset: true, 101 | showScore: true, 102 | scoreValue: provider 103 | .scores[PlaySymbol.inNum(provider.aiChose)]!, 104 | ), 105 | ), 106 | ], 107 | ), 108 | const VerticalSpacer(16), 109 | PlayAnimationOnWidget( 110 | useIncomingEffect: true, 111 | incomingEffect: WidgetTransitionEffects.incomingScaleDown( 112 | delay: const Duration(milliseconds: 800), 113 | curve: Curves.fastOutSlowIn, 114 | ), 115 | child: Padding( 116 | padding: const EdgeInsets.all(32), 117 | child: Container( 118 | width: kIsWeb ? 500 : null, 119 | height: kIsWeb ? 500 : null, 120 | alignment: Alignment.center, 121 | padding: const EdgeInsets.all(2), 122 | decoration: BoxDecoration( 123 | color: themeProvider.primaryColor, 124 | borderRadius: BorderRadius.circular(16), 125 | ), 126 | child: GridView.builder( 127 | padding: const EdgeInsets.all(0), 128 | physics: const NeverScrollableScrollPhysics(), 129 | shrinkWrap: true, 130 | gridDelegate: 131 | SliverGridDelegateWithFixedCrossAxisCount( 132 | crossAxisCount: getBoardSize(provider.board), 133 | mainAxisSpacing: 1, 134 | crossAxisSpacing: 1, 135 | // childAspectRatio: 1, 136 | ), 137 | itemCount: provider.board.length, 138 | itemBuilder: (context, index) { 139 | return GestureDetector( 140 | onTap: () { 141 | if (!kIsWeb) { 142 | Vibration.vibrate( 143 | duration: 80, amplitude: 120); 144 | } 145 | if (widget.twoPlayerMode) { 146 | if (provider.board[index] == 0) { 147 | print(provider.board); 148 | provider.board[index] = 149 | PlaySymbol.inNum(provider.turn); 150 | provider.turn = 151 | (provider.turn == PlaySymbol.x 152 | ? PlaySymbol.o 153 | : PlaySymbol.x); 154 | 155 | provider.validate(); 156 | } 157 | } else { 158 | if (provider.board[index] == 0 && 159 | provider.turn == provider.playerChose) { 160 | provider.board[index] = 161 | PlaySymbol.inNum(provider.turn); 162 | provider.turn = 163 | (provider.turn == PlaySymbol.x 164 | ? PlaySymbol.o 165 | : PlaySymbol.x); 166 | 167 | provider.aiMove(provider.turn); 168 | } 169 | } 170 | }, 171 | child: Container( 172 | decoration: BoxDecoration( 173 | color: 174 | provider.result.positions.contains(index) 175 | ? Colors.deepOrange.withOpacity(0.8) 176 | : themeProvider.bgColor, 177 | border: Border.all( 178 | color: themeProvider.primaryColor, 179 | // width: 2, 180 | ), 181 | borderRadius: provider.corners.contains(index) 182 | ? provider.borders[index] 183 | : null, 184 | ), 185 | child: Center( 186 | child: Text( 187 | provider.board[index] == PlaySymbol.xInt 188 | ? PlaySymbol.x 189 | : provider.board[index] == 190 | PlaySymbol.oInt 191 | ? PlaySymbol.o 192 | : "", 193 | style: GoogleFonts.hennyPenny( 194 | fontSize: 42 - 8, 195 | color: provider.result.positions 196 | .contains(index) 197 | ? themeProvider.bgColor 198 | : themeProvider.primaryColor, 199 | ), 200 | ), 201 | ), 202 | ), 203 | ); 204 | }, 205 | ), 206 | ), 207 | ), 208 | ), 209 | const VerticalSpacer(4), 210 | PlayAnimationOnWidget( 211 | msDelay: 1200, 212 | hasRestEffect: true, 213 | incomingEffect: WidgetTransitionEffects.incomingScaleUp( 214 | delay: const Duration(milliseconds: 1200), 215 | curve: Curves.easeInOut, 216 | ), 217 | child: Container( 218 | padding: const EdgeInsets.symmetric( 219 | horizontal: 16, vertical: 8), 220 | decoration: BoxDecoration( 221 | color: themeProvider.secondaryColor, 222 | borderRadius: BorderRadius.circular(12), 223 | ), 224 | child: Text( 225 | (provider.turn == provider.playerChose) 226 | ? "Your turn" 227 | : widget.twoPlayerMode 228 | ? "Player turn" 229 | : "AI turn", 230 | style: TextStyle( 231 | fontSize: defaultTextSize, 232 | color: themeProvider.bgColor, 233 | ), 234 | ), 235 | ), 236 | ), 237 | ], 238 | ), 239 | ), 240 | MyIconButton( 241 | msDelay: 1200, 242 | iconData: Icons.arrow_back_ios_new_rounded, 243 | onPressed: () { 244 | navigation.changeScreenReplacement( 245 | const HomeScreen(), 246 | widget, 247 | ); 248 | }, 249 | ) 250 | ], 251 | ), 252 | bottomNavigationBar: ad.showBanner(), 253 | ); 254 | }); 255 | } 256 | } 257 | 258 | class PlayAnimationOnWidget extends StatefulWidget { 259 | final Widget child; 260 | final int? msDelay; 261 | final bool useIncomingEffect, hasRestEffect; 262 | final WidgetTransitionEffects? incomingEffect; 263 | 264 | const PlayAnimationOnWidget({ 265 | super.key, 266 | required this.child, 267 | this.msDelay, 268 | this.useIncomingEffect = false, 269 | this.incomingEffect, 270 | this.hasRestEffect = false, 271 | }); 272 | 273 | @override 274 | State createState() => _PlayAnimationOnWidgetState(); 275 | } 276 | 277 | class _PlayAnimationOnWidgetState extends State { 278 | @override 279 | Widget build(BuildContext context) { 280 | return Consumer(builder: (context, provider, _) { 281 | return WidgetAnimator( 282 | incomingEffect: widget.useIncomingEffect 283 | ? widget.incomingEffect 284 | : WidgetTransitionEffects.incomingScaleUp( 285 | delay: Duration(milliseconds: widget.msDelay!), 286 | curve: Curves.easeInOut, 287 | ), 288 | atRestEffect: widget.hasRestEffect ? WidgetRestingEffects.wave() : null, 289 | doStateChange: provider.doStateChange, 290 | child: widget.child, 291 | ); 292 | }); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /lib/screen/room.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:firebase_database/firebase_database.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:fluttertoast/fluttertoast.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:share_plus/share_plus.dart'; 9 | import 'package:tic_tac_toe/components/button.dart'; 10 | import 'package:tic_tac_toe/components/icon_button.dart'; 11 | import 'package:tic_tac_toe/components/my_spacer.dart'; 12 | import 'package:tic_tac_toe/components/pop_up.dart'; 13 | import 'package:tic_tac_toe/constants.dart'; 14 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 15 | import 'package:tic_tac_toe/helper/check_win.dart' as helper; 16 | import 'package:tic_tac_toe/helper/game.dart'; 17 | import 'package:tic_tac_toe/helper/navigation.dart'; 18 | import 'package:tic_tac_toe/helper/screenshot_board.dart'; 19 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 20 | import 'package:tic_tac_toe/helper/show_interstitial_ad.dart'; 21 | import 'package:tic_tac_toe/model/room.dart'; 22 | import 'package:tic_tac_toe/model/symbol.dart'; 23 | import 'package:tic_tac_toe/provider/game_provider.dart'; 24 | import 'package:tic_tac_toe/provider/login_provider.dart'; 25 | import 'package:tic_tac_toe/provider/room_provider.dart'; 26 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 27 | import 'package:tic_tac_toe/screen/game.dart'; 28 | import 'package:tic_tac_toe/screen/home.dart'; 29 | import 'package:tic_tac_toe/screen/lobby.dart'; 30 | import 'package:url_launcher/url_launcher.dart'; 31 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 32 | 33 | class RoomScreen extends StatefulWidget { 34 | const RoomScreen({super.key}); 35 | 36 | @override 37 | State createState() => _RoomScreenState(); 38 | } 39 | 40 | class _RoomScreenState extends State { 41 | TextEditingController roomCodeController = TextEditingController(); 42 | 43 | late Navigation navigation; 44 | 45 | BottomBannerAd ad = BottomBannerAd(); 46 | 47 | @override 48 | void didChangeDependencies() { 49 | super.didChangeDependencies(); 50 | 51 | navigation = Navigation(Navigator.of(context)); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | super.dispose(); 57 | 58 | print("Room Dispose"); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | double width = kIsWeb ? 400 : MediaQuery.of(context).size.width; 64 | 65 | return Consumer(builder: (context, themeProvider, _) { 66 | return Scaffold( 67 | backgroundColor: themeProvider.bgColor, 68 | body: Stack( 69 | children: [ 70 | Center( 71 | child: Column( 72 | crossAxisAlignment: CrossAxisAlignment.center, 73 | mainAxisAlignment: MainAxisAlignment.center, 74 | children: [ 75 | AnimationOnWidget( 76 | useIncomingEffect: true, 77 | incomingEffect: 78 | WidgetTransitionEffects.incomingSlideInFromTop( 79 | delay: const Duration(milliseconds: 400), 80 | curve: Curves.fastOutSlowIn, 81 | ), 82 | child: Text( 83 | "Enter room code and join with your friend", 84 | style: TextStyle( 85 | fontSize: defaultTextSize, 86 | color: themeProvider.secondaryColor, 87 | ), 88 | ), 89 | ), 90 | const VerticalSpacer(32), 91 | AnimationOnWidget( 92 | msDelay: 800, 93 | child: Material( 94 | shape: RoundedRectangleBorder( 95 | side: BorderSide( 96 | color: themeProvider.primaryColor, 97 | width: 2, 98 | ), 99 | borderRadius: const BorderRadius.all( 100 | Radius.circular(12), 101 | ), 102 | ), 103 | child: SizedBox( 104 | height: 48, 105 | width: width / 1.6, 106 | child: TextField( 107 | controller: roomCodeController, 108 | style: TextStyle( 109 | color: themeProvider.primaryColor, 110 | fontSize: 18, 111 | ), 112 | maxLines: 1, 113 | // maxLength: 6, 114 | textAlign: TextAlign.center, 115 | textAlignVertical: TextAlignVertical.center, 116 | keyboardType: TextInputType.number, 117 | inputFormatters: [ 118 | FilteringTextInputFormatter.digitsOnly 119 | ], 120 | decoration: InputDecoration( 121 | filled: true, 122 | fillColor: themeProvider.bgColor, 123 | border: const OutlineInputBorder( 124 | borderSide: BorderSide.none), 125 | contentPadding: EdgeInsets.zero, 126 | hintText: "Enter room code", 127 | hintStyle: TextStyle( 128 | color: themeProvider.primaryColor, 129 | fontSize: 18, 130 | ), 131 | ), 132 | ), 133 | ), 134 | ), 135 | ), 136 | const VerticalSpacer(16), 137 | Consumer2( 138 | builder: (context, roomProvider, loginProvider, _) { 139 | return MyButton( 140 | doStateChange: true, 141 | msDelay: 1200, 142 | onPressed: () async { 143 | FocusManager.instance.primaryFocus?.unfocus(); 144 | if (roomCodeController.text.isNotEmpty) { 145 | int roomCodeInput = 146 | int.parse(roomCodeController.text); 147 | bool isRoomExist = 148 | await roomProvider.isRoomExist(roomCodeInput); 149 | if (!isRoomExist) { 150 | Fluttertoast.showToast( 151 | msg: "Room doesn't exist", 152 | toastLength: Toast.LENGTH_LONG, 153 | gravity: ToastGravity.CENTER, 154 | ); 155 | roomCodeController.clear(); 156 | } else { 157 | await roomProvider.joinRoom( 158 | loginProvider.getUserData, 159 | roomCodeInput, 160 | widget, 161 | ); 162 | 163 | bool isRoomOwner = false; 164 | 165 | FullScreenAd.object.show(); 166 | 167 | navigation.changeScreenReplacement( 168 | GameScreenController( 169 | roomCode: roomCodeInput, 170 | isRoomOwner: isRoomOwner, 171 | ), 172 | widget, 173 | ); 174 | } 175 | } else { 176 | Fluttertoast.showToast( 177 | msg: "Please enter a room code", 178 | toastLength: Toast.LENGTH_LONG, 179 | gravity: ToastGravity.CENTER, 180 | ); 181 | } 182 | }, 183 | text: "Join", 184 | showLoading: roomProvider.showJoinLoading, 185 | ); 186 | }), 187 | const VerticalSpacer(32), 188 | AnimationOnWidget( 189 | useIncomingEffect: true, 190 | incomingEffect: 191 | WidgetTransitionEffects.incomingSlideInFromTop( 192 | delay: const Duration(milliseconds: 2000), 193 | curve: Curves.fastOutSlowIn, 194 | ), 195 | child: Text( 196 | "or", 197 | style: TextStyle( 198 | fontSize: defaultTextSize, 199 | color: themeProvider.secondaryColor, 200 | ), 201 | ), 202 | ), 203 | const VerticalSpacer(32), 204 | Consumer2( 205 | builder: (context, roomProvider, loginProvider, _) { 206 | return MyButton( 207 | doStateChange: true, 208 | msDelay: 1600, 209 | hasRestEffect: true, 210 | onPressed: () async { 211 | int roomCode = await roomProvider.createRoom( 212 | loginProvider.getUserData, 213 | widget, 214 | ); 215 | 216 | bool isRoomOwner = true; 217 | 218 | navigation.changeScreenReplacement( 219 | GameScreenController( 220 | roomCode: roomCode, 221 | isRoomOwner: isRoomOwner, 222 | ), 223 | widget, 224 | ); 225 | }, 226 | text: "Create room", 227 | showLoading: roomProvider.loading, 228 | ); 229 | }), 230 | ], 231 | ), 232 | ), 233 | MyIconButton( 234 | onPressed: () { 235 | FullScreenAd.object.show(); 236 | 237 | navigation.changeScreenReplacement( 238 | const HomeScreen(), 239 | widget, 240 | ); 241 | }, 242 | msDelay: 2000, 243 | iconData: Icons.arrow_back_ios_new_rounded, 244 | ), 245 | ], 246 | ), 247 | bottomNavigationBar: ad.showBanner(), 248 | ); 249 | }); 250 | } 251 | } 252 | 253 | class GameScreenController extends StatefulWidget { 254 | const GameScreenController({ 255 | super.key, 256 | required this.roomCode, 257 | required this.isRoomOwner, 258 | }); 259 | 260 | final int roomCode; 261 | final bool isRoomOwner; 262 | 263 | @override 264 | State createState() => _GameScreenControllerState(); 265 | } 266 | 267 | class _GameScreenControllerState extends State { 268 | GlobalKey screenshotImgKey = GlobalKey(); 269 | 270 | late RoomProvider roomProvider; 271 | late Navigation navigation; 272 | 273 | @override 274 | void didChangeDependencies() { 275 | super.didChangeDependencies(); 276 | 277 | navigation = Navigation(Navigator.of(context)); 278 | roomProvider = Provider.of(context, listen: false); 279 | } 280 | 281 | @override 282 | void dispose() { 283 | super.dispose(); 284 | 285 | print("GameController Disposed"); 286 | roomProvider.leaveRoom( 287 | widget.roomCode, 288 | widget.isRoomOwner, 289 | ); 290 | } 291 | 292 | @override 293 | Widget build(BuildContext context) { 294 | return Consumer2( 295 | builder: (context, gameProvider, themeProvider, _) { 296 | return StreamBuilder( 297 | stream: FirebaseDatabase.instance 298 | .ref("$roomPath${widget.roomCode}/") 299 | .onValue, 300 | builder: (context, db) { 301 | if (!db.hasData) { 302 | return Scaffold( 303 | backgroundColor: themeProvider.bgColor, 304 | body: Center( 305 | child: CircularProgressIndicator( 306 | color: themeProvider.primaryColor, 307 | strokeWidth: 2, 308 | ), 309 | ), 310 | ); 311 | } 312 | if (db.data!.snapshot.value == null) { 313 | print("Room closed/deleted in firestore!"); 314 | return const HomeScreen(); 315 | } 316 | RoomData roomData = RoomData.fromJson( 317 | db.data!.snapshot.value, 318 | widget.roomCode, 319 | ); 320 | // print(roomData); 321 | 322 | if (roomData.isStarted) { 323 | print("Current board: ${roomData.board}"); 324 | int player = PlaySymbol.inNum( 325 | roomData.turn == PlaySymbol.x ? PlaySymbol.o : PlaySymbol.x, 326 | ); 327 | helper.Result result = helper.checkWin( 328 | roomData.board, 329 | player, 330 | getBoardSize(roomData.board), 331 | ); 332 | if (result.hasWon || !roomData.board.contains(0)) { 333 | WidgetsBinding.instance.addPostFrameCallback((_) async { 334 | String wonMsg = 335 | "${(player == PlaySymbol.xInt && widget.isRoomOwner) || (player != PlaySymbol.xInt && !widget.isRoomOwner) ? "You" : "Opponent"} won this round!\n\n"; 336 | PopUp.show( 337 | context, 338 | title: result.hasWon ? "Win" : "Game draw", 339 | description: 340 | "${result.hasWon ? wonMsg : ""}Next round restarting in 5 seconds...", 341 | button2Text: "Share", 342 | button1Text: "Rate game", 343 | barrierDismissible: false, 344 | button2OnPressed: () async { 345 | // screenshot and share image 346 | if (kIsWeb) { 347 | Fluttertoast.showToast( 348 | msg: "Oops! Share feature only available in Android", 349 | toastLength: Toast.LENGTH_LONG, 350 | gravity: ToastGravity.CENTER, 351 | ); 352 | } else { 353 | final XFile xFile = 354 | await screenshotBoard(screenshotImgKey); 355 | Share.shareXFiles([xFile], text: "Had fun?"); 356 | } 357 | }, 358 | button1OnPressed: () { 359 | launchUrl( 360 | Uri.parse(Platform.isIOS ? gameLinkIos: gameLinkAndroid), 361 | ); 362 | }, 363 | ); 364 | }); 365 | gameProvider.resetBoard( 366 | "$roomPath${roomData.code}", 367 | roomData, 368 | player, 369 | widget.isRoomOwner, 370 | context, 371 | ); 372 | } 373 | 374 | return GameScreen( 375 | screenshotImgKey: screenshotImgKey, 376 | roomData: roomData, 377 | isRoomOwner: widget.isRoomOwner, 378 | result: result, 379 | ); 380 | } 381 | 382 | print(roomData.toJson()); 383 | print(roomData.players.length); 384 | return LobbyScreen( 385 | roomData: roomData, 386 | isRoomOwner: widget.isRoomOwner, 387 | ); 388 | }, 389 | ); 390 | }, 391 | ); 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /lib/screen/lobby.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:clipboard/clipboard.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:google_fonts/google_fonts.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:share_plus/share_plus.dart'; 9 | import 'package:tic_tac_toe/components/button.dart'; 10 | import 'package:tic_tac_toe/components/icon_button.dart'; 11 | import 'package:tic_tac_toe/components/my_spacer.dart'; 12 | import 'package:tic_tac_toe/components/player_card.dart'; 13 | import 'package:tic_tac_toe/components/pop_up.dart'; 14 | import 'package:tic_tac_toe/constants.dart'; 15 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 16 | import 'package:tic_tac_toe/helper/navigation.dart'; 17 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 18 | import 'package:tic_tac_toe/model/player.dart'; 19 | import 'package:tic_tac_toe/model/room.dart'; 20 | import 'package:tic_tac_toe/provider/game_provider.dart'; 21 | import 'package:tic_tac_toe/provider/login_provider.dart'; 22 | import 'package:tic_tac_toe/provider/room_provider.dart'; 23 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 24 | import 'package:tic_tac_toe/screen/room.dart'; 25 | 26 | class LobbyScreen extends StatefulWidget { 27 | const LobbyScreen({ 28 | super.key, 29 | required this.roomData, 30 | required this.isRoomOwner, 31 | }); 32 | 33 | final RoomData roomData; 34 | final bool isRoomOwner; 35 | 36 | @override 37 | State createState() => _LobbyScreenState(); 38 | } 39 | 40 | class _LobbyScreenState extends State { 41 | late Navigation navigation; 42 | late RoomProvider roomProvider; 43 | 44 | BottomBannerAd ad = BottomBannerAd(); 45 | 46 | @override 47 | void didChangeDependencies() { 48 | super.didChangeDependencies(); 49 | 50 | navigation = Navigation(Navigator.of(context)); 51 | roomProvider = Provider.of(context, listen: false); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | double width = kIsWeb ? 600 : MediaQuery.of(context).size.width; 57 | 58 | return Consumer2( 59 | builder: (context, loginProvider, themeProvider, _) { 60 | return Scaffold( 61 | backgroundColor: themeProvider.bgColor, 62 | body: Stack( 63 | children: [ 64 | Center( 65 | child: Column( 66 | crossAxisAlignment: CrossAxisAlignment.center, 67 | mainAxisAlignment: MainAxisAlignment.center, 68 | children: [ 69 | AnimationOnWidget( 70 | msDelay: 400, 71 | child: Padding( 72 | padding: const EdgeInsets.all(32), 73 | child: Container( 74 | width: width, 75 | padding: const EdgeInsets.all(18), 76 | decoration: BoxDecoration( 77 | borderRadius: BorderRadius.circular(borderRadius), 78 | border: Border.all( 79 | color: themeProvider.primaryColor, 80 | width: 2, 81 | ), 82 | color: themeProvider.bgColor, 83 | boxShadow: shadow, 84 | ), 85 | child: Stack( 86 | children: [ 87 | Column( 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | Text( 91 | "Share this room code\nwith your friend", 92 | style: TextStyle( 93 | fontSize: defaultTextSize, 94 | color: themeProvider.secondaryColor, 95 | ), 96 | ), 97 | const VerticalSpacer(12), 98 | Text( 99 | "${widget.roomData.code}", 100 | style: GoogleFonts.hennyPenny( 101 | fontSize: 24, 102 | color: themeProvider.primaryColor, 103 | ), 104 | ), 105 | ], 106 | ), 107 | 108 | /// share button, show copy button in web 109 | Positioned( 110 | top: 20, 111 | right: 10, 112 | child: ElevatedButton( 113 | onPressed: () { 114 | if (kIsWeb) { 115 | FlutterClipboard.copy( 116 | "Tic Tac Toe Online room code is : ${widget.roomData.code}, vist $gameLinkWeb and play.\n" 117 | "Want to play in Android instead?, visit the link to download: ${Platform.isIOS ? gameLinkIos: gameLinkAndroid}.", 118 | ); 119 | 120 | Fluttertoast.showToast( 121 | msg: "Room code copied", 122 | toastLength: Toast.LENGTH_LONG, 123 | gravity: ToastGravity.CENTER, 124 | ); 125 | } else { 126 | Share.share( 127 | "Tic Tac Toe Online room code is : ${widget.roomData.code}.\n" 128 | "Don't have the game, visit the link to download: ${Platform.isIOS ? gameLinkIos: gameLinkAndroid}."); 129 | } 130 | }, 131 | style: ButtonStyle( 132 | minimumSize: 133 | MaterialStateProperty.all( 134 | const Size(48, 48), 135 | ), 136 | elevation: 137 | MaterialStateProperty.all(4), 138 | shape: MaterialStateProperty.all< 139 | OutlinedBorder>( 140 | RoundedRectangleBorder( 141 | borderRadius: BorderRadius.circular(12), 142 | ), 143 | ), 144 | padding: 145 | MaterialStateProperty.all( 146 | const EdgeInsets.all(0)), 147 | backgroundColor: 148 | MaterialStateProperty.all( 149 | themeProvider.primaryColor), 150 | ), 151 | child: Icon( 152 | kIsWeb ? Icons.copy : Icons.share_rounded, 153 | color: themeProvider.bgColor, 154 | ), 155 | // child: Text( 156 | // "i", 157 | // style: TextStyle( 158 | // fontSize: defaultTextSize, 159 | // color: bgColor, 160 | // letterSpacing: 1, 161 | // ), 162 | // ), 163 | ), 164 | ), 165 | ], 166 | ), 167 | ), 168 | ), 169 | ), 170 | const VerticalSpacer(32), 171 | 172 | /// players vs opponent display cards 173 | Row( 174 | crossAxisAlignment: CrossAxisAlignment.center, 175 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 176 | children: [ 177 | AnimationOnWidget( 178 | msDelay: 800, 179 | child: PlayerCard( 180 | imageUrl: loginProvider.getUserData.displayPicture, 181 | name: loginProvider.getUserData.name, 182 | ), 183 | ), 184 | AnimationOnWidget( 185 | msDelay: 1200, 186 | child: Text( 187 | "vs", 188 | style: GoogleFonts.hennyPenny( 189 | fontSize: defaultTextSize, 190 | color: themeProvider.primaryColor, 191 | ), 192 | ), 193 | ), 194 | widget.roomData.players.length == 2 195 | ? FutureBuilder( 196 | future: loginProvider.getUserById(widget 197 | .roomData 198 | .players[widget.isRoomOwner ? 1 : 0] 199 | .playerId), 200 | builder: (context, snapshot) { 201 | if (!snapshot.hasData) { 202 | return CircularProgressIndicator( 203 | color: themeProvider.bgColor); 204 | } 205 | return AnimationOnWidget( 206 | msDelay: 800, 207 | child: PlayerCard( 208 | imageUrl: snapshot.data!.displayPicture, 209 | name: snapshot.data!.name, 210 | ), 211 | ); 212 | }, 213 | ) 214 | : AnimationOnWidget( 215 | msDelay: 800, 216 | child: Column( 217 | crossAxisAlignment: CrossAxisAlignment.center, 218 | children: [ 219 | Container( 220 | width: 100, 221 | height: 100, 222 | decoration: BoxDecoration( 223 | color: themeProvider.bgColor, 224 | borderRadius: 225 | BorderRadius.circular(100), 226 | boxShadow: shadow, 227 | ), 228 | ), 229 | const VerticalSpacer(16), 230 | Text( 231 | "Waiting for\nopponent", 232 | style: TextStyle( 233 | fontSize: defaultTextSize, 234 | color: themeProvider.secondaryColor, 235 | ), 236 | textAlign: TextAlign.center, 237 | ), 238 | ], 239 | ), 240 | ), 241 | ], 242 | ), 243 | const VerticalSpacer(84), 244 | widget.roomData.players.length == 2 245 | ? widget.isRoomOwner 246 | ? Container() 247 | : Text( 248 | "Wait for Player1 to start the game", 249 | style: TextStyle( 250 | fontSize: defaultTextSize - 4, 251 | color: themeProvider.secondaryColor, 252 | ), 253 | ) 254 | : AnimationOnWidget( 255 | msDelay: 1600, 256 | child: Text( 257 | "Waiting for player to join...", 258 | style: TextStyle( 259 | fontSize: defaultTextSize - 4, 260 | color: themeProvider.secondaryColor, 261 | ), 262 | ), 263 | ), 264 | widget.roomData.players.length == 2 265 | ? !widget.isRoomOwner 266 | ? const VerticalSpacer(8) 267 | : Container() 268 | : const VerticalSpacer(8), 269 | 270 | Consumer2( 271 | builder: (context, roomProvider, gameProvider, _) { 272 | print(widget.roomData.players.length); 273 | return MyButton( 274 | msDelay: 1600, 275 | doStateChange: true, 276 | text: widget.roomData.players.length == 2 277 | ? widget.isRoomOwner 278 | ? "Start" 279 | : "Waiting..." 280 | : "Waiting...", 281 | onPressed: () { 282 | if (widget.isRoomOwner) { 283 | if (widget.roomData.players[0].playerId == 284 | widget.roomData.players[1].playerId) { 285 | Fluttertoast.showToast( 286 | msg: "Looks like you're play against yourself", 287 | toastLength: Toast.LENGTH_LONG, 288 | gravity: ToastGravity.CENTER, 289 | ); 290 | } else { 291 | roomProvider.isStarted( 292 | true, widget.roomData.code); 293 | } 294 | } else { 295 | Fluttertoast.showToast( 296 | msg: "You can't start the game", 297 | toastLength: Toast.LENGTH_LONG, 298 | gravity: ToastGravity.CENTER, 299 | ); 300 | } 301 | }, 302 | showLoading: 303 | widget.roomData.players.length == 2 ? false : true, 304 | ); 305 | }), 306 | ], 307 | ), 308 | ), 309 | MyIconButton( 310 | onPressed: () { 311 | PopUp.show( 312 | context, 313 | title: "Warning", 314 | description: "Are you sure want to leave?", 315 | button1Text: "Yes", 316 | button2Text: "No", 317 | barrierDismissible: false, 318 | button1OnPressed: () async { 319 | navigation.goBack(context); 320 | // To remove GameController Widget 321 | await navigation.changeScreenReplacement( 322 | const RoomScreen(), 323 | widget, 324 | ); 325 | }, 326 | button2OnPressed: () { 327 | Navigator.pop(context); 328 | }, 329 | ); 330 | }, 331 | msDelay: 2000, 332 | iconData: Icons.arrow_back_ios_new_rounded), 333 | ], 334 | ), 335 | bottomNavigationBar: ad.showBanner(), 336 | ); 337 | }, 338 | ); 339 | } 340 | } 341 | 342 | // {"chose": "O", "id": "mpy0Iz7hPWMGQOVYOv15y8o3VlI3"} Prince Sanjivy 343 | // {"chose": "O", "id": "acokUiqN5nXAnL9axIn36PRpX5t2"} Sanjivy Kumaravel 344 | -------------------------------------------------------------------------------- /lib/screen/game.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_database/firebase_database.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:google_fonts/google_fonts.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:tic_tac_toe/components/button.dart'; 7 | import 'package:tic_tac_toe/components/icon_button.dart'; 8 | import 'package:tic_tac_toe/components/my_spacer.dart'; 9 | import 'package:tic_tac_toe/components/player_card.dart'; 10 | import 'package:tic_tac_toe/components/pop_up.dart'; 11 | import 'package:tic_tac_toe/constants.dart'; 12 | import 'package:tic_tac_toe/helper/animation_widget.dart'; 13 | import 'package:tic_tac_toe/helper/check_win.dart'; 14 | import 'package:tic_tac_toe/helper/game.dart'; 15 | import 'package:tic_tac_toe/helper/navigation.dart'; 16 | import 'package:tic_tac_toe/helper/show_banner_ad.dart'; 17 | import 'package:tic_tac_toe/helper/show_interstitial_ad.dart'; 18 | import 'package:tic_tac_toe/model/player.dart'; 19 | import 'package:tic_tac_toe/model/room.dart'; 20 | import 'package:tic_tac_toe/model/symbol.dart'; 21 | import 'package:tic_tac_toe/provider/game_provider.dart'; 22 | import 'package:tic_tac_toe/provider/login_provider.dart'; 23 | import 'package:tic_tac_toe/provider/theme_provider.dart'; 24 | import 'package:tic_tac_toe/screen/room.dart'; 25 | import 'package:vibration/vibration.dart'; 26 | import 'package:widget_and_text_animator/widget_and_text_animator.dart'; 27 | 28 | class GameScreen extends StatefulWidget { 29 | const GameScreen({ 30 | super.key, 31 | required this.roomData, 32 | required this.isRoomOwner, 33 | required this.result, 34 | required this.screenshotImgKey, 35 | }); 36 | 37 | final RoomData roomData; 38 | final bool isRoomOwner; 39 | final Result result; 40 | final GlobalKey screenshotImgKey; 41 | 42 | @override 43 | State createState() => _GameScreenState(); 44 | } 45 | 46 | class _GameScreenState extends State { 47 | late Navigation navigation; 48 | 49 | BottomBannerAd ad = BottomBannerAd(); 50 | 51 | @override 52 | void didChangeDependencies() { 53 | super.didChangeDependencies(); 54 | 55 | // nameless(){ 56 | GameProvider gameProvider = Provider.of(context); 57 | gameProvider.designBoard(widget.roomData.board); 58 | 59 | navigation = Navigation(Navigator.of(context)); 60 | // } 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return Consumer3( 66 | builder: (context, loginProvider, gameProvider, themeProvider, _) { 67 | // See if any player leaves the game, show popup and go to HomeScreen() 68 | if (widget.roomData.players.length <= 1) { 69 | WidgetsBinding.instance.addPostFrameCallback((_) { 70 | PopUp.show( 71 | context, 72 | title: "Oops!", 73 | description: "Player 1 has left the game.\n" 74 | "Do you want to wait a player to join back?", 75 | button1Text: "Yes", 76 | button2Text: "Leave", 77 | barrierDismissible: false, 78 | button1OnPressed: () async { 79 | navigation.goBack(context); 80 | }, 81 | button2OnPressed: () async { 82 | navigation.goBack(context); 83 | // To remove GameController Widget 84 | await navigation.changeScreenReplacement( 85 | const RoomScreen(), 86 | widget, 87 | ); 88 | }, 89 | ); 90 | }); 91 | 92 | return Scaffold( 93 | backgroundColor: themeProvider.bgColor, 94 | body: Center( 95 | child: Column( 96 | mainAxisAlignment: MainAxisAlignment.center, 97 | crossAxisAlignment: CrossAxisAlignment.center, 98 | children: [ 99 | Text( 100 | "Waiting for Player to join...", 101 | style: TextStyle( 102 | fontSize: defaultTextSize + 2, 103 | color: themeProvider.secondaryColor, 104 | ), 105 | ), 106 | const VerticalSpacer(8), 107 | MyButton( 108 | text: "Leave game", 109 | msDelay: 100, 110 | onPressed: () async { 111 | // To remove GameController Widget 112 | await navigation.changeScreenReplacement( 113 | const RoomScreen(), 114 | widget, 115 | ); 116 | }, 117 | ), 118 | ], 119 | ), 120 | ), 121 | ); 122 | } 123 | return Scaffold( 124 | backgroundColor: themeProvider.bgColor, 125 | body: Stack( 126 | children: [ 127 | RepaintBoundary( 128 | key: widget.screenshotImgKey, 129 | child: Center( 130 | child: Column( 131 | crossAxisAlignment: CrossAxisAlignment.center, 132 | mainAxisAlignment: MainAxisAlignment.center, 133 | children: [ 134 | Row( 135 | crossAxisAlignment: CrossAxisAlignment.center, 136 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 137 | children: [ 138 | AnimationOnWidget( 139 | msDelay: 400, 140 | doStateChange: true, 141 | child: PlayerCard( 142 | imageUrl: 143 | loginProvider.getUserData.displayPicture, 144 | name: 145 | "You (${widget.roomData.players[!widget.isRoomOwner ? 1 : 0].chose})", 146 | showScore: true, 147 | scoreValue: widget 148 | .roomData 149 | .players[!widget.isRoomOwner ? 1 : 0] 150 | .winCount, 151 | ), 152 | ), 153 | AnimationOnWidget( 154 | msDelay: 1200, 155 | doStateChange: true, 156 | child: Text( 157 | "Round\n${widget.roomData.round}", 158 | style: GoogleFonts.hennyPenny( 159 | fontSize: defaultTextSize - 2, 160 | color: themeProvider.primaryColor, 161 | ), 162 | textAlign: TextAlign.center, 163 | ), 164 | ), 165 | FutureBuilder( 166 | future: loginProvider.getUserById(widget.roomData 167 | .players[widget.isRoomOwner ? 1 : 0].playerId), 168 | builder: (context, snapshot) { 169 | if (!snapshot.hasData) { 170 | return CircularProgressIndicator( 171 | color: themeProvider.bgColor); 172 | } 173 | return AnimationOnWidget( 174 | msDelay: 400, 175 | doStateChange: true, 176 | child: PlayerCard( 177 | imageUrl: snapshot.data!.displayPicture, 178 | name: 179 | "${snapshot.data!.name.split(" ")[0]} (${widget.roomData.players[widget.isRoomOwner ? 1 : 0].chose})", 180 | showScore: true, 181 | scoreValue: widget 182 | .roomData 183 | .players[widget.isRoomOwner ? 1 : 0] 184 | .winCount, 185 | ), 186 | ); 187 | }, 188 | ), 189 | // PlayerCard( 190 | // imageUrl: imageUrl, 191 | // name: "Opponent", 192 | // showScore: true, 193 | // scoreValue: 0, 194 | // ), 195 | ], 196 | ), 197 | const VerticalSpacer(16), 198 | AnimationOnWidget( 199 | useIncomingEffect: true, 200 | incomingEffect: 201 | WidgetTransitionEffects.incomingScaleDown( 202 | delay: const Duration(milliseconds: 800), 203 | curve: Curves.fastOutSlowIn, 204 | ), 205 | doStateChange: true, 206 | child: Padding( 207 | padding: const EdgeInsets.all(32), 208 | child: Container( 209 | width: kIsWeb ? 500 : null, 210 | height: kIsWeb ? 500 : null, 211 | alignment: Alignment.center, 212 | padding: const EdgeInsets.all(2), 213 | decoration: BoxDecoration( 214 | color: themeProvider.primaryColor, 215 | borderRadius: BorderRadius.circular(16), 216 | ), 217 | child: GridView.builder( 218 | padding: const EdgeInsets.all(0), 219 | physics: const NeverScrollableScrollPhysics(), 220 | shrinkWrap: true, 221 | gridDelegate: 222 | SliverGridDelegateWithFixedCrossAxisCount( 223 | crossAxisCount: 224 | getBoardSize(widget.roomData.board), 225 | mainAxisSpacing: 1, 226 | crossAxisSpacing: 1, 227 | // childAspectRatio: 1, 228 | ), 229 | itemCount: widget.roomData.board.length, 230 | itemBuilder: (context, index) { 231 | return GestureDetector( 232 | onTap: () { 233 | // print(widget.roomData.board); 234 | // result = checkWin( 235 | // widget.roomData.board, 236 | // PlaySymbol.inNum(widget.roomData.turn), 237 | // ); 238 | if (widget.roomData.board[index] == 0 && 239 | widget.roomData.turn == 240 | widget 241 | .roomData 242 | .players[ 243 | !widget.isRoomOwner ? 1 : 0] 244 | .chose) { 245 | if (!kIsWeb) { 246 | Vibration.vibrate( 247 | duration: 80, amplitude: 120); 248 | } 249 | FirebaseDatabase.instance 250 | .ref( 251 | "$roomPath${widget.roomData.code}/board/$index", 252 | ) 253 | .set(PlaySymbol.inNum( 254 | widget.roomData.turn)); 255 | FirebaseDatabase.instance 256 | .ref( 257 | "$roomPath${widget.roomData.code}/turn", 258 | ) 259 | .set( 260 | widget.roomData.turn == PlaySymbol.x 261 | ? PlaySymbol.o 262 | : PlaySymbol.x, 263 | ); 264 | } 265 | }, 266 | child: Container( 267 | decoration: BoxDecoration( 268 | color: widget.result.positions 269 | .contains(index) 270 | ? Colors.deepOrange.withOpacity(0.8) 271 | : themeProvider.bgColor, 272 | border: Border.all( 273 | color: themeProvider.primaryColor, 274 | // width: 2, 275 | ), 276 | borderRadius: 277 | gameProvider.corners.contains(index) 278 | ? gameProvider.borders[index] 279 | : null, 280 | ), 281 | child: Center( 282 | child: Text( 283 | widget.roomData.board[index] == 284 | PlaySymbol.xInt 285 | ? PlaySymbol.x 286 | : widget.roomData.board[index] == 287 | PlaySymbol.oInt 288 | ? PlaySymbol.o 289 | : "", 290 | style: GoogleFonts.hennyPenny( 291 | fontSize: 42 - 8, 292 | color: widget.result.positions 293 | .contains(index) 294 | ? themeProvider.bgColor 295 | : themeProvider.primaryColor, 296 | ), 297 | ), 298 | ), 299 | ), 300 | ); 301 | }, 302 | ), 303 | ), 304 | ), 305 | ), 306 | const VerticalSpacer(4), 307 | AnimationOnWidget( 308 | msDelay: 1200, 309 | doStateChange: true, 310 | hasRestEffect: true, 311 | incomingEffect: WidgetTransitionEffects.incomingScaleUp( 312 | delay: const Duration(milliseconds: 1200), 313 | curve: Curves.easeInOut, 314 | ), 315 | child: Container( 316 | padding: const EdgeInsets.symmetric( 317 | horizontal: 16, vertical: 8), 318 | decoration: BoxDecoration( 319 | color: themeProvider.secondaryColor, 320 | borderRadius: BorderRadius.circular(12), 321 | ), 322 | child: Text( 323 | (widget.roomData.turn == 324 | widget 325 | .roomData 326 | .players[!widget.isRoomOwner ? 1 : 0] 327 | .chose) 328 | ? "Your turn" 329 | : "Opponent turn", 330 | style: TextStyle( 331 | fontSize: defaultTextSize, 332 | color: themeProvider.bgColor, 333 | ), 334 | ), 335 | ), 336 | ), 337 | // const VerticalSpacer(8), 338 | // Text( 339 | // widget.roomData.players[!widget.isRoomOwner ? 1 : 0].chose, 340 | // style: GoogleFonts.hennyPenny( 341 | // fontSize: 32, 342 | // color: primaryColor, 343 | // ), 344 | // ), 345 | ], 346 | ), 347 | ), 348 | ), 349 | MyIconButton( 350 | msDelay: 1200, 351 | iconData: Icons.arrow_back_ios_new_rounded, 352 | onPressed: () { 353 | FullScreenAd.object.show(); 354 | 355 | PopUp.show( 356 | context, 357 | title: "Warning", 358 | description: "Are you sure want to leave the game?", 359 | button1Text: "Yes", 360 | button2Text: "No", 361 | barrierDismissible: false, 362 | button1OnPressed: () async { 363 | navigation.goBack(context); 364 | // To remove GameController Widget 365 | await navigation.changeScreenReplacement( 366 | const RoomScreen(), 367 | widget, 368 | ); 369 | }, 370 | button2OnPressed: () { 371 | Navigator.pop(context); 372 | }, 373 | ); 374 | }, 375 | ), 376 | ], 377 | ), 378 | bottomNavigationBar: ad.showBanner(), 379 | ); 380 | }, 381 | ); 382 | } 383 | } 384 | --------------------------------------------------------------------------------