game_manager_lib\services\recommendation/
scoring.rs1use super::core::*;
7use crate::utils::tag_utils::{combined_multiplier, TagKey, TagRole};
8use chrono::Datelike;
9
10#[derive(Debug, Clone)]
13pub struct DetailedScoreComponents {
14 pub affinity_score: f32,
15 pub context_score: f32,
16 pub diversity_score: f32,
17 pub genre_score: f32,
18 pub tag_score: f32,
19 pub series_score: f32,
20 pub age_penalty: f32,
21 pub top_genres: Vec<(String, f32)>,
22 pub top_affinity_tags: Vec<(String, f32)>,
23 pub top_context_tags: Vec<(String, f32)>,
24}
25
26pub fn score_game_cb(
30 profile: &UserPreferenceVector,
31 game: &GameWithDetails,
32 config: &RecommendationConfig,
33) -> (f32, Option<RecommendationReason>) {
34 let (total_cb, reason, _components) = score_game_cb_detailed(profile, game, config);
35
36 (total_cb, reason)
37}
38
39pub fn score_game_cb_detailed(
41 profile: &UserPreferenceVector,
42 game: &GameWithDetails,
43 config: &RecommendationConfig,
44) -> (f32, Option<RecommendationReason>, DetailedScoreComponents) {
45 let mut affinity_score = 0.0;
46 let mut context_score = 0.0;
47 let mut diversity_score = 0.0;
48
49 let mut genre_score = 0.0;
50 let mut tag_score = 0.0;
51 let mut series_score = 0.0;
52
53 let mut genre_contributions = Vec::new();
54 let mut affinity_tag_contributions = Vec::new();
55 let mut context_tag_contributions = Vec::new();
56
57 let mut best_reason: Option<RecommendationReason> = None;
58 let mut max_affinity_contribution = 0.0;
59
60 process_genres(
62 &game.genres,
63 &profile.genres,
64 &mut affinity_score,
65 &mut genre_score,
66 &mut genre_contributions,
67 &mut best_reason,
68 &mut max_affinity_contribution,
69 );
70
71 process_tags(
73 &game.tags,
74 &profile.tags,
75 &mut affinity_score,
76 &mut context_score,
77 &mut diversity_score,
78 &mut tag_score,
79 &mut affinity_tag_contributions,
80 &mut context_tag_contributions,
81 &mut best_reason,
82 &mut max_affinity_contribution,
83 );
84
85 if config.favor_series {
87 process_series(
88 &game.series,
89 &profile.series,
90 &mut affinity_score,
91 &mut series_score,
92 );
93 }
94
95 let age_penalty = apply_age_penalty(
97 game.release_year,
98 config.age_decay,
99 &mut affinity_score,
100 &mut context_score,
101 );
102
103 let total_cb = affinity_score + context_score + diversity_score;
104
105 genre_contributions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
107 affinity_tag_contributions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
108 context_tag_contributions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
109
110 let components = DetailedScoreComponents {
111 affinity_score,
112 context_score,
113 diversity_score,
114 genre_score,
115 tag_score,
116 series_score,
117 age_penalty,
118 top_genres: genre_contributions.into_iter().take(5).collect(),
119 top_affinity_tags: affinity_tag_contributions.into_iter().take(10).collect(),
120 top_context_tags: context_tag_contributions.into_iter().take(5).collect(),
121 };
122
123 (total_cb, best_reason, components)
124}
125
126pub fn normalize_score(score: f32, max: f32) -> f32 {
128 if max > 0.0 {
129 score / max
130 } else {
131 0.0
132 }
133}
134
135fn process_genres(
138 game_genres: &[String],
139 profile_genres: &std::collections::HashMap<String, f32>,
140 affinity_score: &mut f32,
141 genre_score: &mut f32,
142 genre_contributions: &mut Vec<(String, f32)>,
143 best_reason: &mut Option<RecommendationReason>,
144 max_affinity_contribution: &mut f32,
145) {
146 for genre in game_genres {
147 if let Some(&val) = profile_genres.get(genre) {
148 let contribution = val * WEIGHT_GENRE;
149 *affinity_score += contribution;
150 *genre_score += contribution;
151 genre_contributions.push((genre.clone(), contribution));
152
153 if contribution > *max_affinity_contribution {
154 *max_affinity_contribution = contribution;
155 *best_reason = Some(RecommendationReason {
156 label: format!("Gênero: {}", genre),
157 type_id: "genre".to_string(),
158 });
159 }
160 }
161 }
162}
163
164fn process_tags(
165 game_tags: &[crate::models::GameTag],
166 profile_tags: &std::collections::HashMap<TagKey, f32>,
167 affinity_score: &mut f32,
168 context_score: &mut f32,
169 diversity_score: &mut f32,
170 tag_score: &mut f32,
171 affinity_tag_contributions: &mut Vec<(String, f32)>,
172 context_tag_contributions: &mut Vec<(String, f32)>,
173 best_reason: &mut Option<RecommendationReason>,
174 max_affinity_contribution: &mut f32,
175) {
176 for tag in game_tags {
177 let key = TagKey::new(tag.category.clone(), tag.slug.clone());
178
179 if let Some(&pref_val) = profile_tags.get(&key) {
180 let multiplier = combined_multiplier(&tag.category, &tag.role);
181 let base_contribution = pref_val * multiplier * WEIGHT_PLAYTIME_HOUR;
182 let contribution = base_contribution.min(MAX_TAG_CONTRIBUTION);
183
184 match tag.role {
185 TagRole::Affinity => {
186 *affinity_score += contribution;
187 *tag_score += contribution;
188 affinity_tag_contributions.push((tag.name.clone(), contribution));
189
190 if contribution > *max_affinity_contribution {
191 *max_affinity_contribution = contribution;
192 *best_reason = Some(RecommendationReason {
193 label: format!("Tag: {}", tag.name),
194 type_id: "tag".to_string(),
195 });
196 }
197 }
198 TagRole::Context => {
199 *context_score += contribution;
200 *tag_score += contribution;
201 context_tag_contributions.push((tag.name.clone(), contribution));
202 }
203 TagRole::Diversity => {
204 *diversity_score += contribution;
205 *tag_score += contribution;
206 }
207 TagRole::Filter => {}
208 }
209 }
210 }
211}
212
213fn process_series(
214 game_series: &Option<String>,
215 profile_series: &std::collections::HashMap<String, f32>,
216 affinity_score: &mut f32,
217 series_score: &mut f32,
218) {
219 if let Some(series_name) = game_series {
220 if let Some(&val) = profile_series.get(series_name) {
221 let series_contribution = val.sqrt();
222 *affinity_score += series_contribution;
223 *series_score = series_contribution;
224 }
225 }
226}
227
228fn apply_age_penalty(
229 release_year: Option<i32>,
230 age_decay: f32,
231 affinity_score: &mut f32,
232 context_score: &mut f32,
233) -> f32 {
234 let mut age_penalty = 1.0;
235
236 if let Some(release_year) = release_year {
237 let current_year = chrono::Local::now().year();
238 let age = (current_year - release_year).clamp(0, 15);
239 if age > 0 {
240 age_penalty = age_decay.powi(age);
241 *affinity_score *= age_penalty;
242 *context_score *= age_penalty;
243 }
244 }
245
246 age_penalty
247}