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

Merge branch '74-add-settings-game-board-cell-models' into 'master'

Resolve "Add "settings / game / board / cell" models"

Closes #74

See merge request !73
parents 733cfb65 ac4c3c37
Branches
Tags Release_0.1.19_68
1 merge request!73Resolve "Add "settings / game / board / cell" models"
Pipeline #5595 passed
Showing
with 1788 additions and 20 deletions
File moved
File moved
File moved
File moved
File moved
This diff is collapsed.
import 'package:sudoku/utils/tools.dart';
class DefaultGameSettings {
static const String parameterCodeLevel = 'level';
static const String parameterCodeSize = 'size';
static const List<String> availableParameters = [
parameterCodeLevel,
parameterCodeSize,
];
static const String levelValueEasy = 'easy';
static const String levelValueMedium = 'medium';
static const String levelValueHard = 'hard';
static const String levelValueNightmare = 'nightmare';
static const String defaultLevelValue = levelValueMedium;
static const List<String> allowedLevelValues = [
levelValueEasy,
levelValueMedium,
levelValueHard,
levelValueNightmare,
];
static const String sizeValueTiny = '2x2';
static const String sizeValueSmall = '3x2';
static const String sizeValueStandard = '3x3';
static const String sizeValueLarge = '4x4';
static const String defaultSizeValue = sizeValueStandard;
static const List<String> allowedSizeValues = [
sizeValueTiny,
sizeValueSmall,
sizeValueStandard,
sizeValueLarge,
];
static List<String> getAvailableValues(String parameterCode) {
switch (parameterCode) {
case parameterCodeLevel:
return DefaultGameSettings.allowedLevelValues;
case parameterCodeSize:
return DefaultGameSettings.allowedSizeValues;
}
printlog('Did not find any available value for game parameter "$parameterCode".');
return [];
}
}
import 'package:sudoku/utils/tools.dart';
class DefaultGlobalSettings {
static const String parameterCodeSkin = 'skin';
static const List<String> availableParameters = [
parameterCodeSkin,
];
static const String skinValueDigits = 'digits';
static const String skinValueFood = 'food';
static const String skinValueNature = 'nature';
static const String skinValueMonsters = 'monsters';
static const String defaultSkinValue = skinValueDigits;
static const List<String> allowedSkinValues = [
skinValueDigits,
skinValueFood,
skinValueNature,
skinValueMonsters,
];
static const List<String> shufflableSkins = [
skinValueFood,
skinValueNature,
skinValueMonsters,
];
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 [];
}
static const int defaultTipCountDownValueInSeconds = 20;
}
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 = lightTheme;
import 'dart:async';
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/models/cell.dart';
import 'package:sudoku/models/cell_location.dart';
import 'package:sudoku/models/game.dart';
import 'package:sudoku/models/settings_game.dart';
import 'package:sudoku/models/settings_global.dart';
import 'package:sudoku/utils/board_animate.dart';
part 'game_state.dart';
class GameCubit extends HydratedCubit<GameState> {
GameCubit()
: super(GameState(
game: Game.createNull(),
));
void updateState(Game game) {
emit(GameState(
game: game,
));
}
void refresh() {
updateState(Game(
gameSettings: state.game.gameSettings,
globalSettings: state.game.globalSettings,
board: state.game.board,
solvedBoard: state.game.solvedBoard,
isRunning: state.game.isRunning,
isFinished: state.game.isFinished,
blockSizeHorizontal: state.game.blockSizeHorizontal,
blockSizeVertical: state.game.blockSizeVertical,
boardSize: state.game.boardSize,
shuffledCellValues: state.game.shuffledCellValues,
boardConflicts: state.game.boardConflicts,
selectedCell: state.game.selectedCell,
showConflicts: state.game.showConflicts,
givenTipsCount: state.game.givenTipsCount,
buttonTipsCountdown: state.game.buttonTipsCountdown,
animationInProgress: state.game.animationInProgress,
boardAnimated: state.game.boardAnimated,
));
}
void startNewGame({
required GameSettings gameSettings,
required GlobalSettings globalSettings,
}) {
final Game newGame = Game.createNew(
gameSettings: gameSettings,
globalSettings: globalSettings,
);
newGame.dump();
updateState(newGame);
refresh();
BoardAnimate.startAnimation(this, 'start');
}
void selectCell(CellLocation location) {
state.game.selectedCell = state.game.board.get(location);
refresh();
}
void unselectCell() {
state.game.selectedCell = null;
refresh();
}
void updateCellValue(CellLocation location, int value) {
if (state.game.board.get(location).isFixed == false) {
state.game.board.set(
location,
Cell(
location: location,
value: value,
isFixed: false,
),
);
refresh();
}
if (state.game.checkBoardIsSolved()) {
BoardAnimate.startAnimation(this, 'win');
state.game.isFinished = true;
refresh();
}
}
void toggleShowConflicts() {
state.game.showConflicts = !state.game.showConflicts;
refresh();
}
void increaseGivenTipsCount() {
state.game.givenTipsCount++;
state.game.buttonTipsCountdown = DefaultGlobalSettings.defaultTipCountDownValueInSeconds;
refresh();
const Duration interval = Duration(milliseconds: 500);
Timer.periodic(
interval,
(Timer timer) {
if (state.game.buttonTipsCountdown == 0) {
timer.cancel();
} else {
state.game.buttonTipsCountdown = max(state.game.buttonTipsCountdown - 1, 0);
}
refresh();
},
);
}
void quitGame() {
state.game.isRunning = false;
refresh();
}
void updateAnimationInProgress(bool animationInProgress) {
state.game.animationInProgress = animationInProgress;
refresh();
}
void setAnimatedBackground(List animatedCellsPattern) {
for (int row = 0; row < state.game.boardSize; row++) {
for (int col = 0; col < state.game.boardSize; col++) {
state.game.boardAnimated[row][col] = animatedCellsPattern[row][col];
}
}
refresh();
}
void resetAnimatedBackground() {
for (int row = 0; row < state.game.boardSize; row++) {
for (int col = 0; col < state.game.boardSize; col++) {
state.game.boardAnimated[row][col] = false;
}
}
refresh();
}
@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({
required this.game,
});
final Game game;
@override
List<dynamic> get props => <dynamic>[
game,
];
Map<String, dynamic> get values => <String, dynamic>{
'game': game,
};
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:sudoku/config/default_game_settings.dart';
import 'package:sudoku/models/settings_game.dart';
part 'settings_game_state.dart';
class GameSettingsCubit extends HydratedCubit<GameSettingsState> {
GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault()));
void setValues({
String? level,
String? size,
}) {
emit(
GameSettingsState(
settings: GameSettings(
level: level ?? state.settings.level,
size: size ?? state.settings.size,
),
),
);
}
String getParameterValue(String code) {
switch (code) {
case DefaultGameSettings.parameterCodeLevel:
return GameSettings.getLevelValueFromUnsafe(state.settings.level);
case DefaultGameSettings.parameterCodeSize:
return GameSettings.getSizeValueFromUnsafe(state.settings.size);
}
return '';
}
void setParameterValue(String code, String value) {
final String level = (code == DefaultGameSettings.parameterCodeLevel)
? value
: getParameterValue(DefaultGameSettings.parameterCodeLevel);
final String size = (code == DefaultGameSettings.parameterCodeSize)
? value
: getParameterValue(DefaultGameSettings.parameterCodeSize);
setValues(
level: level,
size: size,
);
}
@override
GameSettingsState? fromJson(Map<String, dynamic> json) {
final String level = json[DefaultGameSettings.parameterCodeLevel] as String;
final String size = json[DefaultGameSettings.parameterCodeSize] as String;
return GameSettingsState(
settings: GameSettings(
level: level,
size: size,
),
);
}
@override
Map<String, dynamic>? toJson(GameSettingsState state) {
return <String, dynamic>{
DefaultGameSettings.parameterCodeLevel: state.settings.level,
DefaultGameSettings.parameterCodeSize: state.settings.size,
};
}
}
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,
];
Map<String, dynamic> get values => <String, dynamic>{
'settings': settings,
};
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/models/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,
};
}
}
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,
];
Map<String, dynamic> get values => <String, dynamic>{
'settings': settings,
};
}
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
import 'package:sudoku/provider/data.dart'; import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/ui/screens/home.dart'; import 'package:sudoku/config/theme.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/cubit/settings_game_cubit.dart';
import 'package:sudoku/cubit/settings_global_cubit.dart';
import 'package:sudoku/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 {
...@@ -17,19 +44,62 @@ class MyApp extends StatelessWidget { ...@@ -17,19 +44,62 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( final List<String> assets = getImagesAssets();
create: (BuildContext context) => Data(), for (String asset in assets) {
child: Consumer<Data>(builder: (context, data, child) { precacheImage(AssetImage(asset), context);
return OverlaySupport( }
return MultiBlocProvider(
providers: [
BlocProvider<GameCubit>(create: (context) => GameCubit()),
BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()),
],
child: OverlaySupport(
child: MaterialApp( child: MaterialApp(
title: 'Sudoku',
theme: appTheme,
home: const SkeletonScreen(),
// Localization stuff
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
), ),
home: const Home(),
), ),
); );
}), }
);
List<String> getImagesAssets() {
final List<String> assets = [];
final List<String> gameImages = [
'button_back',
'button_help',
'button_show_conflicts',
'button_start',
'game_win',
'placeholder',
'cell_empty'
];
for (String image in gameImages) {
assets.add('${'assets/icons/$image'}.png');
}
List<String> skinImages = [];
for (int value = 1; value <= 16; value++) {
skinImages.add(value.toString());
}
for (String skin in DefaultGlobalSettings.allowedSkinValues) {
assets.add('assets/icons/skin_$skin.png');
for (String image in skinImages) {
assets.add('${'${'assets/skins/$skin'}_$image'}.png');
}
}
return assets;
} }
} }
import 'dart:math';
import 'package:sudoku/models/cell.dart';
import 'package:sudoku/models/cell_location.dart';
import 'package:sudoku/models/types.dart';
import 'package:sudoku/utils/tools.dart';
class Board {
Board({
required this.cells,
});
BoardCells cells = const [];
factory Board.createEmpty() {
return Board(
cells: [],
);
}
factory Board.createNew({
required BoardCells cells,
}) {
return Board(
cells: cells,
);
}
Cell get(CellLocation location) {
if (location.row < cells.length) {
if (location.col < cells[location.row].length) {
return cells[location.row][location.col];
}
}
return Cell.none;
}
void set(CellLocation location, Cell cell) {
cells[location.row][location.col] = cell;
}
BoardCells copyCells() {
final BoardCells copiedGrid = [];
for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
final List<Cell> row = [];
for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
row.add(Cell(
location: CellLocation.go(rowIndex, colIndex),
value: cells[rowIndex][colIndex].value,
isFixed: false,
));
}
copiedGrid.add(row);
}
return copiedGrid;
}
BoardCells getSolvedGrid() {
final Board tmpBoard = Board(cells: copyCells());
do {
final List<List<int>> cellsWithUniqueAvailableValue =
tmpBoard.getEmptyCellsWithUniqueAvailableValue();
if (cellsWithUniqueAvailableValue.isEmpty) {
break;
}
for (int i = 0; i < cellsWithUniqueAvailableValue.length; i++) {
final int row = cellsWithUniqueAvailableValue[i][0];
final int col = cellsWithUniqueAvailableValue[i][1];
final int value = cellsWithUniqueAvailableValue[i][2];
tmpBoard.cells[row][col] = Cell(
location: CellLocation.go(row, col),
value: value,
isFixed: tmpBoard.cells[row][col].isFixed,
);
}
} while (true);
return tmpBoard.cells;
}
List<List<int>> getEmptyCellsWithUniqueAvailableValue() {
List<List<int>> candidateCells = [];
final int boardSize = cells.length;
for (int row = 0; row < boardSize; row++) {
for (int col = 0; col < boardSize; col++) {
if (cells[row][col].value == 0) {
int allowedValuesCount = 0;
int candidateValue = 0;
for (int value = 1; value <= boardSize; value++) {
if (isValueAllowed(CellLocation.go(row, col), value)) {
candidateValue = value;
allowedValuesCount++;
}
}
if (allowedValuesCount == 1) {
candidateCells.add([row, col, candidateValue]);
}
}
}
}
return candidateCells;
}
bool isValueAllowed(CellLocation? candidateLocation, int candidateValue) {
if ((candidateLocation == null) || (candidateValue == 0)) {
return true;
}
final int boardSize = cells.length;
// check lines does not contains a value twice
for (int row = 0; row < boardSize; row++) {
final List<int> values = [];
for (int col = 0; col < boardSize; col++) {
int value = cells[row][col].value;
if (row == candidateLocation.row && col == candidateLocation.col) {
value = candidateValue;
}
if (value != 0) {
values.add(value);
}
}
final List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
return false;
}
}
// check columns does not contains a value twice
for (int col = 0; col < boardSize; col++) {
final List<int> values = [];
for (int row = 0; row < boardSize; row++) {
int value = cells[row][col].value;
if (row == candidateLocation.row && col == candidateLocation.col) {
value = candidateValue;
}
if (value != 0) {
values.add(value);
}
}
final List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
return false;
}
}
// check blocks does not contains a value twice
final int blockSizeVertical = sqrt(cells.length).toInt();
final int blockSizeHorizontal = cells.length ~/ blockSizeVertical;
final int horizontalBlocksCount = blockSizeVertical;
final int verticalBlocksCount = blockSizeHorizontal;
for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) {
for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) {
final List<int> values = [];
for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) {
for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) {
final int row = (blockRow * blockSizeVertical) + rowInBlock;
final int col = (blockCol * blockSizeHorizontal) + colInBlock;
int value = cells[row][col].value;
if (row == candidateLocation.row && col == candidateLocation.col) {
value = candidateValue;
}
if (value != 0) {
values.add(value);
}
}
}
final List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
return false;
}
}
}
return true;
}
void dump() {
printlog('');
printlog('$Board:');
printlog(' cells: $cells');
printlog('');
}
@override
String toString() {
return '$Board(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'cells': cells,
};
}
}
import 'package:sudoku/models/cell_location.dart';
import 'package:sudoku/utils/tools.dart';
class Cell {
const Cell({
required this.location,
required this.value,
required this.isFixed,
});
final CellLocation location;
final int value;
final bool isFixed;
static Cell none = Cell(
location: CellLocation.go(0, 0),
value: 0,
isFixed: true,
);
void dump() {
printlog('$Cell:');
printlog(' location: $location');
printlog(' value: $value');
printlog(' isFixed: $isFixed');
printlog('');
}
@override
String toString() {
return '$Cell(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'location': location.toJson(),
'value': value,
'isFixed': isFixed,
};
}
}
import 'package:sudoku/utils/tools.dart';
class CellLocation {
final int col;
final int row;
CellLocation({
required this.col,
required this.row,
});
factory CellLocation.go(int row, int col) {
return CellLocation(col: col, row: row);
}
void dump() {
printlog('$CellLocation:');
printlog(' row: $row');
printlog(' col: $col');
printlog('');
}
@override
String toString() {
return '$CellLocation(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'row': row,
'col': col,
};
}
}
import 'dart:math';
import 'package:sudoku/assets/grids.dart';
import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/models/board.dart';
import 'package:sudoku/models/cell.dart';
import 'package:sudoku/models/cell_location.dart';
import 'package:sudoku/models/settings_game.dart';
import 'package:sudoku/models/settings_global.dart';
import 'package:sudoku/models/types.dart';
import 'package:sudoku/utils/board_utils.dart';
import 'package:sudoku/utils/tools.dart';
class Game {
Game({
required this.gameSettings,
required this.globalSettings,
required this.board,
required this.solvedBoard,
required this.isRunning,
required this.isFinished,
this.shuffledCellValues = const [],
this.boardConflicts = const [],
this.selectedCell,
this.showConflicts = false,
this.givenTipsCount = 0,
this.buttonTipsCountdown = 0,
this.animationInProgress = false,
this.boardAnimated = const [],
required this.blockSizeHorizontal,
required this.blockSizeVertical,
required this.boardSize,
});
final GameSettings gameSettings;
final GlobalSettings globalSettings;
bool isRunning = false;
bool isFinished = false;
int blockSizeVertical = 0;
int blockSizeHorizontal = 0;
int boardSize = 0;
Board board;
Board solvedBoard;
List<int> shuffledCellValues = [];
Cell? selectedCell;
int givenTipsCount = 0;
int buttonTipsCountdown = 0;
bool showConflicts = false;
ConflictsCount boardConflicts = [];
bool animationInProgress = false;
AnimatedBoard boardAnimated = [];
factory Game.createNull() {
return Game(
gameSettings: GameSettings.createDefault(),
globalSettings: GlobalSettings.createDefault(),
board: Board.createEmpty(),
solvedBoard: Board.createEmpty(),
shuffledCellValues: [],
isRunning: false,
isFinished: false,
givenTipsCount: 0,
blockSizeHorizontal: 0,
blockSizeVertical: 0,
boardSize: 0,
);
}
factory Game.createNew({
GameSettings? gameSettings,
GlobalSettings? globalSettings,
}) {
final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault();
final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
final int blockSizeHorizontal = int.parse(newGameSettings.size.split('x')[0]);
final int blockSizeVertical = int.parse(newGameSettings.size.split('x')[1]);
final int boardSize = blockSizeHorizontal * blockSizeVertical;
const int maxCellValue = 16;
final List<int> shuffledCellValues = List<int>.generate(maxCellValue, (i) => i + 1);
if (DefaultGlobalSettings.shufflableSkins.contains(newGlobalSettings.skin)) {
shuffledCellValues.shuffle();
printlog('Shuffled tiles values: $shuffledCellValues');
}
ConflictsCount nonConflictedBoard = [];
for (int row = 0; row < boardSize; row++) {
List<int> line = [];
for (int col = 0; col < boardSize; col++) {
line.add(0);
}
nonConflictedBoard.add(line);
}
final List<String> templates =
SudokuGrids.templates[newGameSettings.size]?[newGameSettings.level] ?? [];
final String template = templates.elementAt(Random().nextInt(templates.length)).toString();
if (template.length != pow(blockSizeHorizontal * blockSizeVertical, 2)) {
printlog('Failed to get grid template...');
return Game.createNull();
}
final Board board = BoardUtils.createBoardFromTemplate(
template: template,
isSymetric: (blockSizeHorizontal == blockSizeVertical),
);
final Board solvedBoard = Board.createNew(cells: board.getSolvedGrid());
// Animated background
AnimatedBoard notAnimatedBoard = [];
for (int row = 0; row < boardSize; row++) {
List<bool> line = [];
for (int col = 0; col < boardSize; col++) {
line.add(false);
}
notAnimatedBoard.add(line);
}
return Game(
gameSettings: newGameSettings,
globalSettings: newGlobalSettings,
board: board,
solvedBoard: solvedBoard,
isRunning: true,
isFinished: false,
boardConflicts: nonConflictedBoard,
shuffledCellValues: shuffledCellValues,
selectedCell: null,
blockSizeHorizontal: blockSizeHorizontal,
blockSizeVertical: blockSizeVertical,
boardSize: boardSize,
boardAnimated: notAnimatedBoard,
);
}
bool canGiveTip() {
return (buttonTipsCountdown == 0);
}
int getTranslatedValueForDisplay(int originalValue) {
return shuffledCellValues[originalValue - 1];
}
bool checkBoardIsSolved() {
// (re)compute conflicts
boardConflicts = computeConflictsInBoard();
// check grid is fully completed and does not contain conflict
for (int row = 0; row < boardSize; row++) {
for (int col = 0; col < boardSize; col++) {
if (board.cells[row][col].value == 0 || boardConflicts[row][col] != 0) {
return false;
}
}
}
printlog('-> ok sudoku solved!');
return true;
}
ConflictsCount computeConflictsInBoard() {
final BoardCells cells = board.cells;
final ConflictsCount conflicts = boardConflicts;
// reset conflict states
for (int row = 0; row < boardSize; row++) {
for (int col = 0; col < boardSize; col++) {
conflicts[row][col] = 0;
}
}
// check lines does not contains a value twice
for (int row = 0; row < boardSize; row++) {
final List<int> values = [];
for (int col = 0; col < boardSize; col++) {
int value = cells[row][col].value;
if (value != 0) {
values.add(value);
}
}
final List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
printlog('line $row contains duplicates');
// Add line to cells in conflict
for (int col = 0; col < boardSize; col++) {
conflicts[row][col]++;
}
}
}
// check columns does not contains a value twice
for (int col = 0; col < boardSize; col++) {
final List<int> values = [];
for (int row = 0; row < boardSize; row++) {
int value = cells[row][col].value;
if (value != 0) {
values.add(value);
}
}
final List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
printlog('column $col contains duplicates');
// Add column to cells in conflict
for (int row = 0; row < boardSize; row++) {
conflicts[row][col]++;
}
}
}
// check blocks does not contains a value twice
final int horizontalBlocksCount = blockSizeVertical;
final int verticalBlocksCount = blockSizeHorizontal;
for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) {
for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) {
List<int> values = [];
for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) {
for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) {
int row = (blockRow * blockSizeVertical) + rowInBlock;
int col = (blockCol * blockSizeHorizontal) + colInBlock;
int value = cells[row][col].value;
if (value != 0) {
values.add(value);
}
}
}
List<int> distinctValues = values.toSet().toList();
if (values.length != distinctValues.length) {
printlog('block [$blockCol,$blockRow] contains duplicates');
// Add blocks to cells in conflict
for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) {
for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) {
int row = (blockRow * blockSizeVertical) + rowInBlock;
int col = (blockCol * blockSizeHorizontal) + colInBlock;
conflicts[row][col]++;
}
}
}
}
}
return conflicts;
}
void showTip(GameCubit gameCubit) {
if (selectedCell == null) {
// no selected cell -> pick one
helpSelectCell(gameCubit);
} else {
// currently selected cell -> set value
helpFillCell(gameCubit);
}
gameCubit.increaseGivenTipsCount();
}
void helpSelectCell(GameCubit gameCubit) {
// pick one of wrong value cells, if found
final List<List<int>> wrongValueCells = getCellsWithWrongValue();
if (wrongValueCells.isNotEmpty) {
printlog('will pick from wrongValueCells');
pickRandomFromList(gameCubit, wrongValueCells);
return;
}
// pick one of conflicting cells, if found
final List<List<int>> conflictingCells = getCellsWithConflicts();
if (conflictingCells.isNotEmpty) {
printlog('will pick from conflictingCells');
pickRandomFromList(gameCubit, conflictingCells);
return;
}
// pick one form cells with unique non-conflicting candidate value
final List<List<int>> candidateCells = board.getEmptyCellsWithUniqueAvailableValue();
if (candidateCells.isNotEmpty) {
printlog('will pick from candidateCells');
pickRandomFromList(gameCubit, candidateCells);
return;
}
}
List<List<int>> getCellsWithWrongValue() {
final BoardCells cells = board.cells;
final BoardCells cellsSolved = solvedBoard.cells;
List<List<int>> cellsWithWrongValue = [];
for (int row = 0; row < boardSize; row++) {
for (int col = 0; col < boardSize; col++) {
if (cells[row][col].value != 0 &&
cells[row][col].value != cellsSolved[row][col].value) {
cellsWithWrongValue.add([row, col]);
}
}
}
return cellsWithWrongValue;
}
List<List<int>> getCellsWithConflicts() {
List<List<int>> cellsWithConflict = [];
for (int row = 0; row < boardSize; row++) {
for (int col = 0; col < boardSize; col++) {
if (boardConflicts[row][col] != 0) {
cellsWithConflict.add([row, col]);
}
}
}
return cellsWithConflict;
}
void pickRandomFromList(GameCubit gameCubit, List<List<int>> cellsCoordinates) {
if (cellsCoordinates.isNotEmpty) {
cellsCoordinates.shuffle();
final List<int> cell = cellsCoordinates[0];
gameCubit.selectCell(CellLocation.go(cell[0], cell[1]));
}
}
void helpFillCell(GameCubit gameCubit) {
// Will clean cell if no eligible value found
int eligibleValue = 0;
// Ensure there is only one eligible value for this cell
int allowedValuesCount = 0;
for (int value = 1; value <= boardSize; value++) {
if (board.isValueAllowed(selectedCell?.location, value)) {
allowedValuesCount++;
eligibleValue = value;
}
}
gameCubit.updateCellValue(
selectedCell!.location, allowedValuesCount == 1 ? eligibleValue : 0);
gameCubit.unselectCell();
}
printGrid() {
final BoardCells cells = board.cells;
final BoardCells solvedCells = solvedBoard.cells;
const String stringValues = '0123456789ABCDEFG';
printlog('');
printlog('-------');
for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) {
String row = '';
String rowSolved = '';
for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
row += stringValues[cells[rowIndex][colIndex].value];
rowSolved += stringValues[solvedCells[rowIndex][colIndex].value];
}
printlog('$row | $rowSolved');
}
printlog('-------');
printlog('');
}
void dump() {
printlog('');
printlog('## Current game dump:');
printlog('');
gameSettings.dump();
globalSettings.dump();
printlog('');
printlog('$Game:');
printlog(' blockSizeHorizontal: $blockSizeHorizontal');
printlog(' blockSizeVertical: $blockSizeVertical');
printlog(' boardSize: $boardSize');
printlog(' shuffledCellValues: $shuffledCellValues');
printlog('');
printlog(' isRunning: $isRunning');
printlog(' isFinished: $isFinished');
printlog(' selectedCell: ${selectedCell?.toString() ?? ''}');
printlog(' showConflicts: $showConflicts');
printlog(' givenTipsCount: $givenTipsCount');
printlog(' givenTipsCountEnableCountdown: $buttonTipsCountdown');
printlog(' animationInProgress: $animationInProgress');
printlog('');
printGrid();
printlog('');
}
@override
String toString() {
return '$Game(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
'gameSettings': gameSettings.toJson(),
'globalSettings': globalSettings.toJson(),
'isRunning': isRunning,
'isFinished': isFinished,
'board': board.toJson(),
'solvedBoard': solvedBoard.toJson(),
'shuffledCellValues': shuffledCellValues,
'boardConflicts': boardConflicts,
'selectedCell': selectedCell?.toJson(),
'showConflicts': showConflicts,
'givenTipsCount': givenTipsCount,
'givenTipsCountEnableCountdown': buttonTipsCountdown,
'animationInProgress': animationInProgress,
'boardAnimated': boardAnimated,
'blockSizeHorizontal': blockSizeHorizontal,
'blockSizeVertical': blockSizeVertical,
};
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment