match-three/lib/services/level_service.dart
savinmax 3f12ce8d3f Add dynamic grid sizing support for levels
- Add gridWidth and gridHeight properties to level configuration
- Update GameGrid to accept custom dimensions instead of using constants
- Modify GridComponent to calculate gem size based on grid dimensions
- Update MatchThreeGame constructor to pass grid dimensions
- Ensure proper scaling and positioning for variable grid sizes
2025-09-21 18:06:00 +02:00

291 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!',
gridWidth: 5,
gridHeight: 5,
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;
}