From 2008c377f4800d3081f22872e8ea5c277c430b0e Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Wed, 19 Nov 2025 11:17:33 +0900 Subject: [PATCH] ... --- .vscode/launch.json | 15 + apps/app_mathquiz/devtools_options.yaml | 3 + .../lib/screens/home_screen.dart | 156 ----- .../feature_common/lib/views/intro_view.dart | 2 +- .../lib/controllers/math_quiz_controller.dart | 225 ++++-- .../lib/controllers/math_quiz_generator.dart | 654 +++++++++++------- .../lib/models/math_quiz_difficulty.dart | 153 ++-- .../lib/models/math_quiz_models.dart | 37 +- .../lib/screens/math_quiz_lobby_screen.dart | 189 ++--- .../lib/screens/math_quiz_screen.dart | 181 +++-- .../controllers/spider_game_controller.dart | 145 ++-- .../lib/screens/spider_game_screen.dart | 2 +- .../lib/screens/spider_lobby_screen.dart | 205 +++--- .../lib/models/game_level.dart | 120 ++-- .../lib/screens/sudoku_lobby_screen.dart | 220 +++--- packages/service_api/lib/service_api.dart | 5 +- .../lib/services/lobby_helper_service.dart | 70 ++ 17 files changed, 1416 insertions(+), 966 deletions(-) create mode 100644 apps/app_mathquiz/devtools_options.yaml delete mode 100644 packages/feature_common/lib/screens/home_screen.dart rename packages/{service_api => feature_game_mathquiz}/lib/models/math_quiz_difficulty.dart (61%) create mode 100644 packages/service_api/lib/services/lobby_helper_service.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 898efdf..c133dda 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "my_game_center", "request": "launch", @@ -15,6 +16,20 @@ "request": "launch", "type": "dart" }, + { + "name": "app_spider (profile mode)", + "cwd": "apps/app_spider", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "app_mathquiz (profile mode)", + "cwd": "apps/app_mathquiz", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, { "name": "app_sudoku (profile mode)", "cwd": "apps/app_sudoku", diff --git a/apps/app_mathquiz/devtools_options.yaml b/apps/app_mathquiz/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/apps/app_mathquiz/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/packages/feature_common/lib/screens/home_screen.dart b/packages/feature_common/lib/screens/home_screen.dart deleted file mode 100644 index 6ea7757..0000000 --- a/packages/feature_common/lib/screens/home_screen.dart +++ /dev/null @@ -1,156 +0,0 @@ -// // packages/feature_common/lib/screens/home_screen.dart - -// import 'dart:developer'; -// import 'package:flutter/material.dart'; -// import 'package:provider/provider.dart'; - -// // ๐Ÿ”ฝ [์ˆ˜์ •] ์„œ๋น„์Šค๋งŒ importํ•˜๊ณ , ๋ชจ๋ธ์€ ์ƒˆ๋กœ ๋งŒ๋“  GameInfo๋ฅผ ์‚ฌ์šฉ -// import 'package:service_api/service_api.dart'; -// import '../models/game_info.dart'; // ๐Ÿ‘ˆ GameInfo ๋ชจ๋ธ import - -// // ๐Ÿ”ฝ [์ˆ˜์ •] ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋˜ ์œ„์ ฏ/ํ™”๋ฉด import -// import 'ranking_screen.dart'; -// import 'settings_screen.dart'; -// import '../widgets/ad_banner_widget.dart'; - -// class HomeScreen extends StatefulWidget { -// // ๐Ÿ”ฝ [์ˆ˜์ •] 'onStartGame' ๋Œ€์‹  'availableGames' ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฃผ์ž…๋ฐ›์Œ -// final List availableGames; - -// const HomeScreen({ -// super.key, -// required this.availableGames, // ๐Ÿ‘ˆ ์ƒ์„ฑ์ž ๋ณ€๊ฒฝ -// }); - -// @override -// State createState() => _HomeScreenState(); -// } - -// class _HomeScreenState extends State { -// // ๐Ÿ”ฝ [์‚ญ์ œ] ์Šค๋„์ฟ  ์ „์šฉ ์ƒํƒœ ๋ณ€์ˆ˜๋“ค ๋ชจ๋‘ ์‚ญ์ œ -// // int _maxUnlockedLevel = 1; -// // Map _rankHistory = {}; -// // String? _userName; -// // late String _selectedThemeName; -// // bool _isLoading = false; - -// // ๐Ÿ”ฝ [์‚ญ์ œ] ์Šค๋„์ฟ  ์ „์šฉ ์„œ๋น„์Šค๋“ค ์‚ญ์ œ -// // final PuzzleService _puzzleService = PuzzleService(); -// // final IdentityService _identityService = IdentityService(); - -// @override -// void initState() { -// super.initState(); -// // ๐Ÿ”ฝ [์‚ญ์ œ] _loadProgress() ๋“ฑ ์Šค๋„์ฟ  ์ „์šฉ ๋กœ์ง ์‚ญ์ œ -// } - -// // ๐Ÿ”ฝ [์‚ญ์ œ] _loadProgress ๋ฉ”์„œ๋“œ ์ „์ฒด ์‚ญ์ œ -// // Future _loadProgress() async { ... } - -// @override -// Widget build(BuildContext context) { -// context.watch(); -// final theme = Theme.of(context); - -// return Scaffold( -// appBar: AppBar( -// // ๐Ÿ”ฝ [์ˆ˜์ •] ์•ฑ ์ด๋ฆ„์€ main.dart์—์„œ ์„ค์ •ํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„  ๋น„์›€ -// title: const Text('๊ฒŒ์ž„ ์„ผํ„ฐ'), -// actions: [ -// IconButton( -// icon: const Icon(Icons.settings_outlined), -// onPressed: () { -// Navigator.push( -// context, -// MaterialPageRoute( -// builder: (context) => const SettingsScreen(), -// ), -// ); -// }, -// ), -// ], -// ), -// body: LayoutBuilder( -// builder: (context, constraints) { -// const double maxContentRatio = 0.6; -// final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 -// ? 500 : (constraints.maxHeight * maxContentRatio); - -// return Center( -// child: ConstrainedBox( -// constraints: BoxConstraints(maxWidth: constrainedWidth), -// child: Column( -// children: [ -// // ๐Ÿ”ฝ [์‚ญ์ œ] ์Šค๋„์ฟ  ์ „์šฉ 'ํ…Œ๋งˆ ์„ ํƒ' Dropdown ์‚ญ์ œ - -// // 2. ๋ ˆ๋ฒจ ์„ ํƒ ๋ฆฌ์ŠคํŠธ (๋ฒ”์šฉ์œผ๋กœ ๋ณ€๊ฒฝ) -// Expanded( -// // ๐Ÿ”ฝ [์ˆ˜์ •] ListView.builder๊ฐ€ ์ฃผ์ž…๋ฐ›์€ 'widget.availableGames' ์‚ฌ์šฉ -// child: ListView.builder( -// itemCount: widget.availableGames.length, -// itemBuilder: (context, index) { -// final GameInfo game = widget.availableGames[index]; - -// return Card( -// margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), -// child: ListTile( -// leading: Icon( -// game.icon, // ๐Ÿ‘ˆ GameInfo์—์„œ ์•„์ด์ฝ˜ ๊ฐ€์ ธ์˜ค๊ธฐ -// color: theme.primaryColor, -// ), -// title: Text(game.name, style: const TextStyle( // ๐Ÿ‘ˆ GameInfo์—์„œ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ -// fontSize: 18, -// fontWeight: FontWeight.bold, -// )), -// trailing: const Icon(Icons.play_arrow_rounded), - -// // ๐Ÿ”ฝ [์ˆ˜์ •] onTap์— ์ฃผ์ž…๋ฐ›์€ game.onTap ํ•จ์ˆ˜ ์—ฐ๊ฒฐ -// onTap: game.onTap, -// ), -// ); -// }, -// ), -// ), - -// // 3. ๋žญํ‚น ๋ณด๊ธฐ ๋ฒ„ํŠผ (๋žญํ‚น ์Šคํฌ๋ฆฐ์€ ๊ณตํ†ต์ด๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋‘ ) -// Container( -// margin: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 8.0), -// // ... (์ดํ•˜ ๋žญํ‚น ๋ณด๊ธฐ ๋ฒ„ํŠผ ์Šคํƒ€์ผ์€ ๋™์ผ) ... -// child: InkWell( -// onTap: () { -// // ๐Ÿ”ฝ [์ˆ˜์ •] ์Šค๋„์ฟ  ๋ ˆ๋ฒจ ๋Œ€์‹  ๊ธฐ๋ณธ ๋žญํ‚น ํ™”๋ฉด์œผ๋กœ -// Navigator.push( -// context, -// MaterialPageRoute( -// builder: (context) => const RankingScreen( -// // initialDifficultyName: "์ค‘๊ธ‰ (9x9)", // ๐Ÿ‘ˆ ํ•„์š”์‹œ ํ•˜๋“œ์ฝ”๋”ฉ -// ), -// ), -// ); -// }, -// child: Container( -// width: double.infinity, -// padding: const EdgeInsets.symmetric(vertical: 14.0), -// child: Text( -// '๐Ÿ† ์ „์ฒด ๋žญํ‚น ๋ณด๊ธฐ', -// textAlign: TextAlign.center, -// style: TextStyle( -// fontSize: 16, -// fontWeight: FontWeight.bold, -// color: theme.colorScheme.onSurfaceVariant, -// ), -// ), -// ), -// ), -// // ... -// ), -// ], -// ), -// ), -// ); -// }, -// ), -// bottomNavigationBar: const AdBannerWidget(), -// ); -// } -// } \ No newline at end of file diff --git a/packages/feature_common/lib/views/intro_view.dart b/packages/feature_common/lib/views/intro_view.dart index 6f53502..844f620 100644 --- a/packages/feature_common/lib/views/intro_view.dart +++ b/packages/feature_common/lib/views/intro_view.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; // ๐Ÿ‘ˆ [ํ•ต์‹ฌ] ์ด import ๋ฌธ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +import 'package:flutter/material.dart'; /// [์ˆ˜์ •] "SBSPACE"๋ฅผ ํ•œ ์ค„๋กœ ๊ทธ๋ฆฌ๋Š” IntroView /// diff --git a/packages/feature_game_mathquiz/lib/controllers/math_quiz_controller.dart b/packages/feature_game_mathquiz/lib/controllers/math_quiz_controller.dart index 626d4d6..ea7f6d3 100644 --- a/packages/feature_game_mathquiz/lib/controllers/math_quiz_controller.dart +++ b/packages/feature_game_mathquiz/lib/controllers/math_quiz_controller.dart @@ -1,15 +1,17 @@ -import 'dart:async'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] Timer +import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:service_api/service_api.dart'; +import 'package:service_api/service_api.dart'; +import 'package:function_tree/function_tree.dart'; import 'math_quiz_generator.dart'; import '../models/math_quiz_models.dart'; +import '../models/math_quiz_difficulty.dart'; // ๐Ÿ‘ˆ MathQuizDifficulty ์ •์˜ ํ•„์š” class MathQuizController with ChangeNotifier { late final MathQuizDifficulty difficulty; - late final MathQuizPuzzle puzzle; late final String userId; late final String? userName; + late MathQuizPuzzle puzzle; late List _userAnswers; List get userAnswers => _userAnswers; @@ -19,78 +21,142 @@ class MathQuizController with ChangeNotifier { bool _isGameCompleted = false; bool get isGameCompleted => _isGameCompleted; - // ๐Ÿ”ฝ [์ถ”๊ฐ€] ํƒ€์ด๋จธ ๋ฐ ์‹œ๊ฐ„ Timer? _timer; int _secondsElapsed = 0; int get secondsElapsed => _secondsElapsed; - - // ๐Ÿ”ฝ [์ถ”๊ฐ€] ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ œ๊ฑฐ๋  ๋•Œ ํƒ€์ด๋จธ ํ•ด์ œ + + late final int _totalPuzzlesInLevel; + int _currentPuzzleIndex = 0; + int get totalPuzzlesInLevel => _totalPuzzlesInLevel; + int get currentPuzzleIndex => _currentPuzzleIndex; + + bool _isWrongAnswer = false; + bool get isWrongAnswer => _isWrongAnswer; + + int _totalBlanksFilled = 0; + int get totalBlanksFilled => _totalBlanksFilled; + + int _remainingTries = 3; + int get remainingTries => _remainingTries; + + bool _isRevealingAnswer = false; + bool get isRevealingAnswer => _isRevealingAnswer; + + /// ํ˜„์žฌ ์„ ํƒ๋œ ๋นˆ์นธ์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ + PuzzleBlankType? get currentSelectedBlankType { + if (puzzle.blankTypes.isEmpty || + _selectedBlankIndex < 0 || + _selectedBlankIndex >= puzzle.blankTypes.length) { + return null; + } + return puzzle.blankTypes[_selectedBlankIndex]; + } + @override void dispose() { _timer?.cancel(); super.dispose(); } - // ๐Ÿ”ฝ [์ถ”๊ฐ€] ํƒ€์ด๋จธ ์‹œ์ž‘ void _startTimer() { _timer?.cancel(); _secondsElapsed = 0; _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _secondsElapsed++; - notifyListeners(); // ๋งค์ดˆ UI ๊ฐฑ์‹  (์‹œ๊ฐ„ ํ‘œ์‹œ์šฉ) + notifyListeners(); }); } - // ๐Ÿ”ฝ [์ถ”๊ฐ€] ํƒ€์ด๋จธ ์ •์ง€ void _stopTimer() { _timer?.cancel(); } - /// 1. ๋กœ๋น„์—์„œ ํ˜ธ์ถœ: ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ void startNewGame(MathQuizDifficulty level, String userId, String? userName) { this.difficulty = level; this.userId = userId; this.userName = userName; + _totalPuzzlesInLevel = level.puzzleCount; + _currentPuzzleIndex = 0; + _isGameCompleted = false; + _totalBlanksFilled = 0; + _loadNextPuzzle(); + _startTimer(); + notifyListeners(); + } + /// [๐Ÿ”ฅ ์ˆ˜์ •] ๋ฌธ์ œ ๋กœ๋“œ ์‹œ ๋กœ๊ทธ ์ถ”๊ฐ€ + void _loadNextPuzzle() { final generator = MathQuizGenerator(); - this.puzzle = generator.generatePuzzle(level); + this.puzzle = generator.generatePuzzle(difficulty); + + // --- [LOG: ๋ฌธ์ œ ๊ตฌ์กฐ ํ™•์ธ] --- + debugPrint("--- MATH QUIZ PUZZLE LOADED (${difficulty.contextId}) ---"); + debugPrint("Grid: ${puzzle.gridCells}"); + debugPrint("Solutions (S): ${puzzle.solutions}"); + debugPrint("Blank Types (T): ${puzzle.blankTypes}"); + debugPrint("Total Blanks: ${puzzle.solutions.length}"); + debugPrint("------------------------------------------"); + // ---------------------------- _userAnswers = List.generate(puzzle.solutions.length, (_) => null); _selectedBlankIndex = 0; - _isGameCompleted = false; - - _startTimer(); // ๐Ÿ‘ˆ [์ถ”๊ฐ€] - - notifyListeners(); + _isWrongAnswer = false; + _remainingTries = 3; + _isRevealingAnswer = false; } - /// 2. UI(๋นˆ์นธ)์—์„œ ํ˜ธ์ถœ: ๋นˆ์นธ ์„ ํƒ + /// [๐Ÿ”ฅ ์ˆ˜์ •] ๋นˆ์นธ ์„ ํƒ ์‹œ ๋กœ๊ทธ ์ถ”๊ฐ€ void onBlankTapped(int index) { - if (_isGameCompleted) return; - - _selectedBlankIndex = index; - notifyListeners(); + if (_isGameCompleted || _isRevealingAnswer) return; + if (_selectedBlankIndex != index) { + _selectedBlankIndex = index; + + // --- [LOG: ์„ ํƒ๋œ ๋นˆ์นธ ํƒ€์ž… ํ™•์ธ] --- + final selectedType = currentSelectedBlankType?.toString() ?? 'None/Invalid'; + debugPrint("[LOG] BLANK TAPPED: Index $index selected."); + debugPrint("[LOG] BLANK TAPPED: Determined Type: $selectedType"); + // ---------------------------------- + + notifyListeners(); + } } - /// 3. UI(์ˆซ์ž ๋ฒ„ํŠผ)์—์„œ ํ˜ธ์ถœ: ๋‹ต ์ž…๋ ฅ void onOptionTapped(String option) { - if (_isGameCompleted) return; + if (_isGameCompleted || _isRevealingAnswer) return; + _isWrongAnswer = false; + + if (_selectedBlankIndex >= puzzle.blankTypes.length) return; + final PuzzleBlankType requiredType = puzzle.blankTypes[_selectedBlankIndex]; + final bool isOptionNumber = int.tryParse(option) != null; + final bool isOptionOperator = ['+', '-', '*', '/'].contains(option); + + if (requiredType == PuzzleBlankType.number && !isOptionNumber) { + return; + } + if (requiredType == PuzzleBlankType.operator && !isOptionOperator) { + return; + } + + if (_selectedBlankIndex < _userAnswers.length) { + _userAnswers[_selectedBlankIndex] = option; + } - _userAnswers[_selectedBlankIndex] = option; _selectNextBlank(); notifyListeners(); _checkCompletion(); } - - /// 4. UI(์ง€์šฐ๊ธฐ ๋ฒ„ํŠผ)์—์„œ ํ˜ธ์ถœ: ๋‹ต ์ง€์šฐ๊ธฐ + void onClearTapped() { - if (_isGameCompleted) return; - _userAnswers[_selectedBlankIndex] = null; + if (_isGameCompleted || _isRevealingAnswer) return; + _isWrongAnswer = false; + if (_selectedBlankIndex < _userAnswers.length) { + _userAnswers[_selectedBlankIndex] = null; + } notifyListeners(); } - /// ๋‹ค์Œ ๋นˆ์นธ (์•„์ง ๋‹ต์ด ์—†๋Š”)์œผ๋กœ ์ž๋™ ์ด๋™ void _selectNextBlank() { + if (_userAnswers.isEmpty) return; int nextIndex = (_selectedBlankIndex + 1) % _userAnswers.length; for (int i = 0; i < _userAnswers.length; i++) { if (_userAnswers[nextIndex] == null) { @@ -101,27 +167,104 @@ class MathQuizController with ChangeNotifier { } } - /// ๋ชจ๋“  ๋‹ต์ด ์ฑ„์›Œ์กŒ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ •๋‹ต์ธ์ง€ ํ™•์ธ void _checkCompletion() { if (_userAnswers.any((answer) => answer == null)) { - return; + _isWrongAnswer = false; + return; } - - bool allCorrect = true; + bool fastMatch = true; for (int i = 0; i < puzzle.solutions.length; i++) { if (_userAnswers[i] != puzzle.solutions[i]) { - allCorrect = false; + fastMatch = false; break; } } - - if (allCorrect) { - _isGameCompleted = true; - _stopTimer(); // ๐Ÿ‘ˆ [์ถ”๊ฐ€] - notifyListeners(); + if (fastMatch) { + _handleCorrectAnswer(); + return; + } + bool slowMatch = _checkSlowPathValidation(); + if (slowMatch) { + _handleCorrectAnswer(); } else { - // [TODO] ์˜ค๋‹ต ์ฒ˜๋ฆฌ (์˜ˆ: ์Šค๋‚ต๋ฐ” ํ‘œ์‹œ) - debugPrint("์˜ค๋‹ต์ž…๋‹ˆ๋‹ค!"); + _handleWrongAnswer(); } } + + bool _checkSlowPathValidation() { + List rebuiltGrid = List.of(puzzle.gridCells); + int answerIndex = 0; + for (int i = 0; i < rebuiltGrid.length; i++) { + if (rebuiltGrid[i] == '?') { + if (answerIndex < _userAnswers.length) { + rebuiltGrid[i] = _userAnswers[answerIndex]!; + answerIndex++; + } + } + } + try { + for (final eq in puzzle.equations) { + final String expression = + eq.expressionIndices.map((i) => rebuiltGrid[i]).join(' '); + final String expectedResultStr = rebuiltGrid[eq.resultIndex]; + if (expression.contains('=') || + expectedResultStr.contains(RegExp(r'[+\-*/]'))) { + return false; + } + final num actualResult = expression.interpret(); + final num expectedResult = num.parse(expectedResultStr); + if (actualResult != expectedResult) { + return false; + } + } + return true; + } catch (e) { + debugPrint("๋ฐฉ์ •์‹ ํ‰๊ฐ€ ์‹คํŒจ: $e"); + return false; + } + } + + void _handleCorrectAnswer() { + _isWrongAnswer = false; + _totalBlanksFilled += _userAnswers.length; + if (_currentPuzzleIndex + 1 < _totalPuzzlesInLevel) { + _currentPuzzleIndex++; + _loadNextPuzzle(); + notifyListeners(); + } else { + _isGameCompleted = true; + _stopTimer(); + notifyListeners(); + } + } + + void _handleWrongAnswer() { + _remainingTries--; + _isWrongAnswer = true; + if (_remainingTries <= 0) { + _handleFailedAnswer(); + } else { + notifyListeners(); + } + } + + void _handleFailedAnswer() { + _isWrongAnswer = false; + _isRevealingAnswer = true; + _userAnswers = List.of(puzzle.solutions); + notifyListeners(); + Future.delayed(const Duration(seconds: 3), () { + if (!_isGameCompleted) { + if (_currentPuzzleIndex + 1 < _totalPuzzlesInLevel) { + _currentPuzzleIndex++; + _loadNextPuzzle(); + notifyListeners(); + } else { + _isGameCompleted = true; + _stopTimer(); + notifyListeners(); + } + } + }); + } } \ No newline at end of file diff --git a/packages/feature_game_mathquiz/lib/controllers/math_quiz_generator.dart b/packages/feature_game_mathquiz/lib/controllers/math_quiz_generator.dart index a8bfd72..33c1d8e 100644 --- a/packages/feature_game_mathquiz/lib/controllers/math_quiz_generator.dart +++ b/packages/feature_game_mathquiz/lib/controllers/math_quiz_generator.dart @@ -1,42 +1,54 @@ +// packages/feature_game_mathquiz/lib/controllers/math_quiz_generator.dart import 'dart:math'; +import 'package:flutter/material.dart'; // debugPrint ์‚ฌ์šฉ์„ ์œ„ํ•ด ์œ ์ง€ import 'package:service_api/service_api.dart'; +import '../models/math_quiz_difficulty.dart'; import '../models/math_quiz_models.dart'; import 'package:function_tree/function_tree.dart'; class MathQuizGenerator { final Random _random = Random(); + /// ๋ฌธ์ž์—ด์ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธ + bool _isOperator(String value) { + return ['+', '-', '*', '/'].contains(value); + } + /// ๋‚œ์ด๋„์— ๋งž๋Š” ํผ์ฆ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. MathQuizPuzzle generatePuzzle(MathQuizDifficulty level) { switch (level.layout) { case MathQuizLayout.singleLine: - if (level.operationCount == 2) { // Lv 1-5 - return _generateSimpleEquation(level); // A + B = C - } else { // operationCount == 3 (Lv 6-10) - return _generateMultiOpEquation(level); // A + B * C = D + if (level.operationCount == 2) { + return _generateSimpleEquation(level); + } else { + return _generateMultiOpEquation(level); } - case MathQuizLayout.linkedL: // operationCount == 4 (Lv 11-15) - return _generateLinkedEquations(level); // 2x2 ๊ทธ๋ฆฌ๋“œ - case MathQuizLayout.gridSquare: - if (level.operationCount == 9) { // Lv 10-12 (3x3) - return _generate3x3GridEquation(level); - } else { // Lv 13-15 (4x4) + case MathQuizLayout.dualLine: + return _generateDualLineEquation(level); + case MathQuizLayout.linkedL: + return _generateLinkedEquations(level); + case MathQuizLayout.gridSquare: + if (level.operationCount == 9) { + return _generate3x3GridEquation(level); + } else { return _generate4x4GridEquation(level); } } } - /// [Lv 1-5] ์ˆซ์ž 2๊ฐœ ์—ฐ์‚ฐ: (A op B = C) + /// [Lv 1-3] ์ˆซ์ž 2๊ฐœ ์—ฐ์‚ฐ: (A op B = C) MathQuizPuzzle _generateSimpleEquation(MathQuizDifficulty level) { - // (์ด์ „๊ณผ ๋™์ผ) final (int a, int b, int c, String op) = _createEquation(level.operators); final List allParts = (op == '+') ? [a.toString(), op, b.toString(), '=', c.toString()] : (op == '-') - ? [c.toString(), op, a.toString(), '=', b.toString()] - : (op == '*') - ? [a.toString(), op, b.toString(), '=', c.toString()] - : [c.toString(), op, a.toString(), '=', b.toString()]; + ? [c.toString(), op, a.toString(), '=', b.toString()] + : (op == '*') + ? [a.toString(), op, b.toString(), '=', c.toString()] + : [c.toString(), op, a.toString(), '=', b.toString()]; + + debugPrint("--- GEN LOG (Lv 1-3): ALL PARTS: $allParts"); // [LOG] + List candidateIndices; switch (level.blankType) { case MathQuizBlankType.numbersOnly: candidateIndices = [0, 2]; break; @@ -45,35 +57,62 @@ class MathQuizGenerator { } final int finalBlankCount = min(level.blankCount, candidateIndices.length); final List blankIndices = (candidateIndices..shuffle(_random)).sublist(0, finalBlankCount); + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + final List solutions = []; + final List finalBlankTypes = []; final List gridCells = List.of(allParts); for (int index in blankIndices) { - solutions.add(allParts[index]); - gridCells[index] = '?'; - } - final Set optionsSet = solutions.toSet(); - while (optionsSet.length < 9) { - optionsSet.add((_random.nextInt(9) + 1).toString()); + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; } + + debugPrint("--- GEN LOG (Lv 1-3): BLANK INDICES: $blankIndices"); // [LOG] + debugPrint("--- GEN LOG (Lv 1-3): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } optionsSet.addAll(level.operators.split(',')); - if (level.blankType != MathQuizBlankType.operatorsOnly) { - optionsSet.add('='); - } + if (level.blankType != MathQuizBlankType.operatorsOnly) { optionsSet.add('='); } final List options = (optionsSet.toList()..shuffle(_random)).toList(); + return MathQuizPuzzle( gridCells: gridCells, - gridCrossAxisCount: 5, // 5x1 ๊ทธ๋ฆฌ๋“œ - solutions: solutions, + gridCrossAxisCount: 5, options: options, + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2], resultIndex: 4), + ], ); } - /// [Lv 6-10] ์ˆซ์ž 3๊ฐœ ์—ฐ์‚ฐ: (A op1 B op2 C = D) + /// [Lv 4-6] ์ˆซ์ž 3๊ฐœ ์—ฐ์‚ฐ: (A op1 B op2 C = D) MathQuizPuzzle _generateMultiOpEquation(MathQuizDifficulty level) { - // 1. ์‹ ์ƒ์„ฑ (์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ํฌํ•จ) final (int a, int b, int c, String op1, String op2, int result) = _createMultiOpEquation(level.operators); - final List allParts = [a.toString(), op1, b.toString(), op2, c.toString(), '=', result.toString()]; - // 2. ๋นˆ์นธ ์ƒ์„ฑ + + // [LOG 1] ํŠœํ”Œ ์ƒ์„ฑ ์งํ›„ ๊ฐ’ ํ™•์ธ + debugPrint("--- GEN LOG: TUPLE: a=$a, op1=$op1, b=$b, op2=$op2, c=$c, R=$result"); + + final List allParts = [ + a.toString(), + op1, + b.toString(), + op2, + c.toString(), + '=', + result.toString() + ]; + + // [LOG 2] allParts ๋ฆฌ์ŠคํŠธ ์™„์„ฑ ์งํ›„ ๊ฐ’ ํ™•์ธ + debugPrint("--- GEN LOG (Lv 4-6): ALL PARTS: $allParts"); + final int blankCount = level.blankCount; List candidateIndices; switch (level.blankType) { @@ -83,31 +122,110 @@ class MathQuizGenerator { } final int finalBlankCount = min(blankCount, candidateIndices.length); final List blankIndices = (candidateIndices..shuffle(_random)).sublist(0, finalBlankCount); + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + + // [LOG 3] blankIndices ๋ฆฌ์ŠคํŠธ ๊ฒฐ์ • ์งํ›„ ํ™•์ธ + debugPrint("--- GEN LOG (Lv 4-6): BLANK INDICES: $blankIndices"); + final List solutions = []; + final List finalBlankTypes = []; final List gridCells = List.of(allParts); for (int index in blankIndices) { - solutions.add(allParts[index]); - gridCells[index] = '?'; - } - // 3. ์˜ต์…˜ ์ƒ์„ฑ - final Set optionsSet = solutions.toSet(); - while (optionsSet.length < 9) { - optionsSet.add((_random.nextInt(9) + 1).toString()); + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; } + + debugPrint("--- GEN LOG (Lv 4-6): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } optionsSet.addAll(level.operators.split(',')); optionsSet.add('='); - // 4. ๊ทธ๋ฆฌ๋“œ ๋ชจ๋ธ๋กœ ๋ฐ˜ํ™˜ + return MathQuizPuzzle( gridCells: gridCells, - gridCrossAxisCount: 7, // 7x1 ๊ทธ๋ฆฌ๋“œ - solutions: solutions, + gridCrossAxisCount: 7, options: (optionsSet.toList()..shuffle(_random)).toList(), + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2, 3, 4], resultIndex: 6), + ], ); } - /// [Lv 11-15] ์ˆซ์ž 4๊ฐœ ์—ฐ์‚ฐ: (2x2 ๊ทธ๋ฆฌ๋“œ 'ใ„ฑ', 'ใ„ด') + /// [Lv 7-9] ๋…๋ฆฝ๋œ 2์ค„ (A op B = C, D op E = F) + MathQuizPuzzle _generateDualLineEquation(MathQuizDifficulty level) { + final (int a, int b, int c, String op1) = _createEquation(level.operators); + final (int d, int e, int f, String op2) = _createEquation(level.operators); + + final List allParts = [ + a.toString(), op1, b.toString(), "=", c.toString(), + " ", " ", " ", " ", " ", + d.toString(), op2, e.toString(), "=", f.toString(), + ]; + debugPrint("--- GEN LOG (Lv 7-9 DUAL): ALL PARTS: $allParts"); // [LOG] + + + List numberIndices = [0, 2, 10, 12]; + List operatorIndices = [1, 11]; + List blankIndices = []; + switch (level.blankType) { + case MathQuizBlankType.numbersOnly: + blankIndices = (numberIndices..shuffle(_random)).sublist(0, min(level.blankCount, numberIndices.length)); + break; + case MathQuizBlankType.operatorsOnly: + blankIndices = (operatorIndices..shuffle(_random)).sublist(0, min(level.blankCount, operatorIndices.length)); + break; + case MathQuizBlankType.numbersAndOperators: + blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); + break; + } + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + + final List solutions = []; + final List finalBlankTypes = []; + final List gridCells = List.of(allParts); + for (int index in blankIndices) { + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; + } + + debugPrint("--- GEN LOG (Lv 7-9 DUAL): BLANK INDICES: $blankIndices"); // [LOG] + debugPrint("--- GEN LOG (Lv 7-9 DUAL): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } + optionsSet.addAll(level.operators.split(',')); + optionsSet.add('='); + + return MathQuizPuzzle( + gridCells: gridCells, + gridCrossAxisCount: 5, + options: (optionsSet.toList()..shuffle(_random)).toList(), + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2], resultIndex: 4), + MathQuizEquation(expressionIndices: [10, 11, 12], resultIndex: 14), + ], + ); + } + + /// [Lv 10-12] ์ˆซ์ž 4๊ฐœ ์—ฐ์‚ฐ: (2x2 ๊ทธ๋ฆฌ๋“œ 'ใ„ฑ', 'ใ„ด') MathQuizPuzzle _generateLinkedEquations(MathQuizDifficulty level) { - // (๋™์  ์ƒ์„ฑ ๋กœ์ง) while (true) { final int a = _random.nextInt(9) + 1; final int b = _random.nextInt(9) + 1; @@ -118,24 +236,23 @@ class MathQuizGenerator { final String op2 = (ops..shuffle(_random)).first; final String op3 = (ops..shuffle(_random)).first; final String op4 = (ops..shuffle(_random)).first; - final int? r1 = _calculate(a, b, op1); - final int? r2 = _calculate(c, d, op2); - final int? r3 = _calculate(a, c, op3); - final int? r4 = _calculate(b, d, op4); + final int? r1 = _calculate(a, b, op1); + final int? r2 = _calculate(c, d, op2); + final int? r3 = _calculate(a, c, op3); + final int? r4 = _calculate(b, d, op4); if (r1 == null || r2 == null || r3 == null || r4 == null) continue; if (r1 < -99 || r2 < -99 || r3 < -99 || r4 < -99 || r1 > 999 || r2 > 999 || r3 > 999 || r4 > 999) continue; - final List allParts = [ a.toString(), op1, b.toString(), "=", r1.toString(), - op3, " ", op4, " ", "=", + op3, " ", op4, " ", "=", c.toString(), op2, d.toString(), "=", r2.toString(), - "=", " ", "=", " ", "=", + "=", " ", "=", " ", "=", r3.toString(), " ", r4.toString(), " ", " ", ]; - final List solutions = []; - final List gridCells = List.of(allParts); - List numberIndices = [0, 2, 10, 12]; // A,B,C,D - List operatorIndices = [1, 5, 7, 11]; // op1,op3,op4,op2 + debugPrint("--- GEN LOG (Lv 10-12 LINKED): ALL PARTS: $allParts"); // [LOG] + + List numberIndices = [0, 2, 10, 12]; + List operatorIndices = [1, 5, 7, 11]; List blankIndices = []; switch (level.blankType) { case MathQuizBlankType.numbersOnly: @@ -148,165 +265,244 @@ class MathQuizGenerator { blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); break; } - blankIndices.sort(); + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + + final List solutions = []; + final List finalBlankTypes = []; + final List gridCells = List.of(allParts); for (int index in blankIndices) { - solutions.add(allParts[index]); - gridCells[index] = '?'; - } - final Set optionsSet = solutions.toSet(); - while (optionsSet.length < 9) { - optionsSet.add((_random.nextInt(9) + 1).toString()); + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; } + + debugPrint("--- GEN LOG (Lv 10-12 LINKED): BLANK INDICES: $blankIndices"); // [LOG] + debugPrint("--- GEN LOG (Lv 10-12 LINKED): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } optionsSet.addAll(level.operators.split(',')); optionsSet.add('='); - + return MathQuizPuzzle( gridCells: gridCells, - gridCrossAxisCount: 5, // 5x5 ๊ทธ๋ฆฌ๋“œ - solutions: solutions, + gridCrossAxisCount: 5, options: (optionsSet.toList()..shuffle(_random)).toList(), + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2], resultIndex: 4), + MathQuizEquation(expressionIndices: [10, 11, 12], resultIndex: 14), + MathQuizEquation(expressionIndices: [0, 5, 10], resultIndex: 20), + MathQuizEquation(expressionIndices: [2, 7, 12], resultIndex: 22), + ], ); - } // end while(true) + } } - /// [์ˆ˜์ •๋จ] ๋ ˆ๋ฒจ 10-12 (3x3 Grid) - ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก ์‚ฌ์šฉ + /// [Lv 13-15] 3x3 Grid - ๋™์  ์ƒ์„ฑ MathQuizPuzzle _generate3x3GridEquation(MathQuizDifficulty level) { - // 1. 3x3 ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก์—์„œ ๋žœ๋ค ์„ ํƒ - final template = _get3x3Templates()..shuffle(); - final selectedTemplate = template.first; - - final List allParts = selectedTemplate.gridCells; - final List numberIndices = selectedTemplate.numberIndices; - final List operatorIndices = selectedTemplate.operatorIndices; + while (true) { + try { + final ops = level.operators.split(','); + final n = List.generate(9, (_) => _random.nextInt(9) + 1); + final o = List.generate(12, (_) => (ops..shuffle(_random)).first); + final expr = [ + "${n[0]} ${o[0]} ${n[1]} ${o[1]} ${n[2]}", // R0 + "${n[3]} ${o[5]} ${n[4]} ${o[6]} ${n[5]}", // R1 + "${n[6]} ${o[10]} ${n[7]} ${o[11]} ${n[8]}", // R2 + "${n[0]} ${o[2]} ${n[3]} ${o[7]} ${n[6]}", // R3 + "${n[1]} ${o[3]} ${n[4]} ${o[8]} ${n[7]}", // R4 + "${n[2]} ${o[4]} ${n[5]} ${o[9]} ${n[8]}", // R5 + ]; + final List r = expr.map((e) => e.interpret()).toList(); + if (r.any((res) => res.toInt() != res || res < -99 || res > 999)) { continue; } + final List rInt = r.map((res) => res.toInt()).toList(); + final List allParts = [ + n[0].toString(), o[0], n[1].toString(), o[1], n[2].toString(), "=", rInt[0].toString(), + o[2], " ", o[3], " ", o[4], " ", "=", + n[3].toString(), o[5], n[4].toString(), o[6], n[5].toString(), "=", rInt[1].toString(), + o[7], " ", o[8], " ", o[9], " ", "=", + n[6].toString(), o[10], n[7].toString(), o[11], n[8].toString(), "=", rInt[2].toString(), + "=", " ", "=", " ", "=", " ", "=", + rInt[3].toString(), " ", rInt[4].toString(), " ", rInt[5].toString(), " ", " ", + ]; + debugPrint("--- GEN LOG (Lv 13-15 Grid): ALL PARTS: $allParts"); // [LOG] - // 2. ๋‚œ์ด๋„(level.blankType)์— ๋”ฐ๋ผ ๋นˆ์นธ('?') ์ƒ์„ฑ - final List finalSolutions = []; - final List gridCells = List.of(allParts); - - List blankIndices = []; - switch (level.blankType) { - case MathQuizBlankType.numbersOnly: - blankIndices = (numberIndices..shuffle(_random)).sublist(0, min(level.blankCount, numberIndices.length)); - break; - case MathQuizBlankType.operatorsOnly: - blankIndices = (operatorIndices..shuffle(_random)).sublist(0, min(level.blankCount, operatorIndices.length)); - break; - case MathQuizBlankType.numbersAndOperators: - blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); - break; - } - - // 3. ๋นˆ์นธ ์ ์šฉ - blankIndices.sort(); - for (int index in blankIndices) { - finalSolutions.add(allParts[index]); - gridCells[index] = '?'; - } - - // 4. ์˜ต์…˜ ์ƒ์„ฑ - final Set optionsSet = finalSolutions.toSet(); // ๐Ÿ‘ˆ [์˜ค๋ฅ˜ ์ˆ˜์ •] - while (optionsSet.length < 9) { - optionsSet.add((_random.nextInt(9) + 1).toString()); - } - optionsSet.addAll(level.operators.split(',')); - optionsSet.add('='); - - final List options = (optionsSet.toList()..shuffle(_random)).toList(); - return MathQuizPuzzle( - gridCells: gridCells, - gridCrossAxisCount: 7, // 3x3 ํผ์ฆ (7x7 ๊ทธ๋ฆฌ๋“œ) - solutions: finalSolutions, - options: options, // ๐Ÿ‘ˆ [์˜ค๋ฅ˜ ์ˆ˜์ •] - ); + final List numberIndices = [0, 2, 4, 14, 16, 18, 28, 30, 32]; + final List operatorIndices = [1, 3, 7, 9, 11, 15, 17, 21, 23, 25, 29, 31]; + List blankIndices = []; + switch (level.blankType) { + case MathQuizBlankType.numbersOnly: + blankIndices = (numberIndices..shuffle(_random)).sublist(0, min(level.blankCount, numberIndices.length)); + break; + case MathQuizBlankType.operatorsOnly: + blankIndices = (operatorIndices..shuffle(_random)).sublist(0, min(level.blankCount, operatorIndices.length)); + break; + case MathQuizBlankType.numbersAndOperators: + blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); + break; + } + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + + final List solutions = []; + final List finalBlankTypes = []; + final List gridCells = List.of(allParts); + for (int index in blankIndices) { + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; + } + + debugPrint("--- GEN LOG (Lv 13-15 Grid): BLANK INDICES: $blankIndices"); // [LOG] + debugPrint("--- GEN LOG (Lv 13-15 Grid): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } + optionsSet.addAll(level.operators.split(',')); + optionsSet.add('='); + final List options = (optionsSet.toList()..shuffle(_random)).toList(); + + return MathQuizPuzzle( + gridCells: gridCells, + gridCrossAxisCount: 7, + options: options, + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2, 3, 4], resultIndex: 6), + MathQuizEquation(expressionIndices: [14, 15, 16, 17, 18], resultIndex: 20), + MathQuizEquation(expressionIndices: [28, 29, 30, 31, 32], resultIndex: 34), + MathQuizEquation(expressionIndices: [0, 7, 14, 21, 28], resultIndex: 42), + MathQuizEquation(expressionIndices: [2, 9, 16, 23, 30], resultIndex: 44), + MathQuizEquation(expressionIndices: [4, 11, 18, 25, 32], resultIndex: 46), + ], + ); + } catch (e) { + continue; + } + } } - /// [์ˆ˜์ •๋จ] ๋ ˆ๋ฒจ 13-15 (4x4 Grid) - ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก ์‚ฌ์šฉ + /// [Lv 16-19] 4x4 Grid - ๋™์  ์ƒ์„ฑ MathQuizPuzzle _generate4x4GridEquation(MathQuizDifficulty level) { - // 1. 4x4 ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก์—์„œ ๋žœ๋ค ์„ ํƒ - final template = _get4x4Templates()..shuffle(); - final selectedTemplate = template.first; - - final List allParts = selectedTemplate.gridCells; - final List numberIndices = selectedTemplate.numberIndices; - final List operatorIndices = selectedTemplate.operatorIndices; + while (true) { + try { + final ops = level.operators.split(','); + final n = List.generate(16, (_) => _random.nextInt(9) + 1); + final o = List.generate(24, (_) => (ops..shuffle(_random)).first); + final expr = [ + "${n[0]} ${o[0]} ${n[1]} ${o[1]} ${n[2]} ${o[2]} ${n[3]}", // R0 + "${n[4]} ${o[3]} ${n[5]} ${o[4]} ${n[6]} ${o[5]} ${n[7]}", // R1 + "${n[8]} ${o[6]} ${n[9]} ${o[7]} ${n[10]} ${o[8]} ${n[11]}", // R2 + "${n[12]} ${o[9]} ${n[13]} ${o[10]} ${n[14]} ${o[11]} ${n[15]}", // R3 + "${n[0]} ${o[12]} ${n[4]} ${o[13]} ${n[8]} ${o[14]} ${n[12]}", // R4 + "${n[1]} ${o[15]} ${n[5]} ${o[16]} ${n[9]} ${o[17]} ${n[13]}", // R5 + "${n[2]} ${o[18]} ${n[6]} ${o[19]} ${n[10]} ${o[20]} ${n[14]}", // R6 + "${n[3]} ${o[21]} ${n[7]} ${o[22]} ${n[11]} ${o[23]} ${n[15]}", // R7 + ]; + final List r = expr.map((e) => e.interpret()).toList(); + if (r.any((res) => res.toInt() != res || res < -999 || res > 9999)) { continue; } + final List rInt = r.map((res) => res.toInt()).toList(); + final List allParts = [ + n[0].toString(), o[0], n[1].toString(), o[1], n[2].toString(), o[2], n[3].toString(), "=", rInt[0].toString(), + o[12], " ", o[15], " ", o[18], " ", o[21], " ", "=", + n[4].toString(), o[3], n[5].toString(), o[4], n[6].toString(), o[5], n[7].toString(), "=", rInt[1].toString(), + o[13], " ", o[16], " ", o[19], " ", o[22], " ", "=", + n[8].toString(), o[6], n[9].toString(), o[7], n[10].toString(), o[8], n[11].toString(), "=", rInt[2].toString(), + o[14], " ", o[17], " ", o[20], " ", o[23], " ", "=", + n[12].toString(), o[9], n[13].toString(), o[10], n[14].toString(), o[11], n[15].toString(), "=", rInt[3].toString(), + "=", " ", "=", " ", "=", " ", "=", " ", "=", + rInt[4].toString(), " ", rInt[5].toString(), " ", rInt[6].toString(), " ", rInt[7].toString(), " ", " ", + ]; + debugPrint("--- GEN LOG (Lv 16-19 Grid): ALL PARTS: $allParts"); // [LOG] - // 2. ๋‚œ์ด๋„(level.blankType)์— ๋”ฐ๋ผ ๋นˆ์นธ('?') ์ƒ์„ฑ - final List finalSolutions = []; - final List gridCells = List.of(allParts); - - List blankIndices = []; - switch (level.blankType) { - case MathQuizBlankType.numbersOnly: - blankIndices = (numberIndices..shuffle(_random)).sublist(0, min(level.blankCount, numberIndices.length)); - break; - case MathQuizBlankType.operatorsOnly: - blankIndices = (operatorIndices..shuffle(_random)).sublist(0, min(level.blankCount, operatorIndices.length)); - break; - case MathQuizBlankType.numbersAndOperators: - blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); - break; - } - - // 3. ๋นˆ์นธ ์ ์šฉ - blankIndices.sort(); - for (int index in blankIndices) { - finalSolutions.add(allParts[index]); - gridCells[index] = '?'; - } - - // 4. ์˜ต์…˜ ์ƒ์„ฑ - final Set optionsSet = finalSolutions.toSet(); // ๐Ÿ‘ˆ [์˜ค๋ฅ˜ ์ˆ˜์ •] - while (optionsSet.length < 9) { - optionsSet.add((_random.nextInt(9) + 1).toString()); - } - optionsSet.addAll(level.operators.split(',')); - optionsSet.add('='); - - final List options = (optionsSet.toList()..shuffle(_random)).toList(); - return MathQuizPuzzle( - gridCells: gridCells, - gridCrossAxisCount: 9, // 4x4 ํผ์ฆ (9x9 ๊ทธ๋ฆฌ๋“œ) - solutions: finalSolutions, - options: options, // ๐Ÿ‘ˆ [์˜ค๋ฅ˜ ์ˆ˜์ •] - ); + final List numberIndices = [0, 2, 4, 6, 18, 20, 22, 24, 36, 38, 40, 42, 54, 56, 58, 60]; + final List operatorIndices = [ 1, 3, 5, 9, 11, 13, 15, 19, 21, 23, 27, 29, 31, 33, 37, 39, 41, 45, 47, 49, 51, 55, 57, 59 ]; + List blankIndices = []; + switch (level.blankType) { + case MathQuizBlankType.numbersOnly: + blankIndices = (numberIndices..shuffle(_random)).sublist(0, min(level.blankCount, numberIndices.length)); + break; + case MathQuizBlankType.operatorsOnly: + blankIndices = (operatorIndices..shuffle(_random)).sublist(0, min(level.blankCount, operatorIndices.length)); + break; + case MathQuizBlankType.numbersAndOperators: + blankIndices = (numberIndices + operatorIndices..shuffle(_random)).sublist(0, level.blankCount); + break; + } + + blankIndices.sort(); // ๐Ÿ‘ˆ FIX + + final List solutions = []; + final List finalBlankTypes = []; + final List gridCells = List.of(allParts); + for (int index in blankIndices) { + final String solutionValue = allParts[index]; + solutions.add(solutionValue); + finalBlankTypes.add(_isOperator(solutionValue) + ? PuzzleBlankType.operator + : PuzzleBlankType.number); + gridCells[index] = '?'; + } + + debugPrint("--- GEN LOG (Lv 16-19 Grid): BLANK INDICES: $blankIndices"); // [LOG] + debugPrint("--- GEN LOG (Lv 16-19 Grid): SOLUTIONS: $solutions | TYPES: $finalBlankTypes"); // [LOG] + + + final Set optionsSet = solutions.toSet(); + while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); } + optionsSet.addAll(level.operators.split(',')); + optionsSet.add('='); + final List options = (optionsSet.toList()..shuffle(_random)).toList(); + + return MathQuizPuzzle( + gridCells: gridCells, + gridCrossAxisCount: 9, + options: options, + solutions: solutions, + blankTypes: finalBlankTypes, + equations: [ + MathQuizEquation(expressionIndices: [0, 1, 2, 3, 4, 5, 6], resultIndex: 8), + MathQuizEquation(expressionIndices: [18, 19, 20, 21, 22, 23, 24], resultIndex: 26), + MathQuizEquation(expressionIndices: [36, 37, 38, 39, 40, 41, 42], resultIndex: 44), + MathQuizEquation(expressionIndices: [54, 55, 56, 57, 58, 59, 60], resultIndex: 62), + MathQuizEquation(expressionIndices: [0, 9, 18, 27, 36, 45, 54], resultIndex: 72), + MathQuizEquation(expressionIndices: [2, 11, 20, 29, 38, 47, 56], resultIndex: 74), + MathQuizEquation(expressionIndices: [4, 13, 22, 31, 40, 49, 58], resultIndex: 76), + MathQuizEquation(expressionIndices: [6, 15, 24, 33, 42, 51, 60], resultIndex: 78), + ], + ); + } catch (e) { + continue; + } + } } - /// [HELPER] (A, B, C, op) ํŠœํ”Œ ์ƒ์„ฑ (์‚ฌ์น™์—ฐ์‚ฐ ์ง€์›) - (int, int, int, String) _createEquation(String operators, {int? firstTerm, int maxResult = 9}) { - // (์ด์ „๊ณผ ๋™์ผ) + // ( ... _createEquation, _createMultiOpEquation, _calculate๋Š” ์ด์ „๊ณผ ๋™์ผ ... ) + (int, int, int, String) _createEquation(String operators, {int? firstTerm, int maxResult = 9}) { final List ops = operators.split(','); final String op = ops[_random.nextInt(ops.length)]; int a, b, c; switch (op) { - case '+': - a = firstTerm ?? _random.nextInt(maxResult - 1) + 1; - b = _random.nextInt(maxResult - a) + 1; - c = a + b; - return (a, b, c, op); - case '-': - c = firstTerm ?? _random.nextInt(maxResult - 1) + 2; - a = _random.nextInt(c - 1) + 1; - b = c - a; - return (a, b, c, op); - case '*': - a = firstTerm ?? _random.nextInt(maxResult ~/ 2) + 2; - b = _random.nextInt(maxResult ~/ a) + 1; - c = a * b; - return (a, b, c, op); - case '/': - b = _random.nextInt(maxResult ~/ 2) + 1; - a = _random.nextInt(maxResult ~/ b) + 1; - c = a * b; - if (c == 0 || a == 0) return _createEquation(operators, firstTerm: firstTerm, maxResult: maxResult); - return (a, b, c, op); - default: - return (1, 1, 2, '+'); + case '+': a = firstTerm ?? _random.nextInt(maxResult - 1) + 1; b = _random.nextInt(maxResult - a) + 1; c = a + b; return (a, b, c, op); + case '-': c = firstTerm ?? _random.nextInt(maxResult - 1) + 2; a = _random.nextInt(c - 1) + 1; b = c - a; return (a, b, c, op); + case '*': a = firstTerm ?? _random.nextInt(maxResult ~/ 2) + 2; b = _random.nextInt(maxResult ~/ a) + 1; c = a * b; return (a, b, c, op); + case '/': b = _random.nextInt(maxResult ~/ 2) + 1; a = _random.nextInt(maxResult ~/ b) + 1; c = a * b; if (c == 0 || a == 0) return _createEquation(operators, firstTerm: firstTerm, maxResult: maxResult); return (a, b, c, op); + default: return (1, 1, 2, '+'); } } - - /// [HELPER] ์ˆซ์ž 3๊ฐœ + ์—ฐ์‚ฐ์ž 2๊ฐœ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ!) (int, int, int, String, String, int) _createMultiOpEquation(String operators) { while(true) { final int a = _random.nextInt(9) + 1; @@ -315,89 +511,25 @@ class MathQuizGenerator { final List ops = operators.split(','); final String op1 = (ops..shuffle(_random)).first; final String op2 = (ops..shuffle(_random)).first; - final String expression = "$a $op1 $b $op2 $c"; - if (expression.contains('/')) continue; - - final num resultNum = expression.interpret(); - final int result = resultNum.toInt(); - - if (result == resultNum && result >= -99 && result <= 999) { // [์ˆ˜์ •] ๋ฒ”์œ„ ํ™•์žฅ - return (a, b, c, op1, op2, result); + try { + final num resultNum = expression.interpret(); + final int result = resultNum.toInt(); + if (result == resultNum && result >= -99 && result <= 999) { + return (a, b, c, op1, op2, result); + } + } catch (e) { + continue; } } } - - /// [HELPER] ๋‘ ์ˆซ์ž์™€ ์—ฐ์‚ฐ์ž๋กœ ๊ณ„์‚ฐ (์‹คํŒจ ์‹œ null ๋ฐ˜ํ™˜) int? _calculate(int a, int b, String op) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; - case '/': - if (b == 0) return null; // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ - if (a % b != 0) return null; // ๋‚˜๋ˆ„์–ด๋–จ์–ด์ง€์ง€ ์•Š์Œ - return a ~/ b; + case '/': if (b == 0) return null; if (a % b != 0) return null; return a ~/ b; } - return 0; // ์•Œ ์ˆ˜ ์—†๋Š” ์—ฐ์‚ฐ์ž + return 0; } - - /// [์‹ ๊ทœ] 3x3 ๊ทธ๋ฆฌ๋“œ ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก (8๊ฐœ) - List<_GridTemplate> _get3x3Templates() { - // [!] ํ…œํ”Œ๋ฆฟ์˜ ์ˆ˜ํ•™์  ์œ ํšจ์„ฑ์€ ์ด๋ฏธ์ง€ ์›๋ณธ์„ ๋”ฐ๋ฅด๋ฉฐ, ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - return [ - // 5.53.19.png (top-left) - _GridTemplate(gridCells:["5","*","3","-","1","=","17","+"," ","*"," ","-"," ","=","2","*","6","-","7","=","5","*"," ","+"," ","+"," ","=","4","+","1","+","1","=","18","="," ","="," ","="," ","=","21"," ","33"," ","22"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.19.png (top-right) - _GridTemplate(gridCells:["1","+","1","+","5","=","17","/"," ","-"," ","+"," ","=","1","*","3","+","2","=","15","*"," ","+"," ","-"," ","=","6","-","4","+","2","=","0","="," ","="," ","="," ","=","12"," ","12"," ","13"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.19.png (bottom-left) - _GridTemplate(gridCells:["9","+","5","+","5","=","19","/"," ","-"," ","+"," ","=","2","*","3","+","9","=","15","*"," ","+"," ","-"," ","=","6","-","7","+","1","=","0","="," ","="," ","="," ","=","12"," ","9"," ","16"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.19.png (bottom-right) - _GridTemplate(gridCells:["7","*","4","-","9","=","39","*"," ","+"," ","*"," ","=","1","*","1","+","8","=","9","+"," ","*"," ","+"," ","=","3","-","4","+","5","=","4","="," ","="," ","="," ","=","11"," ","34"," ","23"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.33.png (top-left) - _GridTemplate(gridCells:["9","-","5","/","1","=","7","*"," ","-"," ","+"," ","=","8","*","5","-","7","=","27","+"," ","+"," ","+"," ","=","4","+","3","-","7","=","2","="," ","="," ","="," ","=","62"," ","0"," ","12"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.33.png (top-right) - _GridTemplate(gridCells:["8","*","2","+","6","=","16","*"," ","+"," ","/"," ","=","5","*","9","/","3","=","3","+"," ","+"," ","*"," ","=","7","+","1","+","1","=","19","="," ","="," ","="," ","=","9"," ","18"," ","16"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.33.png (bottom-left) - _GridTemplate(gridCells:["1","+","7","+","9","=","17","*"," ","+"," ","-"," ","=","8","/","4","+","4","=","8","+"," ","*"," ","/"," ","=","7","*","3","+","3","=","17","="," ","="," ","="," ","=","13"," ","19"," ","6"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - // 5.53.33.png (bottom-right) - _GridTemplate(gridCells:["1","+","6","-","9","=","6","/"," ","-"," ","/"," ","=","2","-","7","/","1","=","0","-"," ","/"," ","*"," ","=","9","-","5","+","4","=","0","="," ","="," ","="," ","=","0"," ","6"," ","3"," "," "], numberIndices:[0,2,4,14,16,18,28,30,32], operatorIndices:[1,3,7,9,11,15,17,21,23,25,29,31]), - ]; - } - - /// [์‹ ๊ทœ] 4x4 ๊ทธ๋ฆฌ๋“œ ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก (8๊ฐœ) - List<_GridTemplate> _get4x4Templates() { - // [!] ํ…œํ”Œ๋ฆฟ์˜ ์ˆ˜ํ•™์  ์œ ํšจ์„ฑ์€ ์ด๋ฏธ์ง€ ์›๋ณธ์„ ๋”ฐ๋ฅด๋ฉฐ, ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - return [ - // 5.54.51.png (top-left) - _GridTemplate(gridCells:["1","*","4","-","2","+","14","=","16","+"," ","*"," ","/"," ","-"," ","=","15","/","5","*","16","+","13","=","61","+"," ","-"," ","+"," ","+"," ","=","3","/","2","+","4","*","7","=","29","*"," ","+"," ","/"," ","*"," ","=","15","+","11","-","8","*","8","=","-38","="," ","="," ","="," ","="," ","=","102"," ","51"," ","54"," ","95"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.54.51.png (bottom-left) - _GridTemplate(gridCells:["8","+","4","*","2","+","15","=","114","-"," ","+"," ","-"," ","-"," ","=","6","/","4","+","3","/","2","=","11","-"," ","-"," ","+"," ","-"," ","=","7","*","3","/","5","-","1","=","0","+"," ","-"," ","*"," ","+"," ","=","12","-","1","*","6","+","11","=","14","="," ","="," ","="," ","="," ","=","1"," ","0"," ","44"," ","18"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.54.37.png (top-left) - _GridTemplate(gridCells:["10","/","2","*","3","+","9","=","24","*"," ","-"," ","*"," ","+"," ","=","2","+","13","*","5","-","7","=","74","+"," ","+"," ","-"," ","+"," ","=","5","*","6","+","1","-","7","=","5","+"," ","-"," ","+"," ","+"," ","=","12","-","11","+","2","*","8","=","49","="," ","="," ","="," ","="," ","=","173"," ","15"," ","17"," ","85"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.54.37.png (bottom-left) - _GridTemplate(gridCells:["9","*","2","+","8","-","1","=","40","/"," ","/"," ","-"," ","*"," ","=","5","+","2","-","13","+","3","=","3","-"," ","*"," ","+"," ","-"," ","=","7","/","1","-","6","+","4","=","4","+"," ","+"," ","*"," ","+"," ","=","16","+","9","-","7","-","10","=","9","="," ","="," ","="," ","="," ","=","5"," ","29"," ","64"," ","13"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.54.29.png (top-left) - _GridTemplate(gridCells:["1","-","5","-","4","+","14","=","8","*"," ","/"," ","*"," ","-"," ","=","7","+","2","*","1","-","11","=","14","-"," ","+"," ","+"," ","-"," ","=","2","/","1","*","7","-","6","=","21","+"," ","+"," ","-"," ","+"," ","=","15","*","10","+","1","+","7","=","173","="," ","="," ","="," ","="," ","=","12"," ","18"," ","21"," ","5"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.54.29.png (bottom-left) - _GridTemplate(gridCells:["9","-","3","-","1","+","13","=","11","*"," ","*"," ","+"," ","+"," ","=","8","*","7","-","9","-","5","=","49","+"," ","+"," ","*"," ","+"," ","=","6","*","5","+","11","-","2","=","41","-"," ","-"," ","-"," ","/"," ","=","15","+","2","-","1","-","4","=","17","="," ","="," ","="," ","="," ","=","71"," ","67"," ","40"," ","18"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.55.00.png (top-left) - _GridTemplate(gridCells:["9","*","8","-","6","+","7","=","141","*"," ","*"," ","/"," ","-"," ","=","2","+","6","*","5","-","10","=","27","-"," ","-"," ","+"," ","-"," ","=","3","+","1","-","3","*","1","=","3","*"," ","+"," ","+"," ","+"," ","=","11","+","7","*","2","+","8","=","25","="," ","="," ","="," ","="," ","=","19"," ","85"," ","20"," ","9"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - // 5.55.00.png (bottom-left) - _GridTemplate(gridCells:["4","-","1","-","2","+","14","=","3","+"," ","*"," ","*"," ","-"," ","=","9","-","1","+","3","-","4","=","7","*"," ","/"," ","-"," ","+"," ","=","6","-","1","+","7","+","1","=","17","+"," ","-"," ","+"," ","*"," ","=","8","*","2","+","1","+","16","=","65","="," ","="," ","="," ","="," ","=","38"," ","9"," ","144"," ","178"," "," "], numberIndices:[0,2,4,6,18,20,22,24,36,38,40,42,54,56,58,60], operatorIndices:[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,45,47,49,51,53,55,57,59,61,63,65,67,69,71]), - ]; - } -} - -/// [์‹ ๊ทœ] 3x3/4x4 ํ…œํ”Œ๋ฆฟ์˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ -class _GridTemplate { - final List gridCells; - final List numberIndices; - final List operatorIndices; - - _GridTemplate({ - required this.gridCells, - required this.numberIndices, - required this.operatorIndices, - }); } \ No newline at end of file diff --git a/packages/service_api/lib/models/math_quiz_difficulty.dart b/packages/feature_game_mathquiz/lib/models/math_quiz_difficulty.dart similarity index 61% rename from packages/service_api/lib/models/math_quiz_difficulty.dart rename to packages/feature_game_mathquiz/lib/models/math_quiz_difficulty.dart index 02d1d7c..1ff57d1 100644 --- a/packages/service_api/lib/models/math_quiz_difficulty.dart +++ b/packages/feature_game_mathquiz/lib/models/math_quiz_difficulty.dart @@ -1,4 +1,5 @@ -import 'game_difficulty.dart'; +// packages/feature_game_mathquiz/lib/models/math_quiz_difficulty.dart +import 'package:service_api/service_api.dart'; /// ๋นˆ์นธ์˜ ์œ ํ˜•์„ ์ •์˜ enum MathQuizBlankType { @@ -9,24 +10,21 @@ enum MathQuizBlankType { /// ํผ์ฆ์˜ ๋ ˆ์ด์•„์›ƒ ํ˜•ํƒœ๋ฅผ ์ •์˜ enum MathQuizLayout { - /// A + B = C ๋˜๋Š” A + B * C = D singleLine, - /// 'ใ„ฑ', 'ใ„ด' ๋ชจ์–‘ (๋ณ€์ˆ˜ 4๊ฐœ) + dualLine, linkedL, - /// 3x3 ์ด์ƒ ๊ทธ๋ฆฌ๋“œ (๋ณ€์ˆ˜ 8๊ฐœ ์ด์ƒ) gridSquare, } -/// ์ˆ˜ํ•™ ํ€ด์ฆˆ ๊ฒŒ์ž„์˜ ๋‚œ์ด๋„ ์ •์˜ +/// 'extends GameDifficulty' ์ถ”๊ฐ€ class MathQuizDifficulty extends GameDifficulty { final int levelIndex; final MathQuizLayout layout; final String operators; final int blankCount; final MathQuizBlankType blankType; - - /// [์ˆ˜์ •] ์—ฐ์‚ฐ ๋ณต์žก๋„ (์˜ˆ: 2=A+B, 3=A+B*C, 4=2x2๊ทธ๋ฆฌ๋“œ) final int operationCount; + final int puzzleCount; const MathQuizDifficulty({ required this.levelIndex, @@ -37,14 +35,14 @@ class MathQuizDifficulty extends GameDifficulty { required this.blankCount, required this.blankType, required this.operationCount, + required this.puzzleCount, }); } -/// [์ˆ˜์ •๋จ] ์•ฑ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ํ•™ ํ€ด์ฆˆ ๋‚œ์ด๋„ ๋ชฉ๋ก (15๋‹จ๊ณ„) +/// ์•ฑ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ํ•™ ํ€ด์ฆˆ ๋‚œ์ด๋„ ๋ชฉ๋ก (19๋‹จ๊ณ„) class MathQuizDifficulties { static final List allDifficulties = [ // --- ํŒจํ„ด 1: ์ˆซ์ž 2๊ฐœ (A op B = C) [Lv 1-3] --- - // (์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ์—†์Œ) const MathQuizDifficulty( levelIndex: 1, name: 'Lv. 1: ์ˆซ์ž 2๊ฐœ (์ˆซ์ž ๋นˆ์นธ)', @@ -53,7 +51,8 @@ class MathQuizDifficulties { operators: '+,-', blankCount: 1, blankType: MathQuizBlankType.numbersOnly, - operationCount: 2, // A, B + operationCount: 2, + puzzleCount: 10, ), const MathQuizDifficulty( levelIndex: 2, @@ -64,6 +63,7 @@ class MathQuizDifficulties { blankCount: 1, blankType: MathQuizBlankType.operatorsOnly, operationCount: 2, + puzzleCount: 10, ), const MathQuizDifficulty( levelIndex: 3, @@ -74,10 +74,9 @@ class MathQuizDifficulties { blankCount: 2, blankType: MathQuizBlankType.numbersAndOperators, operationCount: 2, + puzzleCount: 10, ), - // --- ํŒจํ„ด 2: ์ˆซ์ž 3๊ฐœ (A op1 B op2 C = D) [Lv 4-6] --- - // (์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ *์ ์šฉ*) const MathQuizDifficulty( levelIndex: 4, name: 'Lv. 4: ์ˆซ์ž 3๊ฐœ (์ˆซ์ž ๋นˆ์นธ)', @@ -86,7 +85,8 @@ class MathQuizDifficulties { operators: '+,-,*,/', blankCount: 2, blankType: MathQuizBlankType.numbersOnly, - operationCount: 3, // A, B, C + operationCount: 3, + puzzleCount: 8, ), const MathQuizDifficulty( levelIndex: 5, @@ -97,6 +97,7 @@ class MathQuizDifficulties { blankCount: 2, blankType: MathQuizBlankType.operatorsOnly, operationCount: 3, + puzzleCount: 8, ), const MathQuizDifficulty( levelIndex: 6, @@ -107,102 +108,155 @@ class MathQuizDifficulties { blankCount: 3, blankType: MathQuizBlankType.numbersAndOperators, operationCount: 3, + puzzleCount: 8, ), - - // --- ํŒจํ„ด 3: ์ˆซ์ž 4๊ฐœ (2x2 ๊ทธ๋ฆฌ๋“œ 'ใ„ฑ', 'ใ„ด') [Lv 7-9] --- + // --- ํŒจํ„ด 3: ๋…๋ฆฝ๋œ 2์ค„ (์ˆซ์ž 4๊ฐœ) [Lv 7-9] --- const MathQuizDifficulty( levelIndex: 7, - name: 'Lv. 7: ์ˆซ์ž 4๊ฐœ (์ˆซ์ž ๋นˆ์นธ)', - contextId: 'MATH_L7_OP4_NUM', + name: 'Lv. 7: ๋…๋ฆฝ 2์ค„ (์ˆซ์ž)', + contextId: 'MATH_L7_OP4_DUAL_NUM', + layout: MathQuizLayout.dualLine, + operators: '+,-', + blankCount: 2, + blankType: MathQuizBlankType.numbersOnly, + operationCount: 4, + puzzleCount: 6, + ), + const MathQuizDifficulty( + levelIndex: 8, + name: 'Lv. 8: ๋…๋ฆฝ 2์ค„ (์—ฐ์‚ฐ์ž)', + contextId: 'MATH_L8_OP4_DUAL_OP', + layout: MathQuizLayout.dualLine, + operators: '+,-', + blankCount: 2, + blankType: MathQuizBlankType.operatorsOnly, + operationCount: 4, + puzzleCount: 6, + ), + const MathQuizDifficulty( + levelIndex: 9, + name: 'Lv. 9: ๋…๋ฆฝ 2์ค„ (์‚ฌ์น™์—ฐ์‚ฐ)', + contextId: 'MATH_L9_OP4_DUAL_ANY', + layout: MathQuizLayout.dualLine, + operators: '+,-,*,/', + blankCount: 3, + blankType: MathQuizBlankType.numbersAndOperators, + operationCount: 4, + puzzleCount: 6, + ), + // --- ํŒจํ„ด 4: 2x2 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž 4๊ฐœ) [Lv 10-12] --- + const MathQuizDifficulty( + levelIndex: 10, + name: 'Lv. 10: 2x2 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž)', + contextId: 'MATH_L10_OP4_L_NUM', layout: MathQuizLayout.linkedL, operators: '+,-', blankCount: 2, blankType: MathQuizBlankType.numbersOnly, - operationCount: 4, // A, B, C, D + operationCount: 4, + puzzleCount: 4, ), const MathQuizDifficulty( - levelIndex: 8, - name: 'Lv. 8: ์ˆซ์ž 4๊ฐœ (์—ฐ์‚ฐ์ž ๋นˆ์นธ)', - contextId: 'MATH_L8_OP4_OP', + levelIndex: 11, + name: 'Lv. 11: 2x2 ๊ทธ๋ฆฌ๋“œ (์—ฐ์‚ฐ์ž)', + contextId: 'MATH_L11_OP4_L_OP', layout: MathQuizLayout.linkedL, operators: '+,-', blankCount: 2, blankType: MathQuizBlankType.operatorsOnly, operationCount: 4, + puzzleCount: 4, ), const MathQuizDifficulty( - levelIndex: 9, - name: 'Lv. 9: ์ˆซ์ž 4๊ฐœ (์‚ฌ์น™์—ฐ์‚ฐ)', - contextId: 'MATH_L9_OP4_ANY', + levelIndex: 12, + name: 'Lv. 12: 2x2 ๊ทธ๋ฆฌ๋“œ (์‚ฌ์น™์—ฐ์‚ฐ)', + contextId: 'MATH_L12_OP4_L_ANY', layout: MathQuizLayout.linkedL, operators: '+,-,*,/', blankCount: 3, blankType: MathQuizBlankType.numbersAndOperators, operationCount: 4, + puzzleCount: 4, ), - - // --- ํŒจํ„ด 4: 3x3 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž 9๊ฐœ) [Lv 10-12] --- + // --- ํŒจํ„ด 5: 3x3 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž 9๊ฐœ) [Lv 13-15] --- const MathQuizDifficulty( - levelIndex: 10, - name: 'Lv. 10: 3x3 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž)', - contextId: 'MATH_L10_OP9_NUM', + levelIndex: 13, + name: 'Lv. 13: 3x3 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž)', + contextId: 'MATH_L13_OP9_NUM', layout: MathQuizLayout.gridSquare, operators: '+,-', blankCount: 3, blankType: MathQuizBlankType.numbersOnly, - operationCount: 9, // 9 Variables + operationCount: 9, + puzzleCount: 3, ), const MathQuizDifficulty( - levelIndex: 11, - name: 'Lv. 11: 3x3 ๊ทธ๋ฆฌ๋“œ (์—ฐ์‚ฐ์ž)', - contextId: 'MATH_L11_OP9_OP', + levelIndex: 14, + name: 'Lv. 14: 3x3 ๊ทธ๋ฆฌ๋“œ (์—ฐ์‚ฐ์ž)', + contextId: 'MATH_L14_OP9_OP', layout: MathQuizLayout.gridSquare, operators: '+,-', blankCount: 3, blankType: MathQuizBlankType.operatorsOnly, operationCount: 9, + puzzleCount: 3, ), const MathQuizDifficulty( - levelIndex: 12, - name: 'Lv. 12: 3x3 ๊ทธ๋ฆฌ๋“œ (์‚ฌ์น™์—ฐ์‚ฐ)', - contextId: 'MATH_L12_OP9_ANY', + levelIndex: 15, + name: 'Lv. 15: 3x3 ๊ทธ๋ฆฌ๋“œ (์‚ฌ์น™์—ฐ์‚ฐ)', + contextId: 'MATH_L15_OP9_ANY', layout: MathQuizLayout.gridSquare, operators: '+,-,*,/', blankCount: 4, blankType: MathQuizBlankType.numbersAndOperators, operationCount: 9, + puzzleCount: 3, ), - - // --- ํŒจํ„ด 5: 4x4 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž 16๊ฐœ) [Lv 13-15] --- + // --- ํŒจํ„ด 6: 4x4 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž 16๊ฐœ) [Lv 16-18] --- const MathQuizDifficulty( - levelIndex: 13, - name: 'Lv. 13: 4x4 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž)', - contextId: 'MATH_L13_OP16_NUM', + levelIndex: 16, + name: 'Lv. 16: 4x4 ๊ทธ๋ฆฌ๋“œ (์ˆซ์ž)', + contextId: 'MATH_L16_OP16_NUM', layout: MathQuizLayout.gridSquare, operators: '+,-,*,/', blankCount: 5, blankType: MathQuizBlankType.numbersOnly, operationCount: 16, + puzzleCount: 2, ), const MathQuizDifficulty( - levelIndex: 14, - name: 'Lv. 14: 4x4 ๊ทธ๋ฆฌ๋“œ (์—ฐ์‚ฐ์ž)', - contextId: 'MATH_L14_OP16_OP', + levelIndex: 17, + name: 'Lv. 17: 4x4 ๊ทธ๋ฆฌ๋“œ (์—ฐ์‚ฐ์ž)', + contextId: 'MATH_L17_OP16_OP', layout: MathQuizLayout.gridSquare, operators: '+,-,*,/', blankCount: 5, blankType: MathQuizBlankType.operatorsOnly, operationCount: 16, + puzzleCount: 2, ), const MathQuizDifficulty( - levelIndex: 15, - name: 'Lv. 15: 4x4 ๊ทธ๋ฆฌ๋“œ (๋žœ๋ค)', - contextId: 'MATH_L15_OP16_ANY', + levelIndex: 18, + name: 'Lv. 18: 4x4 ๊ทธ๋ฆฌ๋“œ (๋žœ๋ค)', + contextId: 'MATH_L18_OP16_ANY', layout: MathQuizLayout.gridSquare, operators: '+,-,*,/', blankCount: 6, blankType: MathQuizBlankType.numbersAndOperators, operationCount: 16, + puzzleCount: 2, + ), + // --- [์‹ ๊ทœ] ํŒจํ„ด 7: 4x4 ๊ทธ๋ฆฌ๋“œ (๊ณ ๋‚œ๋„) [Lv 19] --- + const MathQuizDifficulty( + levelIndex: 19, + name: 'Lv. 19: 4x4 ๊ทธ๋ฆฌ๋“œ (์ตœ์ƒ)', + contextId: 'MATH_L19_OP16_HARD', + layout: MathQuizLayout.gridSquare, + operators: '+,-,*,/', + blankCount: 8, + blankType: MathQuizBlankType.numbersAndOperators, + operationCount: 16, + puzzleCount: 1, ), ]; @@ -211,12 +265,11 @@ class MathQuizDifficulties { if (levelIndex < 1) levelIndex = 1; if (levelIndex > allDifficulties.length) levelIndex = allDifficulties.length; return allDifficulties.firstWhere((level) => level.levelIndex == levelIndex, - orElse: () => allDifficulties[0] - ); + orElse: () => allDifficulties[0]); } /// ๋žญํ‚น ํ™”๋ฉด์šฉ ๋งต (ContextId -> ์ด๋ฆ„) static Map get contextIdToNameMap { - return { for (var level in allDifficulties) level.contextId : level.name }; + return {for (var level in allDifficulties) level.contextId: level.name}; } } \ No newline at end of file diff --git a/packages/feature_game_mathquiz/lib/models/math_quiz_models.dart b/packages/feature_game_mathquiz/lib/models/math_quiz_models.dart index 2bbbdc6..71d5332 100644 --- a/packages/feature_game_mathquiz/lib/models/math_quiz_models.dart +++ b/packages/feature_game_mathquiz/lib/models/math_quiz_models.dart @@ -1,29 +1,36 @@ // packages/feature_game_mathquiz/lib/models/math_quiz_models.dart +/// ๊ฐœ๋ณ„ ๋นˆ์นธ์˜ ํƒ€์ž…์„ ์ •์˜ +enum PuzzleBlankType { number, operator } + +/// ๊ฒ€์ฆํ•ด์•ผ ํ•  ๋‹จ์ผ ๋ฐฉ์ •์‹์„ ์ •์˜ +class MathQuizEquation { + final List expressionIndices; + final int resultIndex; + + MathQuizEquation({ + required this.expressionIndices, + required this.resultIndex, + }); +} + /// ํผ์ฆ ํ•œ ํŒ์˜ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ class MathQuizPuzzle { - /// [์ˆ˜์ •] ๊ทธ๋ฆฌ๋“œ ์…€ ๋ฐ์ดํ„ฐ - /// - /// '?'๋Š” ๋นˆ์นธ, ' '๋Š” ๊ณต๋ฐฑ(๋นˆ ์…€)์ž…๋‹ˆ๋‹ค. - /// - /// ์˜ˆ: ["?", "+", "3", "=", "8"] (5x1 ๊ทธ๋ฆฌ๋“œ) - /// ์˜ˆ: ["?", "+", "2", "=", "5", "+", " ", "+", ... ] (5x5 ๊ทธ๋ฆฌ๋“œ) final List gridCells; - - /// ๊ทธ๋ฆฌ๋“œ์˜ ๊ฐ€๋กœ ์นธ ์ˆ˜ (์˜ˆ: 5) final int gridCrossAxisCount; - - /// ์œ ์ €๊ฐ€ ์ฑ„์›Œ์•ผ ํ•  ์ •๋‹ต ๋ชฉ๋ก (์ˆœ์„œ๋Œ€๋กœ) - final List solutions; - - /// ์œ ์ €์—๊ฒŒ ์ œ๊ณต๋  ์ˆซ์ž/๊ธฐํ˜ธ ๋ฒ„ํŠผ ์˜ต์…˜ final List options; + final List equations; + final List solutions; + + /// ๋นˆ์นธ์˜ ํƒ€์ž… ๋ชฉ๋ก (solutions์™€ 1:1 ๋งค์นญ) + final List blankTypes; MathQuizPuzzle({ - // โŒ puzzleType ์‚ญ์ œ required this.gridCells, required this.gridCrossAxisCount, - required this.solutions, required this.options, + required this.equations, + required this.solutions, + required this.blankTypes, // ๐Ÿ‘ˆ [์ถ”๊ฐ€] }); } \ No newline at end of file diff --git a/packages/feature_game_mathquiz/lib/screens/math_quiz_lobby_screen.dart b/packages/feature_game_mathquiz/lib/screens/math_quiz_lobby_screen.dart index 7f373f4..f0e4dc1 100644 --- a/packages/feature_game_mathquiz/lib/screens/math_quiz_lobby_screen.dart +++ b/packages/feature_game_mathquiz/lib/screens/math_quiz_lobby_screen.dart @@ -1,3 +1,4 @@ +// packages/feature_game_mathquiz/lib/screens/math_quiz_lobby_screen.dart import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -5,24 +6,27 @@ import 'package:provider/provider.dart'; // [C] ๊ณตํ†ต ์„œ๋น„์Šค (service_api) import 'package:service_api/service_api.dart'; // [A] ๊ณตํ†ต UI (feature_common) -import 'package:feature_common/feature_common.dart'; +import 'package:feature_common/feature_common.dart'; // [B] ์ด ํŒจํ‚ค์ง€ (mathquiz) import 'math_quiz_screen.dart'; -import '../controllers/math_quiz_controller.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ์ปจํŠธ๋กค๋Ÿฌ ์ž„ํฌํŠธ +import '../controllers/math_quiz_controller.dart'; +import '../models/math_quiz_difficulty.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] class MathQuizLobbyScreen extends StatefulWidget { - const MathQuizLobbyScreen({ super.key }); + const MathQuizLobbyScreen({super.key}); @override State createState() => _MathQuizLobbyScreenState(); } class _MathQuizLobbyScreenState extends State { - int _maxUnlockedLevel = 1; + int _maxUnlockedLevel = 1; Map _rankHistory = {}; bool _isLoading = false; - + late final SessionNotifier _sessionNotifier; + late final LobbyHelperService _lobbyHelper; + // [๐Ÿ”ฅ ์ˆ˜์ •] ์„œ๋น„์Šค๋ฅผ ์ง์ ‘ ์ƒ์„ฑ (Provider๋กœ ์ฝ์ง€ ์•Š์Œ) final PuzzleService _puzzleService = PuzzleService(); final IdentityService _identityService = IdentityService(); @@ -30,61 +34,61 @@ class _MathQuizLobbyScreenState extends State { void initState() { super.initState(); _sessionNotifier = context.read(); + + // [๐Ÿ”ฅ ์ˆ˜์ •] ํ—ฌํผ ์„œ๋น„์Šค ์ดˆ๊ธฐํ™” (์ง์ ‘ ์ƒ์„ฑํ•œ ์„œ๋น„์Šค ์ฃผ์ž…) + _lobbyHelper = LobbyHelperService( + identityService: _identityService, + puzzleService: _puzzleService, + ); + _loadProgress(forceRefreshRanks: true); } - /// ๋žญํ‚น ๋ฐ ๋ ˆ๋ฒจ ์ง„ํ–‰ ์ƒํ™ฉ ๋กœ๋“œ + /// [์ˆ˜์ •๋จ] ๊ณตํ†ต ํ—ฌํผ๋ฅผ ์‚ฌ์šฉ Future _loadProgress({bool forceRefreshRanks = false}) async { // 1. (๊ฐ€๋ฒผ์›€) ๋ ˆ๋ฒจ ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ - final maxLevel = await _identityService.getMaxUnlockedLevel(gameType: 'MATH_QUIZ'); - if (mounted) { setState(() { _maxUnlockedLevel = maxLevel; }); } + final maxLevel = await _lobbyHelper.loadMaxLevel('MATH_QUIZ'); + if (mounted) { + setState(() { + _maxUnlockedLevel = maxLevel; + }); + } // 2. (๋ฌด๊ฑฐ์›€) ๋žญํ‚น ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ (ํ•„์š”ํ•  ๋•Œ๋งŒ) if (!forceRefreshRanks) return; final String? myName = _sessionNotifier.session?.userName; - if (myName == null) return; - + if (myName == null) return; + try { - final Map oldRankMap = await _identityService.getLastSavedRankMap(gameType: 'MATH_QUIZ'); - List>> rankFutures = []; - for (final level in MathQuizDifficulties.allDifficulties) { - rankFutures.add(_puzzleService.fetchRanks('MATH_QUIZ', level.contextId)); + final rankHistory = await _lobbyHelper.loadRankHistory( + gameType: 'MATH_QUIZ', + myName: myName, + allLevels: MathQuizDifficulties.allDifficulties, + getLevelIndex: (level) => level.levelIndex, + ); + if (mounted) { + setState(() { + _rankHistory = rankHistory; + }); } - final List> allRankResults = await Future.wait(rankFutures); - Map newRankMapForStorage = {}; - Map newRankHistoryForState = {}; - - for (int i = 0; i < MathQuizDifficulties.allDifficulties.length; i++) { - final level = MathQuizDifficulties.allDifficulties[i]; - final currentRanks = allRankResults[i]; - final int levelIndex = level.levelIndex; - final int oldRank = oldRankMap[levelIndex] ?? 0; - int currentRank = 0; - int myRankIndex = currentRanks.indexWhere((r) => r.playerName == myName); - if (myRankIndex != -1) { currentRank = myRankIndex + 1; } - newRankMapForStorage[levelIndex] = currentRank; - newRankHistoryForState[levelIndex] = (oldRank, currentRank); - } - - await _identityService.saveLastRankMap(newRankMapForStorage, gameType: 'MATH_QUIZ'); - if (mounted) { setState(() { _rankHistory = newRankHistoryForState; }); } - log("์ˆ˜ํ•™ ํ€ด์ฆˆ ๋žญํ‚น ๋ณ€๋™ ํ™•์ธ ์™„๋ฃŒ. (์œ ์ €: $myName)"); } catch (e) { log("MathQuizLobbyScreen: ๋žญํ‚น ํ™•์ธ ์‹คํŒจ: $e"); } } - /// ๐Ÿ”ฝ [์ˆ˜์ •] ๊ฒŒ์ž„ ์‹œ์ž‘ ํ•จ์ˆ˜ + /// ๐Ÿ”ฝ [์ˆ˜์ • ์—†์Œ] ๊ฒŒ์ž„ ์‹œ์ž‘ ํ•จ์ˆ˜ Future _startGame(MathQuizDifficulty level) async { - setState(() { _isLoading = true; }); - + setState(() { + _isLoading = true; + }); + try { final session = _sessionNotifier.session; if (session == null) { throw Exception("์„ธ์…˜์ด ๋กœ๋“œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } - + // 1. ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ ๋ฐ ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ (์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ˜ธ์ถœ) final controller = MathQuizController(); controller.startNewGame(level, session.userId, session.userName); @@ -100,7 +104,7 @@ class _MathQuizLobbyScreenState extends State { ), ), ); - + // 3. ๊ฒŒ์ž„์—์„œ ๋Œ์•„์˜ค๋ฉด ๋ ˆ๋ฒจ ์ž ๊ธˆ ์ƒํƒœ๋งŒ ์ƒˆ๋กœ๊ณ ์นจ _loadProgress(forceRefreshRanks: false); } @@ -112,48 +116,44 @@ class _MathQuizLobbyScreenState extends State { } } finally { if (mounted) { - setState(() { _isLoading = false; }); + setState(() { + _isLoading = false; + }); } } } @override Widget build(BuildContext context) { - // ... (์ดํ•˜ build ๋ฉ”์„œ๋“œ๋Š” ์ด์ „๊ณผ ๋™์ผ) ... context.watch(); - context.watch(); - - final bool allLevelsUnlocked = _maxUnlockedLevel >= MathQuizDifficulties.allDifficulties.length; + context.watch(); + + final bool allLevelsUnlocked = + _maxUnlockedLevel >= MathQuizDifficulties.allDifficulties.length; final theme = Theme.of(context); return CommonGameShell( - title: '๊ณ„์‚ฐ ํ€ด์ฆˆ', - + title: '๊ณ„์‚ฐ ํ€ด์ฆˆ', onRankingPressed: () { - final List difficulties = MathQuizDifficulties.allDifficulties - .map((level) => GameDifficulty( - name: level.name, - contextId: level.contextId, - )) - .toList(); - Navigator.push( context, MaterialPageRoute( builder: (context) => RankingScreen( gameType: 'MATH_QUIZ', - difficulties: difficulties, - initialDifficultyName: MathQuizDifficulties.getLevel(_maxUnlockedLevel).name, + difficulties: MathQuizDifficulties.allDifficulties, + initialDifficultyName: + MathQuizDifficulties.getLevel(_maxUnlockedLevel).name, ), ), ); }, - body: LayoutBuilder( builder: (context, constraints) { const double maxContentRatio = 0.6; - final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 - ? 500 : (constraints.maxHeight * maxContentRatio); + final double constrainedWidth = + (constraints.maxHeight * maxContentRatio) > 500 + ? 500 + : (constraints.maxHeight * maxContentRatio); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: constrainedWidth), @@ -165,14 +165,19 @@ class _MathQuizLobbyScreenState extends State { child: ListView.builder( itemCount: MathQuizDifficulties.allDifficulties.length, itemBuilder: (context, index) { - final MathQuizDifficulty level = MathQuizDifficulties.allDifficulties[index]; - final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel; - final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0); - - Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null; + final MathQuizDifficulty level = + MathQuizDifficulties.allDifficulties[index]; + final bool isUnlocked = allLevelsUnlocked || + level.levelIndex <= _maxUnlockedLevel; + final (int oldRank, int currentRank) = + _rankHistory[level.levelIndex] ?? (0, 0); + + Widget? trailingWidget = isUnlocked + ? const Icon(Icons.play_arrow_rounded) + : null; String? subtitleText; Color? subtitleColor; - + if (currentRank > 0) { String rankStr = "${currentRank}์œ„"; if (oldRank > 0) { @@ -180,45 +185,71 @@ class _MathQuizLobbyScreenState extends State { if (change > 0) { subtitleText = "$rankStr (โ–ฒ $change)"; subtitleColor = Colors.green; - trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_up_rounded, + color: Colors.green, + size: 28); } else if (change < 0) { subtitleText = "$rankStr (โ–ผ ${change.abs()})"; subtitleColor = Colors.red; - trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_down_rounded, + color: Colors.red, + size: 28); } else { subtitleText = "$rankStr (์œ ์ง€)"; subtitleColor = Colors.grey; - trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28); + trailingWidget = const Icon( + Icons.check_circle_outline_rounded, + color: Colors.grey, + size: 28); } } else { subtitleText = "$rankStr (์‹ ๊ทœ ์ง„์ž…)"; subtitleColor = Colors.blue; - trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28); + trailingWidget = const Icon( + Icons.new_releases_rounded, + color: Colors.blue, + size: 28); } } else { if (oldRank > 0) { subtitleText = "๋žญํ‚น ์ดํƒˆ (์ด์ „ ${oldRank}์œ„)"; subtitleColor = Colors.orange; - trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28); + trailingWidget = const Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + size: 28); } } - + return Card( - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + margin: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 4.0), child: ListTile( leading: Icon( - isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded, + isUnlocked + ? Icons.lock_open_rounded + : Icons.lock_rounded, color: isUnlocked ? theme.primaryColor : Colors.grey, ), - title: Text(level.name, style: TextStyle( - fontSize: 18, - fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, - color: isUnlocked ? theme.textTheme.bodyLarge?.color : Colors.grey, - )), + title: Text(level.name, + style: TextStyle( + fontSize: 18, + fontWeight: isUnlocked + ? FontWeight.bold + : FontWeight.normal, + color: isUnlocked + ? theme.textTheme.bodyLarge?.color + : Colors.grey, + )), subtitle: subtitleText != null - ? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold)) - : null, - trailing: trailingWidget, + ? Text(subtitleText, + style: TextStyle( + color: subtitleColor, + fontWeight: FontWeight.bold)) + : null, + trailing: trailingWidget, onTap: isUnlocked && !_isLoading ? () => _startGame(level) : null, diff --git a/packages/feature_game_mathquiz/lib/screens/math_quiz_screen.dart b/packages/feature_game_mathquiz/lib/screens/math_quiz_screen.dart index bae96bd..4b13402 100644 --- a/packages/feature_game_mathquiz/lib/screens/math_quiz_screen.dart +++ b/packages/feature_game_mathquiz/lib/screens/math_quiz_screen.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:service_api/service_api.dart'; import 'package:feature_common/feature_common.dart'; import '../controllers/math_quiz_controller.dart'; +import '../models/math_quiz_difficulty.dart'; import '../models/math_quiz_models.dart'; class MathQuizScreen extends StatefulWidget { @@ -14,24 +15,28 @@ class MathQuizScreen extends StatefulWidget { class _MathQuizScreenState extends State { bool _isDialogShowing = false; - - // ( ... _showGameCompletion ๋ฉ”์„œ๋“œ๋Š” ์ด์ „๊ณผ ๋™์ผ ... ) - void _showGameCompletion(MathQuizController controller) async { + + // ... ( _showGameCompletion ๋ฉ”์„œ๋“œ๋Š” ์ด์ „๊ณผ ๋™์ผ ... ) + void _showGameCompletion(MathQuizController controller) async { String formatMathQuizScore(int primary, int? secondary) { - final problemCount = primary; + final blanksCount = primary; final time = (secondary ?? 0).toString(); - return '${problemCount}๊ฐœ (${time}์ดˆ)'; + return '์ด ${blanksCount}์นธ (${time}์ดˆ)'; } + Future saveMathQuizProgress(String playerName) async { final identityService = IdentityService(); - final int currentMaxLevel = await identityService.getMaxUnlockedLevel(gameType: 'MATH_QUIZ'); + final int currentMaxLevel = + await identityService.getMaxUnlockedLevel(gameType: 'MATH_QUIZ'); if (currentMaxLevel < 99) { if (controller.difficulty.levelIndex >= currentMaxLevel) { int nextLevel = controller.difficulty.levelIndex + 1; if (nextLevel > MathQuizDifficulties.allDifficulties.length) { - await identityService.saveMaxUnlockedLevel(99, gameType: 'MATH_QUIZ'); + await identityService.saveMaxUnlockedLevel(99, + gameType: 'MATH_QUIZ'); } else { - await identityService.saveMaxUnlockedLevel(nextLevel, gameType: 'MATH_QUIZ'); + await identityService.saveMaxUnlockedLevel(nextLevel, + gameType: 'MATH_QUIZ'); } } } @@ -41,11 +46,11 @@ class _MathQuizScreenState extends State { context, MaterialPageRoute( builder: (context) => GameCompletionScreen( - args: GameResultArgs( + args: GameResultArgs( gameType: 'MATH_QUIZ', contextId: controller.difficulty.contextId, - primaryScore: controller.puzzle.solutions.length, - secondaryScore: controller.secondsElapsed, + primaryScore: controller.totalBlanksFilled, + secondaryScore: controller.secondsElapsed, userId: controller.userId, userName: controller.userName, scoreFormatter: formatMathQuizScore, @@ -73,14 +78,30 @@ class _MathQuizScreenState extends State { return Scaffold( appBar: AppBar( - title: Text(controller.difficulty.name), + title: Text('Lv. ${controller.difficulty.levelIndex}'), actions: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Center( + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _buildTriesWidget(controller.remainingTries), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + '${controller.currentPuzzleIndex + 1} / ${controller.totalPuzzlesInLevel}', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.only(right: 20.0), child: Text( '${(controller.secondsElapsed ~/ 60).toString().padLeft(2, '0')}:${(controller.secondsElapsed % 60).toString().padLeft(2, '0')}', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + style: + const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), ), @@ -99,15 +120,27 @@ class _MathQuizScreenState extends State { ); } - /// ๐Ÿ”ฝ ์„ธ๋กœ ๋ชจ๋“œ ๋ ˆ์ด์•„์›ƒ - Widget _buildPortraitLayout(BuildContext context, MathQuizController controller) { + Widget _buildTriesWidget(int tries) { + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (index) { + return Icon( + index < tries ? Icons.favorite : Icons.favorite_border, + color: Colors.redAccent, + size: 24, + ); + }), + ); + } + + Widget _buildPortraitLayout( + BuildContext context, MathQuizController controller) { return Column( children: [ Expanded( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - // [์ˆ˜์ •] _buildPuzzleGrid๋ฅผ ํ•ญ์ƒ ํ˜ธ์ถœ child: _buildPuzzleGrid(context, controller), ), ), @@ -117,38 +150,33 @@ class _MathQuizScreenState extends State { ); } - /// ๐Ÿ”ฝ ๊ฐ€๋กœ ๋ชจ๋“œ ๋ ˆ์ด์•„์›ƒ - Widget _buildLandscapeLayout(BuildContext context, MathQuizController controller) { + Widget _buildLandscapeLayout( + BuildContext context, MathQuizController controller) { return Row( children: [ Expanded( - flex: 6, // ๋ฐฉ์ •์‹ ์˜์—ญ + flex: 6, child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - // [์ˆ˜์ •] _buildPuzzleGrid๋ฅผ ํ•ญ์ƒ ํ˜ธ์ถœ child: _buildPuzzleGrid(context, controller), ), ), ), Expanded( - flex: 4, // ํ‚คํŒจ๋“œ ์˜์—ญ + flex: 4, child: _buildKeypad(context, controller), ), ], ); } - // โŒ [์‚ญ์ œ] _buildEquationList ๋ฉ”์„œ๋“œ ์‚ญ์ œ - - /// [์ˆ˜์ •] _buildEquationGrid -> _buildPuzzleGrid - /// (๋ชจ๋“  ํผ์ฆ์„ ๊ทธ๋ฆฌ๋Š” ์œ ์ผํ•œ ๋นŒ๋”) Widget _buildPuzzleGrid(BuildContext context, MathQuizController controller) { final puzzle = controller.puzzle; final userAnswers = controller.userAnswers; final int crossAxisCount = puzzle.gridCrossAxisCount; - - int blankIndex = 0; + + int blankIndex = 0; return GridView.builder( shrinkWrap: true, @@ -159,26 +187,32 @@ class _MathQuizScreenState extends State { ), itemBuilder: (context, index) { final String part = puzzle.gridCells[index]; - + if (part == "?") { - // ๋นˆ์นธ์ผ ๊ฒฝ์šฐ final int currentBlankIndex = blankIndex; blankIndex++; return _buildBlankBox( context, controller, currentBlankIndex, - userAnswers[currentBlankIndex], + (currentBlankIndex < userAnswers.length) + ? userAnswers[currentBlankIndex] + : null, ); } else if (part == " ") { - // " " (๋นˆ ๊ณต๊ฐ„) return Container(); } else { - // ์ˆซ์ž๋‚˜ ๊ธฐํ˜ธ์ผ ๊ฒฝ์šฐ return Center( - child: Text( - part, - style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + child: FittedBox( + fit: BoxFit.contain, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + part, + style: const TextStyle( + fontSize: 32, fontWeight: FontWeight.bold), + ), + ), ), ); } @@ -186,38 +220,55 @@ class _MathQuizScreenState extends State { ); } - /// [์ˆ˜์ •] _buildBlankBox (isGrid ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ญ์ œ) - Widget _buildBlankBox(BuildContext context, MathQuizController controller, int index, String? value) { + Widget _buildBlankBox(BuildContext context, MathQuizController controller, + int index, String? value) { final theme = Theme.of(context); final bool isSelected = (controller.selectedBlankIndex == index); + final bool isWrong = controller.isWrongAnswer; + final bool isRevealing = controller.isRevealingAnswer; + + Color borderColor; + Color textColor; + + if (isRevealing) { + borderColor = theme.colorScheme.primary; + textColor = theme.colorScheme.primary; + } else if (isWrong) { + borderColor = theme.colorScheme.error; + textColor = theme.colorScheme.onSurfaceVariant; + } else if (isSelected) { + borderColor = theme.colorScheme.primary; + textColor = theme.colorScheme.onSurfaceVariant; + } else { + borderColor = Colors.transparent; + textColor = theme.colorScheme.onSurfaceVariant; + } - // [์ˆ˜์ •] ๊ทธ๋ฆฌ๋“œ ์…€์˜ ํฌ๊ธฐ๋Š” GridView๊ฐ€ ๊ฒฐ์ •ํ•˜๋ฏ€๋กœ - // AspectRatio๋ฅผ ์‚ฌ์šฉํ•ด 1:1 ๋น„์œจ ์œ ์ง€ return AspectRatio( aspectRatio: 1 / 1, child: GestureDetector( onTap: () => controller.onBlankTapped(index), child: Container( - margin: const EdgeInsets.all(4.0), // ๊ทธ๋ฆฌ๋“œ/๋ฆฌ์ŠคํŠธ ๊ณตํ†ต ์—ฌ๋ฐฑ + margin: const EdgeInsets.all(4.0), decoration: BoxDecoration( color: theme.colorScheme.surfaceVariant, border: Border.all( - color: isSelected ? theme.colorScheme.primary : Colors.transparent, + color: borderColor, width: 3, ), borderRadius: BorderRadius.circular(8), ), child: Center( - // [์ˆ˜์ •] ํฐํŠธ ํฌ๊ธฐ๋ฅผ FittedBox๋กœ ์ž๋™ ์กฐ์ ˆ child: FittedBox( fit: BoxFit.contain, child: Padding( padding: const EdgeInsets.all(4.0), child: Text( - value ?? '', + value ?? '', style: TextStyle( + fontSize: 32, fontWeight: FontWeight.bold, - color: theme.colorScheme.onSurfaceVariant, + color: textColor, ), ), ), @@ -228,13 +279,32 @@ class _MathQuizScreenState extends State { ); } - /// 2. ํ‚คํŒจ๋“œ UI ๋นŒ๋” + /// [๐Ÿ”ฅ ์ˆ˜์ •๋จ] ํ‚คํŒจ๋“œ UI ๋นŒ๋” (๋™์  ํ•„ํ„ฐ๋ง) Widget _buildKeypad(BuildContext context, MathQuizController controller) { - // ... (์ดํ•˜ ํ‚คํŒจ๋“œ ๋กœ์ง์€ ๋™์ผ) ... final puzzle = controller.puzzle; final theme = Theme.of(context); + final bool isDisabled = controller.isRevealingAnswer; + + final PuzzleBlankType? selectedType = controller.currentSelectedBlankType; + + // [๐Ÿ”ฅ ์ตœ์ข… ํ™•์ธ] ํ—ฌํผ ํ•จ์ˆ˜ + bool isOperator(String val) => ['/', '*', '-', '+'].contains(val); + bool isNumber(String val) => int.tryParse(val) != null; + + // [๐Ÿ”ฅ ์ตœ์ข… ์ˆ˜์ •] ํƒ€์ž…์— ๋”ฐ๋ผ ์˜ต์…˜ ํ•„ํ„ฐ๋ง + List availableOptions = []; + if (selectedType == PuzzleBlankType.number) { + // ์ˆซ์ž๊ฐ€ ํ•„์š”ํ•˜๋ฉด ์ˆซ์ž๋งŒ ํ•„ํ„ฐ๋ง + availableOptions = puzzle.options.where((opt) => isNumber(opt)).toList(); + } else if (selectedType == PuzzleBlankType.operator) { + // ์—ฐ์‚ฐ์ž๊ฐ€ ํ•„์š”ํ•˜๋ฉด ์—ฐ์‚ฐ์ž๋งŒ ํ•„ํ„ฐ๋ง + availableOptions = puzzle.options.where((opt) => isOperator(opt)).toList(); + } + + final int totalButtonCount = availableOptions.length + 1; // ํ•„ํ„ฐ๋ง๋œ ์˜ต์…˜ + ์ง€์šฐ๊ธฐ return Container( + // ... (GridView.builder ์ดํ•˜ ๋กœ์ง์€ ๋™์ผ) ... padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, @@ -255,19 +325,18 @@ class _MathQuizScreenState extends State { mainAxisSpacing: 8, crossAxisSpacing: 8, ), - itemCount: puzzle.options.length + 1, // ์˜ต์…˜ + ์ง€์šฐ๊ธฐ ๋ฒ„ํŠผ + itemCount: totalButtonCount, itemBuilder: (context, index) { - - if (index == puzzle.options.length) { + if (index == availableOptions.length) { return FilledButton.tonal( - onPressed: () => controller.onClearTapped(), + onPressed: isDisabled ? null : () => controller.onClearTapped(), child: const Icon(Icons.backspace_outlined), ); } - - final String option = puzzle.options[index]; + + final String option = availableOptions[index]; return FilledButton( - onPressed: () => controller.onOptionTapped(option), + onPressed: isDisabled ? null : () => controller.onOptionTapped(option), child: Text( option, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), diff --git a/packages/feature_game_spider/lib/controllers/spider_game_controller.dart b/packages/feature_game_spider/lib/controllers/spider_game_controller.dart index 96fc08d..0f63531 100644 --- a/packages/feature_game_spider/lib/controllers/spider_game_controller.dart +++ b/packages/feature_game_spider/lib/controllers/spider_game_controller.dart @@ -32,12 +32,13 @@ class SpiderGameController with ChangeNotifier { List _cardsToDealAnimate = []; List get cardsToDealAnimate => _cardsToDealAnimate; - + void clearDealAnimationTrigger() { - debugPrint("[LOG] clearDealAnimationTrigger: CALLED. Clearing animation queue (Current count: ${_cardsToDealAnimate.length})."); + debugPrint( + "[LOG] clearDealAnimationTrigger: CALLED. Clearing animation queue (Current count: ${_cardsToDealAnimate.length})."); _cardsToDealAnimate.clear(); } - + List _cardsToAnimateStack = []; List get cardsToAnimateStack => _cardsToAnimateStack; int _animationSourcePileIndex = -1; @@ -47,8 +48,8 @@ class SpiderGameController with ChangeNotifier { bool get canUndo { return _undoHistory.isNotEmpty && - !_isGameCompleted && - _undoCount < maxUndoCount; + !_isGameCompleted && + _undoCount < maxUndoCount; } void setUserInfo(String userId, String? userName) { @@ -99,6 +100,7 @@ class SpiderGameController with ChangeNotifier { } return deck; } + (List>, List) _dealCards( List shuffledDeck, String distribution) { final List> tableau = List.generate(10, (_) => []); @@ -118,6 +120,7 @@ class SpiderGameController with ChangeNotifier { } return (tableau, stock); } + void _startTimer() { _timer?.cancel(); _secondsElapsed = 0; @@ -126,6 +129,7 @@ class SpiderGameController with ChangeNotifier { notifyListeners(); }); } + void stopTimer() { _timer?.cancel(); } @@ -133,7 +137,7 @@ class SpiderGameController with ChangeNotifier { /// ๐Ÿ”ฝ ๋ฑ ๋ถ„๋ฐฐ (์• ๋‹ˆ๋ฉ”์ด์…˜ ํŠธ๋ฆฌ๊ฑฐ) void dealFromStock() { debugPrint("[LOG] dealFromStock: CALLED. Checking conditions..."); - + if (_currentState.stock.isEmpty) { debugPrint("[LOG] dealFromStock: FAILED (Stock is empty)"); return; @@ -152,8 +156,9 @@ class SpiderGameController with ChangeNotifier { } final bool hasEmptyPile = _currentState.tableau.any((pile) => pile.isEmpty); - debugPrint("[LOG] dealFromStock: Checking for empty piles... Result: $hasEmptyPile"); - + debugPrint( + "[LOG] dealFromStock: Checking for empty piles... Result: $hasEmptyPile"); + if (hasEmptyPile) { debugPrint("[LOG] dealFromStock: FAILED (Empty pile found)"); return; @@ -163,52 +168,66 @@ class SpiderGameController with ChangeNotifier { _saveUndoState(); final int cardsToDealCount = min(10, _currentState.stock.length); - debugPrint("[LOG] dealFromStock: Preparing ${cardsToDealCount} cards for animation."); - + debugPrint( + "[LOG] dealFromStock: Preparing ${cardsToDealCount} cards for animation."); + for (int i = 0; i < cardsToDealCount; i++) { _cardsToDealAnimate.add(_currentState.stock.removeLast()); } - - debugPrint("[LOG] dealFromStock: Cards moved to _cardsToDealAnimate queue (Total: ${_cardsToDealAnimate.length}). Notifying listeners..."); + + debugPrint( + "[LOG] dealFromStock: Cards moved to _cardsToDealAnimate queue (Total: ${_cardsToDealAnimate.length}). Notifying listeners..."); notifyListeners(); } - + /// ๐Ÿ”ฝ ๋ฑ ๋ถ„๋ฐฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚œ ํ›„ UI๊ฐ€ ํ˜ธ์ถœ void finalizeDealFromStock(List dealtCards) { - debugPrint("[LOG] finalizeDealFromStock: CALLED. Finalizing ${dealtCards.length} cards."); - + debugPrint( + "[LOG] finalizeDealFromStock: CALLED. Finalizing ${dealtCards.length} cards."); + for (int i = 0; i < dealtCards.length; i++) { final card = dealtCards[i]; card.isFaceUp = true; _currentState.tableau[i].add(card); } - + _currentState = _currentState.copyWith(moves: _currentState.moves + 1); - - debugPrint("[LOG] finalizeDealFromStock: FINISHED. Calling _checkCompletedStacks..."); + + debugPrint( + "[LOG] finalizeDealFromStock: FINISHED. Calling _checkCompletedStacks..."); _checkCompletedStacks(); } // ( onDragStarted, onDragCancelled, onCardsDropped, _moveCards ๋Š” ๋™์ผ ) void onDragStarted(List cards) { _draggedCards = cards; - for (var card in cards) { card.isBeingDragged = true; } + for (var card in cards) { + card.isBeingDragged = true; + } notifyListeners(); } + void onDragCancelled() { - for (var card in _draggedCards) { card.isBeingDragged = false; } + for (var card in _draggedCards) { + card.isBeingDragged = false; + } _draggedCards = []; notifyListeners(); } + void onCardsDropped(List cards, int targetPileIndex) { final int sourcePileIndex = _findPileIndexForCard(cards.first); - for (var card in cards) { card.isBeingDragged = false; } + for (var card in cards) { + card.isBeingDragged = false; + } _draggedCards = []; _moveCards(cards, sourcePileIndex, targetPileIndex); } + void _moveCards(List cards, int fromIndex, int toIndex) { if (fromIndex == toIndex) { - notifyListeners(); return; + notifyListeners(); + return; } _saveUndoState(); final sourcePile = _currentState.tableau[fromIndex]; @@ -221,17 +240,17 @@ class SpiderGameController with ChangeNotifier { _currentState = _currentState.copyWith(moves: _currentState.moves + 1); _checkCompletedStacks(); } - + int undo() { if (!canUndo) return _undoCount; - + final prevState = _undoHistory.removeLast(); _currentState = SpiderGameState.fromHistory(prevState); _undoCount++; notifyListeners(); return _undoCount; } - + // ( canPickUpCard, getDraggableStack, isValidMove ๋Š” ๋™์ผ ) bool canPickUpCard(SpiderCard card) { for (final pile in _currentState.tableau) { @@ -239,6 +258,7 @@ class SpiderGameController with ChangeNotifier { } return false; } + List getDraggableStack(SpiderCard tappedCard) { final int pileIndex = _findPileIndexForCard(tappedCard); if (pileIndex == -1) return []; @@ -251,15 +271,15 @@ class SpiderGameController with ChangeNotifier { final currentCard = pile[i]; if (currentCard.isFaceUp && prevCard.rank == currentCard.rank + 1 && - prevCard.suit == currentCard.suit) - { + prevCard.suit == currentCard.suit) { draggableStack.add(currentCard); } else { - return []; + return []; } } return draggableStack; } + bool isValidMove(List cardsToMove, int targetPileIndex) { if (cardsToMove.isEmpty) return false; final targetPile = _currentState.tableau[targetPileIndex]; @@ -272,7 +292,7 @@ class SpiderGameController with ChangeNotifier { /// ๐Ÿ”ฝ _checkCompletedStacks (์• ๋‹ˆ๋ฉ”์ด์…˜ ํŠธ๋ฆฌ๊ฑฐ) void _checkCompletedStacks() { // ๐Ÿ”ฝ [์ˆ˜์ •] ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹คํ–‰ ์ค‘์ด๋ฉด ์ค‘๋ณต ๊ฒ€์‚ฌ ๋ฐฉ์ง€ - if (_cardsToAnimateStack.isNotEmpty) return; + if (_cardsToAnimateStack.isNotEmpty) return; bool stackCompleted = false; for (int i = 0; i < _currentState.tableau.length; i++) { @@ -295,12 +315,12 @@ class SpiderGameController with ChangeNotifier { _cardsToAnimateStack = last13Cards; _animationSourcePileIndex = i; _animationTargetFoundationIndex = _currentState.foundation.length; - + stackCompleted = true; - break; + break; } } - + if (stackCompleted) { notifyListeners(); // ๐Ÿ‘ˆ UI์— ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ทธ๋ฆฌ๋ผ๊ณ  ์•Œ๋ฆผ } else { @@ -309,38 +329,43 @@ class SpiderGameController with ChangeNotifier { } /// ๐Ÿ”ฝ [์ˆ˜์ •] ์Šคํƒ ์™„์„ฑ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚œ ํ›„ UI๊ฐ€ ํ˜ธ์ถœ (์ธ์ž ๋ฐ›๋„๋ก ๋ณ€๊ฒฝ) - void finalizeStackCompletion(List cardsToAnimate, int sourceIndex) { - debugPrint("[LOG] finalizeStackCompletion: CALLED. Source Index: $sourceIndex"); + void finalizeStackCompletion( + List cardsToAnimate, int sourceIndex) { + debugPrint( + "[LOG] finalizeStackCompletion: CALLED. Source Index: $sourceIndex"); // ๐Ÿ”ฝ [์ˆ˜์ •] ํฌ๋ž˜์‹œ ๋ฐฉ์ง€ if (sourceIndex < 0 || sourceIndex >= _currentState.tableau.length) { - debugPrint("[LOG] finalizeStackCompletion: FAILED. Invalid Source Index: $sourceIndex"); + debugPrint( + "[LOG] finalizeStackCompletion: FAILED. Invalid Source Index: $sourceIndex"); return; } - + _currentState.foundation.add(cardsToAnimate); final pile = _currentState.tableau[sourceIndex]; - + if (pile.length >= cardsToAnimate.length) { pile.removeRange(pile.length - cardsToAnimate.length, pile.length); } else { - debugPrint("[LOG] finalizeStackCompletion: WARNING. Pile length was ${pile.length}, expected >= ${cardsToAnimate.length}."); + debugPrint( + "[LOG] finalizeStackCompletion: WARNING. Pile length was ${pile.length}, expected >= ${cardsToAnimate.length}."); } if (pile.isNotEmpty && !pile.last.isFaceUp) { pile.last.isFaceUp = true; } - + // ๐Ÿ”ฝ [์‚ญ์ œ] ์ธ๋ฑ์Šค ๋ฆฌ์…‹ ๋ถˆํ•„์š” (์ง€์—ญ ๋ณ€์ˆ˜๋กœ ์ฒ˜๋ฆฌ๋จ) // _animationSourcePileIndex = -1; // _animationTargetFoundationIndex = -1; - + _checkGameCompletion(); // ๐Ÿ‘ˆ [ํ•ต์‹ฌ] ๊ฒŒ์ž„ ์™„๋ฃŒ ๊ฒ€์‚ฌ } - + // ๐Ÿ”ฝ [๋ณต์›๋จ] void clearStackAnimationTrigger() { - debugPrint("[LOG] clearStackAnimationTrigger: CALLED. Clearing animation queue (Current count: ${_cardsToAnimateStack.length})."); + debugPrint( + "[LOG] clearStackAnimationTrigger: CALLED. Clearing animation queue (Current count: ${_cardsToAnimateStack.length})."); _cardsToAnimateStack.clear(); } @@ -348,7 +373,8 @@ class SpiderGameController with ChangeNotifier { if (_currentState.foundation.length == 8 && !_isGameCompleted) { _isGameCompleted = true; stopTimer(); - debugPrint("๊ฒŒ์ž„ ์™„๋ฃŒ! ์ด๋™: ${_currentState.moves}, ์‹œ๊ฐ„: $_secondsElapsed"); + debugPrint( + "๊ฒŒ์ž„ ์™„๋ฃŒ! ์ด๋™: ${_currentState.moves}, ์‹œ๊ฐ„: $_secondsElapsed"); notifyListeners(); // ๐Ÿ‘ˆ [์ˆ˜์ •] ๊ฒŒ์ž„์ด '์™„๋ฃŒ'๋˜์—ˆ์„ ๋•Œ๋งŒ notify } else if (!_isGameCompleted) { // ๐Ÿ”ฝ [์ˆ˜์ •] ๊ฒŒ์ž„์ด ์™„๋ฃŒ๋˜์ง€ '์•Š์•˜์„' ๋•Œ๋„ notify (์นด๋“œ ์ด๋™ ๋“ฑ์„ ๋ฐ˜์˜ํ•˜๊ธฐ ์œ„ํ•ด) @@ -357,42 +383,21 @@ class SpiderGameController with ChangeNotifier { // (๊ฒŒ์ž„์ด ์™„๋ฃŒ๋œ ํ›„์—๋Š” ๋” ์ด์ƒ notifyํ•˜์ง€ ์•Š์Œ) } - // ( _saveUndoState, _findPileIndexForCard, submitRank, dispose ๋Š” ๋™์ผ ) + // ( _saveUndoState, _findPileIndexForCard ๋Š” ๋™์ผ ) void _saveUndoState() { _undoHistory.add(SpiderGameHistory.fromState(_currentState)); if (_undoHistory.length > 20) { _undoHistory.removeAt(0); } } + int _findPileIndexForCard(SpiderCard card) { return _currentState.tableau.indexWhere((pile) => pile.contains(card)); } - Future submitRank(String playerName) async { - final puzzleService = PuzzleService(); - final identityService = IdentityService(); - final rankDto = UnifiedRankDto( - userId: userId, - gameType: 'SPIDER', - contextId: difficulty.contextId, - playerName: playerName, - primaryScore: _currentState.moves, - secondaryScore: _secondsElapsed, - ); - final result = await puzzleService.submitRank(rankDto); - await identityService.saveUserName(playerName); - final int currentMaxLevel = await identityService.getMaxUnlockedLevel(gameType: 'SPIDER'); - if (currentMaxLevel < 99) { - if (difficulty.levelIndex >= currentMaxLevel) { - int nextLevel = difficulty.levelIndex + 1; - if (nextLevel > SpiderDifficulties.allDifficulties.length) { - await identityService.saveMaxUnlockedLevel(99, gameType: 'SPIDER'); - } else { - await identityService.saveMaxUnlockedLevel(nextLevel, gameType: 'SPIDER'); - } - } - } - return result; - } + + // โŒ [์‚ญ์ œ] submitRank ๋ฉ”์„œ๋“œ (์•ฝ 20์ค„) ์‚ญ์ œ + // Future submitRank(String playerName) async { ... } + @override void dispose() { _timer?.cancel(); diff --git a/packages/feature_game_spider/lib/screens/spider_game_screen.dart b/packages/feature_game_spider/lib/screens/spider_game_screen.dart index 3e45bd3..1785428 100644 --- a/packages/feature_game_spider/lib/screens/spider_game_screen.dart +++ b/packages/feature_game_spider/lib/screens/spider_game_screen.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:service_api/service_api.dart'; -import 'package:feature_common/feature_common.dart'; + import 'package:feature_common/feature_common.dart'; import '../controllers/spider_game_controller.dart'; import '../models/spider_difficulty.dart'; import '../models/spider_card.dart'; diff --git a/packages/feature_game_spider/lib/screens/spider_lobby_screen.dart b/packages/feature_game_spider/lib/screens/spider_lobby_screen.dart index d33fda1..c62121a 100644 --- a/packages/feature_game_spider/lib/screens/spider_lobby_screen.dart +++ b/packages/feature_game_spider/lib/screens/spider_lobby_screen.dart @@ -2,146 +2,147 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:service_api/service_api.dart'; // ๐Ÿ‘ˆ SessionNotifier ํฌํ•จ +import 'package:service_api/service_api.dart'; import 'package:feature_common/feature_common.dart'; import 'spider_game_screen.dart'; import '../models/spider_difficulty.dart'; -import '../controllers/spider_game_controller.dart'; +import '../controllers/spider_game_controller.dart'; class SpiderLobbyScreen extends StatefulWidget { - const SpiderLobbyScreen({ super.key }); + const SpiderLobbyScreen({super.key}); @override State createState() => _SpiderLobbyScreenState(); } class _SpiderLobbyScreenState extends State { - int _maxUnlockedLevel = 1; + int _maxUnlockedLevel = 1; Map _rankHistory = {}; - // โŒ String? _userName; (SessionNotifier๊ฐ€ ๊ด€๋ฆฌ) bool _isLoading = false; - - // ๐Ÿ”ฝ [์ˆ˜์ •] SessionNotifier๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด IdentityService ๋Œ€์‹  ์ถ”๊ฐ€ + late final SessionNotifier _sessionNotifier; + late final LobbyHelperService _lobbyHelper; + // [๐Ÿ”ฅ ์ˆ˜์ •] ์„œ๋น„์Šค๋ฅผ ์ง์ ‘ ์ƒ์„ฑ (Provider๋กœ ์ฝ์ง€ ์•Š์Œ) final PuzzleService _puzzleService = PuzzleService(); - final IdentityService _identityService = IdentityService(); // ๐Ÿ‘ˆ (save/load progress๋ฅผ ์œ„ํ•ด ์œ ์ง€) + final IdentityService _identityService = IdentityService(); @override void initState() { super.initState(); - // ๐Ÿ”ฝ [์ˆ˜์ •] initState์—์„œ SessionNotifier๋ฅผ read - // SessionNotifier์˜ loadSession()์ด ๋จผ์ € ์™„๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ๊ฐ€์ • _sessionNotifier = context.read(); - - // ๐Ÿ”ฝ [์ˆ˜์ •] _loadProgress๊ฐ€ ๋žญํ‚น๊นŒ์ง€ ๋ชจ๋‘ ์ƒˆ๋กœ๊ณ ์นจ (์ตœ์ดˆ 1ํšŒ) + + // [๐Ÿ”ฅ ์ˆ˜์ •] ํ—ฌํผ ์„œ๋น„์Šค ์ดˆ๊ธฐํ™” (์ง์ ‘ ์ƒ์„ฑํ•œ ์„œ๋น„์Šค ์ฃผ์ž…) + _lobbyHelper = LobbyHelperService( + identityService: _identityService, + puzzleService: _puzzleService, + ); + _loadProgress(forceRefreshRanks: true); } - /// ๐Ÿ”ฝ [์ˆ˜์ •] _loadProgress ๋ฉ”์„œ๋“œ (SessionNotifier ์‚ฌ์šฉ ๋ฐ ๋กœ์ง ๋ถ„๋ฆฌ) + /// [์ˆ˜์ •๋จ] ๊ณตํ†ต ํ—ฌํผ๋ฅผ ์‚ฌ์šฉ Future _loadProgress({bool forceRefreshRanks = false}) async { // 1. (๊ฐ€๋ฒผ์›€) ๋ ˆ๋ฒจ ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ - final maxLevel = await _identityService.getMaxUnlockedLevel(gameType: 'SPIDER'); - if (mounted) { - setState(() { - _maxUnlockedLevel = maxLevel; - }); + final maxLevel = await _lobbyHelper.loadMaxLevel('SPIDER'); + if (mounted) { + setState(() { + _maxUnlockedLevel = maxLevel; + }); } // 2. (๋ฌด๊ฑฐ์›€) ๋žญํ‚น ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ (ํ•„์š”ํ•  ๋•Œ๋งŒ) if (!forceRefreshRanks) return; - // ๐Ÿ”ฝ [์ˆ˜์ •] _sessionNotifier์—์„œ ์œ ์ € ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ด final String? myName = _sessionNotifier.session?.userName; - if (myName == null) return; // ๊ฒŒ์ŠคํŠธ์ด๊ฑฐ๋‚˜ ์•„์ง ์ด๋ฆ„ ์ €์žฅ์„ ์•ˆ ํ•จ + if (myName == null) return; try { - final Map oldRankMap = await _identityService.getLastSavedRankMap(gameType: 'SPIDER'); - List>> rankFutures = []; - for (final level in SpiderDifficulties.allDifficulties) { - rankFutures.add(_puzzleService.fetchRanks('SPIDER', level.contextId)); + final rankHistory = await _lobbyHelper.loadRankHistory( + gameType: 'SPIDER', + myName: myName, + allLevels: SpiderDifficulties.allDifficulties, + getLevelIndex: (level) => level.levelIndex, + ); + if (mounted) { + setState(() { + _rankHistory = rankHistory; + }); } - final List> allRankResults = await Future.wait(rankFutures); - Map newRankMapForStorage = {}; - Map newRankHistoryForState = {}; - for (int i = 0; i < SpiderDifficulties.allDifficulties.length; i++) { - final level = SpiderDifficulties.allDifficulties[i]; - final currentRanks = allRankResults[i]; - final int levelIndex = level.levelIndex; - final int oldRank = oldRankMap[levelIndex] ?? 0; - int currentRank = 0; - int myRankIndex = currentRanks.indexWhere((r) => r.playerName == myName); - if (myRankIndex != -1) { currentRank = myRankIndex + 1; } - newRankMapForStorage[levelIndex] = currentRank; - newRankHistoryForState[levelIndex] = (oldRank, currentRank); - } - await _identityService.saveLastRankMap(newRankMapForStorage, gameType: 'SPIDER'); - if (mounted) { setState(() { _rankHistory = newRankHistoryForState; }); } - log("์ŠคํŒŒ์ด๋” ๋žญํ‚น ๋ณ€๋™ ํ™•์ธ ์™„๋ฃŒ. (์œ ์ €: $myName)"); } catch (e) { log("SpiderLobbyScreen: ๋žญํ‚น ํ™•์ธ ์‹คํŒจ: $e"); } } - - /// ๐Ÿ”ฝ [์ˆ˜์ •] _startGame ๋ฉ”์„œ๋“œ (SessionNotifier ์‚ฌ์šฉ) + /// ๐Ÿ”ฝ [์ˆ˜์ • ์—†์Œ] _startGame ๋ฉ”์„œ๋“œ Future _startGame(SpiderDifficulty level) async { - setState(() { _isLoading = true; }); + setState(() { + _isLoading = true; + }); - // 1. [์ˆ˜์ •] SessionNotifier์—์„œ ์œ ์ € ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ final session = _sessionNotifier.session; if (session == null) { log("์„ธ์…˜์ด ๋กœ๋“œ๋˜์ง€ ์•Š์•„ ๊ฒŒ์ž„์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - setState(() { _isLoading = false; }); + setState(() { + _isLoading = false; + }); return; } - + final String userId = session.userId; final String? userName = session.userName; - // 2. ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ ๋ฐ ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ final gameController = SpiderGameController(); - gameController.setUserInfo(userId, userName); // ๐Ÿ‘ˆ ์œ ์ € ์ •๋ณด ์ฃผ์ž… + gameController.setUserInfo(userId, userName); gameController.startNewGame(level); - setState(() { _isLoading = false; }); + setState(() { + _isLoading = false; + }); if (!mounted) return; await Navigator.push( context, MaterialPageRoute( - builder: (context) => - ChangeNotifierProvider.value( - value: gameController, - child: const SpiderGameScreen(), - ), + builder: (context) => ChangeNotifierProvider.value( + value: gameController, + child: const SpiderGameScreen(), + ), ), ); - - // ๐Ÿ”ฝ [ํ•ต์‹ฌ ์ˆ˜์ •] - // ๋žญํ‚น(forceRefreshRanks: true)์€ ์ƒˆ๋กœ๊ณ ์นจํ•˜์ง€ ์•Š๊ณ , - // ๋ ˆ๋ฒจ ์ž ๊ธˆ ์ƒํƒœ(forceRefreshRanks: false)๋งŒ ์ƒˆ๋กœ๊ณ ์นจํ•ฉ๋‹ˆ๋‹ค. + _loadProgress(forceRefreshRanks: false); } @override Widget build(BuildContext context) { - // ๐Ÿ”ฝ [์ˆ˜์ •] ThemeNotifier์™€ SessionNotifier๋ฅผ ๋ชจ๋‘ watch context.watch(); - context.watch(); // ๐Ÿ‘ˆ ์„ธ์…˜ ๋ณ€๊ฒฝ(๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ) ๊ฐ์ง€ - - final bool allLevelsUnlocked = _maxUnlockedLevel >= 9; + context.watch(); + + final bool allLevelsUnlocked = + _maxUnlockedLevel >= SpiderDifficulties.allDifficulties.length; final theme = Theme.of(context); return CommonGameShell( title: '์ŠคํŒŒ์ด๋” ์†”๋ฆฌํ…Œ์–ด', onRankingPressed: () { - // ... (๋žญํ‚น ๋ฒ„ํŠผ ๋กœ์ง ๋™์ผ) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RankingScreen( + gameType: 'SPIDER', + difficulties: SpiderDifficulties.allDifficulties, + initialDifficultyName: + SpiderDifficulties.getLevel(_maxUnlockedLevel).name, + ), + ), + ); }, - // ๐Ÿ”ฝ [์ˆ˜์ •] RefreshIndicator ์ถ”๊ฐ€ (๋‹น๊ฒจ์„œ ๋žญํ‚น ์ƒˆ๋กœ๊ณ ์นจ) body: LayoutBuilder( builder: (context, constraints) { const double maxContentRatio = 0.6; - final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 - ? 500 : (constraints.maxHeight * maxContentRatio); + final double constrainedWidth = + (constraints.maxHeight * maxContentRatio) > 500 + ? 500 + : (constraints.maxHeight * maxContentRatio); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: constrainedWidth), @@ -153,11 +154,15 @@ class _SpiderLobbyScreenState extends State { child: ListView.builder( itemCount: SpiderDifficulties.allDifficulties.length, itemBuilder: (context, index) { - // ... (์ดํ•˜ ListTile ๋กœ์ง์€ ๋ชจ๋‘ ๋™์ผ) - final SpiderDifficulty level = SpiderDifficulties.allDifficulties[index]; - final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel; - final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0); - Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null; + final SpiderDifficulty level = + SpiderDifficulties.allDifficulties[index]; + final bool isUnlocked = allLevelsUnlocked || + level.levelIndex <= _maxUnlockedLevel; + final (int oldRank, int currentRank) = + _rankHistory[level.levelIndex] ?? (0, 0); + Widget? trailingWidget = isUnlocked + ? const Icon(Icons.play_arrow_rounded) + : null; String? subtitleText; Color? subtitleColor; if (currentRank > 0) { @@ -167,44 +172,70 @@ class _SpiderLobbyScreenState extends State { if (change > 0) { subtitleText = "$rankStr (โ–ฒ $change)"; subtitleColor = Colors.green; - trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_up_rounded, + color: Colors.green, + size: 28); } else if (change < 0) { subtitleText = "$rankStr (โ–ผ ${change.abs()})"; subtitleColor = Colors.red; - trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_down_rounded, + color: Colors.red, + size: 28); } else { subtitleText = "$rankStr (์œ ์ง€)"; subtitleColor = Colors.grey; - trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28); + trailingWidget = const Icon( + Icons.check_circle_outline_rounded, + color: Colors.grey, + size: 28); } } else { subtitleText = "$rankStr (์‹ ๊ทœ ์ง„์ž…)"; subtitleColor = Colors.blue; - trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28); + trailingWidget = const Icon( + Icons.new_releases_rounded, + color: Colors.blue, + size: 28); } } else { if (oldRank > 0) { subtitleText = "๋žญํ‚น ์ดํƒˆ (์ด์ „ ${oldRank}์œ„)"; subtitleColor = Colors.orange; - trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28); + trailingWidget = const Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + size: 28); } } return Card( - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + margin: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 4.0), child: ListTile( leading: Icon( - isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded, + isUnlocked + ? Icons.lock_open_rounded + : Icons.lock_rounded, color: isUnlocked ? theme.primaryColor : Colors.grey, ), - title: Text(level.name, style: TextStyle( - fontSize: 18, - fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, - color: isUnlocked ? theme.textTheme.bodyLarge?.color : Colors.grey, - )), + title: Text(level.name, + style: TextStyle( + fontSize: 18, + fontWeight: isUnlocked + ? FontWeight.bold + : FontWeight.normal, + color: isUnlocked + ? theme.textTheme.bodyLarge?.color + : Colors.grey, + )), subtitle: subtitleText != null - ? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold)) - : null, - trailing: trailingWidget, + ? Text(subtitleText, + style: TextStyle( + color: subtitleColor, + fontWeight: FontWeight.bold)) + : null, + trailing: trailingWidget, onTap: isUnlocked && !_isLoading ? () => _startGame(level) : null, diff --git a/packages/feature_game_sudoku/lib/models/game_level.dart b/packages/feature_game_sudoku/lib/models/game_level.dart index 03a33f3..47a37d8 100644 --- a/packages/feature_game_sudoku/lib/models/game_level.dart +++ b/packages/feature_game_sudoku/lib/models/game_level.dart @@ -1,22 +1,27 @@ // packages/feature_game_sudoku/lib/models/game_level.dart -// (์ด ํŒŒ์ผ์€ service_api์—์„œ ์ด๋™ํ•ด ์˜ด) +import 'package:service_api/service_api.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ๊ณตํ†ต ๋ชจ๋ธ import -class GameLevel { +// +// [๐Ÿ”ฅ ์ˆ˜์ •] 'extends GameDifficulty' ์ถ”๊ฐ€ +// +class GameLevel extends GameDifficulty { final int levelIndex; // 1-11 - final String name; // "์ž…๋ฌธ (4x4)" final int blockSize; // 2, 3, 4 final int generatorLevel; // ์„œ๋ฒ„์— ์š”์ฒญํ•  ์ƒ์„ฑ๊ธฐ ๋‚œ์ด๋„ (1~5) - final String contextId; // ๋žญํ‚น ID "SUDOKU_4x4_L1" - + + // โŒ 'name'๊ณผ 'contextId'๋Š” GameDifficulty๊ฐ€ ์ด๋ฏธ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ œ๊ฑฐ + // final String name; + // final String contextId; + final bool isSequentialNumbers; final bool isSequentialLetters; const GameLevel({ required this.levelIndex, - required this.name, + required super.name, // ๐Ÿ‘ˆ [์ˆ˜์ •] super()๋กœ ์ „๋‹ฌ + required super.contextId, // ๐Ÿ‘ˆ [์ˆ˜์ •] super()๋กœ ์ „๋‹ฌ required this.blockSize, required this.generatorLevel, - required this.contextId, this.isSequentialNumbers = false, this.isSequentialLetters = false, }); @@ -26,64 +31,91 @@ class AppLevels { static final List allLevels = [ // --- 2x2 (blockSize = 2) --- const GameLevel( - levelIndex: 1, name: "์ž…๋ฌธ (4x4)", blockSize: 2, generatorLevel: 1, - contextId: "SUDOKU_4x4_L1", isSequentialNumbers: true - ), + levelIndex: 1, + name: "์ž…๋ฌธ (4x4)", + blockSize: 2, + generatorLevel: 1, + contextId: "SUDOKU_4x4_L1", + isSequentialNumbers: true), const GameLevel( - levelIndex: 2, name: "์ดˆ๊ธ‰ (4x4)", blockSize: 2, generatorLevel: 3, - contextId: "SUDOKU_4x4_L3", isSequentialLetters: true - ), + levelIndex: 2, + name: "์ดˆ๊ธ‰ (4x4)", + blockSize: 2, + generatorLevel: 3, + contextId: "SUDOKU_4x4_L3", + isSequentialLetters: true), const GameLevel( - levelIndex: 3, name: "์ˆ™๋ จ (4x4)", blockSize: 2, generatorLevel: 5, - contextId: "SUDOKU_4x4_L5" - ), - + levelIndex: 3, + name: "์ˆ™๋ จ (4x4)", + blockSize: 2, + generatorLevel: 5, + contextId: "SUDOKU_4x4_L5"), + // --- 3x3 (blockSize = 3) --- const GameLevel( - levelIndex: 4, name: "์‰ฌ์›€ (9x9)", blockSize: 3, generatorLevel: 1, - contextId: "SUDOKU_9x9_L1", isSequentialNumbers: true - ), + levelIndex: 4, + name: "์‰ฌ์›€ (9x9)", + blockSize: 3, + generatorLevel: 1, + contextId: "SUDOKU_9x9_L1", + isSequentialNumbers: true), const GameLevel( - levelIndex: 5, name: "์ค‘๊ธ‰ (9x9)", blockSize: 3, generatorLevel: 2, - contextId: "SUDOKU_9x9_L2", isSequentialLetters: true - ), + levelIndex: 5, + name: "์ค‘๊ธ‰ (9x9)", + blockSize: 3, + generatorLevel: 2, + contextId: "SUDOKU_9x9_L2", + isSequentialLetters: true), const GameLevel( - levelIndex: 6, name: "์ƒ๊ธ‰ (9x9)", blockSize: 3, generatorLevel: 3, - contextId: "SUDOKU_9x9_L3" - ), + levelIndex: 6, + name: "์ƒ๊ธ‰ (9x9)", + blockSize: 3, + generatorLevel: 3, + contextId: "SUDOKU_9x9_L3"), const GameLevel( - levelIndex: 7, name: "์–ด๋ ค์›€ (9x9)", blockSize: 3, generatorLevel: 4, - contextId: "SUDOKU_9x9_L4" - ), + levelIndex: 7, + name: "์–ด๋ ค์›€ (9x9)", + blockSize: 3, + generatorLevel: 4, + contextId: "SUDOKU_9x9_L4"), const GameLevel( - levelIndex: 8, name: "์ตœ์ƒ๊ธ‰ (9x9)", blockSize: 3, generatorLevel: 5, - contextId: "SUDOKU_9x9_L5" - ), + levelIndex: 8, + name: "์ตœ์ƒ๊ธ‰ (9x9)", + blockSize: 3, + generatorLevel: 5, + contextId: "SUDOKU_9x9_L5"), // --- 4x4 (blockSize = 4) --- const GameLevel( - levelIndex: 9, name: "์ „๋ฌธ๊ฐ€ (16x16)", blockSize: 4, generatorLevel: 1, - contextId: "SUDOKU_16x16_L1", isSequentialNumbers: true - ), + levelIndex: 9, + name: "์ „๋ฌธ๊ฐ€ (16x16)", + blockSize: 4, + generatorLevel: 1, + contextId: "SUDOKU_16x16_L1", + isSequentialNumbers: true), const GameLevel( - levelIndex: 10, name: "๋งˆ์Šคํ„ฐ (16x16)", blockSize: 4, generatorLevel: 3, - contextId: "SUDOKU_16x16_L3", isSequentialLetters: true - ), + levelIndex: 10, + name: "๋งˆ์Šคํ„ฐ (16x16)", + blockSize: 4, + generatorLevel: 3, + contextId: "SUDOKU_16x16_L3", + isSequentialLetters: true), const GameLevel( - levelIndex: 11, name: "์ง€์˜ฅ (16x16)", blockSize: 4, generatorLevel: 5, - contextId: "SUDOKU_16x16_L5" - ), + levelIndex: 11, + name: "์ง€์˜ฅ (16x16)", + blockSize: 4, + generatorLevel: 5, + contextId: "SUDOKU_16x16_L5"), ]; static GameLevel getLevel(int levelIndex) { if (levelIndex < 1) levelIndex = 1; if (levelIndex > allLevels.length) levelIndex = allLevels.length; return allLevels.firstWhere((level) => level.levelIndex == levelIndex, - orElse: () => allLevels[0] - ); + orElse: () => allLevels[0]); } static Map get contextIdToNameMap { - return { for (var level in allLevels) level.contextId : level.name }; + return {for (var level in allLevels) level.contextId: level.name}; } } \ No newline at end of file diff --git a/packages/feature_game_sudoku/lib/screens/sudoku_lobby_screen.dart b/packages/feature_game_sudoku/lib/screens/sudoku_lobby_screen.dart index 1b49814..a7edd83 100644 --- a/packages/feature_game_sudoku/lib/screens/sudoku_lobby_screen.dart +++ b/packages/feature_game_sudoku/lib/screens/sudoku_lobby_screen.dart @@ -6,105 +6,102 @@ import 'package:provider/provider.dart'; // [C] ์„œ๋น„์Šค import import 'package:service_api/service_api.dart'; // [A] ๊ณตํ†ต ์…ธ(Shell) ์œ„์ ฏ import -import 'package:feature_common/feature_common.dart'; +import 'package:feature_common/feature_common.dart'; // [B] ๊ฐ™์€ ํŒจํ‚ค์ง€ ๋‚ด์˜ ํ™”๋ฉด/๋ชจ๋ธ import import 'game_screen.dart'; -import '../models/game_level.dart'; // ๐Ÿ‘ˆ ์Šค๋„์ฟ  ์ „์šฉ ๋ ˆ๋ฒจ +import '../models/game_level.dart'; class SudokuLobbyScreen extends StatefulWidget { - const SudokuLobbyScreen({ super.key }); + const SudokuLobbyScreen({super.key}); @override State createState() => _SudokuLobbyScreenState(); } class _SudokuLobbyScreenState extends State { - int _maxUnlockedLevel = 1; + int _maxUnlockedLevel = 1; Map _rankHistory = {}; - // โŒ String? _userName; (SessionNotifier๊ฐ€ ๊ด€๋ฆฌ) late String _selectedThemeName; bool _isLoading = false; - - // ๐Ÿ”ฝ [์ˆ˜์ •] SessionNotifier๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด IdentityService ๋Œ€์‹  ์ถ”๊ฐ€ + late final SessionNotifier _sessionNotifier; + late final LobbyHelperService _lobbyHelper; + // [๐Ÿ”ฅ ์ˆ˜์ •] ์„œ๋น„์Šค๋ฅผ ์ง์ ‘ ์ƒ์„ฑ (Provider๋กœ ์ฝ์ง€ ์•Š์Œ) final PuzzleService _puzzleService = PuzzleService(); - final IdentityService _identityService = IdentityService(); // ๐Ÿ‘ˆ (save/load progress๋ฅผ ์œ„ํ•ด ์œ ์ง€) + final IdentityService _identityService = IdentityService(); @override void initState() { super.initState(); _selectedThemeName = AppThemes.random; - - // ๐Ÿ”ฝ [์ˆ˜์ •] initState์—์„œ SessionNotifier๋ฅผ read _sessionNotifier = context.read(); - // ๐Ÿ”ฝ [์ˆ˜์ •] _loadProgress๊ฐ€ ๋žญํ‚น๊นŒ์ง€ ๋ชจ๋‘ ์ƒˆ๋กœ๊ณ ์นจ (์ตœ์ดˆ 1ํšŒ) + // [๐Ÿ”ฅ ์ˆ˜์ •] ํ—ฌํผ ์„œ๋น„์Šค ์ดˆ๊ธฐํ™” (์ง์ ‘ ์ƒ์„ฑํ•œ ์„œ๋น„์Šค ์ฃผ์ž…) + _lobbyHelper = LobbyHelperService( + identityService: _identityService, + puzzleService: _puzzleService, + ); + _loadProgress(forceRefreshRanks: true); } - /// ๐Ÿ”ฝ [์ˆ˜์ •] _loadProgress ๋ฉ”์„œ๋“œ (SessionNotifier ์‚ฌ์šฉ ๋ฐ ๋กœ์ง ๋ถ„๋ฆฌ) + /// [์ˆ˜์ •๋จ] ๊ณตํ†ต ํ—ฌํผ๋ฅผ ์‚ฌ์šฉ Future _loadProgress({bool forceRefreshRanks = false}) async { // 1. (๊ฐ€๋ฒผ์›€) ๋ ˆ๋ฒจ ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ - final maxLevel = await _identityService.getMaxUnlockedLevel(); - if (mounted) { setState(() { _maxUnlockedLevel = maxLevel; }); } + final maxLevel = await _lobbyHelper.loadMaxLevel('SUDOKU'); + if (mounted) { + setState(() { + _maxUnlockedLevel = maxLevel; + }); + } // 2. (๋ฌด๊ฑฐ์›€) ๋žญํ‚น ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ (ํ•„์š”ํ•  ๋•Œ๋งŒ) if (!forceRefreshRanks) return; - // ๐Ÿ”ฝ [์ˆ˜์ •] _sessionNotifier์—์„œ ์œ ์ € ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ด final String? myName = _sessionNotifier.session?.userName; - if (myName == null) return; // ๊ฒŒ์ŠคํŠธ์ด๊ฑฐ๋‚˜ ์•„์ง ์ด๋ฆ„ ์ €์žฅ์„ ์•ˆ ํ•จ - + if (myName == null) return; + try { - final Map oldRankMap = await _identityService.getLastSavedRankMap(); - List>> rankFutures = []; - for (final level in AppLevels.allLevels) { - rankFutures.add(_puzzleService.fetchRanks('SUDOKU', level.contextId)); + final rankHistory = await _lobbyHelper.loadRankHistory( + gameType: 'SUDOKU', + myName: myName, + allLevels: AppLevels.allLevels, + getLevelIndex: (level) => level.levelIndex, + ); + if (mounted) { + setState(() { + _rankHistory = rankHistory; + }); } - final List> allRankResults = await Future.wait(rankFutures); - Map newRankMapForStorage = {}; - Map newRankHistoryForState = {}; - for (int i = 0; i < AppLevels.allLevels.length; i++) { - final level = AppLevels.allLevels[i]; - final currentRanks = allRankResults[i]; - final int levelIndex = level.levelIndex; - final int oldRank = oldRankMap[levelIndex] ?? 0; - int currentRank = 0; - int myRankIndex = currentRanks.indexWhere((r) => r.playerName == myName); - if (myRankIndex != -1) { currentRank = myRankIndex + 1; } - newRankMapForStorage[levelIndex] = currentRank; - newRankHistoryForState[levelIndex] = (oldRank, currentRank); - } - await _identityService.saveLastRankMap(newRankMapForStorage); - if (mounted) { setState(() { _rankHistory = newRankHistoryForState; }); } - log("๋ชจ๋“  ๋ ˆ๋ฒจ ๋žญํ‚น ๋ณ€๋™ ํ™•์ธ ์™„๋ฃŒ. (์œ ์ €: $myName)"); } catch (e) { log("SudokuLobbyScreen: ๋žญํ‚น ํ™•์ธ ์‹คํŒจ: $e"); } } - /// ๐Ÿ”ฝ [์ˆ˜์ •] _startGame ๋ฉ”์„œ๋“œ (SessionNotifier ์‚ฌ์šฉ) + /// ๐Ÿ”ฝ [์ˆ˜์ •] _startGame ๋ฉ”์„œ๋“œ (PuzzleService ์ง์ ‘ ์‚ฌ์šฉ) Future _startGame(GameLevel level) async { - setState(() { _isLoading = true; }); - + setState(() { + _isLoading = true; + }); + try { - // 1. [์ˆ˜์ •] SessionNotifier์—์„œ ์œ ์ € ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ final session = _sessionNotifier.session; if (session == null) { throw Exception("์„ธ์…˜์ด ๋กœ๋“œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } final String difficulty = level.levelIndex.toString(); + // [๐Ÿ”ฅ ์ˆ˜์ •] _puzzleService ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ final SudokuGameDto gameData = await _puzzleService.startGame(difficulty); - + final String userId = session.userId; - final String? userName = session.userName; - + final String? userName = session.userName; + if (mounted) { await Navigator.push( context, MaterialPageRoute( - builder: (context) => GameScreen( + builder: (context) => GameScreen( gameData: gameData, themeName: _selectedThemeName, userId: userId, @@ -113,10 +110,7 @@ class _SudokuLobbyScreenState extends State { ), ), ); - - // ๐Ÿ”ฝ [ํ•ต์‹ฌ ์ˆ˜์ •] - // ๋žญํ‚น(forceRefreshRanks: true)์€ ์ƒˆ๋กœ๊ณ ์นจํ•˜์ง€ ์•Š๊ณ , - // ๋ ˆ๋ฒจ ์ž ๊ธˆ ์ƒํƒœ(forceRefreshRanks: false)๋งŒ ์ƒˆ๋กœ๊ณ ์นจํ•ฉ๋‹ˆ๋‹ค. + _loadProgress(forceRefreshRanks: false); } } catch (e) { @@ -127,99 +121,93 @@ class _SudokuLobbyScreenState extends State { } } finally { if (mounted) { - setState(() { _isLoading = false; }); + setState(() { + _isLoading = false; + }); } } } @override Widget build(BuildContext context) { - // ๐Ÿ”ฝ [์ˆ˜์ •] ThemeNotifier์™€ SessionNotifier๋ฅผ ๋ชจ๋‘ watch - context.watch(); // ํ…Œ๋งˆ ๊ฐ์ง€ - context.watch(); // ๐Ÿ‘ˆ ์„ธ์…˜ ๋ณ€๊ฒฝ(๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ) ๊ฐ์ง€ - + context.watch(); + context.watch(); + final bool allLevelsUnlocked = _maxUnlockedLevel >= 99; final theme = Theme.of(context); - // [A] feature_common์˜ CommonGameShell์„ ์‚ฌ์šฉ return CommonGameShell( - title: '์Šค๋„์ฟ  ๊ฒŒ์ž„', // ์…ธ์˜ AppBar์— ํ‘œ์‹œ๋  ์ œ๋ชฉ - - // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์‹คํ–‰๋  ํ•จ์ˆ˜๋ฅผ ์ฃผ์ž… + title: '์Šค๋„์ฟ  ๊ฒŒ์ž„', onRankingPressed: () { - - // 1. ์Šค๋„์ฟ  ๋ ˆ๋ฒจ(AppLevels)์„ ๊ณตํ†ต ๋ชจ๋ธ(GameDifficulty)๋กœ ๋ณ€ํ™˜ - final List sudokuDifficulties = AppLevels.allLevels - .map((level) => GameDifficulty( - name: level.name, - contextId: level.contextId, - )) - .toList(); - - // 2. ๊ณตํ†ต ๋žญํ‚น ํ™”๋ฉด(RankingScreen)์— ์ฃผ์ž…ํ•˜๋ฉฐ ํ˜ธ์ถœ + // [๐Ÿ”ฅ ์ˆ˜์ •] GameLevel์ด GameDifficulty๋ฅผ ์ƒ์†ํ•˜๋ฏ€๋กœ ๋ณ€ํ™˜(map) ๋ถˆํ•„์š” Navigator.push( context, MaterialPageRoute( builder: (context) => RankingScreen( - gameType: 'SUDOKU', // ๐Ÿ‘ˆ ์ด ๊ฒŒ์ž„์€ ์Šค๋„์ฟ  - difficulties: sudokuDifficulties, // ๐Ÿ‘ˆ ์Šค๋„์ฟ  ๋‚œ์ด๋„ ๋ชฉ๋ก + gameType: 'SUDOKU', + difficulties: AppLevels.allLevels, // ๐Ÿ‘ˆ [์ˆ˜์ •] initialDifficultyName: AppLevels.getLevel(_maxUnlockedLevel).name, ), ), ); }, - - // ๐Ÿ”ฝ ์…ธ์˜ 'body'์— ์Šค๋„์ฟ  ๋ ˆ๋ฒจ ๋ชฉ๋ก์„ ์ „๋‹ฌ body: LayoutBuilder( builder: (context, constraints) { const double maxContentRatio = 0.6; - final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 - ? 500 : (constraints.maxHeight * maxContentRatio); + final double constrainedWidth = + (constraints.maxHeight * maxContentRatio) > 500 + ? 500 + : (constraints.maxHeight * maxContentRatio); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: constrainedWidth), child: Column( children: [ - // ํ…Œ๋งˆ ์„ ํƒ Dropdown Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), + padding: const EdgeInsets.symmetric( + horizontal: 20.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("ํ…Œ๋งˆ: ", style: TextStyle(fontSize: 18)), DropdownButton( value: _selectedThemeName, - items: AppThemes.selectableThemeNames.map((themeName) { + items: + AppThemes.selectableThemeNames.map((themeName) { return DropdownMenuItem( value: themeName, - child: Text(themeName, style: const TextStyle(fontSize: 20)), + child: + Text(themeName, style: const TextStyle(fontSize: 20)), ); }).toList(), onChanged: (themeName) { if (themeName != null) { - setState(() { _selectedThemeName = themeName; }); + setState(() { + _selectedThemeName = themeName; + }); } }, ), ], ), ), - // ๋ ˆ๋ฒจ ๋ชฉ๋ก ListView Expanded( - // ๐Ÿ”ฝ [์ˆ˜์ •] RefreshIndicator ์ถ”๊ฐ€ (๋‹น๊ฒจ์„œ ๋žญํ‚น ์ƒˆ๋กœ๊ณ ์นจ) child: RefreshIndicator( onRefresh: () => _loadProgress(forceRefreshRanks: true), child: ListView.builder( itemCount: AppLevels.allLevels.length, itemBuilder: (context, index) { - // ... (์ดํ•˜ ListTile ๋กœ์ง์€ ๋ชจ๋‘ ๋™์ผ) final GameLevel level = AppLevels.allLevels[index]; - final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel; - final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0); - Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null; + final bool isUnlocked = allLevelsUnlocked || + level.levelIndex <= _maxUnlockedLevel; + final (int oldRank, int currentRank) = + _rankHistory[level.levelIndex] ?? (0, 0); + Widget? trailingWidget = isUnlocked + ? const Icon(Icons.play_arrow_rounded) + : null; String? subtitleText; Color? subtitleColor; - + if (currentRank > 0) { String rankStr = "${currentRank}์œ„"; if (oldRank > 0) { @@ -227,45 +215,71 @@ class _SudokuLobbyScreenState extends State { if (change > 0) { subtitleText = "$rankStr (โ–ฒ $change)"; subtitleColor = Colors.green; - trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_up_rounded, + color: Colors.green, + size: 28); } else if (change < 0) { subtitleText = "$rankStr (โ–ผ ${change.abs()})"; subtitleColor = Colors.red; - trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28); + trailingWidget = const Icon( + Icons.arrow_circle_down_rounded, + color: Colors.red, + size: 28); } else { subtitleText = "$rankStr (์œ ์ง€)"; subtitleColor = Colors.grey; - trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28); + trailingWidget = const Icon( + Icons.check_circle_outline_rounded, + color: Colors.grey, + size: 28); } } else { subtitleText = "$rankStr (์‹ ๊ทœ ์ง„์ž…)"; subtitleColor = Colors.blue; - trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28); + trailingWidget = const Icon( + Icons.new_releases_rounded, + color: Colors.blue, + size: 28); } } else { if (oldRank > 0) { subtitleText = "๋žญํ‚น ์ดํƒˆ (์ด์ „ ${oldRank}์œ„)"; subtitleColor = Colors.orange; - trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28); + trailingWidget = const Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + size: 28); } } - + return Card( - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + margin: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 4.0), child: ListTile( leading: Icon( - isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded, + isUnlocked + ? Icons.lock_open_rounded + : Icons.lock_rounded, color: isUnlocked ? theme.primaryColor : Colors.grey, ), - title: Text(level.name, style: TextStyle( - fontSize: 18, - fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, - color: isUnlocked ? theme.textTheme.bodyLarge?.color : Colors.grey, - )), + title: Text(level.name, + style: TextStyle( + fontSize: 18, + fontWeight: isUnlocked + ? FontWeight.bold + : FontWeight.normal, + color: isUnlocked + ? theme.textTheme.bodyLarge?.color + : Colors.grey, + )), subtitle: subtitleText != null - ? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold)) - : null, - trailing: trailingWidget, + ? Text(subtitleText, + style: TextStyle( + color: subtitleColor, + fontWeight: FontWeight.bold)) + : null, + trailing: trailingWidget, onTap: isUnlocked && !_isLoading ? () => _startGame(level) : null, diff --git a/packages/service_api/lib/service_api.dart b/packages/service_api/lib/service_api.dart index 70ed3ed..30ee713 100644 --- a/packages/service_api/lib/service_api.dart +++ b/packages/service_api/lib/service_api.dart @@ -7,10 +7,11 @@ export 'models/sudoku_game_dto.dart'; export 'models/sudoku_theme.dart'; export 'models/unified_rank_dto.dart'; export 'models/validate_result_dto.dart'; -export 'models/math_quiz_difficulty.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] + // Services export 'services/identity_service.dart'; export 'services/puzzle_service.dart'; export 'services/theme_notifier.dart'; -export 'services/session_notifier.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] \ No newline at end of file +export 'services/session_notifier.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] +export 'services/lobby_helper_service.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] \ No newline at end of file diff --git a/packages/service_api/lib/services/lobby_helper_service.dart b/packages/service_api/lib/services/lobby_helper_service.dart new file mode 100644 index 0000000..f9ee127 --- /dev/null +++ b/packages/service_api/lib/services/lobby_helper_service.dart @@ -0,0 +1,70 @@ +// packages/service_api/lib/services/lobby_helper_service.dart +import 'dart:developer'; +import 'package:service_api/service_api.dart'; + +/// [์ˆ˜์ •๋จ] ๋กœ๋น„ ํ™”๋ฉด์˜ ์ค‘๋ณต ๋กœ์ง(๋žญํ‚น, ๋ ˆ๋ฒจ)์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณตํ†ต ์„œ๋น„์Šค +class LobbyHelperService { + final IdentityService _identityService; + final PuzzleService _puzzleService; + + LobbyHelperService({ + required IdentityService identityService, + required PuzzleService puzzleService, + }) : _identityService = identityService, + _puzzleService = puzzleService; + + /// (๊ฐ€๋ฒผ์›€) ํ˜„์žฌ ๊ฒŒ์ž„์˜ ์ตœ๋Œ€ ๋ ˆ๋ฒจ๋งŒ ๋กœ๋“œ + Future loadMaxLevel(String gameType) async { + return await _identityService.getMaxUnlockedLevel(gameType: gameType); + } + + /// [๐Ÿ”ฅ ์ˆ˜์ •๋จ] (๋ฌด๊ฑฐ์›€) ๋žญํ‚น ๋ณ€๋™ ์ด๋ ฅ(โ–ฒโ–ผ)์„ ๋ฐ˜ํ™˜ (levelIndex ์ถ”์ถœ๊ธฐ๋Šฅ ์ฃผ์ž…) + Future> loadRankHistory({ + required String gameType, + required String myName, + required List allLevels, + required int Function(T level) getLevelIndex, // ๐Ÿ‘ˆ [์ˆ˜์ •] levelIndex ์ถ”์ถœ ํ•จ์ˆ˜ + }) async { + try { + final Map oldRankMap = + await _identityService.getLastSavedRankMap(gameType: gameType); + + List>> rankFutures = []; + for (final level in allLevels) { + rankFutures.add(_puzzleService.fetchRanks(gameType, level.contextId)); + } + + final List> allRankResults = + await Future.wait(rankFutures); + Map newRankMapForStorage = {}; + Map newRankHistoryForState = {}; + + for (int i = 0; i < allLevels.length; i++) { + final level = allLevels[i]; + final currentRanks = allRankResults[i]; + + // [๐Ÿ”ฅ ์ˆ˜์ •] ์ฃผ์ž…๋ฐ›์€ ํ•จ์ˆ˜๋กœ levelIndex๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ถ”์ถœ + final int levelIndex = getLevelIndex(level); + + final int oldRank = oldRankMap[levelIndex] ?? 0; + int currentRank = 0; + int myRankIndex = + currentRanks.indexWhere((r) => r.playerName == myName); + if (myRankIndex != -1) { + currentRank = myRankIndex + 1; + } + newRankMapForStorage[levelIndex] = currentRank; + newRankHistoryForState[levelIndex] = (oldRank, currentRank); + } + + await _identityService.saveLastRankMap(newRankMapForStorage, + gameType: gameType); + + log("$gameType ๋žญํ‚น ๋ณ€๋™ ํ™•์ธ ์™„๋ฃŒ. (์œ ์ €: $myName)"); + return newRankHistoryForState; + } catch (e) { + log("LobbyHelperService($gameType): ๋žญํ‚น ํ™•์ธ ์‹คํŒจ: $e"); + rethrow; + } + } +} \ No newline at end of file