Skip to main content

game_manager_lib/services/recommendation/
core.rs

1//! Estruturas e tipos fundamentais do sistema de recomendação
2//!
3//! Este módulo define as estruturas de dados e configurações centrais
4//! usadas em todo o sistema de recomendação.
5
6use 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// === ESTRUTURAS ===
18
19#[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,   // Max 2 em 10
57    Aggressive, // Max 1 em 10
58}
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
91// Serialização customizada para tags
92fn 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
105// Deserialização customizada para tags
106fn 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
126// === UTILITÁRIOS ===
127
128/// Parseia o ano de lançamento de uma string de data
129pub fn parse_release_year(date_str: &str) -> Option<i32> {
130    date_str.split('-').next()?.parse().ok()
131}
132
133/// Calcula o peso de um jogo baseado no tempo jogado, favoritos e avaliação do usuário.
134pub 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}