use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use super::v2::{BuffV2, FingererStateV2};
use super::v4::GameStateV4;
use crate::bignum::Mag;
use crate::game::state::GameState;
use crate::game::tree::UpgradeTreeState;
fn default_v5_version() -> u32 {
5
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GameStateV5 {
#[serde(default = "default_v5_version")]
pub version: u32,
#[serde(default)]
pub cuques: Mag,
#[serde(default)]
pub total_clicks: u64,
#[serde(default)]
pub lifetime_cuques: Mag,
#[serde(default)]
pub best_fps: Mag,
#[serde(default)]
pub golden_caught: u64,
#[serde(default)]
pub lucky_caught: u64,
#[serde(default)]
pub frenzy_caught: u64,
#[serde(default)]
pub buff_caught: u64,
#[serde(default)]
pub green_coin_caught: u64,
#[serde(default)]
pub fingerers_state: HashMap<String, FingererStateV2>,
#[serde(default)]
pub achievements_earned: HashSet<String>,
#[serde(default)]
pub prestige: u64,
#[serde(default)]
pub total_play_ticks: u64,
#[serde(default)]
pub buffs: Vec<BuffV2>,
#[serde(default)]
pub tree: UpgradeTreeState,
}
impl GameStateV5 {
pub fn into_current(self) -> GameState {
let fingerers_state = self
.fingerers_state
.into_iter()
.map(|(id, st)| (id, st.into()))
.collect();
let buffs = self.buffs.into_iter().map(Into::into).collect();
GameState {
version: crate::save::CURRENT_VERSION,
cuques: self.cuques,
total_clicks: self.total_clicks,
lifetime_cuques: self.lifetime_cuques,
best_fps: self.best_fps,
golden_caught: self.golden_caught,
lucky_caught: self.lucky_caught,
frenzy_caught: self.frenzy_caught,
buff_caught: self.buff_caught,
green_coin_caught: self.green_coin_caught,
fingerers_state,
achievements_earned: self.achievements_earned,
prestige: self.prestige,
total_play_ticks: self.total_play_ticks,
buffs,
tree: self.tree,
..GameState::default()
}
}
}
impl From<GameStateV4> for GameStateV5 {
fn from(v4: GameStateV4) -> Self {
GameStateV5 {
version: 5,
cuques: Mag::from_f64(v4.cuques),
total_clicks: v4.total_clicks,
lifetime_cuques: Mag::from_f64(v4.lifetime_cuques),
best_fps: Mag::from_f64(v4.best_fps),
golden_caught: v4.golden_caught,
lucky_caught: v4.lucky_caught,
frenzy_caught: v4.frenzy_caught,
buff_caught: v4.buff_caught,
green_coin_caught: v4.green_coin_caught,
fingerers_state: v4.fingerers_state,
achievements_earned: v4.achievements_earned,
prestige: v4.prestige,
total_play_ticks: v4.total_play_ticks,
buffs: v4.buffs,
tree: v4.tree,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::tree::coord::TreeCoord;
fn empty_v4() -> GameStateV4 {
GameStateV4 {
version: 4,
cuques: 0.0,
total_clicks: 0,
lifetime_cuques: 0.0,
best_fps: 0.0,
golden_caught: 0,
lucky_caught: 0,
frenzy_caught: 0,
buff_caught: 0,
green_coin_caught: 0,
fingerers_state: HashMap::new(),
achievements_earned: HashSet::new(),
prestige: 0,
total_play_ticks: 0,
buffs: vec![],
tree: UpgradeTreeState::default(),
}
}
#[test]
fn v4_to_v5_promotes_f64_counters_to_mag() {
let v4 = GameStateV4 {
cuques: 1.5e30,
lifetime_cuques: 1.5e30,
best_fps: 5_000.0,
..empty_v4()
};
let v5: GameStateV5 = v4.into();
assert_eq!(v5.version, 5);
assert!((v5.cuques.to_f64() - 1.5e30).abs() / 1.5e30 < 1e-12);
assert!((v5.best_fps.to_f64() - 5_000.0).abs() < 1e-9);
}
#[test]
fn v4_to_v5_collapses_nan_and_inf_to_zero() {
let v4 = GameStateV4 {
cuques: f64::INFINITY,
lifetime_cuques: f64::NAN,
best_fps: f64::NEG_INFINITY,
..empty_v4()
};
let v5: GameStateV5 = v4.into();
assert_eq!(v5.cuques, Mag::ZERO);
assert_eq!(v5.lifetime_cuques, Mag::ZERO);
assert_eq!(v5.best_fps, Mag::ZERO);
}
#[test]
fn v5_into_current_preserves_tree_state() {
let mut v5: GameStateV5 = empty_v4().into();
v5.tree.bought.insert(TreeCoord::ORIGIN);
v5.tree.bought.insert(TreeCoord::new(3, -2));
let live = v5.into_current();
assert!(live.tree.bought.contains(&TreeCoord::ORIGIN));
assert!(live.tree.bought.contains(&TreeCoord::new(3, -2)));
}
#[test]
fn v5_serialize_huge_value_round_trips() {
let mut v5: GameStateV5 = empty_v4().into();
v5.cuques = Mag { log10: 600.0 };
v5.lifetime_cuques = Mag { log10: 1200.0 };
let json = serde_json::to_string(&v5).expect("serialize");
let parsed: GameStateV5 = serde_json::from_str(&json).expect("deserialize");
assert!((parsed.cuques.log10 - 600.0).abs() < 1e-9);
assert!((parsed.lifetime_cuques.log10 - 1200.0).abs() < 1e-9);
}
#[test]
fn v5_serialize_small_value_emits_plain_number() {
let mut v5: GameStateV5 = empty_v4().into();
v5.cuques = Mag::from_f64(12345.6);
let json = serde_json::to_string(&v5.cuques).expect("serialize");
assert!(
!json.contains("log10"),
"small Mag should serialize as a bare number, got {json}"
);
}
}