This commit is contained in:
lunaticbum 2025-11-19 17:16:43 +09:00
parent 09665fa073
commit 3228e370c5
3 changed files with 91 additions and 58 deletions

View File

@ -32,10 +32,15 @@ class CardFlipController with ChangeNotifier {
int _remainingTime = 0; int _remainingTime = 0;
int get remainingTime => _remainingTime; int get remainingTime => _remainingTime;
// [🔥 ] ( )
bool _isGameStarted = false; bool _isGameStarted = false;
bool get isGameStarted => _isGameStarted; bool get isGameStarted => _isGameStarted;
// [🔥 ]
int get matchedPairsCount => _cards.where((c) => c.isMatched).length ~/ 2;
// [🔥 ]
int get totalPairsCount => _cards.length ~/ 2;
void setUserInfo(String userId, String? userName) { void setUserInfo(String userId, String? userName) {
this.userId = userId; this.userId = userId;
this.userName = userName; this.userName = userName;
@ -49,21 +54,17 @@ class CardFlipController with ChangeNotifier {
_isGameCompleted = false; _isGameCompleted = false;
_isTimeOut = false; _isTimeOut = false;
_isProcessing = false; _isProcessing = false;
_isGameStarted = false; // _isGameStarted = false;
_firstFlippedCard = null; _firstFlippedCard = null;
_generateCards(); _generateCards();
// [🔥 ] startNewGame에서는 ( )
notifyListeners(); notifyListeners();
} }
void restartGame() { void restartGame() {
startNewGame(difficulty); startNewGame(difficulty);
// startTimer
// UI에서
} }
// [🔥 ] Public으로 (UI에서 )
void startGameTimer() { void startGameTimer() {
if (_isGameStarted) return; if (_isGameStarted) return;
_isGameStarted = true; _isGameStarted = true;
@ -83,38 +84,61 @@ class CardFlipController with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// 🔽 [🔥 ] ( )
void _generateCards() { void _generateCards() {
final int totalCards = difficulty.totalCards; final int totalCards = difficulty.totalCards;
final int pairsCount = totalCards ~/ 2; final int pairsCount = totalCards ~/ 2;
List<CardItem> deck = []; List<CardItem> deck = [];
final Random random = Random();
if (difficulty.contentType == CardContentType.calculation) { if (difficulty.contentType == CardContentType.calculation) {
// 1. ( ) final Set<int> usedResults = {};
var entries = CardFlipDifficulties.calculationPairs.entries.toList()..shuffle();
for (int i = 0; i < pairsCount; i++) { for (int i = 0; i < pairsCount; i++) {
var entry = entries[i % entries.length]; String equation;
String matchKey = "CALC_$i"; // ID String answer;
// int result;
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
// while (true) {
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value)); final int opType = random.nextInt(3);
int a, b;
if (opType == 0) { // +
a = random.nextInt(15) + 1;
b = random.nextInt(15) + 1;
result = a + b;
equation = "$a + $b";
} else if (opType == 1) { // -
a = random.nextInt(20) + 5;
b = random.nextInt(a - 1) + 1;
result = a - b;
equation = "$a - $b";
} else { // *
a = random.nextInt(9) + 2;
b = random.nextInt(5) + 2;
result = a * b;
equation = "$a x $b";
}
if (!usedResults.contains(result)) {
usedResults.add(result);
answer = result.toString();
break;
}
}
String matchKey = "CALC_$i";
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: equation));
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: answer));
} }
} }
else if (difficulty.contentType == CardContentType.pairWord) { else if (difficulty.contentType == CardContentType.pairWord) {
// 2. (A B)
var entries = CardFlipDifficulties.wordPairs.entries.toList()..shuffle(); var entries = CardFlipDifficulties.wordPairs.entries.toList()..shuffle();
for (int i = 0; i < pairsCount; i++) { for (int i = 0; i < pairsCount; i++) {
var entry = entries[i % entries.length]; var entry = entries[i % entries.length];
String matchKey = "PAIR_$i"; String matchKey = "PAIR_$i";
// A
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key)); deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
// B
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value)); deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value));
} }
} }
else { else {
// 3. (, , )
List<String> pool = List.of(CardFlipDifficulties.emojis)..shuffle(); List<String> pool = List.of(CardFlipDifficulties.emojis)..shuffle();
for (int i = 0; i < pairsCount; i++) { for (int i = 0; i < pairsCount; i++) {
String content; String content;
@ -128,16 +152,13 @@ class CardFlipController with ChangeNotifier {
content = pool[i % pool.length]; content = pool[i % pool.length];
} }
// 2
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content)); deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content)); deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
} }
} }
// 4. ID deck.shuffle(random);
deck.shuffle(Random());
for (int i = 0; i < deck.length; i++) { for (int i = 0; i < deck.length; i++) {
// ID
deck[i] = CardItem( deck[i] = CardItem(
id: i, id: i,
matchId: deck[i].matchId, matchId: deck[i].matchId,
@ -147,9 +168,7 @@ class CardFlipController with ChangeNotifier {
_cards = deck; _cards = deck;
} }
// 🔽 []
void onCardTapped(CardItem card) { void onCardTapped(CardItem card) {
// , ,
if (!_isGameStarted || _isGameCompleted || _isProcessing || card.isFaceUp || card.isMatched) return; if (!_isGameStarted || _isGameCompleted || _isProcessing || card.isFaceUp || card.isMatched) return;
card.isFaceUp = true; card.isFaceUp = true;
@ -165,10 +184,8 @@ class CardFlipController with ChangeNotifier {
} }
} }
// 🔽 [🔥 ] : matchId로
void _checkMatch(CardItem card1, CardItem card2) { void _checkMatch(CardItem card1, CardItem card2) {
if (card1.matchId == card2.matchId) { if (card1.matchId == card2.matchId) {
//
card1.isMatched = true; card1.isMatched = true;
card2.isMatched = true; card2.isMatched = true;
_isProcessing = false; _isProcessing = false;
@ -179,7 +196,6 @@ class CardFlipController with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
} else { } else {
//
Future.delayed(const Duration(milliseconds: 800), () { Future.delayed(const Duration(milliseconds: 800), () {
card1.isFaceUp = false; card1.isFaceUp = false;
card2.isFaceUp = false; card2.isFaceUp = false;

View File

@ -3,20 +3,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:service_api/service_api.dart'; import 'package:service_api/service_api.dart';
///
enum CardContentType { enum CardContentType {
emoji, // : (🐰 🐰) emoji, // 🐰 🐰
icon, // : ( ) icon, //
number, // : (1 1) number, // 1 1
calculation, // [🔥 ] (3+4 7) calculation, // 3+4 7 ( )
pairWord, // [🔥 ] ( ) pairWord, //
} }
///
class CardItem { class CardItem {
final int id; // (GridView , ) final int id;
final String matchId; // [🔥 ] ID ( ) final String matchId;
final String displayContent; // [🔥 ] final String displayContent;
bool isFaceUp; bool isFaceUp;
bool isMatched; bool isMatched;
@ -51,7 +49,7 @@ class CardFlipDifficulty extends GameDifficulty {
} }
class CardFlipDifficulties { class CardFlipDifficulties {
// --- --- //
static const List<String> emojis = [ static const List<String> emojis = [
"🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯",
"🦁", "🐮", "🐷", "🐸", "🐵", "🐔", "🐧", "🐦", "🐤", "🦆", "🦁", "🐮", "🐷", "🐸", "🐵", "🐔", "🐧", "🐦", "🐤", "🦆",
@ -59,14 +57,9 @@ class CardFlipDifficulties {
"", "🏀", "🏈", "", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸" "", "🏀", "🏈", "", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸"
]; ];
// [🔥 ] ( : ) // [] calculationPairs ( )
static const Map<String, String> calculationPairs = {
"2 + 3": "5", "5 + 5": "10", "7 - 2": "5", "3 x 3": "9", "10 - 4": "6",
"6 + 6": "12", "8 + 1": "9", "4 x 2": "8", "15 - 5": "10", "20 / 2": "10",
"1 + 1": "2", "9 - 3": "6", "2 x 5": "10", "8 / 2": "4", "3 + 7": "10"
};
// [🔥 ] (A : B) // (A : B)
static const Map<String, String> wordPairs = { static const Map<String, String> wordPairs = {
"토끼": "당근", "원숭이": "바나나", "한국": "서울", "미국": "워싱턴", "토끼": "당근", "원숭이": "바나나", "한국": "서울", "미국": "워싱턴",
"": "", "여름": "겨울", "남자": "여자", "학교": "학생", "": "", "여름": "겨울", "남자": "여자", "학교": "학생",
@ -74,7 +67,7 @@ class CardFlipDifficulties {
"": "", "가을": "단풍", "숟가락": "젓가락" "": "", "가을": "단풍", "숟가락": "젓가락"
}; };
// --- (15) --- // (15)
static final List<CardFlipDifficulty> allDifficulties = [ static final List<CardFlipDifficulty> allDifficulties = [
// Phase 1: ( ) // Phase 1: ( )
const CardFlipDifficulty(levelIndex: 1, name: 'Lv. 1: 입문 (이모지 12)', contextId: 'FLIP_L1_EMOJI', rows: 4, cols: 3, timeLimitSeconds: 40, contentType: CardContentType.emoji), const CardFlipDifficulty(levelIndex: 1, name: 'Lv. 1: 입문 (이모지 12)', contextId: 'FLIP_L1_EMOJI', rows: 4, cols: 3, timeLimitSeconds: 40, contentType: CardContentType.emoji),
@ -100,6 +93,8 @@ class CardFlipDifficulties {
const CardFlipDifficulty(levelIndex: 13, name: 'Lv. 13: 갓모드 (30장)', contextId: 'FLIP_L13_6x5', rows: 6, cols: 5, timeLimitSeconds: 130, contentType: CardContentType.emoji), const CardFlipDifficulty(levelIndex: 13, name: 'Lv. 13: 갓모드 (30장)', contextId: 'FLIP_L13_6x5', rows: 6, cols: 5, timeLimitSeconds: 130, contentType: CardContentType.emoji),
const CardFlipDifficulty(levelIndex: 14, name: 'Lv. 14: 타임어택 (연산)', contextId: 'FLIP_L14_6x5_CALC', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.calculation), const CardFlipDifficulty(levelIndex: 14, name: 'Lv. 14: 타임어택 (연산)', contextId: 'FLIP_L14_6x5_CALC', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.calculation),
const CardFlipDifficulty(levelIndex: 15, name: 'Lv. 15: 엔드게임 (연상)', contextId: 'FLIP_L15_6x5_PAIR', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.pairWord), const CardFlipDifficulty(levelIndex: 15, name: 'Lv. 15: 엔드게임 (연상)', contextId: 'FLIP_L15_6x5_PAIR', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.pairWord),
const CardFlipDifficulty(levelIndex: 16, name: 'Lv. 15: 헬 (연상)', contextId: 'FLIP_L16_6x6_PAIR', rows: 6, cols: 6, timeLimitSeconds: 120, contentType: CardContentType.pairWord),
]; ];
static CardFlipDifficulty getLevel(int levelIndex) { static CardFlipDifficulty getLevel(int levelIndex) {

View File

@ -17,7 +17,6 @@ class CardFlipGameScreen extends StatefulWidget {
class _CardFlipGameScreenState extends State<CardFlipGameScreen> { class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
bool _isDialogShowing = false; bool _isDialogShowing = false;
//
static const List<IconData> _iconPool = [ static const List<IconData> _iconPool = [
Icons.home, Icons.favorite, Icons.star, Icons.person, Icons.settings, Icons.home, Icons.favorite, Icons.star, Icons.person, Icons.settings,
Icons.lock, Icons.map, Icons.camera_alt, Icons.phone, Icons.music_note, Icons.lock, Icons.map, Icons.camera_alt, Icons.phone, Icons.music_note,
@ -31,13 +30,11 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// [🔥 ]
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_showGameGuide(); _showGameGuide();
}); });
} }
// 🔽 [🔥 ]
void _showGameGuide() { void _showGameGuide() {
final controller = context.read<CardFlipController>(); final controller = context.read<CardFlipController>();
final difficulty = controller.difficulty; final difficulty = controller.difficulty;
@ -55,7 +52,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, // barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
content: Text(message, style: const TextStyle(fontSize: 16), textAlign: TextAlign.center), content: Text(message, style: const TextStyle(fontSize: 16), textAlign: TextAlign.center),
@ -63,7 +60,6 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
// [🔥 ]
controller.startGameTimer(); controller.startGameTimer();
}, },
child: const Text("시작하기"), child: const Text("시작하기"),
@ -74,8 +70,17 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
} }
void _showGameCompletion(CardFlipController controller) async { void _showGameCompletion(CardFlipController controller) async {
// [🔥 ] : secondaryScore를
// (secondary = matched * 1000 + (999 - flips))
String formatScore(int primary, int? secondary) { String formatScore(int primary, int? secondary) {
return '남은 시간: ${primary}초 (시도: $secondary회)'; if (secondary == null) return '${primary}';
final int matched = secondary ~/ 1000;
final int flips = 999 - (secondary % 1000);
// ( = ( - *2) / 2)
final int mistakes = (flips - (matched * 2)) ~/ 2;
return '시간: ${primary}초 (성공:$matched / 실수:$mistakes)';
} }
Future<void> saveProgress(String playerName) async { Future<void> saveProgress(String playerName) async {
@ -94,6 +99,10 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
} }
} }
} }
// [🔥 ] secondaryScore에 ( )
// ( + )
final int encodedScore = (controller.matchedPairsCount * 1000) + (999 - controller.flipCount);
await Navigator.push( await Navigator.push(
context, context,
@ -103,7 +112,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
gameType: 'CARD_FLIP', gameType: 'CARD_FLIP',
contextId: controller.difficulty.contextId, contextId: controller.difficulty.contextId,
primaryScore: controller.remainingTime, primaryScore: controller.remainingTime,
secondaryScore: controller.flipCount, secondaryScore: encodedScore, // 👈
userId: controller.userId, userId: controller.userId,
userName: controller.userName, userName: controller.userName,
scoreFormatter: formatScore, scoreFormatter: formatScore,
@ -149,10 +158,24 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
), ),
body: Column( body: Column(
children: [ children: [
// 🔽 [🔥 ] : /
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(12.0),
child: Text("뒤집은 횟수: ${controller.flipCount}", style: TextStyle(fontSize: 16, color: theme.textTheme.bodyMedium?.color)), child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"찾은 짝: ${controller.matchedPairsCount} / ${controller.totalPairsCount}",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)
),
Text(
"뒤집기: ${controller.flipCount}",
style: const TextStyle(fontSize: 16, color: Colors.grey)
),
],
),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -206,7 +229,6 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
); );
} }
// 🔽 [🔥 ] displayContent
Widget _buildCardContent(CardItem card) { Widget _buildCardContent(CardItem card) {
if (card.displayContent.startsWith("ICON_")) { if (card.displayContent.startsWith("ICON_")) {
final int iconIndex = int.tryParse(card.displayContent.split('_')[1]) ?? 0; final int iconIndex = int.tryParse(card.displayContent.split('_')[1]) ?? 0;
@ -218,7 +240,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
card.displayContent, // 👈 displayContent card.displayContent,
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
), ),
), ),