diff --git a/.dart_tool/extension_discovery/vs_code.json b/.dart_tool/extension_discovery/vs_code.json index 5eaf21e..8ccf3cb 100644 --- a/.dart_tool/extension_discovery/vs_code.json +++ b/.dart_tool/extension_discovery/vs_code.json @@ -1 +1 @@ -{"version":2,"entries":[{"package":"my_game_center","rootUri":"../","packageUri":"lib/"},{"package":"app_sudoku","rootUri":"../apps/app_sudoku/","packageUri":"lib/"},{"package":"app_spider","rootUri":"../apps/app_spider/","packageUri":"lib/"},{"package":"service_api","rootUri":"../packages/service_api/","packageUri":"lib/"},{"package":"feature_common","rootUri":"../packages/feature_common/","packageUri":"lib/"},{"package":"feature_game_sudoku","rootUri":"../packages/feature_game_sudoku/","packageUri":"lib/"},{"package":"feature_game_spider","rootUri":"../packages/feature_game_spider/","packageUri":"lib/"}]} \ No newline at end of file +{"version":2,"entries":[{"package":"my_game_center","rootUri":"../","packageUri":"lib/"},{"package":"app_sudoku","rootUri":"../apps/app_sudoku/","packageUri":"lib/"},{"package":"app_spider","rootUri":"../apps/app_spider/","packageUri":"lib/"},{"package":"app_mathquiz","rootUri":"../apps/app_mathquiz/","packageUri":"lib/"},{"package":"app_colormatch","rootUri":"../apps/app_colormatch/","packageUri":"lib/"},{"package":"app_sequence","rootUri":"../apps/app_sequence/","packageUri":"lib/"},{"package":"app_cardflip","rootUri":"../apps/app_cardflip/","packageUri":"lib/"},{"package":"app_finddiff","rootUri":"../apps/app_finddiff/","packageUri":"lib/"},{"package":"service_api","rootUri":"../packages/service_api/","packageUri":"lib/"},{"package":"feature_common","rootUri":"../packages/feature_common/","packageUri":"lib/"},{"package":"feature_game_sudoku","rootUri":"../packages/feature_game_sudoku/","packageUri":"lib/"},{"package":"feature_game_spider","rootUri":"../packages/feature_game_spider/","packageUri":"lib/"},{"package":"feature_game_colormatch","rootUri":"../packages/feature_game_colormatch/","packageUri":"lib/"},{"package":"feature_game_mathquiz","rootUri":"../packages/feature_game_mathquiz/","packageUri":"lib/"},{"package":"feature_game_sequence","rootUri":"../packages/feature_game_sequence/","packageUri":"lib/"},{"package":"feature_game_cardflip","rootUri":"../packages/feature_game_cardflip/","packageUri":"lib/"},{"package":"feature_game_finddiff","rootUri":"../packages/feature_game_finddiff/","packageUri":"lib/"}]} \ No newline at end of file diff --git a/packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart b/packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart index ad6d7ce..2669b7e 100644 --- a/packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart +++ b/packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart @@ -1,9 +1,8 @@ -// packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart - import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import '../models/finddiff_models.dart'; +import '../utils/hangul_utils.dart'; class FindDiffController with ChangeNotifier { late FindDiffDifficulty difficulty; @@ -13,34 +12,35 @@ class FindDiffController with ChangeNotifier { List _items = []; List get items => _items; - // 게임 상태 int _currentRound = 0; int get currentRound => _currentRound; int _score = 0; int get score => _score; - int _incorrectCount = 0; + int _incorrectCount = 0; int get incorrectCount => _incorrectCount; bool _isGameCompleted = false; bool get isGameCompleted => _isGameCompleted; - // 타이머 Timer? _timer; int _remainingTime = 0; int get remainingTime => _remainingTime; + int _secondsElapsed = 0; + int get secondsElapsed => _secondsElapsed; - // [🔥 신규] 게임 시작 여부 (가이드 확인 후 true) bool _isGameStarted = false; bool get isGameStarted => _isGameStarted; - // [🔥 신규] 피드백 상태 bool _showFeedback = false; bool get showFeedback => _showFeedback; bool _isLastAnswerCorrect = false; bool get isLastAnswerCorrect => _isLastAnswerCorrect; + FindDiffType _currentRoundType = FindDiffType.color; + FindDiffType get currentRoundType => _currentRoundType; + final Random _random = Random(); void setUserInfo(String userId, String? userName) { @@ -54,11 +54,11 @@ class FindDiffController with ChangeNotifier { _incorrectCount = 0; _currentRound = 1; _isGameCompleted = false; - _isGameStarted = false; // 대기 상태 + _secondsElapsed = 0; + _isGameStarted = false; _showFeedback = false; _generateLevel(); - // [🔥 수정] 여기서 타이머를 시작하지 않음 notifyListeners(); } @@ -66,7 +66,6 @@ class FindDiffController with ChangeNotifier { startNewGame(difficulty); } - // [🔥 신규] UI에서 호출할 타이머 시작 함수 void startGameTimer() { if (_isGameStarted) return; _isGameStarted = true; @@ -79,53 +78,126 @@ class FindDiffController with ChangeNotifier { final int totalItems = difficulty.totalItems; final int targetIndex = _random.nextInt(totalItems); - // 1. 기본 속성 결정 - IconData baseIcon = Icons.circle; + // 1. 문제 유형 결정 + if (difficulty.diffType == FindDiffType.mix) { + const List types = [ + FindDiffType.color, FindDiffType.icon, FindDiffType.rotate, + FindDiffType.category, FindDiffType.word + ]; + _currentRoundType = types[_random.nextInt(types.length)]; + } else { + _currentRoundType = difficulty.diffType; + } + + // 2. 기본 속성 준비 + IconData? baseIcon; + String? baseText; + String? targetText; Color baseColor = Colors.blue; double baseAngle = 0.0; + + // [🔥 핵심] 유형별 아이콘/데이터 소스 분리 + if (_currentRoundType == FindDiffType.category) { + // 카테고리 (나중에 루프에서 결정) + + } else if (_currentRoundType == FindDiffType.word) { + // 단어 & 오타 + final String originalWord = FindDiffDifficulties.baseWords[ + _random.nextInt(FindDiffDifficulties.baseWords.length) + ]; + final String modifiedWord = HangulUtils.generateSimilarWord(originalWord); + if (_random.nextBool()) { + baseText = originalWord; + targetText = modifiedWord; + } else { + baseText = modifiedWord; + targetText = originalWord; + } + baseColor = Colors.black87; - // 난이도별 속성 설정 - if (difficulty.diffType == FindDiffType.icon) { + } else if (_currentRoundType == FindDiffType.icon) { + // 모양 찾기 final pair = FindDiffDifficulties.iconPairs[_random.nextInt(FindDiffDifficulties.iconPairs.length)]; - baseIcon = pair[0]; + baseIcon = pair[0]; + baseColor = Colors.black87; + + } else if (_currentRoundType == FindDiffType.rotate) { + // [🔥 수정] 회전 모드: rotateIcons에서 선택 (원, 네모 등 제외) + baseIcon = FindDiffDifficulties.rotateIcons[_random.nextInt(FindDiffDifficulties.rotateIcons.length)]; + baseColor = Color.fromARGB(255, _random.nextInt(200), _random.nextInt(200), _random.nextInt(200)); + } else { - baseIcon = FindDiffDifficulties.basicIcons[_random.nextInt(FindDiffDifficulties.basicIcons.length)]; + // 색상 모드: colorIcons에서 선택 + baseIcon = FindDiffDifficulties.colorIcons[_random.nextInt(FindDiffDifficulties.colorIcons.length)]; baseColor = Color.fromARGB(255, _random.nextInt(200), _random.nextInt(200), _random.nextInt(200)); } - // 2. 아이템 생성 + // 카테고리 모드용 리스트 준비 + List baseCatList = []; + List targetCatList = []; + if (_currentRoundType == FindDiffType.category) { + final keys = FindDiffDifficulties.emojiCategories.keys.toList(); + String baseKey = keys[_random.nextInt(keys.length)]; + String targetKey; + do { targetKey = keys[_random.nextInt(keys.length)]; } while (baseKey == targetKey); + baseCatList = FindDiffDifficulties.emojiCategories[baseKey]!; + targetCatList = FindDiffDifficulties.emojiCategories[targetKey]!; + } + + // 3. 아이템 배치 for (int i = 0; i < totalItems; i++) { bool isTarget = (i == targetIndex); - IconData icon = baseIcon; + IconData? icon = baseIcon; + String? textContent = baseText; Color color = baseColor; double angle = baseAngle; - if (isTarget) { - // 정답 아이템 변형 - switch (difficulty.diffType) { - case FindDiffType.color: - int offset = (difficulty.levelIndex >= 4) ? 30 : 60; - color = Color.fromARGB( - 255, - (baseColor.red + offset) % 255, - (baseColor.green + offset) % 255, - (baseColor.blue + offset) % 255, - ); - break; - case FindDiffType.icon: - final pair = FindDiffDifficulties.iconPairs.firstWhere((p) => p.contains(baseIcon)); - icon = (baseIcon == pair[0]) ? pair[1] : pair[0]; - break; - case FindDiffType.rotate: - angle = 0.5; // 약 30도 회전 - break; - } + switch (_currentRoundType) { + case FindDiffType.color: + if (isTarget) { + // 난이도가 높거나 믹스 모드일 경우 색상 차이를 미세하게 + int offset = (difficulty.levelIndex >= 5) ? 30 : 60; + color = Color.fromARGB(255, (baseColor.red + offset) % 255, (baseColor.green + offset) % 255, (baseColor.blue + offset) % 255); + } + break; + + case FindDiffType.icon: + if (isTarget) { + final pair = FindDiffDifficulties.iconPairs.firstWhere((p) => p.contains(baseIcon)); + icon = (baseIcon == pair[0]) ? pair[1] : pair[0]; + } + break; + + case FindDiffType.rotate: + if (isTarget) angle = 0.5; // 약 30도 회전 + break; + + case FindDiffType.category: + if (isTarget) { + textContent = targetCatList[_random.nextInt(targetCatList.length)]; + } else { + textContent = baseCatList[_random.nextInt(baseCatList.length)]; + } + color = Colors.black; + break; + + case FindDiffType.word: + if (isTarget) { + textContent = targetText; + } else { + textContent = baseText; + } + break; + + default: + break; } _items.add(FindDiffItem( id: i, icon: icon, + textContent: textContent, color: color, angle: angle, isTarget: isTarget, @@ -135,13 +207,13 @@ class FindDiffController with ChangeNotifier { _remainingTime = difficulty.timeLimitSeconds; } + // ... (나머지 메서드 동일) ... void _startTimer() { _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + _secondsElapsed++; _remainingTime--; - if (_remainingTime <= 0) { - // 시간 초과: 오답 처리 및 다음 문제 _handleAnswer(false); } notifyListeners(); @@ -150,12 +222,11 @@ class FindDiffController with ChangeNotifier { void onItemTapped(FindDiffItem item) { if (!_isGameStarted || _isGameCompleted || _showFeedback) return; - _handleAnswer(item.isTarget); } void _handleAnswer(bool isCorrect) { - _timer?.cancel(); // 잠시 멈춤 + _timer?.cancel(); _showFeedback = true; _isLastAnswerCorrect = isCorrect; @@ -164,18 +235,15 @@ class FindDiffController with ChangeNotifier { } else { _incorrectCount++; } - notifyListeners(); - // 피드백 후 다음 라운드 Future.delayed(const Duration(milliseconds: 500), () { - if (_score >= 10) { // [목표] 10문제 맞추면 클리어 + if (_score >= 10) { _isGameCompleted = true; } else { - _currentRound++; _showFeedback = false; _generateLevel(); - _startTimer(); // 타이머 재개 + _startTimer(); } notifyListeners(); }); diff --git a/packages/feature_game_finddiff/lib/models/finddiff_models.dart b/packages/feature_game_finddiff/lib/models/finddiff_models.dart index 6a3f20b..932e8d2 100644 --- a/packages/feature_game_finddiff/lib/models/finddiff_models.dart +++ b/packages/feature_game_finddiff/lib/models/finddiff_models.dart @@ -1,37 +1,43 @@ +// packages/feature_game_finddiff/lib/models/finddiff_models.dart + import 'package:flutter/material.dart'; import 'package:service_api/service_api.dart'; -/// 문제 유형 (무엇이 다른가?) +/// 문제 유형 enum FindDiffType { - color, // 색상이 다름 (빨강 vs 핑크) - icon, // 모양이 다름 (😀 vs 😃) - rotate, // 각도가 다름 (↑ vs ↗) + color, // 색상이 다름 + icon, // 모양이 다름 + rotate, // 각도가 다름 + category, // 범주가 다름 + word, // 글자가 다름 + mix, // 무작위 } /// 개별 아이템 데이터 class FindDiffItem { final int id; - final IconData icon; + final IconData? icon; + final String? textContent; final Color color; - final double angle; // 라디안 (0.0 ~ 2*pi) - final bool isTarget; // 정답 여부 (다른 그림) + final double angle; + final bool isTarget; FindDiffItem({ required this.id, - required this.icon, + this.icon, + this.textContent, required this.color, this.angle = 0.0, this.isTarget = false, }); } -/// 난이도 정의 class FindDiffDifficulty extends GameDifficulty { final int levelIndex; - final int rows; // 격자 행 - final int cols; // 격자 열 - final int timeLimitSeconds; // 제한 시간 - final FindDiffType diffType; // 문제 유형 + final int rows; + final int cols; + final int timeLimitSeconds; + final FindDiffType diffType; const FindDiffDifficulty({ required this.levelIndex, @@ -47,54 +53,89 @@ class FindDiffDifficulty extends GameDifficulty { } class FindDiffDifficulties { - // --- 아이콘 풀 (모양 찾기용) --- - // 서로 비슷하게 생긴 아이콘 쌍 (정답 vs 오답) + // --- [🔥 확장] 모양 찾기용 아이콘 쌍 (20쌍) --- static const List> iconPairs = [ [Icons.sentiment_satisfied_alt, Icons.sentiment_satisfied], // 웃음 vs 미소 [Icons.star, Icons.star_border], // 별 vs 빈별 [Icons.check_circle, Icons.check_circle_outline], // 체크 vs 빈체크 [Icons.favorite, Icons.favorite_border], // 하트 vs 빈하트 [Icons.lock, Icons.lock_open], // 잠금 vs 열림 - [Icons.volume_up, Icons.volume_down], // 소리 큼 vs 작음 - [Icons.battery_full, Icons.battery_alert], // 배터리 가득 vs 경고 - [Icons.signal_wifi_4_bar, Icons.signal_wifi_off], // 와이파이 vs 꺼짐 [Icons.brightness_5, Icons.brightness_4], // 해 vs 달 - [Icons.directions_walk, Icons.directions_run], // 걷기 vs 뛰기 + [Icons.mic, Icons.mic_off], // 마이크 vs 꺼짐 + [Icons.videocam, Icons.videocam_off], // 캠 vs 꺼짐 + [Icons.notifications, Icons.notifications_off], // 알림 vs 꺼짐 + [Icons.wifi, Icons.wifi_off], // 와이파이 vs 꺼짐 + // 추가된 쌍 + [Icons.volume_up, Icons.volume_off], // 소리 vs 무음 + [Icons.visibility, Icons.visibility_off], // 보임 vs 안보임 + [Icons.thumb_up, Icons.thumb_down], // 따봉 vs 역따봉 + [Icons.battery_full, Icons.battery_alert], // 배터리 vs 경고 + [Icons.signal_cellular_4_bar, Icons.signal_cellular_off], // 신호 vs 꺼짐 + [Icons.folder, Icons.folder_open], // 폴더 vs 열림 + [Icons.email, Icons.drafts], // 메일 vs 편지 + [Icons.cloud, Icons.cloud_queue], // 구름 vs 윤곽선 + [Icons.location_on, Icons.location_off], // 위치 vs 꺼짐 + [Icons.bookmark, Icons.bookmark_border], // 북마크 vs 빈북마크 + ]; + + // --- [🔥 신규] 회전 찾기용 아이콘 (방향성이 명확한 것들만) --- + static const List rotateIcons = [ + Icons.flight, Icons.navigation, Icons.send, Icons.north, Icons.cut, + Icons.vpn_key, Icons.gavel, Icons.umbrella, Icons.music_note, Icons.flash_on, + Icons.thumb_up, Icons.pan_tool, Icons.pets, Icons.star, Icons.favorite, + Icons.play_arrow, Icons.call, Icons.build, Icons.brush, Icons.edit, + Icons.search, Icons.flag, Icons.push_pin, Icons.local_dining, Icons.local_taxi ]; - // --- 단일 아이콘 풀 (색상/회전 찾기용) --- - static const List basicIcons = [ + // --- 색상 찾기용 아이콘 (단순한 모양) --- + static const List colorIcons = [ Icons.circle, Icons.square, Icons.star, Icons.favorite, Icons.change_history, Icons.hexagon, Icons.pentagon, Icons.emoji_emotions, Icons.pets, Icons.flight, + Icons.cloud, Icons.local_fire_department, Icons.water_drop, Icons.grass, Icons.sunny + ]; + + // --- [🔥 확장] 카테고리별 이모지 풀 (6개 카테고리) --- + static const Map> emojiCategories = { + 'animal': ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐯", "🦁", "🐮", "🐷", "🐸", "🐔", "🐧", "🐦", "🐤", "🦆", "🦅", "🦉"], + 'food': ["🍎", "🍌", "🍇", "🍓", "🍊", "🍋", "🍉", "🍔", "🍕", "🌭", "🍿", "🍩", "🍪", "🍰", "🍫", "🍬", "🍭", "🍮", "🍯", "🥐"], + 'vehicle': ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "✈️", "🚀", "🚁", "🚂", "🚆", "🚲", "🛵", "🏍️", "⛵", "🚤", "🛳️"], + 'face': ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙"], + 'plant': ["🌵", "🎄", "🌲", "🌳", "🌴", "🌱", "🌿", "☘️", "🍀", "🎍", "🎋", "🍃", "🍂", "🍁", "🍄", "🌾", "💐", "🌷", "🌹", "🥀"], + 'sports': ["⚽", "🏀", "🏈", "⚾", "🥎", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸", "🥅", "🥊", "🥋", "🥇", "🥈", "🥉", "🏅", "🎖", "🏆"], + }; + + // --- 유사 단어 풀 (오타 찾기용) - 기존 100개 단어 사용 (이전 코드 유지) + // (HangulUtils를 사용하므로 baseWords 리스트만 있으면 됩니다. 여기서는 생략하지 않고 포함합니다) + static const List baseWords = [ + "강아지", "고양이", "코끼리", "원숭이", "호랑이", "사자", "기린", "토끼", "다람쥐", "거북이", + "독수리", "갈매기", "비둘기", "참새", "까치", "펭귄", "돌고래", "고래", "상어", "문어", + "선풍기", "냉장고", "세탁기", "청소기", "텔레비전", "컴퓨터", "노트북", "스마트폰", "카메라", "시계", + "자동차", "자전거", "비행기", "지하철", "버스", "기차", "택시", "오토바이", "트럭", "배", + "사과", "바나나", "포도", "수박", "딸기", "오렌지", "키위", "복숭아", "자두", "레몬", + "짜장면", "짬뽕", "탕수육", "김치찌개", "된장찌개", "비빔밥", "불고기", "떡볶이", "김밥", "라면", + "하늘", "구름", "바람", "태양", "달", "별", "우주", "바다", "강", "산", + "학교", "병원", "은행", "우체국", "경찰서", "소방서", "도서관", "박물관", "공원", "시장", + "사랑", "행복", "우정", "가족", "친구", "선생님", "학생", "의사", "군인", "요리사", + "대한민국", "무궁화", "태극기", "애국가", "한글", "세종대왕", "이순신", "독도", "서울", "부산", ]; // [15단계 난이도 구성] static final List allDifficulties = [ - // --- Phase 1: 색상 구분 (초급) --- - // 색상 차이가 뚜렷함 -> 미세함 - const FindDiffDifficulty(levelIndex: 1, name: 'Lv. 1: 색상 (2x2)', contextId: 'DIFF_L1_COLOR_2x2', rows: 2, cols: 2, timeLimitSeconds: 10, diffType: FindDiffType.color), - const FindDiffDifficulty(levelIndex: 2, name: 'Lv. 2: 색상 (3x3)', contextId: 'DIFF_L2_COLOR_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.color), - const FindDiffDifficulty(levelIndex: 3, name: 'Lv. 3: 색상 (4x4)', contextId: 'DIFF_L3_COLOR_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.color), - const FindDiffDifficulty(levelIndex: 4, name: 'Lv. 4: 미세 색상 (4x4)', contextId: 'DIFF_L4_COLOR_HARD', rows: 4, cols: 4, timeLimitSeconds: 6, diffType: FindDiffType.color), - - // --- Phase 2: 모양 구분 (중급) --- - // 비슷한 아이콘 찾기 - const FindDiffDifficulty(levelIndex: 5, name: 'Lv. 5: 모양 (3x3)', contextId: 'DIFF_L5_ICON_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.icon), - const FindDiffDifficulty(levelIndex: 6, name: 'Lv. 6: 모양 (4x4)', contextId: 'DIFF_L6_ICON_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.icon), - const FindDiffDifficulty(levelIndex: 7, name: 'Lv. 7: 모양 (5x5)', contextId: 'DIFF_L7_ICON_5x5', rows: 5, cols: 5, timeLimitSeconds: 8, diffType: FindDiffType.icon), - const FindDiffDifficulty(levelIndex: 8, name: 'Lv. 8: 모양 (6x6)', contextId: 'DIFF_L8_ICON_6x6', rows: 6, cols: 6, timeLimitSeconds: 8, diffType: FindDiffType.icon), - - // --- Phase 3: 회전 구분 (상급) --- - // 같은 아이콘인데 각도가 다름 - const FindDiffDifficulty(levelIndex: 9, name: 'Lv. 9: 회전 (4x4)', contextId: 'DIFF_L9_ROT_4x4', rows: 4, cols: 4, timeLimitSeconds: 8, diffType: FindDiffType.rotate), - const FindDiffDifficulty(levelIndex: 10, name: 'Lv. 10: 회전 (5x5)', contextId: 'DIFF_L10_ROT_5x5', rows: 5, cols: 5, timeLimitSeconds: 7, diffType: FindDiffType.rotate), - const FindDiffDifficulty(levelIndex: 11, name: 'Lv. 11: 회전 (6x6)', contextId: 'DIFF_L11_ROT_6x6', rows: 6, cols: 6, timeLimitSeconds: 6, diffType: FindDiffType.rotate), - - // --- Phase 4: 마스터 (대형 그리드 + 짧은 시간) --- - const FindDiffDifficulty(levelIndex: 12, name: 'Lv. 12: 마스터 (색상 7x7)', contextId: 'DIFF_L12_COLOR_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.color), - const FindDiffDifficulty(levelIndex: 13, name: 'Lv. 13: 마스터 (모양 7x7)', contextId: 'DIFF_L13_ICON_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.icon), - const FindDiffDifficulty(levelIndex: 14, name: 'Lv. 14: 마스터 (회전 7x7)', contextId: 'DIFF_L14_ROT_7x7', rows: 7, cols: 7, timeLimitSeconds: 5, diffType: FindDiffType.rotate), - const FindDiffDifficulty(levelIndex: 15, name: 'Lv. 15: 갓모드 (8x8)', contextId: 'DIFF_L15_GOD_8x8', rows: 8, cols: 8, timeLimitSeconds: 4, diffType: FindDiffType.rotate), + const FindDiffDifficulty(levelIndex: 1, name: 'Lv. 1: 색상 기초 (2x2)', contextId: 'DIFF_L1_COLOR', rows: 2, cols: 2, timeLimitSeconds: 15, diffType: FindDiffType.color), + const FindDiffDifficulty(levelIndex: 2, name: 'Lv. 2: 색상 심화 (3x3)', contextId: 'DIFF_L2_COLOR', rows: 3, cols: 3, timeLimitSeconds: 15, diffType: FindDiffType.color), + const FindDiffDifficulty(levelIndex: 3, name: 'Lv. 3: 모양 기초 (3x3)', contextId: 'DIFF_L3_ICON', rows: 3, cols: 3, timeLimitSeconds: 15, diffType: FindDiffType.icon), + const FindDiffDifficulty(levelIndex: 4, name: 'Lv. 4: 모양 심화 (4x4)', contextId: 'DIFF_L4_ICON', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.icon), + const FindDiffDifficulty(levelIndex: 5, name: 'Lv. 5: 미세 색상 (4x4)', contextId: 'DIFF_L5_COLOR_HARD', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.color), + const FindDiffDifficulty(levelIndex: 6, name: 'Lv. 6: 회전 (4x4)', contextId: 'DIFF_L6_ROT', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.rotate), + const FindDiffDifficulty(levelIndex: 7, name: 'Lv. 7: 회전 (5x5)', contextId: 'DIFF_L7_ROT', rows: 5, cols: 5, timeLimitSeconds: 15, diffType: FindDiffType.rotate), + const FindDiffDifficulty(levelIndex: 8, name: 'Lv. 8: 범주 판단 (4x4)', contextId: 'DIFF_L8_CAT', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.category), + const FindDiffDifficulty(levelIndex: 9, name: 'Lv. 9: 단어 찾기 (3x3)', contextId: 'DIFF_L9_WORD', rows: 3, cols: 3, timeLimitSeconds: 15, diffType: FindDiffType.word), + const FindDiffDifficulty(levelIndex: 10, name: 'Lv. 10: 단어 찾기 (4x4)', contextId: 'DIFF_L10_WORD', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.word), + const FindDiffDifficulty(levelIndex: 11, name: 'Lv. 11: 믹스 챌린지 (4x4)', contextId: 'DIFF_L11_MIX', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.mix), + const FindDiffDifficulty(levelIndex: 12, name: 'Lv. 12: 믹스 챌린지 (5x5)', contextId: 'DIFF_L12_MIX', rows: 5, cols: 5, timeLimitSeconds: 15, diffType: FindDiffType.mix), + const FindDiffDifficulty(levelIndex: 13, name: 'Lv. 13: 마스터 (6x6)', contextId: 'DIFF_L13_MASTER', rows: 6, cols: 6, timeLimitSeconds: 15, diffType: FindDiffType.mix), + const FindDiffDifficulty(levelIndex: 14, name: 'Lv. 14: 그랜드마스터 (7x7)', contextId: 'DIFF_L14_GM', rows: 7, cols: 7, timeLimitSeconds: 15, diffType: FindDiffType.mix), + const FindDiffDifficulty(levelIndex: 15, name: 'Lv. 15: 갓모드 (8x8)', contextId: 'DIFF_L15_GOD', rows: 8, cols: 8, timeLimitSeconds: 15, diffType: FindDiffType.mix), ]; static FindDiffDifficulty getLevel(int levelIndex) { diff --git a/packages/feature_game_finddiff/lib/screens/finddiff_game_screen.dart b/packages/feature_game_finddiff/lib/screens/finddiff_game_screen.dart index acd6769..3b8e32e 100644 --- a/packages/feature_game_finddiff/lib/screens/finddiff_game_screen.dart +++ b/packages/feature_game_finddiff/lib/screens/finddiff_game_screen.dart @@ -9,7 +9,6 @@ import '../models/finddiff_models.dart'; class FindDiffGameScreen extends StatefulWidget { const FindDiffGameScreen({super.key}); - @override State createState() => _FindDiffGameScreenState(); } @@ -17,7 +16,7 @@ class FindDiffGameScreen extends StatefulWidget { class _FindDiffGameScreenState extends State { bool _isDialogShowing = false; - @override + @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -27,14 +26,12 @@ class _FindDiffGameScreenState extends State { void _showGameGuide() { final controller = context.read(); + String message = "나머지와 '다른 하나'를 찾으세요."; - String message = "화면에 있는 그림들 중\n나머지와 '다른 하나'를 찾으세요."; - if (controller.difficulty.diffType == FindDiffType.color) { - message += "\n(색상이 다릅니다)"; - } else if (controller.difficulty.diffType == FindDiffType.icon) { - message += "\n(모양이 다릅니다)"; - } else { - message += "\n(각도가 다릅니다)"; + if (controller.difficulty.diffType == FindDiffType.category) { + message += "\n(성격이 다른 이모지를 찾으세요)\n예: 동물들 속에 섞인 과일"; + } else if (controller.difficulty.diffType == FindDiffType.word) { + message += "\n(글자가 다른 단어를 찾으세요)\n예: 오타 찾기"; } showDialog( @@ -110,11 +107,10 @@ class _FindDiffGameScreenState extends State { if (controller.isGameCompleted && !_isDialogShowing) { _isDialogShowing = true; WidgetsBinding.instance.addPostFrameCallback((_) { - _showGameCompletion(controller); + _showGameCompletion(controller); // 정의 필요 }); } - // 시간 임박 경고 색상 final Color timeColor = controller.remainingTime <= 3 ? theme.colorScheme.error : theme.colorScheme.onSurface; return Scaffold( @@ -132,59 +128,64 @@ class _FindDiffGameScreenState extends State { ), ], ), - body: Column( + body: Stack( // [🔥 수정] Stack으로 감싸서 피드백 오버레이 구현 children: [ - // 정보 바 - Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text("목표: ${controller.score}/10", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)), - Text("오답: ${controller.incorrectCount}", style: const TextStyle(fontSize: 16, color: Colors.grey)), - ], - ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("목표: ${controller.score}/10", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)), + Text("오답: ${controller.incorrectCount}", style: const TextStyle(fontSize: 16, color: Colors.grey)), + ], + ), + ), + + // 게임 그리드 + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LayoutBuilder( + builder: (context, constraints) { + return GridView.builder( + itemCount: controller.items.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: controller.difficulty.cols, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1.0, + ), + itemBuilder: (context, index) { + return _buildItem(controller.items[index], controller); + }, + ); + }, + ), + ), + ), + ], ), - // 피드백 오버레이 + // [🔥 신규] 피드백 오버레이 (정답/오답 표시) if (controller.showFeedback) - Expanded( + Container( + color: Colors.black.withOpacity(0.3), child: Center( child: Icon( controller.isLastAnswerCorrect ? Icons.check_circle : Icons.cancel, - size: 100, + size: 120, color: controller.isLastAnswerCorrect ? Colors.green : theme.colorScheme.error, ), ), - ) - else - // 게임 그리드 - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LayoutBuilder( - builder: (context, constraints) { - return GridView.builder( - itemCount: controller.items.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: controller.difficulty.cols, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - childAspectRatio: 1.0, - ), - itemBuilder: (context, index) { - return _buildItem(controller.items[index], controller); - }, - ); - }, - ), - ), ), ], ), ); } + // 🔽 [🔥 핵심 수정] 아이템 빌더 (아이콘 vs 텍스트 분기) Widget _buildItem(FindDiffItem item, FindDiffController controller) { return GestureDetector( onTap: () => controller.onItemTapped(item), @@ -194,13 +195,26 @@ class _FindDiffGameScreenState extends State { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black12), ), - child: Transform.rotate( - angle: item.angle, - child: Icon( - item.icon, - size: 40, // 동적 크기 조절이 필요하면 LayoutBuilder 활용 가능 - color: item.color, - ), + child: Center( + child: item.textContent != null + ? FittedBox( // 단어/이모지 모드 + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + item.textContent!, + style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + ), + ) + : Transform.rotate( // 아이콘/회전 모드 + angle: item.angle, + child: Icon( + item.icon, + size: 40, + color: item.color, + ), + ), ), ), ); diff --git a/packages/feature_game_finddiff/lib/utils/hangul_utils.dart b/packages/feature_game_finddiff/lib/utils/hangul_utils.dart new file mode 100644 index 0000000..45eddd2 --- /dev/null +++ b/packages/feature_game_finddiff/lib/utils/hangul_utils.dart @@ -0,0 +1,92 @@ +import 'dart:math'; + +class HangulUtils { + static const int FIRST_HANGUL = 44032; // '가' + static const int LAST_HANGUL = 55203; // '힣' + + static const List CHOSUNG_LIST = [ + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + ]; + static const List JUNGSUNG_LIST = [ + 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', + 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ' + ]; + static const List JONGSUNG_LIST = [ + '', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', + 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + ]; + + static final Random _random = Random(); + + /// 단어를 입력받아, 랜덤한 한 글자를 아주 살짝 변형해서 반환 + static String generateSimilarWord(String original) { + if (original.isEmpty) return original; + + // 1. 한글인 문자들의 인덱스만 찾음 + List hangulIndices = []; + for (int i = 0; i < original.length; i++) { + int code = original.codeUnitAt(i); + if (code >= FIRST_HANGUL && code <= LAST_HANGUL) { + hangulIndices.add(i); + } + } + + if (hangulIndices.isEmpty) return original; // 한글 없으면 그대로 반환 + + // 2. 바꿀 글자 위치 랜덤 선택 + int targetIndex = hangulIndices[_random.nextInt(hangulIndices.length)]; + String targetChar = original[targetIndex]; + + // 3. 글자 변형 + String modifiedChar = _tweakHangul(targetChar); + + // 4. 조합해서 반환 + return original.replaceRange(targetIndex, targetIndex + 1, modifiedChar); + } + + /// 한 글자를 분해해서 초/중/종성 중 하나를 살짝 바꿈 + static String _tweakHangul(String char) { + int code = char.codeUnitAt(0) - FIRST_HANGUL; + + int choIndex = code ~/ (21 * 28); + int jungIndex = (code % (21 * 28)) ~/ 28; + int jongIndex = code % 28; + + // 무엇을 바꿀지 결정 (0:초성, 1:중성, 2:종성) + // 종성이 없는 글자('가')라면 종성을 추가하는 것도 방법이지만, + // 너무 티가 나므로 초/중성 위주로 변경 + int type = _random.nextInt(3); + + if (type == 0) { + // [초성 변경] 비슷한 모양으로 변경 시도 + choIndex = _getSimilarIndex(choIndex, CHOSUNG_LIST.length); + } else if (type == 1) { + // [중성 변경] (ㅏ -> ㅑ, ㅗ -> ㅜ 등) + jungIndex = _getSimilarIndex(jungIndex, JUNGSUNG_LIST.length); + } else { + // [종성 변경] + jongIndex = _getSimilarIndex(jongIndex, JONGSUNG_LIST.length); + } + + // 재조립 + int newCode = FIRST_HANGUL + (choIndex * 21 * 28) + (jungIndex * 28) + jongIndex; + return String.fromCharCode(newCode); + } + + /// 인덱스를 랜덤하게 +-1 하거나 변경하여 "비슷한" 느낌을 줌 + static int _getSimilarIndex(int current, int max) { + // 단순히 랜덤이 아니라, 현재 값과 가까운 값으로 변경하면 더 헷갈림 + int offset = _random.nextBool() ? 1 : -1; + int next = current + offset; + + if (next < 0) next = 1; + if (next >= max) next = max - 2; + + // 만약 원래랑 같아져버리면 강제로 변경 + if (next == current) next = (current + 1) % max; + + return next; + } +} \ No newline at end of file