189 lines
6.4 KiB
Dart
189 lines
6.4 KiB
Dart
import '../models/sake_item.dart';
|
|
import '../models/mbti_result.dart';
|
|
import 'mbti_types.dart';
|
|
|
|
class MBTIDiagnosisService {
|
|
MBTIResult diagnose(List<SakeItem> items) {
|
|
if (items.isEmpty) {
|
|
return MBTIResult.insufficient(0);
|
|
}
|
|
|
|
// 1. Calculate Scores for each Axis (0.0 to 1.0)
|
|
// > 0.5 leans towards the Left (E, S, T, J)
|
|
// <= 0.5 leans towards the Right (I, N, F, P)
|
|
// We can adjust thresholds if needed.
|
|
|
|
final double eScore = _calculateEScore(items); // E vs I (Social)
|
|
final double sScore = _calculateSScore(items); // S vs N (Recognition)
|
|
final double tScore = _calculateTScore(items); // T vs F (Judgment)
|
|
final double jScore = _calculateJScore(items); // J vs P (Tactics)
|
|
|
|
// 2. Determine Axis Values
|
|
final bool isE = eScore >= 0.5;
|
|
final bool isS = sScore >= 0.5;
|
|
final bool isT = tScore >= 0.5;
|
|
final bool isJ = jScore >= 0.5;
|
|
|
|
// 3. Construct 4-letter code
|
|
final String code = [
|
|
isE ? 'E' : 'I',
|
|
isS ? 'S' : 'N',
|
|
isT ? 'T' : 'F',
|
|
isJ ? 'J' : 'P',
|
|
].join();
|
|
|
|
// 4. Retrieve Type
|
|
final type = MBTIType.types[code] ?? MBTIType.unknown();
|
|
|
|
// 5. Calculate Confidence (Simple Average of "distance from 0.5")
|
|
// e.g. Score 0.9 -> distance 0.4. Score 0.51 -> distance 0.01.
|
|
// Max distance is 0.5. So (dist / 0.5) * 100 is % confidence for that axis.
|
|
final double avgDist = (
|
|
((eScore - 0.5).abs()) +
|
|
((sScore - 0.5).abs()) +
|
|
((tScore - 0.5).abs()) +
|
|
((jScore - 0.5).abs())
|
|
) / 4.0;
|
|
|
|
// Normalize: Max possible sum of diffs is 0.5 * 4 = 2.0.
|
|
// So avgDist max is 0.5.
|
|
// Confidence = avgDist * 2. (0.5 * 2 = 1.0)
|
|
final double confidence = avgDist * 2.0;
|
|
|
|
return MBTIResult(
|
|
type: type,
|
|
sampleSize: items.length,
|
|
confidence: confidence,
|
|
axisScores: {
|
|
'E/I': isE,
|
|
'S/N': isS,
|
|
'T/F': isT,
|
|
'J/P': isJ,
|
|
},
|
|
);
|
|
}
|
|
|
|
// --- Axis Calculation Logic ---
|
|
|
|
/// E (Extrovert) vs I (Introvert)
|
|
/// Logic: Ratio of 'Set/Menu' items to total items.
|
|
/// If user drinks "Sets" (Drinking comparison), they are likely 'Social/Party' (E).
|
|
/// If user drinks "Single" items mainly, they are 'Solitary' (I).
|
|
/// Threshold: Sets are rarer, so if > 10% are sets, lean E.
|
|
double _calculateEScore(List<SakeItem> items) {
|
|
if (items.isEmpty) return 0.5; // Phase B fix: neutral default
|
|
|
|
int setOrderCount = items.where((item) => item.itemType == ItemType.set).length;
|
|
double ratio = setOrderCount / items.length;
|
|
|
|
// Phase B fix: Neutral baseline (0.5) instead of I-biased (0.3)
|
|
// Map 0.0 -> 0.5 (Neutral)
|
|
// Map 0.1 -> 0.6 (Slight E)
|
|
// Map 0.2 -> 0.7 (Strong E)
|
|
|
|
// Formula: score = 0.5 + (ratio * 1.0)
|
|
double score = 0.5 + ratio;
|
|
return score.clamp(0.0, 1.0);
|
|
}
|
|
|
|
/// S (Sensing) vs N (Intuition)
|
|
/// Logic: "Specs" vs "Vibes".
|
|
/// S: Detailed numeric specs (SMV, Polishing, Alcohol, Rice, Yeast).
|
|
/// N: Photos, Emotional Tags, Empty specs.
|
|
double _calculateSScore(List<SakeItem> items) {
|
|
if (items.isEmpty) return 0.5;
|
|
|
|
// Logic combined into the loop below.
|
|
|
|
// Average fill rate of technical specs.
|
|
// If user fills 3/5 avg, they are S.
|
|
// If user fills 0/5 avg, they are N.
|
|
double debugTotalSpecFills = 0;
|
|
int maxPossibleSpecs = items.length * 5;
|
|
|
|
for (var item in items) {
|
|
final h = item.hiddenSpecs;
|
|
if (h.sakeMeterValue != null) debugTotalSpecFills++;
|
|
if (h.polishingRatio != null) debugTotalSpecFills++;
|
|
if (h.alcoholContent != null) debugTotalSpecFills++;
|
|
if (h.yeast != null && h.yeast!.isNotEmpty) debugTotalSpecFills++;
|
|
if (h.riceVariety != null && h.riceVariety!.isNotEmpty) debugTotalSpecFills++;
|
|
}
|
|
|
|
double fillRatio = maxPossibleSpecs == 0 ? 0 : debugTotalSpecFills / maxPossibleSpecs;
|
|
|
|
// Phase B fix: Neutral baseline (0.5) instead of N-biased (0.2)
|
|
// Map 0.0 -> 0.5 (Neutral)
|
|
// Map 0.3 -> 0.65 (Slight S)
|
|
// Map 0.6 -> 0.8 (Strong S)
|
|
// Map 1.0 -> 1.0 (Max S)
|
|
|
|
// Formula: score = 0.5 + (fillRatio * 0.5)
|
|
double score = 0.5 + (fillRatio * 0.5);
|
|
return score.clamp(0.0, 1.0);
|
|
}
|
|
|
|
/// T (Thinking) vs F (Feeling)
|
|
/// Logic: "Logic" vs "Emotion".
|
|
/// T: Strict rating, more critical? Or just low favorites?
|
|
/// F: High favorite ratio. "Love everything".
|
|
double _calculateTScore(List<SakeItem> items) {
|
|
if (items.isEmpty) return 0.5;
|
|
|
|
int favoriteCount = items.where((i) => i.userData.isFavorite).length;
|
|
double favoriteRatio = favoriteCount / items.length;
|
|
|
|
// Phase B fix: Neutral baseline (0.5) instead of extreme T-bias (1.0)
|
|
// Map 0.0 -> 0.5 (Neutral)
|
|
// Map 0.5 -> 0.25 (Slight F)
|
|
// Map 1.0 -> 0.0 (Strong F)
|
|
|
|
// High Favorite -> F (Low T score)
|
|
// Low Favorite -> Neutral (T score 0.5)
|
|
|
|
// Formula: score = 0.5 - (favoriteRatio * 0.5)
|
|
return 0.5 - (favoriteRatio * 0.5);
|
|
}
|
|
|
|
/// J (Judging) vs P (Prospecting)
|
|
/// Logic: "Planned/Completeness" vs "Random".
|
|
/// J: High data completeness (user input fields), Organized.
|
|
/// P: Low completeness (just photo and name), Spontaneous.
|
|
/// Using "Input Completeness" as a proxy for J.
|
|
/// Similar to S/N but includes subjective fields like Memo/Price/Location?
|
|
/// Or "Prefecture Coverage"?
|
|
/// Let's use "Input Completeness of REQUIRED BASIC fields" (Name, Brand, Prefecture)
|
|
/// Almost everyone fills Name/Brand.
|
|
/// Let's use "Has Memo" and "Has Price" and "Has Date".
|
|
/// J types likely fill Price and Memo.
|
|
double _calculateJScore(List<SakeItem> items) {
|
|
if (items.isEmpty) return 0.5;
|
|
|
|
int detailedItemCount = 0;
|
|
|
|
for (var item in items) {
|
|
bool hasMemo = item.userData.memo != null && item.userData.memo!.isNotEmpty;
|
|
bool hasPrice = item.userData.price != null || item.userData.costPrice != null;
|
|
// bool hasPrefecture = item.displayData.displayPrefecture != 'Unknown'; // Default is often unknown
|
|
|
|
if (hasMemo && hasPrice) {
|
|
detailedItemCount++;
|
|
}
|
|
}
|
|
|
|
double detailedRatio = detailedItemCount / items.length;
|
|
|
|
// Phase B fix: Neutral baseline (0.5) instead of P-biased (0.2)
|
|
// Map 0.0 -> 0.5 (Neutral)
|
|
// Map 0.5 -> 0.75 (Slight J)
|
|
// Map 1.0 -> 1.0 (Strong J)
|
|
|
|
// If user inputs price AND memo for > 50% items -> J.
|
|
// Else -> Neutral.
|
|
|
|
// Formula: score = 0.5 + (detailedRatio * 0.5)
|
|
double score = 0.5 + (detailedRatio * 0.5);
|
|
return score.clamp(0.0, 1.0);
|
|
}
|
|
}
|