first commit
This commit is contained in:
209
lib/game/game_controller.dart
Normal file
209
lib/game/game_controller.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user