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

Merge branch '39-improve-game-architecture' into 'master'

Resolve "Improve game architecture"

Closes #39

See merge request !37
parents 1545c652 46154a76
Branches
Tags Release_0.0.37_37
1 merge request!37Resolve "Improve game architecture"
Pipeline #5601 passed
Showing
with 485 additions and 242 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.36
app.versionCode=36
app.versionName=0.0.37
app.versionCode=37
File added
File added
File added
File added
{
"app_name": "Colors",
"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": "Colors, a colorful flood game.",
"about_version": "Version: {version}"
}
{
"app_name": "Colors",
"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": "Colors, un jeu de remplissage haut en couleurs.",
"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:colors/ui/screens/about_page.dart';
import 'package:colors/ui/screens/game_page.dart';
import 'package:colors/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:colors/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:colors/provider/data.dart';
import 'package:colors/utils/board_utils.dart';
import 'package:flutter/material.dart';
class Cell {
int value;
......@@ -9,14 +10,14 @@ class Cell {
this.value,
);
Container widgetFillBoardWithColor(Data myProvider) {
String imageAsset = getImageAssetName(myProvider);
Container interactiveWidget(Data myProvider, ColorScheme colorScheme) {
final String imageAsset = 'assets/skins/${myProvider.parameterSkin}_$value.png';
return Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
color: colorScheme.onSurface,
width: 4,
),
),
......@@ -26,17 +27,11 @@ class Cell {
fit: BoxFit.fill,
),
onTap: () {
if (!myProvider.animationInProgress &&
myProvider.getFirstCellValue() != value) {
if (!myProvider.animationInProgress && myProvider.getFirstCellValue() != value) {
BoardUtils.fillBoardFromFirstCell(myProvider, value);
}
},
),
);
}
String getImageAssetName(Data myProvider) {
int cellValue = value;
return 'assets/skins/${myProvider.parameterSkin}_$cellValue.png';
}
}
import 'package:colors/provider/data.dart';
import 'package:colors/utils/game_utils.dart';
import 'package:flutter/material.dart';
class Parameters {
static double separatorHeight = 2.0;
static double blockMargin = 8.0;
static double blockPadding = 3.0;
static Color buttonBackgroundColor = Colors.white;
static Color buttonBorderColorActive = Colors.blue;
static Color buttonBorderColorInactive = Colors.white;
static double buttonBorderWidth = 8.0;
static double buttonBorderRadius = 6.0;
static double buttonPadding = 0.0;
static double buttonMargin = 0.0;
static Widget buildParametersSelector(Data myProvider) {
List<Widget> lines = [];
List<String> parameters = myProvider.availableParameters;
for (var index = 0; index < parameters.length; index++) {
lines.add(Parameters.buildParameterSelector(myProvider, parameters[index]));
lines.add(SizedBox(height: Parameters.separatorHeight));
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: Parameters.separatorHeight),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: lines,
),
),
SizedBox(height: Parameters.separatorHeight),
Container(
child: Parameters.buildStartGameButton(myProvider),
),
],
);
}
static Widget buildStartGameButton(Data myProvider) {
Column decorationImage = const Column(
children: [
Image(
image: AssetImage('assets/icons/game_win.png'),
fit: BoxFit.fill,
),
],
);
return Container(
margin: EdgeInsets.all(Parameters.blockMargin),
padding: EdgeInsets.all(Parameters.blockPadding),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
decorationImage,
Column(
children: [
TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.all(0)),
),
child: const Image(
image: AssetImage('assets/icons/button_start.png'),
fit: BoxFit.fill,
),
onPressed: () => GameUtils.startGame(myProvider),
),
],
),
decorationImage,
],
),
],
),
);
}
static Widget buildParameterSelector(Data myProvider, String parameterCode) {
List<String> availableValues = myProvider.getParameterAvailableValues(parameterCode);
if (availableValues.length == 1) {
return const SizedBox(height: 0.0);
}
return Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
for (var index = 0; index < availableValues.length; index++)
Column(
children: [
_buildParameterButton(myProvider, parameterCode, availableValues[index])
],
),
],
),
],
);
}
static Widget _buildParameterButton(
Data myProvider, String parameterCode, String parameterValue) {
String currentValue = myProvider.getParameterValue(parameterCode).toString();
bool isActive = (parameterValue == currentValue);
String imageAsset = 'assets/icons/${parameterCode}_$parameterValue.png';
return TextButton(
child: Container(
margin: EdgeInsets.all(Parameters.buttonMargin),
padding: EdgeInsets.all(Parameters.buttonPadding),
decoration: BoxDecoration(
color: Parameters.buttonBackgroundColor,
borderRadius: BorderRadius.circular(Parameters.buttonBorderRadius),
border: Border.all(
color: isActive
? Parameters.buttonBorderColorActive
: Parameters.buttonBorderColorInactive,
width: Parameters.buttonBorderWidth,
),
),
child: Image(
image: AssetImage(imageAsset),
fit: BoxFit.fill,
),
),
onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue),
);
}
}
import 'package:colors/provider/data.dart';
import 'package:colors/screens/home.dart';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
void main() {
import 'package:colors/config/theme.dart';
import 'package:colors/cubit/bottom_nav_cubit.dart';
import 'package:colors/cubit/theme_cubit.dart';
import 'package:colors/provider/data.dart';
import 'package:colors/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,20 +44,34 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
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: 'Minehunter',
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,
theme: ThemeData(
primaryColor: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const Home(),
routes: {
Home.id: (context) => const Home(),
);
},
),
);
......
import 'package:colors/entities/cell.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:colors/entities/cell.dart';
typedef Board = List<List<Cell>>;
class Data extends ChangeNotifier {
// Configuration available parameters
final List<String> _availableParameters = ['level', 'size', 'colors', 'skin'];
......@@ -42,7 +45,7 @@ class Data extends ChangeNotifier {
int _colorsCount = 0;
int _movesCount = 0;
int _maxMovesCount = 0;
List<List<Cell>> _cells = [];
Board _board = [];
int _progress = 0;
int _progressTotal = 0;
......@@ -252,23 +255,23 @@ class Data extends ChangeNotifier {
_colorsCount = colorsCount;
}
List<List<Cell>> get cells => _cells;
void updateCells(List<List<Cell>> cells) {
_cells = cells;
Board get board => _board;
void updateBoard(Board board) {
_board = board;
notifyListeners();
}
updateCellValue(int col, int row, int value) {
_cells[row][col].value = value;
_board[row][col].value = value;
notifyListeners();
}
int getFirstCellValue() {
return _cells[0][0].value;
return _board[0][0].value;
}
int getCellValue(int col, int row) {
return _cells[row][col].value;
return _board[row][col].value;
}
int get movesCount => _movesCount;
......
import 'package:colors/layout/game.dart';
import 'package:colors/layout/parameters.dart';
import 'package:colors/provider/data.dart';
import 'package:colors/utils/game_utils.dart';
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart';
class Home extends StatefulWidget {
const Home({super.key});
static const String id = 'home';
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
@override
void initState() {
super.initState();
Data myProvider = Provider.of<Data>(context, listen: false);
myProvider.initParametersValues();
}
@override
Widget build(BuildContext context) {
Data myProvider = Provider.of<Data>(context);
double boardWidth = MediaQuery.of(context).size.width;
List<Widget> menuActions = [];
if (myProvider.gameIsRunning) {
menuActions = [
TextButton(
child: const Image(
image: AssetImage('assets/icons/button_back.png'),
fit: BoxFit.fill,
),
onPressed: () => toast('Long press to quit game...'),
onLongPress: () => GameUtils.resetGame(myProvider),
),
];
}
return Scaffold(
appBar: AppBar(
actions: menuActions,
),
body: SafeArea(
child: Center(
child: myProvider.gameIsRunning
? Game.buildGameWidget(myProvider, boardWidth)
: Parameters.buildParametersSelector(myProvider),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:colors/ui/layout/tileset.dart';
import 'package:colors/provider/data.dart';
import 'package:colors/ui/widgets/game/indicator_top.dart';
import 'package:colors/ui/widgets/game/message_game_end.dart';
import 'package:colors/ui/widgets/game/select_color_bar.dart';
class Game extends StatelessWidget {
const Game({
super.key,
required this.myProvider,
required this.boardWidth,
});
final Data myProvider;
final double boardWidth;
@override
Widget build(BuildContext context) {
final bool gameIsFinished = myProvider.isGameFinished();
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, boardWidth: boardWidth),
),
const SizedBox(height: 2),
Container(
child: gameIsFinished
? EndGameMessage(myProvider: myProvider)
: SelectColorBar(myProvider: myProvider),
),
],
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment