This commit is contained in:
2025-11-19 11:17:33 +09:00
parent 3b053530f5
commit 2008c377f4
17 changed files with 1416 additions and 966 deletions
@@ -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<String?> _userAnswers;
List<String?> 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<String> 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();
}
}
});
}
}
@@ -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<String> 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<int> 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<int> blankIndices = (candidateIndices..shuffle(_random)).sublist(0, finalBlankCount);
blankIndices.sort(); // 👈 FIX
final List<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> gridCells = List.of(allParts);
for (int index in blankIndices) {
solutions.add(allParts[index]);
gridCells[index] = '?';
}
final Set<String> 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<String> 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<String> 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<String> 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<String> 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<int> candidateIndices;
switch (level.blankType) {
@@ -83,31 +122,110 @@ class MathQuizGenerator {
}
final int finalBlankCount = min(blankCount, candidateIndices.length);
final List<int> blankIndices = (candidateIndices..shuffle(_random)).sublist(0, finalBlankCount);
blankIndices.sort(); // 👈 FIX
// [LOG 3] blankIndices 리스트 결정 직후 확인
debugPrint("--- GEN LOG (Lv 4-6): BLANK INDICES: $blankIndices");
final List<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> gridCells = List.of(allParts);
for (int index in blankIndices) {
solutions.add(allParts[index]);
gridCells[index] = '?';
}
// 3. 옵션 생성
final Set<String> 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<String> 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<String> 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<int> numberIndices = [0, 2, 10, 12];
List<int> operatorIndices = [1, 11];
List<int> 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<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> 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<String> 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<String> 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<String> solutions = [];
final List<String> gridCells = List.of(allParts);
List<int> numberIndices = [0, 2, 10, 12]; // A,B,C,D
List<int> operatorIndices = [1, 5, 7, 11]; // op1,op3,op4,op2
debugPrint("--- GEN LOG (Lv 10-12 LINKED): ALL PARTS: $allParts"); // [LOG]
List<int> numberIndices = [0, 2, 10, 12];
List<int> operatorIndices = [1, 5, 7, 11];
List<int> 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<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> gridCells = List.of(allParts);
for (int index in blankIndices) {
solutions.add(allParts[index]);
gridCells[index] = '?';
}
final Set<String> 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<String> 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<String> allParts = selectedTemplate.gridCells;
final List<int> numberIndices = selectedTemplate.numberIndices;
final List<int> 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<num> r = expr.map((e) => e.interpret()).toList();
if (r.any((res) => res.toInt() != res || res < -99 || res > 999)) { continue; }
final List<int> rInt = r.map((res) => res.toInt()).toList();
final List<String> 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<String> finalSolutions = [];
final List<String> gridCells = List.of(allParts);
List<int> 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<String> optionsSet = finalSolutions.toSet(); // 👈 [오류 수정]
while (optionsSet.length < 9) {
optionsSet.add((_random.nextInt(9) + 1).toString());
}
optionsSet.addAll(level.operators.split(','));
optionsSet.add('=');
final List<String> options = (optionsSet.toList()..shuffle(_random)).toList();
return MathQuizPuzzle(
gridCells: gridCells,
gridCrossAxisCount: 7, // 3x3 퍼즐 (7x7 그리드)
solutions: finalSolutions,
options: options, // 👈 [오류 수정]
);
final List<int> numberIndices = [0, 2, 4, 14, 16, 18, 28, 30, 32];
final List<int> operatorIndices = [1, 3, 7, 9, 11, 15, 17, 21, 23, 25, 29, 31];
List<int> 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<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> 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<String> optionsSet = solutions.toSet();
while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); }
optionsSet.addAll(level.operators.split(','));
optionsSet.add('=');
final List<String> 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<String> allParts = selectedTemplate.gridCells;
final List<int> numberIndices = selectedTemplate.numberIndices;
final List<int> 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<num> r = expr.map((e) => e.interpret()).toList();
if (r.any((res) => res.toInt() != res || res < -999 || res > 9999)) { continue; }
final List<int> rInt = r.map((res) => res.toInt()).toList();
final List<String> 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<String> finalSolutions = [];
final List<String> gridCells = List.of(allParts);
List<int> 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<String> optionsSet = finalSolutions.toSet(); // 👈 [오류 수정]
while (optionsSet.length < 9) {
optionsSet.add((_random.nextInt(9) + 1).toString());
}
optionsSet.addAll(level.operators.split(','));
optionsSet.add('=');
final List<String> options = (optionsSet.toList()..shuffle(_random)).toList();
return MathQuizPuzzle(
gridCells: gridCells,
gridCrossAxisCount: 9, // 4x4 퍼즐 (9x9 그리드)
solutions: finalSolutions,
options: options, // 👈 [오류 수정]
);
final List<int> numberIndices = [0, 2, 4, 6, 18, 20, 22, 24, 36, 38, 40, 42, 54, 56, 58, 60];
final List<int> 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<int> 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<String> solutions = [];
final List<PuzzleBlankType> finalBlankTypes = [];
final List<String> 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<String> optionsSet = solutions.toSet();
while (optionsSet.length < 9) { optionsSet.add((_random.nextInt(9) + 1).toString()); }
optionsSet.addAll(level.operators.split(','));
optionsSet.add('=');
final List<String> 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<String> 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<String> 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<String> gridCells;
final List<int> numberIndices;
final List<int> operatorIndices;
_GridTemplate({
required this.gridCells,
required this.numberIndices,
required this.operatorIndices,
});
}