match-three/lib/bloc/game_bloc.dart
savinmax 64053d1d58 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.
2025-09-21 17:26:51 +02:00

680 lines
18 KiB
Dart

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<Object> get props => [];
}
class SwapGems extends GameEvent {
final int row1, col1, row2, col2;
SwapGems(this.row1, this.col1, this.row2, this.col2);
@override
List<Object> get props => [row1, col1, row2, col2];
}
class ProcessMatches extends GameEvent {
final int combo;
ProcessMatches({this.combo = 0});
@override
List<Object> get props => [combo];
}
class StartLevel extends GameEvent {
final int levelId;
StartLevel(this.levelId);
@override
List<Object> get props => [levelId];
}
class UpdateTimer extends GameEvent {
final int timeElapsed;
UpdateTimer(this.timeElapsed);
@override
List<Object> get props => [timeElapsed];
}
class CompleteLevel extends GameEvent {}
class FailLevel extends GameEvent {}
// States
typedef DoneCallback = void Function([FutureOr<dynamic>?]);
abstract class GameState extends Equatable {
@override
List<Object> 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<int, int> 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<dynamic>? _]) {}
@override
List<Object> 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<Object> get props =>
[grid, score, moves, level, timeElapsed, gemsCleared, gem1, gem2];
}
class GameLevelMatch extends GameLevelPlaying {
final List<Gem> 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<Object> 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<Gem> gems;
GameLevelNewGems({
required super.grid,
required super.level,
super.score,
super.moves,
super.done,
super.timeElapsed,
super.gemsCleared,
required this.gems,
});
@override
List<Object> 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<int, int> gemsCleared;
GameLevelCompleted({
required this.level,
required this.score,
required this.moves,
required this.timeElapsed,
required this.stars,
required this.gemsCleared,
});
@override
List<Object> 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<int, int> 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<Object> 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<int, int> gemsCleared;
final List<Gem> 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<Object> get props =>
[grid, level, score, moves, timeElapsed, gemsCleared, matches];
}
// Bloc
class GameBloc extends Bloc<GameEvent, GameState> {
Timer? _gameTimer;
final LevelService _levelService = LevelService.instance;
GameBloc() : super(GameInitial()) {
on<SwapGems>(_onSwapGems);
on<ProcessMatches>(_onProcessMatches);
on<StartLevel>(_onStartLevel);
on<UpdateTimer>(_onUpdateTimer);
on<CompleteLevel>(_onCompleteLevel);
on<FailLevel>(_onFailLevel);
}
@override
Future<void> close() {
_gameTimer?.cancel();
return super.close();
}
void _onSwapGems(SwapGems event, Emitter<GameState> 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;
}
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(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.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 animation
await done.future;
}
}
}
void _onProcessMatches(ProcessMatches event, Emitter<GameState> 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<GameState> 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();
// 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<GameState> 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<GameState> 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<GameState> 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<int, int> _updateGemsCleared(Map<int, int> current, List<Gem> matches) {
final updated = Map<int, int>.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;
}
}
}