import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import '../game/models/grid.dart'; import '../game/models/gem.dart'; import '../game/models/level.dart'; import '../game/systems/match_detector.dart'; import '../game/systems/gravity_system.dart'; import '../services/level_service.dart'; import '../utils/constants.dart'; // Events abstract class GameEvent extends Equatable { @override List get props => []; } class SwapGems extends GameEvent { final int row1, col1, row2, col2; SwapGems(this.row1, this.col1, this.row2, this.col2); @override List get props => [row1, col1, row2, col2]; } class ProcessMatches extends GameEvent { final int combo; ProcessMatches({this.combo = 0}); @override List get props => [combo]; } class StartLevel extends GameEvent { final int levelId; StartLevel(this.levelId); @override List get props => [levelId]; } class UpdateTimer extends GameEvent { final int timeElapsed; UpdateTimer(this.timeElapsed); @override List get props => [timeElapsed]; } class CompleteLevel extends GameEvent {} class FailLevel extends GameEvent {} // States typedef DoneCallback = void Function([FutureOr?]); abstract class GameState extends Equatable { @override List get props => []; } class GameInitial extends GameState {} // Level-based states class GameLevelPlaying extends GameState { final GameGrid grid; final DoneCallback done; final int score; final int moves; final Level level; final int timeElapsed; final Map gemsCleared; GameLevelPlaying({ required this.grid, required this.level, this.done = _defaultDoneCallback, this.score = 0, this.moves = 0, this.timeElapsed = 0, this.gemsCleared = const {}, }); static void _defaultDoneCallback([FutureOr? _]) {} @override List get props => [grid, score, moves, level, timeElapsed, gemsCleared]; } class GameLevelStart extends GameLevelPlaying { GameLevelStart({ required super.grid, required super.level, super.done, super.score, super.moves, super.timeElapsed, super.gemsCleared, }); } class GameLevelIdle extends GameLevelPlaying { GameLevelIdle({ required super.grid, required super.level, super.done, super.score, super.moves, super.timeElapsed, super.gemsCleared, }); } class GameLevelSwap extends GameLevelPlaying { final Gem gem1, gem2; GameLevelSwap({ required super.grid, required super.level, super.score, super.moves, super.done, super.timeElapsed, super.gemsCleared, required this.gem1, required this.gem2, }); @override List get props => [grid, score, moves, level, timeElapsed, gemsCleared, gem1, gem2]; } class GameLevelMatch extends GameLevelPlaying { final List matches; final int combo; GameLevelMatch({ required super.grid, required super.level, super.score, super.moves, super.done, super.timeElapsed, super.gemsCleared, this.combo = 0, this.matches = const [], }); @override List get props => [grid, score, moves, level, timeElapsed, gemsCleared, matches, combo]; } class GameLevelDrop extends GameLevelPlaying { GameLevelDrop({ required super.grid, required super.level, super.done, super.score, super.moves, super.timeElapsed, super.gemsCleared, }); } class GameLevelNewGems extends GameLevelPlaying { final List gems; GameLevelNewGems({ required super.grid, required super.level, super.score, super.moves, super.done, super.timeElapsed, super.gemsCleared, required this.gems, }); @override List get props => [grid, score, moves, level, timeElapsed, gemsCleared, gems]; } class GameLevelCompleted extends GameState { final Level level; final int score; final int moves; final int timeElapsed; final int stars; final Map gemsCleared; GameLevelCompleted({ required this.level, required this.score, required this.moves, required this.timeElapsed, required this.stars, required this.gemsCleared, }); @override List get props => [level, score, moves, timeElapsed, stars, gemsCleared]; } class GameLevelFailed extends GameState { final Level level; final int score; final int moves; final int timeElapsed; final Map gemsCleared; final String reason; GameLevelFailed({ required this.level, required this.score, required this.moves, required this.timeElapsed, required this.gemsCleared, required this.reason, }); @override List get props => [level, score, moves, timeElapsed, gemsCleared, reason]; } // Internal level match state class _GameLevelPlayingMatch extends GameState { final GameGrid grid; final Level level; final int score; final int moves; final int timeElapsed; final Map gemsCleared; final List matches; _GameLevelPlayingMatch({ required this.grid, required this.level, this.score = 0, this.moves = 0, this.timeElapsed = 0, this.gemsCleared = const {}, this.matches = const [], }); @override List get props => [grid, level, score, moves, timeElapsed, gemsCleared, matches]; } // Bloc class GameBloc extends Bloc { Timer? _gameTimer; final LevelService _levelService = LevelService.instance; GameBloc() : super(GameInitial()) { on(_onSwapGems); on(_onProcessMatches); on(_onStartLevel); on(_onUpdateTimer); on(_onCompleteLevel); on(_onFailLevel); } @override Future close() { _gameTimer?.cancel(); return super.close(); } void _onSwapGems(SwapGems event, Emitter emit) async { // Handle only level-based states (including Free Play) if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; if (!MatchDetector.isValidSwap( currentState.grid, event.row1, event.col1, event.row2, event.col2)) { return; } GameGrid 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) { print("Gem1 or Gem2 is null, that should not be so"); return; } 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(GameLevelSwap( grid: newGrid, level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: currentState.gemsCleared, 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) { final newMoves = currentState.moves + 1; // Check if move limit exceeded if (currentState.level.constraints.hasMoveLimit && newMoves >= currentState.level.constraints.maxMoves!) { // This is the last move, check if level can be completed final updatedGemsCleared = _updateGemsCleared(currentState.gemsCleared, matches); if (!_levelService.isLevelCompleted( currentState.level, currentState.score, newMoves, currentState.timeElapsed, updatedGemsCleared, )) { add(FailLevel()); return; } } emit(_GameLevelPlayingMatch( grid: newGrid, level: currentState.level, score: currentState.score, moves: newMoves, timeElapsed: currentState.timeElapsed, gemsCleared: currentState.gemsCleared, matches: matches, )); add(ProcessMatches()); } else { // Revert swap if no matches newGrid = newGrid.clone(); newGrid.setGem(event.row1, event.col1, gem1); newGrid.setGem(event.row2, event.col2, gem2); var done = Completer(); emit(GameLevelSwap( grid: newGrid, level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: currentState.gemsCleared, gem1: gem2, gem2: gem1, done: done.complete, )); // Wait for swap-back animation await done.future; done = Completer(); emit(GameLevelIdle( grid: newGrid, level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: currentState.gemsCleared, done: done.complete, )); await done.future; } } } void _onProcessMatches(ProcessMatches event, Emitter emit) async { // Handle only level-based matches (including Free Play) if (state is _GameLevelPlayingMatch) { final currentState = state as _GameLevelPlayingMatch; // Calculate score int newScore = currentState.score + currentState.matches.length * GameConstants.baseScore; if (event.combo > 0) { newScore += GameConstants.comboMultiplier * event.combo; } // Update gems cleared count final updatedGemsCleared = _updateGemsCleared(currentState.gemsCleared, currentState.matches); 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(GameLevelMatch( grid: newGrid, level: currentState.level, score: newScore, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: updatedGemsCleared, 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(GameLevelDrop( grid: newGrid, level: currentState.level, score: newScore, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: updatedGemsCleared, done: done.complete, )); await done.future; newGrid = newGrid.clone(); // Apply gravity final newGems = GravitySystem.generateGems(newGrid); // Emit state for drop columns done = Completer(); emit(GameLevelNewGems( grid: newGrid, level: currentState.level, score: newScore, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: updatedGemsCleared, 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(_GameLevelPlayingMatch( grid: newGrid, level: currentState.level, score: newScore, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: updatedGemsCleared, matches: newMatches, )); add(ProcessMatches(combo: event.combo + 1)); } else { final finalState = GameLevelIdle( grid: newGrid, level: currentState.level, score: newScore, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: updatedGemsCleared, ); emit(finalState); // Check level completion/failure after processing matches _checkLevelStatus(finalState); } } } void _onStartLevel(StartLevel event, Emitter emit) async { try { final level = await _levelService.getLevel(event.levelId); if (level == null) { emit(GameLevelFailed( level: Level( id: event.levelId, name: 'Unknown Level', description: '', constraints: const LevelConstraints(), objectives: const LevelObjectives(), starRating: const StarRating( criteria: StarCriteria.SCORE, thresholds: StarThresholds(oneStar: 0, twoStar: 0, threeStar: 0), ), availableGemTypes: [], ), score: 0, moves: 0, timeElapsed: 0, gemsCleared: {}, reason: 'Level not found', )); return; } final grid = GameGrid(width: level.gridWidth, height: level.gridHeight); // Start timer if level has time limit if (level.constraints.hasTimeLimit) { _gameTimer?.cancel(); _gameTimer = Timer.periodic(const Duration(seconds: 1), (timer) { add(UpdateTimer(timer.tick)); }); } final done = Completer(); emit(GameLevelStart( grid: grid, level: level, done: done.complete, )); await done.future; final matches = MatchDetector.findMatches(grid); emit(_GameLevelPlayingMatch( grid: grid, level: level, matches: matches, )); add(ProcessMatches()); } catch (e) { emit(GameLevelFailed( level: Level( id: event.levelId, name: 'Error', description: '', constraints: const LevelConstraints(), objectives: const LevelObjectives(), starRating: const StarRating( criteria: StarCriteria.SCORE, thresholds: StarThresholds(oneStar: 0, twoStar: 0, threeStar: 0), ), availableGemTypes: [], ), score: 0, moves: 0, timeElapsed: 0, gemsCleared: {}, reason: 'Failed to load level: $e', )); } } void _onUpdateTimer(UpdateTimer event, Emitter emit) { if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; // Check if time limit exceeded if (currentState.level.constraints.hasTimeLimit && event.timeElapsed >= currentState.level.constraints.timeLimit!) { _gameTimer?.cancel(); add(FailLevel()); return; } // Update the current state with new time if (currentState is GameLevelIdle) { emit(GameLevelIdle( grid: currentState.grid, level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: event.timeElapsed, gemsCleared: currentState.gemsCleared, )); } } } void _onCompleteLevel(CompleteLevel event, Emitter emit) async { if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; _gameTimer?.cancel(); // Calculate stars final stars = _levelService.calculateStars( currentState.level, currentState.score, currentState.moves, currentState.timeElapsed, currentState.gemsCleared, ); // Update progress final currentProgress = await _levelService.getLevelProgress(currentState.level.id); final newProgress = currentProgress.copyWith( completed: true, stars: stars > currentProgress.stars ? stars : currentProgress.stars, bestScore: currentState.score > currentProgress.bestScore ? currentState.score : currentProgress.bestScore, ); await _levelService.updateLevelProgress(newProgress); emit(GameLevelCompleted( level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: currentState.timeElapsed, stars: stars, gemsCleared: currentState.gemsCleared, )); } } void _onFailLevel(FailLevel event, Emitter emit) { if (state is GameLevelPlaying) { final currentState = state as GameLevelPlaying; _gameTimer?.cancel(); String reason = 'Level failed'; if (currentState.level.constraints.hasMoveLimit && currentState.moves >= currentState.level.constraints.maxMoves!) { reason = 'No moves remaining'; } else if (currentState.level.constraints.hasTimeLimit && currentState.timeElapsed >= currentState.level.constraints.timeLimit!) { reason = 'Time limit exceeded'; } emit(GameLevelFailed( level: currentState.level, score: currentState.score, moves: currentState.moves, timeElapsed: currentState.timeElapsed, gemsCleared: currentState.gemsCleared, reason: reason, )); } } // Helper method to update gems cleared count Map _updateGemsCleared(Map current, List matches) { final updated = Map.from(current); for (final gem in matches) { updated[gem.type] = (updated[gem.type] ?? 0) + 1; } return updated; } // Helper method to check level completion/failure void _checkLevelStatus(GameLevelPlaying currentState) { final level = currentState.level; // Check if level is completed if (_levelService.isLevelCompleted( level, currentState.score, currentState.moves, currentState.timeElapsed, currentState.gemsCleared, )) { add(CompleteLevel()); return; } // Check if level is failed if (_levelService.isLevelFailed( level, currentState.score, currentState.moves, currentState.timeElapsed, currentState.gemsCleared, )) { add(FailLevel()); return; } } }