Is possible to create a video game in Flutterflow? yes, it can be done throught custom widgets, heres a minesweeper, and I share with you the code, is really easy and I made customizable the parameters like colors, mine image, rows, columns, texts. content...
// Automatic FlutterFlow imports
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!
// Imports for Minesweeper logic
import 'dart:math';
import 'dart:async';
// Data model for a single cell on the board
class _Cell {
bool isMine;
bool isRevealed;
bool isFlagged;
int adjacentMines;
_Cell({
this.isMine = false,
this.isRevealed = false,
this.isFlagged = false,
this.adjacentMines = 0,
});
}
class MinesweeperWidget extends StatefulWidget {
const MinesweeperWidget({
Key? key,
this.width,
this.height,
this.rows,
this.cols,
this.mines,
this.unrevealedColor,
this.revealedColor,
this.flagColor,
this.mineColor,
this.winText,
this.loseText,
this.winTextColor,
this.loseTextColor,
this.mineImagePath,
this.flagImagePath,
}) : super(key: key);
final double? width;
final double? height;
final int? rows;
final int? cols;
final int? mines;
final Color? unrevealedColor;
final Color? revealedColor;
final Color? flagColor;
final Color? mineColor;
final String? winText;
final String? loseText;
final Color? winTextColor;
final Color? loseTextColor;
final String? mineImagePath;
final String? flagImagePath;
@override
_MinesweeperWidgetState createState() => _MinesweeperWidgetState();
}
class _MinesweeperWidgetState extends State<MinesweeperWidget> {
// Game state variables
late List<List<_Cell>> _board;
late int _rows;
late int _cols;
late int _mines;
int _flagsPlaced = 0;
bool _isGameOver = false;
bool _isGameWon = false;
bool _isFirstClick = true;
// Timer variables
Timer? _timer;
int _timeElapsed = 0;
@override
void initState() {
super.initState();
_initializeGame();
}
// Re-initialize game if parameters (like difficulty) change
@override
void didUpdateWidget(covariant MinesweeperWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.rows != oldWidget.rows ||
widget.cols != oldWidget.cols ||
widget.mines != oldWidget.mines) {
_initializeGame();
}
}
@override
void dispose() {
_stopGameTimer();
super.dispose();
}
void _initializeGame() {
_rows = widget.rows ?? 10;
_cols = widget.cols ?? 10;
_mines = widget.mines ?? 10;
// Ensure mines don't exceed available cells
if (_mines >= _rows * _cols) {
_mines = _rows * _cols - 1;
}
_isGameOver = false;
_isGameWon = false;
_isFirstClick = true;
_flagsPlaced = 0;
_timeElapsed = 0;
_stopGameTimer();
// Create the empty board
_board = List.generate(
_rows,
(r) => List.generate(
_cols,
(c) => _Cell(),
),
);
// Refresh the UI
if (mounted) {
setState(() {});
}
}
void _startGameTimer() {
_stopGameTimer(); // Stop any existing timer
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_timeElapsed++;
});
}
});
}
void _stopGameTimer() {
_timer?.cancel();
}
// Place mines *after* the first click to ensure a safe start
void _placeMines(int initialRow, int initialCol) {
int minesToPlace = _mines;
final random = Random();
while (minesToPlace > 0) {
int r = random.nextInt(_rows);
int c = random.nextInt(_cols);
// Don't place a mine on the first click or if one is already there
if (r == initialRow && c == initialCol || _board[r][c].isMine) {
continue;
}
_board[r][c].isMine = true;
minesToPlace--;
}
// After placing mines, calculate the numbers for adjacent cells
_calculateAdjacentMines();
}
void _calculateAdjacentMines() {
for (int r = 0; r < _rows; r++) {
for (int c = 0; c < _cols; c++) {
if (_board[r][c].isMine) continue;
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
int nr = r + i;
int nc = c + j;
if (_isValidCell(nr, nc) && _board[nr][nc].isMine) {
count++;
}
}
}
_board[r][c].adjacentMines = count;
}
}
}
bool _isValidCell(int r, int c) {
return r >= 0 && r < _rows && c >= 0 && c < _cols;
}
void _handleTap(int r, int c) {
if (_isGameOver || _isGameWon || _board[r][c].isFlagged) return;
// Handle the very first click of the game
if (_isFirstClick) {
_isFirstClick = false;
_placeMines(r, c);
_startGameTimer();
}
_revealCell(r, c);
if (_board[r][c].isMine) {
_handleGameOver(false); // Lost
} else {
_checkWinCondition();
}
}
void _handleLongPress(int r, int c) {
if (_isGameOver || _isGameWon || _board[r][c].isRevealed) return;
setState(() {
_board[r][c].isFlagged = !_board[r][c].isFlagged;
_flagsPlaced += _board[r][c].isFlagged ? 1 : -1;
});
_checkWinCondition();
}
// This is the "flood fill" algorithm
void _revealCell(int r, int c) {
if (!_isValidCell(r, c) ||
_board[r][c].isRevealed ||
_board[r][c].isFlagged) {
return;
}
setState(() {
_board[r][c].isRevealed = true;
});
// If this cell is empty (0 adjacent mines), reveal its neighbors
if (_board[r][c].adjacentMines == 0 && !_board[r][c].isMine) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
_revealCell(r + i, c + j);
}
}
}
}
void _checkWinCondition() {
int revealedCount = 0;
for (int r = 0; r < _rows; r++) {
for (int c = 0; c < _cols; c++) {
if (_board[r][c].isRevealed && !_board[r][c].isMine) {
revealedCount++;
}
}
}
if (revealedCount == (_rows * _cols) - _mines) {
_handleGameOver(true); // Won
}
}
void _handleGameOver(bool won) {
_stopGameTimer();
setState(() {
_isGameOver = true;
_isGameWon = won;
// Reveal all mines if the player lost
if (!won) {
for (int r = 0; r < _rows; r++) {
for (int c = 0; c < _cols; c++) {
if (_board[r][c].isMine) {
_board[r][c].isRevealed = true;
}
}
}
}
});
}
// --- Helper para construir imagen ---
// Este widget manejará si la imagen es de red (http) o un asset local
Widget _buildImage(String path) {
if (path.startsWith('http')) {
return Image.network(
path,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.error), // Fallback
);
} else {
// Asume que es un asset de FlutterFlow (ej: 'assets/images/my_mine.png')
return Image.asset(
path,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.error), // Fallback
);
}
}
// --- Build Methods ---
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
child: Column(
children: [
_buildControls(),
Expanded(
child: _buildBoard(),
),
],
),
);
}
Widget _buildControls() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Flag Counter
_buildInfoBox(
icon: Icons.flag,
text: '${_mines - _flagsPlaced}',
),
// Restart Button
IconButton(
icon: Icon(Icons.refresh, color: Colors.blue, size: 30),
onPressed: _initializeGame,
),
// Timer
_buildInfoBox(
icon: Icons.timer,
text: '$_timeElapsed',
),
],
),
);
}
Widget _buildInfoBox({required IconData icon, required String text}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: 20),
SizedBox(width: 8),
Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildBoard() {
return LayoutBuilder(
builder: (context, constraints) {
// Calculate cell size to fit the available space
double cellWidth = constraints.maxWidth / _cols;
double cellHeight = constraints.maxHeight / _rows;
double cellSize = min(cellWidth, cellHeight);
return Center(
child: Container(
width: cellSize * _cols,
height: cellSize * _rows,
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 2),
),
child: Stack(
children: [
GridView.builder(
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _cols,
),
itemCount: _rows * _cols,
itemBuilder: (context, index) {
int r = index ~/ _cols;
int c = index % _cols;
return _buildCell(r, c);
},
),
if (_isGameOver) _buildGameOverOverlay(),
],
),
),
);
},
);
}
Widget _buildCell(int r, int c) {
final cell = _board[r][c];
Widget? child;
if (cell.isRevealed) {
if (cell.isMine) {
// Usa imagen si está disponible, si no, el ícono
child =
(widget.mineImagePath != null && widget.mineImagePath!.isNotEmpty)
? _buildImage(widget.mineImagePath!)
: Icon(Icons.dangerous, color: widget.mineColor ?? Colors.red);
} else if (cell.adjacentMines > 0) {
child = Text(
'${cell.adjacentMines}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getNumberColor(cell.adjacentMines),
fontSize: 18,
),
);
}
} else if (cell.isFlagged) {
// Usa imagen de bandera si está disponible, si no, el ícono
child = (widget.flagImagePath != null && widget.flagImagePath!.isNotEmpty)
? _buildImage(widget.flagImagePath!)
: Icon(Icons.flag, color: widget.flagColor ?? Colors.red);
}
return GestureDetector(
onTap: () => _handleTap(r, c),
onLongPress: () => _handleLongPress(r, c),
child: Container(
decoration: BoxDecoration(
color: cell.isRevealed
? (widget.revealedColor ?? Colors.grey[300]) // Color revelado
: (widget.unrevealedColor ?? Colors.grey[400]), // Color oculto
border: Border.all(color: Colors.grey[500]!, width: 0.5),
),
child: Center(child: child),
),
);
}
Widget _buildGameOverOverlay() {
final bool isWin = _isGameWon;
final String text = isWin
? (widget.winText ?? 'You Win!')
: (widget.loseText ?? 'Game Over');
final Color color = isWin
? (widget.winTextColor ?? Colors.greenAccent)
: (widget.loseTextColor ?? Colors.redAccent);
return Container(
color: Colors.black.withOpacity(0.5),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
text,
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: color,
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _initializeGame,
child: Text('Play Again', style: TextStyle(fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
),
);
}
// Helper to get classic Minesweeper number colors
Color _getNumberColor(int number) {
switch (number) {
case 1:
return Colors.blue;
case 2:
return Colors.green;
case 3:
return Colors.red;
case 4:
return Colors.purple;
case 5:
return Colors.orange;
case 6:
return Colors.teal;
case 7:
return Colors.black;
case 8:
return Colors.grey;
default:
return Colors.black;
}
}
}
// DO NOT REMOVE OR MODIFY THE CODE BELOW!