diff --git a/assets/images/diamond7.png b/assets/images/crystal0.png similarity index 100% rename from assets/images/diamond7.png rename to assets/images/crystal0.png diff --git a/assets/images/diamond1.png b/assets/images/crystal1.png similarity index 100% rename from assets/images/diamond1.png rename to assets/images/crystal1.png diff --git a/assets/images/diamond2.png b/assets/images/crystal2.png similarity index 100% rename from assets/images/diamond2.png rename to assets/images/crystal2.png diff --git a/assets/images/diamond3.png b/assets/images/crystal3.png similarity index 100% rename from assets/images/diamond3.png rename to assets/images/crystal3.png diff --git a/assets/images/diamond4.png b/assets/images/crystal4.png similarity index 100% rename from assets/images/diamond4.png rename to assets/images/crystal4.png diff --git a/assets/images/diamond5.png b/assets/images/crystal5.png similarity index 100% rename from assets/images/diamond5.png rename to assets/images/crystal5.png diff --git a/assets/images/diamond6.png b/assets/images/crystal6.png similarity index 100% rename from assets/images/diamond6.png rename to assets/images/crystal6.png diff --git a/lib/flame_components/sprites.dart b/lib/flame_components/sprites.dart new file mode 100644 index 0000000..6d0c9e9 --- /dev/null +++ b/lib/flame_components/sprites.dart @@ -0,0 +1,14 @@ +import 'package:flame/flame.dart'; + +Future loadImages() async { + final imageNames = [ + 'crystal1.png', + 'crystal2.png', + 'crystal3.png', + 'crystal4.png', + 'crystal5.png', + 'crystal6.png', + 'crystal7.png', + ]; + await Future.wait(imageNames.map((name) => Flame.images.load(name))); +} diff --git a/lib/game/match_magic_game.dart b/lib/game/match_magic_game.dart new file mode 100644 index 0000000..6cba590 --- /dev/null +++ b/lib/game/match_magic_game.dart @@ -0,0 +1,117 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/game.dart'; +import 'package:match_magic/models/game_state.dart'; +import 'package:flutter/material.dart'; +import 'dart:ui'; + +class MatchMagicGame extends FlameGame with TapDetector { + final GameState gameState; + + MatchMagicGame(this.gameState); + + late double tileSize; + late double gridWidth; + late double gridHeight; + late double gridOffsetX; + late double gridOffsetY; + final Map sprites = {}; + late SpriteComponent selectedTile; + bool hasSelectedTile = false; + final selectedPaint = Paint()..color = Colors.white.withOpacity(0.5); + + @override + Future onLoad() async { + await super.onLoad(); + await loadImages(); + loadGrid(); + } + + Future loadImages() async { + final imageNames = [ + 'crystal1.png', + 'crystal2.png', + 'crystal3.png', + 'crystal4.png', + 'crystal5.png', + 'crystal6.png', + 'crystal0.png', + ]; + for (var name in imageNames) { + final sprite = Sprite(await Flame.images.load(name)); + sprites[name] = sprite; + } + } + + void loadGrid() { + tileSize = size.x / gameState.cols; + gridWidth = tileSize * gameState.cols; + gridHeight = tileSize * gameState.rows; + + gridOffsetX = (size.x - gridWidth) / 2; + gridOffsetY = (size.y - gridHeight) / 2; + + addGrid(); + } + + void addGrid() { + children + .whereType() + .forEach((component) => component.removeFromParent()); + + for (var row = 0; row < gameState.rows; row++) { + for (var col = 0; col < gameState.cols; col++) { + final value = gameState.grid[row][col]; + final spriteName = 'crystal$value.png'; + final sprite = sprites[spriteName]; + final spriteComponent = SpriteComponent() + ..sprite = sprite + ..width = tileSize + ..height = tileSize + ..x = gridOffsetX + col * tileSize + ..y = gridOffsetY + row * tileSize; + add(spriteComponent); + } + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + if (hasSelectedTile) { + canvas.drawRect( + Rect.fromLTWH( + selectedTile.x, + selectedTile.y, + tileSize, + tileSize, + ), + selectedPaint, + ); + } + } + + @override + void onTapDown(TapDownInfo info) { + final x = info.eventPosition.global.x; + final y = info.eventPosition.global.y; + final col = ((x - gridOffsetX) / tileSize).floor(); + final row = ((y - gridOffsetY) / tileSize).floor(); + + if (col >= 0 && col < gameState.cols && row >= 0 && row < gameState.rows) { + if (hasSelectedTile) { + gameState.selectElement(row, col); + hasSelectedTile = false; + addGrid(); + } else { + selectedTile = SpriteComponent() + ..x = gridOffsetX + col * tileSize + ..y = gridOffsetY + row * tileSize + ..width = tileSize + ..height = tileSize; + hasSelectedTile = true; + } + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 8ff641f..5364453 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:match_magic/screen/game_screen.dart'; import 'package:match_magic/models/game_state.dart'; -import 'package:provider/provider.dart'; void main() { runApp( diff --git a/lib/models/game_state.dart b/lib/models/game_state.dart index 662ab77..094a878 100644 --- a/lib/models/game_state.dart +++ b/lib/models/game_state.dart @@ -1,5 +1,4 @@ import 'dart:math'; - import 'package:flutter/material.dart'; class GameState extends ChangeNotifier { @@ -8,7 +7,6 @@ class GameState extends ChangeNotifier { late List> grid; int? selectedRow; int? selectedCol; - bool isCheckingMatches = false; GameState() { _initializeGrid(); @@ -21,7 +19,7 @@ class GameState extends ChangeNotifier { } int _randomElement() { - return Random().nextInt(7) + 1; + return Random().nextInt(6) + 1; } void _removeInitialMatches() { @@ -48,7 +46,6 @@ class GameState extends ChangeNotifier { swapElements(selectedRow!, selectedCol!, row, col); Future.delayed(const Duration(milliseconds: 300), () { if (!_checkMatches()) { - // Swap back if no match swapElements(row, col, selectedRow!, selectedCol!); } selectedRow = null; @@ -92,7 +89,7 @@ class GameState extends ChangeNotifier { _applyGravity(); Future.delayed(const Duration(milliseconds: 300), () { _fillEmptySpaces(); - _checkMatches(); // Check again for new matches + _checkMatches(); }); return true; } @@ -103,54 +100,34 @@ class GameState extends ChangeNotifier { final value = grid[row][col]; if (value == 0) return false; - // Check horizontal match int count = 1; - for (int i = col + 1; i < cols && grid[row][i] == value; i++) { - count++; - } - for (int i = col - 1; i >= 0 && grid[row][i] == value; i--) { - count++; - } + for (int i = col + 1; i < cols && grid[row][i] == value; i++) count++; + for (int i = col - 1; i >= 0 && grid[row][i] == value; i--) count++; if (count >= 3) return true; - // Check vertical match count = 1; - for (int i = row + 1; i < rows && grid[i][col] == value; i++) { - count++; - } - for (int i = row - 1; i >= 0 && grid[i][col] == value; i--) { - count++; - } + for (int i = row + 1; i < rows && grid[i][col] == value; i++) count++; + for (int i = row - 1; i >= 0 && grid[i][col] == value; i--) count++; return count >= 3; } void _removeMatchedElements(int row, int col) { final value = grid[row][col]; - // Remove horizontal matches int left = col; - while (left > 0 && grid[row][left - 1] == value) { - left--; - } + while (left > 0 && grid[row][left - 1] == value) left--; int right = col; - while (right < cols - 1 && grid[row][right + 1] == value) { - right++; - } + while (right < cols - 1 && grid[row][right + 1] == value) right++; if (right - left + 1 >= 3) { for (int i = left; i <= right; i++) { grid[row][i] = 0; } } - // Remove vertical matches int top = row; - while (top > 0 && grid[top - 1][col] == value) { - top--; - } + while (top > 0 && grid[top - 1][col] == value) top--; int bottom = row; - while (bottom < rows - 1 && grid[bottom + 1][col] == value) { - bottom++; - } + while (bottom < rows - 1 && grid[bottom + 1][col] == value) bottom++; if (bottom - top + 1 >= 3) { for (int i = top; i <= bottom; i++) { grid[i][col] = 0; @@ -178,9 +155,9 @@ class GameState extends ChangeNotifier { for (int row = rows - 1; row >= 0; row--) { if (grid[row][col] == 0) { grid[row][col] = _randomElement(); - notifyListeners(); } } } + notifyListeners(); } } diff --git a/lib/screen/game_screen.dart b/lib/screen/game_screen.dart index 59cac93..21e8d25 100644 --- a/lib/screen/game_screen.dart +++ b/lib/screen/game_screen.dart @@ -1,5 +1,7 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:match_magic/models/game_state.dart'; +import 'package:match_magic/game/match_magic_game.dart'; import 'package:provider/provider.dart'; class GameScreen extends StatelessWidget { @@ -7,66 +9,11 @@ class GameScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final gameState = context.watch(); - return Scaffold( - appBar: AppBar( - title: const Text('Match Magic'), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final tileSize = constraints.maxWidth / gameState.cols; - - return Center( - child: Container( - width: constraints.maxWidth, - height: constraints.maxWidth, - child: Stack( - children: [ - for (int row = 0; row < gameState.rows; row++) - for (int col = 0; col < gameState.cols; col++) - AnimatedPositioned( - key: ValueKey('${row}_$col${gameState.grid[row][col]}'), - duration: const Duration(milliseconds: 300), - left: col * tileSize, - top: row * tileSize, - child: GestureDetector( - onTap: () { - gameState.selectElement(row, col); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: tileSize - 4, - height: tileSize - 4, - margin: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - border: gameState.selectedRow == row && - gameState.selectedCol == col - ? Border.all(color: Colors.red, width: 2.0) - : null, - color: Colors.blue[100], - ), - child: Center( - child: AnimatedOpacity( - opacity: - gameState.grid[row][col] != 0 ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: gameState.grid[row][col] != 0 - ? Image.asset( - 'assets/images/diamond${gameState.grid[row][col]}.png', - fit: BoxFit.cover, - width: tileSize - 4, - height: tileSize - 4, - ) - : const SizedBox.shrink(), - ), - ), - ), - ), - ), - ], - ), - ), + body: Consumer( + builder: (context, gameState, child) { + return GameWidget( + game: MatchMagicGame(gameState), ); }, ), diff --git a/pubspec.lock b/pubspec.lock index 5febb45..80c8014 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + flame: + dependency: "direct main" + description: + name: flame + sha256: "79133dc46a3ff870950f41d0dc1598414e7bd7ae2c29bd9f0a9de208d9a70cb7" + url: "https://pub.dev" + source: hosted + version: "1.18.0" flutter: dependency: "direct main" description: flutter @@ -139,6 +147,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: "1bfaaaee0419e43ecc9eaebd410eb4bd5039657b72011de75ff3e2915c9aac60" + url: "https://pub.dev" + source: hosted + version: "5.0.3" path: dependency: transitive description: @@ -226,4 +242,4 @@ packages: version: "14.2.1" sdks: dart: ">=3.4.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 828a75e..965ba1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 + flame: ^1.18.0 dev_dependencies: flutter_test: @@ -61,13 +62,13 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/diamond1.png - - assets/images/diamond2.png - - assets/images/diamond3.png - - assets/images/diamond4.png - - assets/images/diamond5.png - - assets/images/diamond6.png - - assets/images/diamond7.png + - assets/images/crystal1.png + - assets/images/crystal2.png + - assets/images/crystal3.png + - assets/images/crystal4.png + - assets/images/crystal5.png + - assets/images/crystal6.png + - assets/images/crystal0.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware