Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ A functional and minimalist mobile Minesweeper game built with Flutter, working
- Long-press to flag suspected mines
- Auto-reveal adjacent cells when clicking empty cells
- Numbers indicate adjacent mine count
- **Hint System**: Get help with 3 hints per game that reveal safe cells
- **UI Features**:
- Mine counter showing remaining mines
- Timer tracking game duration
- Hint button with counter (💡) - reveals a safe cell with visual feedback
- Reset button with game state indicator (😊/😄/☹️)
- Clean, minimalist design
- Responsive layout for different screen sizes
Expand Down Expand Up @@ -67,10 +69,14 @@ lib/
1. **Start a Game**: Tap any cell to begin
2. **Reveal Cells**: Tap on cells to reveal them
3. **Flag Mines**: Long-press on cells to flag suspected mines
4. **Win**: Reveal all non-mine cells
5. **Lose**: Reveal a mine
6. **Reset**: Tap the face button to start a new game
7. **Change Difficulty**: Tap the settings icon to select a different difficulty level
4. **Use Hints**: Tap the lightbulb button (💡) to reveal a safe cell (3 hints per game)
- Hints prioritize cells that will reveal larger areas
- Hint-revealed cells are highlighted with an amber glow
- Hints are only available after the game starts
5. **Win**: Reveal all non-mine cells
6. **Lose**: Reveal a mine
7. **Reset**: Tap the face button to start a new game
8. **Change Difficulty**: Tap the settings icon to select a different difficulty level

## Technical Details

Expand Down
56 changes: 56 additions & 0 deletions lib/controllers/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ class GameController extends ChangeNotifier {
int _elapsedSeconds = 0;
Timer? _timer;
bool _firstClick = true;
int _hintsRemaining = 3;
static const int _maxHints = 3;

Difficulty get difficulty => _difficulty;
List<List<Cell>> get board => _board;
GameState get gameState => _gameState;
int get flagCount => _flagCount;
int get remainingMines => _difficulty.mines - _flagCount;
int get elapsedSeconds => _elapsedSeconds;
int get hintsRemaining => _hintsRemaining;
bool get canUseHint => _hintsRemaining > 0 &&
_gameState == GameState.playing &&
!_firstClick;

GameController() {
_initializeBoard();
Expand Down Expand Up @@ -173,6 +179,55 @@ class GameController extends ChangeNotifier {
}
}

void useHint() {
if (!canUseHint) return;

final safeCells = _findSafeCells();
if (safeCells.isEmpty) return;

Cell? bestCell = _selectBestHintCell(safeCells);
if (bestCell != null) {
_hintsRemaining--;
bestCell.isHintRevealed = true;

Future.delayed(const Duration(milliseconds: 1500), () {
if (bestCell.isRevealed) {
bestCell.isHintRevealed = false;
notifyListeners();
}
});

revealCell(bestCell.row, bestCell.col);
}
}

List<Cell> _findSafeCells() {
final safeCells = <Cell>[];
for (var row in _board) {
for (var cell in row) {
if (!cell.isMine && !cell.isRevealed && !cell.isFlagged) {
safeCells.add(cell);
}
}
}
return safeCells;
}

Cell? _selectBestHintCell(List<Cell> safeCells) {
if (safeCells.isEmpty) return null;

final zeroCells = safeCells.where((cell) => cell.adjacentMines == 0).toList();
if (zeroCells.isNotEmpty) {
return zeroCells[Random().nextInt(zeroCells.length)];
}

safeCells.sort((a, b) => a.adjacentMines.compareTo(b.adjacentMines));
final minAdjacentMines = safeCells.first.adjacentMines;
final bestCells = safeCells.where((cell) => cell.adjacentMines == minAdjacentMines).toList();

return bestCells[Random().nextInt(bestCells.length)];
}

void _startTimer() {
_timer?.cancel();
_elapsedSeconds = 0;
Expand All @@ -193,6 +248,7 @@ class GameController extends ChangeNotifier {
_revealedCount = 0;
_elapsedSeconds = 0;
_firstClick = true;
_hintsRemaining = _maxHints;
_initializeBoard();
notifyListeners();
}
Expand Down
3 changes: 3 additions & 0 deletions lib/models/cell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Cell {
bool isMine;
bool isRevealed;
bool isFlagged;
bool isHintRevealed;
int adjacentMines;

Cell({
Expand All @@ -12,13 +13,15 @@ class Cell {
this.isMine = false,
this.isRevealed = false,
this.isFlagged = false,
this.isHintRevealed = false,
this.adjacentMines = 0,
});

void reset() {
isMine = false;
isRevealed = false;
isFlagged = false;
isHintRevealed = false;
adjacentMines = 0;
}
}
48 changes: 48 additions & 0 deletions lib/screens/game_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,54 @@ class GameScreen extends StatelessWidget {
],
),
),
Container(
key: const Key('hint_button_container'),
decoration: BoxDecoration(
color: controller.canUseHint
? Colors.amber[700]
: Colors.grey[600],
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: controller.canUseHint
? controller.useHint
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lightbulb,
color: controller.canUseHint
? Colors.white
: Colors.grey[400],
size: 20,
),
const SizedBox(width: 8),
Text(
'${controller.hintsRemaining}',
key: const Key('hint_counter_text'),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: controller.canUseHint
? Colors.white
: Colors.grey[400],
),
),
],
),
),
),
),
),
IconButton(
key: const Key('reset_button'),
icon: Icon(
Expand Down
31 changes: 25 additions & 6 deletions lib/widgets/cell_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,42 @@ class CellWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
Color cellColor;
if (cell.isHintRevealed) {
cellColor = Colors.amber[200]!;
} else if (cell.isRevealed) {
cellColor = cell.isMine ? Colors.red[300]! : Colors.grey[300]!;
} else {
cellColor = Colors.grey[400]!;
}

return GestureDetector(
key: key,
onTap: onTap,
onLongPress: onLongPress,
child: Container(
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: size,
height: size,
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: cell.isRevealed
? (cell.isMine ? Colors.red[300] : Colors.grey[300])
: Colors.grey[400],
color: cellColor,
border: Border.all(
color: cell.isRevealed ? Colors.grey[400]! : Colors.grey[600]!,
width: 1,
color: cell.isHintRevealed
? Colors.amber[700]!
: (cell.isRevealed ? Colors.grey[400]! : Colors.grey[600]!),
width: cell.isHintRevealed ? 2 : 1,
),
borderRadius: BorderRadius.circular(2),
boxShadow: cell.isHintRevealed
? [
BoxShadow(
color: Colors.amber.withOpacity(0.5),
blurRadius: 8,
spreadRadius: 2,
),
]
: null,
),
child: Center(child: _buildCellContent()),
),
Expand Down
16 changes: 8 additions & 8 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
Expand Down Expand Up @@ -111,18 +111,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
Expand Down Expand Up @@ -204,10 +204,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.8"
vector_math:
dependency: transitive
description:
Expand Down
Loading