210 lines
4.7 KiB
Dart
210 lines
4.7 KiB
Dart
|
|
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();
|
||
|
|
}
|
||
|
|
}
|