File size: 14,671 Bytes
08e67e4
 
 
c378c1e
08e67e4
c378c1e
08e67e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44b5561
08e67e4
 
44b5561
04ada98
44b5561
 
 
 
 
 
 
 
 
 
 
 
04ada98
 
 
 
 
 
 
08e67e4
04ada98
 
44b5561
 
 
 
 
 
 
 
 
 
04ada98
44b5561
04ada98
44b5561
04ada98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44b5561
04ada98
44b5561
04ada98
44b5561
04ada98
 
 
44b5561
1ffd314
08e67e4
04ada98
08e67e4
04ada98
 
 
08e67e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44b5561
04ada98
08e67e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c378c1e
08e67e4
 
1ffd314
 
 
 
 
 
 
 
 
c378c1e
 
1ffd314
 
c378c1e
 
1575c0f
 
 
 
 
 
 
1ffd314
 
08e67e4
 
 
1ffd314
08e67e4
 
 
1575c0f
08e67e4
 
c378c1e
08e67e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
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}")