From 744cef3d3c006cd5ae7b0a11299e0f3080912dde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Thu, 2 Jun 2022 10:43:49 +0200
Subject: [PATCH] Create minimal playable game

---
 android/gradle.properties  |   4 +-
 assets/skins/default_0.png | Bin 0 -> 251 bytes
 assets/skins/images_0.png  | Bin 0 -> 251 bytes
 icons/build_game_icons.sh  |   1 +
 icons/skins/default/0.svg  |   2 +
 icons/skins/images/0.svg   |   2 +
 lib/entities/cell.dart     |  62 ++++++++++++++++++++
 lib/layout/board.dart      |  41 ++++++++++++++
 lib/layout/game.dart       |  39 +++++++++++--
 lib/main.dart              |   3 +-
 lib/provider/data.dart     |  63 ++++++++++++++++++++-
 lib/utils/board_utils.dart | 113 +++++++++++++++++++++++++++++++++++++
 lib/utils/game_utils.dart  |   5 +-
 pubspec.yaml               |   1 +
 14 files changed, 327 insertions(+), 9 deletions(-)
 create mode 100644 assets/skins/default_0.png
 create mode 100644 assets/skins/images_0.png
 create mode 100644 icons/skins/default/0.svg
 create mode 100644 icons/skins/images/0.svg
 create mode 100644 lib/entities/cell.dart
 create mode 100644 lib/layout/board.dart
 create mode 100644 lib/utils/board_utils.dart

diff --git a/android/gradle.properties b/android/gradle.properties
index bc2d95e..818e87b 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.0.1
-app.versionCode=1
+app.versionName=0.0.2
+app.versionCode=2
diff --git a/assets/skins/default_0.png b/assets/skins/default_0.png
new file mode 100644
index 0000000000000000000000000000000000000000..604bda78fcc44f91482257e4580b5d84dd1cbfd9
GIT binary patch
literal 251
zcmeAS@N?(olHy`uVBq!ia0y~yU^oE6EX)iH3=2LLnlLaha29w(7Beu&M1U~k*%{Bz
zFfcGkmbgZg1m~xflqVLYGL)B>>t*I;7bhncr0V4trO$q6BgMeLz#rff;u;VTP+woa
zV#SKJYuEn&|6kZE?L7knL$0TbV@SoEw|5vh859Iq4!%xc<?i4;@^7Zk#|MXV8Z$R|
t{y(OdqdIf@29Qe_$OH}l8$NCI{VrF3-f*|Q-ODbJF`lk|F6*2UngC<sMK}Ne

literal 0
HcmV?d00001

diff --git a/assets/skins/images_0.png b/assets/skins/images_0.png
new file mode 100644
index 0000000000000000000000000000000000000000..604bda78fcc44f91482257e4580b5d84dd1cbfd9
GIT binary patch
literal 251
zcmeAS@N?(olHy`uVBq!ia0y~yU^oE6EX)iH3=2LLnlLaha29w(7Beu&M1U~k*%{Bz
zFfcGkmbgZg1m~xflqVLYGL)B>>t*I;7bhncr0V4trO$q6BgMeLz#rff;u;VTP+woa
zV#SKJYuEn&|6kZE?L7knL$0TbV@SoEw|5vh859Iq4!%xc<?i4;@^7Zk#|MXV8Z$R|
t{y(OdqdIf@29Qe_$OH}l8$NCI{VrF3-f*|Q-ODbJF`lk|F6*2UngC<sMK}Ne

literal 0
HcmV?d00001

diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh
index 52ab3a2..9877717 100755
--- a/icons/build_game_icons.sh
+++ b/icons/build_game_icons.sh
@@ -33,6 +33,7 @@ AVAILABLE_SKINS="
 
 # Images per skin
 SKIN_IMAGES="
+  0
   1
   2
   3
diff --git a/icons/skins/default/0.svg b/icons/skins/default/0.svg
new file mode 100644
index 0000000..b69eb37
--- /dev/null
+++ b/icons/skins/default/0.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><rect width="100" height="100" fill="#fff" stroke="#505050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.6"/></svg>
diff --git a/icons/skins/images/0.svg b/icons/skins/images/0.svg
new file mode 100644
index 0000000..b69eb37
--- /dev/null
+++ b/icons/skins/images/0.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><rect width="100" height="100" fill="#fff" stroke="#505050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.6"/></svg>
diff --git a/lib/entities/cell.dart b/lib/entities/cell.dart
new file mode 100644
index 0000000..57d927a
--- /dev/null
+++ b/lib/entities/cell.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+
+import '../provider/data.dart';
+import '../utils/board_utils.dart';
+
+class Cell {
+  int value;
+
+  Cell(
+    @required this.value,
+  );
+
+  Container widget(Data myProvider, int row, int col) {
+    String imageAsset = this.getImageAssetName(myProvider);
+
+    return Container(
+      child: AnimatedSwitcher(
+        duration: const Duration(milliseconds: 100),
+        transitionBuilder: (Widget child, Animation<double> animation) {
+          return ScaleTransition(child: child, scale: animation);
+        },
+        child: Image(
+          image: AssetImage(imageAsset),
+          fit: BoxFit.fill,
+          key: ValueKey<int>(imageAsset.hashCode),
+        ),
+      ),
+    );
+  }
+
+  Container widgetFillBoardWithColor(Data myProvider) {
+    String imageAsset = this.getImageAssetName(myProvider);
+
+    return Container(
+      margin: EdgeInsets.all(2),
+      decoration: BoxDecoration(
+        border: Border.all(
+          color: Colors.black,
+          width: 4,
+        ),
+      ),
+      child: GestureDetector(
+        child: Image(
+          image: AssetImage(imageAsset),
+          fit: BoxFit.fill
+        ),
+        onTap: () {
+          BoardUtils.fillBoardFromFirstCell(myProvider, this.value);
+          if (BoardUtils.checkBoardIsSolved(myProvider)) {
+            myProvider.updateGameWon(true);
+          }
+        },
+      )
+    );
+  }
+
+  String getImageAssetName(Data myProvider) {
+    int cellValue = this.value;
+    return 'assets/skins/' + myProvider.skin + '_' + cellValue.toString() + '.png';
+  }
+
+}
diff --git a/lib/layout/board.dart b/lib/layout/board.dart
new file mode 100644
index 0000000..f8ae151
--- /dev/null
+++ b/lib/layout/board.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+
+import '../provider/data.dart';
+
+class Board {
+
+  static Container buildGameBoard(Data myProvider) {
+    return Container(
+      margin: EdgeInsets.all(4),
+      padding: EdgeInsets.all(4),
+      child: Column(
+        children: [
+          buildGameTileset(myProvider),
+        ],
+      ),
+    );
+  }
+
+  static Table buildGameTileset(Data myProvider) {
+    int boardSize = myProvider.boardSize;
+    List cells = myProvider.cells;
+
+    return Table(
+      defaultColumnWidth: IntrinsicColumnWidth(),
+      children: [
+        for (var row = 0; row < boardSize; row++)
+          TableRow(children: [
+            for (var col = 0; col < boardSize; col++)
+              Column(children: [
+                cells[row][col].widget(
+                  myProvider,
+                  row,
+                  col
+                )
+              ]),
+          ]),
+      ]
+    );
+  }
+
+}
diff --git a/lib/layout/game.dart b/lib/layout/game.dart
index f479032..b2720f6 100644
--- a/lib/layout/game.dart
+++ b/lib/layout/game.dart
@@ -2,6 +2,8 @@ import 'dart:math';
 
 import 'package:flutter/material.dart';
 
+import '../entities/cell.dart';
+import '../layout/board.dart';
 import '../provider/data.dart';
 import '../utils/game_utils.dart';
 
@@ -16,21 +18,50 @@ class Game {
         crossAxisAlignment: CrossAxisAlignment.center,
         children: [
           Expanded(
-            child: Text('❇️'),
+            child: Board.buildGameBoard(myProvider),
           ),
           SizedBox(height: 2),
           Container(
-            height: 150,
-            width: double.maxFinite,
             child: gameIsFinished
               ? Game.buildEndGameMessage(myProvider)
-              : Text('❇️'),
+              : Game.buildSelectColorBar(myProvider),
           ),
         ],
       ),
     );
   }
 
+  static Container buildSelectColorBar(Data myProvider) {
+    List cells = myProvider.cells;
+
+    int maxValue = myProvider.colorsCount;
+
+    return Container(
+      margin: EdgeInsets.all(2),
+      padding: EdgeInsets.all(2),
+
+      child: Table(
+        defaultColumnWidth: IntrinsicColumnWidth(),
+        children: [
+          TableRow(
+            children: [
+              for (
+                int value = 1;
+                value <= maxValue;
+                value++
+              )
+                Column(
+                  children: [
+                    Cell(value).widgetFillBoardWithColor(myProvider)
+                  ]
+                ),
+            ]
+          ),
+        ]
+      ),
+    );
+  }
+
   static FlatButton buildRestartGameButton(Data myProvider) {
     return FlatButton(
       child: Container(
diff --git a/lib/main.dart b/lib/main.dart
index f4e8d27..7f44866 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -29,7 +29,8 @@ class MyApp extends StatelessWidget {
             routes: {
               Home.id: (context) => Home(),
             },
-          ));
+          ),
+        );
       }),
     );
   }
diff --git a/lib/provider/data.dart b/lib/provider/data.dart
index cd696e5..c421301 100644
--- a/lib/provider/data.dart
+++ b/lib/provider/data.dart
@@ -19,10 +19,17 @@ class Data extends ChangeNotifier {
   // Game data
   bool _gameIsRunning = false;
   bool _gameWon = false;
+  int _boardSize = 0;
+  int _colorsCount = 0;
+  List _cells = [];
 
   String get level => _level;
   void updateLevel(String level) {
     _level = level;
+
+    updateBoardSize(getBoardSizeFromLevel(level));
+    updateColorsCount(getColorsCountFromLevel(level));
+
     notifyListeners();
   }
 
@@ -39,6 +46,7 @@ class Data extends ChangeNotifier {
       case 'skin': { return _skin; }
       break;
     }
+    return '';
   }
 
   List getParameterAvailableValues(String parameterCode) {
@@ -68,6 +76,55 @@ class Data extends ChangeNotifier {
     setParameterValue('skin', prefs.getString('skin') ?? _skinDefault);
   }
 
+  int get boardSize => _boardSize;
+  void updateBoardSize(int boardSize) {
+    _boardSize = boardSize;
+  }
+
+  int get colorsCount => _colorsCount;
+  void updateColorsCount(int colorsCount) {
+    _colorsCount = colorsCount;
+  }
+
+  int getBoardSizeFromLevel(String level) {
+    switch(level) {
+      case 'easy': { return 6; }
+      break;
+      case 'normal': { return 10; }
+      break;
+      case 'hard': { return 14; }
+      break;
+      case 'nightmare': { return 20; }
+      break;
+    }
+    return 8;
+  }
+
+  int getColorsCountFromLevel(String level) {
+    switch(level) {
+      case 'easy': { return 4; }
+      break;
+      case 'normal': { return 5; }
+      break;
+      case 'hard': { return 6; }
+      break;
+      case 'nightmare': { return 7; }
+      break;
+    }
+    return 4;
+  }
+
+  List get cells => _cells;
+  void updateCells(List cells) {
+    _cells = cells;
+    notifyListeners();
+  }
+
+  updateCellValue(int col, int row, int value) {
+    _cells[row][col].value = value;
+    notifyListeners();
+  }
+
   bool get gameIsRunning => _gameIsRunning;
   void updateGameIsRunning(bool gameIsRunning) {
     _gameIsRunning = gameIsRunning;
@@ -75,10 +132,14 @@ class Data extends ChangeNotifier {
   }
 
   bool isGameFinished() {
-    return false;
+    return _gameWon;
   }
 
   bool get gameWon => _gameWon;
+  void updateGameWon(bool gameWon) {
+    _gameWon = gameWon;
+    notifyListeners();
+  }
 
   void resetGame() {
     _gameIsRunning = false;
diff --git a/lib/utils/board_utils.dart b/lib/utils/board_utils.dart
new file mode 100644
index 0000000..61848e3
--- /dev/null
+++ b/lib/utils/board_utils.dart
@@ -0,0 +1,113 @@
+import 'dart:math';
+
+import '../entities/cell.dart';
+import '../provider/data.dart';
+
+class BoardUtils {
+
+  static printGrid(List cells) {
+    String stringValues = '01234567';
+    print('');
+    print('-------');
+    for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
+      String row = '';
+      for (var colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
+        row += stringValues[cells[rowIndex][colIndex].value];
+      }
+      print(row);
+    }
+    print('-------');
+    print('');
+  }
+
+  static createNewBoard(Data myProvider) {
+    int boardSize = myProvider.boardSize;
+    int maxValue = myProvider.colorsCount;
+
+    var rand = new Random();
+
+    List grid = [];
+    for (var rowIndex = 0; rowIndex < boardSize; rowIndex++) {
+      List row = [];
+      for (var colIndex = 0; colIndex < boardSize; colIndex++) {
+        int value = 1 + rand.nextInt(maxValue);
+        row.add(Cell(value));
+      }
+      grid.add(row);
+    }
+    printGrid(grid);
+
+    myProvider.resetGame();
+    myProvider.updateCells(grid);
+  }
+
+  static fillBoardFromFirstCell(Data myProvider, int value) {
+    List cellsToFill = BoardUtils.getSiblingFillableCells(myProvider, 0, 0, [[0, 0]]);
+
+    for (var cellIndex = 0; cellIndex < cellsToFill.length; cellIndex++) {
+      myProvider.updateCellValue(cellsToFill[cellIndex][1], cellsToFill[cellIndex][0], value);
+    }
+  }
+
+  static List getSiblingFillableCells(Data myProvider, row, col, siblingCells) {
+    List cells = myProvider.cells;
+    int boardSize = myProvider.boardSize;
+
+    int referenceValue = cells[row][col].value;
+
+    for (var deltaRow = -1; deltaRow <= 1; deltaRow++) {
+      for (var deltaCol = -1; deltaCol <= 1; deltaCol++) {
+        if (deltaCol == 0 || deltaRow == 0) {
+          int candidateRow = row + deltaRow;
+          int candidateCol = col + deltaCol;
+
+          if (
+            (candidateRow >= 0 && candidateRow < boardSize)
+            &&
+            (candidateCol >= 0 && candidateCol < boardSize)
+          ) {
+            if (cells[candidateRow][candidateCol].value == referenceValue) {
+              bool alreadyFound = false;
+              for (var index = 0; index < siblingCells.length; index++) {
+                if (
+                  (siblingCells[index][0] == candidateRow)
+                  &&
+                  (siblingCells[index][1] == candidateCol)
+                ) {
+                  alreadyFound = true;
+                }
+              }
+              if (!alreadyFound) {
+                siblingCells.add([candidateRow, candidateCol]);
+                siblingCells = getSiblingFillableCells(myProvider, candidateRow, candidateCol, siblingCells);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    return siblingCells;
+  }
+
+  static bool checkBoardIsSolved(Data myProvider) {
+    List cells = myProvider.cells;
+    int boardSize = myProvider.boardSize;
+
+    // check grid is fully completed and does not contain conflict
+    int previousValue = cells[0][0].value;
+    for (var row = 0; row < boardSize; row++) {
+      for (var col = 0; col < boardSize; col++) {
+        if (cells[row][col].value == 0 || cells[row][col].value != previousValue) {
+          return false;
+        }
+        previousValue = cells[row][col].value;
+      }
+    }
+
+    print('-> ok grid solved!');
+
+    return true;
+  }
+
+}
diff --git a/lib/utils/game_utils.dart b/lib/utils/game_utils.dart
index 58e9792..a7005ec 100644
--- a/lib/utils/game_utils.dart
+++ b/lib/utils/game_utils.dart
@@ -1,4 +1,5 @@
 import '../provider/data.dart';
+import '../utils/board_utils.dart';
 
 class GameUtils {
 
@@ -9,8 +10,10 @@ class GameUtils {
   static Future<void> startGame(Data myProvider) async {
     print('Starting game');
     print('- level: ' + myProvider.level);
+    print('- size: ' + myProvider.boardSize.toString());
+    print('- colors: ' + myProvider.colorsCount.toString());
 
-    myProvider.resetGame();
+    BoardUtils.createNewBoard(myProvider);
 
     myProvider.updateGameIsRunning(true);
   }
diff --git a/pubspec.yaml b/pubspec.yaml
index 56bfd6d..c16bdce 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -21,3 +21,4 @@ flutter:
   uses-material-design: true
   assets:
     - assets/icons/
+    - assets/skins/
-- 
GitLab