# phase\Student_view\games\budgetbuilder.py import streamlit as st import os, time from utils import api as backend from utils import db as dbapi DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1" def _rerun(): try: st.rerun() except AttributeError: st.experimental_rerun() def _refresh_global_xp(): user = st.session_state.get("user") if not user: return try: stats = backend.user_stats(user["user_id"]) if DISABLE_DB else dbapi.user_xp_and_level(user["user_id"]) st.session_state.xp = stats.get("xp", st.session_state.get("xp", 0)) st.session_state.streak = stats.get("streak", st.session_state.get("streak", 0)) except Exception as e: st.warning(f"XP refresh failed: {e}") def _persist_budget_result(level_cfg: dict, success: bool, gained_xp: int): user = st.session_state.get("user") if not user: st.info("Login to earn and save XP.") return try: elapsed_ms = int((time.time() - st.session_state.get("bb_start_ts", time.time())) * 1000) allocations = [{"id": cid, "amount": int(val)} for cid, val in st.session_state.categories.items()] budget_score = 100 if success else 0 if DISABLE_DB: backend.record_budget_builder_play( user_id=user["user_id"], weekly_allowance=int(level_cfg["income"]), budget_score=int(budget_score), elapsed_ms=elapsed_ms, allocations=allocations, gained_xp=int(gained_xp), ) else: # Local DB path (if your db layer has one of these) if hasattr(dbapi, "record_budget_builder_result"): dbapi.record_budget_builder_result( user_id=user["user_id"], weekly_allowance=int(level_cfg["income"]), budget_score=int(budget_score), elapsed_ms=elapsed_ms, allocations=allocations, gained_xp=int(gained_xp), ) elif hasattr(dbapi, "award_xp"): dbapi.award_xp(user["user_id"], int(gained_xp), reason="budget_builder") _refresh_global_xp() # <-- this makes the XP bar move immediately except Exception as e: st.warning(f"Could not save budget result: {e}") def show_budget_builder(): # timer for elapsed_ms if "bb_start_ts" not in st.session_state: st.session_state.bb_start_ts = time.time() # Add custom CSS for improved styling st.markdown(""" """, unsafe_allow_html=True) # ----------------------------- # Define Levels and Categories # ----------------------------- levels = [ { "id": 1, "title": "First Budget", "description": "Learn basic budget allocation", "scenario": "You're 14 and just started getting a weekly allowance. Your parents want to see you can manage money responsibly before increasing it.", "income": 300, "objectives": [ "Save at least 20% of your income", "Don't spend more than 30% on entertainment", "Allocate money for food and transport" ], "constraints": { "savings": {"min": 60}, "fun": {"max": 90}, "food": {"min": 40, "required": True}, "transport": {"min": 30, "required": True}, }, "success": [ ("Save at least JA$60 (20%)", lambda cats, inc: cats["savings"] >= 60), ("Keep entertainment under JA$90 (30%)", lambda cats, inc: cats["fun"] <= 90), ("Balance your budget completely", lambda cats, inc: sum(cats.values()) == inc), ], "xp": 20, }, { "id": 2, "title": "Emergency Fund", "description": "Build an emergency fund while managing expenses", "scenario": "Your phone broke last month and you had no savings to fix it. This time, build an emergency fund while still enjoying life.", "income": 400, "objectives": [ "Build an emergency fund (JA$100+)", "Still save for long-term goals", "Cover all essential expenses", ], "constraints": { "savings": {"min": 150}, # Emergency + regular savings "food": {"min": 60, "required": True}, "transport": {"min": 40, "required": True}, "school": {"min": 20, "required": True}, }, "success": [ ("Save at least JA$150 total", lambda cats, inc: cats["savings"] >= 150), ( "Cover all essential expenses", lambda cats, inc: cats["food"] >= 60 and cats["transport"] >= 40 and cats["school"] >= 20, ), ], "xp": 30, }, { "id": 3, "title": "Reduced Income", "description": "Manage when money is tight", "scenario": "Your allowance got cut because of family finances. You need to make tough choices while still maintaining your savings habit.", "income": 250, "objectives": [ "Still save something (minimum JA$25)", "Cut non-essential spending", "Maintain essential expenses", ], "constraints": { "savings": {"min": 25}, "fun": {"max": 40}, "food": {"min": 50, "required": True}, "transport": {"min": 35, "required": True}, }, "success": [ ("Save at least JA$25 (10%)", lambda cats, inc: cats["savings"] >= 25), ("Keep entertainment under JA$40", lambda cats, inc: cats["fun"] <= 40), ("Balance your budget", lambda cats, inc: sum(cats.values()) == inc), ], "xp": 35, }, { "id": 4, "title": "Debt & Goals", "description": "Pay off debt while saving for something special", "scenario": "You borrowed JA$100 from your parents for a school trip. Now you need to pay it back (JA$25/week) while saving for a new game console.", "income": 450, "objectives": [ "Pay debt installment (JA$25)", "Save for console (JA$50+ per week)", "Don't compromise on essentials", ], "constraints": { "savings": {"min": 75}, # 50 for console + 25 debt payment "food": {"min": 70, "required": True}, "transport": {"min": 45, "required": True}, "school": {"min": 30, "required": True}, }, "success": [ ("Allocate JA$75+ for savings & debt", lambda cats, inc: cats["savings"] >= 75), ( "Cover all essentials adequately", lambda cats, inc: cats["food"] >= 70 and cats["transport"] >= 45 and cats["school"] >= 30, ), ], "xp": 40, }, { "id": 5, "title": "Master Budgeter", "description": "Handle multiple financial goals like an adult", "scenario": "You're 16 now with part-time job income. Manage multiple goals: emergency fund, college savings, social life, and family contribution.", "income": 600, "objectives": [ "Build emergency fund (JA$50)", "Save for college (JA$100)", "Contribute to family (JA$40)", "Maintain social life and hobbies", ], "constraints": { "savings": {"min": 150}, # Emergency + college "charity": {"min": 40}, # Family contribution "food": {"min": 80, "required": True}, "transport": {"min": 60, "required": True}, "school": {"min": 50, "required": True}, }, "success": [ ("Save JA$150+ for future goals", lambda cats, inc: cats["savings"] >= 150), ("Contribute JA$40+ to family", lambda cats, inc: cats["charity"] >= 40), ( "Balance entertainment & responsibilities", lambda cats, inc: cats["fun"] >= 30 and cats["fun"] <= 150, ), ("Perfect budget balance", lambda cats, inc: sum(cats.values()) == inc), ], "xp": 50, }, ] # ----------------------------- # Initialize Session State # ----------------------------- if "current_level" not in st.session_state: st.session_state.current_level = 1 if "completed_levels" not in st.session_state: st.session_state.completed_levels = [] if "categories" not in st.session_state: st.session_state.categories = {} if "level_completed" not in st.session_state: st.session_state.level_completed = False # ----------------------------- # Categories Master # ----------------------------- categories_master = { "food": {"name": "Food & Snacks", "color": "#16a34a", "icon": "🍎", "min": 0, "max": 300}, "savings": {"name": "Savings", "color": "#2563eb", "icon": "💰", "min": 0, "max": 400}, "fun": {"name": "Entertainment", "color": "#dc2626", "icon": "🎮", "min": 0, "max": 300}, "charity": {"name": "Charity/Family", "color": "#e11d48", "icon": "❤️", "min": 0, "max": 200}, "transport": {"name": "Transport", "color": "#ea580c", "icon": "🚌", "min": 0, "max": 200}, "school": {"name": "School Supplies", "color": "#0891b2", "icon": "📚", "min": 0, "max": 150}, } if not st.session_state.categories: st.session_state.categories = {cid: 0 for cid in categories_master.keys()} # ----------------------------- # Current Level Setup # ----------------------------- level = [l for l in levels if l["id"] == st.session_state.current_level][0] # Header section with improved styling st.markdown(f"""
{level["scenario"]}
Use the sliders below to allocate your weekly income across different categories. Make sure to meet the objectives!
""", unsafe_allow_html=True) st.markdown("### 📊 Allocate Your Budget") # Render sliders without dynamic inter-dependencies for cid, cat in categories_master.items(): constraints = level["constraints"].get(cid, {}) min_val = 0 #max is set to the level income for more flexibility max_val = level["income"] st.session_state.categories[cid] = st.slider( f"{cat['icon']} {cat['name']}", min_value=min_val, max_value=max_val, value=st.session_state.categories[cid], step=5, help=f"Min: JA${min_val}, Max: JA${max_val}" ) # Calculate totals after sliders have been selected total_allocated = sum(st.session_state.categories.values()) remaining = level["income"] - total_allocated st.metric("Remaining", f"JA${remaining}", delta_color="inverse" if remaining < 0 else "normal") # Remaining budget display with better styling color = "#ef4444" if remaining < 0 else "#059669" if remaining == 0 else "#f59e0b" st.markdown(f"""