diff --git a/fastlane/metadata/android/en-US/changelogs/19.txt b/fastlane/metadata/android/en-US/changelogs/19.txt new file mode 100644 index 0000000000000000000000000000000000000000..c4767c31cd60be21ad424c3e0b8357a7a045e229 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/19.txt @@ -0,0 +1 @@ +Add minimal playable game. diff --git a/fastlane/metadata/android/fr-FR/changelogs/19.txt b/fastlane/metadata/android/fr-FR/changelogs/19.txt new file mode 100644 index 0000000000000000000000000000000000000000..fa0c74f6946c75534baca4f6411a9e3a690c3dfe --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/19.txt @@ -0,0 +1 @@ +Ajout du jeu minimal. diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart index ff819249949acc531d1d9ae941412138fc0bab1c..d281ea1e861324687fb282f5a4e661105a37e939 100644 --- a/lib/config/default_game_settings.dart +++ b/lib/config/default_game_settings.dart @@ -2,48 +2,48 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; class DefaultGameSettings { // available game parameters codes - static const String parameterCodeLevel = 'level'; - static const String parameterCodeSize = 'size'; + static const String parameterCodeDifficultyLevel = 'difficultyLevel'; + static const String parameterCodeBoardSize = 'boardSize'; static const List<String> availableParameters = [ - parameterCodeLevel, - parameterCodeSize, + parameterCodeDifficultyLevel, + parameterCodeBoardSize, ]; - // level: available values - static const String levelValueEasy = 'easy'; - static const String levelValueMedium = 'medium'; - static const String levelValueHard = 'hard'; - static const String levelValueNightmare = 'nightmare'; - static const List<String> allowedLevelValues = [ - levelValueEasy, - levelValueMedium, - levelValueHard, - levelValueNightmare, + // difficulty level: available values + static const String difficultyLevelValueEasy = 'easy'; + static const String difficultyLevelValueMedium = 'medium'; + static const String difficultyLevelValueHard = 'hard'; + static const String difficultyLevelValueNightmare = 'nightmare'; + static const List<String> allowedDifficultyLevelValues = [ + difficultyLevelValueEasy, + difficultyLevelValueMedium, + difficultyLevelValueHard, + difficultyLevelValueNightmare, ]; - // level: default value - static const String defaultLevelValue = levelValueMedium; + // difficulty level: default value + static const String defaultDifficultyLevelValue = difficultyLevelValueMedium; - // size: available values - static const String sizeValueSmall = '10x10'; - static const String sizeValueMedium = '15x15'; - static const String sizeValueLarge = '20x20'; - static const String sizeValueExtraLarge = '30x30'; - static const List<String> allowedSizeValues = [ - sizeValueSmall, - sizeValueMedium, - sizeValueLarge, - sizeValueExtraLarge, + // board size: available values + static const String boardSizeValueSmall = 'small'; + static const String boardSizeValueMedium = 'medium'; + static const String boardSizeValueLarge = 'large'; + static const String boardSizeValueExtra = 'extra'; + static const List<String> allowedBoardSizeValues = [ + boardSizeValueSmall, + boardSizeValueMedium, + boardSizeValueLarge, + boardSizeValueExtra, ]; - // size: default value - static const String defaultSizeValue = sizeValueMedium; + // board size: default value + static const String defaultBoardSizeValue = boardSizeValueMedium; // available values from parameter code static List<String> getAvailableValues(String parameterCode) { switch (parameterCode) { - case parameterCodeLevel: - return DefaultGameSettings.allowedLevelValues; - case parameterCodeSize: - return DefaultGameSettings.allowedSizeValues; + case parameterCodeDifficultyLevel: + return DefaultGameSettings.allowedDifficultyLevelValues; + case parameterCodeBoardSize: + return DefaultGameSettings.allowedBoardSizeValues; } printlog('Did not find any available value for game parameter "$parameterCode".'); diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart index e77766fda44454584a4f9ae662a2f0c1a48c3414..c58edcd25044c657fca909167ad3ff2191925b9d 100644 --- a/lib/config/default_global_settings.dart +++ b/lib/config/default_global_settings.dart @@ -12,7 +12,7 @@ class DefaultGlobalSettings { static const String skinValueRetro = 'retro'; static const List<String> allowedSkinValues = [ skinValueColors, - skinValueRetro, + // skinValueRetro, ]; // skin: default value static const String defaultSkinValue = skinValueRetro; diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index 8fdb84545f296953e92f7e153b297f2144a76cd5..88d029b7b4935c5614038ef0e06e727d29f49b49 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; @@ -30,7 +31,12 @@ class GameCubit extends HydratedCubit<GameState> { isFinished: state.currentGame.isFinished, animationInProgress: state.currentGame.animationInProgress, // Base data + snake: state.currentGame.snake, board: state.currentGame.board, + boardSize: state.currentGame.boardSize, + // Game data + score: state.currentGame.score, + gameWon: state.currentGame.gameWon, ); // game.dump(); @@ -50,6 +56,10 @@ class GameCubit extends HydratedCubit<GameState> { newGame.dump(); updateState(newGame); + updateGameIsRunning(true); + + startSnake(); + refresh(); } @@ -69,6 +79,69 @@ class GameCubit extends HydratedCubit<GameState> { refresh(); } + void startSnake() async { + int speed = 500; + + while (!state.currentGame.isFinished && state.currentGame.isRunning) { + moveSnake(); + await Future.delayed(Duration(milliseconds: speed)); + + // speed up + speed -= 1; + + if (speed == 0) { + state.currentGame.isFinished = true; + state.currentGame.gameWon = true; + refresh(); + } + } + } + + void turnLeft() { + state.currentGame.turnLeft(); + refresh(); + } + + void turnRight() { + state.currentGame.turnRight(); + refresh(); + } + + void moveSnake([bool enlarge = false]) { + state.currentGame.moveSnake(enlarge); + refresh(); + } + + void increaseScore() { + state.currentGame.score++; + refresh(); + } + + void updateAnimationInProgress(bool inProgress) { + state.currentGame.animationInProgress = inProgress; + refresh(); + } + + void updateGameIsRunning(bool gameIsRunning) { + state.currentGame.isRunning = gameIsRunning; + refresh(); + } + + void updateGameIsStarted(bool gameIsStarted) { + state.currentGame.isStarted = gameIsStarted; + refresh(); + } + + void updateGameIsFinished(bool gameIsFinished) { + state.currentGame.isFinished = gameIsFinished; + refresh(); + } + + void updateGameWon(bool gameWon) { + state.currentGame.gameWon = gameWon; + refresh(); + } + @override GameState? fromJson(Map<String, dynamic> json) { final Game currentGame = json['currentGame'] as Game; @@ -81,7 +154,7 @@ class GameCubit extends HydratedCubit<GameState> { @override Map<String, dynamic>? toJson(GameState state) { return <String, dynamic>{ - 'currentGame': state.currentGame.toJson(), + 'currentGame': state.currentGame, }; } } diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart index 6be014634f062308a93d982e0c29935bb4312d4e..949337bd70b87d4b993383d7b3e70f1def4c4a9f 100644 --- a/lib/cubit/settings_game_cubit.dart +++ b/lib/cubit/settings_game_cubit.dart @@ -10,14 +10,14 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); void setValues({ - String? level, - String? size, + String? difficultyLevel, + String? boardSize, }) { emit( GameSettingsState( settings: GameSettings( - level: level ?? state.settings.level, - size: size ?? state.settings.size, + difficultyLevel: difficultyLevel ?? state.settings.difficultyLevel, + parameterSize: boardSize ?? state.settings.parameterSize, ), ), ); @@ -25,38 +25,39 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { String getParameterValue(String code) { switch (code) { - case DefaultGameSettings.parameterCodeLevel: - return GameSettings.getLevelValueFromUnsafe(state.settings.level); - case DefaultGameSettings.parameterCodeSize: - return GameSettings.getSizeValueFromUnsafe(state.settings.size); + case DefaultGameSettings.parameterCodeDifficultyLevel: + return GameSettings.getLevelValueFromUnsafe(state.settings.difficultyLevel); + case DefaultGameSettings.parameterCodeBoardSize: + return GameSettings.getSizeValueFromUnsafe(state.settings.parameterSize); } return ''; } void setParameterValue(String code, String value) { - final String level = code == DefaultGameSettings.parameterCodeLevel + final String difficultyLevel = (code == DefaultGameSettings.parameterCodeDifficultyLevel) ? value - : getParameterValue(DefaultGameSettings.parameterCodeLevel); - final String size = code == DefaultGameSettings.parameterCodeSize + : getParameterValue(DefaultGameSettings.parameterCodeDifficultyLevel); + final String boardSize = (code == DefaultGameSettings.parameterCodeBoardSize) ? value - : getParameterValue(DefaultGameSettings.parameterCodeSize); + : getParameterValue(DefaultGameSettings.parameterCodeBoardSize); setValues( - level: level, - size: size, + difficultyLevel: difficultyLevel, + boardSize: boardSize, ); } @override GameSettingsState? fromJson(Map<String, dynamic> json) { - final String level = json[DefaultGameSettings.parameterCodeLevel] as String; - final String size = json[DefaultGameSettings.parameterCodeSize] as String; + final String difficultyLevel = + json[DefaultGameSettings.parameterCodeDifficultyLevel] as String; + final String boardSize = json[DefaultGameSettings.parameterCodeBoardSize] as String; return GameSettingsState( settings: GameSettings( - level: level, - size: size, + difficultyLevel: difficultyLevel, + parameterSize: boardSize, ), ); } @@ -64,8 +65,8 @@ class GameSettingsCubit extends HydratedCubit<GameSettingsState> { @override Map<String, dynamic>? toJson(GameSettingsState state) { return <String, dynamic>{ - DefaultGameSettings.parameterCodeLevel: state.settings.level, - DefaultGameSettings.parameterCodeSize: state.settings.size, + DefaultGameSettings.parameterCodeDifficultyLevel: state.settings.difficultyLevel, + DefaultGameSettings.parameterCodeBoardSize: state.settings.parameterSize, }; } } diff --git a/lib/models/game/board.dart b/lib/models/game/board.dart index b5e73d58a1f9c0b9f3fb7e302ad8f46846b614b0..c4cfd2dc3269d36fd4e6e797d6f8e69d7c2314e2 100644 --- a/lib/models/game/board.dart +++ b/lib/models/game/board.dart @@ -1,198 +1,40 @@ -import 'dart:math'; - -import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; - import 'package:snake/models/game/cell.dart'; import 'package:snake/models/game/cell_location.dart'; +import 'package:snake/models/settings/settings_game.dart'; typedef BoardCells = List<List<Cell>>; class Board { + final BoardCells cells; + Board({ required this.cells, }); - BoardCells cells = const []; + factory Board.createEmpty(GameSettings gameSettings) { + final int boardSizeHorizontal = gameSettings.boardSize; + final int boardSizeVertical = gameSettings.boardSize; - factory Board.createEmpty() { - return Board( - cells: [], - ); - } + BoardCells cells = []; + for (int rowIndex = 0; rowIndex < boardSizeVertical; rowIndex++) { + List<Cell> row = []; + for (int colIndex = 0; colIndex < boardSizeHorizontal; colIndex++) { + row.add(Cell(CellValue.empty)); + } + cells.add(row); + } - factory Board.createNew({ - required BoardCells cells, - }) { return Board( cells: cells, ); } - Cell get(CellLocation location) { - if (location.row < cells.length) { - if (location.col < cells[location.row].length) { - return cells[location.row][location.col]; - } - } - - return Cell.none; - } - - void set(CellLocation location, Cell cell) { - cells[location.row][location.col] = cell; - } - - BoardCells copyCells() { - final BoardCells copiedGrid = []; - for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { - final List<Cell> row = []; - for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { - row.add(Cell( - location: CellLocation.go(rowIndex, colIndex), - value: cells[rowIndex][colIndex].value, - isFixed: false, - )); - } - copiedGrid.add(row); - } - - return copiedGrid; - } - - BoardCells getSolvedGrid() { - final Board tmpBoard = Board(cells: copyCells()); - - do { - final List<List<int>> cellsWithUniqueAvailableValue = - tmpBoard.getEmptyCellsWithUniqueAvailableValue(); - if (cellsWithUniqueAvailableValue.isEmpty) { - break; - } - - for (int i = 0; i < cellsWithUniqueAvailableValue.length; i++) { - final int row = cellsWithUniqueAvailableValue[i][0]; - final int col = cellsWithUniqueAvailableValue[i][1]; - final int value = cellsWithUniqueAvailableValue[i][2]; - - tmpBoard.cells[row][col] = Cell( - location: CellLocation.go(row, col), - value: value, - isFixed: tmpBoard.cells[row][col].isFixed, - ); - } - } while (true); - - return tmpBoard.cells; - } - - List<List<int>> getEmptyCellsWithUniqueAvailableValue() { - List<List<int>> candidateCells = []; - - final int boardSize = cells.length; - - for (int row = 0; row < boardSize; row++) { - for (int col = 0; col < boardSize; col++) { - if (cells[row][col].value == 0) { - int allowedValuesCount = 0; - int candidateValue = 0; - for (int value = 1; value <= boardSize; value++) { - if (isValueAllowed(CellLocation.go(row, col), value)) { - candidateValue = value; - allowedValuesCount++; - } - } - if (allowedValuesCount == 1) { - candidateCells.add([row, col, candidateValue]); - } - } - } - } - - return candidateCells; - } - - bool isValueAllowed(CellLocation? candidateLocation, int candidateValue) { - if ((candidateLocation == null) || (candidateValue == 0)) { - return true; - } - - final int boardSize = cells.length; - - // check lines does not contains a value twice - for (int row = 0; row < boardSize; row++) { - final List<int> values = []; - for (int col = 0; col < boardSize; col++) { - int value = cells[row][col].value; - if (row == candidateLocation.row && col == candidateLocation.col) { - value = candidateValue; - } - if (value != 0) { - values.add(value); - } - } - final List<int> distinctValues = values.toSet().toList(); - if (values.length != distinctValues.length) { - return false; - } - } - - // check columns does not contains a value twice - for (int col = 0; col < boardSize; col++) { - final List<int> values = []; - for (int row = 0; row < boardSize; row++) { - int value = cells[row][col].value; - if (row == candidateLocation.row && col == candidateLocation.col) { - value = candidateValue; - } - if (value != 0) { - values.add(value); - } - } - final List<int> distinctValues = values.toSet().toList(); - if (values.length != distinctValues.length) { - return false; - } - } - - // check blocks does not contains a value twice - final int blockSizeVertical = sqrt(cells.length).toInt(); - final int blockSizeHorizontal = cells.length ~/ blockSizeVertical; - - final int horizontalBlocksCount = blockSizeVertical; - final int verticalBlocksCount = blockSizeHorizontal; - for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) { - for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) { - final List<int> values = []; - - for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) { - for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) { - final int row = (blockRow * blockSizeVertical) + rowInBlock; - final int col = (blockCol * blockSizeHorizontal) + colInBlock; - int value = cells[row][col].value; - if (row == candidateLocation.row && col == candidateLocation.col) { - value = candidateValue; - } - if (value != 0) { - values.add(value); - } - } - } - - final List<int> distinctValues = values.toSet().toList(); - if (values.length != distinctValues.length) { - return false; - } - } - } - - return true; + CellValue getCellValue(CellLocation cellLocation) { + return cells[cellLocation.row][cellLocation.col].value; } - void dump() { - printlog(''); - printlog('$Board:'); - printlog(' cells: $cells'); - printlog(''); + void setCellValue(CellLocation cellLocation, CellValue cellValue) { + cells[cellLocation.row][cellLocation.col].value = cellValue; } @override diff --git a/lib/models/game/cell.dart b/lib/models/game/cell.dart index 6077cdccf5f26e69d4fadea7ebe27a33d104a5a3..3ca9cd68007ac22255b223ea5a0e1ae107c5a108 100644 --- a/lib/models/game/cell.dart +++ b/lib/models/game/cell.dart @@ -1,31 +1,16 @@ -import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; - -import 'package:snake/models/game/cell_location.dart'; +enum CellValue { + empty, + head, + body, + fruit, +} class Cell { - const Cell({ - required this.location, - required this.value, - required this.isFixed, - }); - - final CellLocation location; - final int value; - final bool isFixed; - - static Cell none = Cell( - location: CellLocation.go(0, 0), - value: 0, - isFixed: true, + Cell( + this.value, ); - void dump() { - printlog('$Cell:'); - printlog(' location: $location'); - printlog(' value: $value'); - printlog(' isFixed: $isFixed'); - printlog(''); - } + CellValue value; @override String toString() { @@ -34,9 +19,7 @@ class Cell { Map<String, dynamic>? toJson() { return <String, dynamic>{ - 'location': location.toJson(), - 'value': value, - 'isFixed': isFixed, + 'value': value.toString(), }; } } diff --git a/lib/models/game/cell_location.dart b/lib/models/game/cell_location.dart index faab76a9b8941302457f763d3bb869f94944e850..40a12c813c3992e91149e15b4e0a7c45340d3749 100644 --- a/lib/models/game/cell_location.dart +++ b/lib/models/game/cell_location.dart @@ -1,8 +1,8 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; class CellLocation { - final int col; - final int row; + int col; + int row; CellLocation({ required this.col, @@ -13,6 +13,22 @@ class CellLocation { return CellLocation(col: col, row: row); } + void moveRight() { + col += 1; + } + + void moveLeft() { + col -= 1; + } + + void moveTop() { + row -= 1; + } + + void moveBottom() { + row += 1; + } + void dump() { printlog('$CellLocation:'); printlog(' row: $row'); diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart index 55379bcd13e5b96e363d2827c3166d69705950bf..f47274765cc983ef83a70ad1100191f4ff74785f 100644 --- a/lib/models/game/game.dart +++ b/lib/models/game/game.dart @@ -1,6 +1,9 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:snake/models/game/board.dart'; +import 'package:snake/models/game/cell.dart'; +import 'package:snake/models/game/cell_location.dart'; +import 'package:snake/models/game/snake.dart'; import 'package:snake/models/settings/settings_game.dart'; import 'package:snake/models/settings/settings_global.dart'; @@ -17,7 +20,9 @@ class Game { this.animationInProgress = false, // Base data + required this.snake, required this.board, + required this.boardSize, // Game data this.score = 0, @@ -35,19 +40,26 @@ class Game { bool animationInProgress; // Base data - final Board board; + Snake snake; + Board board; + final int boardSize; // Game data int score; bool gameWon; factory Game.createEmpty() { + final GameSettings defaultGameSettings = GameSettings.createDefault(); + final GlobalSettings defaultGlobalSettings = GlobalSettings.createDefault(); + return Game( // Settings - gameSettings: GameSettings.createDefault(), - globalSettings: GlobalSettings.createDefault(), + gameSettings: defaultGameSettings, + globalSettings: defaultGlobalSettings, // Base data - board: Board.createEmpty(), + snake: Snake.create(defaultGameSettings), + board: Board.createEmpty(defaultGameSettings), + boardSize: defaultGameSettings.boardSize, ); } @@ -58,21 +70,157 @@ class Game { final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); - final Board board = Board.createEmpty(); - - return Game( + Game newGame = Game( // Settings gameSettings: newGameSettings, globalSettings: newGlobalSettings, // State isRunning: true, // Base data - board: board, + snake: Snake.create(newGameSettings), + board: Board.createEmpty(newGameSettings), + boardSize: newGameSettings.boardSize, + // Game data + score: 0, ); + newGame.computeBoard(); + + return newGame; } bool get canBeResumed => isStarted && !isFinished; + void computeBoard() { + Board newBoard = Board.createEmpty(gameSettings); + + // Add snake + for (int i = 0; i < snake.cells.length - 1; i++) { + newBoard.setCellValue(snake.cells[i], CellValue.body); + } + newBoard.setCellValue(snake.cells[snake.cells.length - 1], CellValue.head); + + board = newBoard; + } + + void turnLeft() { + switch (snake.direction) { + case SnakeDirection.left: + snake.direction = SnakeDirection.bottom; + case SnakeDirection.right: + snake.direction = SnakeDirection.top; + case SnakeDirection.top: + snake.direction = SnakeDirection.left; + case SnakeDirection.bottom: + snake.direction = SnakeDirection.right; + } + } + + void turnRight() { + switch (snake.direction) { + case SnakeDirection.left: + snake.direction = SnakeDirection.top; + case SnakeDirection.right: + snake.direction = SnakeDirection.bottom; + case SnakeDirection.top: + snake.direction = SnakeDirection.right; + case SnakeDirection.bottom: + snake.direction = SnakeDirection.left; + } + } + + CellLocation nextHeadLocation() { + CellLocation head = CellLocation(col: snake.head.col, row: snake.head.row); + + switch (snake.direction) { + case SnakeDirection.left: + head.moveLeft(); + case SnakeDirection.right: + head.moveRight(); + case SnakeDirection.top: + head.moveTop(); + case SnakeDirection.bottom: + head.moveBottom(); + } + + return head; + } + + bool canMove() { + // compute head's next cell + CellLocation head = nextHeadLocation(); + + // check head is inside board + if (head.col < 0 || + head.col > (boardSize - 1) || + head.row < 0 || + head.row > (boardSize - 1)) { + return false; + } + + // check head is not looped on snake body + for (int i = 0; i < snake.cells.length - 1; i++) { + if (head.col == snake.cells[i].col && head.row == snake.cells[i].row) { + return false; + } + } + + return true; + } + + void moveSnake([bool enlarge = false]) { + if (!canMove()) { + printlog('boom'); + isFinished = true; + return; + } + + // compute head's next cell + CellLocation head = nextHeadLocation(); + + // Append new head to snake cells + snake.cells.add(head); + + // Drop tail + if (!enlarge) { + snake.cells.removeAt(0); + } + + computeBoard(); + } + + void drawBoard() { + String horizontalRule = '----'; + for (int i = 0; i < board.cells[0].length; i++) { + horizontalRule += '-'; + } + + printlog('Board:'); + printlog(horizontalRule); + + for (int rowIndex = 0; rowIndex < board.cells.length; rowIndex++) { + String row = '| '; + for (int colIndex = 0; colIndex < board.cells[rowIndex].length; colIndex++) { + String cellValue = ' '; + switch (board.getCellValue(CellLocation.go(rowIndex, colIndex))) { + case CellValue.empty: + cellValue = ' '; + case CellValue.head: + cellValue = 'O'; + case CellValue.body: + cellValue = 'o'; + case CellValue.fruit: + cellValue = 'X'; + } + row += cellValue; + } + row += ' |'; + + printlog(row); + } + + printlog(horizontalRule); + } + void dump() { printlog(''); printlog('## Current game dump:'); @@ -87,28 +235,10 @@ class Game { printlog(' isFinished: $isFinished'); printlog(' animationInProgress: $animationInProgress'); printlog(' Base data'); - printlog('board:'); - board.dump(); - printGrid(); + drawBoard(); printlog(' Game data'); printlog(' score: $score'); - printlog(''); - } - - printGrid() { - final BoardCells cells = board.cells; - - const String stringValues = '0123456789ABCDEFG'; - printlog(''); - printlog('-------'); - for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { - String row = ''; - for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { - row += stringValues[cells[rowIndex][colIndex].value]; - } - printlog(row); - } - printlog('-------'); + printlog(' gameWon: $gameWon'); printlog(''); } @@ -128,9 +258,11 @@ class Game { 'isFinished': isFinished, 'animationInProgress': animationInProgress, // Base data + 'snake': snake.toJson(), 'board': board.toJson(), // Game data 'score': score, + 'gameWon': gameWon, }; } } diff --git a/lib/models/game/snake.dart b/lib/models/game/snake.dart new file mode 100644 index 0000000000000000000000000000000000000000..2f8598ca27b5a21802398fca34042d77f7e70317 --- /dev/null +++ b/lib/models/game/snake.dart @@ -0,0 +1,50 @@ +import 'package:snake/models/game/cell_location.dart'; +import 'package:snake/models/settings/settings_game.dart'; + +typedef SnakeCells = List<CellLocation>; + +enum SnakeDirection { top, left, bottom, right } + +class Snake { + // Snake's cells: last in list is head + SnakeCells cells; + SnakeDirection direction; + + Snake({ + required this.cells, + required this.direction, + }); + + factory Snake.create(GameSettings gameSettings) { + // Default init snake size: 3 + + // ~ center cell: + final int middleColumn = gameSettings.boardSize ~/ 2; + final int middleRow = gameSettings.boardSize ~/ 2; + + SnakeCells cells = [ + CellLocation.go(middleRow, middleColumn), + CellLocation.go(middleRow, middleColumn), + CellLocation.go(middleRow, middleColumn), + ]; + + return Snake( + cells: cells, + direction: SnakeDirection.right, + ); + } + + CellLocation get head => cells[cells.length - 1]; + + @override + String toString() { + return '$Snake(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'cells': cells, + 'direction': direction.toString(), + }; + } +} diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart index dfdcd1e35ecf62a7126a06c6f20c6d02df0d172d..81a35ce040276da4351ee0c73026cddb8563df86 100644 --- a/lib/models/settings/settings_game.dart +++ b/lib/models/settings/settings_game.dart @@ -3,41 +3,54 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:snake/config/default_game_settings.dart'; class GameSettings { - final String level; - final String size; + String difficultyLevel; + String parameterSize; GameSettings({ - required this.level, - required this.size, + required this.difficultyLevel, + required this.parameterSize, }); static String getLevelValueFromUnsafe(String level) { - if (DefaultGameSettings.allowedLevelValues.contains(level)) { + if (DefaultGameSettings.allowedDifficultyLevelValues.contains(level)) { return level; } - return DefaultGameSettings.defaultLevelValue; + return DefaultGameSettings.defaultDifficultyLevelValue; } static String getSizeValueFromUnsafe(String size) { - if (DefaultGameSettings.allowedSizeValues.contains(size)) { + if (DefaultGameSettings.allowedBoardSizeValues.contains(size)) { return size; } - return DefaultGameSettings.defaultSizeValue; + return DefaultGameSettings.defaultBoardSizeValue; } factory GameSettings.createDefault() { return GameSettings( - level: DefaultGameSettings.defaultLevelValue, - size: DefaultGameSettings.defaultSizeValue, + difficultyLevel: DefaultGameSettings.defaultDifficultyLevelValue, + parameterSize: DefaultGameSettings.defaultBoardSizeValue, ); } + int getBoardSizeFromParameter(String parameterSize) { + const Map<String, int> values = { + DefaultGameSettings.boardSizeValueSmall: 12, + DefaultGameSettings.boardSizeValueMedium: 20, + DefaultGameSettings.boardSizeValueLarge: 30, + DefaultGameSettings.boardSizeValueExtra: 40, + }; + return values[parameterSize] ?? + getBoardSizeFromParameter(DefaultGameSettings.defaultBoardSizeValue); + } + + int get boardSize => getBoardSizeFromParameter(parameterSize); + void dump() { printlog('$GameSettings:'); - printlog(' ${DefaultGameSettings.parameterCodeLevel}: $level'); - printlog(' ${DefaultGameSettings.parameterCodeSize}: $size'); + printlog(' ${DefaultGameSettings.parameterCodeDifficultyLevel}: $difficultyLevel'); + printlog(' ${DefaultGameSettings.parameterCodeBoardSize}: $parameterSize'); printlog(''); } @@ -48,8 +61,8 @@ class GameSettings { Map<String, dynamic>? toJson() { return <String, dynamic>{ - DefaultGameSettings.parameterCodeLevel: level, - DefaultGameSettings.parameterCodeSize: size, + DefaultGameSettings.parameterCodeDifficultyLevel: difficultyLevel, + DefaultGameSettings.parameterCodeBoardSize: parameterSize, }; } } diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart index 50fd73554fcd4e445c68f0970b69902259544f0b..92f6ece9b90aee7aba4098167edacb05d8929dc1 100644 --- a/lib/models/settings/settings_global.dart +++ b/lib/models/settings/settings_global.dart @@ -24,7 +24,7 @@ class GlobalSettings { } void dump() { - printlog('$GlobalSettings:'); + printlog('$GlobalSettings: '); printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin'); printlog(''); } diff --git a/lib/ui/game/game_bottom.dart b/lib/ui/game/game_bottom.dart index 98e3afacb5e25afab1002669ffe57b122510a7fb..4c1f5f3be598c1e57948f7eac32160df621ef402 100644 --- a/lib/ui/game/game_bottom.dart +++ b/lib/ui/game/game_bottom.dart @@ -3,6 +3,7 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:snake/cubit/game_cubit.dart'; import 'package:snake/models/game/game.dart'; +import 'package:snake/ui/widgets/game/controller_bar.dart'; class GameBottomWidget extends StatelessWidget { const GameBottomWidget({super.key}); @@ -13,7 +14,7 @@ class GameBottomWidget extends StatelessWidget { builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; - return Text(currentGame.score.toString()); + return !currentGame.isFinished ? const ControllerBar() : const SizedBox.shrink(); }, ); } diff --git a/lib/ui/game/game_top.dart b/lib/ui/game/game_top.dart index 78f1b3374ca425770816a4a9be8e7703ff89b83a..abd99b8af704d53b9147d74745e760edff0f9226 100644 --- a/lib/ui/game/game_top.dart +++ b/lib/ui/game/game_top.dart @@ -1,12 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:snake/ui/widgets/indicators/indicator_score.dart'; - class GameTopWidget extends StatelessWidget { const GameTopWidget({super.key}); @override Widget build(BuildContext context) { - return const ScoreIndicator(); + return Table( + children: const [ + TableRow( + children: [ + Column( + children: [ + Text('xxx'), + ], + ), + Column( + children: [ + Text('xxx'), + ], + ), + ], + ), + ], + ); } } diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart index 56d3d7e0a9ad5457f153f7a24de2a8e9d576bca7..5a2a0cc218c340574741ef9ccaabdf5eb8234666 100644 --- a/lib/ui/parameters/parameter_painter.dart +++ b/lib/ui/parameters/parameter_painter.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:snake/config/default_game_settings.dart'; +import 'package:snake/config/default_global_settings.dart'; import 'package:snake/models/settings/settings_game.dart'; import 'package:snake/models/settings/settings_global.dart'; @@ -27,11 +28,14 @@ class ParameterPainter extends CustomPainter { // content switch (code) { - case DefaultGameSettings.parameterCodeLevel: - paintLevelParameterItem(canvas, canvasSize); + case DefaultGameSettings.parameterCodeDifficultyLevel: + paintDifficultyLevelParameterItem(canvas, canvasSize); break; - case DefaultGameSettings.parameterCodeSize: - paintSizeParameterItem(canvas, canvasSize); + case DefaultGameSettings.parameterCodeBoardSize: + paintBoardSizeParameterItem(canvas, canvasSize); + break; + case DefaultGlobalSettings.parameterCodeSkin: + paintSkinParameterItem(canvas, canvasSize); break; default: printlog('Unknown parameter: $code/$value'); @@ -76,22 +80,45 @@ class ParameterPainter extends CustomPainter { ); } - void paintLevelParameterItem( + void paintDifficultyLevelParameterItem( final Canvas canvas, final double size, ) { - const text = 'X'; + final List<dynamic> stars = []; + + switch (value) { + case DefaultGameSettings.difficultyLevelValueEasy: + stars.add([0.5, 0.5]); + break; + case DefaultGameSettings.difficultyLevelValueMedium: + stars.add([0.3, 0.5]); + stars.add([0.7, 0.5]); + break; + case DefaultGameSettings.difficultyLevelValueHard: + stars.add([0.3, 0.3]); + stars.add([0.7, 0.3]); + stars.add([0.5, 0.7]); + break; + case DefaultGameSettings.difficultyLevelValueNightmare: + stars.add([0.3, 0.3]); + stars.add([0.7, 0.3]); + stars.add([0.3, 0.7]); + stars.add([0.7, 0.7]); + break; + default: + printlog('Wrong value for level parameter value: $value'); + } final paint = Paint(); paint.strokeJoin = StrokeJoin.round; paint.strokeWidth = 3 / 100 * size; - // centered text value + // Stars final textSpan = TextSpan( - text: text, + text: '⭐', style: TextStyle( color: Colors.black, - fontSize: size / 2.6, + fontSize: size / 3, fontWeight: FontWeight.bold, ), ); @@ -101,46 +128,99 @@ class ParameterPainter extends CustomPainter { textAlign: TextAlign.center, ); textPainter.layout(); - textPainter.paint( - canvas, - Offset( - (size - textPainter.width) * 0.5, - (size - textPainter.height) * 0.5, - ), - ); + + for (var center in stars) { + textPainter.paint( + canvas, + Offset( + size * center[0] - textPainter.width * 0.5, + size * center[1] - textPainter.height * 0.5, + ), + ); + } + } + + void paintBoardSizeParameterItem( + final Canvas canvas, + final double size, + ) { + int gridWidth = 1; + + switch (value) { + case DefaultGameSettings.boardSizeValueSmall: + gridWidth = 2; + break; + case DefaultGameSettings.boardSizeValueMedium: + gridWidth = 3; + break; + case DefaultGameSettings.boardSizeValueLarge: + gridWidth = 4; + break; + case DefaultGameSettings.boardSizeValueExtra: + gridWidth = 5; + break; + default: + printlog('Wrong value for boardSize parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Mini grid + final squareBackgroundColor = Colors.grey.shade200; + final squareBorderColor = Colors.grey.shade800; + + final double cellSize = size / 7; + final double origin = (size - gridWidth * cellSize) / 2; + + for (int row = 0; row < gridWidth; row++) { + for (int col = 0; col < gridWidth; col++) { + final Offset topLeft = Offset(origin + col * cellSize, origin + row * cellSize); + final Offset bottomRight = topLeft + Offset(cellSize, cellSize); + + paint.color = squareBackgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + + paint.color = squareBorderColor; + paint.style = PaintingStyle.stroke; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + } + } } - void paintSizeParameterItem( + void paintSkinParameterItem( final Canvas canvas, final double size, ) { - const text = 'o'; + const int gridWidth = 4; final paint = Paint(); paint.strokeJoin = StrokeJoin.round; paint.strokeWidth = 3; - // centered text value - final textSpan = TextSpan( - text: text, - style: TextStyle( - color: Colors.black, - fontSize: size / 2.6, - fontWeight: FontWeight.bold, - ), - ); - final textPainter = TextPainter( - text: textSpan, - textDirection: TextDirection.ltr, - textAlign: TextAlign.center, - ); - textPainter.layout(); - textPainter.paint( - canvas, - Offset( - (size - textPainter.width) * 0.5, - (size - textPainter.height) * 0.5, - ), - ); + // Mini grid + final borderColor = Colors.grey.shade800; + + final double cellSize = size / gridWidth; + final double origin = (size - gridWidth * cellSize) / 2; + + for (int row = 0; row < gridWidth; row++) { + for (int col = 0; col < gridWidth; col++) { + final Offset topLeft = Offset(origin + col * cellSize, origin + row * cellSize); + final Offset bottomRight = topLeft + Offset(cellSize, cellSize); + + const squareColor = Colors.pink; + + paint.color = squareColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + + paint.color = borderColor; + paint.style = PaintingStyle.stroke; + canvas.drawRect(Rect.fromPoints(topLeft, bottomRight), paint); + } + } } } diff --git a/lib/ui/parameters/parameter_widget.dart b/lib/ui/parameters/parameter_widget.dart index 496a4f1eb0adc2c95aaa9f7271a13c07fa624a08..26e506b47bbb0c8277294478ace367c572c88fd8 100644 --- a/lib/ui/parameters/parameter_widget.dart +++ b/lib/ui/parameters/parameter_widget.dart @@ -37,11 +37,11 @@ class ParameterWidget extends StatelessWidget { Widget content = const SizedBox.shrink(); switch (code) { - case DefaultGameSettings.parameterCodeLevel: - content = getLevelParameterItem(); + case DefaultGameSettings.parameterCodeDifficultyLevel: + content = getDifficultyLevelParameterItem(); break; - case DefaultGameSettings.parameterCodeSize: - content = getSizeParameterItem(); + case DefaultGameSettings.parameterCodeBoardSize: + content = getBoardSizeParameterItem(); break; case DefaultGlobalSettings.parameterCodeSkin: content = getSkinParameterItem(); @@ -75,21 +75,21 @@ class ParameterWidget extends StatelessWidget { ); } - Widget getLevelParameterItem() { + Widget getDifficultyLevelParameterItem() { Color backgroundColor = Colors.grey; switch (value) { - case DefaultGameSettings.levelValueEasy: + case DefaultGameSettings.difficultyLevelValueEasy: backgroundColor = Colors.green; break; - case DefaultGameSettings.levelValueMedium: + case DefaultGameSettings.difficultyLevelValueMedium: backgroundColor = Colors.orange; break; - case DefaultGameSettings.levelValueHard: + case DefaultGameSettings.difficultyLevelValueHard: backgroundColor = Colors.red; break; - case DefaultGameSettings.levelValueNightmare: - backgroundColor = Colors.grey; + case DefaultGameSettings.difficultyLevelValueNightmare: + backgroundColor = Colors.purple; break; default: printlog('Wrong value for level parameter value: $value'); @@ -112,24 +112,24 @@ class ParameterWidget extends StatelessWidget { ); } - Widget getSizeParameterItem() { + Widget getBoardSizeParameterItem() { Color backgroundColor = Colors.grey; switch (value) { - case DefaultGameSettings.sizeValueSmall: + case DefaultGameSettings.boardSizeValueSmall: backgroundColor = Colors.green; break; - case DefaultGameSettings.sizeValueMedium: + case DefaultGameSettings.boardSizeValueMedium: backgroundColor = Colors.orange; break; - case DefaultGameSettings.sizeValueLarge: + case DefaultGameSettings.boardSizeValueLarge: backgroundColor = Colors.red; break; - case DefaultGameSettings.sizeValueExtraLarge: - backgroundColor = Colors.grey; + case DefaultGameSettings.boardSizeValueExtra: + backgroundColor = Colors.purple; break; default: - printlog('Wrong value for size parameter value: $value'); + printlog('Wrong value for boardSize parameter value: $value'); } return StyledButton( @@ -150,10 +150,22 @@ class ParameterWidget extends StatelessWidget { } Widget getSkinParameterItem() { - return StyledButton.text( - caption: '$code/$value', - color: Colors.green.shade800, + Color backgroundColor = Colors.grey; + + return StyledButton( + color: backgroundColor, onPressed: onPressed, + child: CustomPaint( + size: Size(size, size), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + gameSettings: gameSettings, + globalSettings: globalSettings, + ), + isComplex: true, + ), ); } } diff --git a/lib/ui/widgets/game/cell.dart b/lib/ui/widgets/game/cell.dart new file mode 100644 index 0000000000000000000000000000000000000000..c81a87c7a44d09c47fbab07611d7c4b2db272627 --- /dev/null +++ b/lib/ui/widgets/game/cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/cell.dart'; +import 'package:snake/models/game/game.dart'; + +class CellWidget extends StatelessWidget { + const CellWidget({ + super.key, + required this.cellValue, + }); + + final CellValue cellValue; + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final String skin = currentGame.globalSettings.skin; + final String cellValueCode = getCellValueAsset(cellValue); + final String imageAsset = 'assets/skins/${skin}_$cellValueCode.png'; + + return Image( + image: AssetImage(imageAsset), + fit: BoxFit.fill, + ); + }, + ); + } + + String getCellValueAsset(CellValue cellValue) { + String cellValueAsset = 'empty'; + + switch (cellValue) { + case CellValue.empty: + cellValueAsset = 'empty'; + case CellValue.head: + cellValueAsset = 'head'; + case CellValue.body: + cellValueAsset = 'body'; + case CellValue.fruit: + cellValueAsset = 'fruit'; + } + + return cellValueAsset; + } +} diff --git a/lib/ui/widgets/game/controller_bar.dart b/lib/ui/widgets/game/controller_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..6bcb7d5a15707e637bc01253bf85af12730336a9 --- /dev/null +++ b/lib/ui/widgets/game/controller_bar.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +import 'package:snake/cubit/game_cubit.dart'; + +class ControllerBar extends StatelessWidget { + const ControllerBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + StyledButton( + color: Colors.orange, + onPressed: () { + BlocProvider.of<GameCubit>(context).turnLeft(); + }, + child: Text('<'), + ), + StyledButton( + color: Colors.red, + onPressed: () { + BlocProvider.of<GameCubit>(context).moveSnake(true); + }, + child: Text('+'), + ), + StyledButton( + color: Colors.orange, + onPressed: () { + BlocProvider.of<GameCubit>(context).turnRight(); + }, + child: Text('>'), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart index 31cb21f8a64bfcb78eac56b5c82fd734489b375f..45f2d59ee9765fe13871c57fa236ac48d4d75cf5 100644 --- a/lib/ui/widgets/game/game_board.dart +++ b/lib/ui/widgets/game/game_board.dart @@ -2,21 +2,57 @@ import 'package:flutter/material.dart'; import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/cell_location.dart'; import 'package:snake/models/game/game.dart'; +import 'package:snake/ui/widgets/game/cell.dart'; class GameBoardWidget extends StatelessWidget { const GameBoardWidget({super.key}); @override Widget build(BuildContext context) { - return Center( - child: BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - final Game currentGame = gameState.currentGame; + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; - return Text(currentGame.toString()); - }, - ), + final Color borderColor = Theme.of(context).colorScheme.onSurface; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + child: Column( + children: [ + Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + for (int row = 0; row < currentGame.boardSize; row++) + TableRow( + children: [ + for (int col = 0; col < currentGame.boardSize; col++) + Column( + children: [ + CellWidget( + cellValue: + currentGame.board.getCellValue(CellLocation.go(row, col)), + ) + ], + ), + ], + ), + ], + ), + ], + ), + ); + }, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index c4c00ef287e282ebe1107dcc421c5effcdf0673c..637b555a025e135646bc4fe9d78e1db46e3aa4cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: snake game publish_to: "none" -version: 0.3.1+18 +version: 0.4.0+19 environment: sdk: "^3.0.0"