ponshu-room-lite/lib/services/mbti_compatibility_service....

248 lines
8.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import '../models/sake_item.dart';
import 'mbti_types.dart';
/// MBTIタイプと日本酒の相性を計算するサービス
///
/// ユーザーの公式MBTIタイプと、日本酒のタグ・特定条件・五味チャートを照合して
/// 相性スコア0.0〜1.0)と理由を返す
class MBTICompatibilityService {
/// 16タイプ別の五味チャート好み1-5スケールを公開
/// { aroma, sweetness, acidity, bitterness, body }
static Map<String, Map<String, double>> get flavorPreferences => _flavorPreferences;
/// 16タイプ別の五味チャート好み1-5スケール
/// { aroma, sweetness, acidity, bitterness, body }
static const Map<String, Map<String, double>> _flavorPreferences = {
// 分析家グループNT型- 論理的、複雑な味わい好み
'INTJ': {'aroma': 4.5, 'sweetness': 2.0, 'acidity': 3.5, 'bitterness': 4.0, 'body': 4.0},
'INTP': {'aroma': 4.0, 'sweetness': 2.5, 'acidity': 4.0, 'bitterness': 3.5, 'body': 3.0},
'ENTJ': {'aroma': 4.5, 'sweetness': 2.0, 'acidity': 3.0, 'bitterness': 4.5, 'body': 4.5},
'ENTP': {'aroma': 4.0, 'sweetness': 3.5, 'acidity': 4.5, 'bitterness': 3.0, 'body': 3.5},
// 外交官グループNF型- 感性的、華やかで甘め好み
'INFJ': {'aroma': 4.5, 'sweetness': 3.5, 'acidity': 3.0, 'bitterness': 2.5, 'body': 4.0},
'INFP': {'aroma': 5.0, 'sweetness': 4.0, 'acidity': 2.5, 'bitterness': 2.0, 'body': 3.0},
'ENFJ': {'aroma': 4.5, 'sweetness': 3.5, 'acidity': 3.0, 'bitterness': 2.5, 'body': 3.5},
'ENFP': {'aroma': 5.0, 'sweetness': 4.5, 'acidity': 3.5, 'bitterness': 2.0, 'body': 3.0},
// 番人グループSJ型- 伝統的、バランス重視
'ISTJ': {'aroma': 2.5, 'sweetness': 2.5, 'acidity': 3.0, 'bitterness': 4.0, 'body': 4.0},
'ISFJ': {'aroma': 3.0, 'sweetness': 4.0, 'acidity': 2.5, 'bitterness': 2.5, 'body': 3.5},
'ESTJ': {'aroma': 2.5, 'sweetness': 2.0, 'acidity': 3.5, 'bitterness': 4.5, 'body': 4.0},
'ESFJ': {'aroma': 3.5, 'sweetness': 3.5, 'acidity': 3.0, 'bitterness': 3.0, 'body': 3.5},
// 探検家グループSP型- 冒険的、インパクト重視
'ISTP': {'aroma': 3.0, 'sweetness': 2.0, 'acidity': 3.5, 'bitterness': 4.0, 'body': 5.0},
'ISFP': {'aroma': 4.5, 'sweetness': 4.0, 'acidity': 3.5, 'bitterness': 2.0, 'body': 3.0},
'ESTP': {'aroma': 3.0, 'sweetness': 2.0, 'acidity': 4.0, 'bitterness': 4.5, 'body': 5.0},
'ESFP': {'aroma': 5.0, 'sweetness': 4.5, 'acidity': 4.0, 'bitterness': 2.0, 'body': 2.5},
};
/// 16タイプ別の酒種好み複数条件
static const Map<String, List<String>> _sakeTypePreferences = {
'INTJ': ['大吟醸', '純米大吟醸', '斗瓶'],
'INTP': ['実験', '低アルコール', '酵母'],
'ENTJ': ['大吟醸', '純米大吟醸', '金賞', 'プレミアム'],
'ENTP': ['貴醸酒', '古酒', '熟成'],
'INFJ': ['古酒', '熟成', '秘蔵'],
'INFP': ['', '', '', '', '限定', '', ''],
'ENFJ': ['純米吟醸', '吟醸'],
'ENFP': ['限定', 'コラボ', '可愛い'],
'ISTJ': ['生酛', '山廃', '純米', '本醸造'],
'ISFJ': ['特別純米', '純米'],
'ESTJ': ['辛口', '本醸造', '淡麗'],
'ESFJ': ['純米', '食中酒', '地酒'],
'ISTP': ['無濾過', '生原酒', '原酒'],
'ISFP': ['スパークリング', '微発泡', 'にごり'],
'ESTP': ['原酒', '高アルコール', '超辛口'],
'ESFP': ['スパークリング', 'にごり', 'ピンク'],
};
/// 相性スコアを計算0.0〜1.0
static CompatibilityResult calculateCompatibility(String? mbtiType, SakeItem sake) {
if (mbtiType == null || mbtiType.isEmpty) {
return CompatibilityResult.noMbti();
}
final typeProfile = MBTIType.types[mbtiType];
if (typeProfile == null) {
return CompatibilityResult.noMbti();
}
double score = 0.0;
final reasons = <String>[];
double maxPossibleScore = 0.0;
// 1. 五味チャートマッチング最大40点
final flavorPref = _flavorPreferences[mbtiType];
final tasteStats = sake.hiddenSpecs.tasteStats;
if (flavorPref != null && tasteStats.isNotEmpty) {
maxPossibleScore += 40;
double flavorScore = 0;
int matchedAxes = 0;
for (final axis in ['aroma', 'sweetness', 'acidity', 'bitterness', 'body']) {
final pref = flavorPref[axis] ?? 3.0;
final actual = (tasteStats[axis] ?? 3).toDouble();
final diff = (pref - actual).abs();
// 差が1以下なら高スコア、2以下なら中スコア
if (diff <= 1.0) {
flavorScore += 8; // 8点/軸
matchedAxes++;
} else if (diff <= 2.0) {
flavorScore += 4; // 4点/軸
}
}
score += flavorScore;
if (matchedAxes >= 3) {
reasons.add('五味チャートが好みに合致');
} else if (matchedAxes >= 2) {
reasons.add('味わいのバランスが良好');
}
} else {
// 五味データがない場合はニュートラル加点
maxPossibleScore += 40;
score += 20;
}
// 2. タグマッチング最大30点
final sakeTags = sake.hiddenSpecs.flavorTags.map((t) => t.toLowerCase()).toList();
final favoriteTags = typeProfile.favorableTags.map((t) => t.toLowerCase()).toList();
maxPossibleScore += 30;
int tagMatchCount = 0;
final matchedTags = <String>[];
for (final tag in sakeTags) {
for (final fav in favoriteTags) {
if (tag.contains(fav) || fav.contains(tag)) {
tagMatchCount++;
matchedTags.add(fav);
break;
}
}
}
// 最大3タグまでカウント各10点
score += (tagMatchCount.clamp(0, 3) * 10);
if (matchedTags.isNotEmpty) {
reasons.add('${matchedTags.first}」にマッチ');
}
// 3. 酒種マッチング最大20点
final sakeType = sake.hiddenSpecs.type?.toLowerCase() ?? '';
final sakeName = sake.displayData.displayName.toLowerCase();
final typePref = _sakeTypePreferences[mbtiType] ?? [];
maxPossibleScore += 20;
bool typeMatched = false;
for (final pref in typePref) {
if (sakeType.contains(pref.toLowerCase()) || sakeName.contains(pref.toLowerCase())) {
score += 20;
typeMatched = true;
reasons.add('おすすめ酒種にマッチ');
break;
}
}
// 酒種マッチがなくてもニュートラル加点
if (!typeMatched) {
score += 10;
}
// 4. 評価補正最大10点
maxPossibleScore += 10;
final rating = sake.displayData.rating ?? 3.0;
if (rating >= 4.0) {
score += 10;
if (reasons.length < 3) reasons.add('高評価銘柄');
} else if (rating >= 3.0) {
score += 5;
}
// 最終スコア計算0.0〜1.0に正規化)
final normalizedScore = maxPossibleScore > 0
? (score / maxPossibleScore).clamp(0.3, 1.0)
: 0.5;
// 理由がない場合のデフォルト
if (reasons.isEmpty) {
reasons.add('バランスの良い一本');
}
return CompatibilityResult(
score: normalizedScore,
mbtiType: mbtiType,
reasons: reasons,
);
}
/// 相性パーセンテージを取得(表示用)
static int getCompatibilityPercent(String? mbtiType, SakeItem sake) {
final result = calculateCompatibility(mbtiType, sake);
return (result.score * 100).round();
}
}
/// 相性計算結果
class CompatibilityResult {
final double score;
final String? mbtiType;
final List<String> reasons;
final bool hasResult;
CompatibilityResult({
required this.score,
required this.mbtiType,
required this.reasons,
this.hasResult = true,
});
factory CompatibilityResult.noMbti() {
return CompatibilityResult(
score: 0.0,
mbtiType: null,
reasons: [],
hasResult: false,
);
}
/// パーセンテージ表示
int get percent => (score * 100).round();
/// 星評価0〜5
int get starRating {
if (score >= 0.9) return 5;
if (score >= 0.75) return 4;
if (score >= 0.6) return 3;
if (score >= 0.45) return 2;
return 1;
}
/// 星評価文字列
String get starDisplay {
const filledStar = '';
const emptyStar = '';
return filledStar * starRating + emptyStar * (5 - starRating);
}
/// 相性レベル
String get level {
if (score >= 0.85) return '最高';
if (score >= 0.70) return '良好';
if (score >= 0.55) return '普通';
if (score >= 0.40) return 'まあまあ';
return 'お試し';
}
/// 主な理由1つのみ
String get primaryReason => reasons.isNotEmpty ? reasons.first : '';
}