...
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// packages/feature_game_cardflip/lib/controllers/cardflip_controller.dart
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/cardflip_models.dart';
|
||||
|
||||
class CardFlipController with ChangeNotifier {
|
||||
late CardFlipDifficulty difficulty;
|
||||
late String userId;
|
||||
late String? userName;
|
||||
|
||||
List<CardItem> _cards = [];
|
||||
List<CardItem> get cards => _cards;
|
||||
|
||||
CardItem? _firstFlippedCard;
|
||||
|
||||
bool _isProcessing = false;
|
||||
bool get isProcessing => _isProcessing;
|
||||
|
||||
bool _isGameCompleted = false;
|
||||
bool get isGameCompleted => _isGameCompleted;
|
||||
|
||||
bool _isTimeOut = false;
|
||||
bool get isTimeOut => _isTimeOut;
|
||||
|
||||
int _flipCount = 0;
|
||||
int get flipCount => _flipCount;
|
||||
|
||||
Timer? _timer;
|
||||
int _secondsElapsed = 0;
|
||||
int _remainingTime = 0;
|
||||
int get remainingTime => _remainingTime;
|
||||
|
||||
// [🔥 신규] 게임이 시작되었는지(타이머가 도는지) 여부
|
||||
bool _isGameStarted = false;
|
||||
bool get isGameStarted => _isGameStarted;
|
||||
|
||||
void setUserInfo(String userId, String? userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
void startNewGame(CardFlipDifficulty level) {
|
||||
difficulty = level;
|
||||
_flipCount = 0;
|
||||
_secondsElapsed = 0;
|
||||
_remainingTime = level.timeLimitSeconds;
|
||||
_isGameCompleted = false;
|
||||
_isTimeOut = false;
|
||||
_isProcessing = false;
|
||||
_isGameStarted = false; // 타이머 대기 상태
|
||||
_firstFlippedCard = null;
|
||||
|
||||
_generateCards();
|
||||
// [🔥 수정] startNewGame에서는 타이머를 시작하지 않음 (가이드 확인 후 시작)
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void restartGame() {
|
||||
startNewGame(difficulty);
|
||||
// 재시작 시에는 가이드 없이 바로 시작하고 싶다면 여기서 startTimer 호출
|
||||
// 하지만 일관성을 위해 UI에서 다시 가이드를 띄우도록 유도
|
||||
}
|
||||
|
||||
// [🔥 수정] Public으로 변경 (UI에서 호출)
|
||||
void startGameTimer() {
|
||||
if (_isGameStarted) return;
|
||||
_isGameStarted = true;
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_secondsElapsed++;
|
||||
_remainingTime--;
|
||||
|
||||
if (_remainingTime <= 0) {
|
||||
_timer?.cancel();
|
||||
_isTimeOut = true;
|
||||
_isGameCompleted = true;
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 🔽 [🔥 핵심] 카드 생성 로직 (타입별 분기)
|
||||
void _generateCards() {
|
||||
final int totalCards = difficulty.totalCards;
|
||||
final int pairsCount = totalCards ~/ 2;
|
||||
List<CardItem> deck = [];
|
||||
|
||||
if (difficulty.contentType == CardContentType.calculation) {
|
||||
// 1. 연산 모드 (식 ↔ 답)
|
||||
var entries = CardFlipDifficulties.calculationPairs.entries.toList()..shuffle();
|
||||
for (int i = 0; i < pairsCount; i++) {
|
||||
var entry = entries[i % entries.length];
|
||||
String matchKey = "CALC_$i"; // 논리적 ID
|
||||
// 식 카드
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
|
||||
// 답 카드
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value));
|
||||
}
|
||||
}
|
||||
else if (difficulty.contentType == CardContentType.pairWord) {
|
||||
// 2. 연상 모드 (A ↔ B)
|
||||
var entries = CardFlipDifficulties.wordPairs.entries.toList()..shuffle();
|
||||
for (int i = 0; i < pairsCount; i++) {
|
||||
var entry = entries[i % entries.length];
|
||||
String matchKey = "PAIR_$i";
|
||||
// 단어 A
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
|
||||
// 단어 B
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 3. 동일 매칭 모드 (이모지, 아이콘, 숫자)
|
||||
List<String> pool = List.of(CardFlipDifficulties.emojis)..shuffle();
|
||||
for (int i = 0; i < pairsCount; i++) {
|
||||
String content;
|
||||
String matchKey = "SAME_$i";
|
||||
|
||||
if (difficulty.contentType == CardContentType.number) {
|
||||
content = (i + 1).toString();
|
||||
} else if (difficulty.contentType == CardContentType.icon) {
|
||||
content = "ICON_$i";
|
||||
} else {
|
||||
content = pool[i % pool.length];
|
||||
}
|
||||
|
||||
// 똑같은 카드 2장
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
|
||||
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 전체 섞기 및 ID 부여
|
||||
deck.shuffle(Random());
|
||||
for (int i = 0; i < deck.length; i++) {
|
||||
// 기존 객체를 복사하며 고유 ID 부여
|
||||
deck[i] = CardItem(
|
||||
id: i,
|
||||
matchId: deck[i].matchId,
|
||||
displayContent: deck[i].displayContent
|
||||
);
|
||||
}
|
||||
_cards = deck;
|
||||
}
|
||||
|
||||
// 🔽 [핵심] 카드 뒤집기 로직
|
||||
void onCardTapped(CardItem card) {
|
||||
// 게임이 시작되지 않았거나, 이미 완료되었거나, 처리 중이면 무시
|
||||
if (!_isGameStarted || _isGameCompleted || _isProcessing || card.isFaceUp || card.isMatched) return;
|
||||
|
||||
card.isFaceUp = true;
|
||||
_flipCount++;
|
||||
notifyListeners();
|
||||
|
||||
if (_firstFlippedCard == null) {
|
||||
_firstFlippedCard = card;
|
||||
} else {
|
||||
_isProcessing = true;
|
||||
_checkMatch(_firstFlippedCard!, card);
|
||||
_firstFlippedCard = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 [🔥 수정] 매칭 로직: matchId로 비교
|
||||
void _checkMatch(CardItem card1, CardItem card2) {
|
||||
if (card1.matchId == card2.matchId) {
|
||||
// 정답
|
||||
card1.isMatched = true;
|
||||
card2.isMatched = true;
|
||||
_isProcessing = false;
|
||||
|
||||
if (_cards.every((c) => c.isMatched)) {
|
||||
_isGameCompleted = true;
|
||||
_timer?.cancel();
|
||||
}
|
||||
notifyListeners();
|
||||
} else {
|
||||
// 오답
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
card1.isFaceUp = false;
|
||||
card2.isFaceUp = false;
|
||||
_isProcessing = false;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user