game_manager_lib/services/recommendation/
core.rs1use crate::constants::{
7 MINUTES_PER_HOUR, RECOMMENDATION_DEFAULT_AGE_DECAY,
8 RECOMMENDATION_DEFAULT_COLLABORATIVE_WEIGHT, RECOMMENDATION_DEFAULT_CONTENT_WEIGHT,
9 RECOMMENDATION_MAX_PLAYTIME_HOURS, RECOMMENDATION_WEIGHT_FAVORITE,
10 RECOMMENDATION_WEIGHT_PLAYTIME_HOUR, RECOMMENDATION_WEIGHT_USER_RATING,
11};
12use crate::models::{Game, GameTag};
13use crate::utils::tag_utils::TagKey;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17#[derive(Debug, Deserialize, Serialize, Clone)]
20pub struct RecommendationConfig {
21 pub content_weight: f32,
22 pub collaborative_weight: f32,
23 pub age_decay: f32,
24 pub favor_series: bool,
25}
26
27impl Default for RecommendationConfig {
28 fn default() -> Self {
29 Self {
30 content_weight: RECOMMENDATION_DEFAULT_CONTENT_WEIGHT,
31 collaborative_weight: RECOMMENDATION_DEFAULT_COLLABORATIVE_WEIGHT,
32 age_decay: RECOMMENDATION_DEFAULT_AGE_DECAY,
33 favor_series: true,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct UserSettings {
40 pub filter_adult_content: bool,
41 pub series_limit: SeriesLimit,
42}
43
44impl Default for UserSettings {
45 fn default() -> Self {
46 Self {
47 filter_adult_content: false,
48 series_limit: SeriesLimit::Moderate,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy)]
54pub enum SeriesLimit {
55 None,
56 Moderate, Aggressive, }
59
60#[derive(Debug, Serialize, Clone)]
61pub struct RecommendationReason {
62 pub label: String,
63 pub type_id: String,
64}
65
66#[derive(Debug, Clone)]
67pub struct GameWithDetails {
68 pub game: Game,
69 pub genres: Vec<String>,
70 pub tags: Vec<GameTag>,
71 pub series: Option<String>,
72 pub release_year: Option<i32>,
73 pub steam_app_id: Option<u32>,
74}
75
76#[derive(Debug, Serialize, Deserialize, Clone)]
77pub struct UserPreferenceVector {
78 pub genres: HashMap<String, f32>,
79 #[serde(
80 serialize_with = "serialize_tags",
81 deserialize_with = "deserialize_tags"
82 )]
83 pub tags: HashMap<TagKey, f32>,
84 pub series: HashMap<String, f32>,
85 #[serde(rename = "totalPlaytime")]
86 pub total_playtime: i32,
87 #[serde(rename = "totalGames")]
88 pub total_games: i32,
89}
90
91fn serialize_tags<S>(tags: &HashMap<TagKey, f32>, serializer: S) -> Result<S::Ok, S::Error>
93where
94 S: serde::Serializer,
95{
96 use serde::ser::SerializeMap;
97 let mut map = serializer.serialize_map(Some(tags.len()))?;
98 for (key, value) in tags {
99 let key_string = format!("{:?}:{}", key.category, key.slug);
100 map.serialize_entry(&key_string, value)?;
101 }
102 map.end()
103}
104
105fn deserialize_tags<'de, D>(deserializer: D) -> Result<HashMap<TagKey, f32>, D::Error>
107where
108 D: serde::Deserializer<'de>,
109{
110 let map: HashMap<String, f32> = HashMap::deserialize(deserializer)?;
111 let mut result = HashMap::new();
112
113 for (key_string, value) in map {
114 let parts: Vec<&str> = key_string.split(':').collect();
115 if parts.len() == 2 {
116 if let Ok(category) = serde_json::from_str(&format!("\"{}\"", parts[0])) {
117 let key = TagKey::new(category, parts[1].to_string());
118 result.insert(key, value);
119 }
120 }
121 }
122
123 Ok(result)
124}
125
126pub fn parse_release_year(date_str: &str) -> Option<i32> {
130 date_str.split('-').next()?.parse().ok()
131}
132
133pub fn calculate_game_weight(game: &Game) -> f32 {
135 let playtime_hours =
136 (game.playtime.unwrap_or(0) / MINUTES_PER_HOUR).min(RECOMMENDATION_MAX_PLAYTIME_HOURS);
137 let mut weight = 1.0 + (playtime_hours as f32 * RECOMMENDATION_WEIGHT_PLAYTIME_HOUR);
138
139 if game.favorite {
140 weight += RECOMMENDATION_WEIGHT_FAVORITE;
141 }
142
143 if let Some(rating) = game.user_rating {
144 weight += (rating as f32) * RECOMMENDATION_WEIGHT_USER_RATING;
145 }
146
147 weight
148}