← Назад к вопросам
Реализовать простую игру Memory (найди пару)
1.7 Middle🔥 61 комментариев
#Flutter виджеты#State Management#Анимации
Условие
Создайте игру на запоминание, где нужно находить пары одинаковых карточек.
Требования
- Сетка карточек 4x4 (8 пар)
- Карточки перевёрнуты рубашкой вверх
- При нажатии карточка переворачивается
- Если открыты 2 одинаковые — они остаются открытыми
- Если разные — переворачиваются обратно через 1 секунду
- Счётчик ходов и времени
- Победа при нахождении всех пар
Дополнительные баллы
- Выбор уровня сложности (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