From 3228e370c5c09c48b6b80fc99e5584f6ff06435a Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Wed, 19 Nov 2025 17:16:43 +0900 Subject: [PATCH] ... --- .../lib/controllers/cardflip_controller.dart | 72 +++++++++++-------- .../lib/models/cardflip_models.dart | 33 ++++----- .../lib/screens/cardflip_game_screen.dart | 44 +++++++++--- 3 files changed, 91 insertions(+), 58 deletions(-) diff --git a/packages/feature_game_cardflip/lib/controllers/cardflip_controller.dart b/packages/feature_game_cardflip/lib/controllers/cardflip_controller.dart index 306ae0c..cb6d891 100644 --- a/packages/feature_game_cardflip/lib/controllers/cardflip_controller.dart +++ b/packages/feature_game_cardflip/lib/controllers/cardflip_controller.dart @@ -32,10 +32,15 @@ class CardFlipController with ChangeNotifier { int _remainingTime = 0; int get remainingTime => _remainingTime; - // [๐Ÿ”ฅ ์‹ ๊ทœ] ๊ฒŒ์ž„์ด ์‹œ์ž‘๋˜์—ˆ๋Š”์ง€(ํƒ€์ด๋จธ๊ฐ€ ๋„๋Š”์ง€) ์—ฌ๋ถ€ bool _isGameStarted = false; 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) { this.userId = userId; this.userName = userName; @@ -49,21 +54,17 @@ class CardFlipController with ChangeNotifier { _isGameCompleted = false; _isTimeOut = false; _isProcessing = false; - _isGameStarted = false; // ํƒ€์ด๋จธ ๋Œ€๊ธฐ ์ƒํƒœ + _isGameStarted = false; _firstFlippedCard = null; _generateCards(); - // [๐Ÿ”ฅ ์ˆ˜์ •] startNewGame์—์„œ๋Š” ํƒ€์ด๋จธ๋ฅผ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Œ (๊ฐ€์ด๋“œ ํ™•์ธ ํ›„ ์‹œ์ž‘) notifyListeners(); } void restartGame() { startNewGame(difficulty); - // ์žฌ์‹œ์ž‘ ์‹œ์—๋Š” ๊ฐ€์ด๋“œ ์—†์ด ๋ฐ”๋กœ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ startTimer ํ˜ธ์ถœ - // ํ•˜์ง€๋งŒ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด UI์—์„œ ๋‹ค์‹œ ๊ฐ€์ด๋“œ๋ฅผ ๋„์šฐ๋„๋ก ์œ ๋„ } - // [๐Ÿ”ฅ ์ˆ˜์ •] Public์œผ๋กœ ๋ณ€๊ฒฝ (UI์—์„œ ํ˜ธ์ถœ) void startGameTimer() { if (_isGameStarted) return; _isGameStarted = true; @@ -83,38 +84,61 @@ class CardFlipController with ChangeNotifier { notifyListeners(); } - // ๐Ÿ”ฝ [๐Ÿ”ฅ ํ•ต์‹ฌ] ์นด๋“œ ์ƒ์„ฑ ๋กœ์ง (ํƒ€์ž…๋ณ„ ๋ถ„๊ธฐ) void _generateCards() { final int totalCards = difficulty.totalCards; final int pairsCount = totalCards ~/ 2; List deck = []; + final Random random = Random(); if (difficulty.contentType == CardContentType.calculation) { - // 1. ์—ฐ์‚ฐ ๋ชจ๋“œ (์‹ โ†” ๋‹ต) - var entries = CardFlipDifficulties.calculationPairs.entries.toList()..shuffle(); + final Set usedResults = {}; 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)); + String equation; + String answer; + int result; + + while (true) { + 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) { - // 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 pool = List.of(CardFlipDifficulties.emojis)..shuffle(); for (int i = 0; i < pairsCount; i++) { String content; @@ -128,16 +152,13 @@ class CardFlipController with ChangeNotifier { 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()); + deck.shuffle(random); for (int i = 0; i < deck.length; i++) { - // ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•˜๋ฉฐ ๊ณ ์œ  ID ๋ถ€์—ฌ deck[i] = CardItem( id: i, matchId: deck[i].matchId, @@ -147,9 +168,7 @@ class CardFlipController with ChangeNotifier { _cards = deck; } - // ๐Ÿ”ฝ [ํ•ต์‹ฌ] ์นด๋“œ ๋’ค์ง‘๊ธฐ ๋กœ์ง void onCardTapped(CardItem card) { - // ๊ฒŒ์ž„์ด ์‹œ์ž‘๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜, ์ด๋ฏธ ์™„๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜, ์ฒ˜๋ฆฌ ์ค‘์ด๋ฉด ๋ฌด์‹œ if (!_isGameStarted || _isGameCompleted || _isProcessing || card.isFaceUp || card.isMatched) return; card.isFaceUp = true; @@ -165,10 +184,8 @@ class CardFlipController with ChangeNotifier { } } - // ๐Ÿ”ฝ [๐Ÿ”ฅ ์ˆ˜์ •] ๋งค์นญ ๋กœ์ง: matchId๋กœ ๋น„๊ต void _checkMatch(CardItem card1, CardItem card2) { if (card1.matchId == card2.matchId) { - // ์ •๋‹ต card1.isMatched = true; card2.isMatched = true; _isProcessing = false; @@ -179,7 +196,6 @@ class CardFlipController with ChangeNotifier { } notifyListeners(); } else { - // ์˜ค๋‹ต Future.delayed(const Duration(milliseconds: 800), () { card1.isFaceUp = false; card2.isFaceUp = false; diff --git a/packages/feature_game_cardflip/lib/models/cardflip_models.dart b/packages/feature_game_cardflip/lib/models/cardflip_models.dart index fa2f62a..3e09b5b 100644 --- a/packages/feature_game_cardflip/lib/models/cardflip_models.dart +++ b/packages/feature_game_cardflip/lib/models/cardflip_models.dart @@ -3,20 +3,18 @@ import 'package:flutter/material.dart'; import 'package:service_api/service_api.dart'; -/// ์นด๋“œ์— ๋“ค์–ด๊ฐˆ ์ฝ˜ํ…์ธ  ํƒ€์ž… enum CardContentType { - emoji, // ๊ธฐ์กด: ๋˜‘๊ฐ™์€ ์ด๋ชจ์ง€ (๐Ÿฐ โ†” ๐Ÿฐ) - icon, // ๊ธฐ์กด: ๋˜‘๊ฐ™์€ ์•„์ด์ฝ˜ (โญ๏ธ โ†” โญ๏ธ) - number, // ๊ธฐ์กด: ๋˜‘๊ฐ™์€ ์ˆซ์ž (1 โ†” 1) - calculation, // [๐Ÿ”ฅ ์‹ ๊ทœ] ์—ฐ์‚ฐ (3+4 โ†” 7) - pairWord, // [๐Ÿ”ฅ ์‹ ๊ทœ] ์—ฐ์ƒ ๋‹จ์–ด (ํ† ๋ผ โ†” ๋‹น๊ทผ) + emoji, // ๐Ÿฐ โ†” ๐Ÿฐ + icon, // โญ๏ธ โ†” โญ๏ธ + number, // 1 โ†” 1 + calculation, // 3+4 โ†” 7 (๋™์  ์ƒ์„ฑ) + pairWord, // ํ† ๋ผ โ†” ๋‹น๊ทผ } -/// ๊ฐœ๋ณ„ ์นด๋“œ ์ƒํƒœ ๋ชจ๋ธ class CardItem { - final int id; // ์นด๋“œ์˜ ๊ณ ์œ  ์‹๋ณ„์ž (GridView ์ธ๋ฑ์Šค์™€ ๋ฌด๊ด€, ์…”ํ”Œ๋จ) - final String matchId; // [๐Ÿ”ฅ ์‹ ๊ทœ] ๋งค์นญ ํŒ๋‹จ์šฉ ID (์ด๊ฒŒ ๊ฐ™์œผ๋ฉด ์ •๋‹ต) - final String displayContent; // [๐Ÿ”ฅ ์‹ ๊ทœ] ํ™”๋ฉด์— ๋ณด์—ฌ์งˆ ๋‚ด์šฉ + final int id; + final String matchId; + final String displayContent; bool isFaceUp; bool isMatched; @@ -51,7 +49,7 @@ class CardFlipDifficulty extends GameDifficulty { } class CardFlipDifficulties { - // --- ์ฝ˜ํ…์ธ  ํ’€ --- + // ์ด๋ชจ์ง€ ํ’€ static const List emojis = [ "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", "๐Ÿฆ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿฆ†", @@ -59,14 +57,9 @@ class CardFlipDifficulties { "โšฝ", "๐Ÿ€", "๐Ÿˆ", "โšพ", "๐ŸŽพ", "๐Ÿ", "๐Ÿ‰", "๐ŸŽฑ", "๐Ÿ“", "๐Ÿธ" ]; - // [๐Ÿ”ฅ ์‹ ๊ทœ] ์—ฐ์‚ฐ ๋ฌธ์ œ ํ’€ (์งˆ๋ฌธ : ์ •๋‹ต) - static const Map 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" - }; + // โŒ [์‚ญ์ œ๋จ] calculationPairs (์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋™์  ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์ œ๊ฑฐ) - // [๐Ÿ”ฅ ์‹ ๊ทœ] ์—ฐ์ƒ ๋‹จ์–ด ํ’€ (A : B) + // ์—ฐ์ƒ ๋‹จ์–ด ํ’€ (A : B) static const Map wordPairs = { "ํ† ๋ผ": "๋‹น๊ทผ", "์›์ˆญ์ด": "๋ฐ”๋‚˜๋‚˜", "ํ•œ๊ตญ": "์„œ์šธ", "๋ฏธ๊ตญ": "์›Œ์‹ฑํ„ด", "ํ•ด": "๋‹ฌ", "์—ฌ๋ฆ„": "๊ฒจ์šธ", "๋‚จ์ž": "์—ฌ์ž", "ํ•™๊ต": "ํ•™์ƒ", @@ -74,7 +67,7 @@ class CardFlipDifficulties { "๋ด„": "๊ฝƒ", "๊ฐ€์„": "๋‹จํ’", "์ˆŸ๊ฐ€๋ฝ": "์ “๊ฐ€๋ฝ" }; - // --- ๋‚œ์ด๋„ ๋ชฉ๋ก (15๋‹จ๊ณ„) --- + // ๋‚œ์ด๋„ ๋ชฉ๋ก (15๋‹จ๊ณ„) static final List allDifficulties = [ // Phase 1: ์ž…๋ฌธ (๋™์ผ ๋งค์นญ) 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: 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: 16, name: 'Lv. 15: ํ—ฌ (์—ฐ์ƒ)', contextId: 'FLIP_L16_6x6_PAIR', rows: 6, cols: 6, timeLimitSeconds: 120, contentType: CardContentType.pairWord), ]; static CardFlipDifficulty getLevel(int levelIndex) { diff --git a/packages/feature_game_cardflip/lib/screens/cardflip_game_screen.dart b/packages/feature_game_cardflip/lib/screens/cardflip_game_screen.dart index 9ef5737..4cbe2bf 100644 --- a/packages/feature_game_cardflip/lib/screens/cardflip_game_screen.dart +++ b/packages/feature_game_cardflip/lib/screens/cardflip_game_screen.dart @@ -17,7 +17,6 @@ class CardFlipGameScreen extends StatefulWidget { class _CardFlipGameScreenState extends State { bool _isDialogShowing = false; - // ์•„์ด์ฝ˜ ํ’€ static const List _iconPool = [ Icons.home, Icons.favorite, Icons.star, Icons.person, Icons.settings, Icons.lock, Icons.map, Icons.camera_alt, Icons.phone, Icons.music_note, @@ -31,13 +30,11 @@ class _CardFlipGameScreenState extends State { @override void initState() { super.initState(); - // [๐Ÿ”ฅ ์‹ ๊ทœ] ๊ฒŒ์ž„ ์‹œ์ž‘ ์ „ ๊ฐ€์ด๋“œ ํ‘œ์‹œ WidgetsBinding.instance.addPostFrameCallback((_) { _showGameGuide(); }); } - // ๐Ÿ”ฝ [๐Ÿ”ฅ ์‹ ๊ทœ] ๊ฒŒ์ž„ ๊ฐ€์ด๋“œ ๋‹ค์ด์–ผ๋กœ๊ทธ void _showGameGuide() { final controller = context.read(); final difficulty = controller.difficulty; @@ -55,7 +52,7 @@ class _CardFlipGameScreenState extends State { showDialog( context: context, - barrierDismissible: false, // ๋ฐ˜๋“œ์‹œ ํ™•์ธ์„ ๋ˆŒ๋Ÿฌ์•ผ ํ•จ + barrierDismissible: false, builder: (context) => AlertDialog( title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), content: Text(message, style: const TextStyle(fontSize: 16), textAlign: TextAlign.center), @@ -63,7 +60,6 @@ class _CardFlipGameScreenState extends State { ElevatedButton( onPressed: () { Navigator.of(context).pop(); - // [๐Ÿ”ฅ ํ•ต์‹ฌ] ๊ฐ€์ด๋“œ ๋‹ซ์œผ๋ฉด ํƒ€์ด๋จธ ์‹œ์ž‘ controller.startGameTimer(); }, child: const Text("์‹œ์ž‘ํ•˜๊ธฐ"), @@ -74,8 +70,17 @@ class _CardFlipGameScreenState extends State { } void _showGameCompletion(CardFlipController controller) async { + // [๐Ÿ”ฅ ์ˆ˜์ •] ์ ์ˆ˜ ํฌ๋งทํ„ฐ: secondaryScore๋ฅผ ๋””์ฝ”๋”ฉํ•˜์—ฌ ํ‘œ์‹œ + // (secondary = matched * 1000 + (999 - flips)) 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 saveProgress(String playerName) async { @@ -94,6 +99,10 @@ class _CardFlipGameScreenState extends State { } } } + + // [๐Ÿ”ฅ ์ˆ˜์ •] secondaryScore์— ๋งค์นญ ์ˆ˜์™€ ํด๋ฆญ ์ˆ˜๋ฅผ ํ•จ๊ป˜ ์ธ์ฝ”๋”ฉ (์ •๋ ฌ์„ ์œ„ํ•ด) + // (๋งŽ์€ ๋งค์นญ + ์ ์€ ํด๋ฆญ์ผ์ˆ˜๋ก ๋†’์€ ๊ฐ’์ด ๋˜๋„๋ก ์„ค๊ณ„) + final int encodedScore = (controller.matchedPairsCount * 1000) + (999 - controller.flipCount); await Navigator.push( context, @@ -103,7 +112,7 @@ class _CardFlipGameScreenState extends State { gameType: 'CARD_FLIP', contextId: controller.difficulty.contextId, primaryScore: controller.remainingTime, - secondaryScore: controller.flipCount, + secondaryScore: encodedScore, // ๐Ÿ‘ˆ ์ธ์ฝ”๋”ฉ๋œ ์ ์ˆ˜ ์ „๋‹ฌ userId: controller.userId, userName: controller.userName, scoreFormatter: formatScore, @@ -149,10 +158,24 @@ class _CardFlipGameScreenState extends State { ), body: Column( children: [ + // ๐Ÿ”ฝ [๐Ÿ”ฅ ์ˆ˜์ •] ์ƒ๋‹จ ์ •๋ณด: ๋งค์นญ ์ˆ˜ / ์‹œ๋„ ํšŸ์ˆ˜ ํ‘œ์‹œ Padding( - padding: const EdgeInsets.all(8.0), - child: Text("๋’ค์ง‘์€ ํšŸ์ˆ˜: ${controller.flipCount}", style: TextStyle(fontSize: 16, color: theme.textTheme.bodyMedium?.color)), + padding: const EdgeInsets.all(12.0), + 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( child: Padding( padding: const EdgeInsets.all(8.0), @@ -206,7 +229,6 @@ class _CardFlipGameScreenState extends State { ); } - // ๐Ÿ”ฝ [๐Ÿ”ฅ ์ˆ˜์ •] displayContent ์‚ฌ์šฉ Widget _buildCardContent(CardItem card) { if (card.displayContent.startsWith("ICON_")) { final int iconIndex = int.tryParse(card.displayContent.split('_')[1]) ?? 0; @@ -218,7 +240,7 @@ class _CardFlipGameScreenState extends State { child: FittedBox( fit: BoxFit.scaleDown, child: Text( - card.displayContent, // ๐Ÿ‘ˆ displayContent ํ‘œ์‹œ + card.displayContent, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), ), ),