Doing video games in Flutterflow: Minesweeper (buscaminas) 💣

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!
Screen Recording 2025-10-30 at 10.17.44.mov
1.41MB


3
1 reply