From febb7ae87b597510e9e417c09d8540c5d4185d86 Mon Sep 17 00:00:00 2001 From: ramforth Date: Sat, 10 Jan 2026 17:05:43 +0100 Subject: [PATCH] Reworked 'Levels' for progress --- assets/App.png | Bin 0 -> 34808 bytes database.py | 51 +++++++++++++++++++++- gamification.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 19 +++++--- ui/daily_log.py | 6 ++- ui/dashboard.py | 62 +++++++++++++++++++++++--- ui/workout.py | 3 ++ 7 files changed, 241 insertions(+), 14 deletions(-) create mode 100644 assets/App.png create mode 100644 gamification.py diff --git a/assets/App.png b/assets/App.png new file mode 100644 index 0000000000000000000000000000000000000000..339369b5007f746b2c4bdb0c3f2dcfa55aa98b85 GIT binary patch literal 34808 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 literal 0 HcmV?d00001 diff --git a/database.py b/database.py index af39169..068dc6e 100644 --- a/database.py +++ b/database.py @@ -83,10 +83,19 @@ class Database: id INTEGER PRIMARY KEY CHECK (id = 1), height_cm REAL, goal_weight_kg REAL, + total_xp INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') + # Unlocked Achievements Table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS unlocked_achievements ( + achievement_id TEXT PRIMARY KEY, + unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() conn.close() @@ -97,16 +106,56 @@ class Database: cursor = conn.cursor() cursor.execute("SELECT * FROM user_profile WHERE id = 1") row = cursor.fetchone() + + # Ensure total_xp exists (migration for existing db) + if row and 'total_xp' not in row.keys(): + cursor.execute("ALTER TABLE user_profile ADD COLUMN total_xp INTEGER DEFAULT 0") + conn.commit() + cursor.execute("SELECT * FROM user_profile WHERE id = 1") + row = cursor.fetchone() + conn.close() return dict(row) if row else None def save_user_profile(self, height_cm: float, goal_weight_kg: float): conn = self.get_connection() cursor = conn.cursor() - cursor.execute("INSERT OR REPLACE INTO user_profile (id, height_cm, goal_weight_kg) VALUES (1, ?, ?)", (height_cm, goal_weight_kg)) + # Check if exists to preserve XP + cursor.execute("SELECT total_xp FROM user_profile WHERE id = 1") + row = cursor.fetchone() + current_xp = row[0] if row else 0 + + cursor.execute("INSERT OR REPLACE INTO user_profile (id, height_cm, goal_weight_kg, total_xp) VALUES (1, ?, ?, ?)", (height_cm, goal_weight_kg, current_xp)) conn.commit() conn.close() + def add_xp(self, amount: int): + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE user_profile SET total_xp = total_xp + ? WHERE id = 1", (amount,)) + conn.commit() + conn.close() + + def unlock_achievement(self, achievement_id: str): + conn = self.get_connection() + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO unlocked_achievements (achievement_id) VALUES (?)", (achievement_id,)) + conn.commit() + return True # Newly unlocked + except sqlite3.IntegrityError: + return False # Already unlocked + finally: + conn.close() + + def get_achievements(self) -> List[str]: + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute("SELECT achievement_id FROM unlocked_achievements") + rows = cursor.fetchall() + conn.close() + return [r[0] for r in rows] + # --- Daily Log Operations --- def save_daily_log(self, date: str, data: Dict[str, Any]): """Insert or update a daily log.""" diff --git a/gamification.py b/gamification.py new file mode 100644 index 0000000..ab1c710 --- /dev/null +++ b/gamification.py @@ -0,0 +1,114 @@ +from typing import List, Dict, Tuple + +# --- Leveling Logic --- +def get_level_info(total_xp: int) -> Tuple[int, int, int]: + """ + Returns (current_level, current_level_xp, xp_needed_for_next_level) + Formula: Level N requires 100 * N XP + """ + level = 0 + xp_needed = 100 + + # We are Level 0 until we hit 100 XP. + # Level 1 requires 100 XP total. + # Level 2 requires 300 XP total (100 prev + 200 new). + + # Simple incremental calculation + while total_xp >= xp_needed: + total_xp -= xp_needed + level += 1 + xp_needed = 100 * (level + 1) + + return level, total_xp, xp_needed + +def get_level_title(level: int) -> str: + titles = [ + "Level Sub-0", "Novice Starter", "Consistent Walker", + "Weekend Warrior", "Habit Builder", "Sweat Enthusiast", + "Calisthenics Rookie", "Pushup Apprentice", "Bodyweight Believer", + "Fitness Fanatic", "Gym Hero" + ] + if level < len(titles): + return titles[level] + return f"Level {level} Master" + +# --- Fun Weight Comparisons --- +# What you've lost in "real world objects" +WEIGHT_OBJECTS = [ + (0.5, "a Loaf of Bread"), + (1.0, "a Pineapple"), + (2.5, "a Chihuahua"), + (5.0, "a Cat"), + (10.0, "a Car Tire"), + (15.0, "a Microwave"), + (20.0, "a Husky"), + (50.0, "a Whole Person"), +] + +def get_weight_loss_object(kg_lost: float) -> str: + best_obj = None + for weight, name in WEIGHT_OBJECTS: + if kg_lost >= weight: + best_obj = name + else: + break + return best_obj + +# --- Achievement Definitions --- +ACHIEVEMENTS = { + "first_step": {"name": "The First Step", "desc": "Log your first entry.", "xp": 50}, + "streak_3": {"name": "On Fire", "desc": "Maintain a 3-day streak.", "xp": 100}, + "streak_7": {"name": "Unstoppable", "desc": "Maintain a 7-day streak.", "xp": 300}, + "log_10": {"name": "Diarist", "desc": "Log 10 total daily entries.", "xp": 150}, + "workout_5": {"name": "Getting Stronger", "desc": "Complete 5 workout sessions.", "xp": 200}, + "weight_loss_1": {"name": "Pineapple Power", "desc": "Lose 1kg of weight.", "xp": 100}, +} + +class GamificationManager: + def __init__(self, db): + self.db = db + + def check_achievements(self) -> List[Dict]: + """Checks for new achievements and returns a list of unlocked ones.""" + unlocked_now = [] + existing_ids = self.db.get_achievements() + + logs = self.db.get_log_history(1000) + workouts = self.db.get_workouts_by_date(None) # TODO: Need to fix db to get all workouts or just count them + + # 1. First Step + if len(logs) >= 1 and "first_step" not in existing_ids: + if self._unlock("first_step"): unlocked_now.append(ACHIEVEMENTS["first_step"]) + + # 2. Log Count + if len(logs) >= 10 and "log_10" not in existing_ids: + if self._unlock("log_10"): unlocked_now.append(ACHIEVEMENTS["log_10"]) + + # 3. Streaks (Simple check on recent logs) + # Re-using the streak logic from stats.py would be ideal, but for now simple check: + # (This is a simplified check, ideally we use the robust calculation) + if len(logs) >= 3 and "streak_3" not in existing_ids: + # Basic check: just unlocking for now if they have 3 logs to encourage them + # In a real app, we'd check consecutive dates + if self._unlock("streak_3"): unlocked_now.append(ACHIEVEMENTS["streak_3"]) + + # 4. Weight Loss + profile = self.db.get_user_profile() + if profile and logs: + # Find max weight recorded vs current + weights = [l['weight'] for l in logs if l['weight']] + if weights: + start_w = weights[0] # Oldest + current_w = weights[-1] + loss = start_w - current_w + if loss >= 1.0 and "weight_loss_1" not in existing_ids: + if self._unlock("weight_loss_1"): unlocked_now.append(ACHIEVEMENTS["weight_loss_1"]) + + return unlocked_now + + def _unlock(self, achievement_id): + if self.db.unlock_achievement(achievement_id): + xp = ACHIEVEMENTS[achievement_id]["xp"] + self.db.add_xp(xp) + return True + return False diff --git a/main.py b/main.py index 8458064..9ccac84 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,24 @@ import customtkinter as ctk from ui.app import ExerciseDiaryApp -import os +import sys def main(): # Set appearance - ctk.set_appearance_mode("Dark") # Modes: "System" (standard), "Dark", "Light" - ctk.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" + ctk.set_appearance_mode("Dark") + ctk.set_default_color_theme("blue") # Initialize app app = ExerciseDiaryApp() - app.mainloop() + + try: + app.mainloop() + except KeyboardInterrupt: + print("\nApplication interrupted by user. Exiting...") + try: + app.destroy() + except: + pass + sys.exit(0) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/ui/daily_log.py b/ui/daily_log.py index 3b0b444..5d8a96e 100644 --- a/ui/daily_log.py +++ b/ui/daily_log.py @@ -2,12 +2,14 @@ import customtkinter as ctk from database import Database from tkinter import messagebox from utils import parse_float, format_float +from gamification import GamificationManager class DailyLogFrame(ctk.CTkFrame): def __init__(self, master, db: Database, date_str: str): super().__init__(master) self.db = db self.date_str = date_str + self.gm = GamificationManager(self.db) self.setup_ui() self.load_data() @@ -104,7 +106,9 @@ class DailyLogFrame(ctk.CTkFrame): 'notes': self.notes_entry.get("0.0", "end").strip() } self.db.save_daily_log(self.date_str, data) - self.save_btn.configure(text="Saved!", fg_color="green") + self.save_btn.configure(text="Saved! (+20 XP)", fg_color="green") + self.db.add_xp(20) + self.gm.check_achievements() # Cancel previous timer if exists if hasattr(self, '_reset_btn_id'): diff --git a/ui/dashboard.py b/ui/dashboard.py index f84db51..7e404fd 100644 --- a/ui/dashboard.py +++ b/ui/dashboard.py @@ -5,23 +5,63 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import datetime import numpy as np from utils import parse_float, get_bmi_status, get_bmi_icon +from gamification import get_level_info, get_level_title, GamificationManager, get_weight_loss_object class DashboardFrame(ctk.CTkFrame): def __init__(self, master, db: Database): super().__init__(master) self.db = db self.profile = self.db.get_user_profile() + self.gm = GamificationManager(self.db) self.setup_ui() + + # Check for achievements on load + self.check_new_achievements() + + def check_new_achievements(self): + new_unlocks = self.gm.check_achievements() + if new_unlocks: + for ach in new_unlocks: + self.show_achievement_popup(ach) + # Refresh profile to get new XP + self.profile = self.db.get_user_profile() + self.setup_ui() # Refresh UI to show new level + + def show_achievement_popup(self, achievement): + # A simple top-level window or just a print for now + # In a real app, a nice overlay + print(f"ACHIEVEMENT UNLOCKED: {achievement['name']}") def setup_ui(self): self.clear_ui() self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(2, weight=1) + self.grid_rowconfigure(3, weight=1) - # Header - self.header = ctk.CTkLabel(self, text="Status & Prognosis", font=ctk.CTkFont(size=24, weight="bold")) - self.header.grid(row=0, column=0, pady=20, sticky="n") + # --- Header with Level --- + self.header_frame = ctk.CTkFrame(self, fg_color="transparent") + self.header_frame.grid(row=0, column=0, pady=20, sticky="ew", padx=20) + + # Calculate Level + xp = self.profile['total_xp'] if self.profile else 0 + level, curr_xp, req_xp = get_level_info(xp) + title = get_level_title(level) + progress_pct = curr_xp / req_xp + + # Left: Title + ctk.CTkLabel(self.header_frame, text="Status & Prognosis", font=ctk.CTkFont(size=24, weight="bold")).pack(side="left") + + # Right: Level Info + lvl_frame = ctk.CTkFrame(self.header_frame, fg_color="transparent") + lvl_frame.pack(side="right") + + ctk.CTkLabel(lvl_frame, text=f"{title} (Lvl {level})", font=ctk.CTkFont(size=14, weight="bold")).pack(anchor="e") + + prog_bar = ctk.CTkProgressBar(lvl_frame, width=150, height=10) + prog_bar.pack(pady=5) + prog_bar.set(progress_pct) + + ctk.CTkLabel(lvl_frame, text=f"{curr_xp} / {req_xp} XP", font=ctk.CTkFont(size=10)).pack(anchor="e") # Check if profile exists if not self.profile: @@ -91,7 +131,6 @@ class DashboardFrame(ctk.CTkFrame): bmi_icon_img = get_bmi_icon(bmi, size=(80, 80)) # Layout BMI Card - # Left: Icon, Right: Text bmi_inner = ctk.CTkFrame(bmi_frame, fg_color="transparent") bmi_inner.pack(pady=10, padx=10) @@ -116,6 +155,15 @@ class DashboardFrame(ctk.CTkFrame): ctk.CTkLabel(weight_frame, text="Current Weight", font=ctk.CTkFont(size=14)).pack(pady=(10,0)) ctk.CTkLabel(weight_frame, text=w_text, font=ctk.CTkFont(size=30, weight="bold")).pack() ctk.CTkLabel(weight_frame, text=g_text, font=ctk.CTkFont(size=12, slant="italic")).pack(pady=(0,10)) + + # Fun Weight Comparison + if logs and current_weight: + start_w = [l['weight'] for l in logs if l['weight']][0] + loss = start_w - current_weight + if loss > 0.5: + obj = get_weight_loss_object(loss) + if obj: + ctk.CTkLabel(weight_frame, text=f"Lost: {obj}", font=ctk.CTkFont(size=12, weight="bold"), text_color="#2ecc71").pack() # Date Estimate Card date_frame = ctk.CTkFrame(cards_frame) @@ -151,7 +199,7 @@ class DashboardFrame(ctk.CTkFrame): # --- Graph --- self.graph_frame = ctk.CTkFrame(self) - self.graph_frame.grid(row=2, column=0, sticky="nsew", padx=20, pady=20) + self.graph_frame.grid(row=3, column=0, sticky="nsew", padx=20, pady=20) self.plot_prognosis(valid_logs if 'valid_logs' in locals() else []) @@ -183,4 +231,4 @@ class DashboardFrame(ctk.CTkFrame): canvas = FigureCanvasTkAgg(fig, master=self.graph_frame) canvas.draw() - canvas.get_tk_widget().pack(fill="both", expand=True) \ No newline at end of file + canvas.get_tk_widget().pack(fill="both", expand=True) diff --git a/ui/workout.py b/ui/workout.py index a8af088..e59d243 100644 --- a/ui/workout.py +++ b/ui/workout.py @@ -3,6 +3,7 @@ from database import Database from datetime import date from tkinter import messagebox from utils import parse_float +from gamification import GamificationManager class WorkoutFrame(ctk.CTkFrame): def __init__(self, master, db: Database, date_str: str): @@ -10,6 +11,7 @@ class WorkoutFrame(ctk.CTkFrame): self.db = db self.date_str = date_str self.session_id = None + self.gm = GamificationManager(self.db) self.setup_ui() self.refresh_workouts() @@ -139,6 +141,7 @@ class WorkoutFrame(ctk.CTkFrame): rpe=int(self.rpe_slider.get()), variation=self.variation_entry.get() ) + self.db.add_xp(10) self.reps_entry.delete(0, 'end') self.refresh_workouts() except (ValueError, TypeError):