mod achievements;
mod progress;
pub use achievements::{
achievements_by_category, achievements_by_tier, get_achievement, Achievement,
AchievementCategory, AchievementTier, ACHIEVEMENTS,
};
pub use progress::{DailyGoal, GoalType, UserProgress};
use serde::{Deserialize, Serialize};
const LEVEL_XP: [u64; 20] = [
0, 100, 250, 500, 1000, 1750, 2750, 4000, 5500, 7500, 10000, 13000, 16500, 20500, 25000, 30000, 36000, 43000, 51000, 60000, ];
#[must_use]
pub fn level_from_xp(xp: u64) -> u32 {
for (level, &required) in LEVEL_XP.iter().enumerate().rev() {
if xp >= required {
#[allow(clippy::cast_possible_truncation)]
return (level + 1) as u32;
}
}
1
}
#[must_use]
pub fn level_progress(xp: u64) -> LevelProgress {
let level = level_from_xp(xp);
let level_idx = (level as usize).saturating_sub(1);
let current_level_xp = LEVEL_XP.get(level_idx).copied().unwrap_or(0);
let next_level_xp = LEVEL_XP.get(level_idx + 1).copied().unwrap_or(u64::MAX);
let xp_in_level = xp.saturating_sub(current_level_xp);
let xp_for_level = next_level_xp.saturating_sub(current_level_xp);
#[allow(clippy::cast_precision_loss)]
let percentage = if xp_for_level > 0 {
((xp_in_level as f64 / xp_for_level as f64) * 100.0).min(100.0)
} else {
100.0
};
LevelProgress {
level,
current_xp: xp,
xp_in_level,
xp_for_level,
percentage,
#[allow(clippy::cast_possible_truncation)]
is_max_level: level >= LEVEL_XP.len() as u32,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LevelProgress {
pub level: u32,
pub current_xp: u64,
pub xp_in_level: u64,
pub xp_for_level: u64,
pub percentage: f64,
pub is_max_level: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct XpReward;
impl XpReward {
pub const QUERY_EXECUTE: u64 = 1;
pub const FAST_QUERY: u64 = 5;
pub const ALGORITHM_RUN: u64 = 10;
pub const DAILY_FIRST: u64 = 25;
pub const STREAK_BONUS: u64 = 50;
pub const ACHIEVEMENT_UNLOCK: u64 = 100;
}
#[must_use]
pub const fn level_title(level: u32) -> &'static str {
match level {
1 => "Novice Operator",
2 => "Data Apprentice",
3 => "Query Runner",
4 => "Graph Walker",
5 => "Vector Seeker",
6 => "Index Builder",
7 => "Schema Architect",
8 => "Query Optimizer",
9 => "Data Wrangler",
10 => "Engine Master",
11 => "Tensor Adept",
12 => "Algorithm Sage",
13 => "Performance Tuner",
14 => "System Architect",
15 => "Data Scientist",
16 => "Graph Theorist",
17 => "Vector Mathematician",
18 => "Distributed Systems Expert",
19 => "Neumann Veteran",
20 => "Tensor Grandmaster",
_ => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_level_from_xp() {
assert_eq!(level_from_xp(0), 1);
assert_eq!(level_from_xp(50), 1);
assert_eq!(level_from_xp(100), 2);
assert_eq!(level_from_xp(250), 3);
assert_eq!(level_from_xp(60000), 20);
assert_eq!(level_from_xp(100000), 20);
}
#[test]
fn test_level_progress() {
let progress = level_progress(150);
assert_eq!(progress.level, 2);
assert_eq!(progress.xp_in_level, 50);
assert_eq!(progress.xp_for_level, 150);
assert!(!progress.is_max_level);
}
#[test]
fn test_level_progress_max() {
let progress = level_progress(60000);
assert_eq!(progress.level, 20);
assert!(progress.is_max_level);
}
#[test]
fn test_level_progress_zero() {
let progress = level_progress(0);
assert_eq!(progress.level, 1);
assert_eq!(progress.xp_in_level, 0);
assert_eq!(progress.percentage, 0.0);
}
#[test]
fn test_level_title() {
assert_eq!(level_title(1), "Novice Operator");
assert_eq!(level_title(10), "Engine Master");
assert_eq!(level_title(20), "Tensor Grandmaster");
assert_eq!(level_title(21), "Unknown");
}
#[test]
fn test_xp_rewards() {
assert_eq!(XpReward::QUERY_EXECUTE, 1);
assert_eq!(XpReward::FAST_QUERY, 5);
assert_eq!(XpReward::ACHIEVEMENT_UNLOCK, 100);
}
#[test]
fn test_level_progress_serialization() {
let progress = level_progress(500);
let json = serde_json::to_string(&progress).expect("serialization failed");
assert!(json.contains("level"));
assert!(json.contains("current_xp"));
}
#[test]
fn test_level_from_xp_all_boundaries() {
assert_eq!(level_from_xp(99), 1);
assert_eq!(level_from_xp(249), 2);
assert_eq!(level_from_xp(499), 3);
assert_eq!(level_from_xp(999), 4);
assert_eq!(level_from_xp(1749), 5);
assert_eq!(level_from_xp(2749), 6);
assert_eq!(level_from_xp(3999), 7);
assert_eq!(level_from_xp(5499), 8);
assert_eq!(level_from_xp(7499), 9);
assert_eq!(level_from_xp(9999), 10);
}
#[test]
fn test_level_from_xp_high_levels() {
assert_eq!(level_from_xp(10000), 11);
assert_eq!(level_from_xp(13000), 12);
assert_eq!(level_from_xp(16500), 13);
assert_eq!(level_from_xp(20500), 14);
assert_eq!(level_from_xp(25000), 15);
assert_eq!(level_from_xp(30000), 16);
assert_eq!(level_from_xp(36000), 17);
assert_eq!(level_from_xp(43000), 18);
assert_eq!(level_from_xp(51000), 19);
}
#[test]
fn test_level_title_all() {
assert_eq!(level_title(2), "Data Apprentice");
assert_eq!(level_title(3), "Query Runner");
assert_eq!(level_title(4), "Graph Walker");
assert_eq!(level_title(5), "Vector Seeker");
assert_eq!(level_title(6), "Index Builder");
assert_eq!(level_title(7), "Schema Architect");
assert_eq!(level_title(8), "Query Optimizer");
assert_eq!(level_title(9), "Data Wrangler");
assert_eq!(level_title(11), "Tensor Adept");
assert_eq!(level_title(12), "Algorithm Sage");
assert_eq!(level_title(13), "Performance Tuner");
assert_eq!(level_title(14), "System Architect");
assert_eq!(level_title(15), "Data Scientist");
assert_eq!(level_title(16), "Graph Theorist");
assert_eq!(level_title(17), "Vector Mathematician");
assert_eq!(level_title(18), "Distributed Systems Expert");
assert_eq!(level_title(19), "Neumann Veteran");
}
#[test]
fn test_level_title_edge_cases() {
assert_eq!(level_title(0), "Unknown");
assert_eq!(level_title(100), "Unknown");
}
#[test]
fn test_level_progress_mid_level() {
let progress = level_progress(1250); assert_eq!(progress.level, 5);
assert_eq!(progress.xp_in_level, 250);
assert_eq!(progress.xp_for_level, 750);
assert!(progress.percentage > 30.0 && progress.percentage < 40.0);
}
#[test]
fn test_level_progress_just_over_boundary() {
let progress = level_progress(101);
assert_eq!(progress.level, 2);
assert_eq!(progress.xp_in_level, 1);
}
#[test]
fn test_level_progress_at_max_level() {
let progress = level_progress(100000);
assert_eq!(progress.level, 20);
assert!(progress.is_max_level);
assert!(progress.percentage >= 0.0);
}
#[test]
fn test_xp_reward_all() {
assert_eq!(XpReward::QUERY_EXECUTE, 1);
assert_eq!(XpReward::FAST_QUERY, 5);
assert_eq!(XpReward::ALGORITHM_RUN, 10);
assert_eq!(XpReward::DAILY_FIRST, 25);
assert_eq!(XpReward::STREAK_BONUS, 50);
assert_eq!(XpReward::ACHIEVEMENT_UNLOCK, 100);
}
#[test]
fn test_level_progress_deserialization() {
let progress = level_progress(500);
let json = serde_json::to_string(&progress).expect("serialization failed");
let decoded: LevelProgress = serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(decoded.level, progress.level);
assert_eq!(decoded.current_xp, progress.current_xp);
}
}