Hints have been added to the game. The main menu page has been added. Added new game animation.

This commit is contained in:
Alex Vasilev 2024-09-04 15:11:40 +03:00
parent 8eb5c86496
commit 34d7687dc0
6 changed files with 511 additions and 243 deletions

View File

@ -20,6 +20,8 @@ class Board extends FlameGame {
Tile? lastMovedTile; Tile? lastMovedTile;
Tile? lastMovedByGravity; Tile? lastMovedByGravity;
bool isFirstLaunch = true;
Board({required this.sprites, required this.swapNotifier}); Board({required this.sprites, required this.swapNotifier});
@override @override
@ -34,32 +36,53 @@ class Board extends FlameGame {
selectedCol = null; selectedCol = null;
animating = false; animating = false;
tileSize = size.x / cols; tileSize = size.x / cols;
_initializeGrid(); _initializeGrid(isFirstLaunch);
_removeInitialMatches(); _removeInitialMatches();
isFirstLaunch = false;
} }
void restartGame() { void restartGame() {
isFirstLaunch = true;
_resetGame(); _resetGame();
swapNotifier.resetScore(); swapNotifier.resetScore();
} }
void _initializeGrid() { void _initializeGrid(bool animate) {
for (int row = 0; row < rows; row++) { for (int row = 0; row < rows; row++) {
List<Tile?> rowTiles = []; List<Tile?> rowTiles = [];
for (int col = 0; col < cols; col++) { for (int col = 0; col < cols; col++) {
int spriteIndex = _randomElement(); int spriteIndex = _randomElement();
var initialPosition = animate
? Vector2(col * tileSize, -tileSize * rows)
: Vector2(col * tileSize, row * tileSize);
var tile = Tile( var tile = Tile(
sprite: sprites[spriteIndex], sprite: sprites[spriteIndex],
spriteIndex: spriteIndex, spriteIndex: spriteIndex,
size: Vector2.all(tileSize), size: Vector2.all(tileSize),
position: Vector2(col * tileSize, row * tileSize), position: initialPosition,
row: row, row: row,
col: col, col: col,
// onTileTap: handleTileTap,
onSwipe: handleTileSwipe, onSwipe: handleTileSwipe,
); );
rowTiles.add(tile); rowTiles.add(tile);
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); tiles.add(rowTiles);
} }
@ -86,38 +109,6 @@ class Board extends FlameGame {
} while (hasMatches); } 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<void> handleTileSwipe(Tile tile, Vector2 delta) async { Future<void> handleTileSwipe(Tile tile, Vector2 delta) async {
if (animating) return; if (animating) return;
@ -148,6 +139,8 @@ class Board extends FlameGame {
if (!checkMatches()) { if (!checkMatches()) {
swapTiles(tile, targetTile, true); swapTiles(tile, targetTile, true);
} else {
swapNotifier.incrementMoveCount();
} }
selectedRow = null; selectedRow = null;
selectedCol = null; selectedCol = null;
@ -186,7 +179,17 @@ class Board extends FlameGame {
tile1.deselect(); 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; animating = true;
final matches = <List<int>>[]; final matches = <List<int>>[];
@ -239,85 +242,6 @@ class Board extends FlameGame {
return count >= 3; 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 _removeMatchedElements(int row, int col) {
int score = 0; int score = 0;
final int? value = tiles[row][col]?.spriteIndex; 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) { void explodeBomb(Tile bombTile) {
final bombPosition = bombTile.position.clone(); final bombPosition = bombTile.position.clone();
final bombRow = bombTile.row; final bombRow = bombTile.row;
@ -568,30 +453,10 @@ class Board extends FlameGame {
} }
} }
// Анимация взрыва бомбы
_animateBombExplosion(bombPosition); _animateBombExplosion(bombPosition);
tiles[bombRow][bombCol] = null; 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() { void _applyGravity() {
for (int col = 0; col < cols; col++) { for (int col = 0; col < cols; col++) {
for (int row = rows - 1; row >= 0; row--) { 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;
// }
// }
// }
// }
// }
// }

View File

@ -31,60 +31,133 @@ class _MatchMagicGameScreenState extends State<MatchMagicGameScreen> {
}); });
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<Sprite>>( return Scaffold(
future: _spritesFuture, backgroundColor: Colors.black,
builder: (context, snapshot) { body: FutureBuilder<List<Sprite>>(
if (snapshot.connectionState == ConnectionState.done) { future: _spritesFuture,
if (snapshot.hasError) { builder: (context, snapshot) {
return Center(child: Text('Error: ${snapshot.error}')); if (snapshot.connectionState == ConnectionState.done) {
} else if (!snapshot.hasData || snapshot.data == null) { if (snapshot.hasError) {
return const Center(child: Text('No sprites found')); return Center(child: Text('Error: ${snapshot.error}'));
} else { } else if (!snapshot.hasData || snapshot.data == null) {
final sprites = snapshot.data!; return const Center(child: Text('No sprites found'));
board ??= Board( } else {
sprites: sprites, swapNotifier: context.read<SwapNotifier>()); final sprites = snapshot.data!;
return Column( board ??= Board(
children: [ sprites: sprites, swapNotifier: context.read<SwapNotifier>());
const SizedBox(height: 50),
const ScoreDisplay(), return Stack(
Expanded( children: [
child: Center( Column(
child: AspectRatio( children: [
aspectRatio: 1, const SizedBox(height: 50),
child: GameWidget(game: board!), 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: [ } else {
ElevatedButton( return const Center(child: CircularProgressIndicator());
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()); ),
}
},
); );
} }
} }
@ -96,14 +169,44 @@ class ScoreDisplay extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Consumer<SwapNotifier>( child: Row(
builder: (context, notifier, child) { mainAxisAlignment: MainAxisAlignment.center,
return Text( children: [
'Score: ${notifier.score}', Consumer<SwapNotifier>(
style: const TextStyle( builder: (context, notifier, child) {
fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), 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<SwapNotifier>(
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),
),
),
);
},
),
],
), ),
); );
} }

View File

@ -4,11 +4,14 @@ import 'tile.dart';
class SwapNotifier extends ChangeNotifier { class SwapNotifier extends ChangeNotifier {
Tile? selectedTile; Tile? selectedTile;
int _score = 0; int _score = 0;
int _moveCount = 0;
int get score => _score; int get score => _score;
int get moveCount => _moveCount;
void resetScore() { void resetScore() {
_score = 0; _score = 0;
_moveCount = 0;
selectedTile = null; selectedTile = null;
notifyListeners(); notifyListeners();
} }
@ -18,6 +21,11 @@ class SwapNotifier extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void incrementMoveCount() {
_moveCount += 1;
notifyListeners();
}
void selectTile(Tile tile) { void selectTile(Tile tile) {
if (selectedTile == null) { if (selectedTile == null) {
selectedTile = tile; selectedTile = tile;

View File

@ -61,9 +61,24 @@ class Tile extends SpriteComponent with TapCallbacks, DragCallbacks {
return true; return true;
} }
// void select() {
// isSelected = true;
// updateBorder();
// }
void select() { void select() {
isSelected = true; final effectController = EffectController(
updateBorder(); duration: 0.3,
repeatCount: 2,
reverseDuration: 0.3,
);
final blinkEffect = OpacityEffect.to(
0.0,
effectController,
);
add(blinkEffect);
} }
void deselect() { void deselect() {

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:match_magic/screens/main_menu.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'game/match_magic_game.dart'; import 'game/match_magic_game.dart';
import 'game/swap_notifier.dart'; import 'game/swap_notifier.dart';
@ -21,15 +22,8 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return const MaterialApp(
title: 'Match Magic', home: MainMenu(),
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Scaffold(
backgroundColor: Colors.black,
body: MatchMagicGameScreen(),
),
); );
} }
} }

View File

@ -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)),
),
],
),
),
);
}
}