match-three/lib/bloc/game_bloc.dart

320 lines
7.7 KiB
Dart

import 'dart:async';
import 'package:flame/game.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../game/models/grid.dart';
import '../game/models/gem.dart';
import '../game/systems/match_detector.dart';
import '../game/systems/gravity_system.dart';
import '../utils/constants.dart';
// Events
abstract class GameEvent extends Equatable {
@override
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);
@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];
}
// States
typedef DoneCallback = void Function([FutureOr<dynamic>?]);
abstract class GameState extends Equatable {
@override
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];
}
// Bloc
class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(GameInitial()) {
on<StartGame>(_onStartGame);
on<SwapGems>(_onSwapGems);
on<ProcessMatches>(_onProcessMatches);
}
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 {
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;
}
}
}
void _onProcessMatches(ProcessMatches event, Emitter<GameState> emit) async {
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));
}
}
}
}