From 64053d1d589fbb2b67d31584bc6a4275ecf0e018 Mon Sep 17 00:00:00 2001 From: savinmax Date: Sun, 21 Sep 2025 17:26:51 +0200 Subject: [PATCH] Refactor game state management to use level-based architecture Remove legacy GamePlaying states and consolidate all gameplay logic to use the GameLevelPlaying state system. This simplifies state management by eliminating duplicate code paths and ensures consistent behavior across all game modes including Free Play. --- lib/bloc/game_bloc.dart | 276 +----------------------- lib/game/components/grid_component.dart | 57 ++--- lib/screens/game_screen.dart | 48 +---- lib/screens/menu_screen.dart | 9 +- lib/services/level_service.dart | 32 ++- 5 files changed, 58 insertions(+), 364 deletions(-) diff --git a/lib/bloc/game_bloc.dart b/lib/bloc/game_bloc.dart index 9958938..88b3ce1 100644 --- a/lib/bloc/game_bloc.dart +++ b/lib/bloc/game_bloc.dart @@ -16,8 +16,6 @@ abstract class GameEvent extends Equatable { List get props => []; } -class StartGame extends GameEvent {} - class SwapGems extends GameEvent { final int row1, col1, row2, col2; SwapGems(this.row1, this.col1, this.row2, this.col2); @@ -58,118 +56,8 @@ abstract class GameState extends Equatable { List get props => []; } -// Internal match state event -class _GamePlayingMatch extends GameState { - final GameGrid grid; - final int score; - final int moves; - final List matches; - - _GamePlayingMatch({ - required this.grid, - this.score = 0, - this.moves = 0, - this.matches = const [], - }); - - @override - List get props => [grid, score, moves, matches]; -} - class GameInitial extends GameState {} -class GamePlaying extends GameState { - final GameGrid grid; - final DoneCallback done; - final int score; - final int moves; - - GamePlaying({ - required this.grid, - this.done = _defaultDoneCallback, - this.score = 0, - this.moves = 0, - }); - - static void _defaultDoneCallback([FutureOr? _]) {} - - @override - List get props => [grid, score, moves]; -} - -class GamePlayingStart extends GamePlaying { - GamePlayingStart({ - required super.grid, - super.done, - super.score, - super.moves, - }); -} - -class GamePlayingIdle extends GamePlaying { - GamePlayingIdle({ - required super.grid, - super.done, - super.score, - super.moves, - }); -} - -class GamePlayingDrop extends GamePlaying { - GamePlayingDrop({ - required super.grid, - super.done, - super.score, - super.moves, - }); -} - -class GamePlayingSwap extends GamePlaying { - final Gem gem1, gem2; - - GamePlayingSwap( - {required super.grid, - super.score, - super.moves, - super.done, - required this.gem1, - required this.gem2}); - - @override - List get props => [grid, score, moves, gem1, gem2]; -} - -class GamePlayingNewGems extends GamePlaying { - final List gems; - - GamePlayingNewGems( - {required super.grid, - super.score, - super.moves, - super.done, - required this.gems}); - - @override - List get props => [grid, score, moves, gems]; -} - -class GamePlayingMatch extends GamePlaying { - final List matches; - final int combo; - - GamePlayingMatch({ - required super.grid, - super.score, - super.moves, - super.done, - this.combo = 0, - this.matches = const [], - }); - - @override - List get props => [grid, score, moves, matches]; -} - // Level-based states class GameLevelPlaying extends GameState { final GameGrid grid; @@ -368,10 +256,8 @@ class GameBloc extends Bloc { final LevelService _levelService = LevelService.instance; GameBloc() : super(GameInitial()) { - on(_onStartGame); on(_onSwapGems); on(_onProcessMatches); - on(_onStartLevel); on(_onUpdateTimer); on(_onCompleteLevel); @@ -384,84 +270,9 @@ class GameBloc extends Bloc { return super.close(); } - void _onStartGame(StartGame event, Emitter emit) async { - final grid = GameGrid(); - - final done = Completer(); - emit(GamePlayingStart(grid: grid, done: done.complete)); - await done.future; - - final matches = MatchDetector.findMatches(grid); - emit(_GamePlayingMatch(grid: grid, matches: matches)); - add(ProcessMatches()); - } - void _onSwapGems(SwapGems event, Emitter emit) async { - // Handle regular game states - if (state is GamePlaying) { - final currentState = state as GamePlaying; - - if (!MatchDetector.isValidSwap( - currentState.grid, event.row1, event.col1, event.row2, event.col2)) { - return; - } - final newGrid = currentState.grid.clone(); - - // Perform swap - final gem1 = newGrid.getGem(event.row1, event.col1); - final gem2 = newGrid.getGem(event.row2, event.col2); - - if (gem1 != null && gem2 != null) { - newGrid.setGem(event.row1, event.col1, - gem2.copyWith(row: event.row1, col: event.col1)); - newGrid.setGem(event.row2, event.col2, - gem1.copyWith(row: event.row2, col: event.col2)); - - // Emit state for swap animation - var done = Completer(); - emit(GamePlayingSwap( - grid: newGrid, - score: currentState.score, - moves: currentState.moves, - gem1: gem1, - gem2: gem2, - done: done.complete, - )); - - // Wait for swap animation - await done.future; - - // Check for matches - final matches = MatchDetector.findMatches(newGrid); - if (matches.isNotEmpty) { - emit(_GamePlayingMatch( - grid: newGrid, - score: currentState.score, - moves: currentState.moves + 1, - matches: matches, - )); - add(ProcessMatches()); - } else { - // Revert swap if no matches - newGrid.setGem(event.row1, event.col1, gem1); - newGrid.setGem(event.row2, event.col2, gem2); - var done = Completer(); - - emit(GamePlayingSwap( - grid: newGrid, - score: currentState.score, - moves: currentState.moves, - gem1: gem2, - gem2: gem1, - done: done.complete, - )); - } - // Wait for swap animation - await done.future; - } - } - // Handle level-based states - else if (state is GameLevelPlaying) { + // Handle only level-based states (including Free Play) + if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; if (!MatchDetector.isValidSwap( @@ -555,87 +366,8 @@ class GameBloc extends Bloc { } void _onProcessMatches(ProcessMatches event, Emitter emit) async { - // Handle regular game matches - if (state is _GamePlayingMatch) { - final currentState = state as _GamePlayingMatch; - - // Calculate score - int newScore = currentState.score + - currentState.matches.length * GameConstants.baseScore; - - if (event.combo > 0) { - newScore += GameConstants.comboMultiplier * event.combo; - } - var newGrid = currentState.grid.clone(); - - // Mark matched gems and emit state for animation - for (final gem in currentState.matches) { - newGrid.setGem(gem.row, gem.col, null); - } - - var done = Completer(); - emit(GamePlayingMatch( - grid: newGrid, - score: newScore, - moves: currentState.moves, - matches: currentState.matches, - combo: event.combo, - done: done.complete, - )); - - // Wait for match animations to complete - await done.future; - newGrid = newGrid.clone(); - - // Apply gravity - GravitySystem.applyGravity(newGrid); - - // Emit state for drop columns - done = Completer(); - emit(GamePlayingDrop( - grid: newGrid, - score: newScore, - moves: currentState.moves, - done: done.complete, - )); - await done.future; - newGrid = newGrid.clone(); - - // Apply gravity - final newGems = GravitySystem.generateGems(newGrid); - - // Emit state for drop columns - done = Completer(); - emit(GamePlayingNewGems( - grid: newGrid, - score: newScore, - moves: currentState.moves, - gems: newGems, - done: done.complete, - )); - // Wait for fall animations - await done.future; - - newGrid = newGrid.clone(); - - // Check for new matches - final newMatches = MatchDetector.findMatches(newGrid); - - if (newMatches.isNotEmpty) { - emit(_GamePlayingMatch( - grid: newGrid, - score: newScore, - moves: currentState.moves, - matches: newMatches, - )); - add(ProcessMatches(combo: event.combo + 1)); - } else { - emit(GamePlayingIdle( - grid: newGrid, score: newScore, moves: currentState.moves)); - } - } - // Handle level-based matches - else if (state is _GameLevelPlayingMatch) { + // Handle only level-based matches (including Free Play) + if (state is _GameLevelPlayingMatch) { final currentState = state as _GameLevelPlayingMatch; // Calculate score diff --git a/lib/game/components/grid_component.dart b/lib/game/components/grid_component.dart index 0315168..16e112c 100644 --- a/lib/game/components/grid_component.dart +++ b/lib/game/components/grid_component.dart @@ -19,11 +19,9 @@ class GridComponent extends PositionComponent @override Future onLoad() async { - if (levelId == null) { - game.gameBloc.add(StartGame()); - } else { - game.gameBloc.add(StartLevel(levelId!)); - } + // Always use StartLevel - Free Play is level ID 0 + final id = levelId ?? 0; // Default to Free Play if no level specified + game.gameBloc.add(StartLevel(id)); } void _createGemComponents() { @@ -61,21 +59,14 @@ class GridComponent extends PositionComponent } void updateGrid(GameState state) async { - // Work only with relevant events - if (state is! GamePlaying && state is! GameLevelPlaying) { + // Work only with level-based states (including Free Play) + if (state is! GameLevelPlaying) { return; } canInterract = false; print("Update event with state ${state.runtimeType.toString()}"); - // Handle regular game states - if (state is GamePlayingSwap) { - await _swapGems( - state.grid, - state.gem1, - state.gem2, - ); - } + // Handle level-based states if (state is GameLevelSwap) { await _swapGems( state.grid, @@ -83,43 +74,27 @@ class GridComponent extends PositionComponent state.gem2, ); } - if (state is GamePlayingMatch) { - await _animateMatch(state.grid, state.matches); - } if (state is GameLevelMatch) { await _animateMatch(state.grid, state.matches); } - if (state is GamePlayingDrop) { - await _animateDrop(state.grid); - } if (state is GameLevelDrop) { await _animateDrop(state.grid); } - if (state is GamePlayingNewGems) { - await _animateNewGemsFall(state.grid, state.gems); - } if (state is GameLevelNewGems) { await _animateNewGemsFall(state.grid, state.gems); } - if (state is GamePlayingIdle || state is GameLevelIdle) { + if (state is GameLevelIdle) { canInterract = true; } - if (state is GamePlaying) { - gameGrid = state.grid; - gameGrid.printGrid(); - _createGemComponents(); - await Future.delayed(const Duration(milliseconds: 100)); - print("Updated state ${state.runtimeType.toString()}"); - state.done(); - } else { - state = state as GameLevelPlaying; - gameGrid = state.grid; - gameGrid.printGrid(); - _createGemComponents(); - await Future.delayed(const Duration(milliseconds: 100)); - print("Updated state ${state.runtimeType.toString()}"); - state.done(); - } + + // Update grid and create components for all GameLevelPlaying states + final levelState = state as GameLevelPlaying; + gameGrid = levelState.grid; + gameGrid.printGrid(); + _createGemComponents(); + await Future.delayed(const Duration(milliseconds: 100)); + print("Updated state ${state.runtimeType.toString()}"); + levelState.done(); } GemComponent? _findGemComponent(Gem gem) { diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart index eb48fb3..c47bcec 100644 --- a/lib/screens/game_screen.dart +++ b/lib/screens/game_screen.dart @@ -30,7 +30,7 @@ class _GameScreenState extends State { body: BlocListener( listener: (context, state) { if (game.gridComponent == null) return; - if (state is GameLevelPlaying || state is GamePlaying) { + if (state is GameLevelPlaying) { game.gridComponent!.updateGrid(state); } else if (state is GameLevelCompleted) { _showLevelCompletedDialog(context, state); @@ -48,52 +48,6 @@ class _GameScreenState extends State { GameWidget.controlled( gameFactory: () => game, ), - if (state is GamePlaying) - Positioned( - top: 50, - left: 20, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Score: ${state.score}', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Moves: ${state.moves}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - Text( - 'Combo x ${state is GamePlayingMatch ? (state.combo + 1) : "-"}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - Text( - 'Last state: ${state.runtimeType}', - style: const TextStyle( - color: Colors.blue, - fontSize: 16, - ), - ), - ], - ), - ), - ), if (state is GameLevelPlaying) Positioned( top: 50, diff --git a/lib/screens/menu_screen.dart b/lib/screens/menu_screen.dart index 29e7392..f4a43b8 100644 --- a/lib/screens/menu_screen.dart +++ b/lib/screens/menu_screen.dart @@ -53,9 +53,16 @@ class MenuScreen extends StatelessWidget { const SizedBox(height: 20), ElevatedButton( onPressed: () { + // Start Free Play level (ID: 0) + context.read().add(StartLevel(0)); Navigator.push( context, - MaterialPageRoute(builder: (context) => GameScreen()), + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: GameScreen(), + ), + ), ); }, style: ElevatedButton.styleFrom( diff --git a/lib/services/level_service.dart b/lib/services/level_service.dart index 055e995..dc525f2 100644 --- a/lib/services/level_service.dart +++ b/lib/services/level_service.dart @@ -6,6 +6,7 @@ import '../game/models/level.dart'; class LevelService { static const String _progressKey = 'level_progress'; + static const int freePlayLevelId = 0; // Special ID for Free Play static LevelService? _instance; static LevelService get instance => _instance ??= LevelService._(); @@ -14,6 +15,25 @@ class LevelService { List? _levels; Map? _progress; + /// Get the hardcoded Free Play level + Level get freePlayLevel => const Level( + id: freePlayLevelId, + name: 'Free Play', + description: 'Play without constraints - match gems and score points!', + constraints: LevelConstraints(), // No constraints + objectives: LevelObjectives(), // No objectives + starRating: StarRating( + criteria: StarCriteria.SCORE, + thresholds: StarThresholds( + oneStar: 1000, + twoStar: 5000, + threeStar: 10000, + ), + ), + availableGemTypes: [0, 1, 2, 3, 4, 5], // All gem types available + unlocked: true, // Always unlocked + ); + /// Load all levels from JSON configuration Future> loadLevels() async { if (_levels != null) return _levels!; @@ -35,6 +55,11 @@ class LevelService { /// Get a specific level by ID Future getLevel(int levelId) async { + // Return Free Play level for ID 0 + if (levelId == freePlayLevelId) { + return freePlayLevel; + } + final levels = await loadLevels(); try { return levels.firstWhere((level) => level.id == levelId); @@ -181,7 +206,7 @@ class LevelService { if (!objectives.hasGemTypeObjectives) { return true; } - + for (final entry in objectives.clearGemTypes.entries) { final requiredCount = entry.value; final clearedCount = gemsCleared[entry.key] ?? 0; @@ -196,8 +221,9 @@ class LevelService { bool isLevelCompleted(Level level, int score, int moves, int timeUsed, Map gemsCleared) { // Check score constraint - if (level.constraints.hasScoreTarget && - score < level.constraints.targetScore!) { + if (level.id == 0 || + (level.constraints.hasScoreTarget && + score < level.constraints.targetScore!)) { return false; }