diff --git a/.gitignore b/.gitignore index 91c2dfe..81e5c37 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ /build/ # Web related lib/generated_plugin_registrant.dart +lib/firebase_options.dart # Symbolication related app.*.symbols diff --git a/CHANGELOG.md b/CHANGELOG.md index 99eea77..d023992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.0 + + - Firebase integrated for community scores + - Personal best scores updated in firestore + ## 0.4.4 - Refined game state mechanic diff --git a/README.md b/README.md index 833b39b..dc1db47 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The live build is available to try [here](https://n-puzzle-solver-1.web.app/) - The solving function used here incorporates IDA* algorithm (with pattern database heuristic) written in python (code modified from [Michael Schrandt](https://github.com/mschrandt/NPuzzle)) which is executed in Google Cloud Run and is accessed via http requests. After trying to implement the algorithm in dart and realising that it could result in potential UI freezes in web along with many other technical problems, I decided to outsource the computational task. For a puzzle of grid size 3, it is able to solve the puzzle within seconds. Grid size of 4 takes quite some time and 5 is beyond the scope of the algorithm which is why I had to disable the solve button for it. The solution is definitely not optimal (usually 50+ moves) and I am not even trying to go for it because it will take a lot more time (sometimes over 2 minutes to solve with manhattan distance heuristic). Moreover, the player wouldn't even understand the moves the AI makes so it is not ideal to go for the optimal solutions anyway. It is a pure aesthetic feature that just looks "cool" and is extremely satisfying to watch. ## 🛠️ Building and Compiling -This project was created in Flutter 2.8.0 but the final build is produced with the latest version of 2.10.1. Please follow the official [documentation](https://docs.flutter.dev/get-started/install) to set up flutter in your system before proceeding. Clone the repository and open it in terminal/cmd/powershell. Run the following commands to get the app running: +This project was created in Flutter 2.8.0 but the final build is produced with the latest version of 2.10.1. Please follow the official [documentation](https://docs.flutter.dev/get-started/install) to set up flutter in your system before proceeding. It also uses Firebase to access the community scores. Please setup a firebase project for your app by following the [FlutterFire documentations](https://firebase.flutter.dev/docs/overview/#installation). Clone the repository and open it in terminal/cmd/powershell. Run the following commands to get the app running: `flutter pub get` `flutter run -d chrome` ### Important! diff --git a/lib/code/auth.dart b/lib/code/auth.dart new file mode 100644 index 0000000..e394e11 --- /dev/null +++ b/lib/code/auth.dart @@ -0,0 +1,58 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:slide_puzzle/code/models.dart'; + +class AuthService { + // static final AuthService instance = AuthService._init(); + // AuthService._init(); + final FirebaseAuth _auth = FirebaseAuth.instance; + + Stream? user; + + AuthService() { + user = _auth.authStateChanges(); + } + + signInAnonymously() async { + print("signing in"); + UserCredential userCredential = await _auth.signInAnonymously(); + if (userCredential.additionalUserInfo!.isNewUser) { + DatabaseService.instance.createUser(userCredential.user!.uid); + } else { + DatabaseService.instance.updateLastSeen(userCredential.user!.uid); + } + return userCredential; + } +} + +class DatabaseService { + static final DatabaseService instance = DatabaseService._init(); + DatabaseService._init(); + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + createUser(String uid) { + print("creating user"); + final userData = UserData.newUser(uid); + // UserData(uid: uid, move3: 0, time3: 0, lastSeen: Timestamp.now()); + _firestore.collection("users").doc(uid).set(userData.toMap()); + } + + updateUserData(UserData userData) { + _firestore.collection("users").doc(userData.uid).set(userData.toMap()); + } + + updateLastSeen(String uid) { + _firestore.collection("users").doc(uid).set({ + "uid": uid, + "lastSeen": Timestamp.now(), + }, SetOptions(merge: true)); + } + + Stream currentUser(String uid) { + return _firestore + .collection("users") + .doc(uid) + .snapshots() + .map((event) => UserData.fromMap((event.data()!))); + } +} diff --git a/lib/code/models.dart b/lib/code/models.dart index f0949ba..d9ceff1 100644 --- a/lib/code/models.dart +++ b/lib/code/models.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; class TilesModel { @@ -36,3 +39,68 @@ class TweenModel { } enum Direction { left, right, up, down } + +class UserData { + String uid; + Map moves; + Map times; + Timestamp lastSeen; + UserData({ + required this.uid, + required this.moves, + required this.times, + required this.lastSeen, + }); + + Map toMap() { + return { + 'uid': uid, + 'moves': moves, + 'times': times, + 'lastSeen': lastSeen, + }; + } + + factory UserData.newUser(String uid) { + return UserData( + uid: uid, + moves: { + "three": 0, + "four": 0, + }, + times: { + "three": 0, + "four": 0, + }, + lastSeen: Timestamp.now(), + ); + } + + factory UserData.fromMap(Map map) { + return UserData( + uid: map['uid'] ?? '', + moves: Map.from(map['moves']), + times: Map.from(map['times']), + lastSeen: (map['lastSeen']) ?? Timestamp.now(), + ); + } + + String toJson() => json.encode(toMap()); + + factory UserData.fromJson(String source) => + UserData.fromMap(json.decode(source)); + + UserData copyWith({ + String? uid, + Map? moves, + Map? times, + Timestamp? lastSeen, + }) { + return UserData( + uid: uid ?? this.uid, + moves: moves ?? this.moves, + times: times ?? this.times, + lastSeen: lastSeen ?? this.lastSeen, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index d80f6f6..410984d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,23 @@ import 'dart:math'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; +import 'package:slide_puzzle/code/auth.dart'; import 'package:slide_puzzle/code/constants.dart'; import 'package:slide_puzzle/code/models.dart'; import 'package:slide_puzzle/code/providers.dart'; +import 'package:slide_puzzle/firebase_options.dart'; import 'package:slide_puzzle/screen/app.dart'; import 'package:provider/provider.dart'; import 'package:slide_puzzle/screen/landing.dart'; +import 'package:rxdart/rxdart.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); runApp(const MyApp()); } @@ -35,6 +44,20 @@ class MyApp extends StatelessWidget { // Provider>.value( // value: list, // ), + StreamProvider.value( + initialData: null, value: AuthService().user), + StreamProvider.value( + initialData: null, + catchError: (_, __) { + print("error at $__"); + }, + value: AuthService().user!.transform( + FlatMapStreamTransformer( + (firebaseUser) => + DatabaseService.instance.currentUser(firebaseUser!.uid), + ), + ), + ), ChangeNotifierProvider(create: (context) => TileProvider()), ChangeNotifierProvider(create: (context) => TweenProvider()), ChangeNotifierProvider(create: (context) => ConfigProvider()), diff --git a/lib/screen/app.dart b/lib/screen/app.dart index a790d3a..59bb591 100644 --- a/lib/screen/app.dart +++ b/lib/screen/app.dart @@ -85,7 +85,11 @@ class _LayoutPageState extends State { if (shuffle) scoreProvider.beginTimer(); var duration = Duration(milliseconds: isChangingGrid ? 0 : 500); configProvider.setDuration(duration, curve: Curves.easeInOutBack); - if (shuffle) configProvider.start(); + if (shuffle) { + configProvider.start(); + } else { + configProvider.wait(); + } Future.delayed(isChangingGrid ? Duration(milliseconds: 10) : duration) .then((value) => configProvider.resetDuration()); } diff --git a/lib/screen/landing.dart b/lib/screen/landing.dart index 7c390dd..7db312f 100644 --- a/lib/screen/landing.dart +++ b/lib/screen/landing.dart @@ -1,6 +1,10 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:provider/provider.dart'; +import 'package:slide_puzzle/code/auth.dart'; import 'package:slide_puzzle/code/constants.dart'; +import 'package:slide_puzzle/code/models.dart'; import 'package:slide_puzzle/screen/app.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -39,6 +43,8 @@ class _LandingPageState extends State } } + _authenticateUser() async {} + @override void initState() { super.initState(); @@ -49,10 +55,19 @@ class _LandingPageState extends State value: 0, lowerBound: 0, upperBound: 1); + + User? user = context.read(); + if (user == null) AuthService().signInAnonymously(); } @override Widget build(BuildContext context) { + UserData? userData = context.watch(); + if (userData != null) { + print("user found: ${userData.toMap()}"); + } else { + print("no user found"); + } return Scaffold( backgroundColor: secondaryColor, body: ScaleTransition( diff --git a/lib/screen/puzzle.dart b/lib/screen/puzzle.dart index f39b704..01c6c79 100644 --- a/lib/screen/puzzle.dart +++ b/lib/screen/puzzle.dart @@ -1,9 +1,11 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:math'; import 'package:provider/provider.dart'; import 'package:slide_puzzle/code/audio.dart'; +import 'package:slide_puzzle/code/auth.dart'; import 'package:slide_puzzle/code/constants.dart'; import 'package:slide_puzzle/code/models.dart'; import 'package:slide_puzzle/code/providers.dart'; @@ -111,6 +113,7 @@ class _PuzzleState extends State { _checkIfSolved(List tileList, ScoreProvider scoreProvider, ConfigProvider configProvider) { + UserData? userData = context.read(); isSolved = Service().isSolved(tileList); bool aiSolved = configProvider.gamestate == GameState.aiSolving; if (isSolved && @@ -119,10 +122,46 @@ class _PuzzleState extends State { print("Solved!!"); scoreProvider.stopTimer(); configProvider.finish(); + print(gridSize); + if (!aiSolved) { + _calculateAndSubmitScore(gridSize, scoreProvider); + } _launchScoreBoard(aiSolved, scoreProvider); } } + _calculateAndSubmitScore( + int gridSize, + ScoreProvider scoreProvider, + ) { + UserData? userData = context.read(); + // Map score = userData!.moves; + String grid = gridSize == 3 ? "three" : "four"; + Map allMoves = userData!.moves; + Map allTimes = userData.times; + int bestMove = allMoves[grid]!; + int bestTime = allTimes[grid]!; + int currentMove = scoreProvider.moves; + int currentTime = scoreProvider.seconds; + if (currentMove < bestMove || + currentTime < bestTime || + bestMove == 0 || + bestTime == 0) { + allMoves[grid] = bestMove == 0 ? currentMove : min(bestMove, currentMove); + allTimes[grid] = bestTime == 0 ? currentTime : min(bestTime, currentTime); + print(allTimes); + print(allMoves); + final newData = userData.copyWith( + uid: userData.uid, + moves: allMoves, + times: allTimes, + lastSeen: Timestamp.now(), + ); + // print(newData.toString()); + DatabaseService.instance.updateUserData(newData); + } + } + _launchScoreBoard(bool aiSolved, ScoreProvider scoreProvider) async { await Future.delayed(Duration(milliseconds: 100)); showDialog( diff --git a/lib/ui/Scoreboard.dart b/lib/ui/Scoreboard.dart new file mode 100644 index 0000000..d1c7039 --- /dev/null +++ b/lib/ui/Scoreboard.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ScoreBoard extends StatefulWidget { + const ScoreBoard({Key? key}) : super(key: key); + + @override + _ScoreBoardState createState() => _ScoreBoardState(); +} + +class _ScoreBoardState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 118d67d..6403364 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.8" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.4.13" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.8" collection: dependency: transitive description: @@ -106,6 +127,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.7" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.11" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.7" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.12.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.4" flutter: dependency: "direct main" description: flutter @@ -163,6 +226,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.1" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e0b33aa..bbcef85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.4.4+6 +version: 0.5.0+7 environment: sdk: ">=2.15.1 <3.0.0" @@ -46,6 +46,9 @@ dependencies: rive: any package_info_plus: ^1.4.0 url_launcher: any + firebase_core: ^1.12.0 + firebase_auth: ^3.3.7 + cloud_firestore: ^3.1.8 dev_dependencies: flutter_test: