Reworked 'Levels' for progress
This commit is contained in:
BIN
assets/App.png
Normal file
BIN
assets/App.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
51
database.py
51
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."""
|
||||
|
||||
114
gamification.py
Normal file
114
gamification.py
Normal file
@@ -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
|
||||
15
main.py
15
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()
|
||||
|
||||
try:
|
||||
app.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
print("\nApplication interrupted by user. Exiting...")
|
||||
try:
|
||||
app.destroy()
|
||||
except:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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'):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -117,6 +156,15 @@ class DashboardFrame(ctk.CTkFrame):
|
||||
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)
|
||||
date_frame.grid(row=0, column=2, padx=10, sticky="ew")
|
||||
@@ -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 [])
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user