From 40f0f045730f165fa49dcd12db97b9ad7f85d432 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Tue, 21 Jun 2022 11:58:15 +0200
Subject: [PATCH] Create minimal playable game

---
 .../metadata/android/en-US/changelogs/19.txt  |   1 +
 .../metadata/android/fr-FR/changelogs/19.txt  |   1 +
 lib/config/default_game_settings.dart         |  64 +++---
 lib/config/default_global_settings.dart       |   2 +-
 lib/cubit/game_cubit.dart                     |  75 ++++++-
 lib/cubit/settings_game_cubit.dart            |  41 ++--
 lib/models/game/board.dart                    | 194 ++----------------
 lib/models/game/cell.dart                     |  37 +---
 lib/models/game/cell_location.dart            |  20 +-
 lib/models/game/game.dart                     | 188 ++++++++++++++---
 lib/models/game/snake.dart                    |  50 +++++
 lib/models/settings/settings_game.dart        |  41 ++--
 lib/models/settings/settings_global.dart      |   2 +-
 lib/ui/game/game_bottom.dart                  |   3 +-
 lib/ui/game/game_top.dart                     |  21 +-
 lib/ui/parameters/parameter_painter.dart      | 160 +++++++++++----
 lib/ui/parameters/parameter_widget.dart       |  52 +++--
 lib/ui/widgets/game/cell.dart                 |  50 +++++
 lib/ui/widgets/game/controller_bar.dart       |  43 ++++
 lib/ui/widgets/game/game_board.dart           |  50 ++++-
 pubspec.yaml                                  |   2 +-
 21 files changed, 723 insertions(+), 374 deletions(-)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/19.txt
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/19.txt
 create mode 100644 lib/models/game/snake.dart
 create mode 100644 lib/ui/widgets/game/cell.dart
 create mode 100644 lib/ui/widgets/game/controller_bar.dart

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 0000000..c4767c3
--- /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 0000000..fa0c74f
--- /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 ff81924..d281ea1 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 e77766f..c58edcd 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 8fdb845..88d029b 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 6be0146..949337b 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 b5e73d5..c4cfd2d 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 6077cdc..3ca9cd6 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 faab76a..40a12c8 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 55379bc..f472747 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 0000000..2f8598c
--- /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 dfdcd1e..81a35ce 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 50fd735..92f6ece 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 98e3afa..4c1f5f3 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 78f1b33..abd99b8 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 56d3d7e..5a2a0cc 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 496a4f1..26e506b 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 0000000..c81a87c
--- /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 0000000..6bcb7d5
--- /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 31cb21f..45f2d59 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 c4c00ef..637b555 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"
-- 
GitLab