From 2c5aec71cbd67756deb2fd370eeac74733dc49af Mon Sep 17 00:00:00 2001 From: alexvasl Date: Mon, 5 Aug 2024 11:04:54 +0300 Subject: [PATCH] Changes --- assets/images/{crystal0.png => crystal7.png} | Bin lib/flame_components/sprites.dart | 14 -- lib/game/board.dart | 226 +++++++++++++++++++ lib/game/match_magic_game.dart | 122 ++-------- lib/game/sprite_loader.dart | 11 + lib/game/swap_notifier.dart | 26 +++ lib/game/tile.dart | 36 +++ lib/main.dart | 41 +++- lib/models/game_state.dart | 163 ------------- lib/screen/game_screen.dart | 22 -- pubspec.yaml | 2 +- 11 files changed, 350 insertions(+), 313 deletions(-) rename assets/images/{crystal0.png => crystal7.png} (100%) delete mode 100644 lib/flame_components/sprites.dart create mode 100644 lib/game/board.dart create mode 100644 lib/game/sprite_loader.dart create mode 100644 lib/game/swap_notifier.dart create mode 100644 lib/game/tile.dart delete mode 100644 lib/models/game_state.dart delete mode 100644 lib/screen/game_screen.dart diff --git a/assets/images/crystal0.png b/assets/images/crystal7.png similarity index 100% rename from assets/images/crystal0.png rename to assets/images/crystal7.png diff --git a/lib/flame_components/sprites.dart b/lib/flame_components/sprites.dart deleted file mode 100644 index 6d0c9e9..0000000 --- a/lib/flame_components/sprites.dart +++ /dev/null @@ -1,14 +0,0 @@ -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/board.dart b/lib/game/board.dart new file mode 100644 index 0000000..34fb86c --- /dev/null +++ b/lib/game/board.dart @@ -0,0 +1,226 @@ +import 'dart:math'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:provider/provider.dart'; +import 'tile.dart'; +import 'swap_notifier.dart'; +import 'package:flame/sprite.dart'; + +class Board extends FlameGame { + final List sprites; + static const int rows = 8; + static const int cols = 8; + late double tileSize; + List> tiles = []; + int? selectedRow; + int? selectedCol; + + Board({required this.sprites}); + + @override + Future onLoad() async { + super.onLoad(); + + tileSize = size.x / cols; + _initializeGrid(); + _removeInitialMatches(); + } + + void _initializeGrid() { + for (int row = 0; row < rows; row++) { + List rowTiles = []; + for (int col = 0; col < cols; col++) { + int spriteIndex = _randomElement(); + var tile = Tile( + sprite: sprites[spriteIndex], + spriteIndex: spriteIndex, + size: Vector2.all(tileSize), + position: Vector2(col * tileSize, row * tileSize), + row: row, + col: col, + onTileTap: handleTileTap, + ); + rowTiles.add(tile); + add(tile); + } + tiles.add(rowTiles); + } + } + + int _randomElement() { + return Random().nextInt(7); + } + + void _removeInitialMatches() { + bool hasMatches; + do { + hasMatches = false; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + if (_hasMatch(row, col)) { + int spriteIndex = _randomElement(); + tiles[row][col]!.sprite = sprites[spriteIndex]; + tiles[row][col]!.spriteIndex = spriteIndex; + hasMatches = true; + } + } + } + } while (hasMatches); + } + + void handleTileTap(Tile tappedTile) { + int row = tappedTile.row; + int col = tappedTile.col; + + if (selectedRow == null || selectedCol == null) { + selectedRow = row; + selectedCol = col; + } else { + if (_isAdjacent(selectedRow!, selectedCol!, row, col)) { + swapTiles(tiles[selectedRow!][selectedCol!]!, tiles[row][col]!); + Future.delayed(const Duration(milliseconds: 300), () { + if (!_checkMatches()) { + swapTiles(tiles[row][col]!, tiles[selectedRow!][selectedCol!]!); + } + selectedRow = null; + selectedCol = null; + }); + } else { + selectedRow = row; + selectedCol = col; + } + } + } + + bool _isAdjacent(int row1, int col1, int row2, int col2) { + return (row1 == row2 && (col1 - col2).abs() == 1) || + (col1 == col2 && (row1 - row2).abs() == 1); + } + + void swapTiles(Tile tile1, Tile tile2) { + final tempPosition = tile1.position; + final tempRow = tile1.row; + final tempCol = tile1.col; + + tile1.position = tile2.position; + tile1.row = tile2.row; + tile1.col = tile2.col; + + tile2.position = tempPosition; + tile2.row = tempRow; + tile2.col = tempCol; + + tiles[tile1.row][tile1.col] = tile1; + tiles[tile2.row][tile2.col] = tile2; + + tile1.animateMoveTo(tile1.position, () {}); + tile2.animateMoveTo(tile2.position, () {}); + + _checkMatches(); + } + + bool _checkMatches() { + final matches = >[]; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + if (_hasMatch(row, col)) { + matches.add([row, col]); + } + } + } + + if (matches.isNotEmpty) { + for (final match in matches) { + _removeMatchedElements(match[0], match[1]); + } + _applyGravity(); + Future.delayed(const Duration(milliseconds: 300), () { + _fillEmptySpaces(); + _checkMatches(); + }); + return true; + } + return false; + } + + bool _hasMatch(int row, int col) { + final value = tiles[row][col]!.spriteIndex; + + int count = 1; + for (int i = col + 1; i < cols && tiles[row][i]!.spriteIndex == value; i++) + count++; + for (int i = col - 1; i >= 0 && tiles[row][i]!.spriteIndex == value; i--) + count++; + if (count >= 3) return true; + + count = 1; + for (int i = row + 1; i < rows && tiles[i][col]!.spriteIndex == value; i++) + count++; + for (int i = row - 1; i >= 0 && tiles[i][col]!.spriteIndex == value; i--) + count++; + return count >= 3; + } + + void _removeMatchedElements(int row, int col) { + final value = tiles[row][col]!.spriteIndex; + + int left = col; + while (left > 0 && tiles[row][left - 1]!.spriteIndex == value) left--; + int right = col; + while (right < cols - 1 && tiles[row][right + 1]!.spriteIndex == value) + right++; + if (right - left + 1 >= 3) { + for (int i = left; i <= right; i++) { + tiles[row][i] = null; + } + } + + int top = row; + while (top > 0 && tiles[top - 1][col]!.spriteIndex == value) top--; + int bottom = row; + while (bottom < rows - 1 && tiles[bottom + 1][col]!.spriteIndex == value) + bottom++; + if (bottom - top + 1 >= 3) { + for (int i = top; i <= bottom; i++) { + tiles[i][col] = null; + } + } + } + + void _applyGravity() { + for (int col = 0; col < cols; col++) { + int emptyRow = rows - 1; + for (int row = rows - 1; row >= 0; row--) { + if (tiles[row][col] != null) { + if (row != emptyRow) { + tiles[emptyRow][col] = tiles[row][col]; + tiles[emptyRow][col]!.row = emptyRow; + tiles[row][col] = null; + } + emptyRow--; + } + } + } + } + + void _fillEmptySpaces() { + for (int col = 0; col < cols; col++) { + for (int row = rows - 1; row >= 0; row--) { + if (tiles[row][col] == null) { + int spriteIndex = _randomElement(); + var tile = Tile( + sprite: sprites[spriteIndex], + spriteIndex: spriteIndex, + size: Vector2.all(tileSize), + position: Vector2(col * tileSize, row * tileSize), + row: row, + col: col, + onTileTap: handleTileTap, + ); + tiles[row][col] = tile; + add(tile); + } + } + } + } +} diff --git a/lib/game/match_magic_game.dart b/lib/game/match_magic_game.dart index 6cba590..a964713 100644 --- a/lib/game/match_magic_game.dart +++ b/lib/game/match_magic_game.dart @@ -1,117 +1,23 @@ 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'; +import 'package:provider/provider.dart'; +import 'sprite_loader.dart'; +import 'board.dart'; +import 'swap_notifier.dart'; -class MatchMagicGame extends FlameGame with TapDetector { - final GameState gameState; +class MatchMagicGame extends StatelessWidget { + final List sprites; - 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); + const MatchMagicGame({required this.sprites, Key? key}) : super(key: key); @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; - } - } + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => SwapNotifier(), + child: GameWidget( + game: Board(sprites: sprites), + ), + ); } } diff --git a/lib/game/sprite_loader.dart b/lib/game/sprite_loader.dart new file mode 100644 index 0000000..7907be9 --- /dev/null +++ b/lib/game/sprite_loader.dart @@ -0,0 +1,11 @@ +import 'package:flame/components.dart'; + +class SpriteLoader { + static Future> loadSprites() async { + List sprites = []; + for (int i = 1; i <= 7; i++) { + sprites.add(await Sprite.load('crystal$i.png')); + } + return sprites; + } +} diff --git a/lib/game/swap_notifier.dart b/lib/game/swap_notifier.dart new file mode 100644 index 0000000..20ca3c9 --- /dev/null +++ b/lib/game/swap_notifier.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'board.dart'; +import 'tile.dart'; + +class SwapNotifier extends ChangeNotifier { + Tile? selectedTile; + + void selectTile(Tile tile, Board board) { + if (selectedTile == null) { + selectedTile = tile; + } else { + if (_isNeighbor(selectedTile!, tile)) { + board.swapTiles(selectedTile!, tile); + selectedTile = null; + } else { + selectedTile = tile; + } + } + notifyListeners(); + } + + bool _isNeighbor(Tile tile1, Tile tile2) { + return (tile1.row == tile2.row && (tile1.col - tile2.col).abs() == 1) || + (tile1.col == tile2.col && (tile1.row - tile2.row).abs() == 1); + } +} diff --git a/lib/game/tile.dart b/lib/game/tile.dart new file mode 100644 index 0000000..d070d03 --- /dev/null +++ b/lib/game/tile.dart @@ -0,0 +1,36 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class Tile extends SpriteComponent with TapCallbacks { + int row; + int col; + int spriteIndex; + final void Function(Tile) onTileTap; + + Tile({ + required Sprite sprite, + required this.spriteIndex, + required Vector2 size, + required Vector2 position, + required this.row, + required this.col, + required this.onTileTap, + }) : super(sprite: sprite, size: size, position: position); + + @override + bool onTapDown(TapDownEvent event) { + onTileTap(this); + return true; + } + + void animateMoveTo(Vector2 newPosition, VoidCallback onComplete) { + add(MoveEffect.to( + newPosition, + EffectController(duration: 0.5), + onComplete: onComplete, + )); + } +} diff --git a/lib/main.dart b/lib/main.dart index 5364453..b42f2c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,16 @@ +import 'package:flame/sprite.dart'; import 'package:flutter/material.dart'; +import 'package:match_magic/game/sprite_loader.dart'; import 'package:provider/provider.dart'; -import 'package:match_magic/screen/game_screen.dart'; -import 'package:match_magic/models/game_state.dart'; +import 'game/match_magic_game.dart'; +import 'game/swap_notifier.dart'; void main() { runApp( - ChangeNotifierProvider( - create: (context) => GameState(), + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => SwapNotifier()), + ], child: const MyApp(), ), ); @@ -22,7 +26,34 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: const GameScreen(), + home: const Scaffold( + body: MatchMagicGameScreen(), + ), + ); + } +} + +class MatchMagicGameScreen extends StatelessWidget { + const MatchMagicGameScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: SpriteLoader.loadSprites(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data == null) { + return const Center(child: Text('No sprites found')); + } else { + final sprites = snapshot.data!; + return MatchMagicGame(sprites: sprites); + } + } else { + return const Center(child: CircularProgressIndicator()); + } + }, ); } } diff --git a/lib/models/game_state.dart b/lib/models/game_state.dart deleted file mode 100644 index 094a878..0000000 --- a/lib/models/game_state.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'dart:math'; -import 'package:flutter/material.dart'; - -class GameState extends ChangeNotifier { - final int rows = 8; - final int cols = 8; - late List> grid; - int? selectedRow; - int? selectedCol; - - GameState() { - _initializeGrid(); - } - - void _initializeGrid() { - grid = List.generate( - rows, (i) => List.generate(cols, (j) => _randomElement())); - _removeInitialMatches(); - } - - int _randomElement() { - return Random().nextInt(6) + 1; - } - - void _removeInitialMatches() { - bool hasMatches; - do { - hasMatches = false; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (_hasMatch(i, j)) { - grid[i][j] = _randomElement(); - hasMatches = true; - } - } - } - } while (hasMatches); - } - - void selectElement(int row, int col) { - if (selectedRow == null || selectedCol == null) { - selectedRow = row; - selectedCol = col; - } else { - if (_isAdjacent(selectedRow!, selectedCol!, row, col)) { - swapElements(selectedRow!, selectedCol!, row, col); - Future.delayed(const Duration(milliseconds: 300), () { - if (!_checkMatches()) { - swapElements(row, col, selectedRow!, selectedCol!); - } - selectedRow = null; - selectedCol = null; - notifyListeners(); - }); - } else { - selectedRow = row; - selectedCol = col; - } - } - notifyListeners(); - } - - bool _isAdjacent(int row1, int col1, int row2, int col2) { - return (row1 == row2 && (col1 - col2).abs() == 1) || - (col1 == col2 && (row1 - row2).abs() == 1); - } - - void swapElements(int row1, int col1, int row2, int col2) { - final temp = grid[row1][col1]; - grid[row1][col1] = grid[row2][col2]; - grid[row2][col2] = temp; - notifyListeners(); - } - - bool _checkMatches() { - final matches = >[]; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (_hasMatch(i, j)) { - matches.add([i, j]); - } - } - } - - if (matches.isNotEmpty) { - for (final match in matches) { - _removeMatchedElements(match[0], match[1]); - } - _applyGravity(); - Future.delayed(const Duration(milliseconds: 300), () { - _fillEmptySpaces(); - _checkMatches(); - }); - return true; - } - return false; - } - - bool _hasMatch(int row, int col) { - final value = grid[row][col]; - if (value == 0) return false; - - 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++; - if (count >= 3) return true; - - 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++; - return count >= 3; - } - - void _removeMatchedElements(int row, int col) { - final value = grid[row][col]; - - int left = col; - while (left > 0 && grid[row][left - 1] == value) left--; - int right = col; - 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; - } - } - - int top = row; - while (top > 0 && grid[top - 1][col] == value) top--; - int bottom = row; - 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; - } - } - } - - void _applyGravity() { - for (int col = 0; col < cols; col++) { - int emptyRow = rows - 1; - for (int row = rows - 1; row >= 0; row--) { - if (grid[row][col] != 0) { - if (row != emptyRow) { - grid[emptyRow][col] = grid[row][col]; - grid[row][col] = 0; - } - emptyRow--; - } - } - } - } - - void _fillEmptySpaces() { - for (int col = 0; col < cols; col++) { - for (int row = rows - 1; row >= 0; row--) { - if (grid[row][col] == 0) { - grid[row][col] = _randomElement(); - } - } - } - notifyListeners(); - } -} diff --git a/lib/screen/game_screen.dart b/lib/screen/game_screen.dart deleted file mode 100644 index 21e8d25..0000000 --- a/lib/screen/game_screen.dart +++ /dev/null @@ -1,22 +0,0 @@ -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 { - const GameScreen({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Consumer( - builder: (context, gameState, child) { - return GameWidget( - game: MatchMagicGame(gameState), - ); - }, - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 965ba1e..8c02ca0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ flutter: - assets/images/crystal4.png - assets/images/crystal5.png - assets/images/crystal6.png - - assets/images/crystal0.png + - assets/images/crystal7.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware