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.
This commit is contained in:
parent
ea3f0c4e18
commit
64053d1d58
@ -16,8 +16,6 @@ abstract class GameEvent extends Equatable {
|
||||
List<Object> 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<Object> get props => [];
|
||||
}
|
||||
|
||||
// Internal match state event
|
||||
class _GamePlayingMatch extends GameState {
|
||||
final GameGrid grid;
|
||||
final int score;
|
||||
final int moves;
|
||||
final List<Gem> matches;
|
||||
|
||||
_GamePlayingMatch({
|
||||
required this.grid,
|
||||
this.score = 0,
|
||||
this.moves = 0,
|
||||
this.matches = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> 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<dynamic>? _]) {}
|
||||
|
||||
@override
|
||||
List<Object> 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<Object> get props => [grid, score, moves, gem1, gem2];
|
||||
}
|
||||
|
||||
class GamePlayingNewGems extends GamePlaying {
|
||||
final List<Gem> gems;
|
||||
|
||||
GamePlayingNewGems(
|
||||
{required super.grid,
|
||||
super.score,
|
||||
super.moves,
|
||||
super.done,
|
||||
required this.gems});
|
||||
|
||||
@override
|
||||
List<Object> get props => [grid, score, moves, gems];
|
||||
}
|
||||
|
||||
class GamePlayingMatch extends GamePlaying {
|
||||
final List<Gem> matches;
|
||||
final int combo;
|
||||
|
||||
GamePlayingMatch({
|
||||
required super.grid,
|
||||
super.score,
|
||||
super.moves,
|
||||
super.done,
|
||||
this.combo = 0,
|
||||
this.matches = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [grid, score, moves, matches];
|
||||
}
|
||||
|
||||
// Level-based states
|
||||
class GameLevelPlaying extends GameState {
|
||||
final GameGrid grid;
|
||||
@ -368,10 +256,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
|
||||
final LevelService _levelService = LevelService.instance;
|
||||
|
||||
GameBloc() : super(GameInitial()) {
|
||||
on<StartGame>(_onStartGame);
|
||||
on<SwapGems>(_onSwapGems);
|
||||
on<ProcessMatches>(_onProcessMatches);
|
||||
|
||||
on<StartLevel>(_onStartLevel);
|
||||
on<UpdateTimer>(_onUpdateTimer);
|
||||
on<CompleteLevel>(_onCompleteLevel);
|
||||
@ -384,84 +270,9 @@ class GameBloc extends Bloc<GameEvent, GameState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onStartGame(StartGame event, Emitter<GameState> 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<GameState> 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<GameEvent, GameState> {
|
||||
}
|
||||
|
||||
void _onProcessMatches(ProcessMatches event, Emitter<GameState> 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
|
||||
|
||||
@ -19,11 +19,9 @@ class GridComponent extends PositionComponent
|
||||
|
||||
@override
|
||||
Future<void> 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) {
|
||||
|
||||
@ -30,7 +30,7 @@ class _GameScreenState extends State<GameScreen> {
|
||||
body: BlocListener<GameBloc, GameState>(
|
||||
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<GameScreen> {
|
||||
GameWidget<MatchThreeGame>.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,
|
||||
|
||||
@ -53,9 +53,16 @@ class MenuScreen extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Start Free Play level (ID: 0)
|
||||
context.read<GameBloc>().add(StartLevel(0));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => GameScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: context.read<GameBloc>(),
|
||||
child: GameScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@ -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<Level>? _levels;
|
||||
Map<int, LevelProgress>? _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<List<Level>> loadLevels() async {
|
||||
if (_levels != null) return _levels!;
|
||||
@ -35,6 +55,11 @@ class LevelService {
|
||||
|
||||
/// Get a specific level by ID
|
||||
Future<Level?> 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<int, int> 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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user