import streamlit as st from utils import db as dbapi import os import utils.api as api USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" # --- Load external CSS (optional) --- def load_css(file_name: str): try: with open(file_name, "r", encoding="utf-8") as f: st.markdown(f"", unsafe_allow_html=True) except FileNotFoundError: st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.") st.session_state.setdefault("current_game", None) # --- GAME RENDERERS --- def _render_budget_builder(): try: from phase.Student_view.games import budgetbuilder as budget_module except Exception as e: st.error(f"Couldn't import Budget Builder module: {e}") return if hasattr(budget_module, "show_budget_builder"): budget_module.show_budget_builder() elif hasattr(budget_module, "show_page"): budget_module.show_page() else: st.error("Budget Builder module found, but no show function (show_budget_builder/show_page).") def _render_debt_dilemma(): try: from phase.Student_view.games import debtdilemma as debt_module except Exception as e: st.error(f"Couldn't import Debt Dilemma module: {e}") return if hasattr(debt_module, "show_debt_dilemma"): debt_module.show_debt_dilemma() elif hasattr(debt_module, "show_page"): debt_module.show_page() else: st.error("Debt Dilemma module found, but no show function (show_debt_dilemma/show_page).") def _render_money_match(): """ Renders Money Match if the file exists at phase/games/MoneyMatch.py and exposes a show_page() function. """ try: from phase.Student_view.games import MoneyMatch as mm_module except Exception as e: st.error(f"Couldn't import Money Match module: {e}") st.info("Tip: ensure phase/games/MoneyMatch.py exists and defines show_page()") return if hasattr(mm_module, "show_page"): mm_module.show_page() else: st.error("Money Match module found, but no show_page() function.") #render for profit puzzle def _render_profit_puzzle(): try: from phase.Student_view.games import profitpuzzle as pp_module except Exception as e: st.error(f"Couldn't import Profit Puzzle module: {e}") return if hasattr(pp_module, "show_profit_puzzle"): pp_module.show_profit_puzzle() elif hasattr(pp_module, "show_page"): pp_module.show_page() else: st.error("Profit Puzzle module found, but no show function (show_profit_puzzle/show_page).") import textwrap def render_leaderboard(leaderboard): def rank_symbol(rank): if rank == "You": return "🟒" if isinstance(rank, int): return "πŸ₯‡" if rank == 1 else "πŸ₯ˆ" if rank == 2 else "πŸ₯‰" if rank == 3 else f"#{rank}" return str(rank) def rank_medal_class(rank): if isinstance(rank, int) and rank in (1, 2, 3): return f"medal-{rank}" return "" rows = [] head = '
πŸ† Leaderboard
' for p in leaderboard: is_you = p["rank"] == "You" medal_cls = rank_medal_class(p["rank"]) symbol = rank_symbol(p["rank"]) you_pill = 'YOU' if is_you else "" rows.append( textwrap.dedent(f"""
{symbol}
{p['name']}
Lvl {p['level']}
{p['xp']:,} XP
{you_pill}
""").strip() ) html = textwrap.dedent(f"""
{head} {''.join(rows)}
""").strip() st.markdown(html, unsafe_allow_html=True) def _load_leaderboard(user_id: int, limit: int = 10) -> list[dict]: you_name = (st.session_state.get("user") or {}).get("name") or "You" class_id = st.session_state.get("current_class_id") rows: list[dict] = [] try: if USE_LOCAL_DB: # ---------- local DB path ---------- if not class_id and hasattr(dbapi, "list_classes_for_student"): classes = dbapi.list_classes_for_student(user_id) or [] if classes: class_id = classes[0]["class_id"] st.session_state.current_class_id = class_id if class_id and hasattr(dbapi, "leaderboard_for_class"): rows = dbapi.leaderboard_for_class(class_id, limit=limit) or [] elif hasattr(dbapi, "leaderboard_global"): rows = dbapi.leaderboard_global(limit=limit) or [] elif class_id and hasattr(dbapi, "class_student_metrics"): metrics = dbapi.class_student_metrics(class_id) or [] rows = [{ "user_id": m.get("student_id"), "name": m.get("name") or m.get("email") or "Student", "xp": int(m.get("total_xp", 0)), "level": dbapi.level_from_xp(int(m.get("total_xp", 0))), } for m in metrics] else: # ---------- backend API path (DISABLE_DB=1) ---------- # 1) pick a class for the logged-in student if not class_id: try: classes = api.list_classes_for_student(user_id) or [] except Exception: classes = [] if classes: class_id = classes[0].get("class_id") st.session_state.current_class_id = class_id if class_id: # 2) get roster try: roster = api.list_students_in_class(class_id) or [] except Exception: roster = [] # 3) for each student, pull stats (XP/level) rows = [] for s in roster: sid = s.get("user_id") or s.get("student_id") if not sid: continue try: stt = api.user_stats(int(sid)) or {} except Exception: stt = {} rows.append({ "user_id": int(sid), "name": s.get("name") or s.get("email") or "Student", "xp": int(stt.get("xp", 0)), "level": int(stt.get("level", 1)), }) else: # No class available; at least show the current user try: s = api.user_stats(user_id) or {} except Exception: s = {} rows = [{"user_id": user_id, "name": you_name, "xp": int(s.get("xp", 0)), "level": int(s.get("level", 1))}] except Exception: rows = [] # Ensure YOU is present if not any(r.get("user_id") == user_id for r in rows): rows.append({"user_id": user_id, "name": you_name, "xp": 0, "level": 1}) # Rank, mark YOU, put YOU first rows.sort(key=lambda r: int(r.get("xp", 0)), reverse=True) ranked = [] for i, r in enumerate(rows, start=1): ranked.append({ "rank": i, "user_id": r["user_id"], "name": r["name"], "level": int(r["level"]), "xp": int(r["xp"]), }) for r in ranked: if r["user_id"] == user_id: r["rank"] = "You" break you = [r for r in ranked if r["rank"] == "You"] others = [r for r in ranked if r["rank"] != "You"] return (you + others)[:limit] # --- MAIN GAMES HUB & ROUTER --- def show_games(): load_css(os.path.join("assets", "styles.css")) if "user" not in st.session_state or st.session_state.user is None: st.error("❌ Please login first.") st.session_state.current_page = "Welcome" st.rerun() game_key = st.session_state.current_game # If a specific game is active β†’ render it if game_key is not None: if game_key == "budget_builder": _render_budget_builder() elif game_key == "money_match": _render_money_match() elif game_key == "debt_dilemma": _render_debt_dilemma() elif game_key == "profit_puzzle": _render_profit_puzzle() st.markdown("---") if st.button("β¬… Back to Games Hub"): st.session_state.current_game = None st.rerun() return # don’t render the hub # ===== Games Hub ===== st.title("Financial Games") st.subheader("Learn by playing! Master financial concepts through interactive games.") # Progress overview col1, col2 = st.columns([1, 5]) with col1: st.markdown( """
✨
""", unsafe_allow_html=True ) with col2: # pull live XP/level user_id = st.session_state.user["user_id"] # Prefer local DB only if enabled. Otherwise call backend. if USE_LOCAL_DB and hasattr(dbapi, "user_xp_and_level"): stats = dbapi.user_xp_and_level(user_id) # {'xp', 'level', 'streak', maybe 'into','need'} else: try: stats = api.user_stats(user_id) # backend /students/{id}/stats except Exception as e: # hard fallback so the page still renders stats = {"xp": int(st.session_state.get("xp", 0)), "level": 1, "streak": 0} total_xp = int(stats.get("xp", 0)) level = int(stats.get("level", 1)) st.session_state.xp = total_xp st.session_state.streak = int(stats.get("streak", 0)) # Show progress as TOTAL XP toward the NEXT threshold base = 500 # keep the server's level if it is sane, otherwise recompute level = level if level >= 1 else max(1, total_xp // base + 1) cap = level * base # Level 1 -> 500, Level 2 -> 1000, etc. progress_pct = min(100, int(round((total_xp / cap) * 100))) st.write(f"Level {level} Experience Points") st.markdown(f"""
{total_xp:,} / {cap:,} XP
Total XP: {total_xp:,}
""", unsafe_allow_html=True) st.markdown("---") # Game list games = [ {"key": "money_match", "icon": "πŸ’°", "title": "Money Match", "description": "Drag coins and notes to match target values. Perfect for learning denominations!", "difficulty": "Easy", "xp": "+10 XP", "time": "5-10 min", "color": "linear-gradient(135deg, #22c55e, #059669)"}, {"key": "budget_builder", "icon": "πŸ“Š", "title": "Budget Builder", "description": "Allocate your weekly allowance across different spending categories with real-time pie charts.", "difficulty": "Easy", "xp": "+50 XP", "time": "10-15 min", "color": "linear-gradient(135deg, #3b82f6, #06b6d4)"}, {"key": "profit_puzzle", "icon": "🧩", "title": "Profit Puzzle", "description": "Solve business scenarios with interactive sliders. Calculate profit, revenue, and costs!", "difficulty": "Medium", "xp": "+150 XP", "time": "10-15 min", "color": "linear-gradient(135deg, #6366f1, #8b5cf6)"}, {"key": "debt_dilemma", "icon": "⚠️", "title": "Debt Dilemma", "description": "Experience borrowing JA$100 and learn about interest, repayment schedules, and credit scores.", "difficulty": "Hard", "xp": "+200 XP", "time": "20-25 min", "color": "linear-gradient(135deg, #f97316, #dc2626)"}, ] cols = st.columns(2) color_map = {"Easy": "green", "Medium": "orange", "Hard": "red"} for i, g in enumerate(games): with cols[i % 2]: st.markdown( f"""
{g['icon']}
""", unsafe_allow_html=True ) st.subheader(g["title"]) st.write(g["description"]) diff_color = color_map.get(g["difficulty"], "gray") st.markdown( f"
{g['difficulty']}
| " f"{g['xp']} | {g['time']}", unsafe_allow_html=True ) if st.button("β–Ά Play Now", key=f"play_{g['key']}"): st.session_state.current_game = g["key"] st.rerun() st.markdown("---") # Leaderboard & Tips col_leader, col_tips = st.columns(2) with col_leader: user_id = st.session_state.user["user_id"] lb = _load_leaderboard(user_id, limit=10) if lb: render_leaderboard(lb) else: st.info("No leaderboard data yet.") with col_tips: st.subheader("Game Tips") for tip in [ "🌟 Start with easier games to build confidence", "⏰ Take your time to understand concepts", "πŸ† Replay games to improve your score", "🌍 Apply game lessons to real life", ]: st.markdown(f"- {tip}")