diff --git a/assets/fonts/Nunito-Bold.ttf b/assets/fonts/Nunito-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6519feb781449ebe0015cbc74dfd9e13110fbba9 Binary files /dev/null and b/assets/fonts/Nunito-Bold.ttf differ diff --git a/assets/fonts/Nunito-Light.ttf b/assets/fonts/Nunito-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8a0736c41cd6c2a1225d356bf274de1d0afc3497 Binary files /dev/null and b/assets/fonts/Nunito-Light.ttf differ diff --git a/assets/fonts/Nunito-Medium.ttf b/assets/fonts/Nunito-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..88fccdc0638b6f5d6ac49d9d269dc3d518618ad1 Binary files /dev/null and b/assets/fonts/Nunito-Medium.ttf differ diff --git a/assets/fonts/Nunito-Regular.ttf b/assets/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7b8375a896ef0cd8e06730a78c84532b377e784 Binary files /dev/null and b/assets/fonts/Nunito-Regular.ttf differ diff --git a/assets/icons/button_restart.png b/assets/icons/button_restart.png deleted file mode 100644 index ea561bc7bc18e930e84c7b768d12eb4963a778a9..0000000000000000000000000000000000000000 Binary files a/assets/icons/button_restart.png and /dev/null differ diff --git a/assets/skins/default_tile.png b/assets/skins/default_tile.png deleted file mode 100644 index 586de0573e7f77d97c16942f603b8ee6d9c7d7bc..0000000000000000000000000000000000000000 Binary files a/assets/skins/default_tile.png and /dev/null differ diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..87863ba812d671bc80c6cba6b3caa2e25520e340 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,12 @@ +{ + "app_name": "Reversi", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "Informations", + "about_content": "Reversi", + "about_version": "Version: {version}", + + "": "" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..2340d9e9b3cc2d6598b119ffe7a363efe791d793 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,12 @@ +{ + "app_name": "Reversi", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Reversi.", + "about_version": "Version : {version}", + + "": "" +} diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png new file mode 100644 index 0000000000000000000000000000000000000000..51d7a01d171f7d7f047ecf9dee2d7ceee23b310d Binary files /dev/null and b/assets/ui/button_back.png differ diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca5b749c208c4b7eac2a4b141a1bd918d7cb98f Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe433b7d18a39880a14e3f0af18cb75c4ccbaed Binary files /dev/null and b/assets/ui/button_resume_game.png differ diff --git a/assets/ui/button_start.png b/assets/ui/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..23c7a4f670de19ffac455d6c510c3c53653a048b Binary files /dev/null and b/assets/ui/button_start.png differ diff --git a/assets/icons/empty.png b/assets/ui/empty.png similarity index 62% rename from assets/icons/empty.png rename to assets/ui/empty.png index 047508dc7427561315ec1c3834f25ab888324c06..814df31be6ddc4275ebe4490c79365578dbef1f0 100644 Binary files a/assets/icons/empty.png and b/assets/ui/empty.png differ diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec Binary files /dev/null and b/assets/ui/game_fail.png differ diff --git a/assets/icons/game_win.png b/assets/ui/game_win.png similarity index 100% rename from assets/icons/game_win.png rename to assets/ui/game_win.png diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/ui/placeholder.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/20.txt b/fastlane/metadata/android/en-US/changelogs/20.txt new file mode 100644 index 0000000000000000000000000000000000000000..d3b792db0729b6b6a06eba353000bc505e0d21f5 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/20.txt @@ -0,0 +1 @@ +Clean / improve / update code and UI.. diff --git a/fastlane/metadata/android/fr-FR/changelogs/20.txt b/fastlane/metadata/android/fr-FR/changelogs/20.txt new file mode 100644 index 0000000000000000000000000000000000000000..8e88019e5d88c4e6e123ad73c0c1c0dce6e4ff70 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/20.txt @@ -0,0 +1 @@ +Nettoyage / amélioration / mise à jour de code et de l'interface. diff --git a/icons/button_back.svg b/icons/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..018d8b734d2932028fbfce1643c4e888ff1b45b1 --- /dev/null +++ b/icons/button_back.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.3783 .61747 -.61747 1.3783 45.198 93.762)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#950e4f" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg> diff --git a/icons/button_delete_saved_game.svg b/icons/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..c3f872e434052a6b4e7036b530ced8e6233508e4 --- /dev/null +++ b/icons/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m76.652 23.303-3.6441 58.302c-0.28153 4.5103-4.0223 8.0241-8.5413 8.0241h-35.27c-4.5189 0-8.2598-3.5138-8.5413-8.0241l-3.6441-58.302h-5.4824c-1.7723 0-3.2093-1.437-3.2093-3.2093 0-1.773 1.437-3.2093 3.2093-3.2093h70.605c1.7723 0 3.2093 1.4363 3.2093 3.2093 0 1.7723-1.437 3.2093-3.2093 3.2093zm-6.8314 0h-45.979l3.0819 55.867c0.12535 2.268 2.0008 4.0433 4.2732 4.0433h31.268c2.2724 0 4.1478-1.7752 4.2732-4.0433zm-22.99 6.4188c1.6541 0 2.9952 1.3411 2.9952 2.9952v41.08c0 1.6541-1.3411 2.9952-2.9952 2.9952-1.6542 0-2.9952-1.3411-2.9952-2.9952v-41.08c0-1.6541 1.3411-2.9952 2.9952-2.9952zm-12.837 0c1.6756 0 3.0553 1.3181 3.1312 2.9921l1.8776 41.3c0.06665 1.4664-1.0681 2.7087-2.5345 2.7762-0.04011 0.0015-0.08024 0.0021-0.12108 0.0021-1.5595 0-2.8476-1.2193-2.9328-2.7774l-2.253-41.3c-0.08524-1.5646 1.114-2.9012 2.6779-2.9864 0.05157-0.0029 0.10317-0.0042 0.15474-0.0042zm25.675 0c1.5667 0 2.8361 1.2694 2.8361 2.8361 0 0.05156-6.87e-4 0.10317-0.0036 0.15474l-2.2416 41.088c-0.09171 1.6778-1.4786 2.991-3.1586 2.991-1.5667 0-2.8361-1.2694-2.8361-2.8361 0-0.05156 7.31e-4 -0.10315 0.0036-0.15474l2.2417-41.088c0.09172-1.6778 1.4786-2.991 3.1586-2.991zm-21.397-25.675h17.117c4.7265 0 8.5578 3.8313 8.5578 8.5578v4.2795h-34.231v-4.2795c0-4.7265 3.8313-8.5578 8.5578-8.5578zm0.42837 6.4188c-1.4184 0-2.5675 1.1491-2.5675 2.5675v3.8512h21.394v-3.8512c0-1.4184-1.1491-2.5675-2.5675-2.5675z" fill="#fff" fill-rule="evenodd" stroke="#050200"/></svg> diff --git a/icons/button_resume_game.svg b/icons/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..2bf973276aefa564ecff7d6149899298344819f9 --- /dev/null +++ b/icons/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-5.618)" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 55.567 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" stroke-width="7.2832"/><path d="m15.535 12.852 2e-3 67.973z" stroke-width="11"/></g></svg> diff --git a/icons/button_start.svg b/icons/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..4d7634a9f3fb559e590ee965e1341ae2634bf80f --- /dev/null +++ b/icons/button_start.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 46.954 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg> diff --git a/lib/game_board_scorer.dart b/lib/ai/game_board_scorer.dart similarity index 97% rename from lib/game_board_scorer.dart rename to lib/ai/game_board_scorer.dart index ac314aa27145fc1e1a058e478025bdfab36bae62..1f0d9502921dfba95b57337073ff3bbd70584e76 100644 --- a/lib/game_board_scorer.dart +++ b/lib/ai/game_board_scorer.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'game_board.dart'; +import 'package:reversi/models/game/game_board.dart'; class GameBoardScorer { // Values for each position on the board. diff --git a/lib/move_finder.dart b/lib/ai/move_finder.dart similarity index 85% rename from lib/move_finder.dart rename to lib/ai/move_finder.dart index c5e97824a5dba0ba9ba05c54bd30baceab95c929..ca52de253a76250d4da6400d1ca229f31df307e5 100644 --- a/lib/move_finder.dart +++ b/lib/ai/move_finder.dart @@ -6,8 +6,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'game_board.dart'; -import 'game_board_scorer.dart'; +import 'package:reversi/ai/game_board_scorer.dart'; +import 'package:reversi/models/game/game_board.dart'; class MoveSearchArgs { MoveSearchArgs({required this.board, required this.player, required this.numPlies}); @@ -28,8 +28,7 @@ class ScoredMove { // The [compute] function requires a top-level method as its first argument. // This is that method for [MoveFinder]. Position? _findNextMove(MoveSearchArgs args) { - final bestMove = - _performSearchPly(args.board, args.player, args.player, args.numPlies - 1); + final bestMove = _performSearchPly(args.board, args.player, args.player, args.numPlies - 1); return bestMove?.move; } @@ -47,15 +46,12 @@ ScoredMove? _performSearchPly( return null; } - var score = - (scoringPlayer == player) ? GameBoardScorer.minScore : GameBoardScorer.maxScore; + var score = (scoringPlayer == player) ? GameBoardScorer.minScore : GameBoardScorer.maxScore; ScoredMove? bestMove; for (var i = 0; i < availableMoves.length; i++) { - final newBoard = - board.updateForMove(availableMoves[i].x, availableMoves[i].y, player); - if (pliesRemaining > 0 && - newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) { + final newBoard = board.updateForMove(availableMoves[i].x, availableMoves[i].y, player); + if (pliesRemaining > 0 && newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) { // Opponent has next turn. score = _performSearchPly( newBoard, diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..00143db91b64210d3c22e0996cd8b245f778b3a5 --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,40 @@ +import 'package:reversi/utils/tools.dart'; + +class DefaultGameSettings { + // available game parameters codes + static const String parameterCodeGameMode = 'gameMode'; + static const String parameterCodeDifficultyLevel = 'difficultyLevel'; + static const List<String> availableParameters = [ + parameterCodeGameMode, + parameterCodeDifficultyLevel, + ]; + + // game mode: available values + static const String gameModeValueMedium = 'human-vs-cpu'; + static const List<String> allowedGameModeValues = [ + gameModeValueMedium, + ]; + // game mode: default value + static const String defaultGameModeValue = gameModeValueMedium; + + // difficulty level: available values + static const String difficultyLevelMedium = 'medium'; + static const List<String> allowedDifficultyLevelValues = [ + difficultyLevelMedium, + ]; + // difficulty level: default value + static const String difficultyLevelValue = difficultyLevelMedium; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeGameMode: + return DefaultGameSettings.allowedGameModeValues; + case parameterCodeDifficultyLevel: + return DefaultGameSettings.allowedDifficultyLevelValues; + } + + printlog('Did not find any available value for game parameter "$parameterCode".'); + return []; + } +} diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..2de24e5d146fecb47acfccf2a125828e74d0cf19 --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,28 @@ +import 'package:reversi/utils/tools.dart'; + +class DefaultGlobalSettings { + // available global parameters codes + static const String parameterCodeSkin = 'skin'; + static const List<String> availableParameters = [ + parameterCodeSkin, + ]; + + // skin: available values + static const String skinValueDefault = 'default'; + static const List<String> allowedSkinValues = [ + skinValueDefault, + ]; + // skin: default value + static const String defaultSkinValue = skinValueDefault; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeSkin: + return DefaultGlobalSettings.allowedSkinValues; + } + + printlog('Did not find any available value for global parameter "$parameterCode".'); + return []; + } +} diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..b09d6841a3e2d01a875f7ebd33bc246ad02016fb --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:reversi/ui/screens/page_about.dart'; +import 'package:reversi/ui/screens/page_game.dart'; +import 'package:reversi/ui/screens/page_settings.dart'; + +class MenuItem { + final Icon icon; + final Widget page; + + const MenuItem({ + required this.icon, + required this.page, + }); +} + +class Menu { + static const indexGame = 0; + static const menuItemGame = MenuItem( + icon: Icon(UniconsLine.home), + page: PageGame(), + ); + + static const indexSettings = 1; + static const menuItemSettings = MenuItem( + icon: Icon(UniconsLine.setting), + page: PageSettings(), + ); + + static const indexAbout = 2; + static const menuItemAbout = MenuItem( + icon: Icon(UniconsLine.info_circle), + page: PageAbout(), + ); + + static Map<int, MenuItem> items = { + indexGame: menuItemGame, + indexSettings: menuItemSettings, + indexAbout: menuItemAbout, + }; + + static bool isIndexAllowed(int pageIndex) { + return items.keys.contains(pageIndex); + } + + static Widget getPageWidget(int pageIndex) { + return items[pageIndex]?.page ?? menuItemGame.page; + } + + static int itemsCount = Menu.items.length; +} diff --git a/lib/styling.dart b/lib/config/styling.dart similarity index 92% rename from lib/styling.dart rename to lib/config/styling.dart index 6372e69382566eaa5a48cc14e5a8be4f0bf4f7cf..888f74f88e1b248e7c1d7d85200ac21b5f7b1184 100644 --- a/lib/styling.dart +++ b/lib/config/styling.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; -import 'game_board.dart'; +import 'package:reversi/models/game/game_board.dart'; abstract class Styling { // **** GRADIENTS AND COLORS **** @@ -26,8 +26,7 @@ abstract class Styling { ), ); - static const BoxDecoration mainWidgetDecoration = - BoxDecoration(color: Color(0xffffffff)); + static const BoxDecoration mainWidgetDecoration = BoxDecoration(color: Color(0xffffffff)); static const thinkingColor = Color(0xff2196f3); diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; + +/// Colors from Tailwind CSS (v3.0) - June 2022 +/// +/// https://tailwindcss.com/docs/customizing-colors + +const int _primaryColor = 0xFF6366F1; +const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{ + 50: Color(0xFFEEF2FF), // indigo-50 + 100: Color(0xFFE0E7FF), // indigo-100 + 200: Color(0xFFC7D2FE), // indigo-200 + 300: Color(0xFFA5B4FC), // indigo-300 + 400: Color(0xFF818CF8), // indigo-400 + 500: Color(_primaryColor), // indigo-500 + 600: Color(0xFF4F46E5), // indigo-600 + 700: Color(0xFF4338CA), // indigo-700 + 800: Color(0xFF3730A3), // indigo-800 + 900: Color(0xFF312E81), // indigo-900 +}); + +const int _textColor = 0xFF64748B; +const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{ + 50: Color(0xFFF8FAFC), // slate-50 + 100: Color(0xFFF1F5F9), // slate-100 + 200: Color(0xFFE2E8F0), // slate-200 + 300: Color(0xFFCBD5E1), // slate-300 + 400: Color(0xFF94A3B8), // slate-400 + 500: Color(_textColor), // slate-500 + 600: Color(0xFF475569), // slate-600 + 700: Color(0xFF334155), // slate-700 + 800: Color(0xFF1E293B), // slate-800 + 900: Color(0xFF0F172A), // slate-900 +}); + +const Color errorColor = Color(0xFFDC2626); // red-600 + +final ColorScheme lightColorScheme = ColorScheme.light( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade500, + surface: textSwatch.shade50, + surfaceContainerHighest: Colors.white, + shadow: textSwatch.shade900.withOpacity(.1), +); + +final ColorScheme darkColorScheme = ColorScheme.dark( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade300, + surface: const Color(0xFF262630), + surfaceContainerHighest: const Color(0xFF282832), + shadow: textSwatch.shade900.withOpacity(.2), +); + +final ThemeData lightTheme = ThemeData( + colorScheme: lightColorScheme, + fontFamily: 'Nunito', + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + ), +); + +final ThemeData darkTheme = lightTheme.copyWith( + colorScheme: darkColorScheme, + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + ), +); diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..face7682f0b74048fde6d297245d5ccd08fad3d2 --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,90 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:reversi/models/game/game.dart'; +import 'package:reversi/models/settings/settings_game.dart'; +import 'package:reversi/models/settings/settings_global.dart'; + +part 'game_state.dart'; + +class GameCubit extends HydratedCubit<GameState> { + GameCubit() + : super(GameState( + currentGame: Game.createNull(), + )); + + void updateState(Game game) { + emit(GameState( + currentGame: game, + )); + } + + void refresh() { + final Game game = Game( + // Settings + gameSettings: state.currentGame.gameSettings, + globalSettings: state.currentGame.globalSettings, + // State + isRunning: state.currentGame.isRunning, + isStarted: state.currentGame.isStarted, + isFinished: state.currentGame.isFinished, + animationInProgress: state.currentGame.animationInProgress, + // Base data + gameModel: state.currentGame.gameModel, + // Game data + score: state.currentGame.score, + ); + // game.dump(); + + updateState(game); + } + + void startNewGame({ + required GameSettings gameSettings, + required GlobalSettings globalSettings, + }) { + final Game newGame = Game.createNew( + // Settings + gameSettings: gameSettings, + globalSettings: globalSettings, + ); + + newGame.dump(); + + updateState(newGame); + refresh(); + } + + void quitGame() { + state.currentGame.isRunning = false; + refresh(); + } + + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; + refresh(); + } + + @override + GameState? fromJson(Map<String, dynamic> json) { + final Game currentGame = json['currentGame'] as Game; + + return GameState( + currentGame: currentGame, + ); + } + + @override + Map<String, dynamic>? toJson(GameState state) { + return <String, dynamic>{ + 'currentGame': state.currentGame.toJson(), + }; + } +} diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..00e211668c3269255926939324355792abd61c41 --- /dev/null +++ b/lib/cubit/game_state.dart @@ -0,0 +1,15 @@ +part of 'game_cubit.dart'; + +@immutable +class GameState extends Equatable { + const GameState({ + required this.currentGame, + }); + + final Game currentGame; + + @override + List<dynamic> get props => <dynamic>[ + currentGame, + ]; +} diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..141a1f6785c7d3d8281f085fc127d75d78313563 --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:reversi/config/menu.dart'; + +class NavCubit extends HydratedCubit<int> { + NavCubit() : super(0); + + void updateIndex(int index) { + if (Menu.isIndexAllowed(index)) { + emit(index); + } else { + goToGamePage(); + } + } + + void goToGamePage() { + emit(Menu.indexGame); + } + + void goToSettingsPage() { + emit(Menu.indexSettings); + } + + void goToAboutPage() { + emit(Menu.indexAbout); + } + + @override + int fromJson(Map<String, dynamic> json) { + return Menu.indexGame; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'pageIndex': state}; + } +} diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..ecfa21cc7d9a83d652421c52e148c3bcf09e0ae4 --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:reversi/config/default_game_settings.dart'; +import 'package:reversi/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? itemsCount, + String? timerValue, + }) { + emit( + GameSettingsState( + settings: GameSettings( + itemsCount: itemsCount ?? state.settings.itemsCount, + timerValue: timerValue ?? state.settings.timerValue, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeGameMode: + return GameSettings.getItemsCountValueFromUnsafe(state.settings.itemsCount); + case DefaultGameSettings.parameterCodeDifficultyLevel: + return GameSettings.getTimerValueFromUnsafe(state.settings.timerValue); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String itemsCount = code == DefaultGameSettings.parameterCodeGameMode + ? value + : getParameterValue(DefaultGameSettings.parameterCodeGameMode); + final String timerValue = code == DefaultGameSettings.parameterCodeDifficultyLevel + ? value + : getParameterValue(DefaultGameSettings.parameterCodeDifficultyLevel); + + setValues( + itemsCount: itemsCount, + timerValue: timerValue, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String itemsCount = json[DefaultGameSettings.parameterCodeGameMode] as String; + final String timerValue = json[DefaultGameSettings.parameterCodeDifficultyLevel] as String; + + return GameSettingsState( + settings: GameSettings( + itemsCount: itemsCount, + timerValue: timerValue, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: state.settings.itemsCount, + DefaultGameSettings.parameterCodeDifficultyLevel: state.settings.timerValue, + }; + } +} diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed --- /dev/null +++ b/lib/cubit/settings_game_state.dart @@ -0,0 +1,15 @@ +part of 'settings_game_cubit.dart'; + +@immutable +class GameSettingsState extends Equatable { + const GameSettingsState({ + required this.settings, + }); + + final GameSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..cb4094b21e3ca1fdfb44e621f7cc341947276caf --- /dev/null +++ b/lib/cubit/settings_global_cubit.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:reversi/config/default_global_settings.dart'; +import 'package:reversi/models/settings/settings_global.dart'; + +part 'settings_global_state.dart'; + +class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { + GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); + + void setValues({ + String? skin, + }) { + emit( + GlobalSettingsState( + settings: GlobalSettings( + skin: skin ?? state.settings.skin, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGlobalSettings.parameterCodeSkin: + return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin); + } + return ''; + } + + void setParameterValue(String code, String value) { + final String skin = (code == DefaultGlobalSettings.parameterCodeSkin) + ? value + : getParameterValue(DefaultGlobalSettings.parameterCodeSkin); + + setValues( + skin: skin, + ); + } + + @override + GlobalSettingsState? fromJson(Map<String, dynamic> json) { + final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String; + + return GlobalSettingsState( + settings: GlobalSettings( + skin: skin, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GlobalSettingsState state) { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: state.settings.skin, + }; + } +} diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24 --- /dev/null +++ b/lib/cubit/settings_global_state.dart @@ -0,0 +1,15 @@ +part of 'settings_global_cubit.dart'; + +@immutable +class GlobalSettingsState extends Equatable { + const GlobalSettingsState({ + required this.settings, + }); + + final GlobalSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..b793e895dbb0c672d451cd403e0036c3d9ac9b42 --- /dev/null +++ b/lib/cubit/theme_cubit.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +part 'theme_state.dart'; + +class ThemeCubit extends HydratedCubit<ThemeModeState> { + ThemeCubit() : super(const ThemeModeState()); + + void getTheme(ThemeModeState state) { + emit(state); + } + + @override + ThemeModeState? fromJson(Map<String, dynamic> json) { + switch (json['themeMode']) { + case 'ThemeMode.dark': + return const ThemeModeState(themeMode: ThemeMode.dark); + case 'ThemeMode.light': + return const ThemeModeState(themeMode: ThemeMode.light); + case 'ThemeMode.system': + default: + return const ThemeModeState(themeMode: ThemeMode.system); + } + } + + @override + Map<String, String>? toJson(ThemeModeState state) { + return <String, String>{'themeMode': state.themeMode.toString()}; + } +} diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..e479a50f12fe72a35a1fd1722ff72afbb692a136 --- /dev/null +++ b/lib/cubit/theme_state.dart @@ -0,0 +1,15 @@ +part of 'theme_cubit.dart'; + +@immutable +class ThemeModeState extends Equatable { + const ThemeModeState({ + this.themeMode, + }); + + final ThemeMode? themeMode; + + @override + List<Object?> get props => <Object?>[ + themeMode, + ]; +} diff --git a/lib/game/game_engine.dart b/lib/game/game_engine.dart new file mode 100644 index 0000000000000000000000000000000000000000..4be8afc2d89347a1fd5fb97be1dc9295c9b14cb6 --- /dev/null +++ b/lib/game/game_engine.dart @@ -0,0 +1,176 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; + +import 'package:reversi/ai/move_finder.dart'; +import 'package:reversi/config/styling.dart'; +import 'package:reversi/models/game/game_board.dart'; +import 'package:reversi/models/game/game_model.dart'; +import 'package:reversi/ui/widgets/actions/button_game_quit.dart'; +import 'package:reversi/ui/widgets/game/game_board_display.dart'; +import 'package:reversi/ui/widgets/game/score_box.dart'; +import 'package:reversi/ui/widgets/game/thinking_indicator.dart'; + +/// The [GameEngine] Widget represents the entire game +/// display, from scores to board state and everything in between. +class GameEngine extends StatefulWidget { + const GameEngine({super.key}); + + @override + State createState() => _GameEngineState(); +} + +/// State class for [GameEngine]. +/// +/// The game is modeled as a [Stream] of immutable instances of [GameModel]. +/// Each move by the player or CPU results in a new [GameModel], which is +/// sent downstream. [GameEngine] uses a [StreamBuilder] wired up to that stream +/// of models to build out its [Widget] tree. +class _GameEngineState extends State<GameEngine> { + final StreamController<GameModel> _userMovesController = StreamController<GameModel>(); + final StreamController<GameModel> _restartController = StreamController<GameModel>(); + Stream<GameModel>? _modelStream; + + _GameEngineState() { + // Below is the combination of streams that controls the flow of the game. + // There are two streams of models produced by player interaction (either by + // restarting the game, which produces a brand new game model and sends it + // downstream, or tapping on one of the board locations to play a piece, and + // which creates a new board model with the result of the move and sends it + // downstream. The StreamGroup combines these into a single stream, then + // does a little trick with asyncExpand. + // + // The function used in asyncExpand checks to see if it's the CPU's turn + // (white), and if so creates a [MoveFinder] to look for the best move. It + // awaits the calculation, and then creates a new [GameModel] with the + // result of that move and sends it downstream by yielding it. If it's still + // the CPU's turn after making that move (which can happen in reversi), this + // is repeated. + // + // The final stream of models that exits the asyncExpand call is a + // combination of "new game" models, models with the results of player + // moves, and models with the results of CPU moves. These are fed into the + // StreamBuilder in [build], and used to create the widgets that comprise + // the game's display. + _modelStream = StreamGroup.merge([ + _userMovesController.stream, + _restartController.stream, + ]).asyncExpand((model) async* { + yield model; + + var newModel = model; + + while (newModel.player == PieceType.white) { + final finder = MoveFinder(newModel.board); + + final move = await Future.delayed(const Duration(milliseconds: 2000), () { + return finder.findNextMove(newModel.player, 5); + }); + if (move != null) { + newModel = newModel.updateForMove(move.x, move.y); + yield newModel; + } + } + }); + } + + // Thou shalt tidy up thy stream controllers. + @override + void dispose() { + _userMovesController.close(); + _restartController.close(); + super.dispose(); + } + + /// The build method mostly just sets up the StreamBuilder and leaves the + /// details to _buildWidgets. + @override + Widget build(BuildContext context) { + return StreamBuilder<GameModel>( + stream: _modelStream, + builder: (context, snapshot) { + return _buildWidgets( + snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()), + ); + }, + ); + } + + // Called when the user taps on the game's board display. If it's the player's + // turn, this method will attempt to make the move, creating a new GameModel + // in the process. + void _attemptUserMove(GameModel model, int x, int y) { + if (model.player == PieceType.black && model.board.isLegalMove(x, y, model.player)) { + _userMovesController.add(model.updateForMove(x, y)); + } + } + + // Builds out the Widget tree using the most recent GameModel from the stream. + Widget _buildWidgets(GameModel model) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(flex: 1), + ScoreBox(player: PieceType.black, model: model), + const Spacer(flex: 4), + ScoreBox(player: PieceType.white, model: model), + const Spacer(flex: 1), + ], + ), + const SizedBox(height: 8), + GameBoardDisplay( + context: context, + model: model, + callback: _attemptUserMove, + ), + const SizedBox(height: 8), + ThinkingIndicator( + color: Styling.thinkingColor, + height: Styling.thinkingSize, + visible: model.player == PieceType.white, + ), + model.gameIsOver ? _buildGameEnd(model) : SizedBox.shrink() + ], + ); + } + + Widget _buildGameEnd(GameModel model) { + Image decorationImage = Image( + image: AssetImage(model.gameResultString == 'black' + ? 'assets/ui/game_win.png' + : 'assets/ui/placeholder.png'), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + defaultVerticalAlignment: TableCellVerticalAlignment.bottom, + children: [ + TableRow( + children: [ + Column( + children: [decorationImage], + ), + Column( + children: [QuitGameButton()], + ), + Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index a248c75c34922242245401157ea2b47dde5ccd66..10cc981cf005be1187a19262d802cbdf415639be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,343 +1,114 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +import 'dart:io'; -import 'dart:async'; - -import 'package:async/async.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation; - -import 'game_board.dart'; -import 'game_model.dart'; -import 'move_finder.dart'; -import 'styling.dart'; -import 'thinking_indicator.dart'; - -/// Main function for the app. Turns off the system overlays and locks portrait -/// orientation for a more game-like UI, and then runs the [Widget] tree. -void main() { +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:reversi/config/default_global_settings.dart'; +import 'package:reversi/config/theme.dart'; +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/cubit/nav_cubit.dart'; +import 'package:reversi/cubit/settings_game_cubit.dart'; +import 'package:reversi/cubit/settings_global_cubit.dart'; +import 'package:reversi/cubit/theme_cubit.dart'; +import 'package:reversi/ui/skeleton.dart'; + +void main() async { + // Initialize packages WidgetsFlutterBinding.ensureInitialized(); - - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - - runApp(const FlutterFlipApp()); -} - -/// The App class. Unlike many Flutter apps, this one does not use Material -/// widgets, so there's no [MaterialApp] or [Theme] objects. -class FlutterFlipApp extends StatelessWidget { - const FlutterFlipApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - primaryColor: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const GameScreen(), - ); - } -} - -/// The [GameScreen] Widget represents the entire game -/// display, from scores to board state and everything in between. -class GameScreen extends StatefulWidget { - const GameScreen({super.key}); - - @override - State createState() => _GameScreenState(); + await EasyLocalization.ensureInitialized(); + final Directory tmpDir = await getTemporaryDirectory(); + Hive.init(tmpDir.toString()); + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: tmpDir, + ); + + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) + .then((value) => runApp(EasyLocalization( + path: 'assets/translations', + supportedLocales: const <Locale>[ + Locale('en'), + Locale('fr'), + ], + fallbackLocale: const Locale('en'), + useFallbackTranslations: true, + child: const MyApp(), + ))); } -/// State class for [GameScreen]. -/// -/// The game is modeled as a [Stream] of immutable instances of [GameModel]. -/// Each move by the player or CPU results in a new [GameModel], which is -/// sent downstream. [GameScreen] uses a [StreamBuilder] wired up to that stream -/// of models to build out its [Widget] tree. -class _GameScreenState extends State<GameScreen> { - final StreamController<GameModel> _userMovesController = StreamController<GameModel>(); - final StreamController<GameModel> _restartController = StreamController<GameModel>(); - Stream<GameModel>? _modelStream; - - _GameScreenState() { - // Below is the combination of streams that controls the flow of the game. - // There are two streams of models produced by player interaction (either by - // restarting the game, which produces a brand new game model and sends it - // downstream, or tapping on one of the board locations to play a piece, and - // which creates a new board model with the result of the move and sends it - // downstream. The StreamGroup combines these into a single stream, then - // does a little trick with asyncExpand. - // - // The function used in asyncExpand checks to see if it's the CPU's turn - // (white), and if so creates a [MoveFinder] to look for the best move. It - // awaits the calculation, and then creates a new [GameModel] with the - // result of that move and sends it downstream by yielding it. If it's still - // the CPU's turn after making that move (which can happen in reversi), this - // is repeated. - // - // The final stream of models that exits the asyncExpand call is a - // combination of "new game" models, models with the results of player - // moves, and models with the results of CPU moves. These are fed into the - // StreamBuilder in [build], and used to create the widgets that comprise - // the game's display. - _modelStream = StreamGroup.merge([ - _userMovesController.stream, - _restartController.stream, - ]).asyncExpand((model) async* { - yield model; - - var newModel = model; - - while (newModel.player == PieceType.white) { - final finder = MoveFinder(newModel.board); - - final move = await Future.delayed(const Duration(milliseconds: 2000), () { - return finder.findNextMove(newModel.player, 5); - }); - if (move != null) { - newModel = newModel.updateForMove(move.x, move.y); - yield newModel; - } - } - }); - } +class MyApp extends StatelessWidget { + const MyApp({super.key}); - // Thou shalt tidy up thy stream controllers. - @override - void dispose() { - _userMovesController.close(); - _restartController.close(); - super.dispose(); - } - - /// The build method mostly just sets up the StreamBuilder and leaves the - /// details to _buildWidgets. @override Widget build(BuildContext context) { - return StreamBuilder<GameModel>( - stream: _modelStream, - builder: (context, snapshot) { - return _buildWidgets( - context, - snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()), - ); - }, - ); - } - - // Called when the user taps on the game's board display. If it's the player's - // turn, this method will attempt to make the move, creating a new GameModel - // in the process. - void _attemptUserMove(GameModel model, int x, int y) { - if (model.player == PieceType.black && model.board.isLegalMove(x, y, model.player)) { - _userMovesController.add(model.updateForMove(x, y)); + final List<String> assets = getImagesAssets(); + for (String asset in assets) { + precacheImage(AssetImage(asset), context); } - } - Widget _buildScoreBox(PieceType player, GameModel model) { - var assetImageCode = player == PieceType.black ? 'black' : 'white'; - String assetImageName = - 'assets/skins/${model.skin}_tile_$assetImageCode.png'; - - var scoreText = - player == PieceType.black ? '${model.blackScore}' : '${model.whiteScore}'; - - return Container( - padding: const EdgeInsets.symmetric( - vertical: 3.0, - horizontal: 25.0, - ), - decoration: (model.player == player) - ? Styling.activePlayerIndicator - : Styling.inactivePlayerIndicator, - child: Row( - children: <Widget>[ - SizedBox( - width: 25.0, - height: 25.0, - child: Image( - image: AssetImage(assetImageName), - fit: BoxFit.fill, - ), - ), - const SizedBox( - width: 10.0, - ), - Text( - scoreText, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 35.0, - color: Color(0xff000000), - ), - ), - ], - ), - ); - } - - Widget _buildScoreBoxes(GameModel model) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(flex: 1), - _buildScoreBox(PieceType.black, model), - const Spacer(flex: 4), - _buildScoreBox(PieceType.white, model), - const Spacer(flex: 1), + return MultiBlocProvider( + providers: [ + BlocProvider<NavCubit>(create: (context) => NavCubit()), + BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()), + BlocProvider<GameCubit>(create: (context) => GameCubit()), + BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()), + BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()), ], + child: BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return MaterialApp( + title: 'Reversi', + home: const SkeletonScreen(), + + // Theme stuff + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state.themeMode, + + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + ); + }, + ), ); } - Table _buildGameBoardDisplay(BuildContext context, GameModel model) { - final rows = <TableRow>[]; - - for (var y = 0; y < GameBoard.height; y++) { - final cells = <Column>[]; - - for (var x = 0; x < GameBoard.width; x++) { - PieceType pieceType = model.board.getPieceAtLocation(x, y); - String? assetImageCode = Styling.assetImageCodes[pieceType]; - String assetImageName = 'assets/skins/${model.skin}_tile_${assetImageCode ?? 'empty'}.png'; + List<String> getImagesAssets() { + final List<String> assets = []; + + const List<String> gameImages = [ + 'button_back', + 'button_delete_saved_game', + 'button_resume_game', + 'button_start', + 'game_fail', + 'game_win', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } - Column cell = Column( - children: [ - Container( - decoration: Styling.boardCellDecoration, - child: SizedBox( - child: GestureDetector( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - transitionBuilder: (Widget child, Animation<double> animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: Image( - image: AssetImage(assetImageName), - fit: BoxFit.fill, - key: ValueKey<int>(pieceType == PieceType.empty - ? 0 - : (pieceType == PieceType.black ? 1 : 2)), - ), - ), - onTap: () { - _attemptUserMove(model, x, y); - }, - ), - ), - ) - ], - ); + final List<String> skinImages = [ + 'tile_black', + 'tile_empty', + 'tile_white', + ]; - cells.add(cell); + for (String skin in DefaultGlobalSettings.allowedSkinValues) { + for (String image in skinImages) { + assets.add('assets/skins/${skin}_$image.png'); } - - rows.add(TableRow(children: cells)); } - return Table(defaultColumnWidth: const IntrinsicColumnWidth(), children: rows); - } - - Widget _buildThinkingIndicator(GameModel model) { - return ThinkingIndicator( - color: Styling.thinkingColor, - height: Styling.thinkingSize, - visible: model.player == PieceType.white, - ); - } - - Widget _buildRestartGameWidget() { - return Container( - padding: const EdgeInsets.symmetric( - vertical: 5.0, - horizontal: 15.0, - ), - child: const Image( - image: AssetImage('assets/icons/button_restart.png'), - fit: BoxFit.fill, - ), - ); - } - - Widget _buildEndGameWidget(GameModel model) { - Image decorationImage = Image( - image: AssetImage(model.gameResultString == 'black' - ? 'assets/icons/game_win.png' - : 'assets/icons/empty.png'), - fit: BoxFit.fill, - ); - - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Table(defaultColumnWidth: const IntrinsicColumnWidth(), children: [ - TableRow( - children: [ - Column(children: [decorationImage]), - Column(children: [decorationImage]), - Column(children: [ - GestureDetector( - onTap: () { - _restartController.add( - GameModel(board: GameBoard()), - ); - }, - child: _buildRestartGameWidget(), - ) - ]), - Column(children: [decorationImage]), - Column(children: [decorationImage]), - ], - ), - ])); - } - - // Builds out the Widget tree using the most recent GameModel from the stream. - Widget _buildWidgets(BuildContext context, GameModel model) { - return Scaffold( - appBar: AppBar( - actions: [ - TextButton( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Colors.blue, - width: 4, - ), - ), - margin: const EdgeInsets.all(8), - child: const Image( - image: AssetImage('assets/icons/button_restart.png'), - fit: BoxFit.fill, - ), - ), - onPressed: () => _restartController.add(GameModel(board: GameBoard())), - ) - ], - ), - body: Container( - padding: const EdgeInsets.only( - top: 5.0, - left: 5.0, - right: 5.0, - ), - decoration: Styling.mainWidgetDecoration, - child: SafeArea( - child: Column( - children: [ - _buildScoreBoxes(model), - const SizedBox(height: 5), - _buildGameBoardDisplay(context, model), - const SizedBox(height: 5), - _buildThinkingIndicator(model), - if (model.gameIsOver) _buildEndGameWidget(model), - ], - ), - ), - ), - ); + return assets; } } diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..22b2fd83a4de1d7741d4a7249b39fa0a7a18395b --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,117 @@ +import 'package:reversi/models/game/game_board.dart'; +import 'package:reversi/models/game/game_model.dart'; +import 'package:reversi/models/settings/settings_game.dart'; +import 'package:reversi/models/settings/settings_global.dart'; +import 'package:reversi/utils/tools.dart'; + +class Game { + Game({ + // Settings + required this.gameSettings, + required this.globalSettings, + + // State + this.isRunning = false, + this.isStarted = false, + this.isFinished = false, + this.animationInProgress = false, + + // Base data + required this.gameModel, + + // Game data + this.score = 0, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + final GameModel gameModel; + + // Game data + int score; + + factory Game.createNull() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + // Base data + gameModel: GameModel(board: GameBoard()), + // Game data + score: 0, + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + // Base data + gameModel: GameModel(board: GameBoard()), + // Game data + score: 0, + ); + } + + bool get canBeResumed => isStarted && !isFinished; + + void dump() { + printlog(''); + printlog('## Current game dump:'); + printlog(''); + printlog('$Game:'); + printlog(' Settings'); + gameSettings.dump(); + globalSettings.dump(); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' Base data'); + printlog(' gameModel: $gameModel'); + printlog(' Game data'); + printlog(' score: $score'); + printlog(''); + } + + @override + String toString() { + return '$Game(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + // Settings + 'gameSettings': gameSettings.toJson(), + 'globalSettings': globalSettings.toJson(), + // State + 'isRunning': isRunning, + 'isStarted': isStarted, + 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + // Base data + // 'gameModel': gameModel, + // Game data + 'score': score, + }; + } +} diff --git a/lib/game_board.dart b/lib/models/game/game_board.dart similarity index 100% rename from lib/game_board.dart rename to lib/models/game/game_board.dart diff --git a/lib/game_model.dart b/lib/models/game/game_model.dart similarity index 88% rename from lib/game_model.dart rename to lib/models/game/game_model.dart index b20952a568522a3dac95b4824b551cd61d32e747..aa31385e62f2c920f64a7240a3a106a3c9f383a8 100644 --- a/lib/game_model.dart +++ b/lib/models/game/game_model.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'game_board.dart'; +import 'package:reversi/models/game/game_board.dart'; /// A model representing the state of a game of reversi. It's a board and the /// player who's next to go, essentially. @@ -39,7 +39,10 @@ class GameModel { /// returned. If unsuccessful, null is returned. GameModel updateForMove(int x, int y) { if (!board.isLegalMove(x, y, player)) { - return GameModel(board: board, player: player); + return GameModel( + board: board, + player: player, + ); } final newBoard = board.updateForMove(x, y, player); @@ -53,6 +56,9 @@ class GameModel { nextPlayer = PieceType.empty; } - return GameModel(board: newBoard, player: nextPlayer); + return GameModel( + board: newBoard, + player: nextPlayer, + ); } } diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..68831589adedbd206dd7ffe417c7ddce893f23b8 --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,58 @@ +import 'package:reversi/config/default_game_settings.dart'; +import 'package:reversi/utils/tools.dart'; + +class GameSettings { + final String itemsCount; + final String timerValue; + + GameSettings({ + required this.itemsCount, + required this.timerValue, + }); + + // Getters to convert String to int + int get itemsCountValue => int.parse(itemsCount); + int get timerCountValue => int.parse(timerValue); + + static String getItemsCountValueFromUnsafe(String itemsCount) { + if (DefaultGameSettings.allowedGameModeValues.contains(itemsCount)) { + return itemsCount; + } + + return DefaultGameSettings.defaultGameModeValue; + } + + static String getTimerValueFromUnsafe(String timerValue) { + if (DefaultGameSettings.allowedDifficultyLevelValues.contains(timerValue)) { + return timerValue; + } + + return DefaultGameSettings.difficultyLevelValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + itemsCount: DefaultGameSettings.defaultGameModeValue, + timerValue: DefaultGameSettings.difficultyLevelValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeGameMode}: $itemsCount'); + printlog(' ${DefaultGameSettings.parameterCodeDifficultyLevel}: $timerValue'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: itemsCount, + DefaultGameSettings.parameterCodeDifficultyLevel: timerValue, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..1260f1fb345b52dd649cab8698765c64eb838935 --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:reversi/config/default_global_settings.dart'; +import 'package:reversi/utils/tools.dart'; + +class GlobalSettings { + String skin; + + GlobalSettings({ + required this.skin, + }); + + static String getSkinValueFromUnsafe(String skin) { + if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) { + return skin; + } + + return DefaultGlobalSettings.defaultSkinValue; + } + + factory GlobalSettings.createDefault() { + return GlobalSettings( + skin: DefaultGlobalSettings.defaultSkinValue, + ); + } + + void dump() { + printlog('$GlobalSettings:'); + printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin'); + printlog(''); + } + + @override + String toString() { + return '$GlobalSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: skin, + }; + } +} diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart new file mode 100644 index 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad --- /dev/null +++ b/lib/ui/helpers/app_titles.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ); + } +} + +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/helpers/styled_button.dart b/lib/ui/helpers/styled_button.dart new file mode 100644 index 0000000000000000000000000000000000000000..c79eada33578b6786a372cc0a4401f088eaa003c --- /dev/null +++ b/lib/ui/helpers/styled_button.dart @@ -0,0 +1,210 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; + +import 'package:reversi/utils/color_extensions.dart'; + +class StyledButton extends StatelessWidget { + const StyledButton({ + super.key, + required this.color, + required this.onPressed, + this.onLongPress, + required this.child, + }); + + final Color color; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final Widget child; + + factory StyledButton.text({ + Key? key, + required VoidCallback? onPressed, + VoidCallback? onLongPress, + required String caption, + required Color color, + }) { + final Widget captionWidget = AutoSizeText( + caption, + maxLines: 1, + style: TextStyle( + inherit: true, + fontWeight: FontWeight.w900, + color: color.darken(60), + shadows: [ + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, -2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, -2), + ), + ], + ), + ); + + return StyledButton( + color: color, + onPressed: onPressed, + onLongPress: onLongPress, + child: captionWidget, + ); + } + + factory StyledButton.icon({ + Key? key, + required VoidCallback? onPressed, + VoidCallback? onLongPress, + required Icon icon, + required Color color, + required double iconSize, + }) { + return StyledButton( + color: color, + onPressed: onPressed, + onLongPress: onLongPress, + child: Icon( + icon.icon, + color: icon.color ?? color.darken(60), + size: iconSize, + shadows: [ + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(2, -2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, 2), + ), + Shadow( + blurRadius: 5.0, + color: color.lighten(60), + offset: const Offset(-2, -2), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + const double borderWidth = 4; + final Color borderColor = color.darken(40); + const double borderRadius = 10; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: color, + border: Border.all( + color: borderColor, + width: borderWidth, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + child: CustomPaint( + painter: StyledButtonPainter( + baseColor: color, + ), + child: MaterialButton( + onPressed: onPressed, + onLongPress: onLongPress, + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 40, + child: child, + ), + ), + ); + } +} + +class StyledButtonPainter extends CustomPainter { + StyledButtonPainter({ + required this.baseColor, + }); + + final Color baseColor; + + @override + void paint(Canvas canvas, Size size) { + final Color lightColor = baseColor.lighten(20); + final Color darkColor = baseColor.darken(20); + + final Paint paint = Paint()..style = PaintingStyle.fill; + + const double cornerRadius = 6; + + Path topPath = Path() + ..moveTo(cornerRadius, 0) + ..lineTo(size.width - cornerRadius, 0) + ..arcToPoint( + Offset(size.width, cornerRadius), + radius: const Radius.circular(cornerRadius), + ) + ..lineTo(size.width, size.height * .35) + ..quadraticBezierTo( + size.width * .4, + size.height * .1, + 0, + size.height * .3, + ) + ..lineTo(0, cornerRadius) + ..arcToPoint( + const Offset(cornerRadius, 0), + radius: const Radius.circular(cornerRadius), + ); + + Path bottomPath = Path() + ..moveTo(cornerRadius, size.height) + ..lineTo(size.width - cornerRadius, size.height) + ..arcToPoint( + Offset(size.width, size.height - cornerRadius), + radius: const Radius.circular(cornerRadius), + clockwise: false, + ) + ..lineTo(size.width, size.height * .7) + ..quadraticBezierTo( + size.width * .6, + size.height * .9, + 0, + size.height * .7, + ) + ..lineTo(0, size.height - cornerRadius) + ..arcToPoint( + Offset(cornerRadius, size.height), + radius: const Radius.circular(cornerRadius), + clockwise: false, + ); + + paint.color = lightColor; + canvas.drawPath(topPath, paint); + + paint.color = darkColor; + canvas.drawPath(bottomPath, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..45d1958818a712cb090ecc8a4f0439b337cfceae --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:reversi/ui/widgets/game/game_board.dart'; + +class GameLayout extends StatelessWidget { + const GameLayout({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topCenter, + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const GameBoardWidget(), + const Expanded(child: SizedBox.shrink()), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..55acc0b5d209bd30097485811a7dd78de9b0e9ec --- /dev/null +++ b/lib/ui/layouts/parameters_layout.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/config/default_game_settings.dart'; +import 'package:reversi/config/default_global_settings.dart'; +import 'package:reversi/cubit/settings_game_cubit.dart'; +import 'package:reversi/cubit/settings_global_cubit.dart'; +import 'package:reversi/ui/parameters/parameter_widget.dart'; +import 'package:reversi/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:reversi/ui/widgets/actions/button_game_start_new.dart'; +import 'package:reversi/ui/widgets/actions/button_resume_saved_game.dart'; + +class ParametersLayout extends StatelessWidget { + const ParametersLayout({super.key, required this.canResume}); + + final bool canResume; + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Game settings + for (String code in DefaultGameSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: false, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + lines.add(Expanded( + child: SizedBox(height: separatorHeight), + )); + + if (canResume == false) { + // Start new game + lines.add( + const AspectRatio( + aspectRatio: 3, + child: StartNewGameButton(), + ), + ); + } else { + // Resume game + lines.add(const AspectRatio( + aspectRatio: 3, + child: ResumeSavedGameButton(), + )); + // Delete saved game + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 5, + child: const DeleteSavedGameButton(), + )); + } + + lines.add(SizedBox(height: separatorHeight)); + + // Global settings + for (String code in DefaultGlobalSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: true, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + return Column( + children: lines, + ); + } + + List<Widget> buildParametersLine({ + required String code, + required bool isGlobal, + }) { + final List<Widget> parameterButtons = []; + + final List<String> availableValues = isGlobal + ? DefaultGlobalSettings.getAvailableValues(code) + : DefaultGameSettings.getAvailableValues(code); + + if (availableValues.length <= 1) { + return []; + } + + for (String value in availableValues) { + final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + final GameSettingsCubit gameSettingsCubit = + BlocProvider.of<GameSettingsCubit>(context); + final GlobalSettingsCubit globalSettingsCubit = + BlocProvider.of<GlobalSettingsCubit>(context); + + final String currentValue = isGlobal + ? globalSettingsCubit.getParameterValue(code) + : gameSettingsCubit.getParameterValue(code); + + final bool isSelected = (value == currentValue); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / availableValues.length - 4; + + return SizedBox.square( + dimension: itemWidth, + child: ParameterWidget( + code: code, + value: value, + isSelected: isSelected, + size: itemWidth, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + onPressed: () { + isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value); + }, + ), + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..cc7b3edf4fd047b6275274498f016e33941f5f45 --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,71 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:reversi/models/settings/settings_game.dart'; +import 'package:reversi/models/settings/settings_global.dart'; + +import 'package:reversi/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final String value; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + // content + switch (code) { + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple block with text + void paintUnknownParameterItem( + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + final textSpan = TextSpan( + text: '$code\n$value', + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } +} diff --git a/lib/ui/parameters/parameter_widget.dart b/lib/ui/parameters/parameter_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..6a6941d89607e41bb7f968b7a60208442ac76b0a --- /dev/null +++ b/lib/ui/parameters/parameter_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:reversi/models/settings/settings_game.dart'; +import 'package:reversi/models/settings/settings_global.dart'; + +import 'package:reversi/ui/helpers/styled_button.dart'; +import 'package:reversi/utils/tools.dart'; + +class ParameterWidget extends StatelessWidget { + const ParameterWidget({ + super.key, + required this.code, + required this.value, + required this.isSelected, + required this.size, + required this.gameSettings, + required this.globalSettings, + required this.onPressed, + }); + + final String code; + final String value; + final bool isSelected; + final double size; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + final VoidCallback onPressed; + + static const Color buttonColorActive = Colors.blue; + static const Color buttonColorInactive = Colors.white; + static const double buttonBorderWidth = 4.0; + static const double buttonBorderRadius = 12.0; + + @override + Widget build(BuildContext context) { + Widget content = const SizedBox.shrink(); + + switch (code) { + default: + printlog('Unknown parameter: $code/$value'); + content = getUnknownParameterItem(); + } + + final Color buttonColor = isSelected ? buttonColorActive : buttonColorInactive; + + return Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: buttonColor, + width: buttonBorderWidth, + ), + ), + child: content, + ); + } + + // "unknown" parameter -> simple block with text + Widget getUnknownParameterItem() { + return StyledButton.text( + caption: '$code / $value', + color: Colors.grey, + onPressed: null, + ); + } +} diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart new file mode 100644 index 0000000000000000000000000000000000000000..1706939b8183cd797d296a846c8e0c2602343050 --- /dev/null +++ b/lib/ui/screens/page_about.dart @@ -0,0 +1,41 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'package:reversi/ui/helpers/app_titles.dart'; + +class PageAbout extends StatelessWidget { + const PageAbout({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + const SizedBox(height: 8), + const AppTitle(text: 'about_title'), + const Text('about_content').tr(), + FutureBuilder<PackageInfo>( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return const Text('about_version').tr( + namedArgs: { + 'version': snapshot.data!.version, + }, + ); + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..8605ef24fb42bd12781ae310d997427493eb30fb --- /dev/null +++ b/lib/ui/screens/page_game.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/models/game/game.dart'; +import 'package:reversi/ui/layouts/parameters_layout.dart'; +import 'package:reversi/ui/layouts/game_layout.dart'; + +class PageGame extends StatelessWidget { + const PageGame({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return currentGame.isRunning + ? const GameLayout() + : ParametersLayout(canResume: currentGame.canBeResumed); + }, + ); + } +} diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..3df22a1bb87618619ddcaa5d8fd118bef17137b1 --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:reversi/ui/helpers/app_titles.dart'; +import 'package:reversi/ui/settings/settings_form.dart'; + +class PageSettings extends StatelessWidget { + const PageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppTitle(text: 'settings_title'), + SizedBox(height: 8), + SettingsForm(), + ], + ), + ); + } +} diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..e7df8b8bd7043bede0900b9d63b913720c9e8b19 --- /dev/null +++ b/lib/ui/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:reversi/ui/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/settings/theme_card.dart b/lib/ui/settings/theme_card.dart new file mode 100644 index 0000000000000000000000000000000000000000..7581a2d3199384dfea7efb78741c519fb22ecac7 --- /dev/null +++ b/lib/ui/settings/theme_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/theme_cubit.dart'; + +class ThemeCard extends StatelessWidget { + const ThemeCard({ + super.key, + required this.mode, + required this.icon, + }); + + final IconData icon; + final ThemeMode mode; + + @override + Widget build(BuildContext context) { + return BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: state.themeMode == mode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(5), + child: InkWell( + onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( + ThemeModeState(themeMode: mode), + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + icon, + size: 32, + color: state.themeMode != mode + ? Theme.of(context).colorScheme.primary + : Colors.white, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart new file mode 100644 index 0000000000000000000000000000000000000000..a460301d4eb07994f60c7fabed5cb62968060d10 --- /dev/null +++ b/lib/ui/skeleton.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/config/menu.dart'; +import 'package:reversi/cubit/nav_cubit.dart'; +import 'package:reversi/ui/widgets/global_app_bar.dart'; + +class SkeletonScreen extends StatelessWidget { + const SkeletonScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const GlobalAppBar(), + extendBodyBehindAppBar: false, + body: Material( + color: Theme.of(context).colorScheme.surface, + child: BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + return Padding( + padding: const EdgeInsets.only( + top: 8, + left: 2, + right: 2, + ), + child: Menu.getPageWidget(pageIndex), + ); + }, + ), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + ); + } +} diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..ddc09a1d0e0a1cce905fde156a2efe11952db330 --- /dev/null +++ b/lib/ui/widgets/actions/button_delete_saved_game.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/ui/helpers/styled_button.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.grey, + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart new file mode 100644 index 0000000000000000000000000000000000000000..fbe48a34cf300c4d9e0650ffe31ff6e0aa37ab1e --- /dev/null +++ b/lib/ui/widgets/actions/button_game_quit.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/ui/helpers/styled_button.dart'; + +class QuitGameButton extends StatelessWidget { + const QuitGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.red, + onPressed: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart new file mode 100644 index 0000000000000000000000000000000000000000..9e88d264825404f4f3a5c1515ef1bdbabb73b3cd --- /dev/null +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/cubit/settings_game_cubit.dart'; +import 'package:reversi/cubit/settings_global_cubit.dart'; +import 'package:reversi/ui/helpers/styled_button.dart'; + +class StartNewGameButton extends StatelessWidget { + const StartNewGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + return StyledButton( + color: Colors.blue, + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, + child: const Image( + image: AssetImage('assets/ui/button_start.png'), + fit: BoxFit.fill, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..d28b455eed3747dd4e9422c186176146ae48e00d --- /dev/null +++ b/lib/ui/widgets/actions/button_resume_saved_game.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/ui/helpers/styled_button.dart'; + +class ResumeSavedGameButton extends StatelessWidget { + const ResumeSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return StyledButton( + color: Colors.blue, + onPressed: () { + BlocProvider.of<GameCubit>(context).resumeSavedGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_resume_game.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000000000000000000000000000000000000..5c8eb368149d15f5675774579a93d9be1c17a31e --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/game/game_engine.dart'; +import 'package:reversi/models/game/game.dart'; +import 'package:reversi/utils/tools.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + printlog(currentGame.toString()); + + return const GameEngine(); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_board_display.dart b/lib/ui/widgets/game/game_board_display.dart new file mode 100644 index 0000000000000000000000000000000000000000..3bbb2a5e11dd3b6da94e6adae1c9807ecb837806 --- /dev/null +++ b/lib/ui/widgets/game/game_board_display.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import 'package:reversi/models/game/game_board.dart'; +import 'package:reversi/models/game/game_model.dart'; +import 'package:reversi/config/styling.dart'; + +class GameBoardDisplay extends StatelessWidget { + const GameBoardDisplay({ + super.key, + required this.context, + required this.model, + required this.callback, + }); + + final BuildContext context; + final GameModel model; + final Function callback; + + @override + Widget build(BuildContext context) { + final rows = <TableRow>[]; + + for (int y = 0; y < GameBoard.height; y++) { + final cells = <Column>[]; + + for (int x = 0; x < GameBoard.width; x++) { + PieceType pieceType = model.board.getPieceAtLocation(x, y); + String? assetImageCode = Styling.assetImageCodes[pieceType]; + String assetImageName = + 'assets/skins/${model.skin}_tile_${assetImageCode ?? 'empty'}.png'; + + Column cell = Column( + children: [ + Container( + decoration: Styling.boardCellDecoration, + child: SizedBox( + child: GestureDetector( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation<double> animation) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + child: Image( + image: AssetImage(assetImageName), + fit: BoxFit.fill, + key: ValueKey<int>( + pieceType == PieceType.empty + ? 0 + : (pieceType == PieceType.black ? 1 : 2), + ), + ), + ), + onTap: () { + callback(model, x, y); + }, + ), + ), + ) + ], + ); + + cells.add(cell); + } + + rows.add(TableRow( + children: cells, + )); + } + + return Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: rows, + ); + } +} diff --git a/lib/ui/widgets/game/score_box.dart b/lib/ui/widgets/game/score_box.dart new file mode 100644 index 0000000000000000000000000000000000000000..d1916a728766ffb6711e95b9be91f548022b4b72 --- /dev/null +++ b/lib/ui/widgets/game/score_box.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import 'package:reversi/models/game/game_board.dart'; +import 'package:reversi/models/game/game_model.dart'; +import 'package:reversi/config/styling.dart'; + +class ScoreBox extends StatelessWidget { + const ScoreBox({ + super.key, + required this.player, + required this.model, + }); + + final PieceType player; + final GameModel model; + + @override + Widget build(BuildContext context) { + var assetImageCode = player == PieceType.black ? 'black' : 'white'; + String assetImageName = 'assets/skins/${model.skin}_tile_$assetImageCode.png'; + + var scoreText = player == PieceType.black ? '${model.blackScore}' : '${model.whiteScore}'; + + return Container( + padding: const EdgeInsets.symmetric( + vertical: 3.0, + horizontal: 25.0, + ), + decoration: (model.player == player) + ? Styling.activePlayerIndicator + : Styling.inactivePlayerIndicator, + child: Row( + children: <Widget>[ + SizedBox( + width: 25.0, + height: 25.0, + child: Image( + image: AssetImage(assetImageName), + fit: BoxFit.fill, + ), + ), + const SizedBox( + width: 10.0, + ), + Text( + scoreText, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 35.0, + color: Color(0xff000000), + ), + ), + ], + ), + ); + } +} diff --git a/lib/thinking_indicator.dart b/lib/ui/widgets/game/thinking_indicator.dart similarity index 98% rename from lib/thinking_indicator.dart rename to lib/ui/widgets/game/thinking_indicator.dart index b64739c7614ab782772ee4580468d98128b4c4b1..69aeaf2f8d6781f4710d246a9433f07592c349ee 100644 --- a/lib/thinking_indicator.dart +++ b/lib/ui/widgets/game/thinking_indicator.dart @@ -3,7 +3,8 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; -import 'styling.dart'; + +import 'package:reversi/config/styling.dart'; /// This is a self-animated progress spinner, only instead of spinning it /// moves five little circles in a horizontal arrangement. @@ -28,7 +29,6 @@ class ThinkingIndicator extends ImplicitlyAnimatedWidget { class _ThinkingIndicatorState extends AnimatedWidgetBaseState<ThinkingIndicator> { Tween<double>? _opacityTween; - @override Widget build(BuildContext context) { return Center( diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..ffc7930b886372a785f7de4eb71b35fe40577759 --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:reversi/config/menu.dart'; +import 'package:reversi/cubit/game_cubit.dart'; +import 'package:reversi/cubit/nav_cubit.dart'; +import 'package:reversi/models/game/game.dart'; +import 'package:reversi/ui/helpers/app_titles.dart'; +import 'package:reversi/ui/helpers/styled_button.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + final Game currentGame = gameState.currentGame; + + final List<Widget> menuActions = []; + + if (currentGame.isRunning && !currentGame.isFinished) { + menuActions.add(StyledButton( + color: Colors.red, + onPressed: () {}, + onLongPress: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + )); + } else { + if (pageIndex == Menu.indexGame) { + // go to Settings page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToSettingsPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemSettings.icon, + )); + + // go to About page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToAboutPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemAbout.icon, + )); + } else { + // back to Home page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToGamePage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemGame.icon, + )); + } + } + + return AppBar( + title: const AppHeader(text: 'app_name'), + actions: menuActions, + ); + }, + ); + }, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart new file mode 100644 index 0000000000000000000000000000000000000000..4e55e338f0d3ed98b233d1ef887b7b3e17e29d97 --- /dev/null +++ b/lib/utils/color_extensions.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + alpha, + (red * value).round(), + (green * value).round(), + (blue * value).round(), + ); + } + + Color lighten([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = percent / 100; + return Color.fromARGB( + alpha, + (red + ((255 - red) * value)).round(), + (green + ((255 - green) * value)).round(), + (blue + ((255 - blue) * value)).round(), + ); + } + + Color avg(Color other) { + final red = (this.red + other.red) ~/ 2; + final green = (this.green + other.green) ~/ 2; + final blue = (this.blue + other.blue) ~/ 2; + final alpha = (this.alpha + other.alpha) ~/ 2; + return Color.fromARGB(alpha, red, green, blue); + } +} diff --git a/pubspec.lock b/pubspec.lock index fc4dddda5e0f692204218f1817cf4a11e2444902..c4807cc893593f0a6b2dbef258bfc7d941409824 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" async: dependency: "direct main" description: @@ -9,6 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -17,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" collection: dependency: transitive description: @@ -25,6 +57,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" ffi: dependency: transitive description: @@ -46,6 +110,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -54,11 +126,56 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c + url: "https://pub.dev" + source: hosted + version: "9.1.5" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" lints: dependency: transitive description: @@ -91,6 +208,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" + url: "https://pub.dev" + source: hosted + version: "8.0.3" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: transitive description: @@ -99,6 +232,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + url: "https://pub.dev" + source: hosted + version: "2.2.12" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -140,7 +297,7 @@ packages: source: hosted version: "2.1.8" provider: - dependency: "direct main" + dependency: transitive description: name: provider sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c @@ -148,7 +305,7 @@ packages: source: hosted version: "6.1.2" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" @@ -208,6 +365,54 @@ packages: description: flutter source: sdk version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: f3eab9d87c226415ef857cfd2167e1d12ad81ea1f5783b46cf644224fea4eab7 + url: "https://pub.dev" + source: hosted + version: "3.0.0" vector_math: dependency: transitive description: @@ -224,6 +429,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd + url: "https://pub.dev" + source: hosted + version: "5.6.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1ae88f29216a51ae9c34ba8812f24c8a313f22f3..7cf23d6c99793c2d75b97cce715fee294de5c44f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,28 @@ -name: simpler_reversi +name: reversi description: A reversi game application. + publish_to: 'none' -version: 0.1.1+19 +version: 0.1.2+20 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter - provider: ^6.0.5 - shared_preferences: ^2.2.1 + + # base + auto_size_text: ^3.0.0 + easy_localization: ^3.0.1 + equatable: ^2.0.5 + flutter_bloc: ^8.1.1 + hive: ^2.2.3 + hydrated_bloc: ^9.0.0 + package_info_plus: ^8.0.0 + path_provider: ^2.0.11 + unicons: ^3.0.0 + + # specific async: ^2.11.0 dev_dependencies: @@ -19,5 +31,18 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/icons/ - assets/skins/ + - assets/translations/ + - assets/ui/ + + fonts: + - family: Nunito + fonts: + - asset: assets/fonts/Nunito-Bold.ttf + weight: 700 + - asset: assets/fonts/Nunito-Medium.ttf + weight: 500 + - asset: assets/fonts/Nunito-Regular.ttf + weight: 400 + - asset: assets/fonts/Nunito-Light.ttf + weight: 300 diff --git a/resources/app/build_application_resources.sh b/resources/app/build_application_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..1ace90d0e0029bf1704122d2b60bced59d5ed348 --- /dev/null +++ b/resources/app/build_application_resources.sh @@ -0,0 +1,127 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { + echo >&2 "I require inkscape but it's not installed. Aborting." + exit 1 +} +command -v scour >/dev/null 2>&1 || { + echo >&2 "I require scour but it's not installed. Aborting." + exit 1 +} +command -v optipng >/dev/null 2>&1 || { + echo >&2 "I require optipng but it's not installed. Aborting." + exit 1 +} + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" + +SOURCE_ICON="${CURRENT_DIR}/icon.svg" +SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" +SOURCE_LAUNCH_IMAGE="${CURRENT_DIR}/icon.svg" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" + +if [ ! -f "${SOURCE_ICON}" ]; then + echo "Missing file: ${SOURCE_ICON}" +fi + +if [ ! -f "${SOURCE_FASTLANE}" ]; then + echo "Missing file: ${SOURCE_FASTLANE}" +fi + +if [ ! -f "${SOURCE_LAUNCH_IMAGE}" ]; then + echo "Missing file: ${SOURCE_LAUNCH_IMAGE}" +fi + +function optimize_svg() { + SVG="$1" + + cp ${SVG} ${SVG}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SVG}.tmp \ + -o ${SVG} + rm ${SVG}.tmp +} + +# optimize source svg files +optimize_svg ${SOURCE_ICON} +optimize_svg ${SOURCE_FASTLANE} +optimize_svg ${SOURCE_LAUNCH_IMAGE} + +# build icons +function build_application_icon() { + ICON_SIZE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + TARGET_PNG="${TARGET}.png" + + inkscape \ + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_ICON} + + optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} +} + +# build fastlane image +function build_fastlane_image() { + WIDTH="$1" + HEIGHT="$2" + TARGET="$3" + + echo "Building ${TARGET}" + + TARGET_PNG="${TARGET}.png" + + inkscape \ + --export-width=${WIDTH} \ + --export-height=${HEIGHT} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_FASTLANE} + + optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} +} + +# build launch images (splash screen) +function build_launch_image() { + ICON_SIZE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + TARGET_PNG="${TARGET}.png" + + inkscape \ + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET_PNG} \ + ${SOURCE_LAUNCH_IMAGE} + + optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG} +} + +build_application_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher +build_application_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher +build_application_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher +build_application_icon 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher +build_application_icon 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher +build_application_icon 512 ${BASE_DIR}/fastlane/metadata/android/en-US/images/icon + +build_launch_image 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/launch_image +build_launch_image 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/launch_image +build_launch_image 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/launch_image +build_launch_image 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/launch_image +build_launch_image 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/launch_image + +build_fastlane_image 1024 500 ${BASE_DIR}/fastlane/metadata/android/en-US/images/featureGraphic diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg similarity index 100% rename from icons/featureGraphic.svg rename to resources/app/featureGraphic.svg diff --git a/icons/icon.svg b/resources/app/icon.svg similarity index 100% rename from icons/icon.svg rename to resources/app/icon.svg diff --git a/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..774953c5b885aae73f710aaa9d8b55a0d8dcc2c0 --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +${CURRENT_DIR}/app/build_application_resources.sh +${CURRENT_DIR}/ui/build_ui_resources.sh diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..f6c3f33201ae81b1f43cf677bb76c91e04727f33 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,144 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { + echo >&2 "I require inkscape but it's not installed. Aborting." + exit 1 +} +command -v scour >/dev/null 2>&1 || { + echo >&2 "I require scour but it's not installed. Aborting." + exit 1 +} +command -v optipng >/dev/null 2>&1 || { + echo >&2 "I require optipng but it's not installed. Aborting." + exit 1 +} + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" +ASSETS_DIR="${BASE_DIR}/assets" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" +IMAGE_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_SVG_IMAGES="" +AVAILABLE_GAME_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_SVG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" + AVAILABLE_GAME_PNG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" +fi + +# Skins (subfolders found in `skins` folder) +AVAILABLE_SKINS="" +if [ -d "${CURRENT_DIR}/skins" ]; then + AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')" +fi + +# Images per skin (svg files found recursively in `skins` folder and subfolders) +SKIN_SVG_IMAGES="" +SKIN_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_SVG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" + SKIN_PNG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" +fi + +####################################################### + +# optimize svg +function optimize_svg() { + SOURCE="$1" + + cp ${SOURCE} ${SOURCE}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SOURCE}.tmp \ + -o ${SOURCE} + rm ${SOURCE}.tmp +} + +# build png from svg +function build_svg_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + optimize_svg "${SOURCE}" + + mkdir -p "$(dirname "${TARGET}")" + + inkscape \ + --export-width=${IMAGE_SIZE} \ + --export-height=${IMAGE_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +# build png from png +function build_png_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + mkdir -p "$(dirname "${TARGET}")" + + convert -resize ${IMAGE_SIZE}x${IMAGE_SIZE} "${SOURCE}" "${TARGET}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_images_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_SVG_IMAGE in ${SKIN_SVG_IMAGES}; do + build_svg_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_SVG_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_SVG_IMAGE}.png + done + for SKIN_PNG_IMAGE in ${SKIN_PNG_IMAGES}; do + build_png_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_PNG_IMAGE}.png ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_PNG_IMAGE}.png + done +} + +####################################################### + +# Delete existing generated images +if [ -d "${ASSETS_DIR}/ui" ]; then + find ${ASSETS_DIR}/ui -type f -name "*.png" -delete +fi +if [ -d "${ASSETS_DIR}/skins" ]; then + find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +fi + +# build game images +for GAME_SVG_IMAGE in ${AVAILABLE_GAME_SVG_IMAGES}; do + build_svg_image ${CURRENT_DIR}/images/${GAME_SVG_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_SVG_IMAGE}.png +done +for GAME_PNG_IMAGE in ${AVAILABLE_GAME_PNG_IMAGES}; do + build_png_image ${CURRENT_DIR}/images/${GAME_PNG_IMAGE}.png ${ASSETS_DIR}/ui/${GAME_PNG_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS}; do + build_images_for_skin "${SKIN}" +done diff --git a/resources/ui/images/button_back.svg b/resources/ui/images/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..018d8b734d2932028fbfce1643c4e888ff1b45b1 --- /dev/null +++ b/resources/ui/images/button_back.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.3783 .61747 -.61747 1.3783 45.198 93.762)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#950e4f" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg> diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..c3f872e434052a6b4e7036b530ced8e6233508e4 --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m76.652 23.303-3.6441 58.302c-0.28153 4.5103-4.0223 8.0241-8.5413 8.0241h-35.27c-4.5189 0-8.2598-3.5138-8.5413-8.0241l-3.6441-58.302h-5.4824c-1.7723 0-3.2093-1.437-3.2093-3.2093 0-1.773 1.437-3.2093 3.2093-3.2093h70.605c1.7723 0 3.2093 1.4363 3.2093 3.2093 0 1.7723-1.437 3.2093-3.2093 3.2093zm-6.8314 0h-45.979l3.0819 55.867c0.12535 2.268 2.0008 4.0433 4.2732 4.0433h31.268c2.2724 0 4.1478-1.7752 4.2732-4.0433zm-22.99 6.4188c1.6541 0 2.9952 1.3411 2.9952 2.9952v41.08c0 1.6541-1.3411 2.9952-2.9952 2.9952-1.6542 0-2.9952-1.3411-2.9952-2.9952v-41.08c0-1.6541 1.3411-2.9952 2.9952-2.9952zm-12.837 0c1.6756 0 3.0553 1.3181 3.1312 2.9921l1.8776 41.3c0.06665 1.4664-1.0681 2.7087-2.5345 2.7762-0.04011 0.0015-0.08024 0.0021-0.12108 0.0021-1.5595 0-2.8476-1.2193-2.9328-2.7774l-2.253-41.3c-0.08524-1.5646 1.114-2.9012 2.6779-2.9864 0.05157-0.0029 0.10317-0.0042 0.15474-0.0042zm25.675 0c1.5667 0 2.8361 1.2694 2.8361 2.8361 0 0.05156-6.87e-4 0.10317-0.0036 0.15474l-2.2416 41.088c-0.09171 1.6778-1.4786 2.991-3.1586 2.991-1.5667 0-2.8361-1.2694-2.8361-2.8361 0-0.05156 7.31e-4 -0.10315 0.0036-0.15474l2.2417-41.088c0.09172-1.6778 1.4786-2.991 3.1586-2.991zm-21.397-25.675h17.117c4.7265 0 8.5578 3.8313 8.5578 8.5578v4.2795h-34.231v-4.2795c0-4.7265 3.8313-8.5578 8.5578-8.5578zm0.42837 6.4188c-1.4184 0-2.5675 1.1491-2.5675 2.5675v3.8512h21.394v-3.8512c0-1.4184-1.1491-2.5675-2.5675-2.5675z" fill="#fff" fill-rule="evenodd" stroke="#050200"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..2bf973276aefa564ecff7d6149899298344819f9 --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-5.618)" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 55.567 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" stroke-width="7.2832"/><path d="m15.535 12.852 2e-3 67.973z" stroke-width="11"/></g></svg> diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..4d7634a9f3fb559e590ee965e1341ae2634bf80f --- /dev/null +++ b/resources/ui/images/button_start.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(-1.3783 -.61747 .61747 -1.3783 46.954 -.086035)" d="m11.645-14.603-44.77-4.6003 26.369-36.472z" fill="#fff" stroke="#105ea2" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.2832"/></svg> diff --git a/icons/empty.svg b/resources/ui/images/empty.svg similarity index 57% rename from icons/empty.svg rename to resources/ui/images/empty.svg index d988c4937ec1d00ce581b412ff4a859a0fd1862d..23ace81fbb82a8409cc0710c0f7bddd6381f7256 100644 --- a/icons/empty.svg +++ b/resources/ui/images/empty.svg @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/> diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000000000000000000000000000000000000..2922fd7adc2bd2e813836c728f095376c73d4143 --- /dev/null +++ b/resources/ui/images/game_fail.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#d11717" stroke="#fff" stroke-width=".238"/><path d="m71.624 59.304c3.5089 3.5089 3.5089 9.0561 0 12.565-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034l-12.452-12.452-12.452 12.452c-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034c-3.5089-3.5089-3.5089-9.0561 0-12.565l12.452-12.452-12.452-12.452c-3.5089-3.5089-3.5089-9.0561 0-12.565s9.0561-3.5089 12.565 0l12.452 12.452 12.452-12.452c3.5089-3.5089 9.0561-3.5089 12.565 0s3.5089 9.0561 0 12.565l-12.452 12.452z" fill="#e7e7e7" stroke-width=".20213"/></svg> diff --git a/icons/game_win.svg b/resources/ui/images/game_win.svg similarity index 100% rename from icons/game_win.svg rename to resources/ui/images/game_win.svg diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256 --- /dev/null +++ b/resources/ui/images/placeholder.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/> diff --git a/icons/skins/default/tile_black.svg b/resources/ui/skins/default/tile_black.svg similarity index 100% rename from icons/skins/default/tile_black.svg rename to resources/ui/skins/default/tile_black.svg diff --git a/icons/skins/default/tile_empty.svg b/resources/ui/skins/default/tile_empty.svg similarity index 100% rename from icons/skins/default/tile_empty.svg rename to resources/ui/skins/default/tile_empty.svg diff --git a/icons/skins/default/tile_white.svg b/resources/ui/skins/default/tile_white.svg similarity index 100% rename from icons/skins/default/tile_white.svg rename to resources/ui/skins/default/tile_white.svg