This commit is contained in:
lunaticbum 2025-11-21 15:54:17 +09:00
parent 6335220e57
commit de84353fc5
5 changed files with 364 additions and 149 deletions

View File

@ -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/"}]} {"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/"}]}

View File

@ -1,9 +1,8 @@
// packages/feature_game_finddiff/lib/controllers/finddiff_controller.dart
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/finddiff_models.dart'; import '../models/finddiff_models.dart';
import '../utils/hangul_utils.dart';
class FindDiffController with ChangeNotifier { class FindDiffController with ChangeNotifier {
late FindDiffDifficulty difficulty; late FindDiffDifficulty difficulty;
@ -13,7 +12,6 @@ class FindDiffController with ChangeNotifier {
List<FindDiffItem> _items = []; List<FindDiffItem> _items = [];
List<FindDiffItem> get items => _items; List<FindDiffItem> get items => _items;
//
int _currentRound = 0; int _currentRound = 0;
int get currentRound => _currentRound; int get currentRound => _currentRound;
@ -26,21 +24,23 @@ class FindDiffController with ChangeNotifier {
bool _isGameCompleted = false; bool _isGameCompleted = false;
bool get isGameCompleted => _isGameCompleted; bool get isGameCompleted => _isGameCompleted;
//
Timer? _timer; Timer? _timer;
int _remainingTime = 0; int _remainingTime = 0;
int get remainingTime => _remainingTime; int get remainingTime => _remainingTime;
int _secondsElapsed = 0;
int get secondsElapsed => _secondsElapsed;
// [🔥 ] ( true)
bool _isGameStarted = false; bool _isGameStarted = false;
bool get isGameStarted => _isGameStarted; bool get isGameStarted => _isGameStarted;
// [🔥 ]
bool _showFeedback = false; bool _showFeedback = false;
bool get showFeedback => _showFeedback; bool get showFeedback => _showFeedback;
bool _isLastAnswerCorrect = false; bool _isLastAnswerCorrect = false;
bool get isLastAnswerCorrect => _isLastAnswerCorrect; bool get isLastAnswerCorrect => _isLastAnswerCorrect;
FindDiffType _currentRoundType = FindDiffType.color;
FindDiffType get currentRoundType => _currentRoundType;
final Random _random = Random(); final Random _random = Random();
void setUserInfo(String userId, String? userName) { void setUserInfo(String userId, String? userName) {
@ -54,11 +54,11 @@ class FindDiffController with ChangeNotifier {
_incorrectCount = 0; _incorrectCount = 0;
_currentRound = 1; _currentRound = 1;
_isGameCompleted = false; _isGameCompleted = false;
_isGameStarted = false; // _secondsElapsed = 0;
_isGameStarted = false;
_showFeedback = false; _showFeedback = false;
_generateLevel(); _generateLevel();
// [🔥 ]
notifyListeners(); notifyListeners();
} }
@ -66,7 +66,6 @@ class FindDiffController with ChangeNotifier {
startNewGame(difficulty); startNewGame(difficulty);
} }
// [🔥 ] UI에서
void startGameTimer() { void startGameTimer() {
if (_isGameStarted) return; if (_isGameStarted) return;
_isGameStarted = true; _isGameStarted = true;
@ -79,53 +78,126 @@ class FindDiffController with ChangeNotifier {
final int totalItems = difficulty.totalItems; final int totalItems = difficulty.totalItems;
final int targetIndex = _random.nextInt(totalItems); final int targetIndex = _random.nextInt(totalItems);
// 1. // 1.
IconData baseIcon = Icons.circle; if (difficulty.diffType == FindDiffType.mix) {
const List<FindDiffType> 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; Color baseColor = Colors.blue;
double baseAngle = 0.0; double baseAngle = 0.0;
// // [🔥 ] /
if (difficulty.diffType == FindDiffType.icon) { 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;
} else if (_currentRoundType == FindDiffType.icon) {
//
final pair = FindDiffDifficulties.iconPairs[_random.nextInt(FindDiffDifficulties.iconPairs.length)]; 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 { } 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)); baseColor = Color.fromARGB(255, _random.nextInt(200), _random.nextInt(200), _random.nextInt(200));
} }
// 2. //
List<String> baseCatList = [];
List<String> 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++) { for (int i = 0; i < totalItems; i++) {
bool isTarget = (i == targetIndex); bool isTarget = (i == targetIndex);
IconData icon = baseIcon; IconData? icon = baseIcon;
String? textContent = baseText;
Color color = baseColor; Color color = baseColor;
double angle = baseAngle; double angle = baseAngle;
if (isTarget) { switch (_currentRoundType) {
// case FindDiffType.color:
switch (difficulty.diffType) { if (isTarget) {
case FindDiffType.color: //
int offset = (difficulty.levelIndex >= 4) ? 30 : 60; int offset = (difficulty.levelIndex >= 5) ? 30 : 60;
color = Color.fromARGB( color = Color.fromARGB(255, (baseColor.red + offset) % 255, (baseColor.green + offset) % 255, (baseColor.blue + offset) % 255);
255, }
(baseColor.red + offset) % 255, break;
(baseColor.green + offset) % 255,
(baseColor.blue + offset) % 255, case FindDiffType.icon:
); if (isTarget) {
break; final pair = FindDiffDifficulties.iconPairs.firstWhere((p) => p.contains(baseIcon));
case FindDiffType.icon: icon = (baseIcon == pair[0]) ? pair[1] : pair[0];
final pair = FindDiffDifficulties.iconPairs.firstWhere((p) => p.contains(baseIcon)); }
icon = (baseIcon == pair[0]) ? pair[1] : pair[0]; break;
break;
case FindDiffType.rotate: case FindDiffType.rotate:
angle = 0.5; // 30 if (isTarget) angle = 0.5; // 30
break; 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( _items.add(FindDiffItem(
id: i, id: i,
icon: icon, icon: icon,
textContent: textContent,
color: color, color: color,
angle: angle, angle: angle,
isTarget: isTarget, isTarget: isTarget,
@ -135,13 +207,13 @@ class FindDiffController with ChangeNotifier {
_remainingTime = difficulty.timeLimitSeconds; _remainingTime = difficulty.timeLimitSeconds;
} }
// ... ( ) ...
void _startTimer() { void _startTimer() {
_timer?.cancel(); _timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_secondsElapsed++;
_remainingTime--; _remainingTime--;
if (_remainingTime <= 0) { if (_remainingTime <= 0) {
// :
_handleAnswer(false); _handleAnswer(false);
} }
notifyListeners(); notifyListeners();
@ -150,12 +222,11 @@ class FindDiffController with ChangeNotifier {
void onItemTapped(FindDiffItem item) { void onItemTapped(FindDiffItem item) {
if (!_isGameStarted || _isGameCompleted || _showFeedback) return; if (!_isGameStarted || _isGameCompleted || _showFeedback) return;
_handleAnswer(item.isTarget); _handleAnswer(item.isTarget);
} }
void _handleAnswer(bool isCorrect) { void _handleAnswer(bool isCorrect) {
_timer?.cancel(); // _timer?.cancel();
_showFeedback = true; _showFeedback = true;
_isLastAnswerCorrect = isCorrect; _isLastAnswerCorrect = isCorrect;
@ -164,18 +235,15 @@ class FindDiffController with ChangeNotifier {
} else { } else {
_incorrectCount++; _incorrectCount++;
} }
notifyListeners(); notifyListeners();
//
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (_score >= 10) { // [] 10 if (_score >= 10) {
_isGameCompleted = true; _isGameCompleted = true;
} else { } else {
_currentRound++;
_showFeedback = false; _showFeedback = false;
_generateLevel(); _generateLevel();
_startTimer(); // _startTimer();
} }
notifyListeners(); notifyListeners();
}); });

View File

@ -1,37 +1,43 @@
// packages/feature_game_finddiff/lib/models/finddiff_models.dart
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 FindDiffType { enum FindDiffType {
color, // ( vs ) color, //
icon, // (😀 vs 😃) icon, //
rotate, // ( vs ) rotate, //
category, //
word, //
mix, //
} }
/// ///
class FindDiffItem { class FindDiffItem {
final int id; final int id;
final IconData icon; final IconData? icon;
final String? textContent;
final Color color; final Color color;
final double angle; // (0.0 ~ 2*pi) final double angle;
final bool isTarget; // ( ) final bool isTarget;
FindDiffItem({ FindDiffItem({
required this.id, required this.id,
required this.icon, this.icon,
this.textContent,
required this.color, required this.color,
this.angle = 0.0, this.angle = 0.0,
this.isTarget = false, this.isTarget = false,
}); });
} }
///
class FindDiffDifficulty extends GameDifficulty { class FindDiffDifficulty extends GameDifficulty {
final int levelIndex; final int levelIndex;
final int rows; // final int rows;
final int cols; // final int cols;
final int timeLimitSeconds; // final int timeLimitSeconds;
final FindDiffType diffType; // final FindDiffType diffType;
const FindDiffDifficulty({ const FindDiffDifficulty({
required this.levelIndex, required this.levelIndex,
@ -47,54 +53,89 @@ class FindDiffDifficulty extends GameDifficulty {
} }
class FindDiffDifficulties { class FindDiffDifficulties {
// --- ( ) --- // --- [🔥 ] (20) ---
// ( vs )
static const List<List<IconData>> iconPairs = [ static const List<List<IconData>> iconPairs = [
[Icons.sentiment_satisfied_alt, Icons.sentiment_satisfied], // vs [Icons.sentiment_satisfied_alt, Icons.sentiment_satisfied], // vs
[Icons.star, Icons.star_border], // vs [Icons.star, Icons.star_border], // vs
[Icons.check_circle, Icons.check_circle_outline], // vs [Icons.check_circle, Icons.check_circle_outline], // vs
[Icons.favorite, Icons.favorite_border], // vs [Icons.favorite, Icons.favorite_border], // vs
[Icons.lock, Icons.lock_open], // 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.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<IconData> basicIcons = [ static const List<IconData> 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<IconData> colorIcons = [
Icons.circle, Icons.square, Icons.star, Icons.favorite, Icons.change_history, Icons.circle, Icons.square, Icons.star, Icons.favorite, Icons.change_history,
Icons.hexagon, Icons.pentagon, Icons.emoji_emotions, Icons.pets, Icons.flight, 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<String, List<String>> emojiCategories = {
'animal': ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐯", "🦁", "🐮", "🐷", "🐸", "🐔", "🐧", "🐦", "🐤", "🦆", "🦅", "🦉"],
'food': ["🍎", "🍌", "🍇", "🍓", "🍊", "🍋", "🍉", "🍔", "🍕", "🌭", "🍿", "🍩", "🍪", "🍰", "🍫", "🍬", "🍭", "🍮", "🍯", "🥐"],
'vehicle': ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "✈️", "🚀", "🚁", "🚂", "🚆", "🚲", "🛵", "🏍️", "", "🚤", "🛳️"],
'face': ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙"],
'plant': ["🌵", "🎄", "🌲", "🌳", "🌴", "🌱", "🌿", "☘️", "🍀", "🎍", "🎋", "🍃", "🍂", "🍁", "🍄", "🌾", "💐", "🌷", "🌹", "🥀"],
'sports': ["", "🏀", "🏈", "", "🥎", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸", "🥅", "🥊", "🥋", "🥇", "🥈", "🥉", "🏅", "🎖", "🏆"],
};
// --- ( ) - 100 ( )
// (HangulUtils를 baseWords . )
static const List<String> baseWords = [
"강아지", "고양이", "코끼리", "원숭이", "호랑이", "사자", "기린", "토끼", "다람쥐", "거북이",
"독수리", "갈매기", "비둘기", "참새", "까치", "펭귄", "돌고래", "고래", "상어", "문어",
"선풍기", "냉장고", "세탁기", "청소기", "텔레비전", "컴퓨터", "노트북", "스마트폰", "카메라", "시계",
"자동차", "자전거", "비행기", "지하철", "버스", "기차", "택시", "오토바이", "트럭", "",
"사과", "바나나", "포도", "수박", "딸기", "오렌지", "키위", "복숭아", "자두", "레몬",
"짜장면", "짬뽕", "탕수육", "김치찌개", "된장찌개", "비빔밥", "불고기", "떡볶이", "김밥", "라면",
"하늘", "구름", "바람", "태양", "", "", "우주", "바다", "", "",
"학교", "병원", "은행", "우체국", "경찰서", "소방서", "도서관", "박물관", "공원", "시장",
"사랑", "행복", "우정", "가족", "친구", "선생님", "학생", "의사", "군인", "요리사",
"대한민국", "무궁화", "태극기", "애국가", "한글", "세종대왕", "이순신", "독도", "서울", "부산",
]; ];
// [15 ] // [15 ]
static final List<FindDiffDifficulty> allDifficulties = [ static final List<FindDiffDifficulty> allDifficulties = [
// --- Phase 1: () --- 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: 1, name: 'Lv. 1: 색상 (2x2)', contextId: 'DIFF_L1_COLOR_2x2', rows: 2, cols: 2, timeLimitSeconds: 10, 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: 2, name: 'Lv. 2: 색상 (3x3)', contextId: 'DIFF_L2_COLOR_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.color), const FindDiffDifficulty(levelIndex: 4, name: 'Lv. 4: 모양 심화 (4x4)', contextId: 'DIFF_L4_ICON', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.icon),
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: 5, name: 'Lv. 5: 미세 색상 (4x4)', contextId: 'DIFF_L5_COLOR_HARD', rows: 4, cols: 4, timeLimitSeconds: 15, 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), 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),
// --- Phase 2: () --- 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: 5, name: 'Lv. 5: 모양 (3x3)', contextId: 'DIFF_L5_ICON_3x3', rows: 3, cols: 3, timeLimitSeconds: 10, diffType: FindDiffType.icon), const FindDiffDifficulty(levelIndex: 10, name: 'Lv. 10: 단어 찾기 (4x4)', contextId: 'DIFF_L10_WORD', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.word),
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: 11, name: 'Lv. 11: 믹스 챌린지 (4x4)', contextId: 'DIFF_L11_MIX', rows: 4, cols: 4, timeLimitSeconds: 15, diffType: FindDiffType.mix),
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: 12, name: 'Lv. 12: 믹스 챌린지 (5x5)', contextId: 'DIFF_L12_MIX', rows: 5, cols: 5, timeLimitSeconds: 15, diffType: FindDiffType.mix),
const FindDiffDifficulty(levelIndex: 8, name: 'Lv. 8: 모양 (6x6)', contextId: 'DIFF_L8_ICON_6x6', rows: 6, cols: 6, timeLimitSeconds: 8, diffType: FindDiffType.icon), 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),
// --- Phase 3: () --- const FindDiffDifficulty(levelIndex: 15, name: 'Lv. 15: 갓모드 (8x8)', contextId: 'DIFF_L15_GOD', rows: 8, cols: 8, timeLimitSeconds: 15, diffType: FindDiffType.mix),
//
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),
]; ];
static FindDiffDifficulty getLevel(int levelIndex) { static FindDiffDifficulty getLevel(int levelIndex) {

View File

@ -9,7 +9,6 @@ import '../models/finddiff_models.dart';
class FindDiffGameScreen extends StatefulWidget { class FindDiffGameScreen extends StatefulWidget {
const FindDiffGameScreen({super.key}); const FindDiffGameScreen({super.key});
@override @override
State<FindDiffGameScreen> createState() => _FindDiffGameScreenState(); State<FindDiffGameScreen> createState() => _FindDiffGameScreenState();
} }
@ -17,7 +16,7 @@ class FindDiffGameScreen extends StatefulWidget {
class _FindDiffGameScreenState extends State<FindDiffGameScreen> { class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
bool _isDialogShowing = false; bool _isDialogShowing = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -27,14 +26,12 @@ class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
void _showGameGuide() { void _showGameGuide() {
final controller = context.read<FindDiffController>(); final controller = context.read<FindDiffController>();
String message = "나머지와 '다른 하나'를 찾으세요.";
String message = "화면에 있는 그림들 중\n나머지와 '다른 하나'를 찾으세요."; if (controller.difficulty.diffType == FindDiffType.category) {
if (controller.difficulty.diffType == FindDiffType.color) { message += "\n(성격이 다른 이모지를 찾으세요)\n예: 동물들 속에 섞인 과일";
message += "\n(색상이 다릅니다)"; } else if (controller.difficulty.diffType == FindDiffType.word) {
} else if (controller.difficulty.diffType == FindDiffType.icon) { message += "\n(글자가 다른 단어를 찾으세요)\n예: 오타 찾기";
message += "\n(모양이 다릅니다)";
} else {
message += "\n(각도가 다릅니다)";
} }
showDialog( showDialog(
@ -110,11 +107,10 @@ class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
if (controller.isGameCompleted && !_isDialogShowing) { if (controller.isGameCompleted && !_isDialogShowing) {
_isDialogShowing = true; _isDialogShowing = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_showGameCompletion(controller); _showGameCompletion(controller); //
}); });
} }
//
final Color timeColor = controller.remainingTime <= 3 ? theme.colorScheme.error : theme.colorScheme.onSurface; final Color timeColor = controller.remainingTime <= 3 ? theme.colorScheme.error : theme.colorScheme.onSurface;
return Scaffold( return Scaffold(
@ -132,59 +128,64 @@ class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
), ),
], ],
), ),
body: Column( body: Stack( // [🔥 ] Stack으로
children: [ children: [
// Column(
Padding( children: [
padding: const EdgeInsets.all(12.0), Padding(
child: Row( padding: const EdgeInsets.all(12.0),
mainAxisAlignment: MainAxisAlignment.spaceAround, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceAround,
Text("목표: ${controller.score}/10", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)), children: [
Text("오답: ${controller.incorrectCount}", style: const TextStyle(fontSize: 16, color: Colors.grey)), 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) if (controller.showFeedback)
Expanded( Container(
color: Colors.black.withOpacity(0.3),
child: Center( child: Center(
child: Icon( child: Icon(
controller.isLastAnswerCorrect ? Icons.check_circle : Icons.cancel, controller.isLastAnswerCorrect ? Icons.check_circle : Icons.cancel,
size: 100, size: 120,
color: controller.isLastAnswerCorrect ? Colors.green : theme.colorScheme.error, 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) { Widget _buildItem(FindDiffItem item, FindDiffController controller) {
return GestureDetector( return GestureDetector(
onTap: () => controller.onItemTapped(item), onTap: () => controller.onItemTapped(item),
@ -194,13 +195,26 @@ class _FindDiffGameScreenState extends State<FindDiffGameScreen> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.black12), border: Border.all(color: Colors.black12),
), ),
child: Transform.rotate( child: Center(
angle: item.angle, child: item.textContent != null
child: Icon( ? FittedBox( // /
item.icon, fit: BoxFit.scaleDown,
size: 40, // LayoutBuilder child: Padding(
color: item.color, 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,
),
),
), ),
), ),
); );

View File

@ -0,0 +1,92 @@
import 'dart:math';
class HangulUtils {
static const int FIRST_HANGUL = 44032; // ''
static const int LAST_HANGUL = 55203; // ''
static const List<String> CHOSUNG_LIST = [
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', ''
];
static const List<String> JUNGSUNG_LIST = [
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', ''
];
static const List<String> JONGSUNG_LIST = [
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', ''
];
static final Random _random = Random();
/// ,
static String generateSimilarWord(String original) {
if (original.isEmpty) return original;
// 1.
List<int> 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;
}
}