From 34d7687dc05630a9d01ddbcc3d06f9e072142417 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Wed, 4 Sep 2024 15:11:40 +0300 Subject: [PATCH] Hints have been added to the game. The main menu page has been added. Added new game animation. --- lib/game/board.dart | 428 +++++++++++++++++++-------------- lib/game/match_magic_game.dart | 217 ++++++++++++----- lib/game/swap_notifier.dart | 8 + lib/game/tile.dart | 19 +- lib/main.dart | 12 +- lib/screens/main_menu.dart | 70 ++++++ 6 files changed, 511 insertions(+), 243 deletions(-) create mode 100644 lib/screens/main_menu.dart diff --git a/lib/game/board.dart b/lib/game/board.dart index 2237926..56c84e1 100644 --- a/lib/game/board.dart +++ b/lib/game/board.dart @@ -20,6 +20,8 @@ class Board extends FlameGame { Tile? lastMovedTile; Tile? lastMovedByGravity; + bool isFirstLaunch = true; + Board({required this.sprites, required this.swapNotifier}); @override @@ -34,32 +36,53 @@ class Board extends FlameGame { selectedCol = null; animating = false; tileSize = size.x / cols; - _initializeGrid(); + _initializeGrid(isFirstLaunch); _removeInitialMatches(); + isFirstLaunch = false; } void restartGame() { + isFirstLaunch = true; _resetGame(); swapNotifier.resetScore(); } - void _initializeGrid() { + void _initializeGrid(bool animate) { for (int row = 0; row < rows; row++) { List rowTiles = []; for (int col = 0; col < cols; col++) { int spriteIndex = _randomElement(); + + var initialPosition = animate + ? Vector2(col * tileSize, -tileSize * rows) + : Vector2(col * tileSize, row * tileSize); + var tile = Tile( sprite: sprites[spriteIndex], spriteIndex: spriteIndex, size: Vector2.all(tileSize), - position: Vector2(col * tileSize, row * tileSize), + position: initialPosition, row: row, col: col, - // onTileTap: handleTileTap, onSwipe: handleTileSwipe, ); rowTiles.add(tile); add(tile); + + if (animate) { + double delay = 0.04 * ((rows - 1 - row) * cols + col); + + tile.add( + MoveEffect.to( + Vector2(col * tileSize, row * tileSize), + EffectController( + duration: 0.5, + startDelay: delay, + curve: Curves.bounceOut, + ), + ), + ); + } } tiles.add(rowTiles); } @@ -86,38 +109,6 @@ class Board extends FlameGame { } while (hasMatches); } - // void handleTileTap(Tile tappedTile) { - // if (animating) return; - - // int row = tappedTile.row; - // int col = tappedTile.col; - - // if (selectedRow == null || selectedCol == null) { - // tappedTile.select(); - // selectedRow = row; - // selectedCol = col; - // } else { - // tiles[selectedRow!][selectedCol!]?.deselect(); - // if (_isAdjacent(selectedRow!, selectedCol!, row, col)) { - // lastMovedTile = tiles[selectedRow!][selectedCol!]; - // swapTiles(tiles[selectedRow!][selectedCol!]!, tiles[row][col]!, true); - // Future.delayed(const Duration(milliseconds: 300), () { - // if (!checkMatches()) { - // swapTiles( - // tiles[row][col]!, tiles[selectedRow!][selectedCol!]!, true); - // } - // selectedRow = null; - // selectedCol = null; - // }); - // } else { - // tiles[selectedRow!][selectedCol!]?.deselect(); - // tappedTile.select(); - // selectedRow = row; - // selectedCol = col; - // } - // } - // } - Future handleTileSwipe(Tile tile, Vector2 delta) async { if (animating) return; @@ -148,6 +139,8 @@ class Board extends FlameGame { if (!checkMatches()) { swapTiles(tile, targetTile, true); + } else { + swapNotifier.incrementMoveCount(); } selectedRow = null; selectedCol = null; @@ -186,7 +179,17 @@ class Board extends FlameGame { tile1.deselect(); } - bool checkMatches() { + bool checkMatches({bool simulate = false}) { + if (simulate) { + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + if (_hasMatch(row, col)) { + return true; + } + } + } + return false; + } animating = true; final matches = >[]; @@ -239,85 +242,6 @@ class Board extends FlameGame { return count >= 3; } - // int _removeMatchedElements(int row, int col) { - // int score = 0; - // final int? value = tiles[row][col]?.spriteIndex; - // bool bombTriggered = false; - // Tile? tileToTransformIntoBomb = null; - - // 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) { - // score += _calculateScore(right - left + 1); - - // if (right - left + 1 >= 4 && - // lastMovedTile != null && - // lastMovedTile!.row == row && - // lastMovedTile!.col >= left && - // lastMovedTile!.col <= right) { - // tileToTransformIntoBomb = lastMovedTile; - // } - - // for (int i = left; i <= right; i++) { - // if (tiles[row][i] != null) { - // if (tiles[row][i]!.isBomb) { - // bombTriggered = true; - // _triggerBomb(row, i); - // } - // if (tiles[row][i] != tileToTransformIntoBomb) { - // _animateRemoveTile(tiles[row][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) { - // score += _calculateScore(bottom - top + 1); - - // if (bottom - top + 1 >= 4 && - // lastMovedTile != null && - // lastMovedTile!.col == col && - // lastMovedTile!.row >= top && - // lastMovedTile!.row <= bottom) { - // tileToTransformIntoBomb = lastMovedTile; - // } - - // for (int i = top; i <= bottom; i++) { - // if (tiles[i][col] != null) { - // if (tiles[i][col]!.isBomb) { - // bombTriggered = true; - // _triggerBomb(i, col); - // } - // if (tiles[i][col] != tileToTransformIntoBomb) { - // _animateRemoveTile(tiles[i][col]!); - // tiles[i][col] = null; - // } - // } - // } - // } - - // if (bombTriggered) { - // _triggerBomb(row, col); - // } - - // if (tileToTransformIntoBomb != null) { - // _createBomb(tileToTransformIntoBomb.row, tileToTransformIntoBomb.col); - // } - - // return score; - // } - int _removeMatchedElements(int row, int col) { int score = 0; final int? value = tiles[row][col]?.spriteIndex; @@ -511,45 +435,6 @@ class Board extends FlameGame { ); } - // void explodeBomb(Tile bombTile) { - // final bombPosition = bombTile.position; - // final bombRow = bombTile.row; - // final bombCol = bombTile.col; - - // for (int rowOffset = -1; rowOffset <= 1; rowOffset++) { - // for (int colOffset = -1; colOffset <= 1; colOffset++) { - // final row = bombRow + rowOffset; - // final col = bombCol + colOffset; - - // if (row >= 0 && row < rows && col >= 0 && col < cols) { - // final tile = tiles[row][col]; - // if (tile != null && tile != bombTile) { - // _animateRemoveTile(tile); - // tiles[row][col] = null; - // } - // } - // } - // } - - // bombTile.add(RemoveEffect( - // delay: 0.5, - // onComplete: () => remove(bombTile), - // )); - - // final explosion = CircleComponent( - // radius: tileSize / 2, - // paint: Paint()..color = Colors.orange.withOpacity(0.7), - // position: bombPosition, - // ); - // add(explosion); - - // explosion.add(ScaleEffect.to( - // Vector2.all(2), - // EffectController(duration: 0.5), - // onComplete: () => explosion.removeFromParent(), - // )); - // } - void explodeBomb(Tile bombTile) { final bombPosition = bombTile.position.clone(); final bombRow = bombTile.row; @@ -568,30 +453,10 @@ class Board extends FlameGame { } } - // Анимация взрыва бомбы _animateBombExplosion(bombPosition); tiles[bombRow][bombCol] = null; } - // void _applyGravity() { - // for (int col = 0; col < cols; col++) { - // for (int row = rows - 1; row >= 0; row--) { - // if (tiles[row][col] == null) { - // for (int k = row - 1; k >= 0; k--) { - // if (tiles[k][col] != null) { - // tiles[row][col] = tiles[k][col]!; - // tiles[k][col] = null; - // tiles[row][col]!.row = row; - // tiles[row][col]!.animateMoveTo( - // Vector2(col * tileSize, row * tileSize), () {}); - // break; - // } - // } - // } - // } - // } - // } - void _applyGravity() { for (int col = 0; col < cols; col++) { for (int row = rows - 1; row >= 0; row--) { @@ -634,4 +499,217 @@ class Board extends FlameGame { } } } + + Tile? findHint() { + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + Tile? tile = tiles[row][col]; + + if (col < cols - 1 && _canSwap(row, col, row, col + 1)) { + return tile; + } + if (row < rows - 1 && _canSwap(row, col, row + 1, col)) { + return tile; + } + } + } + return null; + } + + bool _canSwap(int row1, int col1, int row2, int col2) { + Tile tempTile1 = tiles[row1][col1]!; + Tile tempTile2 = tiles[row2][col2]!; + + tiles[row1][col1] = tempTile2; + tiles[row2][col2] = tempTile1; + + bool matchFound = checkMatches(simulate: true); + + tiles[row1][col1] = tempTile1; + tiles[row2][col2] = tempTile2; + + return matchFound; + } + + void showHint() { + Tile? hintTile = findHint(); + if (hintTile != null) { + hintTile.select(); + } + } } + + + + // void handleTileTap(Tile tappedTile) { + // if (animating) return; + + // int row = tappedTile.row; + // int col = tappedTile.col; + + // if (selectedRow == null || selectedCol == null) { + // tappedTile.select(); + // selectedRow = row; + // selectedCol = col; + // } else { + // tiles[selectedRow!][selectedCol!]?.deselect(); + // if (_isAdjacent(selectedRow!, selectedCol!, row, col)) { + // lastMovedTile = tiles[selectedRow!][selectedCol!]; + // swapTiles(tiles[selectedRow!][selectedCol!]!, tiles[row][col]!, true); + // Future.delayed(const Duration(milliseconds: 300), () { + // if (!checkMatches()) { + // swapTiles( + // tiles[row][col]!, tiles[selectedRow!][selectedCol!]!, true); + // } + // selectedRow = null; + // selectedCol = null; + // }); + // } else { + // tiles[selectedRow!][selectedCol!]?.deselect(); + // tappedTile.select(); + // selectedRow = row; + // selectedCol = col; + // } + // } + // } + + + // int _removeMatchedElements(int row, int col) { + // int score = 0; + // final int? value = tiles[row][col]?.spriteIndex; + // bool bombTriggered = false; + // Tile? tileToTransformIntoBomb = null; + + // 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) { + // score += _calculateScore(right - left + 1); + + // if (right - left + 1 >= 4 && + // lastMovedTile != null && + // lastMovedTile!.row == row && + // lastMovedTile!.col >= left && + // lastMovedTile!.col <= right) { + // tileToTransformIntoBomb = lastMovedTile; + // } + + // for (int i = left; i <= right; i++) { + // if (tiles[row][i] != null) { + // if (tiles[row][i]!.isBomb) { + // bombTriggered = true; + // _triggerBomb(row, i); + // } + // if (tiles[row][i] != tileToTransformIntoBomb) { + // _animateRemoveTile(tiles[row][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) { + // score += _calculateScore(bottom - top + 1); + + // if (bottom - top + 1 >= 4 && + // lastMovedTile != null && + // lastMovedTile!.col == col && + // lastMovedTile!.row >= top && + // lastMovedTile!.row <= bottom) { + // tileToTransformIntoBomb = lastMovedTile; + // } + + // for (int i = top; i <= bottom; i++) { + // if (tiles[i][col] != null) { + // if (tiles[i][col]!.isBomb) { + // bombTriggered = true; + // _triggerBomb(i, col); + // } + // if (tiles[i][col] != tileToTransformIntoBomb) { + // _animateRemoveTile(tiles[i][col]!); + // tiles[i][col] = null; + // } + // } + // } + // } + + // if (bombTriggered) { + // _triggerBomb(row, col); + // } + + // if (tileToTransformIntoBomb != null) { + // _createBomb(tileToTransformIntoBomb.row, tileToTransformIntoBomb.col); + // } + + // return score; + // } + + + + // void explodeBomb(Tile bombTile) { + // final bombPosition = bombTile.position; + // final bombRow = bombTile.row; + // final bombCol = bombTile.col; + + // for (int rowOffset = -1; rowOffset <= 1; rowOffset++) { + // for (int colOffset = -1; colOffset <= 1; colOffset++) { + // final row = bombRow + rowOffset; + // final col = bombCol + colOffset; + + // if (row >= 0 && row < rows && col >= 0 && col < cols) { + // final tile = tiles[row][col]; + // if (tile != null && tile != bombTile) { + // _animateRemoveTile(tile); + // tiles[row][col] = null; + // } + // } + // } + // } + + // bombTile.add(RemoveEffect( + // delay: 0.5, + // onComplete: () => remove(bombTile), + // )); + + // final explosion = CircleComponent( + // radius: tileSize / 2, + // paint: Paint()..color = Colors.orange.withOpacity(0.7), + // position: bombPosition, + // ); + // add(explosion); + + // explosion.add(ScaleEffect.to( + // Vector2.all(2), + // EffectController(duration: 0.5), + // onComplete: () => explosion.removeFromParent(), + // )); + // } + + + // void _applyGravity() { + // for (int col = 0; col < cols; col++) { + // for (int row = rows - 1; row >= 0; row--) { + // if (tiles[row][col] == null) { + // for (int k = row - 1; k >= 0; k--) { + // if (tiles[k][col] != null) { + // tiles[row][col] = tiles[k][col]!; + // tiles[k][col] = null; + // tiles[row][col]!.row = row; + // tiles[row][col]!.animateMoveTo( + // Vector2(col * tileSize, row * tileSize), () {}); + // break; + // } + // } + // } + // } + // } + // } \ No newline at end of file diff --git a/lib/game/match_magic_game.dart b/lib/game/match_magic_game.dart index bc626a3..8e8a96a 100644 --- a/lib/game/match_magic_game.dart +++ b/lib/game/match_magic_game.dart @@ -31,60 +31,133 @@ class _MatchMagicGameScreenState extends State { }); } + void _showSettingsDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Settings'), + content: const Text('The settings have not yet been implemented.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _showExitConfirmationDialog(); + }, + child: const Text('Exit to Main Menu'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close'), + ), + ], + ); + }, + ); + } + + void _showExitConfirmationDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Warning'), + content: const Text( + 'You will lose all game progress. Are you sure you want to exit?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).popUntil((route) => route.isFirst); + }, + child: const Text('Yes'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('No'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { - return FutureBuilder>( - future: _spritesFuture, - 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!; - board ??= Board( - sprites: sprites, swapNotifier: context.read()); - return Column( - children: [ - const SizedBox(height: 50), - const ScoreDisplay(), - Expanded( - child: Center( - child: AspectRatio( - aspectRatio: 1, - child: GameWidget(game: board!), + return Scaffold( + backgroundColor: Colors.black, + body: FutureBuilder>( + future: _spritesFuture, + 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!; + board ??= Board( + sprites: sprites, swapNotifier: context.read()); + + return Stack( + children: [ + Column( + children: [ + const SizedBox(height: 50), + const ScoreDisplay(), + Expanded( + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: GameWidget(game: board!), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _restartGame, + child: const Text( + 'Restart', + style: TextStyle(color: Colors.black), + ), + ), + const SizedBox(width: 20), + ElevatedButton( + onPressed: () { + board?.showHint(); + }, + child: const Text( + 'Hint', + style: TextStyle(color: Colors.black), + ), + ), + ], + ), + const SizedBox(height: 50), + ], + ), + Positioned( + top: 16, + left: 16, + child: IconButton( + icon: const Icon(Icons.settings, color: Colors.white), + onPressed: _showSettingsDialog, ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: _restartGame, - child: const Text( - 'Restart', - style: TextStyle(color: Colors.black), - ), - ), - const SizedBox(width: 20), - ElevatedButton( - onPressed: () {}, - child: const Text( - 'Hint', - style: TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox(height: 50), - ], - ); + ], + ); + } + } else { + return const Center(child: CircularProgressIndicator()); } - } else { - return const Center(child: CircularProgressIndicator()); - } - }, + }, + ), ); } } @@ -96,14 +169,44 @@ class ScoreDisplay extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), - child: Consumer( - builder: (context, notifier, child) { - return Text( - 'Score: ${notifier.score}', - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), - ); - }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Consumer( + builder: (context, notifier, child) { + return Card( + color: Colors.blueGrey[900], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Score: ${notifier.score}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + ), + ); + }, + ), + Consumer( + builder: (context, notifier, child) { + return Card( + color: Colors.blueGrey[900], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Moves: ${notifier.moveCount}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + ), + ); + }, + ), + ], ), ); } diff --git a/lib/game/swap_notifier.dart b/lib/game/swap_notifier.dart index 1b07727..0c81db0 100644 --- a/lib/game/swap_notifier.dart +++ b/lib/game/swap_notifier.dart @@ -4,11 +4,14 @@ import 'tile.dart'; class SwapNotifier extends ChangeNotifier { Tile? selectedTile; int _score = 0; + int _moveCount = 0; int get score => _score; + int get moveCount => _moveCount; void resetScore() { _score = 0; + _moveCount = 0; selectedTile = null; notifyListeners(); } @@ -18,6 +21,11 @@ class SwapNotifier extends ChangeNotifier { notifyListeners(); } + void incrementMoveCount() { + _moveCount += 1; + notifyListeners(); + } + void selectTile(Tile tile) { if (selectedTile == null) { selectedTile = tile; diff --git a/lib/game/tile.dart b/lib/game/tile.dart index 4a1bf81..337f5c2 100644 --- a/lib/game/tile.dart +++ b/lib/game/tile.dart @@ -61,9 +61,24 @@ class Tile extends SpriteComponent with TapCallbacks, DragCallbacks { return true; } + // void select() { + // isSelected = true; + // updateBorder(); + // } + void select() { - isSelected = true; - updateBorder(); + final effectController = EffectController( + duration: 0.3, + repeatCount: 2, + reverseDuration: 0.3, + ); + + final blinkEffect = OpacityEffect.to( + 0.0, + effectController, + ); + + add(blinkEffect); } void deselect() { diff --git a/lib/main.dart b/lib/main.dart index a3ec77c..ce89a71 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:match_magic/screens/main_menu.dart'; import 'package:provider/provider.dart'; import 'game/match_magic_game.dart'; import 'game/swap_notifier.dart'; @@ -21,15 +22,8 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Match Magic', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const Scaffold( - backgroundColor: Colors.black, - body: MatchMagicGameScreen(), - ), + return const MaterialApp( + home: MainMenu(), ); } } diff --git a/lib/screens/main_menu.dart b/lib/screens/main_menu.dart new file mode 100644 index 0000000..36bae61 --- /dev/null +++ b/lib/screens/main_menu.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:match_magic/game/match_magic_game.dart'; + +class MainMenu extends StatelessWidget { + const MainMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.settings, color: Colors.white), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Settings'), + content: + const Text('The settings have not yet been implemented.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Ok'), + ), + ], + ), + ); + }, + ), + backgroundColor: Colors.black, + ), + backgroundColor: Colors.black, + body: Center( + child: Column( + children: [ + const SizedBox( + height: 100, + ), + const Text( + 'Match Magic', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + const SizedBox( + height: 300, + ), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MatchMagicGameScreen()), + ); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 60.0, vertical: 16.0), + ), + child: const Text('Play', style: TextStyle(fontSize: 16)), + ), + ], + ), + ), + ); + } +}