import 'dart:async'; import 'package:flutter/foundation.dart'; import 'engine.dart'; import 'levels.dart'; import 'types.dart'; class MismatchState { final int token; final List ids; const MismatchState({required this.token, required this.ids}); MismatchState copyWith({int? token, List? ids}) { return MismatchState(token: token ?? this.token, ids: ids ?? this.ids); } } class GameController extends ChangeNotifier { final LevelConfig level; final List allFaceIds; late final int flipBackDelayMs; late final int? maxMistakes; GameStatus status = GameStatus.playing; List cards = []; List flippedIds = []; int moves = 0; int mistakes = 0; MismatchState mismatch = const MismatchState(token: 0, ids: []); int score = 0; int streak = 0; bool _locked = false; Timer? _timer; GameController({required this.level, required this.allFaceIds}) { flipBackDelayMs = level.flipBackDelayMs ?? 600; maxMistakes = level.maxMistakes; cards = _build(); } List _build() { assert( level.totalCards % 2 == 0, 'level.totalCards must be even, current=${level.totalCards}, level=${level.id}', ); return createDeck( rows: level.rows, totalCards: level.totalCards, poolSize: level.poolSize, allFaceIds: allFaceIds, ); } int _pow2(int n) => 1 << n; void _clearTimer() { _timer?.cancel(); _timer = null; } void reset() { _clearTimer(); _locked = false; status = GameStatus.playing; cards = _build(); flippedIds = []; moves = 0; mistakes = 0; mismatch = const MismatchState(token: 0, ids: []); score = 0; streak = 0; notifyListeners(); } void reduceMistakes(int n) { if (n <= 0) return; mistakes = (mistakes - n).clamp(0, 1 << 30); if (status == GameStatus.lost) { status = GameStatus.playing; } mismatch = mismatch.copyWith(ids: []); notifyListeners(); } void reduceMoves(int n) { if (n <= 0) return; moves = (moves - n).clamp(0, 1 << 30); notifyListeners(); } void flip(String cardId) { if (status != GameStatus.playing || _locked) return; final index = cards.indexWhere((e) => e.id == cardId); if (index < 0) return; final target = cards[index]; if (target.isMatched || target.isFlipped) return; cards = cards .map((e) => e.id == cardId ? e.copyWith(isFlipped: true) : e) .toList(); flippedIds = [...flippedIds, cardId]; if (flippedIds.length < 2) { notifyListeners(); return; } _locked = true; final aId = flippedIds[0]; final bId = flippedIds[1]; final a = cards.firstWhere((e) => e.id == aId); final b = cards.firstWhere((e) => e.id == bId); moves += 1; if (a.faceId == b.faceId) { cards = cards.map((e) { if (e.id == aId || e.id == bId) { return e.copyWith(isMatched: true); } return e; }).toList(); streak += 1; score += _pow2(streak - 1); flippedIds = []; mismatch = mismatch.copyWith(ids: []); if (cards.every((e) => e.isMatched)) { status = GameStatus.won; } _locked = false; notifyListeners(); return; } mistakes += 1; score = score > 0 ? score - 1 : 0; streak = 0; mismatch = MismatchState(token: mismatch.token + 1, ids: [aId, bId]); final lostNow = maxMistakes != null && mistakes >= maxMistakes!; if (lostNow) { status = GameStatus.lost; } _clearTimer(); _timer = Timer(Duration(milliseconds: flipBackDelayMs), () { cards = cards.map((e) { if (e.id == aId || e.id == bId) { return e.copyWith(isFlipped: false); } return e; }).toList(); flippedIds = []; mismatch = mismatch.copyWith(ids: []); final lost = maxMistakes != null && mistakes >= maxMistakes!; if (lost) { status = GameStatus.lost; } _locked = false; notifyListeners(); }); notifyListeners(); } int reviveWithReward({ int baseSeconds = 30, int penaltyPerMove = 10, int? rollbackMistakes, int? rollbackMoves, }) { final backMistakes = rollbackMistakes ?? mistakes; final rewardSec = baseSeconds < 0 ? 0 : baseSeconds; _locked = false; _clearTimer(); mistakes = (mistakes - backMistakes).clamp(0, 1 << 30); if (rollbackMoves != null) { moves = (moves - rollbackMoves).clamp(0, 1 << 30); } flippedIds = []; mismatch = mismatch.copyWith(ids: []); status = GameStatus.playing; notifyListeners(); return rewardSec; } @override void dispose() { _clearTimer(); super.dispose(); } }