diff --git a/lib/enums/Color.dart b/lib/enums/StoneColor.dart similarity index 92% rename from lib/enums/Color.dart rename to lib/enums/StoneColor.dart index ca61ed3..23b1a5d 100644 --- a/lib/enums/Color.dart +++ b/lib/enums/StoneColor.dart @@ -1,5 +1,5 @@ /// Enum for the stone colors. -enum Color { +enum StoneColors { red, green, blue, @@ -7,4 +7,3 @@ enum Color { pink, special, // not a "real" color; indicates that a stone is special (Not retarded, in a nice way, like mama always used to tell me i am her special child) } - diff --git a/lib/game/Board.dart b/lib/game/Board.dart new file mode 100644 index 0000000..bbd9c24 --- /dev/null +++ b/lib/game/Board.dart @@ -0,0 +1,253 @@ +import 'dart:math'; + +import 'package:bubbletwist/game/stone/SpecialStone.dart'; + +import '../enums/StoneColor.dart'; +import 'Game.dart'; +import 'stone/PentaSpecialStone.dart'; +import 'stone/Stone.dart'; +import 'stone/StoneLocation.dart'; + +class Board { + static const int boardSize = 8; + final Game game; + late final List> stones = + List.filled(boardSize, List.filled(boardSize, Stone())); + + Board(this.game) { + for (var row in stones) { + for (var stone in row) { + stone = Stone(); + } + } + // The map generates with pairs of 3+ stones already in place. + // To fix that we just let the game remove all 3+ stone pairs until none are left + //while (checkBoard()); + } + void updateBoard() { + game.updateBoard(); + } + + bool swapStones(StoneLocation sl1, StoneLocation sl2) { + // Check if the two stones that have been clicked are next to each other + if (!(sl1.row >= sl2.row - 1 && sl1.row <= sl2.row + 1) || + !(sl1.column >= sl2.column - 1 && sl1.column <= sl2.column + 1)) { + return false; // If they are not next to each other just abort + } + + final s1 = game.getStone(sl1); + final ss1 = s1 is PentaSpecialStone ? s1 : null; + if (ss1 != null) { + ss1.stoneColor = getStone(sl2)!.stoneColor; + performSpecialStone(ss1, sl1); + return true; + } + + final s2 = game.getStone(sl2); + final ss2 = s2 is PentaSpecialStone ? s2 : null; + if (ss2 != null) { + ss2.stoneColor = getStone(sl1)!.stoneColor; + performSpecialStone(ss2, sl2); + return true; + } + + // Swap stones + swap(sl1, sl2); + if (checkBoard()) { + // Check if swapping was a valid move + // If Swap is valid, Stones have been removed and others have to fall down + applyGravity(); + while (checkBoard()) { + // Then we have to keep checking until no more 3+ stones are created by falling stones + applyGravity(); + } + return true; + } else { + // If Swap is invalid, the stones get swapped back + swap(sl1, sl2); + return false; + } + } + + bool checkBoard() { + bool stuffDeleted = false; + + // FOR HORIZONTALS + for (int row = 0; row < boardSize; ++row) { + // First go through each row + int startPosition = 0; + int counter = 1; + for (int column = 1; column < boardSize; ++column) { + // And then through each column + if (stones[row][column] == null || stones[row][column - 1] == null) { + // Ignore deleted stones + continue; + } + + final colorsAreSame = stones[row][column]!.stoneColor == + stones[row][column - 1]!.stoneColor; + + // If both stones have the same color increase counter + if (colorsAreSame) { + counter++; + } + // If they are not the same color or we reach the end of the Board, + // check the counter to see if the last 3+ stones had the same color + if (!colorsAreSame || column == boardSize - 1) { + if (counter >= 3) { + // And if we had 3+ stones, remove them + if (game.isRunning()) { + game.addPoints(counter); + game.addTime(3); + } + for (int k = 0; k < counter; ++k) { + final st = StoneLocation(row: row, column: startPosition + k); + removeStone(st); + stuffDeleted = true; + } + if (counter == 3 && game.isRunning()) { + //stones[row][startPosition] = TripleSpecialStone(this); + } + if (counter == 4 && game.isRunning()) { + //stones[row][startPosition] = QuadSpecialStone(this, false); + } + if (counter == 5 && game.isRunning()) { + stones[row][startPosition] = PentaSpecialStone(this); + } + } + // Reset counter and start position after each color change + counter = 1; + startPosition = column; + } + } + } + + // FOR VERTICALS + for (int column = 0; column < boardSize; ++column) { + // First go through each column + int startPosition = 0; + int counter = 1; + for (int row = 1; row < boardSize; ++row) { + // And then through each row + if (stones[row][column] == null || stones[row - 1][column] == null) { + // Ignore deleted stones + continue; + } + + // If both stones have the same color increase counter + final colorsAreSame = stones[row][column]!.stoneColor == + stones[row - 1][column]!.stoneColor; + if (colorsAreSame) { + counter++; + } + // If they are not the same color or we reach the end of the Board, + // check the counter to see if the last 3+ stones had the same color + if (!colorsAreSame || row == boardSize - 1) { + if (counter >= 3) { + //And if we had 3+ stones, remove them + if (game.isRunning()) { + game.addPoints(counter); + game.addTime(3); + } + for (int k = 0; k < counter; ++k) { + final st = StoneLocation(row: startPosition + k, column: column); + removeStone(st); + stuffDeleted = true; + } + if (counter == 3 && game.isRunning()) { + //stones[startPosition][column] = TripleSpecialStone(this); + } + if (counter == 4 && game.isRunning()) { + //stones[startPosition][column] = QuadSpecialStone(this, true); + } + if (counter == 5 && game.isRunning()) { + stones[startPosition][column] = PentaSpecialStone(this); + } + } + // Reset counter and start position after each color change + counter = 1; + startPosition = row; + } + } + } + if (stuffDeleted) { + // If stuff has been deleted we have to update the Gui + game.updateBoard(); + } + return stuffDeleted; + } + + void removeStone(StoneLocation sl) { + if (sl.row < 0 || + sl.row >= boardSize || + sl.column < 0 || + sl.column >= boardSize) { + return; + } + if (!game.isRunning()) { + // If game is not running, just give the stones a new color + stones[sl.row][sl.column]!.stoneColor = + StoneColors.values[Random().nextInt(StoneColors.values.length)]; + } else { + // Otherwise mark stones as deleted + stones[sl.row][sl.column] = null; + } + } + + void applyGravity() { + bool stonesHaveFallenDown; + // Move Stones down 1 row if the stone below them is marked as deleted + do { + stonesHaveFallenDown = false; + for (int column = 0; column < boardSize; ++column) { + for (int row = boardSize - 1; row > 0; --row) { + if (stones[row][column] == null) { + if (stones[row - 1][column] == null) { + continue; + } + stones[row][column] = stones[row - 1][column]; + stones[row - 1][column] = null; + stonesHaveFallenDown = true; + } + } + } + } while ( + stonesHaveFallenDown); // Continue doing so until all deleted Stones are at the top most position + + // Then generate new stones that rain from the sky) + for (var row in stones) { + for (var stone in row) { + if (stone == null) { + stone = Stone(); + } + } + } + game.updateBoard(); + } + + void performSpecialStone(SpecialStone ss, StoneLocation sl) { + //Delete Stones + ss.performSpecialStoneAction(); + removeStone(sl); + //show Deleted Stones + updateBoard(); + //Make Stones fall down into new "holes" + applyGravity(); + updateBoard(); //Show the Gravity Effect + while (checkBoard()) { + // Then we have to keep checking until no more 3+ stones are created by falling stones + applyGravity(); + } + } + + void swap(StoneLocation sl1, StoneLocation sl2) { + final tmp = stones[sl1.row][sl1.column]; + stones[sl1.row][sl1.column] = stones[sl2.row][sl2.column]; + stones[sl2.row][sl2.column] = tmp; + game.updateBoard(); + } + + Stone? getStone(StoneLocation sl) { + return stones[sl.row][sl.column]; + } +} diff --git a/lib/game/Game.dart b/lib/game/Game.dart index e69de29..ab46144 100644 --- a/lib/game/Game.dart +++ b/lib/game/Game.dart @@ -0,0 +1,121 @@ +import 'dart:async'; + +import '../enums/StoneColor.dart'; +import 'Board.dart'; +import 'IGameConsumer.dart'; +import 'stone/Stone.dart'; +import 'stone/StoneLocation.dart'; + +class Game { + IGameConsumer gameConsumer; + late int timeInSeconds; + int startTime = 60; // hardcoded + int points = 0; + bool running = false; + late Board board; + late Timer counterTimer; + StoneLocation stoneToSwap = StoneLocation(row: -1, column: -1); + + Game(this.gameConsumer) { + timeInSeconds = startTime; + board = Board(this); + } + + int getPoints() => points; + + int getTimeInSeconds() => timeInSeconds; + + void addPoints(int pointsToAdd) { + if (timeInSeconds != 0 && running) { + points += pointsToAdd; + gameConsumer.updatePoints(); + } + } + + void addTime(int timeToAdd) { + if (timeInSeconds != 0 && running) { + timeInSeconds += timeToAdd; + gameConsumer.updateTime(); + } + } + + void endGame() { + running = false; + counterTimer?.cancel(); + gameConsumer.gameStopped(); + } + + Future countDown() async { + do { + timeInSeconds--; + gameConsumer.updateTime(); + if (timeInSeconds > 0) { + await Future.delayed(Duration(seconds: 1)); + } + } while (timeInSeconds > 0); + if (running) { + endGame(); + } + } + + StoneColors? getStoneColor(StoneLocation location) { + return board.getStone(location)?.getStoneColor(); + } + + Stone? getStone(StoneLocation location) { + return board.getStone(location); + } + + void start() { + if (!running) { + timeInSeconds = startTime; + points = 0; + board = Board(this); + gameConsumer.updateStones(); + counterTimer = Timer.periodic(Duration(seconds: 1), (_) => countDown()); + gameConsumer.updatePoints(); + running = true; + } + } + + void stop() { + if (running) { + running = false; + timeInSeconds = 0; + counterTimer.cancel(); + } + } + + bool swapStones(StoneLocation sl1, StoneLocation sl2) { + return board.swapStones(sl1, sl2); + } + + void updateBoard() { + if (running) { + gameConsumer.updateStones(); + } + } + + bool isRunning() => running; + + bool swap(StoneLocation sl) { + if (stoneToSwap.row == -1) { + stoneToSwap = sl; + return false; + } else { + swapStones(stoneToSwap, sl); + stoneToSwap = StoneLocation(row: -1, column: -1); + return true; + } + } + + bool performSpecialStone(StoneLocation sl) { + Stone? s = getStone(sl); + /*if (s is TriggerableSpecialStone) { + s.setActivationLocation(sl); + board.performSpecialStone(s, sl); + return true; + }*/ + return false; + } +} diff --git a/lib/game/stone/PentaSpecialStone.dart b/lib/game/stone/PentaSpecialStone.dart new file mode 100644 index 0000000..b7e1283 --- /dev/null +++ b/lib/game/stone/PentaSpecialStone.dart @@ -0,0 +1,27 @@ +import '../Board.dart'; +import 'SpecialStone.dart'; +import 'StoneLocation.dart'; + +class PentaSpecialStone extends SpecialStone { + PentaSpecialStone(Board board) : super(board); + + int getSpecialStoneNumber() { + return 5; + } + + void performSpecialStoneAction() { + StoneLocation sl = StoneLocation(row: -1, column: -1); + for (int i = 0; i < Board.boardSize; ++i) { + for (int j = 0; j < Board.boardSize; ++j) { + sl.row = i; + sl.column = j; + if (this == board.getStone(sl)) { + continue; + } + if (board.getStone(sl)?.getStoneColor() == this.getStoneColor()) { + board.removeStone(sl); + } + } + } + } +} diff --git a/lib/game/stone/SpecialStone.dart b/lib/game/stone/SpecialStone.dart new file mode 100644 index 0000000..fc06995 --- /dev/null +++ b/lib/game/stone/SpecialStone.dart @@ -0,0 +1,14 @@ +import '../Board.dart'; +import 'Stone.dart'; + +abstract class SpecialStone extends Stone { + final Board board; + + SpecialStone(this.board); + + /// Returns the associated number of the special stone + int getSpecialStoneNumber(); + + /// Performs the action of the special stone + void performSpecialStoneAction(); +} diff --git a/lib/game/stone/Stone.dart b/lib/game/stone/Stone.dart index a4d24b9..b145b73 100644 --- a/lib/game/stone/Stone.dart +++ b/lib/game/stone/Stone.dart @@ -1,10 +1,11 @@ import 'dart:math'; -import '../../enums/Color.dart'; + +import '../../enums/StoneColor.dart'; /// Class to represent a stone on the board. class Stone { /// Color of the stone - late Color color; + late StoneColors stoneColor; Stone() { setRandomColor(); @@ -12,16 +13,17 @@ class Stone { /// Getter for the color. /// \return color - Color getColor() => color; + StoneColors getStoneColor() => stoneColor; /// Setter for the color /// \param color - void setColor(Color color) { - this.color = color; + void setColor(StoneColors stoneColor) { + this.stoneColor = stoneColor; } /// method that sets a random color void setRandomColor() { - color = Color.values[Random().nextInt(Color.values.length)]; + stoneColor = + StoneColors.values[Random().nextInt(StoneColors.values.length)]; } } diff --git a/lib/game/stone/StoneLocation.dart b/lib/game/stone/StoneLocation.dart new file mode 100644 index 0000000..c9574ec --- /dev/null +++ b/lib/game/stone/StoneLocation.dart @@ -0,0 +1,10 @@ +/// Simple structure that stores two ints to locate a stone on the board. +class StoneLocation { + int row; + int column; + + StoneLocation({ + required this.row, + required this.column, + }); +} diff --git a/lib/main.dart b/lib/main.dart index f2b8048..8d63c45 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,11 @@ -import 'dart:math'; - +import 'package:bubbletwist/enums/StoneColor.dart'; +import 'package:bubbletwist/game/Game.dart'; +import 'package:bubbletwist/game/IGameConsumer.dart'; import 'package:flutter/material.dart'; +import 'game/stone/Stone.dart'; +import 'game/stone/StoneLocation.dart'; + void main() { runApp(const MyApp()); } @@ -9,7 +13,7 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - final int gridSize = 12; + final int gridSize = 8; // This widget is the root of your application. @override @@ -20,7 +24,7 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page', gridSize: 12), + home: const MyHomePage(title: 'Flutter Demo Home Page', gridSize: 8), ); } } @@ -44,44 +48,25 @@ class MyHomePage extends StatefulWidget { State createState() => _MyHomePageState(); } -class _MyHomePageState extends State { - late List> _grid; - int _selectedRow = -1; - int _selectedCol = -1; - int _score = 0; - +class _MyHomePageState extends State implements IGameConsumer { + late List> _grid; + late Game game; + final int _score = 0; @override void initState() { super.initState(); - _initializeGrid(); - } - - void _initializeGrid() { + game = Game(this); _grid = List.generate(widget.gridSize, - (_) => List.generate(widget.gridSize, (_) => Random().nextInt(5))); - _removeInitialMatches(); - } - - void _removeInitialMatches() { - bool hasMatches; - do { - hasMatches = _removeMatches(); - _applyGravity(); - } while (hasMatches); - _score = 0; + (index) => List.generate(widget.gridSize, (index) => Stone())); + game.start(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Candy Crush'), - ], - ), + title: Text(widget.title), ), body: Center( child: Column( @@ -90,12 +75,11 @@ class _MyHomePageState extends State { padding: const EdgeInsets.all(8.0), child: Text( 'Score: $_score', - style: TextStyle(fontSize: 24), + style: const TextStyle(fontSize: 24), ), ), Expanded( child: GridView.builder( - shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: widget.gridSize, ), @@ -103,13 +87,18 @@ class _MyHomePageState extends State { itemBuilder: (BuildContext context, int index) { int row = index ~/ widget.gridSize; int col = index % widget.gridSize; + StoneLocation location = StoneLocation(row: row, column: col); return GestureDetector( - onTap: () => _onTileTap(row, col), + onTap: () { + setState(() { + //game.handleTap(row, col); + }); + }, child: Container( margin: const EdgeInsets.all(2), decoration: BoxDecoration( - color: _getColor(_grid[row][col]), borderRadius: BorderRadius.circular(8), + color: _getColorForStone(game.getStone(location)), ), ), ); @@ -122,143 +111,50 @@ class _MyHomePageState extends State { ); } - - Color _getColor(int value) { - switch (value) { - case 0: + Color _getColorForStone(Stone? stone) { + // Beispielhafte Farben basierend auf dem Stein. + switch (stone?.getStoneColor()) { + case StoneColors.red: return Colors.red; - case 1: - return Colors.blue; - case 2: + case StoneColors.green: return Colors.green; - case 3: + case StoneColors.blue: + return Colors.blue; + case StoneColors.yellow: return Colors.yellow; - case 4: - return Colors.orange; default: - return Colors.white; + return Colors.grey; } } - void _onTileTap(int row, int col) { + @override + void gameStopped() { + // TODO: implement gameStopped + } + + @override + void updatePoints() { + int _score = game.getPoints(); + } + + @override + void updateStones() { setState(() { - if (_selectedRow == -1 && _selectedCol == -1) { - _selectedRow = row; - _selectedCol = col; - } else { - if (_isValidMove(_selectedRow, _selectedCol, row, col)) { - _swapTiles(_selectedRow, _selectedCol, row, col); - while (_removeMatches()) { - _applyGravity(); - } + // _grid aktualisieren + for (int row = 0; row < widget.gridSize; row++) { + for (int col = 0; col < widget.gridSize; col++) { + _grid[row][col] = game.getStone(StoneLocation(row: row, column: col)); } - _selectedRow = -1; - _selectedCol = -1; } }); } - bool _isValidMove(int row1, int col1, int row2, int col2) { - if ((row1 == row2 && (col1 - col2).abs() == 1) || - (col1 == col2 && (row1 - row2).abs() == 1)) { - int temp = _grid[row1][col1]; - _grid[row1][col1] = _grid[row2][col2]; - _grid[row2][col2] = temp; - - bool isValid = _findMatches(row1, col1).isNotEmpty || - _findMatches(row2, col2).isNotEmpty; - - _grid[row2][col2] = _grid[row1][col1]; - _grid[row1][col1] = temp; - - return isValid; - } - return false; + @override + void updateTime() { + // TODO: implement updateTime } - List> _findMatches(int row, int col) { - int color = _grid[row][col]; - List> matches = []; - - // Check horizontally - List horizontalMatch = []; - for (int c = col; c >= 0 && _grid[row][c] == color; c--) { - horizontalMatch.add(c); - } - for (int c = col + 1; c < widget.gridSize && _grid[row][c] == color; c++) { - horizontalMatch.add(c); - } - if (horizontalMatch.length >= 3) { - for (int c in horizontalMatch) { - matches.add([row, c]); - } - } - - // Check vertically - List verticalMatch = []; - for (int r = row; r >= 0 && _grid[r][col] == color; r--) { - verticalMatch.add(r); - } - for (int r = row + 1; r < widget.gridSize && _grid[r][col] == color; r++) { - verticalMatch.add(r); - } - if (verticalMatch.length >= 3) { - for (int r in verticalMatch) { - matches.add([r, col]); - } - } - - return matches; - } - - bool _removeMatches() { - List> allMatches = []; - int scoreIncrement = 0; - - for (int row = 0; row < widget.gridSize; row++) { - for (int col = 0; col < widget.gridSize; col++) { - List> matches = _findMatches(row, col); - if (matches.isNotEmpty) { - allMatches.addAll(matches); - scoreIncrement++; // Increment score based on number of matches - } - } - } - - for (var match in allMatches) { - _grid[match[0]][match[1]] = -1; // Mark the tile as removed - } - - setState(() { - _score += scoreIncrement; - }); - - return allMatches.isNotEmpty; - } - - - void _applyGravity() { - for (int col = 0; col < widget.gridSize; col++) { - int emptyRow = widget.gridSize - 1; - for (int row = widget.gridSize - 1; row >= 0; row--) { - if (_grid[row][col] != -1) { - _grid[emptyRow][col] = _grid[row][col]; - if (emptyRow != row) { - _grid[row][col] = -1; - } - emptyRow--; - } - } - - for (int row = emptyRow; row >= 0; row--) { - _grid[row][col] = Random().nextInt(5); - } - } - } - - void _swapTiles(int row1, int col1, int row2, int col2) { - int temp = _grid[row1][col1]; - _grid[row1][col1] = _grid[row2][col2]; - _grid[row2][col2] = temp; + Game getGame() { + return game; } }