Skip to content
Snippets Groups Projects
Commit c7a396f4 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Add minimal gameplay

parent 71f43bf3
No related branches found
No related tags found
1 merge request!4Resolve "Set up minimal gameplay"
Pipeline #1758 passed
<?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 x="3.5527e-15" y="-7.1054e-15" width="100" height="100" fill="#696969" fill-opacity=".28943" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.1508"/></svg>
<?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"/></svg>
import 'package:flutter/material.dart';
import '../provider/data.dart';
import '../utils/board_utils.dart';
class Cell {
bool isMined = false;
bool isExplored = false;
bool isMarked = false;
int minesCountAround = 0;
Cell(
@required this.isMined,
);
Container widget(Data myProvider, int row, int col) {
String imageAsset = 'assets/skins/' + myProvider.skin + '_unknown.png';
Color backgroundColor = this.getBackgroundColor();
if (this.isExplored) {
if (this.isMined) {
imageAsset = 'assets/skins/' + myProvider.skin + '_mine.png';
} else {
imageAsset = 'assets/skins/' + myProvider.skin + '_' + this.minesCountAround.toString() + '.png';
}
}
return Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(
color: Colors.grey,
width: 1,
),
),
child: GestureDetector(
child: Image(
image: AssetImage(imageAsset),
fit: BoxFit.fill,
),
onTap: () {
BoardUtils.walkOnCell(myProvider, row, col);
BoardUtils.checkBoardIsSolved(myProvider);
},
),
);
}
Color getBackgroundColor() {
return Colors.white;
}
}
import 'package:flutter/material.dart';
import '../provider/data.dart';
class Board {
static Container buildGameBoard(Data myProvider) {
return Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(2),
border: Border.all(
color: Colors.black,
width: 2,
),
),
child: buildGameTileset(myProvider),
);
}
static Table buildGameTileset(Data myProvider) {
List cells = myProvider.cells;
return Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
for (var row = 0; row < myProvider.sizeVertical; row++)
TableRow(children: [
for (var col = 0; col < myProvider.sizeHorizontal; col++)
Column(children: [
cells[row][col].widget(
myProvider,
row,
col
)
]),
]),
]
);
}
}
import 'package:flutter/material.dart';
import '../layout/board.dart';
import '../provider/data.dart';
import '../utils/game_utils.dart';
class Game {
static Container buildGameWidget(Data myProvider) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Board.buildGameBoard(myProvider),
SizedBox(height: 2),
(myProvider.gameWin || myProvider.gameFail) ? Game.buildEndGameMessage(myProvider) : SizedBox(height: 2),
],
),
);
}
static FlatButton buildRestartGameButton(Data myProvider) {
return FlatButton(
child: Container(
child: Image(
image: AssetImage('assets/icons/button_back.png'),
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.resetGame(myProvider),
);
}
static Container buildEndGameMessage(Data myProvider) {
Image decorationImage = Image(
image: AssetImage(
myProvider.gameWin
? 'assets/icons/game_win.png'
: myProvider.gameFail
? 'assets/icons/game_fail.png'
: ''
),
fit: BoxFit.fill,
);
return Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(2),
child: Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
TableRow(
children: [
Column(children: [ decorationImage ]),
Column(children: [ buildRestartGameButton(myProvider) ]),
Column(children: [ decorationImage ]),
],
),
]
)
);
}
}
import 'package:flutter/material.dart';
import '../provider/data.dart';
import '../utils/game_utils.dart';
class Parameters {
static Container buildParametersSelector(Data myProvider) {
return Container(
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Parameters.buildParameterSelector(myProvider, 'difficulty'),
SizedBox(height: 5),
Parameters.buildParameterSelector(myProvider, 'size'),
SizedBox(height: 5),
Parameters.buildStartGameButton(myProvider),
],
),
);
}
static Container buildStartGameButton(Data myProvider) {
return Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(2),
child: Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
TableRow(
children: [
Column(
children: [
Image(
image: AssetImage('assets/skins/empty.png'),
fit: BoxFit.fill,
),
]
),
Column(
children: [
FlatButton(
child: Container(
child: Image(
image: AssetImage('assets/icons/button_start.png'),
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.startGame(myProvider),
),
]
),
Column(
children: [
Image(
image: AssetImage('assets/skins/empty.png'),
fit: BoxFit.fill,
),
]
),
],
),
]
)
);
}
static Table buildParameterSelector(Data myProvider, String parameterCode) {
List availableValues = myProvider.getParameterAvailableValues(parameterCode);
return Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
TableRow(
children: [
for (var index = 0; index < availableValues.length; index++)
Column(
children: [
_buildParameterButton(myProvider, parameterCode, availableValues[index])
]
),
],
),
],
);
}
static FlatButton _buildParameterButton(Data myProvider, String parameterCode, String parameterValue) {
String currentValue = myProvider.getParameterValue(parameterCode).toString();
bool isActive = (parameterValue == currentValue);
String imageAsset = 'assets/icons/' + parameterCode + '_' + parameterValue + '.png';
return FlatButton(
padding: EdgeInsets.all(2),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isActive ? Colors.blue : Colors.white,
width: 10,
),
),
child: Image(
image: AssetImage(imageAsset),
fit: BoxFit.fill,
),
),
onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'provider/data.dart';
import 'screens/home.dart';
void main() => runApp(MyApp());
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((value) => runApp(MyApp()));
}
class MyApp extends StatelessWidget {
@override
......
......
......@@ -2,4 +2,105 @@ import 'package:flutter/foundation.dart';
class Data extends ChangeNotifier {
// Configuration available values
List _availableDifficultyLevels = ['easy', 'medium', 'hard', 'nightmare'];
List _availableSizes = ['10x10', '15x15', '20x20'];
List get availableDifficultyLevels => _availableDifficultyLevels;
List get availableSizes => _availableSizes;
// Application default configuration
String _level = 'medium';
String _size = '15x15';
String _skin = 'default';
// Game data
bool _stateRunning = false;
bool _gameWin = false;
bool _gameFail = false;
int _sizeVertical = null;
int _sizeHorizontal = null;
List _cells = [];
String get level => _level;
void updateLevel(String level) {
_level = level;
notifyListeners();
}
String get size => _size;
int get sizeVertical => _sizeVertical;
int get sizeHorizontal => _sizeHorizontal;
void updateSize(String size) {
_size = size;
_sizeHorizontal = int.parse(_size.split('x')[0]);
_sizeVertical = int.parse(_size.split('x')[1]);
notifyListeners();
}
String get skin => _skin;
void updateSkin(String skin) {
_skin = skin;
notifyListeners();
}
getParameterValue(String parameterCode) {
switch(parameterCode) {
case 'difficulty': { return _level; }
break;
case 'size': { return _size; }
break;
}
}
List getParameterAvailableValues(String parameterCode) {
switch(parameterCode) {
case 'difficulty': { return _availableDifficultyLevels; }
break;
case 'size': { return _availableSizes; }
break;
}
}
setParameterValue(String parameterCode, String parameterValue) {
switch(parameterCode) {
case 'difficulty': { updateLevel(parameterValue); }
break;
case 'size': { updateSize(parameterValue); }
break;
}
}
List get cells => _cells;
void updateCells(List cells) {
_cells = cells;
notifyListeners();
}
void setCellAsExplored(int row, int col) {
_cells[row][col].isExplored = true;
notifyListeners();
}
bool get stateRunning => _stateRunning;
void updateStateRunning(bool stateRunning) {
_stateRunning = stateRunning;
updateGameWin(false);
updateGameFail(false);
notifyListeners();
}
bool get gameWin => _gameWin;
void updateGameWin(bool gameWin) {
print('updateGameWin: ' + (gameWin ? 'true' : 'false'));
_gameWin = gameWin;
notifyListeners();
}
bool get gameFail => _gameFail;
void updateGameFail(bool gameFail) {
print('updateGameFail: ' + (gameFail ? 'true' : 'false'));
_gameFail = gameFail;
notifyListeners();
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../layout/game.dart';
import '../layout/parameters.dart';
import '../provider/data.dart';
import '../utils/game_utils.dart';
class Home extends StatelessWidget {
static const String id = 'home';
@override
Widget build(BuildContext context) {
Data _myProvider = Provider.of<Data>(context);
Data myProvider = Provider.of<Data>(context);
return Scaffold(
appBar: AppBar(
title: Text('Minehunter game'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.all(4),
padding: EdgeInsets.all(4),
List<Widget> menuActions = [];
if (myProvider.stateRunning) {
menuActions = [
FlatButton(
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.green,
color: Colors.blue,
width: 4,
),
),
child: FlatButton(
child: Text(
'💣',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.w600,
color: Colors.black,
),
margin: EdgeInsets.all(8),
child: Image(
image: AssetImage('assets/icons/button_back.png'),
fit: BoxFit.fill
),
),
onPressed: () => GameUtils.resetGame(myProvider),
),
],
),
),
],
];
}
return Scaffold(
appBar: AppBar(
actions: menuActions,
),
body: SafeArea(
child: Center(
child: myProvider.stateRunning
? Game.buildGameWidget(myProvider)
: Parameters.buildParametersSelector(myProvider)
),
)
);
}
}
import '../entities/cell.dart';
import '../provider/data.dart';
class BoardUtils {
static printGrid(List cells) {
print('');
print('-------');
for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
String row = '';
for (var colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) {
row += cells[rowIndex][colIndex].isMined ? 'X' : '.';
}
print(row);
}
print('-------');
print('');
}
static List createEmptyBoard(int sizeHorizontal, int sizeVertical) {
int index = 0;
List cells = [];
for (var rowIndex = 0; rowIndex < sizeVertical; rowIndex++) {
List row = [];
for (var colIndex = 0; colIndex < sizeHorizontal; colIndex++) {
row.add(Cell(false));
}
cells.add(row);
}
return cells;
}
static int getMinesCount(int sizeHorizontal, int sizeVertical, String level) {
int minesCountRatio = 0;
switch(level) {
case 'easy': {
minesCountRatio = 5;
}
break;
case 'medium': {
minesCountRatio = 10;
}
break;
case 'hard': {
minesCountRatio = 15;
}
break;
case 'nightmare': {
minesCountRatio = 20;
}
break;
}
int minesCount = ((sizeHorizontal * sizeVertical) * minesCountRatio / 100).round();
print('Mines count: ' + minesCount.toString());
return minesCount;
}
static List createBoard(int sizeHorizontal, int sizeVertical, String level) {
List cells = createEmptyBoard(sizeHorizontal, sizeVertical);
// Shuffle cells to put random mines
List cellsArray = [];
for (var row = 0; row < sizeVertical; row++) {
for (var col = 0; col < sizeHorizontal; col++) {
cellsArray.add([row, col]);
}
}
cellsArray.shuffle();
// Put random mines on board
int minesCount = getMinesCount(sizeHorizontal, sizeVertical, level);
for (var mineIndex = 0; mineIndex < minesCount; mineIndex++) {
cells[cellsArray[mineIndex][0]][cellsArray[mineIndex][1]].isMined = true;
}
// Compute all mines counts on cells
for (var row = 0; row < sizeVertical; row++) {
for (var col = 0; col < sizeHorizontal; col++) {
cells[row][col].minesCountAround = getMinesCountAround(cells, row, col);
}
}
printGrid(cells);
return cells;
}
static void walkOnCell(Data myProvider, int row, int col) {
print('Walk on cell [' + col.toString() + ',' + row.toString() + ']');
myProvider.setCellAsExplored(row, col);
if (myProvider.cells[row][col].minesCountAround == 0) {
List safeCells = getAllSafeCellsAround(myProvider.cells, row, col);
for (var safeCellIndex = 0; safeCellIndex < safeCells.length; safeCellIndex++) {
int safeCellRow = safeCells[safeCellIndex][0];
int safeCellCol = safeCells[safeCellIndex][1];
if (!myProvider.cells[safeCellRow][safeCellCol].isExplored) {
walkOnCell(myProvider, safeCellRow, safeCellCol);
}
}
}
}
static List getAllSafeCellsAround(List cells, int row, int col) {
int sizeHorizontal = cells.length;
int sizeVertical = cells[0].length;
List safeCellsCoordinates = [];
if (cells[row][col].minesCountAround == 0) {
for (var deltaRow = -1; deltaRow <= 1; deltaRow++) {
for (var deltaCol = -1; deltaCol <= 1; deltaCol++) {
int candidateRow = row + deltaRow;
int candidateCol = col + deltaCol;
if (
(candidateRow >= 0 && candidateRow < sizeVertical)
&&
(candidateCol >= 0 && candidateCol < sizeHorizontal)
&&
!cells[candidateRow][candidateCol].isExplored
) {
safeCellsCoordinates.add([candidateRow, candidateCol]);
}
}
}
}
return safeCellsCoordinates;
}
static int getMinesCountAround(List cells, int row, int col) {
int sizeHorizontal = cells.length;
int sizeVertical = cells[0].length;
int minesCountAround = 0;
for (var deltaRow = -1; deltaRow <= 1; deltaRow++) {
for (var deltaCol = -1; deltaCol <= 1; deltaCol++) {
if (
(row + deltaRow >= 0 && row + deltaRow < sizeVertical)
&&
(col + deltaCol >= 0 && col + deltaCol < sizeHorizontal)
&&
(cells[row + deltaRow][col + deltaCol].isMined)
) {
minesCountAround++;
}
}
}
return minesCountAround;
}
static bool checkBoardIsSolved(Data myProvider) {
print('checkBoardIsSolved');
List cells = myProvider.cells;
int sizeHorizontal = cells.length;
int sizeVertical = cells[0].length;
myProvider.updateGameWin(false);
myProvider.updateGameFail(false);
for (var row = 0; row < sizeVertical; row++) {
for (var col = 0; col < sizeHorizontal; col++) {
// Walked on a mine
if (cells[row][col].isMined == true && cells[row][col].isExplored == true) {
myProvider.updateGameFail(true);
return false;
}
}
}
for (var row = 0; row < sizeVertical; row++) {
for (var col = 0; col < sizeHorizontal; col++) {
if (
// Mine not already found
(cells[row][col].isMined == true && cells[row][col].isMarked == false)
||
// Safe cell marked as mined
(cells[row][col].isMined == false && cells[row][col].isMarked == true)
) {
return false;
}
}
}
print('-> ok all mines found!');
myProvider.updateGameWin(true);
return true;
}
}
import '../provider/data.dart';
import '../utils/board_utils.dart';
class GameUtils {
static void resetGame(Data myProvider) {
myProvider.updateStateRunning(false);
}
static void startGame(Data myProvider) {
print('Starting game: ' + myProvider.size + ' - ' + myProvider.level);
myProvider.updateSize(myProvider.size);
myProvider.updateStateRunning(true);
myProvider.updateCells(
BoardUtils.createBoard(
myProvider.sizeHorizontal,
myProvider.sizeVertical,
myProvider.level
)
);
}
}
......@@ -7,7 +7,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
version: "2.7.0"
boolean_selector:
dependency: transitive
description:
......@@ -73,7 +73,7 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.4.0"
nested:
dependency: transitive
description:
......@@ -141,7 +141,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
version: "0.4.0"
typed_data:
dependency: transitive
description:
......
......
......@@ -19,3 +19,5 @@ flutter:
uses-material-design: true
assets:
- assets/files/
- assets/icons/
- assets/skins/
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment