import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:match_magic/db/main_db.dart'; import 'package:match_magic/game/game_mode_manager.dart'; import 'package:match_magic/game/sprite_loader.dart'; import 'package:match_magic/screens/game_over_screen.dart'; import 'package:match_magic/utilities/audio_manager.dart'; import 'package:match_magic/widgets/overlays/game_overlay/hint_button.dart'; import 'package:match_magic/widgets/overlays/game_overlay/pause_button.dart'; import 'package:match_magic/widgets/overlays/game_overlay/restart_button.dart'; import 'tile.dart'; class Board extends FlameGame { BuildContext context; final bool gameMode; late GameModeManager gameModeManager; static const int rows = 8; static const int cols = 8; late double tileSize; Tile? selectedTile; List> tiles = []; int? selectedRow; int? selectedCol; bool animating = false; bool isGridInitialized = false; Tile? lastMovedTile; Tile? lastMovedByGravity; static int score = 0; late TextComponent _playerScore; late TextComponent _playerMoves; late TextComponent _remainingTime; bool isFirstLaunch = true; bool isGameOver = false; static bool isSoundPlaying = true; Board( this.context, { required this.gameMode, }) { gameModeManager = GameModeManager( currentMode: gameMode, onGameOver: _onGameOver, ); } @override Future onLoad() async { super.onLoad(); await loadImages(); gameModeManager.initializeMode(); newGame(); } void newGame() { isFirstLaunch = true; resetGame(); gameModeManager.initializeMode(); tiles.clear(); selectedRow = null; selectedCol = null; animating = false; tileSize = size.x / cols; _initializeGrid(isFirstLaunch); _removeInitialMatches(); isFirstLaunch = false; _playerScore = TextComponent( text: 'Score: ', position: Vector2(60, 70), anchor: Anchor.centerLeft, ); add(_playerScore); if (gameModeManager.currentMode) { _playerMoves = TextComponent( text: 'Moves: ', position: Vector2(200, 70), anchor: Anchor.centerLeft, ); add(_playerMoves); } else { _remainingTime = TextComponent( text: 'Time: ', position: Vector2(200, 70), anchor: Anchor.centerLeft, ); add(_remainingTime); } } void resetGame() { isGameOver = false; this.overlays.remove(GameOverMenu.id); score = 0; gameModeManager.resetMode(); selectedTile = null; final List tilesToRemove = []; for (final component in children) { if (component is Tile) { tilesToRemove.add(component); } } for (final tile in tilesToRemove) { remove(tile); } } @override void render(Canvas canvas) { super.render(canvas); // Progress bar renderer in level mode if (gameModeManager.currentMode == GameMode.levelProgression) { final progressBarWidth = size.x * (score / gameModeManager.targetScore); final progressBarHeight = 20.0; final rect = Rect.fromLTWH(10, size.y - progressBarHeight - 10, progressBarWidth, progressBarHeight); final paint = Paint()..color = Colors.green; canvas.drawRect(rect, paint); } } @override void update(double dt) { _playerScore.text = 'Score: $score'; if (gameModeManager.currentMode) { _playerMoves.text = 'Moves: ${GameModeManager.movesLeft}'; } else { _remainingTime.text = 'Time: ${gameModeManager.remainingTime}'; } if (!isGameOver && gameModeManager.isGameOverCondition()) { _onGameOver(); } super.update(dt); } void _initializeGrid(bool animate) { int totalTiles = rows * cols; int completedTiles = 0; 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: Tile.crystals[spriteIndex], spriteIndex: spriteIndex, size: Vector2.all(tileSize), position: initialPosition, row: row, col: col, 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 + 200), EffectController( duration: 0.5, startDelay: delay, curve: Curves.bounceOut, ), onComplete: () { completedTiles++; if (completedTiles == totalTiles) { isGridInitialized = true; } }, ), ); } } tiles.add(rowTiles); } if (!animate) { isGridInitialized = true; } } int _randomElement() { return Random().nextInt(Tile.crystals.length); } 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 = Tile.crystals[spriteIndex]; tiles[row][col]!.spriteIndex = spriteIndex; hasMatches = true; } } } } while (hasMatches); } int _calculateScore(int matchLength) { if (matchLength == 3) { return 50; } else if (matchLength == 4) { return 400; } else if (matchLength == 5) { return 200; } return 0; } // Moving elements void selectTile(Tile tile) { if (selectedTile == null) { selectedTile = tile; tile.select(); } else { if (_isNeighbor(selectedTile!, tile) || selectedTile!.isMagicCube) { } else { selectedTile?.deselect(); selectedTile = tile; tile.select(); } } } 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); } Future handleTileSwipe(Tile tile, Vector2 delta) async { if (!isGridInitialized || animating) return; Tile? targetTile; if (tile.isMagicCube) { targetTile = _getTileBySwipeDirection(tile, delta); if (targetTile != null) { _removeAllOfType(targetTile.spriteIndex); _animateRemoveTile(tile); isSoundPlaying = await MainDB.instance.getSoundEnabled(); if (isSoundPlaying) { AudioManager.playExplosionSound(); } tiles[tile.row][tile.col] = null; } return; } targetTile = _getTileBySwipeDirection(tile, delta); if (targetTile != null) { animating = true; lastMovedTile = tile; swapTiles(tile, targetTile, true); await Future.delayed(const Duration(milliseconds: 300)); bool matchesFound = await checkMatches(); if (!matchesFound) { swapTiles(tile, targetTile, true); } else { GameModeManager.movesLeft--; } selectedRow = null; selectedCol = null; animating = false; } } Tile? _getTileBySwipeDirection(Tile tile, Vector2 delta) { int row = tile.row; int col = tile.col; Tile? targetTile; if (delta.x.abs() > delta.y.abs()) { if (delta.x > 0 && col < cols - 1) { targetTile = tiles[row][col + 1]; } else if (delta.x < 0 && col > 0) { targetTile = tiles[row][col - 1]; } } else { if (delta.y > 0 && row < rows - 1) { targetTile = tiles[row + 1][col]; } else if (delta.y < 0 && row > 0) { targetTile = tiles[row - 1][col]; } } return targetTile; } void swapTiles(Tile tile1, Tile tile2, bool animate) { final tempRow1 = tile1.row; final tempCol1 = tile1.col; final tempRow2 = tile2.row; final tempCol2 = tile2.col; tile1.row = tempRow2; tile1.col = tempCol2; tile2.row = tempRow1; tile2.col = tempCol1; tiles[tile1.row][tile1.col] = tile1; tiles[tile2.row][tile2.col] = tile2; if (animate) { final tempPosition1 = tile1.position.clone(); final tempPosition2 = tile2.position.clone(); tile1.animateMoveTo(tempPosition2, () {}); tile2.animateMoveTo(tempPosition1, () {}); } tile1.deselect(); tile1.deselect(); } // Hint button Future findHint() async { for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { Tile? tile = tiles[row][col]; if (col < cols - 1 && await _canSwap(row, col, row, col + 1)) { return tile; } if (row < rows - 1 && await _canSwap(row, col, row + 1, col)) { return tile; } } } return null; } Future _canSwap(int row1, int col1, int row2, int col2) async { Tile tempTile1 = tiles[row1][col1]!; Tile tempTile2 = tiles[row2][col2]!; tiles[row1][col1] = tempTile2; tiles[row2][col2] = tempTile1; bool matchFound = await checkMatches(simulate: true); tiles[row1][col1] = tempTile1; tiles[row2][col2] = tempTile2; return matchFound; } void showHint() async { Tile? hintTile = await findHint(); if (hintTile != null) { hintTile.select(); } } // Match checks Future checkMatches({bool simulate = false}) async { 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 = >[]; 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) { int points = 0; for (final match in matches) { points += _removeMatchedElements(match[0], match[1]); } isSoundPlaying = await MainDB.instance.getSoundEnabled(); if (isSoundPlaying) { AudioManager.playSelectSound(); } Future.delayed(const Duration(milliseconds: 300), () { _applyGravity(); Future.delayed(const Duration(milliseconds: 300), () { _fillEmptySpaces(); Future.delayed(const Duration(milliseconds: 300), () { animating = false; checkMatches(); }); }); }); score += points; return true; } animating = false; 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; } // Removing items void _removeAllOfType(int spriteIndex) { int removedCount = 0; for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { if (tiles[row][col]?.spriteIndex == spriteIndex) { _animateRemoveTile(tiles[row][col]!); tiles[row][col] = null; removedCount++; } } } score += removedCount * 100; Future.delayed(const Duration(milliseconds: 300), () { _applyGravity(); Future.delayed(const Duration(milliseconds: 300), () { _fillEmptySpaces(); }); }); } int _removeMatchedElements(int row, int col) { int score = 0; final int? value = tiles[row][col]?.spriteIndex; bool specialTriggered = false; Tile? tileToTransformIntoSpecial; 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) { tileToTransformIntoSpecial = lastMovedTile ?? lastMovedByGravity; } else if (right - left + 1 >= 5) { tileToTransformIntoSpecial = lastMovedTile ?? lastMovedByGravity; } for (int i = left; i <= right; i++) { if (tiles[row][i] != null) { if (tiles[row][i]!.isBomb) { specialTriggered = true; _triggerBomb(row, i); } if (tiles[row][i] != tileToTransformIntoSpecial) { _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) { tileToTransformIntoSpecial = lastMovedTile ?? lastMovedByGravity; } else if (bottom - top + 1 >= 5) { tileToTransformIntoSpecial = lastMovedTile ?? lastMovedByGravity; } for (int i = top; i <= bottom; i++) { if (tiles[i][col] != null) { if (tiles[i][col]!.isBomb) { specialTriggered = true; _triggerBomb(i, col); } if (tiles[i][col] != tileToTransformIntoSpecial) { _animateRemoveTile(tiles[i][col]!); tiles[i][col] = null; } } } } if (tileToTransformIntoSpecial != null) { if ((right - left + 1 >= 5) || (bottom - top + 1 >= 5)) { _createMagicCube( tileToTransformIntoSpecial.row, tileToTransformIntoSpecial.col); } else { _createBomb( tileToTransformIntoSpecial.row, tileToTransformIntoSpecial.col); } } if (specialTriggered) { _triggerBomb(row, col); } return score; } void _animateRemoveTile(Tile tile) { tile.add(ScaleEffect.to( Vector2.zero(), EffectController( duration: 0.2, curve: Curves.easeInBack, ), onComplete: () => tile.removeFromParent(), )); } // The emergence of new elements 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 + 200), () {}); lastMovedByGravity = tiles[row][col]; break; } } } } } } 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: Tile.crystals[spriteIndex], spriteIndex: spriteIndex, size: Vector2.all(tileSize), position: Vector2(col * tileSize, -tileSize), row: row, col: col, onSwipe: handleTileSwipe, ); tiles[row][col] = tile; add(tile); tile.animateMoveTo( Vector2(col * tileSize, row * tileSize + 200), () {}); } } } } // Bomb void _triggerBomb(int row, int col) async { final tile = tiles[row][col]; if (tile == null || !tile.isBomb) return; final bombPosition = tile.position.clone(); for (int i = max(0, row - 1); i <= min(rows - 1, row + 1); i++) { for (int j = max(0, col - 1); j <= min(cols - 1, col + 1); j++) { if (tiles[i][j] != null) { if (tiles[i][j]!.isBomb && !(i == row && j == col)) { tiles[i][j]!.isBomb = false; _triggerBomb(i, j); } else { _animateRemoveTile(tiles[i][j]!); tiles[i][j] = null; } } } } _animateBombExplosion(bombPosition); _animateRemoveTile(tile); tiles[row][col] = null; isSoundPlaying = await MainDB.instance.getSoundEnabled(); if (isSoundPlaying) { AudioManager.playExplosionSound(); } } void _animateBombExplosion(Vector2 position) { final explosion = CircleComponent( radius: tileSize / 2, paint: Paint()..color = Colors.orange.withOpacity(0.7), position: position - Vector2.all(tileSize / 2), ); add(explosion); explosion.add( ScaleEffect.to( Vector2.all(3), EffectController(duration: 0.5), onComplete: () => explosion.removeFromParent(), ), ); } void _createBomb(int row, int col) async { isSoundPlaying = await MainDB.instance.getSoundEnabled(); if (isSoundPlaying) { AudioManager.playFourElementsSound(); } final tile = tiles[row][col]; if (tile != null) { tile.isBomb = true; tile.add( OpacityEffect.to( 0.5, EffectController( duration: 0.5, infinite: true, reverseDuration: 0.5, ), ), ); tile.add( ColorEffect( Colors.orange, EffectController( duration: 0.5, infinite: true, reverseDuration: 0.5, ), opacityFrom: 0.5, opacityTo: 1.0, ), ); } } void explodeBomb(Tile bombTile) { final bombPosition = bombTile.position.clone(); final bombRow = bombTile.row; final bombCol = bombTile.col; for (int i = max(0, bombRow - 1); i <= min(rows - 1, bombRow + 1); i++) { for (int j = max(0, bombCol - 1); j <= min(cols - 1, bombCol + 1); j++) { if (tiles[i][j] != null) { if (tiles[i][j]!.isBomb && (i != bombRow || j != bombCol)) { _triggerBomb(i, j); } else { _animateRemoveTile(tiles[i][j]!); tiles[i][j] = null; } } } } _animateBombExplosion(bombPosition); tiles[bombRow][bombCol] = null; } // Magic cube void _createMagicCube(int row, int col) async { isSoundPlaying = await MainDB.instance.getSoundEnabled(); if (isSoundPlaying) { AudioManager.playFourElementsSound(); } var tile = tiles[row][col]; tile?.sprite = Tile.magicCubeSprite; tile?.isMagicCube = true; } // Game over void _onGameOver() { print("Game Over! Score: ${score}"); showGameOverScreen(); } void showGameOverScreen() { isGameOver = true; remove(_playerScore); if (gameModeManager.currentMode) { remove(_playerMoves); } else { remove(_remainingTime); } this.overlays.remove(PauseButton.id); this.overlays.remove(RestartButton.id); this.overlays.remove(HintButton.id); MainDB.instance.addHighScore(score, () { this.overlays.add(GameOverMenu.id); }); } }