Files
flame_flip/lib/game/game_controller.dart

210 lines
4.7 KiB
Dart
Raw Normal View History

2026-06-10 18:23:39 +08:00
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'engine.dart';
import 'levels.dart';
import 'types.dart';
class MismatchState {
final int token;
final List<String> ids;
const MismatchState({required this.token, required this.ids});
MismatchState copyWith({int? token, List<String>? ids}) {
return MismatchState(token: token ?? this.token, ids: ids ?? this.ids);
}
}
class GameController extends ChangeNotifier {
final LevelConfig level;
final List<String> allFaceIds;
late final int flipBackDelayMs;
late final int? maxMistakes;
GameStatus status = GameStatus.playing;
List<CardModel> cards = [];
List<String> 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<CardModel> _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();
}
}