match-three/lib/services/level_service.dart
savinmax 64053d1d58 Refactor game state management to use level-based architecture
Remove legacy GamePlaying states and consolidate all gameplay logic
to use the GameLevelPlaying state system. This simplifies state
management by eliminating duplicate code paths and ensures consistent
behavior across all game modes including Free Play.
2025-09-21 17:26:51 +02:00

289 lines
8.9 KiB
Dart

import 'dart:convert';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../game/models/level.dart';
class LevelService {
static const String _progressKey = 'level_progress';
static const int freePlayLevelId = 0; // Special ID for Free Play
static LevelService? _instance;
static LevelService get instance => _instance ??= LevelService._();
LevelService._();
List<Level>? _levels;
Map<int, LevelProgress>? _progress;
/// Get the hardcoded Free Play level
Level get freePlayLevel => const Level(
id: freePlayLevelId,
name: 'Free Play',
description: 'Play without constraints - match gems and score points!',
constraints: LevelConstraints(), // No constraints
objectives: LevelObjectives(), // No objectives
starRating: StarRating(
criteria: StarCriteria.SCORE,
thresholds: StarThresholds(
oneStar: 1000,
twoStar: 5000,
threeStar: 10000,
),
),
availableGemTypes: [0, 1, 2, 3, 4, 5], // All gem types available
unlocked: true, // Always unlocked
);
/// Load all levels from JSON configuration
Future<List<Level>> loadLevels() async {
if (_levels != null) return _levels!;
try {
final String jsonString =
await rootBundle.loadString('assets/levels/levels.json');
final Map<String, dynamic> jsonData = json.decode(jsonString);
_levels = (jsonData['levels'] as List)
.map((levelJson) => Level.fromJson(levelJson))
.toList();
return _levels!;
} catch (e) {
throw Exception('Failed to load levels: $e');
}
}
/// Get a specific level by ID
Future<Level?> getLevel(int levelId) async {
// Return Free Play level for ID 0
if (levelId == freePlayLevelId) {
return freePlayLevel;
}
final levels = await loadLevels();
try {
return levels.firstWhere((level) => level.id == levelId);
} catch (e) {
return null;
}
}
/// Load level progress from persistent storage
Future<Map<int, LevelProgress>> loadProgress() async {
if (_progress != null) return _progress!;
try {
final prefs = await SharedPreferences.getInstance();
final String? progressJson = prefs.getString(_progressKey);
if (progressJson != null) {
final Map<String, dynamic> progressData = json.decode(progressJson);
_progress = progressData.map(
(key, value) => MapEntry(
int.parse(key),
LevelProgress.fromJson(value),
),
);
} else {
// Initialize default progress - level 1 is unlocked
_progress = {1: const LevelProgress(levelId: 1, unlocked: true)};
await saveProgress();
}
return _progress!;
} catch (e) {
// Fallback to default progress
_progress = {1: const LevelProgress(levelId: 1, unlocked: true)};
return _progress!;
}
}
/// Save level progress to persistent storage
Future<void> saveProgress() async {
if (_progress == null) return;
try {
final prefs = await SharedPreferences.getInstance();
final Map<String, dynamic> progressData = _progress!.map(
(key, value) => MapEntry(key.toString(), value.toJson()),
);
await prefs.setString(_progressKey, json.encode(progressData));
} catch (e) {
throw Exception('Failed to save progress: $e');
}
}
/// Get progress for a specific level
Future<LevelProgress> getLevelProgress(int levelId) async {
final progress = await loadProgress();
return progress[levelId] ?? LevelProgress(levelId: levelId);
}
/// Update progress for a specific level
Future<void> updateLevelProgress(LevelProgress newProgress) async {
final progress = await loadProgress();
_progress![newProgress.levelId] = newProgress;
// Auto-unlock next level if current level is completed
if (newProgress.completed) {
final nextLevelId = newProgress.levelId + 1;
final levels = await loadLevels();
// Check if next level exists
if (levels.any((level) => level.id == nextLevelId)) {
final nextProgress =
progress[nextLevelId] ?? LevelProgress(levelId: nextLevelId);
if (!nextProgress.unlocked) {
_progress![nextLevelId] = nextProgress.copyWith(unlocked: true);
}
}
}
await saveProgress();
}
/// Get all levels with their current progress
Future<List<LevelWithProgress>> getLevelsWithProgress() async {
final levels = await loadLevels();
final progress = await loadProgress();
return levels.map((level) {
final levelProgress =
progress[level.id] ?? LevelProgress(levelId: level.id);
return LevelWithProgress(level: level, progress: levelProgress);
}).toList();
}
/// Calculate stars earned based on level performance
int calculateStars(Level level, int score, int movesUsed, int timeUsed,
Map<int, int> gemsCleared) {
final criteria = level.starRating.criteria;
final thresholds = level.starRating.thresholds;
int value;
switch (criteria) {
case StarCriteria.SCORE:
value = score;
break;
case StarCriteria.MOVES_REMAINING:
value = (level.constraints.maxMoves ?? 0) - movesUsed;
break;
case StarCriteria.TIME_REMAINING:
value = (level.constraints.timeLimit ?? 0) - timeUsed;
break;
case StarCriteria.EFFICIENCY:
// For efficiency, we use score but consider objectives completion
value = score;
// Bonus for completing objectives
if (_areObjectivesCompleted(level.objectives, gemsCleared)) {
value += 500; // Bonus points for completing objectives
}
break;
case StarCriteria.COMBINED:
// Combined scoring considers multiple factors
value = score;
if (level.constraints.hasMoveLimit) {
final movesRemaining = (level.constraints.maxMoves ?? 0) - movesUsed;
value += movesRemaining * 50; // Bonus for efficient moves
}
if (_areObjectivesCompleted(level.objectives, gemsCleared)) {
value += 500; // Bonus for completing objectives
}
break;
}
if (value >= thresholds.threeStar) return 3;
if (value >= thresholds.twoStar) return 2;
if (value >= thresholds.oneStar) return 1;
return 0;
}
/// Check if level objectives are completed
bool _areObjectivesCompleted(
LevelObjectives objectives, Map<int, int> gemsCleared) {
// If there are no objectives, they are considered completed
if (!objectives.hasGemTypeObjectives) {
return true;
}
for (final entry in objectives.clearGemTypes.entries) {
final requiredCount = entry.value;
final clearedCount = gemsCleared[entry.key] ?? 0;
if (clearedCount < requiredCount) {
return false;
}
}
return true;
}
/// Check if level is completed (all constraints and objectives met)
bool isLevelCompleted(Level level, int score, int moves, int timeUsed,
Map<int, int> gemsCleared) {
// Check score constraint
if (level.id == 0 ||
(level.constraints.hasScoreTarget &&
score < level.constraints.targetScore!)) {
return false;
}
// Check move constraint
if (level.constraints.hasMoveLimit && moves > level.constraints.maxMoves!) {
return false;
}
// Check time constraint
if (level.constraints.hasTimeLimit &&
timeUsed > level.constraints.timeLimit!) {
return false;
}
// Check objectives
if (!_areObjectivesCompleted(level.objectives, gemsCleared)) {
return false;
}
return true;
}
/// Check if level is failed (constraints violated but not completed)
bool isLevelFailed(Level level, int score, int moves, int timeUsed,
Map<int, int> gemsCleared) {
// Failed if move limit exceeded and not completed
if (level.constraints.hasMoveLimit &&
moves >= level.constraints.maxMoves!) {
return !isLevelCompleted(level, score, moves, timeUsed, gemsCleared);
}
// Failed if time limit exceeded and not completed
if (level.constraints.hasTimeLimit &&
timeUsed >= level.constraints.timeLimit!) {
return !isLevelCompleted(level, score, moves, timeUsed, gemsCleared);
}
return false;
}
/// Reset all progress (for testing or new game)
Future<void> resetProgress() async {
_progress = {1: const LevelProgress(levelId: 1, unlocked: true)};
await saveProgress();
}
}
/// Helper class to combine level and progress data
class LevelWithProgress {
final Level level;
final LevelProgress progress;
const LevelWithProgress({
required this.level,
required this.progress,
});
bool get isUnlocked => progress.unlocked || level.unlocked;
bool get isCompleted => progress.completed;
int get stars => progress.stars;
int get bestScore => progress.bestScore;
}