diff --git a/assets/levels/levels.json b/assets/levels/levels.json index 5ada4cf..f71bdb1 100644 --- a/assets/levels/levels.json +++ b/assets/levels/levels.json @@ -4,6 +4,8 @@ "id": 1, "name": "Getting Started", "description": "Learn the basics by reaching the target score", + "gridWidth": 8, + "gridHeight": 8, "constraints": { "targetScore": 1000 }, @@ -23,6 +25,8 @@ "id": 2, "name": "Move Master", "description": "Reach the target score with limited moves", + "gridWidth": 7, + "gridHeight": 9, "constraints": { "targetScore": 1500, "maxMoves": 20 @@ -43,6 +47,8 @@ "id": 3, "name": "Time Trial", "description": "Beat the clock to reach your goal", + "gridWidth": 10, + "gridHeight": 6, "constraints": { "targetScore": 2000, "timeLimit": 60 @@ -63,6 +69,8 @@ "id": 4, "name": "Color Focus", "description": "Clear specific gem colors while reaching the score", + "gridWidth": 6, + "gridHeight": 10, "constraints": { "targetScore": 1800 }, @@ -87,6 +95,8 @@ "id": 5, "name": "Ultimate Challenge", "description": "Master all skills in this combined challenge", + "gridWidth": 9, + "gridHeight": 8, "constraints": { "targetScore": 2500, "maxMoves": 25 diff --git a/lib/bloc/game_bloc.dart b/lib/bloc/game_bloc.dart index 88b3ce1..53893d8 100644 --- a/lib/bloc/game_bloc.dart +++ b/lib/bloc/game_bloc.dart @@ -501,7 +501,7 @@ class GameBloc extends Bloc { return; } - final grid = GameGrid(); + final grid = GameGrid(width: level.gridWidth, height: level.gridHeight); // Start timer if level has time limit if (level.constraints.hasTimeLimit) { diff --git a/lib/game/components/gem_component.dart b/lib/game/components/gem_component.dart index c5237b6..9b990cb 100644 --- a/lib/game/components/gem_component.dart +++ b/lib/game/components/gem_component.dart @@ -24,7 +24,7 @@ class GemComponent extends RectangleComponent with TapCallbacks { GemComponent({required this.gem, required this.gridComponent}) : super( - size: Vector2.all(GameConstants.gemSize - 4), + size: Vector2.all(gridComponent.gemSize - 4), paint: Paint()..color = gemColors[gem.type % gemColors.length], ); diff --git a/lib/game/components/grid_component.dart b/lib/game/components/grid_component.dart index 16e112c..284823a 100644 --- a/lib/game/components/grid_component.dart +++ b/lib/game/components/grid_component.dart @@ -12,16 +12,22 @@ class GridComponent extends PositionComponent late GameGrid gameGrid; final List> gemComponents = []; final MatchThreeGame game; - int? levelId; + final int levelId; bool canInterract = false; + double gemSize = GameConstants.gemSize; + final int gridWidth; // Default grid width + final int gridHeight; // Default grid height - GridComponent(this.game, this.levelId) : super(); + GridComponent(this.game, this.levelId, this.gridHeight, this.gridWidth) + : super(); @override Future onLoad() async { - // Always use StartLevel - Free Play is level ID 0 - final id = levelId ?? 0; // Default to Free Play if no level specified - game.gameBloc.add(StartLevel(id)); + // Calculate dynamic gem size based on available screen space + // For now, use a reasonable default size that will be updated when we have screen dimensions + gemSize = + GameConstants.calculateGemSize(gridWidth, gridHeight, 600.0, 800.0); + game.gameBloc.add(StartLevel(levelId)); } void _createGemComponents() { @@ -39,15 +45,15 @@ class GridComponent extends PositionComponent } } - for (int row = 0; row < GameConstants.gridHeight; row++) { + for (int row = 0; row < gridHeight; row++) { gemComponents.add([]); - for (int col = 0; col < GameConstants.gridWidth; col++) { + for (int col = 0; col < gridWidth; col++) { final gem = gameGrid.getGem(row, col); if (gem != null) { final gemComponent = GemComponent(gem: gem, gridComponent: this); gemComponent.position = Vector2( - col * GameConstants.gemSize + GameConstants.gridPadding, - row * GameConstants.gemSize + GameConstants.gridPadding, + col * gemSize + GameConstants.gridPadding, + row * gemSize + GameConstants.gridPadding, ); gemComponents[row].add(gemComponent); add(gemComponent); @@ -88,13 +94,12 @@ class GridComponent extends PositionComponent } // Update grid and create components for all GameLevelPlaying states - final levelState = state as GameLevelPlaying; - gameGrid = levelState.grid; + gameGrid = state.grid; gameGrid.printGrid(); _createGemComponents(); await Future.delayed(const Duration(milliseconds: 100)); print("Updated state ${state.runtimeType.toString()}"); - levelState.done(); + state.done(); } GemComponent? _findGemComponent(Gem gem) { @@ -137,8 +142,8 @@ class GridComponent extends PositionComponent _animateDrop(GameGrid newGrid) async { final effects = []; - for (int col = GameConstants.gridWidth - 1; col >= 0; col--) { - for (int row = GameConstants.gridHeight - 1; row >= 0; row--) { + for (int col = gridWidth - 1; col >= 0; col--) { + for (int row = gridHeight - 1; row >= 0; row--) { final gem = gameGrid.getGem(row, col); // if gem is removed - animate fall effect from top @@ -149,8 +154,8 @@ class GridComponent extends PositionComponent final gemComponent = gemComponents[row][col]; if (gemAbove != null) { final targetPosition = Vector2( - col * GameConstants.gemSize + GameConstants.gridPadding, - fallToRow * GameConstants.gemSize + GameConstants.gridPadding, + col * gemSize + GameConstants.gridPadding, + fallToRow * gemSize + GameConstants.gridPadding, ); print( "@@ Fall $row, $col -> $fallToRow, $col ${gemComponent == null ? "NULL" : "OK"}"); @@ -172,12 +177,12 @@ class GridComponent extends PositionComponent // Create gem component from gem and animate falling effect to proper location final gemComponent = GemComponent(gem: gem, gridComponent: this); final targetPosition = Vector2( - gem.col * GameConstants.gemSize + GameConstants.gridPadding, - gem.row * GameConstants.gemSize + GameConstants.gridPadding, + gem.col * gemSize + GameConstants.gridPadding, + gem.row * gemSize + GameConstants.gridPadding, ); gemComponent.position.x = targetPosition.x; - gemComponent.position.y = -GameConstants.gemSize; + gemComponent.position.y = -gemSize; add(gemComponent); effects.add(gemComponent.animateFall(targetPosition)); } diff --git a/lib/game/match_three_game.dart b/lib/game/match_three_game.dart index b7098aa..0557020 100644 --- a/lib/game/match_three_game.dart +++ b/lib/game/match_three_game.dart @@ -12,18 +12,24 @@ const gridSize = 8 * 64.0 + 32.0; // 8 gems * 64px + padding class MatchThreeGame extends FlameGame { late BackgroundComponent backgroundComponent; late GameBloc gameBloc; - final int? levelId; + final int levelId; + final int gridHeight; + final int gridWidth; GridComponent? gridComponent; Gem? selectedGem; - MatchThreeGame(this.levelId) : super(); + MatchThreeGame( + {required this.levelId, + required this.gridHeight, + required this.gridWidth}) + : super(); @override Future onLoad() async { backgroundComponent = BackgroundComponent(); add(backgroundComponent); - gridComponent = GridComponent(this, levelId); + gridComponent = GridComponent(this, levelId, gridHeight, gridWidth); add(gridComponent!); } diff --git a/lib/game/models/grid.dart b/lib/game/models/grid.dart index e32e333..fecce34 100644 --- a/lib/game/models/grid.dart +++ b/lib/game/models/grid.dart @@ -5,16 +5,18 @@ import '../../utils/constants.dart'; class GameGrid { late List> _grid; final Random _random = Random(); + final int width; + final int height; - GameGrid() { + GameGrid({required this.width, required this.height}) { _initializeGrid(); } void _initializeGrid() { _grid = List.generate( - GameConstants.gridHeight, + height, (row) => List.generate( - GameConstants.gridWidth, + width, (col) => Gem( type: _random.nextInt(GameConstants.gemTypes.length), row: row, @@ -25,20 +27,14 @@ class GameGrid { } Gem? getGem(int row, int col) { - if (row < 0 || - row >= GameConstants.gridHeight || - col < 0 || - col >= GameConstants.gridWidth) { + if (row < 0 || row >= height || col < 0 || col >= width) { return null; } return _grid[row][col]; } void setGem(int row, int col, Gem? gem) { - if (row >= 0 && - row < GameConstants.gridHeight && - col >= 0 && - col < GameConstants.gridWidth) { + if (row >= 0 && row < height && col >= 0 && col < width) { _grid[row][col] = gem; } } @@ -46,14 +42,11 @@ class GameGrid { List> get grid => _grid; bool isValidPosition(int row, int col) { - return row >= 0 && - row < GameConstants.gridHeight && - col >= 0 && - col < GameConstants.gridWidth; + return row >= 0 && row < height && col >= 0 && col < width; } clone() { - final clonedGrid = GameGrid(); + final clonedGrid = GameGrid(width: width, height: height); for (int row = 0; row < _grid.length; row++) { for (int col = 0; col < _grid[row].length; col++) { final gem = getGem(row, col); diff --git a/lib/game/models/level.dart b/lib/game/models/level.dart index 40e3235..cf76837 100644 --- a/lib/game/models/level.dart +++ b/lib/game/models/level.dart @@ -18,6 +18,8 @@ class Level extends Equatable { final StarRating starRating; final List availableGemTypes; final bool unlocked; + final int gridWidth; + final int gridHeight; const Level({ required this.id, @@ -28,6 +30,9 @@ class Level extends Equatable { required this.starRating, required this.availableGemTypes, this.unlocked = false, + this.gridWidth = + 9, // Default to current grid size for backward compatibility + this.gridHeight = 8, }); factory Level.fromJson(Map json) { @@ -40,6 +45,8 @@ class Level extends Equatable { starRating: StarRating.fromJson(json['starRating']), availableGemTypes: List.from(json['availableGemTypes']), unlocked: json['unlocked'] ?? false, + gridWidth: json['gridWidth'] ?? 9, // Default for backward compatibility + gridHeight: json['gridHeight'] ?? 8, ); } @@ -53,6 +60,8 @@ class Level extends Equatable { 'starRating': starRating.toJson(), 'availableGemTypes': availableGemTypes, 'unlocked': unlocked, + 'gridWidth': gridWidth, + 'gridHeight': gridHeight, }; } @@ -65,6 +74,8 @@ class Level extends Equatable { StarRating? starRating, List? availableGemTypes, bool? unlocked, + int? gridWidth, + int? gridHeight, }) { return Level( id: id ?? this.id, @@ -75,6 +86,8 @@ class Level extends Equatable { starRating: starRating ?? this.starRating, availableGemTypes: availableGemTypes ?? this.availableGemTypes, unlocked: unlocked ?? this.unlocked, + gridWidth: gridWidth ?? this.gridWidth, + gridHeight: gridHeight ?? this.gridHeight, ); } @@ -88,6 +101,8 @@ class Level extends Equatable { starRating, availableGemTypes, unlocked, + gridWidth, + gridHeight, ]; } diff --git a/lib/game/systems/gravity_system.dart b/lib/game/systems/gravity_system.dart index 6c497c6..f40f09f 100644 --- a/lib/game/systems/gravity_system.dart +++ b/lib/game/systems/gravity_system.dart @@ -5,7 +5,7 @@ import '../../utils/constants.dart'; class GravitySystem { static void applyGravity(GameGrid grid) { - for (int col = 0; col < GameConstants.gridWidth; col++) { + for (int col = 0; col < grid.width; col++) { _dropColumn(grid, col); } } @@ -14,8 +14,8 @@ class GravitySystem { // Fill empty spaces with new gems final random = Random(); final List newGems = []; - for (int row = 0; row < GameConstants.gridHeight; row++) { - for (int col = 0; col < GameConstants.gridWidth; col++) { + for (int row = 0; row < grid.height; row++) { + for (int col = 0; col < grid.width; col++) { if (grid.getGem(row, col) == null) { final gem = Gem( row: row, @@ -34,7 +34,7 @@ class GravitySystem { final gems = []; // Collect non-null gems from bottom to top - for (int row = GameConstants.gridHeight - 1; row >= 0; row--) { + for (int row = grid.height - 1; row >= 0; row--) { final gem = grid.getGem(row, col); if (gem != null) { gems.add(gem); @@ -42,13 +42,13 @@ class GravitySystem { } // Clear column - for (int row = 0; row < GameConstants.gridHeight; row++) { + for (int row = 0; row < grid.height; row++) { grid.setGem(row, col, null); } // Place gems at bottom for (int i = 0; i < gems.length; i++) { - final newRow = GameConstants.gridHeight - 1 - i; + final newRow = grid.height - 1 - i; grid.setGem(newRow, col, gems[i].copyWith(row: newRow, col: col)); } } diff --git a/lib/game/systems/match_detector.dart b/lib/game/systems/match_detector.dart index 2397cb6..6e7ee0a 100644 --- a/lib/game/systems/match_detector.dart +++ b/lib/game/systems/match_detector.dart @@ -8,14 +8,14 @@ class MatchDetector { // Check matches List currentMatch = []; - for (int row = 0; row < GameConstants.gridHeight; row++) { - for (int col = 0; col < GameConstants.gridWidth; col++) { + for (int row = 0; row < grid.height; row++) { + for (int col = 0; col < grid.width; col++) { final gem = grid.getGem(row, col); if (gem == null || matches.contains(gem)) { continue; } currentMatch.add(gem); - for (int i = row + 1; i < GameConstants.gridHeight; i++) { + for (int i = row + 1; i < grid.height; i++) { final nextGem = grid.getGem(i, col); if (nextGem == null || nextGem.type != gem.type) { break; @@ -27,7 +27,7 @@ class MatchDetector { } currentMatch.clear(); currentMatch.add(gem); - for (int i = col + 1; i < GameConstants.gridWidth; i++) { + for (int i = col + 1; i < grid.width; i++) { final nextGem = grid.getGem(row, i); if (nextGem == null || nextGem.type != gem.type) { break; diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart index c47bcec..e5499d1 100644 --- a/lib/screens/game_screen.dart +++ b/lib/screens/game_screen.dart @@ -5,11 +5,13 @@ import 'package:match_three/game/models/gem.dart'; import '../game/match_three_game.dart'; import '../bloc/game_bloc.dart'; -// ignore: must_be_immutable class GameScreen extends StatefulWidget { - int? levelId; + final int levelId; + final int gridHeight; + final int gridWidth; - GameScreen({super.key, this.levelId}); + const GameScreen( + {super.key, this.levelId = 0, this.gridHeight = 5, this.gridWidth = 5}); @override State createState() => _GameScreenState(); @@ -21,7 +23,10 @@ class _GameScreenState extends State { @override void initState() { super.initState(); - game = MatchThreeGame(widget.levelId); + game = MatchThreeGame( + levelId: widget.levelId, + gridHeight: widget.gridHeight, + gridWidth: widget.gridWidth); } @override diff --git a/lib/screens/level_selection_screen.dart b/lib/screens/level_selection_screen.dart index 6028a41..2e1431e 100644 --- a/lib/screens/level_selection_screen.dart +++ b/lib/screens/level_selection_screen.dart @@ -164,7 +164,7 @@ class _LevelSelectionScreenState extends State { final isCompleted = levelWithProgress.isCompleted; return GestureDetector( - onTap: isUnlocked ? () => _startLevel(level.id) : null, + onTap: isUnlocked ? () => _startLevel(level) : null, child: Container( decoration: BoxDecoration( color: isUnlocked ? Colors.white : Colors.grey.shade400, @@ -291,9 +291,9 @@ class _LevelSelectionScreenState extends State { ); } - void _startLevel(int levelId) { + void _startLevel(Level level) { // Start the level first - print("Starting Level $levelId"); + print("Starting Level ${level.id}"); // Then navigate to game screen Navigator.push( @@ -301,7 +301,11 @@ class _LevelSelectionScreenState extends State { MaterialPageRoute( builder: (context) => BlocProvider.value( value: context.read(), - child: GameScreen(levelId: levelId), + child: GameScreen( + levelId: level.id, + gridHeight: level.gridHeight, + gridWidth: level.gridWidth, + ), ), ), ).then((_) { diff --git a/lib/services/level_service.dart b/lib/services/level_service.dart index dc525f2..fc9106a 100644 --- a/lib/services/level_service.dart +++ b/lib/services/level_service.dart @@ -20,6 +20,8 @@ class LevelService { 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( diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 9858e88..7671b97 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -1,6 +1,6 @@ +import 'dart:math' as math; + class GameConstants { - static const int gridWidth = 9; - static const int gridHeight = 8; static const double gemSize = 64.0; static const double gridPadding = 16.0; static const int minMatchLength = 3; @@ -16,4 +16,18 @@ class GameConstants { // Scoring static const int baseScore = 100; static const int comboMultiplier = 50; + + // Dynamic gem size calculation + static double calculateGemSize(int gridWidth, int gridHeight, + double availableWidth, double availableHeight) { + // Calculate gem size based on available space and grid dimensions + final gemSizeByWidth = (availableWidth - (2 * gridPadding)) / gridWidth; + final gemSizeByHeight = (availableHeight - (2 * gridPadding)) / gridHeight; + + // Use the smaller dimension to ensure the grid fits in both directions + final calculatedSize = math.min(gemSizeByWidth, gemSizeByHeight); + + // Ensure minimum size for playability and maximum size for performance + return math.max(32.0, math.min(calculatedSize, 80.0)); + } } diff --git a/test/game_test.dart b/test/game_test.dart index ae2813e..35597d3 100644 --- a/test/game_test.dart +++ b/test/game_test.dart @@ -6,7 +6,7 @@ import 'package:match_three/game/systems/match_detector.dart'; void main() { group('Match Three Game Tests', () { test('Grid initialization creates 8x8 grid', () { - final grid = GameGrid(); + final grid = GameGrid(width: 8, height: 8); for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { @@ -15,8 +15,22 @@ void main() { } }); + test('Grid initialization creates custom size grid', () { + final grid = GameGrid(width: 5, height: 5); + + for (int row = 0; row < 5; row++) { + for (int col = 0; col < 5; col++) { + expect(grid.getGem(row, col), isNotNull); + } + } + + // Test that positions outside the grid return null + expect(grid.getGem(5, 0), isNull); + expect(grid.getGem(0, 5), isNull); + }); + test('Match detector validates adjacent positions', () { - final grid = GameGrid(); + final grid = GameGrid(width: 8, height: 8); // Test adjacent positions (should be valid) expect(MatchDetector.isValidSwap(grid, 0, 0, 0, 1), isTrue);