Kerikim's picture
elkay frontend game leaderboard
04ada98
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"<style>{f.read()}</style>", 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 = '<div class="lb-head">πŸ† Leaderboard</div>'
for p in leaderboard:
is_you = p["rank"] == "You"
medal_cls = rank_medal_class(p["rank"])
symbol = rank_symbol(p["rank"])
you_pill = '<span class="lb-you-pill">YOU</span>' if is_you else ""
rows.append(
textwrap.dedent(f"""
<div class="lb-row {'is-you' if is_you else ''}">
<div class="lb-rank {medal_cls}">{symbol}</div>
<div class="lb-name">{p['name']}</div>
<div class="lb-level">Lvl {p['level']}</div>
<div class="lb-xp">{p['xp']:,} XP</div>
{you_pill}
</div>
""").strip()
)
html = textwrap.dedent(f"""
<div class="leaderboard">
{head}
{''.join(rows)}
</div>
""").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(
"""
<div style="
width:50px; height:50px;
border-radius:15px;
background: linear-gradient(135deg, #22c55e, #059669);
display:flex; align-items:center; justify-content:center;
font-size:28px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
">
✨
</div>
""",
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"""
<div style="background:#e0e0e0;border-radius:12px;padding:3px;width:100%;">
<div style="
width:{progress_pct}%;
background:linear-gradient(135deg,#22c55e,#059669);
height:24px;border-radius:10px;text-align:right;
color:white;font-weight:bold;padding-right:8px;line-height:24px;">
{total_xp:,} / {cap:,} XP
</div>
</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">Total XP: {total_xp:,}</div>
""", 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"""
<div style="
width:60px; height:60px;
border-radius:16px;
background:{g['color']};
display:flex; align-items:center; justify-content:center;
font-size:28px; margin-bottom:10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
">
{g['icon']}
</div>
""",
unsafe_allow_html=True
)
st.subheader(g["title"])
st.write(g["description"])
diff_color = color_map.get(g["difficulty"], "gray")
st.markdown(
f"<div style='color:{diff_color}; font-weight:bold'>{g['difficulty']}</div> | "
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}")