cuqueclicker_lib/save/versions/
v5.rs1use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28
29use super::v2::{BuffV2, FingererStateV2};
30use super::v4::GameStateV4;
31use crate::bignum::Mag;
32use crate::game::state::GameState;
33use crate::game::tree::UpgradeTreeState;
34
35fn default_v5_version() -> u32 {
36 5
37}
38
39#[derive(Clone, Serialize, Deserialize)]
40pub struct GameStateV5 {
41 #[serde(default = "default_v5_version")]
42 pub version: u32,
43 #[serde(default)]
44 pub cuques: Mag,
45 #[serde(default)]
46 pub total_clicks: u64,
47 #[serde(default)]
48 pub lifetime_cuques: Mag,
49 #[serde(default)]
50 pub best_fps: Mag,
51 #[serde(default)]
52 pub golden_caught: u64,
53 #[serde(default)]
54 pub lucky_caught: u64,
55 #[serde(default)]
56 pub frenzy_caught: u64,
57 #[serde(default)]
58 pub buff_caught: u64,
59 #[serde(default)]
60 pub green_coin_caught: u64,
61 #[serde(default)]
62 pub fingerers_state: HashMap<String, FingererStateV2>,
63 #[serde(default)]
64 pub achievements_earned: HashSet<String>,
65 #[serde(default)]
66 pub prestige: u64,
67 #[serde(default)]
68 pub total_play_ticks: u64,
69 #[serde(default)]
70 pub buffs: Vec<BuffV2>,
71 #[serde(default)]
72 pub tree: UpgradeTreeState,
73}
74
75impl GameStateV5 {
76 pub fn into_current(self) -> GameState {
80 let fingerers_state = self
81 .fingerers_state
82 .into_iter()
83 .map(|(id, st)| (id, st.into()))
84 .collect();
85 let buffs = self.buffs.into_iter().map(Into::into).collect();
86 GameState {
87 version: crate::save::CURRENT_VERSION,
88 cuques: self.cuques,
89 total_clicks: self.total_clicks,
90 lifetime_cuques: self.lifetime_cuques,
91 best_fps: self.best_fps,
92 golden_caught: self.golden_caught,
93 lucky_caught: self.lucky_caught,
94 frenzy_caught: self.frenzy_caught,
95 buff_caught: self.buff_caught,
96 green_coin_caught: self.green_coin_caught,
97 fingerers_state,
98 achievements_earned: self.achievements_earned,
99 prestige: self.prestige,
100 total_play_ticks: self.total_play_ticks,
101 buffs,
102 tree: self.tree,
103 ..GameState::default()
104 }
105 }
106}
107
108impl From<GameStateV4> for GameStateV5 {
115 fn from(v4: GameStateV4) -> Self {
116 GameStateV5 {
117 version: 5,
118 cuques: Mag::from_f64(v4.cuques),
119 total_clicks: v4.total_clicks,
120 lifetime_cuques: Mag::from_f64(v4.lifetime_cuques),
121 best_fps: Mag::from_f64(v4.best_fps),
122 golden_caught: v4.golden_caught,
123 lucky_caught: v4.lucky_caught,
124 frenzy_caught: v4.frenzy_caught,
125 buff_caught: v4.buff_caught,
126 green_coin_caught: v4.green_coin_caught,
127 fingerers_state: v4.fingerers_state,
128 achievements_earned: v4.achievements_earned,
129 prestige: v4.prestige,
130 total_play_ticks: v4.total_play_ticks,
131 buffs: v4.buffs,
132 tree: v4.tree,
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::game::tree::coord::TreeCoord;
141
142 fn empty_v4() -> GameStateV4 {
143 GameStateV4 {
144 version: 4,
145 cuques: 0.0,
146 total_clicks: 0,
147 lifetime_cuques: 0.0,
148 best_fps: 0.0,
149 golden_caught: 0,
150 lucky_caught: 0,
151 frenzy_caught: 0,
152 buff_caught: 0,
153 green_coin_caught: 0,
154 fingerers_state: HashMap::new(),
155 achievements_earned: HashSet::new(),
156 prestige: 0,
157 total_play_ticks: 0,
158 buffs: vec![],
159 tree: UpgradeTreeState::default(),
160 }
161 }
162
163 #[test]
164 fn v4_to_v5_promotes_f64_counters_to_mag() {
165 let v4 = GameStateV4 {
166 cuques: 1.5e30,
167 lifetime_cuques: 1.5e30,
168 best_fps: 5_000.0,
169 ..empty_v4()
170 };
171 let v5: GameStateV5 = v4.into();
172 assert_eq!(v5.version, 5);
173 assert!((v5.cuques.to_f64() - 1.5e30).abs() / 1.5e30 < 1e-12);
175 assert!((v5.best_fps.to_f64() - 5_000.0).abs() < 1e-9);
176 }
177
178 #[test]
179 fn v4_to_v5_collapses_nan_and_inf_to_zero() {
180 let v4 = GameStateV4 {
186 cuques: f64::INFINITY,
187 lifetime_cuques: f64::NAN,
188 best_fps: f64::NEG_INFINITY,
189 ..empty_v4()
190 };
191 let v5: GameStateV5 = v4.into();
192 assert_eq!(v5.cuques, Mag::ZERO);
193 assert_eq!(v5.lifetime_cuques, Mag::ZERO);
194 assert_eq!(v5.best_fps, Mag::ZERO);
195 }
196
197 #[test]
198 fn v5_into_current_preserves_tree_state() {
199 let mut v5: GameStateV5 = empty_v4().into();
200 v5.tree.bought.insert(TreeCoord::ORIGIN);
201 v5.tree.bought.insert(TreeCoord::new(3, -2));
202 let live = v5.into_current();
203 assert!(live.tree.bought.contains(&TreeCoord::ORIGIN));
204 assert!(live.tree.bought.contains(&TreeCoord::new(3, -2)));
205 }
206
207 #[test]
208 fn v5_serialize_huge_value_round_trips() {
209 let mut v5: GameStateV5 = empty_v4().into();
216 v5.cuques = Mag { log10: 600.0 };
217 v5.lifetime_cuques = Mag { log10: 1200.0 };
218 let json = serde_json::to_string(&v5).expect("serialize");
219 let parsed: GameStateV5 = serde_json::from_str(&json).expect("deserialize");
220 assert!((parsed.cuques.log10 - 600.0).abs() < 1e-9);
221 assert!((parsed.lifetime_cuques.log10 - 1200.0).abs() < 1e-9);
222 }
223
224 #[test]
225 fn v5_serialize_small_value_emits_plain_number() {
226 let mut v5: GameStateV5 = empty_v4().into();
231 v5.cuques = Mag::from_f64(12345.6);
232 let json = serde_json::to_string(&v5.cuques).expect("serialize");
233 assert!(
234 !json.contains("log10"),
235 "small Mag should serialize as a bare number, got {json}"
236 );
237 }
238}