← Назад к вопросам

Реализовать простую игру Memory (найди пару)

1.7 Middle🔥 61 комментариев
#Flutter виджеты#State Management#Анимации

Условие

Создайте игру на запоминание, где нужно находить пары одинаковых карточек.

Требования

  1. Сетка карточек 4x4 (8 пар)
  2. Карточки перевёрнуты рубашкой вверх
  3. При нажатии карточка переворачивается
  4. Если открыты 2 одинаковые — они остаются открытыми
  5. Если разные — переворачиваются обратно через 1 секунду
  6. Счётчик ходов и времени
  7. Победа при нахождении всех пар

Дополнительные баллы

  • Выбор уровня сложности (3x4, 4x4, 4x5)
  • Таблица лучших результатов
  • Анимация переворота карточек
  • Разные темы карточек (животные, еда, флаги)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Игра Memory на Flutter

Полнофункциональная игра на запоминание с уровнями сложности, анимациями и таблицей результатов.

Модели данных

enum GameDifficulty { easy, medium, hard }
enum CardTheme { animals, food, flags }

class Card {
  final int id;
  final String symbol;
  final bool isFlipped;
  final bool isMatched;

  Card({
    required this.id,
    required this.symbol,
    this.isFlipped = false,
    this.isMatched = false,
  });

  Card copyWith({
    int? id,
    String? symbol,
    bool? isFlipped,
    bool? isMatched,
  }) {
    return Card(
      id: id ?? this.id,
      symbol: symbol ?? this.symbol,
      isFlipped: isFlipped ?? this.isFlipped,
      isMatched: isMatched ?? this.isMatched,
    );
  }
}

class GameResult {
  final int moves;
  final int time;
  final GameDifficulty difficulty;
  final DateTime timestamp;

  GameResult({
    required this.moves,
    required this.time,
    required this.difficulty,
    required this.timestamp,
  });
}

Контроллер игры

class GameController extends GetxController {
  final cards = <Card>[].obs;
  final moves = 0.obs;
  final time = 0.obs;
  final isGameActive = false.obs;
  final gameResults = <GameResult>[].obs;
  
  late Timer gameTimer;
  late GameDifficulty currentDifficulty;
  late CardTheme currentTheme;
  
  int? firstFlippedIndex;
  int? secondFlippedIndex;

  final animalSymbols = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];
  final foodSymbols = ['🍎', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍒'];
  final flagSymbols = ['🇺🇸', '🇬🇧', '🇫🇷', '🇩🇪', '🇮🇹', '🇪🇸', '🇯🇵', '🇰🇷'];

  void initGame(GameDifficulty difficulty, CardTheme theme) {
    currentDifficulty = difficulty;
    currentTheme = theme;
    
    moves.value = 0;
    time.value = 0;
    firstFlippedIndex = null;
    secondFlippedIndex = null;
    
    final symbols = getSymbolsByTheme(theme);
    final gridSize = getGridSize(difficulty);
    final pairCount = (gridSize.width * gridSize.height) ~/ 2;
    
    final gameSymbols = symbols.take(pairCount).toList();
    final allCards = [...gameSymbols, ...gameSymbols]..shuffle();
    
    cards.assignAll(
      allCards.asMap().entries.map((entry) {
        return Card(id: entry.key, symbol: entry.value);
      }).toList(),
    );
    
    isGameActive.value = true;
    _startTimer();
  }

  void _startTimer() {
    gameTimer = Timer.periodic(Duration(seconds: 1), (_) {
      time.value++;
    });
  }

  void flipCard(int index) {
    if (!isGameActive.value || cards[index].isMatched || cards[index].isFlipped) {
      return;
    }

    cards[index] = cards[index].copyWith(isFlipped: true);

    if (firstFlippedIndex == null) {
      firstFlippedIndex = index;
    } else if (secondFlippedIndex == null) {
      secondFlippedIndex = index;
      moves.value++;
      
      Future.delayed(Duration(milliseconds: 500), () {
        _checkMatch();
      });
    }
  }

  void _checkMatch() {
    final firstCard = cards[firstFlippedIndex!];
    final secondCard = cards[secondFlippedIndex!];

    if (firstCard.symbol == secondCard.symbol) {
      // Совпадение!
      cards[firstFlippedIndex!] = cards[firstFlippedIndex!].copyWith(isMatched: true);
      cards[secondFlippedIndex!] = cards[secondFlippedIndex!].copyWith(isMatched: true);
      
      firstFlippedIndex = null;
      secondFlippedIndex = null;
      
      if (cards.every((card) => card.isMatched)) {
        _gameWon();
      }
    } else {
      // Не совпадают
      cards[firstFlippedIndex!] = cards[firstFlippedIndex!].copyWith(isFlipped: false);
      cards[secondFlippedIndex!] = cards[secondFlippedIndex!].copyWith(isFlipped: false);
      
      firstFlippedIndex = null;
      secondFlippedIndex = null;
    }
  }

  void _gameWon() {
    isGameActive.value = false;
    gameTimer.cancel();
    
    final result = GameResult(
      moves: moves.value,
      time: time.value,
      difficulty: currentDifficulty,
      timestamp: DateTime.now(),
    );
    
    gameResults.add(result);
    _saveResults();
  }

  void resetGame() {
    gameTimer.cancel();
    initGame(currentDifficulty, currentTheme);
  }

  List<String> getSymbolsByTheme(CardTheme theme) {
    switch (theme) {
      case CardTheme.animals:
        return animalSymbols;
      case CardTheme.food:
        return foodSymbols;
      case CardTheme.flags:
        return flagSymbols;
    }
  }

  ({int width, int height}) getGridSize(GameDifficulty difficulty) {
    switch (difficulty) {
      case GameDifficulty.easy:
        return (width: 3, height: 4);
      case GameDifficulty.medium:
        return (width: 4, height: 4);
      case GameDifficulty.hard:
        return (width: 4, height: 5);
    }
  }

  Future<void> _saveResults() async {
    final prefs = await SharedPreferences.getInstance();
    final resultsJson = gameResults
        .map((r) => jsonEncode({
          'moves': r.moves,
          'time': r.time,
          'difficulty': r.difficulty.toString(),
          'timestamp': r.timestamp.toIso8601String(),
        }))
        .toList();
    await prefs.setStringList('game_results', resultsJson);
  }

  @override
  void onClose() {
    gameTimer.cancel();
    super.onClose();
  }
}

Главная страница игры

class GamePage extends StatefulWidget {
  @override
  _GamePageState createState() => _GamePageState();
}

class _GamePageState extends State<GamePage> with TickerProviderStateMixin {
  final GameController _controller = Get.put(GameController());
  late AnimationController _flipController;

  @override
  void initState() {
    super.initState();
    _flipController = AnimationController(
      duration: Duration(milliseconds: 600),
      vsync: this,
    );
    _showDifficultyDialog();
  }

  void _showDifficultyDialog() {
    Get.dialog(
      AlertDialog(
        title: Text('Выберите уровень'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              onPressed: () {
                _controller.initGame(GameDifficulty.easy, CardTheme.animals);
                Get.back();
              },
              child: Text('Лёгкий (3x4)'),
            ),
            SizedBox(height: 8),
            ElevatedButton(
              onPressed: () {
                _controller.initGame(GameDifficulty.medium, CardTheme.animals);
                Get.back();
              },
              child: Text('Средний (4x4)'),
            ),
            SizedBox(height: 8),
            ElevatedButton(
              onPressed: () {
                _controller.initGame(GameDifficulty.hard, CardTheme.animals);
                Get.back();
              },
              child: Text('Сложный (4x5)'),
            ),
          ],
        ),
      ),
      barrierDismissible: false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Memory Game'),
        elevation: 0,
        backgroundColor: Color(0xFF667EEA),
      ),
      body: Obx(() {
        if (!_controller.isGameActive.value && _controller.cards.isEmpty) {
          return Center(child: CircularProgressIndicator());
        }

        return SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              children: [
                _buildStats(),
                SizedBox(height: 24),
                _buildGameGrid(),
                SizedBox(height: 24),
                _buildButtons(),
              ],
            ),
          ),
        );
      }),
    );
  }

  Widget _buildStats() {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Column(
              children: [
                Text('Ходов', style: TextStyle(fontSize: 12, color: Colors.grey)),
                Obx(() => Text(
                  _controller.moves.value.toString(),
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                )),
              ],
            ),
            Column(
              children: [
                Text('Время', style: TextStyle(fontSize: 12, color: Colors.grey)),
                Obx(() => Text(
                  '${_controller.time.value}s',
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                )),
              ],
            ),
            Column(
              children: [
                Text('Пар найдено', style: TextStyle(fontSize: 12, color: Colors.grey)),
                Obx(() {
                  final matched = _controller.cards.where((c) => c.isMatched).length;
                  final total = _controller.cards.length ~/ 2;
                  return Text(
                    '$matched/$total',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  );
                }),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildGameGrid() {
    final gridSize = _controller.getGridSize(_controller.currentDifficulty);
    return GridView.builder(
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: gridSize.width,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      itemCount: _controller.cards.length,
      itemBuilder: (context, index) => _buildCard(index),
    );
  }

  Widget _buildCard(int index) {
    return Obx(() {
      final card = _controller.cards[index];
      return GestureDetector(
        onTap: () => _controller.flipCard(index),
        child: AnimatedBuilder(
          animation: _flipController,
          builder: (context, child) {
            final angle = _flipController.value * 3.14159;
            final isFlipped = card.isFlipped || card.isMatched;
            
            return Transform(
              alignment: Alignment.center,
              transform: Matrix4.identity()
                ..setEntry(3, 2, 0.001)
                ..rotateY(isFlipped ? angle : 0),
              child: Container(
                decoration: BoxDecoration(
                  color: isFlipped ? Colors.white : Color(0xFF667EEA),
                  borderRadius: BorderRadius.circular(8),
                  boxShadow: [
                    BoxShadow(
                      color: Colors.black.withOpacity(0.1),
                      blurRadius: 4,
                    ),
                  ],
                ),
                child: Center(
                  child: isFlipped
                      ? Text(card.symbol, style: TextStyle(fontSize: 32))
                      : Icon(Icons.help, color: Colors.white, size: 32),
                ),
              ),
            );
          },
        ),
      );
    });
  }

  Widget _buildButtons() {
    return Row(
      children: [
        Expanded(
          child: ElevatedButton(
            onPressed: _controller.resetGame,
            child: Text('Заново'),
          ),
        ),
        SizedBox(width: 8),
        Expanded(
          child: ElevatedButton(
            onPressed: () => Get.back(),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.grey[400],
            ),
            child: Text('Назад'),
          ),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _flipController.dispose();
    super.dispose();
  }
}

Dependencies в pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  get: ^4.6.5
  shared_preferences: ^2.2.0

Ключевые особенности

  • Три уровня сложности — 3x4, 4x4, 4x5
  • Три темы карточек — животные, еда, флаги
  • Счётчик ходов и времени — отслеживание прогресса
  • Таблица результатов — сохранение лучших результатов
  • Анимация переворота — 3D эффект при открытии карточки
  • Проверка совпадений — автоматическое обновление статуса
  • Автосохранение — результаты сохраняются в SharedPreferences
Реализовать простую игру Memory (найди пару) | PrepBro