From 23c3cf4173c2789228fb8e91548c499828e4d439 Mon Sep 17 00:00:00 2001 From: Yasien Mac Mini Date: Wed, 29 Oct 2025 13:06:55 +0200 Subject: [PATCH] NEW: MIH MineSweeper Package pt2 --- .../mih_objects/minesweeper_player_score.dart | 42 ++ .../mih_dropdwn_field.dart | 3 + .../mih_mine_sweeper_provider.dart | 34 +- Frontend/lib/mih_config/mih_go_router.dart | 2 +- .../build_minesweeper_leaderboard_list.dart | 88 ++++ .../builders/build_my_scoreboard_list.dart | 70 +++ .../components/leaderboard_user_ranking.dart | 85 ++++ .../mih_mine_sweeper_start_game_window.dart | 174 +++++++ .../mine_sweeper/mih_mine_sweeper.dart | 34 +- .../package_tiles/mih_mine_sweeper_tile.dart | 4 +- .../mih_mine_sweeper_leader_board.dart | 202 ++++++++ .../package_tools/mine_sweeper_game.dart | 446 ++++++++++++------ .../mine_sweeper_quick_start_guide.dart | 279 +++++++++++ .../package_tools/my_score_board.dart | 212 +++++++++ .../mih_minesweeper_services.dart | 80 ++++ backend/.DS_Store | Bin 8196 -> 10244 bytes backend/main.py | 2 + backend/mih_database/mihDbObjects.py | 23 +- backend/routers/mine_sweeper_leaderboard.py | 171 +++++++ 19 files changed, 1805 insertions(+), 146 deletions(-) create mode 100644 Frontend/lib/mih_components/mih_objects/minesweeper_player_score.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/builders/build_minesweeper_leaderboard_list.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/builders/build_my_scoreboard_list.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/components/leaderboard_user_ranking.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/components/mih_mine_sweeper_start_game_window.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/package_tools/mih_mine_sweeper_leader_board.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_quick_start_guide.dart create mode 100644 Frontend/lib/mih_packages/mine_sweeper/package_tools/my_score_board.dart create mode 100644 Frontend/lib/mih_services/mih_minesweeper_services.dart create mode 100644 backend/routers/mine_sweeper_leaderboard.py diff --git a/Frontend/lib/mih_components/mih_objects/minesweeper_player_score.dart b/Frontend/lib/mih_components/mih_objects/minesweeper_player_score.dart new file mode 100644 index 00000000..9a1dad89 --- /dev/null +++ b/Frontend/lib/mih_components/mih_objects/minesweeper_player_score.dart @@ -0,0 +1,42 @@ +class MinesweeperPlayerScore { + String app_id; + String username; + String proPicUrl; + String difficulty; + String game_time; + double game_score; + DateTime played_date; + + MinesweeperPlayerScore({ + required this.app_id, + required this.username, + required this.proPicUrl, + required this.difficulty, + required this.game_time, + required this.game_score, + required this.played_date, + }); + + factory MinesweeperPlayerScore.fromJson(Map json) { + return MinesweeperPlayerScore( + app_id: json['app_id'], + username: json['username'], + proPicUrl: json['proPicUrl'], + difficulty: json['difficulty'], + game_time: json['game_time'], + game_score: json['game_score'], + played_date: DateTime.parse(json['played_date']), + ); + } + + Map toJson() { + return { + 'app_id': app_id, + 'username': username, + 'proPicUrl': proPicUrl, + 'difficulty': difficulty, + 'game_time': game_score, + 'played_date': played_date.toIso8601String(), + }; + } +} diff --git a/Frontend/lib/mih_components/mih_package_components/mih_dropdwn_field.dart b/Frontend/lib/mih_components/mih_package_components/mih_dropdwn_field.dart index 9bd6d44f..8bb9a88d 100644 --- a/Frontend/lib/mih_components/mih_package_components/mih_dropdwn_field.dart +++ b/Frontend/lib/mih_components/mih_package_components/mih_dropdwn_field.dart @@ -10,6 +10,7 @@ class MihDropdownField extends StatefulWidget { final bool editable; final bool enableSearch; final FormFieldValidator? validator; + final Function(String?)? onSelected; const MihDropdownField({ super.key, @@ -20,6 +21,7 @@ class MihDropdownField extends StatefulWidget { required this.editable, required this.enableSearch, this.validator, + this.onSelected, }); @override @@ -153,6 +155,7 @@ class _MihDropdownFieldState extends State { ), onSelected: (String? selectedValue) { field.didChange(selectedValue); + widget.onSelected?.call(selectedValue); }, menuStyle: MenuStyle( backgroundColor: WidgetStatePropertyAll( diff --git a/Frontend/lib/mih_components/mih_providers/mih_mine_sweeper_provider.dart b/Frontend/lib/mih_components/mih_providers/mih_mine_sweeper_provider.dart index f6d3ea90..50b58831 100644 --- a/Frontend/lib/mih_components/mih_providers/mih_mine_sweeper_provider.dart +++ b/Frontend/lib/mih_components/mih_providers/mih_mine_sweeper_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/minesweeper_player_score.dart'; class MihMineSweeperProvider extends ChangeNotifier { String difficulty; @@ -6,9 +7,12 @@ class MihMineSweeperProvider extends ChangeNotifier { int rowCount; int columnCount; int totalMines; + List? leaderboard; + List? myScoreboard; + List?> leaderboardUserPictures = []; MihMineSweeperProvider({ - this.difficulty = "Normal", + this.difficulty = "Easy", this.toolIndex = 0, this.rowCount = 10, this.columnCount = 10, @@ -16,7 +20,7 @@ class MihMineSweeperProvider extends ChangeNotifier { }); void reset() { - difficulty = "Normal"; + difficulty = "Easy"; toolIndex = 0; rowCount = 10; columnCount = 10; @@ -48,4 +52,30 @@ class MihMineSweeperProvider extends ChangeNotifier { this.totalMines = totalMines; notifyListeners(); } + + void setLeaderboard({required List? leaderboard}) { + if (leaderboard == null) { + this.leaderboard = []; + } else { + this.leaderboard = leaderboard; + } + notifyListeners(); + } + + void setMyScoreboard({ + required List? myScoreboard, + }) { + if (myScoreboard == null) { + this.myScoreboard = []; + } else { + this.myScoreboard = myScoreboard; + } + notifyListeners(); + } + + void setLeaderboardUserPictures( + {required List?> leaderboardUserPictures}) { + this.leaderboardUserPictures = leaderboardUserPictures; + notifyListeners(); + } } diff --git a/Frontend/lib/mih_config/mih_go_router.dart b/Frontend/lib/mih_config/mih_go_router.dart index 9dae1781..e0963b7c 100644 --- a/Frontend/lib/mih_config/mih_go_router.dart +++ b/Frontend/lib/mih_config/mih_go_router.dart @@ -422,7 +422,7 @@ class MihGoRouter { return MIHPrintPreview(arguments: args!); }, ), - // ========================== MIH Calculator ================================== + // ========================== MIH Minesweeper ================================== GoRoute( name: "mihMinesweeper", path: MihGoRouterPaths.mihMineSweeper, diff --git a/Frontend/lib/mih_packages/mine_sweeper/builders/build_minesweeper_leaderboard_list.dart b/Frontend/lib/mih_packages/mine_sweeper/builders/build_minesweeper_leaderboard_list.dart new file mode 100644 index 00000000..cc622ed0 --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/builders/build_minesweeper_leaderboard_list.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_circle_avatar.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_profile_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:provider/provider.dart'; + +class BuildMinesweeperLeaderboardList extends StatefulWidget { + const BuildMinesweeperLeaderboardList({super.key}); + + @override + State createState() => + _BuildMinesweeperLeaderboardListState(); +} + +class _BuildMinesweeperLeaderboardListState + extends State { + @override + Widget build(BuildContext context) { + return Consumer2( + builder: (BuildContext context, MzansiProfileProvider profileProvider, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (BuildContext context, index) { + return Divider( + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ); + }, + itemCount: mineSweeperProvider.leaderboard!.length, + itemBuilder: (context, index) { + return ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "#${index + 1}", + style: TextStyle( + fontSize: 25, + ), + ), + const SizedBox(width: 10), + MihCircleAvatar( + key: UniqueKey(), + imageFile: + mineSweeperProvider.leaderboardUserPictures.isNotEmpty + ? mineSweeperProvider.leaderboardUserPictures[index] + : null, + width: 60, + editable: false, + fileNameController: null, + userSelectedfile: null, + frameColor: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + backgroundColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + onChange: () {}, + ), + ], + ), + title: Text( + "${mineSweeperProvider.leaderboard![index].username}${profileProvider.user!.username == mineSweeperProvider.leaderboard![index].username ? " (You)" : ""}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + subtitle: Text( + "Score: ${mineSweeperProvider.leaderboard![index].game_score}\nTime: ${mineSweeperProvider.leaderboard![index].game_time}", + style: TextStyle( + fontSize: 18, + // fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/builders/build_my_scoreboard_list.dart b/Frontend/lib/mih_packages/mine_sweeper/builders/build_my_scoreboard_list.dart new file mode 100644 index 00000000..9b877c06 --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/builders/build_my_scoreboard_list.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_profile_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:provider/provider.dart'; + +class BuildMyScoreBoardList extends StatefulWidget { + const BuildMyScoreBoardList({super.key}); + + @override + State createState() => + _BuildMinesweeperLeaderboardListState(); +} + +class _BuildMinesweeperLeaderboardListState + extends State { + @override + Widget build(BuildContext context) { + return Consumer2( + builder: (BuildContext context, MzansiProfileProvider profileProvider, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (BuildContext context, index) { + return Divider( + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ); + }, + itemCount: mineSweeperProvider.myScoreboard!.length, + itemBuilder: (context, index) { + return ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "#${index + 1}", + style: TextStyle( + fontSize: 25, + ), + ), + ], + ), + title: Text( + "Score: ${mineSweeperProvider.myScoreboard![index].game_score}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + subtitle: Text( + "Time: ${mineSweeperProvider.myScoreboard![index].game_time}", + style: TextStyle( + fontSize: 18, + // fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/components/leaderboard_user_ranking.dart b/Frontend/lib/mih_packages/mine_sweeper/components/leaderboard_user_ranking.dart new file mode 100644 index 00000000..e2c4feb2 --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/components/leaderboard_user_ranking.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:ken_logger/ken_logger.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_circle_avatar.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:redacted/redacted.dart'; + +class LeaderboardUserRanking extends StatelessWidget { + final int index; + final String proPicUrl; + final String username; + final dynamic gameScore; + final String gameTime; + final bool isCurrentUser; + final Future?> Function(String) getUserPicture; + + const LeaderboardUserRanking({ + super.key, + required this.index, + required this.proPicUrl, + required this.username, + required this.gameScore, + required this.gameTime, + required this.isCurrentUser, + required this.getUserPicture, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: getUserPicture(proPicUrl), + builder: (context, asyncSnapshot) { + bool isLoading = + asyncSnapshot.connectionState == ConnectionState.waiting; + + KenLogger.success("URL: ${asyncSnapshot.data.toString()}"); + return ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "#${index + 1}", + style: const TextStyle( + fontSize: 25, + ), + ), + const SizedBox(width: 10), + MihCircleAvatar( + key: ValueKey(asyncSnapshot.data + .toString()), // Use ValueKey for stable identity + imageFile: asyncSnapshot.data, + width: 60, + editable: false, + fileNameController: null, + userSelectedfile: null, + frameColor: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + backgroundColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + onChange: () {}, + ), + ], + ), + title: Text( + "$username${isCurrentUser ? " (You)" : ""}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ).redacted(context: context, redact: isLoading), + subtitle: Text( + "Score: $gameScore\nTime: $gameTime", + style: TextStyle( + fontSize: 18, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ).redacted(context: context, redact: isLoading), + ); + }, + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/components/mih_mine_sweeper_start_game_window.dart b/Frontend/lib/mih_packages/mine_sweeper/components/mih_mine_sweeper_start_game_window.dart new file mode 100644 index 00000000..3ec5092e --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/components/mih_mine_sweeper_start_game_window.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_button.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_dropdwn_field.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_form.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_window.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:provider/provider.dart'; + +class MihMineSweeperStartGameWindow extends StatefulWidget { + final void Function()? onPressed; + const MihMineSweeperStartGameWindow({ + super.key, + required this.onPressed, + }); + + @override + State createState() => + _MihMineSweeperStartGameWindowState(); +} + +class _MihMineSweeperStartGameWindowState + extends State { + TextEditingController modeController = TextEditingController(); + final _formKey = GlobalKey(); + + void applyGameSettings(MihMineSweeperProvider mihMineSweeperProvider) { + mihMineSweeperProvider.setDifficulty(modeController.text); + switch (mihMineSweeperProvider.difficulty) { + case ("Very Easy"): + mihMineSweeperProvider.setRowCount(6); + mihMineSweeperProvider.setCoulmnCount(6); + mihMineSweeperProvider.setTotalMines(5); + // mihMineSweeperProvider.setRowCount(5); + // mihMineSweeperProvider.setCoulmnCount(5); + // mihMineSweeperProvider.setTotalMines(3); + break; + case ("Easy"): + mihMineSweeperProvider.setRowCount(8); + mihMineSweeperProvider.setCoulmnCount(8); + mihMineSweeperProvider.setTotalMines(10); + // mihMineSweeperProvider.setRowCount(10); + // mihMineSweeperProvider.setCoulmnCount(10); + // mihMineSweeperProvider.setTotalMines(15); + break; + case ("Intermediate"): + mihMineSweeperProvider.setRowCount(10); + mihMineSweeperProvider.setCoulmnCount(10); + mihMineSweeperProvider.setTotalMines(18); + // mihMineSweeperProvider.setRowCount(15); + // mihMineSweeperProvider.setCoulmnCount(10); + // mihMineSweeperProvider.setTotalMines(23); + break; + case ("Hard"): + mihMineSweeperProvider.setRowCount(12); + mihMineSweeperProvider.setCoulmnCount(10); + mihMineSweeperProvider.setTotalMines(30); + // mihMineSweeperProvider.setRowCount(20); + // mihMineSweeperProvider.setCoulmnCount(10); + // mihMineSweeperProvider.setTotalMines(30); + break; + default: + break; + } + } + + String getModeConfig() { + switch (modeController.text) { + case ("Very Easy"): + return "Columns: 6\nRows: 6\nBombs: 5"; + case ("Easy"): + return "Columns: 8\nRows: 8\nBombs: 10"; + case ("Intermediate"): + return "Columns: 10\nRows: 10\nBombs: 18"; + case ("Hard"): + return "Columns: 10\nRows: 12\nBombs: 30"; + default: + return "Error"; + } + } + + void _onModeChanged() { + setState(() {}); + } + + @override + void dispose() { + modeController.removeListener(_onModeChanged); + modeController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + modeController.text = context.read().difficulty; + modeController.addListener(_onModeChanged); + } + + @override + Widget build(BuildContext context) { + return MihPackageWindow( + fullscreen: false, + windowTitle: "New Game Settings", + onWindowTapClose: () { + context.pop(); + }, + windowBody: Consumer( + builder: (BuildContext context, + MihMineSweeperProvider mihMineSweeperProvider, Widget? child) { + return Column( + children: [ + MihForm( + formKey: _formKey, + formFields: [ + MihDropdownField( + controller: modeController, + hintText: "Difficulty", + dropdownOptions: [ + "Very Easy", + "Easy", + "Intermediate", + "Hard" + ], + requiredText: true, + editable: true, + enableSearch: false, + ), + const SizedBox(height: 10), + Text( + getModeConfig(), + style: TextStyle( + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 25), + Center( + child: MihButton( + onPressed: () { + applyGameSettings(mihMineSweeperProvider); + context.pop(); + widget.onPressed?.call(); + }, + buttonColor: MihColors.getGreenColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + width: 300, + child: Text( + "Start Game", + style: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/mih_mine_sweeper.dart b/Frontend/lib/mih_packages/mine_sweeper/mih_mine_sweeper.dart index b0659619..ef545c7a 100644 --- a/Frontend/lib/mih_packages/mine_sweeper/mih_mine_sweeper.dart +++ b/Frontend/lib/mih_packages/mine_sweeper/mih_mine_sweeper.dart @@ -4,8 +4,12 @@ import 'package:go_router/go_router.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_action.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tools.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_banner_ad_provider.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/package_tools/mih_mine_sweeper_leader_board.dart'; import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/package_tools/mine_sweeper_game.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/package_tools/mine_sweeper_quick_start_guide.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/package_tools/my_score_board.dart'; import 'package:provider/provider.dart'; class MihMineSweeper extends StatefulWidget { @@ -16,6 +20,14 @@ class MihMineSweeper extends StatefulWidget { } class _MihMineSweeperState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + context.read().loadBannerAd(); + }); + } + @override Widget build(BuildContext context) { return MihPackage( @@ -35,9 +47,12 @@ class _MihMineSweeperState extends State { icon: const Icon(Icons.arrow_back), iconSize: 35, onTap: () { + MihMineSweeperProvider mineSweeperProvider = + context.read(); + mineSweeperProvider.setToolIndex(0); + mineSweeperProvider.setDifficulty("Easy"); context.goNamed( 'mihHome', - extra: true, ); FocusScope.of(context).unfocus(); }, @@ -49,6 +64,15 @@ class _MihMineSweeperState extends State { temp[const Icon(FontAwesomeIcons.bomb)] = () { context.read().setToolIndex(0); }; + temp[const Icon(Icons.leaderboard_rounded)] = () { + context.read().setToolIndex(1); + }; + temp[const Icon(Icons.perm_identity_rounded)] = () { + context.read().setToolIndex(2); + }; + temp[const Icon(Icons.rule_rounded)] = () { + context.read().setToolIndex(3); + }; return MihPackageTools( tools: temp, selcetedIndex: context.watch().toolIndex, @@ -57,7 +81,10 @@ class _MihMineSweeperState extends State { List getToolTitle() { List toolTitles = [ - "MineSweeper", + "Minesweeper", + "Leader Board", + "My Scores", + "Guide", ]; return toolTitles; } @@ -65,6 +92,9 @@ class _MihMineSweeperState extends State { List getToolBody() { List toolBodies = [ const MineSweeperGame(), + const MihMineSweeperLeaderBoard(), + const MyScoreBoard(), + const MineSweeperQuickStartGuide(), ]; return toolBodies; } diff --git a/Frontend/lib/mih_packages/mine_sweeper/package_tiles/mih_mine_sweeper_tile.dart b/Frontend/lib/mih_packages/mine_sweeper/package_tiles/mih_mine_sweeper_tile.dart index 81cab8fc..9654cfcb 100644 --- a/Frontend/lib/mih_packages/mine_sweeper/package_tiles/mih_mine_sweeper_tile.dart +++ b/Frontend/lib/mih_packages/mine_sweeper/package_tiles/mih_mine_sweeper_tile.dart @@ -24,10 +24,10 @@ class _MihMineSweeperTileState extends State { return MihPackageTile( onTap: () { context.goNamed( - "mihMineSweeper", + "mihMinesweeper", ); }, - appName: "MineSweeper", + appName: "Minesweeper", appIcon: Icon( MihIcons.mineSweeper, color: MihColors.getSecondaryColor( diff --git a/Frontend/lib/mih_packages/mine_sweeper/package_tools/mih_mine_sweeper_leader_board.dart b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mih_mine_sweeper_leader_board.dart new file mode 100644 index 00000000..5e3769c1 --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mih_mine_sweeper_leader_board.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:ken_logger/ken_logger.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_dropdwn_field.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tool_body.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/builders/build_minesweeper_leaderboard_list.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_file_services.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_minesweeper_services.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_validation_services.dart'; +import 'package:provider/provider.dart'; + +class MihMineSweeperLeaderBoard extends StatefulWidget { + const MihMineSweeperLeaderBoard({super.key}); + + @override + State createState() => + _MihMineSweeperLeaderBoardState(); +} + +class _MihMineSweeperLeaderBoardState extends State { + TextEditingController filterController = TextEditingController(); + + Future initialiseLeaderboard() async { + MihMineSweeperProvider mineSweeperProvider = + context.read(); + filterController.text = mineSweeperProvider.difficulty; + if (mineSweeperProvider.leaderboard == null || + mineSweeperProvider.leaderboard!.isEmpty) { + KenLogger.success("getting data"); + await MihMinesweeperServices().getTop20Leaderboard(mineSweeperProvider); + List?> userPictures = []; + String userPicUrl = ""; + for (final ranking in mineSweeperProvider.leaderboard!) { + userPicUrl = + await MihFileApi.getMinioFileUrl(ranking.proPicUrl, context); + userPictures.add(NetworkImage(userPicUrl)); + } + mineSweeperProvider.setLeaderboardUserPictures( + leaderboardUserPictures: userPictures); + } + } + + void refreshLeaderBoard( + MihMineSweeperProvider mineSweeperProvider, String difficulty) { + mineSweeperProvider.setDifficulty(difficulty); + mineSweeperProvider.setLeaderboard(leaderboard: null); + mineSweeperProvider.setMyScoreboard(myScoreboard: null); + initialiseLeaderboard(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await initialiseLeaderboard(); + }); + } + + @override + Widget build(BuildContext context) { + final double width = MediaQuery.sizeOf(context).width; + return Consumer( + builder: (BuildContext context, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + return RefreshIndicator( + onRefresh: () async { + refreshLeaderBoard(mineSweeperProvider, filterController.text); + }, + child: MihPackageToolBody( + borderOn: false, + bodyItem: getBody(width), + ), + ); + }, + ); + } + + Widget getBody(double width) { + return Consumer( + builder: (BuildContext context, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + if (mineSweeperProvider.leaderboard == null) { + return Center( + child: Mihloadingcircle(), + ); + } else { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: width / 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: MihDropdownField( + controller: filterController, + hintText: "Leaderboards", + dropdownOptions: const [ + "Very Easy", + "Easy", + "Intermediate", + "Hard", + ], + requiredText: true, + editable: true, + enableSearch: true, + validator: (value) { + return MihValidationServices().isEmpty(value); + }, + onSelected: (selection) { + refreshLeaderBoard(mineSweeperProvider, selection!); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + mineSweeperProvider.leaderboard!.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Icon( + MihIcons.mineSweeper, + size: 165, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + const SizedBox(height: 10), + Text( + "Be the first on the leaderboard.", + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + ), + const SizedBox(height: 25), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + children: [ + TextSpan(text: "Press "), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + FontAwesomeIcons.bomb, + size: 20, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + ), + TextSpan(text: " and start a new game"), + ], + ), + ), + ), + ], + ), + ) + : BuildMinesweeperLeaderboardList(), + ], + ), + ); + } + }, + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_game.dart b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_game.dart index 01b16f05..6933b0b1 100644 --- a/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_game.dart +++ b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_game.dart @@ -1,22 +1,26 @@ +import 'dart:async'; import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_banner_ad.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_button.dart'; -import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_dropdwn_field.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_floating_menu.dart'; -import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_form.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_alert.dart'; -import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_window.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tool_body.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_single_child_scroll.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_banner_ad_provider.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_profile_provider.dart'; import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/components/board_square.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/components/mih_mine_sweeper_start_game_window.dart'; import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/components/mine_tile.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_minesweeper_services.dart'; import 'package:provider/provider.dart'; class MineSweeperGame extends StatefulWidget { @@ -27,92 +31,115 @@ class MineSweeperGame extends StatefulWidget { } class _MineSweeperGameState extends State { - TextEditingController modeController = TextEditingController(); - final _formKey = GlobalKey(); List> board = []; bool isGameOver = false; bool isGameWon = false; int squaresLeft = -1; - bool _isFirstLoad = true; + Timer? _timer; + int _milliseconds = 0; + bool _isRunning = false; + static const int millisecondsPerUpdate = 10; - String getModeConfig() { - switch (modeController.text) { - case ("Easy"): - return "Columns: 10\nRows: 10\nBomds: 15"; - case ("Normal"): - return "Columns: 10\nRows: 15\nBomds: 23"; - case ("Hard"): - return "Columns: 10\nRows: 20\nBomds: 30"; - default: - return "Error"; + double timeStringToTotalSeconds(String timeString) { + try { + List parts = timeString.split(':'); + if (parts.length < 4) { + return 0.0; + } + double hours = double.parse(parts[0]); + double minutes = double.parse(parts[1]); + double seconds = double.parse(parts[2]); + double milliseconds = double.parse(parts[3]); + double totalSeconds = + (hours * 3600) + (minutes * 60) + seconds + (milliseconds / 1000); + return totalSeconds; + } catch (e) { + print("Error parsing time string: $e"); + return 0.0; } } - void showStartGameWindow(MihMineSweeperProvider mihMineSweeperProvider) { + double calculateGameScore(MihMineSweeperProvider mihMineSweeperProvider) { + int scoreConst = 10000; + double dificusltyMultiplier; + switch (mihMineSweeperProvider.difficulty) { + case ("Very Easy"): + dificusltyMultiplier = 0.5; + break; + case ("Easy"): + dificusltyMultiplier = 1.0; + break; + case ("Intermediate"): + dificusltyMultiplier = 2.5; + break; + case ("Hard"): + dificusltyMultiplier = 5.0; + break; + default: + dificusltyMultiplier = 0.0; + break; + } + double rawScore = (scoreConst * dificusltyMultiplier) / + timeStringToTotalSeconds(_formatTime()); + + String scoreString = rawScore.toStringAsFixed(5); + return double.parse(scoreString); + } + + void startTimer() { + if (_isRunning) return; + _isRunning = true; + _timer = Timer.periodic(const Duration(milliseconds: millisecondsPerUpdate), + (timer) { + setState(() { + _milliseconds += millisecondsPerUpdate; // Increment by the interval + }); + }); + } + + void stopTimer() { + _timer?.cancel(); + setState(() { + _isRunning = false; + }); + } + + void resetTimer() { + stopTimer(); // Stop the timer first + setState(() { + _milliseconds = 0; // Reset the time to zero + }); + } + + String _formatTime() { + Duration duration = Duration(milliseconds: _milliseconds); + final int hours = duration.inHours.remainder(60); + final int minutes = duration.inMinutes.remainder(60); + final int seconds = duration.inSeconds.remainder(60); + final int centiseconds = (duration.inMilliseconds.remainder(1000)) ~/ 10; + String hoursStr = hours.toString().padLeft(2, '0'); + String minutesStr = minutes.toString().padLeft(2, '0'); + String secondsStr = seconds.toString().padLeft(2, '0'); + String centiStr = centiseconds.toString().padLeft(2, '0'); + return '$hoursStr:$minutesStr:$secondsStr:$centiStr'; + } + + void showStartGameWindow(MihMineSweeperProvider mihMineSweeperProvider, + MihBannerAdProvider addProvider) { // easy - 10 * 10 & 15 bombs - // Normal - 10 * 15 & 23 bombs + // Intermediate - 10 * 15 & 23 bombs // Hard - 10 * 20 & 30 bombs + addProvider.loadBannerAd(); showDialog( context: context, builder: (context) { - return MihPackageWindow( - fullscreen: false, - windowTitle: "New Game Settings", - onWindowTapClose: () { - context.pop(); + return MihMineSweeperStartGameWindow( + onPressed: () { + resetTimer(); + mihMineSweeperProvider + .setDifficulty(mihMineSweeperProvider.difficulty); + setState(() => initializeBoard(mihMineSweeperProvider)); }, - windowBody: Column( - children: [ - MihForm( - formKey: _formKey, - formFields: [ - MihDropdownField( - controller: modeController, - hintText: "Difficulty", - dropdownOptions: ["Easy", "Normal", "Hard"], - requiredText: true, - editable: true, - enableSearch: false, - ), - const SizedBox(height: 10), - Text( - getModeConfig(), - style: TextStyle( - color: MihColors.getSecondaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 25), - Center( - child: MihButton( - onPressed: () { - setState( - () => initializeBoard(mihMineSweeperProvider)); - Navigator.of(context).pop(); - }, - buttonColor: MihColors.getGreenColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - width: 300, - child: Text( - "Start Game", - style: TextStyle( - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ], - ), ); }); } @@ -137,6 +164,7 @@ class _MineSweeperGameState extends State { isGameOver = false; isGameWon = false; // You'd typically add a call to setState here, but it's in initState. + startTimer(); } void placeBombs(MihMineSweeperProvider mihMineSweeperProvider) { @@ -213,12 +241,18 @@ class _MineSweeperGameState extends State { } } - void handleTap(MihMineSweeperProvider mihMineSweeperProvider, int r, int c) { + Future handleTap( + MzansiProfileProvider profileProvider, + MihMineSweeperProvider mihMineSweeperProvider, + MihBannerAdProvider adProvider, + int r, + int c) async { if (isGameOver || board[r][c].isOpened || board[r][c].isFlagged) { return; } // 1. Check for bomb (LOSS) if (board[r][c].hasBomb) { + stopTimer(); setState(() { board[r][c].isOpened = true; isGameOver = true; @@ -227,19 +261,34 @@ class _MineSweeperGameState extends State { context: context, builder: (context) { return MihPackageAlert( - alertIcon: Icon( - FontAwesomeIcons.bomb, - color: MihColors.getRedColor( - MzansiInnovationHub.of(context)!.theme.mode == "Dark"), - size: 100, + alertIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + FontAwesomeIcons.bomb, + color: MihColors.getRedColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + size: 100, + ), ), alertTitle: "Better Luck Next Time", alertBody: Column( children: [ Text( "Your lost this game of MIH MineSweeper!!!", + textAlign: TextAlign.center, style: TextStyle( - fontSize: 15, + fontSize: 20, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + ), + const SizedBox(height: 10), + Text( + "Please feel free to start a New Game or check out the Leader Board to find out who's the best in Mzansi.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, color: MihColors.getSecondaryColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), @@ -254,9 +303,9 @@ class _MineSweeperGameState extends State { children: [ MihButton( onPressed: () { - setState( - () => initializeBoard(mihMineSweeperProvider)); - Navigator.of(context).pop(); + context.pop(); + showStartGameWindow( + mihMineSweeperProvider, adProvider); }, buttonColor: MihColors.getGreenColor( MzansiInnovationHub.of(context)!.theme.mode == @@ -273,6 +322,26 @@ class _MineSweeperGameState extends State { ), ), ), + MihButton( + onPressed: () { + context.pop(); + mihMineSweeperProvider.setToolIndex(1); + }, + buttonColor: MihColors.getOrangeColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + width: 300, + child: Text( + "Leader Board", + style: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), ], ), ], @@ -295,7 +364,7 @@ class _MineSweeperGameState extends State { squaresLeft--; } // 3. Check for win - _checkWinCondition(mihMineSweeperProvider); + _checkWinCondition(profileProvider, mihMineSweeperProvider, adProvider); // Update the UI setState(() {}); } @@ -311,9 +380,14 @@ class _MineSweeperGameState extends State { } // --- GAME ACTION LOGIC --- - void _checkWinCondition(MihMineSweeperProvider mihMineSweeperProvider) { + Future _checkWinCondition( + MzansiProfileProvider profileProvider, + MihMineSweeperProvider mihMineSweeperProvider, + MihBannerAdProvider adProvider, + ) async { // Game is won if all non-mine squares are opened. if (squaresLeft <= mihMineSweeperProvider.totalMines) { + stopTimer(); isGameWon = true; isGameOver = true; // win alert @@ -327,13 +401,41 @@ class _MineSweeperGameState extends State { MzansiInnovationHub.of(context)!.theme.mode == "Dark"), size: 100, ), - alertTitle: "Congradulations", + alertTitle: "Congratulations", alertBody: Column( children: [ Text( "Your won this game of MIH MineSweeper!!!", + textAlign: TextAlign.center, style: TextStyle( - fontSize: 15, + fontSize: 20, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + const SizedBox(height: 10), + // Text( + // "You took ${_formatTime()} to complete the game on ${mihMineSweeperProvider.difficulty} mode.", + // style: TextStyle( + // fontSize: 15, + // color: MihColors.getSecondaryColor( + // MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + // ), + // ), + // const SizedBox(height: 10), + Text( + "Time Taken: ${_formatTime().replaceAll("00:", "")}", + style: TextStyle( + fontSize: 20, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + const SizedBox(height: 10), + Text( + "Score: ${calculateGameScore(mihMineSweeperProvider)}", + style: TextStyle( + fontSize: 20, color: MihColors.getSecondaryColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), ), @@ -347,8 +449,8 @@ class _MineSweeperGameState extends State { children: [ MihButton( onPressed: () { - setState(() => initializeBoard(mihMineSweeperProvider)); - Navigator.of(context).pop(); + context.pop(); + showStartGameWindow(mihMineSweeperProvider, adProvider); }, buttonColor: MihColors.getGreenColor( MzansiInnovationHub.of(context)!.theme.mode == @@ -365,6 +467,26 @@ class _MineSweeperGameState extends State { ), ), ), + MihButton( + onPressed: () { + context.pop(); + mihMineSweeperProvider.setToolIndex(1); + }, + buttonColor: MihColors.getOrangeColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + width: 300, + child: Text( + "Leader Board", + style: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), ], ), ], @@ -374,17 +496,35 @@ class _MineSweeperGameState extends State { ); }, ); + showDialog( + context: context, + builder: (context) { + return Mihloadingcircle( + message: "Uploading your score", + ); + }); + await MihMinesweeperServices().addPlayerScore( + profileProvider, + mihMineSweeperProvider, + _formatTime().replaceAll("00:", ""), + calculateGameScore(mihMineSweeperProvider), + ); + context.pop(); } } - Color? getDifficultyColor() { - String mode = modeController.text; + Color? getDifficultyColor(MihMineSweeperProvider mihMineSweeperProvider) { + String mode = mihMineSweeperProvider.difficulty; switch (mode) { - case "Easy": + case "Very Easy": return MihColors.getGreenColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark", ); - case "Normal": + case "Easy": + return MihColors.getGreenColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark", + ); + case "Intermediate": return MihColors.getOrangeColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark", ); @@ -398,42 +538,33 @@ class _MineSweeperGameState extends State { } @override - void initState() { - super.initState(); - modeController.text = "Easy"; - // showStartGameWindow(context.read()); - // initializeBoard(); + void dispose() { + _timer?.cancel(); + super.dispose(); } @override - void didChangeDependencies() { - super.didChangeDependencies(); - // This method is safe for calling showDialog or reading provider values. - if (_isFirstLoad) { - // 1. Get the provider safely. - WidgetsBinding.instance.addPostFrameCallback((_) { - final mihMineSweeperProvider = context.read(); - // board = List.generate( - // mihMineSweeperProvider.rowCount, - // (i) => List.generate( - // mihMineSweeperProvider.columnCount, - // (j) => BoardSquare(), - // ), - // ); - // 2. Show the dialog to get initial game settings. - // The user selection in the dialog will call initializeBoard(). - showStartGameWindow(mihMineSweeperProvider); - }); - // 3. Set flag to prevent showing the dialog on subsequent dependency changes - _isFirstLoad = false; - } + void initState() { + // UBongani was here during the MIH Live + super.initState(); } @override Widget build(BuildContext context) { - return Consumer( + return MihPackageToolBody( + borderOn: false, + bodyItem: getBody(), + ); + } + + Widget getBody() { + return Consumer3( builder: (BuildContext context, - MihMineSweeperProvider mihMineSweeperProvider, Widget? child) { + MzansiProfileProvider profileProvider, + MihMineSweeperProvider mihMineSweeperProvider, + MihBannerAdProvider adProvider, + Widget? child) { return Stack( alignment: Alignment.topCenter, children: [ @@ -456,7 +587,7 @@ class _MineSweeperGameState extends State { ), const SizedBox(height: 10), Text( - "Welcom to MIH MineSweeper, the first game of MIH.", + "Welcom to Minesweeper, the first game of MIH.", textAlign: TextAlign.center, overflow: TextOverflow.visible, style: TextStyle( @@ -495,7 +626,9 @@ class _MineSweeperGameState extends State { "Dark"), ), ), - TextSpan(text: " to start a new game."), + TextSpan( + text: + " to start a new game or learn how to play the minesweeper."), ], ), ), @@ -514,7 +647,8 @@ class _MineSweeperGameState extends State { children: [ Expanded( child: Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.symmetric( + horizontal: 10.0), child: Text( 'Mines: ${mihMineSweeperProvider.totalMines}', textAlign: TextAlign.left, @@ -526,20 +660,29 @@ class _MineSweeperGameState extends State { ), Expanded( child: Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.symmetric( + horizontal: 10.0), child: Text( - modeController.text, + _formatTime().replaceAll("00:", ""), textAlign: TextAlign.right, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: getDifficultyColor(), - ), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold), ), ), ), ], ), + Text( + mihMineSweeperProvider.difficulty, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: getDifficultyColor(mihMineSweeperProvider), + ), + ), + // const SizedBox( // height: 30, // ), @@ -571,13 +714,16 @@ class _MineSweeperGameState extends State { return MineTile( square: board[r][c], - onTap: () => - handleTap(mihMineSweeperProvider, r, c), + onTap: () => handleTap(profileProvider, + mihMineSweeperProvider, adProvider, r, c), onLongPress: () => handleLongPress(r, c), ); }, ), ), + SizedBox(height: 30), + MihBannerAd(), + // const SizedBox(height: 100), ], ), ), @@ -587,6 +733,30 @@ class _MineSweeperGameState extends State { child: MihFloatingMenu( animatedIcon: AnimatedIcons.menu_close, children: [ + SpeedDialChild( + child: Icon( + Icons.add, + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + label: "Learn how to play", + labelBackgroundColor: MihColors.getGreenColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + labelStyle: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontWeight: FontWeight.bold, + ), + backgroundColor: MihColors.getGreenColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + onTap: () { + mihMineSweeperProvider.setToolIndex(2); + }, + ), SpeedDialChild( child: Icon( Icons.add, @@ -610,9 +780,9 @@ class _MineSweeperGameState extends State { MzansiInnovationHub.of(context)!.theme.mode == "Dark"), onTap: () { - showStartGameWindow(mihMineSweeperProvider); + showStartGameWindow(mihMineSweeperProvider, adProvider); }, - ) + ), ]), ) ], diff --git a/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_quick_start_guide.dart b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_quick_start_guide.dart new file mode 100644 index 00000000..4079f2f0 --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/package_tools/mine_sweeper_quick_start_guide.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tool_body.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_single_child_scroll.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; + +class MineSweeperQuickStartGuide extends StatefulWidget { + const MineSweeperQuickStartGuide({super.key}); + + @override + State createState() => + _MineSweeperQuickStartGuideState(); +} + +class _MineSweeperQuickStartGuideState + extends State { + Widget _buildSectionTitle(String title) { + return Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildSubSectionTitle(String title) { + return Text( + title, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ); + } + + Widget _buildRulePoint({ + required String title, + required List points, + required Color color, + }) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0, bottom: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ...points + .map((point) => Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + '• $point', + style: const TextStyle(fontSize: 16), + ), + )) + .toList(), + ], + ), + ); + } + + Widget _buildNumberClue(String clue, String explanation) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• ', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 16, + color: MihColors.getYellowColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark")), + children: [ + TextSpan( + text: 'If you see a $clue: ', + style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: explanation), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildStrategyPoint(String title, String explanation, + {bool isAction = false}) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: TextStyle( + fontSize: 16, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark")), + children: [ + TextSpan( + text: title, + style: TextStyle( + fontWeight: isAction ? FontWeight.bold : FontWeight.normal, + color: isAction + ? MihColors.getRedColor( + MzansiInnovationHub.of(context)!.theme.mode != + "Dark") + : MihColors.getPurpleColor( + MzansiInnovationHub.of(context)!.theme.mode != + "Dark"), + ), + ), + TextSpan(text: isAction ? ' $explanation' : ': $explanation'), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTipPoint(String title, String explanation) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('• ', + style: TextStyle( + fontSize: 18, + color: MihColors.getOrangeColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark"), + fontWeight: FontWeight.bold)), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 16, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark")), + children: [ + TextSpan( + text: title, + style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: explanation), + ], + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final Size size = MediaQuery.sizeOf(context); + final double width = size.width; + return MihPackageToolBody( + borderOn: false, + bodyItem: getBody(width), + ); + } + + Widget getBody(double width) { + return MihSingleChildScroll( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: width / 20), + child: Column( + children: [ + const Text( + 'Simple Rules and Strategy', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Minesweeper is a puzzle game where you use numbers to figure out where the hidden bombs (mines) are located.', + style: TextStyle(fontSize: 16), + ), + const Divider(height: 30), + + // --- 1. Two Main Actions --- + _buildSectionTitle('1. Two Main Actions (Your Controls)'), + const SizedBox(height: 8), + _buildRulePoint( + title: 'Quick Tap (or Click): This is the Dig action.', + points: [ + 'Goal: To uncover a square and see a number clue.', + 'Risk: If you click a mine, the game ends!', + ], + color: MihColors.getGreenColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark"), + ), + _buildRulePoint( + title: + 'Tap and Hold (or Long Press): This is the Flag action (🚩).', + points: [ + 'Goal: To safely mark a square that you are **certain** is a mine.', + 'Benefit: You cannot accidentally click a square that is flagged.', + ], + color: MihColors.getRedColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark"), + ), + const Divider(height: 30), + + // --- 2. The Golden Rule: Reading the Numbers --- + _buildSectionTitle('2. The Golden Rule: Reading the Numbers'), + const SizedBox(height: 8), + const Text( + 'The number tells you exactly how many mines are touching that square (including sides and corners).', + style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 8), + _buildNumberClue('Blank Space (a \'0\')', + 'Zero (0) mines are touching it. All surrounding squares are safe, and the game will open them for you automatically.'), + _buildNumberClue('\'1\'', + 'Only **one** mine is touching this square. You must find and flag that single mine.'), + _buildNumberClue('\'3\'', + 'Three mines are touching this square. You must find and flag all three.'), + const Divider(height: 30), + + // --- 3. The Winning Strategy --- + _buildSectionTitle('3. The Winning Strategy (The Deduction Loop)'), + const SizedBox(height: 8), + const Text( + 'The game is won by uncovering every single safe square and correctly flagging all the mines. Use this two-step loop to clear the board:', + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 8), + + _buildSubSectionTitle('A. Find the Mines (Where to Flag 🚩)'), + _buildStrategyPoint( + 'Look for a number that only has one choice for a mine.', + 'Example: If a \'1\' is touching only one hidden square, that hidden square **must** be the mine.'), + _buildStrategyPoint('Action:', + 'Tap and Hold to place a **Flag** on the square you are sure is a mine.', + isAction: true), + + const SizedBox(height: 12), + + _buildSubSectionTitle('B. Find the Safe Squares (Where to Dig)'), + _buildStrategyPoint( + 'Look for a number that has been \'satisfied\' by your flags.', + 'Example: You see a \'2\'. You have already placed two 🚩 flags touching it. The \'2\' is satisfied.'), + _buildStrategyPoint('Action:', + 'Quick Tap any of the remaining hidden squares touching that \'satisfied\' number. They **must be safe** because the mine requirement has already been met.', + isAction: true), + + const Divider(height: 30), + + // --- Key Beginner Tip --- + _buildSectionTitle('✨ Key Beginner Tips'), + _buildTipPoint('Start on the Edges and Corners:', + 'Numbers on the edge or corner of the board are easier to solve because they have fewer surrounding squares to check.'), + _buildTipPoint('Don\'t Guess:', + 'If you are down to two squares and either one could be the mine, look somewhere else on the board for a guaranteed, safe move.'), + ], + ), + ), + ); + } +} diff --git a/Frontend/lib/mih_packages/mine_sweeper/package_tools/my_score_board.dart b/Frontend/lib/mih_packages/mine_sweeper/package_tools/my_score_board.dart new file mode 100644 index 00000000..eca9dd2f --- /dev/null +++ b/Frontend/lib/mih_packages/mine_sweeper/package_tools/my_score_board.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:ken_logger/ken_logger.dart'; +import 'package:mzansi_innovation_hub/main.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_circle_avatar.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_dropdwn_field.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tool_body.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_profile_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart'; +import 'package:mzansi_innovation_hub/mih_packages/mine_sweeper/builders/build_my_scoreboard_list.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_minesweeper_services.dart'; +import 'package:mzansi_innovation_hub/mih_services/mih_validation_services.dart'; +import 'package:provider/provider.dart'; + +class MyScoreBoard extends StatefulWidget { + const MyScoreBoard({super.key}); + + @override + State createState() => _MihMineSweeperLeaderBoardState(); +} + +class _MihMineSweeperLeaderBoardState extends State { + TextEditingController filterController = TextEditingController(); + + Future initialiseLeaderboard() async { + MzansiProfileProvider profileProvider = + context.read(); + MihMineSweeperProvider mineSweeperProvider = + context.read(); + filterController.text = mineSweeperProvider.difficulty; + if (mineSweeperProvider.myScoreboard == null || + mineSweeperProvider.myScoreboard!.isEmpty) { + KenLogger.success("getting data"); + await MihMinesweeperServices() + .getMyScoreboard(profileProvider, mineSweeperProvider); + KenLogger.success("${mineSweeperProvider.myScoreboard}"); + } + } + + void refreshLeaderBoard( + MihMineSweeperProvider mineSweeperProvider, String difficulty) { + mineSweeperProvider.setDifficulty(difficulty); + mineSweeperProvider.setLeaderboard(leaderboard: null); + mineSweeperProvider.setMyScoreboard(myScoreboard: null); + initialiseLeaderboard(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await initialiseLeaderboard(); + }); + } + + @override + Widget build(BuildContext context) { + final double width = MediaQuery.sizeOf(context).width; + return Consumer( + builder: (BuildContext context, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + return RefreshIndicator( + onRefresh: () async { + refreshLeaderBoard(mineSweeperProvider, filterController.text); + }, + child: MihPackageToolBody( + borderOn: false, + bodyItem: getBody(width), + ), + ); + }, + ); + } + + Widget getBody(double width) { + return Consumer2( + builder: (BuildContext context, MzansiProfileProvider profileProvider, + MihMineSweeperProvider mineSweeperProvider, Widget? child) { + if (mineSweeperProvider.myScoreboard == null) { + return Center( + child: Mihloadingcircle(), + ); + } else { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Center( + child: MihCircleAvatar( + imageFile: profileProvider.userProfilePicture, + width: 150, + editable: false, + fileNameController: null, + userSelectedfile: null, + frameColor: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + backgroundColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + onChange: (selectedImage) {}, + key: ValueKey(profileProvider.userProfilePicUrl), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: width / 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: MihDropdownField( + controller: filterController, + hintText: "Scoreboards", + dropdownOptions: const [ + "Very Easy", + "Easy", + "Intermediate", + "Hard", + ], + requiredText: true, + editable: true, + enableSearch: true, + validator: (value) { + return MihValidationServices().isEmpty(value); + }, + onSelected: (selection) { + refreshLeaderBoard(mineSweeperProvider, selection!); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + mineSweeperProvider.myScoreboard!.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Icon( + MihIcons.mineSweeper, + size: 165, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + const SizedBox(height: 10), + Text( + "You have played and ${mineSweeperProvider.difficulty} yet.", + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + ), + const SizedBox(height: 25), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + children: [ + TextSpan(text: "Press "), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + FontAwesomeIcons.bomb, + size: 20, + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + ), + ), + TextSpan(text: " and start a new game"), + ], + ), + ), + ), + ], + ), + ) + : BuildMyScoreBoardList(), + ], + ), + ); + } + }, + ); + } +} diff --git a/Frontend/lib/mih_services/mih_minesweeper_services.dart b/Frontend/lib/mih_services/mih_minesweeper_services.dart new file mode 100644 index 00000000..6d5eb524 --- /dev/null +++ b/Frontend/lib/mih_services/mih_minesweeper_services.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'package:mzansi_innovation_hub/mih_components/mih_objects/minesweeper_player_score.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mih_mine_sweeper_provider.dart'; +import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_profile_provider.dart'; +import 'package:mzansi_innovation_hub/mih_config/mih_env.dart'; +import 'package:supertokens_flutter/http.dart' as http; + +class MihMinesweeperServices { + Future getTop20Leaderboard( + MihMineSweeperProvider mineSweeperProvider, + ) async { + String difficulty = mineSweeperProvider.difficulty; + var response = await http.get( + Uri.parse( + "${AppEnviroment.baseApiUrl}/minesweeper/leaderboard/top20/$difficulty"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + ); + if (response.statusCode == 200) { + Iterable l = jsonDecode(response.body); + List leaderboard = + List.from( + l.map((model) => MinesweeperPlayerScore.fromJson(model))); + mineSweeperProvider.setLeaderboard(leaderboard: leaderboard); + } else { + mineSweeperProvider.setLeaderboard(leaderboard: null); + } + return response.statusCode; + } + + Future getMyScoreboard( + MzansiProfileProvider profileProvider, + MihMineSweeperProvider mineSweeperProvider, + ) async { + String difficulty = mineSweeperProvider.difficulty; + var response = await http.get( + Uri.parse( + "${AppEnviroment.baseApiUrl}/minesweeper/leaderboard/top_score/$difficulty/${profileProvider.user!.app_id}"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + ); + if (response.statusCode == 200) { + Iterable l = jsonDecode(response.body); + List leaderboard = + List.from( + l.map((model) => MinesweeperPlayerScore.fromJson(model))); + mineSweeperProvider.setMyScoreboard(myScoreboard: leaderboard); + } else { + mineSweeperProvider.setMyScoreboard(myScoreboard: null); + } + return response.statusCode; + } + + Future addPlayerScore( + MzansiProfileProvider profileProvider, + MihMineSweeperProvider mineSweeperProvider, + String game_time, + double game_score, + ) async { + DateTime now = DateTime.now(); + String formattedDateTime = now.toString(); + var response = await http.post( + Uri.parse( + "${AppEnviroment.baseApiUrl}/minesweeper/leaderboard/player_score/insert/"), + headers: { + "Content-Type": "application/json; charset=UTF-8" + }, + body: jsonEncode({ + "app_id": profileProvider.user!.app_id, + "difficulty": mineSweeperProvider.difficulty, + "game_time": game_time, + "game_score": game_score, + "played_date": formattedDateTime, + }), + ); + return response.statusCode; + } +} diff --git a/backend/.DS_Store b/backend/.DS_Store index 74465b9f37d4f3079203ee8e7c04443801d8f858..051f6a8cbd7f20dbeb4bf2d4e55cc55989e7065d 100644 GIT binary patch literal 10244 zcmeHMYitzP6~5mX?3rbNv11JEX1xo>ffxhEHW;vh^@~R^;IMx4Sa!WrJY~IOc6RL~ zE{WSpQCkxAnJDTbQhp>2eek1dBB~~B(<)Karaux9RFXE8+Nf%)HmcgF`O_ag_ug6X zuI;F8QqhVTY39ti_nbR>&V1*7bMG?7(2+}SWh~4XQ|aW=(CISI!Tb3+=TO`h-U;$& zEXm?5OJ{;k@9;9bg}cOnbf^9>Hzyf!<&2clok6-Y zxKD<=LVSe&oz}yV*veDT%v)CAm6|CPKW+tYbiA2gykj>@9M9Olq z4GmYJlq_0YTBeq%OVpFmF?%YSjc0RCM?7_(o-is;OGcST#M}($Z2NZf{+1Nn$P210(*VGj? zTUw9B&ZfrG_Ge^BaHj>fv?#<2?-yUnne3GDmH6}V{URTq=35dN9P*6}S)+D3YTakX zjO7jmc#zcIj5$ex4UXEW+<4lF8Ret#)Mzdh&zkM26gPH;vgSQmemCOSnQW&pj2TAG zF^`#KXHA-2WKFIWe59ijS<6m417^k{SrciHVQ8216~VQ4Y;4%JYj4-V!*^d|bka(n8KDfOv(`r}GozJbMC(h($IUB7)yh??{eeKS zb$*ngT*5k+v$FRZHL5C~5elr)jAzsnN&1n8?Ia&DDS85-K&`4C=k1*tldiJaw;@oc z8F97Gp^)Ven|xa|!_oTA(rcEkjlO2pNNWA5c*0D{rWW5$)i|ULPRht-W1DY}T8?^c zXw)7br+6IM+#cvujTh8Y{MkKmCwp|lOy_^qzQ6(1cv&5B#^M?C=oBGM`s@qzsH%8# z)Xtr{lUCNwh?E=&1p>o-u=ZrqOj6{ls#Y3-XrNy;ULzmFN>5sZZF(UR;%ZH#PQDkB zR;{uKYn;PYv$d?5wXt3{Ow}~S9%7HMN7<9?5_^_C$6jKuve($_>?(Vcz0Ll}-ev#5 zVw6FJh6>z<6{tcD>ahu%u?wB(!anRr5Bf2HAq?Xb&R`55#aW!geVE31+>ZzGS$qy( z#Fy|jJcY00BEEs|;m7z1Uc^gy6>s28{1WfrJq3!QlqkBgR9UVVN{zBk*`zcoO)?54 zUqQzUqf)v^UvP$?3UX%%<=lJ~6x_GpckrfG&^4XcOnJp^l`E=iHf(C#-Ws_MD+$Cr z_GV*Rz_hVll3GetAk{nT zfcoQ!7D zDLZ|JY&fvXSccgwN1M|o-8na%*qV3LhqpCt{eZ+)6xA#`Yswen8?)$4U+Nzi92gqb z=OvwgkKVZTXvR*ZdTqzzQ{?bZaf%>{F~aL#pTCgi#8}n-@LZbXJ^pI{x+0nr4(t7Q-k>?rQm=2m zL36@&tKKw==0wwWeMbS!iN;oacM;8r=7_&zKAIDK-TwW>H0QP-nwREy=!dKQ{sNj4 z9>@HB3({P^&1-rEpw{;V{@yX!?@mdT?F)>UKvrXaw%-|FFBtDHV5VJi(Q^VuL zY)^wG1w4Zv;D>mD7*2BAukaTBfWP8zcvmSUW?QZJl>l*Dt+HO(ptLG&O1E-Ei7F># zR15Ol$+^?SEOvW({(=e}IC$W$L;pVobuG_@RV%~1u6FG1_>kF+s|{O2y32-K-BwR3XG5;++@zuP`fl*b8=Es8un~_qu&_!&=lN8(Wr0{*k4Rmr_cQZ@eN8GjXMnq{ c1Gcv=&;BNw=&##3gN^yhL&7wx&Ev8@8nnO4r?;0VYgm%FgT- zY>M@l2Gqnq8sGnjiO+;bh>sus@PR)xA%uX@#AxJ~e=t!KF~)P}?h@M4ADU=D=O*`_ zIrp4%XU>`L+`DI%F@~1B(ZpCSV@#&bsix9!i^k==PisT2oXHD4L>JVnCRaJi^t9#*1`9P+`s>%o&PzMzKOc zaCfqc1nLZNLBlbOKp27P5#Z@lK~=lU^31=ves?n@IhPr+EzfUkyacJNeAes=xk6Uu zeaVCFXwpynd9Nj%+snCb*R?aH>vU$1Wsat_>V)g~Y0EKlfq`Y1q%zoRIfgsh;^rO0 z3v81@AS<$x(rU-XHZ*UHMb@ot8jnTBHmzxlMVi;HA0Job1<{7hyHkhkVaGi#96@n2 zU~{K9c4E3$wpU1GZ0=RrV9BemN?s{*RCTW2Kd^Uj-%v`^@}9Zdw9~#-Fk92U>7}&! z9yz%!>pERIvp~-EW?ef!?2x1~o3^ugQZ~2RcG7xOXlHY-ZFjkzN(?C7PI}n9SXH}d@!d;weX4e8 zmA-t&I-Ixsqgqs!#XaivsG^;b2Muzv!!`JdMj+Aa^%b(bm(M41CXHfRU8TnqEiHF@ z)^Ops@L^?zSvg7Fb(p2aqXfiy+`krwF_jUSx&(s#M6rqz?Y-(_K5&>(fBHXzNjFK z1%Zyn8wFoonl4g)(&?;`wXp4Mh>ftL>;!v`y~a+nGwdw;n0>)6v9H-xc8&eSerCV0 z>nO(@RAUL2VFgy=UaY}dtiyUFuoE5Fg#lz?U_TDPMjizm!BHH;6L=EGaRSfcMZAPp z@G4H?9lVQEIE@eS2|mSne2y#l4&UPkT*Wo~hU@rUnkUsr_0m!)D#fJLQiIeeC8SoV zL)s&yBtse&UPz_POHKApI!64I{e&`)z6bKmlTO__^Xl5SZQrr83&t!$|*U8Y|BH$ zGQM93KG8}m@l=!lDafB^7uaR?E&Gum{|Cw-qXrQyM~oo81#yCTE7}O=UD%Bzy0H%_ zq>&-e+i);~!^mL_<9G}cc$^^r6rRR2cpfhh>|Z9}pTz5U18?FjoW(i3hxhS8kN{ue zGQPppDS+NJ8PNE3I-m>5oMXGr0g{x`V6xF2!j*~VtmXB8+sxnp?+|ao#|$G7M&R#6 z0OhU8))umyX*ZKd7NPzKb>6t%grI>54P1rK2jw`?8-Ezmb)+bZ7wLqcB%$`Ne+Vf4 VDE}+Jyu" ) + class UserConsent(Base): __tablename__ = 'user_consent' __table_args__ = {'schema': 'app_data'} @@ -97,4 +98,24 @@ class UserConsent(Base): f"app_id='{self.app_id}', " f"privacy_policy_accepted='{self.privacy_policy_accepted}', " f"terms_of_services_accepted='{self.terms_of_services_accepted}')>" + ) + +class MineSweeperLeaderboard(Base): + __tablename__ = 'player_score' + __table_args__ = {'schema': 'minesweeper_leaderboard'} + idplayer_score = Column(Integer, primary_key=True) + app_id = Column(String(128), nullable=False,server_default=text("''")) + difficulty = Column(String(45), nullable=False,server_default=text("''")) + game_time = Column(String(45), nullable=False,server_default=text("''")) + game_score = Column(DECIMAL(45), nullable=False) + played_date = Column(DateTime, nullable=True) + + def __repr__(self): + return ( + f"" ) \ No newline at end of file diff --git a/backend/routers/mine_sweeper_leaderboard.py b/backend/routers/mine_sweeper_leaderboard.py new file mode 100644 index 00000000..1a665abe --- /dev/null +++ b/backend/routers/mine_sweeper_leaderboard.py @@ -0,0 +1,171 @@ +from fastapi import APIRouter, HTTPException, status +from pydantic import BaseModel +from supertokens_python.recipe.session.framework.fastapi import verify_session +from supertokens_python.recipe.session import SessionContainer +from fastapi import Depends +import mih_database +import mih_database.mihDbConnections +from mih_database.mihDbObjects import MineSweeperLeaderboard, User +from sqlalchemy import and_, func, literal_column +from sqlalchemy.orm import Session, aliased +from sqlalchemy.exc import IntegrityError, SQLAlchemyError +from datetime import datetime +from sqlalchemy.sql.expression import select + +router = APIRouter() + +class playerScoreInsertRequest(BaseModel): + app_id: str + difficulty: str + game_time: str + game_score: float + played_date: datetime + +# get top 20 scores +@router.get("/minesweeper/leaderboard/top20/{difficulty}", tags=["Minesweeper"]) +async def get_user_consent(difficulty: str, session: SessionContainer = Depends(verify_session())):#session: SessionContainer = Depends(verify_session()) + dbEngine = mih_database.mihDbConnections.dbAllConnect() + dbSession = Session(dbEngine) + try: + max_score_subquery = ( + dbSession.query( + MineSweeperLeaderboard.app_id, + func.max(MineSweeperLeaderboard.game_score).label('max_score') + ) + .filter(MineSweeperLeaderboard.difficulty == difficulty) + .group_by(MineSweeperLeaderboard.app_id) + .subquery('max_scores') + ) + queryResults = ( + dbSession.query(MineSweeperLeaderboard, User) + .join(User, User.app_id == MineSweeperLeaderboard.app_id) + .join( + max_score_subquery, + and_( + MineSweeperLeaderboard.app_id == max_score_subquery.c.app_id, + MineSweeperLeaderboard.game_score == max_score_subquery.c.max_score + ) + ) + .filter(MineSweeperLeaderboard.difficulty == difficulty) + .order_by(MineSweeperLeaderboard.game_score.desc()) + .limit(20) + .all() + ) + leaderboardData = [] + if queryResults: + for playerScore, user in queryResults: + leaderboardData.append({ + "app_id": playerScore.app_id, + "username": user.username, + "proPicUrl":user.pro_pic_path, + "difficulty":playerScore.difficulty, + "game_time":playerScore.game_time, + "game_score":playerScore.game_score, + "played_date":playerScore.played_date, + }) + return leaderboardData + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No Score available for user." + ) + except HTTPException as http_exc: + raise http_exc + except Exception as e: + print(f"An error occurred during the ORM query: {e}") + if dbSession.is_active: + dbSession.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve records due to an internal server error." + ) + finally: + dbSession.close() + +@router.get("/minesweeper/leaderboard/top_score/{difficulty}/{app_id}", tags=["Minesweeper"]) +async def get_user_consent(app_id: str, + difficulty: str, + session: SessionContainer = Depends(verify_session())):#session: SessionContainer = Depends(verify_session()) + dbEngine = mih_database.mihDbConnections.dbAllConnect() + dbSession = Session(dbEngine) + try: + queryResults =(dbSession.query(MineSweeperLeaderboard, User) + .join(User, User.app_id == MineSweeperLeaderboard.app_id) + .filter( + and_( + MineSweeperLeaderboard.app_id == app_id, + MineSweeperLeaderboard.difficulty == difficulty + ) + ) + .order_by(MineSweeperLeaderboard.game_score.desc()) + .all()) + if not queryResults: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No scores found for this user and difficulty level." + ) + leaderboard_data = [] + for player_score, user in queryResults: + score_data = { + "app_id": player_score.app_id, + "username": user.username, + "proPicUrl": user.pro_pic_path, + "difficulty": player_score.difficulty, + "game_time": player_score.game_time, + "game_score": player_score.game_score, + "played_date": player_score.played_date, + } + leaderboard_data.append(score_data) + return leaderboard_data + except HTTPException as http_exc: + raise http_exc + except Exception as e: + print(f"An error occurred during the ORM query: {e}") + if dbSession.is_active: + dbSession.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve records due to an internal server error." + ) + finally: + dbSession.close() + +@router.post("/minesweeper/leaderboard/player_score/insert/", + tags=["Minesweeper"], + status_code=status.HTTP_201_CREATED) +async def insert_user_consent(itemRequest: playerScoreInsertRequest, + session: SessionContainer = Depends(verify_session())):#session: SessionContainer = Depends(verify_session()) + dbEngine = mih_database.mihDbConnections.dbAllConnect() + dbSession = Session(dbEngine) + try: + newPlayerScore = MineSweeperLeaderboard( + app_id = itemRequest.app_id, + difficulty = itemRequest.difficulty, + game_time = itemRequest.game_time, + game_score = itemRequest.game_score, + played_date = itemRequest.played_date, + ) + dbSession.add(newPlayerScore) + dbSession.commit() + dbSession.refresh(newPlayerScore) + return {"message": "Successfully Created Player Score Record"} + except IntegrityError as e: + dbSession.rollback() + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, # 409 Conflict is often suitable for constraint errors + detail=f"Data integrity error: The provided data violates a database constraint. Details: {e.orig}" + ) from e + except SQLAlchemyError as e: + dbSession.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"A database error occurred during insertion. Details: {e.orig}" + ) from e + except Exception as e: + dbSession.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An unexpected error occurred: {e}" + ) from e + finally: + dbSession.close()