import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../game/engine/game_engine.dart'; import '../game/engine/piece_generator.dart'; import '../game/models/grid.dart'; import '../game/models/objective.dart'; import '../game/models/piece.dart'; import '../game/models/stage.dart'; /// Immutable snapshot of one engine moment; the only game state the UI sees. class GameViewState { const GameViewState({ required this.grid, required this.tray, required this.score, required this.comboStreak, required this.movesLeft, required this.moveLimit, required this.phase, required this.stuckReason, required this.objectives, required this.starsEarned, required this.objectiveProgress, required this.lastPlacement, required this.fxTick, }); final GridState grid; final List tray; final int score; final int comboStreak; final int movesLeft; final int moveLimit; final GamePhase phase; final StuckReason? stuckReason; final List objectives; final int starsEarned; final double objectiveProgress; final PlacementResult? lastPlacement; /// Increments on every accepted placement so animations can retrigger. final int fxTick; } /// Bridges the pure-Dart [GameEngine] to the widget tree. The engine object /// never leaks past this class. class GameSessionNotifier extends Notifier { GameEngine? _engine; StageConfig? _stage; PieceGenerator? _generatorOverride; int _attempt = 0; int _fxTick = 0; @override GameViewState? build() => null; void startStage(StageConfig stage, {int attempt = 0, PieceGenerator? generator}) { _stage = stage; _attempt = attempt; _generatorOverride = generator; _engine = GameEngine(stage, attempt: attempt, generator: generator); _fxTick = 0; _publish(lastPlacement: null); } /// Restarts the current stage as a new attempt (fresh piece sequence). void restart() { final stage = _stage; if (stage == null) throw StateError('no stage to restart'); startStage(stage, attempt: _attempt + 1, generator: _generatorOverride); } bool tryPlace(int trayIndex, int x, int y) { final engine = _engine; if (engine == null) return false; final result = engine.tryPlace(trayIndex, x, y); if (result == null) return false; _fxTick++; _publish(lastPlacement: result); return true; } bool canPlaceAt(int trayIndex, int x, int y) => _engine?.tryPlaceWouldSucceed(trayIndex, x, y) ?? false; void useContinue() { _engine?.useContinue(); _publish(lastPlacement: null); } void addExtraMoves() { _engine?.addExtraMoves(); _publish(lastPlacement: null); } void declineAndLose() { _engine?.declineAndLose(); _publish(lastPlacement: null); } void _publish({required PlacementResult? lastPlacement}) { final engine = _engine!; state = GameViewState( grid: engine.grid, tray: engine.tray, score: engine.score, comboStreak: engine.combo.streak, movesLeft: engine.movesLeft, moveLimit: engine.movesLeft + engine.movesUsed, phase: engine.phase, stuckReason: engine.stuckReason, objectives: engine.objectives, starsEarned: engine.starsEarned, objectiveProgress: engine.objectiveProgress, lastPlacement: lastPlacement, fxTick: _fxTick, ); } }