Skip to content
Snippets Groups Projects
Commit 5ebf63f4 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Improve game architecture

parent e9d89ace
No related branches found
No related tags found
1 merge request!18Resolve "Improve game architecture"
Pipeline #5492 passed
Showing
with 604 additions and 73 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.16
app.versionCode=16
app.versionName=0.0.17
app.versionCode=17
File added
File added
File added
File added
{
"app_name": "Solitaire",
"long_press_to_quit": "Long press to quit game...",
"bottom_nav_home": "Game",
"bottom_nav_settings": "Settings",
"bottom_nav_about": "About",
"settings_title": "Settings",
"settings_label_theme": "Theme mode",
"about_title": "About",
"about_content": "Solitaire.",
"about_version": "Version: {version}"
}
{
"app_name": "Solitaire",
"long_press_to_quit": "Appuyer longtemps pour quitter le jeu...",
"bottom_nav_home": "Jeu",
"bottom_nav_settings": "Réglages",
"bottom_nav_about": "Infos",
"settings_title": "Réglages",
"settings_label_theme": "Thème de couleurs",
"about_title": "Informations",
"about_content": "Solitaire.",
"about_version": "Version : {version}"
}
Improve game conception/architecture.
Amélioration de la conception/architecture du jeu.
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:solitaire/ui/screens/about_page.dart';
import 'package:solitaire/ui/screens/game_page.dart';
import 'package:solitaire/ui/screens/settings_page.dart';
class MenuItem {
final String code;
final Icon icon;
final Widget page;
const MenuItem({
required this.code,
required this.icon,
required this.page,
});
}
class Menu {
static List<MenuItem> items = [
const MenuItem(
code: 'bottom_nav_home',
icon: Icon(UniconsLine.home),
page: GamePage(),
),
const MenuItem(
code: 'bottom_nav_settings',
icon: Icon(UniconsLine.setting),
page: SettingsPage(),
),
const MenuItem(
code: 'bottom_nav_about',
icon: Icon(UniconsLine.info_circle),
page: AboutPage(),
),
];
static Widget getPageWidget(int pageIndex) {
return Menu.items.elementAt(pageIndex).page;
}
static List<BottomNavigationBarItem> getMenuItems() {
return Menu.items
.map((MenuItem item) => BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
))
.toList();
}
static int itemsCount = Menu.items.length;
}
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,
background: textSwatch.shade200,
onBackground: textSwatch.shade500,
onSurface: textSwatch.shade500,
surface: textSwatch.shade50,
surfaceVariant: Colors.white,
shadow: textSwatch.shade900.withOpacity(.1),
);
final ColorScheme darkColorScheme = ColorScheme.dark(
primary: primarySwatch.shade500,
secondary: primarySwatch.shade500,
onSecondary: Colors.white,
error: errorColor,
background: const Color(0xFF171724),
onBackground: textSwatch.shade400,
onSurface: textSwatch.shade300,
surface: const Color(0xFF262630),
surfaceVariant: 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',
),
),
);
final ThemeData appTheme = darkTheme;
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:solitaire/config/menu.dart';
class BottomNavCubit extends HydratedCubit<int> {
BottomNavCubit() : super(0);
void updateIndex(int index) {
if (isIndexAllowed(index)) {
emit(index);
} else {
goToHomePage();
}
}
bool isIndexAllowed(int index) {
return (index >= 0) && (index < Menu.itemsCount);
}
void goToHomePage() => emit(0);
@override
int fromJson(Map<String, dynamic> json) {
return 0;
}
@override
Map<String, dynamic>? toJson(int state) {
return <String, int>{'pageIndex': state};
}
}
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()};
}
}
part of 'theme_cubit.dart';
@immutable
class ThemeModeState extends Equatable {
const ThemeModeState({
this.themeMode,
});
final ThemeMode? themeMode;
@override
List<Object?> get props => <Object?>[
themeMode,
];
}
import 'package:flutter/material.dart';
import 'package:solitaire_game/provider/data.dart';
import 'package:solitaire_game/utils/game_utils.dart';
import 'package:solitaire/provider/data.dart';
import 'package:solitaire/utils/game_utils.dart';
class Tile {
int currentRow;
......
import 'package:flutter/material.dart';
import 'package:solitaire_game/entities/tile.dart';
import 'package:solitaire_game/provider/data.dart';
class Board {
static Widget buildGameBoard(Data myProvider) {
return Column(
children: [
buildGameTileset(myProvider),
],
);
}
static Table buildGameTileset(Data myProvider) {
List<List<Tile?>> board = myProvider.board;
Widget boardTileWithoutHole = Image(
image: AssetImage('assets/skins/${myProvider.parameterSkin}_board.png'),
width: myProvider.tileSize,
height: myProvider.tileSize,
fit: BoxFit.fill,
);
return Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
for (int row = 0; row < board.length; row++)
TableRow(
children: [
for (int col = 0; col < board[row].length; col++)
TableCell(
child: board[row][col] != null
? (board[row][col]?.render(myProvider) ?? Container())
: boardTileWithoutHole,
),
],
),
],
);
}
}
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
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:provider/provider.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:solitaire_game/provider/data.dart';
import 'package:solitaire_game/screens/home.dart';
void main() {
import 'package:solitaire/config/theme.dart';
import 'package:solitaire/cubit/bottom_nav_cubit.dart';
import 'package:solitaire/cubit/theme_cubit.dart';
import 'package:solitaire/provider/data.dart';
import 'package:solitaire/ui/skeleton.dart';
void main() async {
/// Initialize packages
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((value) => runApp(const MyApp()));
await EasyLocalization.ensureInitialized();
final Directory tmpDir = await getTemporaryDirectory();
Hive.init(tmpDir.toString());
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: tmpDir,
);
runApp(
EasyLocalization(
path: 'assets/translations',
supportedLocales: const <Locale>[
Locale('en'),
Locale('fr'),
],
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
......@@ -16,23 +44,39 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => Data(),
child: Consumer<Data>(builder: (context, data, child) {
return OverlaySupport(
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
return MultiBlocProvider(
providers: [
BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
],
child: BlocBuilder<ThemeCubit, ThemeModeState>(
builder: (BuildContext context, ThemeModeState state) {
return ChangeNotifierProvider(
create: (BuildContext context) => Data(),
child: Consumer<Data>(
builder: (context, data, child) {
return OverlaySupport(
child: MaterialApp(
title: 'Solitaire',
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,
),
);
},
),
home: const Home(),
routes: {
Home.id: (context) => const Home(),
},
),
);
}),
);
},
),
);
}
}
......@@ -2,8 +2,10 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solitaire_game/entities/tile.dart';
import 'package:solitaire_game/utils/game_utils.dart';
import 'package:solitaire/entities/tile.dart';
import 'package:solitaire/utils/game_utils.dart';
typedef Board = List<List<Tile?>>;
class Data extends ChangeNotifier {
// Configuration available values
......@@ -30,7 +32,7 @@ class Data extends ChangeNotifier {
bool _assetsPreloaded = false;
bool _gameIsRunning = false;
bool _gameIsFinished = false;
List<List<Tile?>> _board = [];
Board _board = [];
int _boardSize = 0;
double _tileSize = 0;
int _movesCount = 0;
......@@ -145,7 +147,7 @@ class Data extends ChangeNotifier {
Map<String, dynamic> getCurrentSavedState() {
if (_currentState != '') {
Map<String, dynamic> savedState = json.decode(_currentState);
final Map<String, dynamic> savedState = json.decode(_currentState);
if (savedState.isNotEmpty) {
return savedState;
}
......@@ -168,8 +170,8 @@ class Data extends ChangeNotifier {
_boardSize = boardSize;
}
List<List<Tile?>> get board => _board;
void updateBoard(List<List<Tile?>> board) {
Board get board => _board;
void updateBoard(Board board) {
_board = board;
updateBoardSize(board.length);
updateRemainingPegsCount(GameUtils.countRemainingPegs(this));
......
import 'package:flutter/material.dart';
import 'package:solitaire/provider/data.dart';
import 'package:solitaire/ui/layout/tileset.dart';
import 'package:solitaire/ui/widgets/game/indicator_top.dart';
import 'package:solitaire/ui/widgets/game/message_game_end.dart';
class Game extends StatelessWidget {
const Game({super.key, required this.myProvider});
final Data myProvider;
@override
Widget build(BuildContext context) {
final bool gameIsFinished = myProvider.gameIsFinished;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
TopIndicator(myProvider: myProvider),
const SizedBox(height: 2),
Expanded(
child: Tileset(myProvider: myProvider),
),
const SizedBox(height: 2),
Container(
child: gameIsFinished
? EndGameMessage(myProvider: myProvider)
: const SizedBox(height: 2),
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:solitaire_game/provider/data.dart';
import 'package:solitaire_game/utils/game_utils.dart';
class Parameters {
static double separatorHeight = 2.0;
static double blockMargin = 3.0;
static double blockPadding = 2.0;
static Color buttonBackgroundColor = Colors.white;
static Color buttonBorderColorActive = Colors.blue;
static Color buttonBorderColorInactive = Colors.white;
static double buttonBorderWidth = 10.0;
static double buttonBorderRadius = 8.0;
static double buttonPadding = 0.0;
static double buttonMargin = 0.0;
static Widget buildParametersSelector(Data myProvider) {
import 'package:solitaire/provider/data.dart';
import 'package:solitaire/ui/widgets/home/button_game_resume.dart';
import 'package:solitaire/ui/widgets/home/button_game_start_new.dart';
class Parameters extends StatelessWidget {
const Parameters({super.key, required this.myProvider});
final Data myProvider;
static const double separatorHeight = 2.0;
static const double blockMargin = 0.0;
static const double blockPadding = 0.0;
static const Color buttonBackgroundColor = Colors.white;
static const Color buttonBorderColorActive = Colors.blue;
static const Color buttonBorderColorInactive = Colors.white;
static const double buttonBorderWidth = 6.0;
static const double buttonBorderRadius = 8.0;
static const double buttonPadding = 0.0;
static const double buttonMargin = 0.0;
@override
Widget build(BuildContext context) {
List<Widget> lines = [];
List parameters = myProvider.availableParameters;
for (int index = 0; index < parameters.length; index++) {
lines.add(buildParameterSelector(myProvider, parameters[index]));
lines.add(SizedBox(height: separatorHeight));
lines.add(const SizedBox(height: separatorHeight));
}
myProvider.loadCurrentSavedState();
Widget buttonsBlock = myProvider.hasCurrentSavedState()
? buildResumeGameButton(myProvider)
: buildStartNewGameButton(myProvider);
? ResumeGameButton(myProvider: myProvider)
: StartNewGameButton(myProvider: myProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: separatorHeight),
const SizedBox(height: separatorHeight),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
......@@ -40,7 +47,7 @@ class Parameters {
children: lines,
),
),
SizedBox(height: separatorHeight),
const SizedBox(height: separatorHeight),
Container(
child: buttonsBlock,
),
......@@ -72,66 +79,7 @@ class Parameters {
);
}
static Container buildStartNewGameButton(Data myProvider) {
return Container(
margin: EdgeInsets.all(blockMargin),
padding: EdgeInsets.all(blockPadding),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
buildDecorationImageWidget(),
Column(
children: [
TextButton(
child: buildImageContainerWidget('button_start'),
onPressed: () => GameUtils.startNewGame(myProvider),
),
],
),
buildDecorationImageWidget(),
],
),
],
),
);
}
static Container buildResumeGameButton(Data myProvider) {
return Container(
margin: EdgeInsets.all(blockMargin),
padding: EdgeInsets.all(blockPadding),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
Column(
children: [
TextButton(
child: buildImageContainerWidget('button_delete_saved_game'),
onPressed: () => GameUtils.deleteSavedGame(myProvider),
),
],
),
Column(
children: [
TextButton(
child: buildImageContainerWidget('button_resume_game'),
onPressed: () => GameUtils.resumeSavedGame(myProvider),
),
],
),
buildDecorationImageWidget(),
],
),
],
),
);
}
static Widget buildParameterSelector(Data myProvider, String parameterCode) {
Widget buildParameterSelector(Data myProvider, String parameterCode) {
List availableValues = myProvider.getParameterAvailableValues(parameterCode);
if (availableValues.length == 1) {
......@@ -146,7 +94,7 @@ class Parameters {
for (int index = 0; index < availableValues.length; index++)
Column(
children: [
_buildParameterButton(myProvider, parameterCode, availableValues[index])
buildParameterButton(myProvider, parameterCode, availableValues[index])
],
),
],
......@@ -155,8 +103,7 @@ class Parameters {
);
}
static Widget _buildParameterButton(
Data myProvider, String parameterCode, String parameterValue) {
Widget buildParameterButton(Data myProvider, String parameterCode, String parameterValue) {
String currentValue = myProvider.getParameterValue(parameterCode).toString();
bool isActive = (parameterValue == currentValue);
......@@ -164,8 +111,8 @@ class Parameters {
return TextButton(
child: Container(
margin: EdgeInsets.all(buttonMargin),
padding: EdgeInsets.all(buttonPadding),
margin: const EdgeInsets.all(buttonMargin),
padding: const EdgeInsets.all(buttonPadding),
decoration: BoxDecoration(
color: buttonBackgroundColor,
borderRadius: BorderRadius.circular(buttonBorderRadius),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment