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

Improve game architecture/conception

parent 7a8b70f1
No related branches found
No related tags found
1 merge request!38Resolve "Improve game conception"
Pipeline #5444 passed
Showing
with 537 additions and 329 deletions
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
app.versionName=1.2.28 app.versionName=1.2.29
app.versionCode=34 app.versionCode=35
File added
File added
File added
File added
{
"app_name": "Categories",
"bottom_nav_home": "Game",
"bottom_nav_settings": "Settings",
"bottom_nav_about": "About",
"settings_title": "Settings",
"settings_label_timer_value":"Game turn allowed time",
"about_title": "Informations",
"about_content": "Categories",
"about_version": "Version: {version}",
"": ""
}
{
"app_name": "Petit Bac",
"bottom_nav_home": "Jeu",
"bottom_nav_settings": "Réglages",
"bottom_nav_about": "Infos",
"settings_title": "Réglages",
"settings_label_timer_value":"Durée du tour de jeu",
"about_title": "Informations",
"about_content": "Petit Bac.",
"about_version": "Version : {version}",
"": ""
}
Improve game architecture / conception.
Amélioration de l'architecture / conception du jeu.
class DefaultSettings {
static const List<int> allowedTimerValues = [
5,
10,
20,
30,
60,
90,
];
static const int defaultTimerValue = 10;
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:petitbac/ui/screens/about_page.dart';
import 'package:petitbac/ui/screens/game_page.dart';
import 'package:petitbac/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:petitbac/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';
import 'package:petitbac/models/game/game.dart';
part 'game_state.dart';
class GameCubit extends HydratedCubit<GameState> {
GameCubit() : super(const GameState());
void getData(GameState gameState) {
emit(gameState);
}
void updateGameState(Game gameData) {
emit(GameState(game: gameData));
}
@override
GameState? fromJson(Map<String, dynamic> json) {
Game game = json['game'] as Game;
return GameState(
game: game,
);
}
@override
Map<String, dynamic>? toJson(GameState state) {
return <String, dynamic>{
'game': state.game?.toJson(),
};
}
}
part of 'game_cubit.dart';
@immutable
class GameState extends Equatable {
const GameState({
this.game,
});
final Game? game;
@override
List<Object?> get props => <Object?>[
game,
];
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:petitbac/config/default_settings.dart';
part 'settings_state.dart';
class SettingsCubit extends HydratedCubit<SettingsState> {
SettingsCubit() : super(const SettingsState());
Object getSetting(String key, [String? defaultValue]) {
if (state.values.keys.contains(key)) {
return state.values[key] ?? defaultValue ?? '';
}
return defaultValue ?? '';
}
int getTimerValue() {
return state.timerValue ?? DefaultSettings.defaultTimerValue;
}
void setValues({
int? timerValue,
}) {
emit(SettingsState(
timerValue: timerValue ?? state.timerValue,
));
}
@override
SettingsState? fromJson(Map<String, dynamic> json) {
int timerValue = json['timerValue'] as int;
return SettingsState(
timerValue: timerValue,
);
}
@override
Map<String, dynamic>? toJson(SettingsState state) {
return <String, dynamic>{
'timerValue': state.timerValue ?? DefaultSettings.defaultTimerValue,
};
}
}
part of 'settings_cubit.dart';
@immutable
class SettingsState extends Equatable {
const SettingsState({
this.timerValue,
});
final int? timerValue;
@override
List<dynamic> get props => <dynamic>[
timerValue,
];
Map<String, dynamic> get values => <String, dynamic>{
'timerValue': timerValue,
};
}
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:petitbac/config/theme.dart';
import 'package:petitbac/cubit/bottom_nav_cubit.dart';
import 'package:petitbac/cubit/game_cubit.dart';
import 'package:petitbac/cubit/settings_cubit.dart';
import 'package:petitbac/provider/data.dart'; import 'package:petitbac/provider/data.dart';
import 'package:petitbac/screens/home.dart'; import 'package:petitbac/ui/skeleton.dart';
void main() { void main() async {
/// Initialize packages
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) await EasyLocalization.ensureInitialized();
.then((value) => runApp(const MyApp())); 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 { class MyApp extends StatelessWidget {
...@@ -16,23 +44,28 @@ class MyApp extends StatelessWidget { ...@@ -16,23 +44,28 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return MultiBlocProvider(
providers: [
BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
BlocProvider<GameCubit>(create: (context) => GameCubit()),
BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()),
],
child: ChangeNotifierProvider(
create: (BuildContext context) => Data(), create: (BuildContext context) => Data(),
child: Consumer<Data>( child: Consumer<Data>(
builder: (context, data, child) { builder: (context, data, child) {
return MaterialApp( return MaterialApp(
title: 'Petit Bac',
theme: appTheme,
home: const SkeletonScreen(),
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const Home(),
routes: {
Home.id: (context) => const Home(),
},
); );
}, },
), ),
),
); );
} }
} }
import 'package:petitbac/utils/tools.dart';
class Game {
bool isRunning = false;
Game({
this.isRunning = false,
});
factory Game.createNull() {
return Game();
}
factory Game.createNew() {
return Game(
isRunning: true,
);
}
void stop() {
isRunning = false;
}
@override
String toString() {
return 'Game(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'isRunning': isRunning,
};
}
void dump() {
printlog(toString());
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:petitbac/provider/data.dart';
import 'package:petitbac/utils/random_pick_category.dart';
import 'package:petitbac/utils/random_pick_letter.dart';
class Home extends StatelessWidget {
const Home({super.key});
static const String id = 'home';
static Timer? _timer;
static int _countdownStart = 10;
Future<void> startMiniGame(Data myProvider) async {
if (myProvider.countdown <= 0) {
pickCategory(myProvider);
pickLetter(myProvider);
startTimer(myProvider);
}
}
Future<void> startTimer(Data myProvider) async {
const oneSec = Duration(seconds: 1);
if (_timer != null) {
dispose();
}
_countdownStart = 10;
myProvider.updateCountdown(_countdownStart);
_timer = Timer.periodic(
oneSec,
(Timer timer) {
if (_countdownStart < 0) {
timer.cancel();
} else {
_countdownStart--;
myProvider.updateCountdown(_countdownStart);
}
},
);
}
void dispose() {
_timer?.cancel();
}
Future<void> pickCategory(Data myProvider) async {
myProvider.setSearchingCategory(true);
RandomPickCategory randomPickCategory;
int attempts = 0;
do {
randomPickCategory = RandomPickCategory();
await randomPickCategory.init();
if (!myProvider.isCategoryRecentlyPicked(randomPickCategory.category)) {
myProvider.updateCategory(randomPickCategory.category);
myProvider.setSearchingCategory(false);
break;
}
attempts++;
} while (attempts < 10);
}
Future<void> pickLetter(Data myProvider) async {
myProvider.setSearchingLetter(true);
RandomPickLetter randomPickLetter;
int attempts = 0;
do {
randomPickLetter = RandomPickLetter();
await randomPickLetter.init();
if (!myProvider.isLetterRecentlyPicked(randomPickLetter.letter)) {
myProvider.updateLetter(randomPickLetter.letter);
myProvider.setSearchingLetter(false);
break;
}
attempts++;
} while (attempts < 10);
}
Color darken(Color baseColor, {double amount = 0.2}) {
final hsl = HSLColor.fromColor(baseColor);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
Container mainLetterButtonContainer(
Data myProvider,
Color backgroundColor,
double borderWidth,
) {
final Color borderColor = darken(backgroundColor);
return Container(
margin: EdgeInsets.all(borderWidth),
padding: EdgeInsets.all(borderWidth),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderWidth),
border: Border.all(
color: borderColor,
width: borderWidth,
),
),
child: TextButton(
onPressed: () => pickLetter(myProvider),
child: Text(
myProvider.letter == '' ? "🔀" : myProvider.letter,
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
);
}
Container previousLetterBlockContainer(Data myProvider, int position, bool displayed) {
const double spacingWidth = 2;
const double borderWidth = 3;
Color backgroundColor = Colors.grey;
Color borderColor = darken(backgroundColor);
Color fontColor = Colors.black;
final String letter = myProvider.recentlyPickedLetter(position);
if (letter == '' || displayed == false) {
backgroundColor = Colors.white;
borderColor = Colors.white;
fontColor = Colors.white;
}
return Container(
margin: const EdgeInsets.all(spacingWidth),
padding: const EdgeInsets.all(spacingWidth),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderWidth),
border: Border.all(
color: borderColor,
width: borderWidth,
),
),
child: Text(
' $letter ',
style: TextStyle(
fontSize: 35.0 - (7 * position),
fontWeight: FontWeight.w600,
color: fontColor,
),
),
);
}
Container _buildPickedLetterContainer(
Data myProvider,
Color backgroundColor,
double borderWidth,
) {
const int previousLettersCountToShow = 3;
final List<Widget> cells = [];
// Add previous letters blocks
for (var i = 0; i < previousLettersCountToShow; i++) {
cells.add(TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: previousLetterBlockContainer(myProvider, previousLettersCountToShow - i, true),
));
}
// Add current letter block
cells.add(TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: mainLetterButtonContainer(myProvider, backgroundColor, borderWidth),
));
// Pad with empty blocks to keep symetrical layout
for (var i = 0; i < previousLettersCountToShow; i++) {
cells.add(TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: previousLetterBlockContainer(myProvider, i + 1, false),
));
}
return Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(2),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(children: cells),
],
),
);
}
Widget _buildPickedCategoryContainer(
Data myProvider,
Color backgroundColor,
double borderWidth,
) {
final Color borderColor = darken(backgroundColor);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.all(borderWidth),
padding: EdgeInsets.all(borderWidth),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderWidth),
border: Border.all(
color: borderColor,
width: borderWidth,
),
),
child: TextButton(
onPressed: () => pickCategory(myProvider),
child: Text(
myProvider.category == '' ? "🔀" : myProvider.category,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
),
],
);
}
Widget _buildMiniGameContainer(
Data myProvider,
Color backgroundColor,
double borderWidth,
) {
final Color borderColor = darken(backgroundColor);
Color countDownColor = Colors.black;
if (myProvider.countdown == 0) {
countDownColor = Colors.red;
}
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.all(borderWidth),
padding: EdgeInsets.all(borderWidth),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderWidth),
border: Border.all(
color: borderColor,
width: borderWidth,
),
),
child: TextButton(
onPressed: (myProvider.countdown >= 0) ? null : () => startMiniGame(myProvider),
child: Text(
(myProvider.countdown >= 0) ? myProvider.countdown.toString() : '🎲',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.w600,
color: countDownColor,
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final Data myProvider = Provider.of<Data>(context);
const double borderWidth = 8;
return Scaffold(
appBar: AppBar(
title: const Text('Petit bac'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildPickedLetterContainer(myProvider, Colors.orange, borderWidth),
const SizedBox(height: 5),
_buildMiniGameContainer(myProvider, Colors.blue, borderWidth),
const SizedBox(height: 5),
_buildPickedCategoryContainer(myProvider, Colors.green, borderWidth),
],
),
),
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment