This commit is contained in:
lunaticbum 2025-11-19 17:16:43 +09:00
parent 09665fa073
commit 3228e370c5
3 changed files with 91 additions and 58 deletions

View File

@ -32,10 +32,15 @@ class CardFlipController with ChangeNotifier {
int _remainingTime = 0;
int get remainingTime => _remainingTime;
// [🔥 ] ( )
bool _isGameStarted = false;
bool get isGameStarted => _isGameStarted;
// [🔥 ]
int get matchedPairsCount => _cards.where((c) => c.isMatched).length ~/ 2;
// [🔥 ]
int get totalPairsCount => _cards.length ~/ 2;
void setUserInfo(String userId, String? userName) {
this.userId = userId;
this.userName = userName;
@ -49,21 +54,17 @@ class CardFlipController with ChangeNotifier {
_isGameCompleted = false;
_isTimeOut = false;
_isProcessing = false;
_isGameStarted = false; //
_isGameStarted = false;
_firstFlippedCard = null;
_generateCards();
// [🔥 ] startNewGame에서는 ( )
notifyListeners();
}
void restartGame() {
startNewGame(difficulty);
// startTimer
// UI에서
}
// [🔥 ] Public으로 (UI에서 )
void startGameTimer() {
if (_isGameStarted) return;
_isGameStarted = true;
@ -83,38 +84,61 @@ class CardFlipController with ChangeNotifier {
notifyListeners();
}
// 🔽 [🔥 ] ( )
void _generateCards() {
final int totalCards = difficulty.totalCards;
final int pairsCount = totalCards ~/ 2;
List<CardItem> deck = [];
final Random random = Random();
if (difficulty.contentType == CardContentType.calculation) {
// 1. ( )
var entries = CardFlipDifficulties.calculationPairs.entries.toList()..shuffle();
final Set<int> usedResults = {};
for (int i = 0; i < pairsCount; i++) {
var entry = entries[i % entries.length];
String matchKey = "CALC_$i"; // ID
//
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
//
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value));
String equation;
String answer;
int result;
while (true) {
final int opType = random.nextInt(3);
int a, b;
if (opType == 0) { // +
a = random.nextInt(15) + 1;
b = random.nextInt(15) + 1;
result = a + b;
equation = "$a + $b";
} else if (opType == 1) { // -
a = random.nextInt(20) + 5;
b = random.nextInt(a - 1) + 1;
result = a - b;
equation = "$a - $b";
} else { // *
a = random.nextInt(9) + 2;
b = random.nextInt(5) + 2;
result = a * b;
equation = "$a x $b";
}
if (!usedResults.contains(result)) {
usedResults.add(result);
answer = result.toString();
break;
}
}
String matchKey = "CALC_$i";
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: equation));
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: answer));
}
}
else if (difficulty.contentType == CardContentType.pairWord) {
// 2. (A B)
var entries = CardFlipDifficulties.wordPairs.entries.toList()..shuffle();
for (int i = 0; i < pairsCount; i++) {
var entry = entries[i % entries.length];
String matchKey = "PAIR_$i";
// A
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.key));
// B
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: entry.value));
}
}
else {
// 3. (, , )
List<String> pool = List.of(CardFlipDifficulties.emojis)..shuffle();
for (int i = 0; i < pairsCount; i++) {
String content;
@ -128,16 +152,13 @@ class CardFlipController with ChangeNotifier {
content = pool[i % pool.length];
}
// 2
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
deck.add(CardItem(id: 0, matchId: matchKey, displayContent: content));
}
}
// 4. ID
deck.shuffle(Random());
deck.shuffle(random);
for (int i = 0; i < deck.length; i++) {
// ID
deck[i] = CardItem(
id: i,
matchId: deck[i].matchId,
@ -147,9 +168,7 @@ class CardFlipController with ChangeNotifier {
_cards = deck;
}
// 🔽 []
void onCardTapped(CardItem card) {
// , ,
if (!_isGameStarted || _isGameCompleted || _isProcessing || card.isFaceUp || card.isMatched) return;
card.isFaceUp = true;
@ -165,10 +184,8 @@ class CardFlipController with ChangeNotifier {
}
}
// 🔽 [🔥 ] : matchId로
void _checkMatch(CardItem card1, CardItem card2) {
if (card1.matchId == card2.matchId) {
//
card1.isMatched = true;
card2.isMatched = true;
_isProcessing = false;
@ -179,7 +196,6 @@ class CardFlipController with ChangeNotifier {
}
notifyListeners();
} else {
//
Future.delayed(const Duration(milliseconds: 800), () {
card1.isFaceUp = false;
card2.isFaceUp = false;

View File

@ -3,20 +3,18 @@
import 'package:flutter/material.dart';
import 'package:service_api/service_api.dart';
///
enum CardContentType {
emoji, // : (🐰 🐰)
icon, // : ( )
number, // : (1 1)
calculation, // [🔥 ] (3+4 7)
pairWord, // [🔥 ] ( )
emoji, // 🐰 🐰
icon, //
number, // 1 1
calculation, // 3+4 7 ( )
pairWord, //
}
///
class CardItem {
final int id; // (GridView , )
final String matchId; // [🔥 ] ID ( )
final String displayContent; // [🔥 ]
final int id;
final String matchId;
final String displayContent;
bool isFaceUp;
bool isMatched;
@ -51,7 +49,7 @@ class CardFlipDifficulty extends GameDifficulty {
}
class CardFlipDifficulties {
// --- ---
//
static const List<String> emojis = [
"🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯",
"🦁", "🐮", "🐷", "🐸", "🐵", "🐔", "🐧", "🐦", "🐤", "🦆",
@ -59,14 +57,9 @@ class CardFlipDifficulties {
"", "🏀", "🏈", "", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸"
];
// [🔥 ] ( : )
static const Map<String, String> calculationPairs = {
"2 + 3": "5", "5 + 5": "10", "7 - 2": "5", "3 x 3": "9", "10 - 4": "6",
"6 + 6": "12", "8 + 1": "9", "4 x 2": "8", "15 - 5": "10", "20 / 2": "10",
"1 + 1": "2", "9 - 3": "6", "2 x 5": "10", "8 / 2": "4", "3 + 7": "10"
};
// [] calculationPairs ( )
// [🔥 ] (A : B)
// (A : B)
static const Map<String, String> wordPairs = {
"토끼": "당근", "원숭이": "바나나", "한국": "서울", "미국": "워싱턴",
"": "", "여름": "겨울", "남자": "여자", "학교": "학생",
@ -74,7 +67,7 @@ class CardFlipDifficulties {
"": "", "가을": "단풍", "숟가락": "젓가락"
};
// --- (15) ---
// (15)
static final List<CardFlipDifficulty> allDifficulties = [
// Phase 1: ( )
const CardFlipDifficulty(levelIndex: 1, name: 'Lv. 1: 입문 (이모지 12)', contextId: 'FLIP_L1_EMOJI', rows: 4, cols: 3, timeLimitSeconds: 40, contentType: CardContentType.emoji),
@ -100,6 +93,8 @@ class CardFlipDifficulties {
const CardFlipDifficulty(levelIndex: 13, name: 'Lv. 13: 갓모드 (30장)', contextId: 'FLIP_L13_6x5', rows: 6, cols: 5, timeLimitSeconds: 130, contentType: CardContentType.emoji),
const CardFlipDifficulty(levelIndex: 14, name: 'Lv. 14: 타임어택 (연산)', contextId: 'FLIP_L14_6x5_CALC', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.calculation),
const CardFlipDifficulty(levelIndex: 15, name: 'Lv. 15: 엔드게임 (연상)', contextId: 'FLIP_L15_6x5_PAIR', rows: 6, cols: 5, timeLimitSeconds: 120, contentType: CardContentType.pairWord),
const CardFlipDifficulty(levelIndex: 16, name: 'Lv. 15: 헬 (연상)', contextId: 'FLIP_L16_6x6_PAIR', rows: 6, cols: 6, timeLimitSeconds: 120, contentType: CardContentType.pairWord),
];
static CardFlipDifficulty getLevel(int levelIndex) {

View File

@ -17,7 +17,6 @@ class CardFlipGameScreen extends StatefulWidget {
class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
bool _isDialogShowing = false;
//
static const List<IconData> _iconPool = [
Icons.home, Icons.favorite, Icons.star, Icons.person, Icons.settings,
Icons.lock, Icons.map, Icons.camera_alt, Icons.phone, Icons.music_note,
@ -31,13 +30,11 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
@override
void initState() {
super.initState();
// [🔥 ]
WidgetsBinding.instance.addPostFrameCallback((_) {
_showGameGuide();
});
}
// 🔽 [🔥 ]
void _showGameGuide() {
final controller = context.read<CardFlipController>();
final difficulty = controller.difficulty;
@ -55,7 +52,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
showDialog(
context: context,
barrierDismissible: false, //
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
content: Text(message, style: const TextStyle(fontSize: 16), textAlign: TextAlign.center),
@ -63,7 +60,6 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// [🔥 ]
controller.startGameTimer();
},
child: const Text("시작하기"),
@ -74,8 +70,17 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
}
void _showGameCompletion(CardFlipController controller) async {
// [🔥 ] : secondaryScore를
// (secondary = matched * 1000 + (999 - flips))
String formatScore(int primary, int? secondary) {
return '남은 시간: ${primary}초 (시도: $secondary회)';
if (secondary == null) return '${primary}';
final int matched = secondary ~/ 1000;
final int flips = 999 - (secondary % 1000);
// ( = ( - *2) / 2)
final int mistakes = (flips - (matched * 2)) ~/ 2;
return '시간: ${primary}초 (성공:$matched / 실수:$mistakes)';
}
Future<void> saveProgress(String playerName) async {
@ -94,6 +99,10 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
}
}
}
// [🔥 ] secondaryScore에 ( )
// ( + )
final int encodedScore = (controller.matchedPairsCount * 1000) + (999 - controller.flipCount);
await Navigator.push(
context,
@ -103,7 +112,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
gameType: 'CARD_FLIP',
contextId: controller.difficulty.contextId,
primaryScore: controller.remainingTime,
secondaryScore: controller.flipCount,
secondaryScore: encodedScore, // 👈
userId: controller.userId,
userName: controller.userName,
scoreFormatter: formatScore,
@ -149,10 +158,24 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
),
body: Column(
children: [
// 🔽 [🔥 ] : /
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("뒤집은 횟수: ${controller.flipCount}", style: TextStyle(fontSize: 16, color: theme.textTheme.bodyMedium?.color)),
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"찾은 짝: ${controller.matchedPairsCount} / ${controller.totalPairsCount}",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.primaryColor)
),
Text(
"뒤집기: ${controller.flipCount}",
style: const TextStyle(fontSize: 16, color: Colors.grey)
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
@ -206,7 +229,6 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
);
}
// 🔽 [🔥 ] displayContent
Widget _buildCardContent(CardItem card) {
if (card.displayContent.startsWith("ICON_")) {
final int iconIndex = int.tryParse(card.displayContent.split('_')[1]) ?? 0;
@ -218,7 +240,7 @@ class _CardFlipGameScreenState extends State<CardFlipGameScreen> {
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
card.displayContent, // 👈 displayContent
card.displayContent,
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
),