Muthuraja18 commited on
Commit
139e933
·
verified ·
1 Parent(s): cdc107e
Files changed (1) hide show
  1. app.py +807 -213
app.py CHANGED
@@ -1,231 +1,825 @@
1
- import pandas as pd
2
- import streamlit as st
3
- import random
4
- import uuid
 
 
 
 
 
 
5
  import os
6
- import json
7
  import time
8
- import requests # For calling the Groq API
9
- import plotly.express as px
10
- import plotly.graph_objects as go
11
- from streamlit.components.v1 import html
12
-
13
- # Define file paths
14
- LEADERBOARD_FILE = "leaderboard.csv"
15
- GAME_DATA_FILE = "game_data.json"
16
- PLAYERS_FILE = "players.json"
17
 
18
- # Groq API Configuration
19
- GROQ_API_KEY = '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'
20
- GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
 
 
21
 
22
- # Define questions for multiple topics with at least 20 questions each
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  questions_db = {
24
  "Geography": [
25
  ("What is the capital of France?", ["Paris", "London", "Berlin", "Madrid"], "Paris"),
26
- ("What is the largest country in the world by land area?", ["Canada", "United States", "Russia", "China"], "Russia"),
27
- ("Which river flows through Egypt?", ["Nile", "Amazon", "Ganges", "Yangtze"], "Nile"),
28
- ("What is the capital of Japan?", ["Tokyo", "Kyoto", "Osaka", "Sapporo"], "Tokyo"),
29
- ("Which is the smallest country in the world?", ["Monaco", "Vatican City", "San Marino", "Liechtenstein"], "Vatican City"),
30
- ],
31
- "Science": [
32
- ("What is the chemical symbol for water?", ["H2O", "CO2", "O2", "H2"], "H2O"),
33
- ("What is the powerhouse of the cell?", ["Nucleus", "Mitochondria", "Ribosome", "Endoplasmic Reticulum"], "Mitochondria"),
34
- ("What planet is known as the Red Planet?", ["Venus", "Mars", "Jupiter", "Saturn"], "Mars"),
35
- ("Who developed the theory of relativity?", ["Isaac Newton", "Albert Einstein", "Nikola Tesla", "Marie Curie"], "Albert Einstein"),
36
- ("What is the largest organ in the human body?", ["Heart", "Skin", "Lungs", "Brain"], "Skin"),
37
  ],
38
  "Math": [
39
- ("What is 5 * 12?", ["50", "60", "55", "70"], "60"),
40
- ("What is the square root of 64?", ["6", "7", "8", "9"], "8"),
41
- ("What is 15 + 25?", ["35", "40", "45", "50"], "40"),
42
- ("What is the value of pi?", ["3.14", "3.15", "3.16", "3.17"], "3.14"),
43
- ("What is 20 / 4?", ["5", "6", "7", "8"], "5"),
 
 
44
  ],
45
  "IPL": [
46
- ("Which IPL team won the 2020 IPL season?", ["Mumbai Indians", "Delhi Capitals", "Royal Challengers Bangalore", "Chennai Super Kings"], "Mumbai Indians"),
47
- ("Who is the all-time highest run-scorer in IPL history?", ["Virat Kohli", "Rohit Sharma", "Suresh Raina", "Chris Gayle"], "Virat Kohli"),
48
- ("Who won the first-ever IPL match?", ["Kolkata Knight Riders", "Royal Challengers Bangalore", "Chennai Super Kings", "Mumbai Indians"], "Kolkata Knight Riders"),
49
- ("Which IPL team is known as the 'Yellow Army'?", ["Chennai Super Kings", "Mumbai Indians", "Delhi Capitals", "Sunrisers Hyderabad"], "Chennai Super Kings"),
50
- ("Who hit the most sixes in the 2020 IPL season?", ["Shivam Dube", "AB de Villiers", "Ishan Kishan", "Kieron Pollard"], "Ishan Kishan"),
51
  ]
52
  }
53
 
54
- # Function to load leaderboard from CSV file
55
- def load_leaderboard():
56
- if os.path.exists(LEADERBOARD_FILE):
57
- leaderboard_df = pd.read_csv(LEADERBOARD_FILE, names=['name', 'score', 'question', 'answer', 'correct', 'topic', 'avatar'], header=0)
58
- return leaderboard_df
59
- return pd.DataFrame(columns=['name', 'score', 'question', 'answer', 'correct', 'topic', 'avatar'])
60
-
61
- # Function to save leaderboard data to CSV
62
- def save_leaderboard(leaderboard_df):
63
- leaderboard_df.to_csv(LEADERBOARD_FILE, index=False)
64
-
65
- # Function to create a new game
66
- def create_game():
67
- game_id = str(uuid.uuid4())[:8] # Generate a unique Game ID
68
- selected_topics = st.multiselect("Choose Topics", list(questions_db.keys())) # Topic selection
69
- num_questions = st.selectbox("Select the number of questions", [5, 10, 15, 20])
70
-
71
- if selected_topics:
72
- # Collect questions from the selected topics
73
- questions = []
74
- questions_per_topic = num_questions // len(selected_topics)
75
-
76
- # Ensure we don't ask for more questions than are available
77
- for topic in selected_topics:
78
- available_questions = questions_db[topic]
79
- if len(available_questions) < questions_per_topic:
80
- st.warning(f"Not enough questions in {topic}. Only {len(available_questions)} questions available.")
81
- questions_per_topic = len(available_questions)
82
-
83
- questions.extend(random.sample(available_questions, questions_per_topic))
84
-
85
- random.shuffle(questions) # Shuffle questions
86
-
87
- game_data = {'game_id': game_id, 'topics': selected_topics, 'questions': questions}
88
-
89
- # Store the game data to a JSON file
90
- if os.path.exists(GAME_DATA_FILE):
91
- with open(GAME_DATA_FILE, "r") as file:
92
- all_games = json.load(file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  else:
94
- all_games = {}
95
-
96
- all_games[game_id] = game_data
97
-
98
- with open(GAME_DATA_FILE, "w") as file:
99
- json.dump(all_games, file)
100
-
101
- st.success(f"Game created successfully! Game ID: {game_id}")
102
- return game_id, selected_topics, questions
 
 
 
 
 
103
  else:
104
- st.error("Please select at least one topic.")
105
-
106
- # Function to join a game
107
- def join_game():
108
- game_id = st.text_input("Enter Game ID to join:")
109
-
110
- if game_id:
111
- with open(GAME_DATA_FILE, "r") as file:
112
- all_games = json.load(file)
113
-
114
- if game_id in all_games:
115
- game_data = all_games[game_id]
116
- st.session_state['game_data'] = game_data
117
- st.success(f"Joined game with ID: {game_id}")
118
- start_game(game_data) # Start the game with the joined data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  else:
120
- st.error("Invalid Game ID. Please check and try again.")
121
-
122
- # Function to start the game
123
- def start_game(game_data):
124
- topics = game_data['topics']
125
- questions = game_data['questions']
126
-
127
- username = st.text_input("Enter your name:")
128
- avatar = st.selectbox("Choose your Avatar", ["🐱", "🐶", "🦄", "👽", "🎮"])
129
- st.session_state['avatar'] = avatar
130
-
131
- if username:
132
- if 'answers' not in st.session_state:
133
- st.session_state['answers'] = [""] * len(questions)
134
-
135
- score = 0
136
- current_question = 0
137
- while current_question < len(questions):
138
- question, options, correct_answer = questions[current_question]
139
- topic = next(topic for topic in topics if any(q[0] == question for q in questions_db[topic]))
140
-
141
- if current_question not in st.session_state.get('options', {}):
142
- st.session_state['options'] = {current_question: options}
143
-
144
- options = st.session_state['options'][current_question]
145
- answer = st.radio(f"Question {current_question+1}: {question}", options, key=f"q{current_question}")
146
- st.session_state['answers'][current_question] = answer
147
-
148
- with st.empty():
149
- timer_text = st.empty()
150
- for time_left in range(10, 0, -1):
151
- timer_text.text(f"Time left for this question: {time_left} seconds")
152
- time.sleep(1)
153
-
154
- # Display Correct or Incorrect after the timer ends
155
- if answer == correct_answer:
156
- st.write("Correct!")
157
- score += 10
158
  else:
159
- st.write(f"Incorrect! The correct answer is: {correct_answer}")
160
-
161
- current_question += 1
162
-
163
- submit_button = st.button("Submit Answers")
164
- if submit_button:
165
- leaderboard_df = load_leaderboard()
166
- new_row = pd.DataFrame({
167
- 'name': [username],
168
- 'score': [score],
169
- 'question': [', '.join([q[0] for q in questions])],
170
- 'answer': [', '.join(st.session_state['answers'])],
171
- 'correct': [', '.join(['Yes' if st.session_state['answers'][i].strip().lower() == questions[i][2].lower() else 'No' for i in range(len(st.session_state['answers']))])],
172
- 'topic': [', '.join(topics)],
173
- 'avatar': [avatar]
174
- })
175
-
176
- leaderboard_df = pd.concat([leaderboard_df, new_row], ignore_index=True)
177
- leaderboard_df = leaderboard_df.sort_values(by='score', ascending=False).reset_index(drop=True)
178
- save_leaderboard(leaderboard_df)
179
-
180
- st.success(f"Game Over! Your final score is {score}")
181
-
182
- # Function to display the leaderboard with animations
183
- def display_dashboard():
184
- leaderboard_df = load_leaderboard()
185
-
186
- st.subheader("Top 3 Players")
187
- top_3 = leaderboard_df.head(3)
188
-
189
- # First Place Winner - Celebration Animation
190
- first_place = top_3.iloc[0] if not leaderboard_df.empty else None
191
- if first_place is not None:
192
- st.write(f"🏆 {first_place['name']} - {first_place['score']} points! 🎉 {first_place['avatar']}")
193
- st.markdown("<h3 style='color: gold;'>🎉 Congratulations on First Place! 🎉</h3>", unsafe_allow_html=True)
194
- html_code = f"""
195
- <div style="text-align: center;">
196
- <h2>🏆 Winner: {first_place['name']} 🏆</h2>
197
- <p>Score: {first_place['score']}</p>
198
- <p>Avatar: {first_place['avatar']}</p>
199
- </div>
200
- <div id="balloon" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
201
- <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Yellow_balloon.svg/1024px-Yellow_balloon.svg.png" width="150" alt="Balloon" />
202
- </div>
203
- """
204
- html(html_code)
205
- st.balloons()
206
-
207
- st.write("Top 3 Players:")
208
- for index, row in top_3.iterrows():
209
- st.write(f"{row['name']} - {row['score']} points! {row['avatar']}")
210
-
211
- # Main function to handle Streamlit app
212
- def main():
213
- st.title('AI Quiz Game')
214
-
215
- mode = st.sidebar.selectbox("Select Mode", ["Home", "Create Game", "Join Game", "Leaderboard"])
216
-
217
- if mode == "Home":
218
- st.write("Welcome to the Game!")
219
- st.write("You can create a new game, join an existing game, or check the leaderboard.")
220
-
221
- elif mode == "Create Game":
222
- create_game()
223
-
224
- elif mode == "Join Game":
225
- join_game()
226
-
227
- elif mode == "Leaderboard":
228
- display_dashboard()
229
-
230
- if __name__ == "__main__":
231
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ """
3
+ Unified Online/Offline AI Quiz Game with Friends, Chat, Presence, Invites.
4
+
5
+ - Offline: uses local JSON files under ./data/
6
+ - Online: uses Firebase Realtime Database when configured (optional)
7
+ - Put your Firebase service account JSON next to this file and name it serviceAccountKey.json
8
+ - FIREBASE_DB_URL is set to your Firebase project's Realtime DB (from your screenshot)
9
+ """
10
+
11
  import os
12
+ import uuid
13
  import time
14
+ import json
15
+ from datetime import datetime, timedelta
 
 
 
 
 
 
 
16
 
17
+ import streamlit as st
18
+ import pandas as pd
19
+ import random
20
+ from streamlit.components.v1 import html
21
+ import plotly.express as px
22
 
23
+ # Try to import firebase-admin (optional). If unavailable, app will run Offline.
24
+ try:
25
+ import firebase_admin
26
+ from firebase_admin import credentials, db
27
+ FIREBASE_AVAILABLE = True
28
+ except Exception:
29
+ FIREBASE_AVAILABLE = False
30
+
31
+ # ---------------- Page config ----------------
32
+ st.set_page_config(page_title="AI Quiz Game — Online/Offline", layout="wide")
33
+
34
+ # ---------------- Configuration ----------------
35
+ DATA_DIR = "data"
36
+ os.makedirs(DATA_DIR, exist_ok=True)
37
+
38
+ # Local filenames
39
+ GAMES_FILE = os.path.join(DATA_DIR, "games.json")
40
+ PLAYERS_FILE = os.path.join(DATA_DIR, "players.json")
41
+ MESSAGES_FILE = os.path.join(DATA_DIR, "messages.json")
42
+ SESSIONS_FILE = os.path.join(DATA_DIR, "sessions.json")
43
+ LEADERBOARD_FILE = os.path.join(DATA_DIR, "leaderboard.csv")
44
+ FRIENDS_FILE = os.path.join(DATA_DIR, "friends.json")
45
+ INBOX_FILE = os.path.join(DATA_DIR, "inbox.json") # friend requests & invitations
46
+
47
+ # Firebase defaults (you provided project)
48
+ FIREBASE_CREDENTIALS = os.getenv("FIREBASE_CREDENTIALS", "serviceAccountKey.json")
49
+ FIREBASE_DB_URL = os.getenv("FIREBASE_DB_URL", "https://real-time-database-fe632-default-rtdb.firebaseio.com/")
50
+
51
+ # Heartbeat threshold (seconds) to consider a session active
52
+ HEARTBEAT_THRESHOLD_SECONDS = 40
53
+
54
+ # ---------------- Example Questions DB (expand as needed) ----------------
55
  questions_db = {
56
  "Geography": [
57
  ("What is the capital of France?", ["Paris", "London", "Berlin", "Madrid"], "Paris"),
58
+ ("Largest country by area?", ["Canada", "USA", "Russia", "China"], "Russia"),
59
+ ("River through Egypt?", ["Nile", "Amazon", "Ganges", "Yangtze"], "Nile"),
 
 
 
 
 
 
 
 
 
60
  ],
61
  "Math": [
62
+ ("5 * 12?", ["50", "60", "55", "70"], "60"),
63
+ ("sqrt(64)?", ["6","7","8","9"], "8"),
64
+ ("What is 15 + 25?", ["35","40","45","50"], "40"),
65
+ ],
66
+ "Science": [
67
+ ("H2O is?", ["Water","CO2","O2","H2"], "Water"),
68
+ ("Who developed relativity?", ["Newton","Einstein","Tesla","Curie"], "Einstein"),
69
  ],
70
  "IPL": [
71
+ ("2020 IPL winner?", ["Mumbai Indians","Delhi Capitals","RCB","CSK"], "Mumbai Indians"),
72
+ ("Which team is called Yellow Army?", ["CSK","MI","DC","SRH"], "CSK")
 
 
 
73
  ]
74
  }
75
 
76
+ # ----------------- JSON helpers -----------------
77
+ def load_json(path, default):
78
+ if os.path.exists(path):
79
+ try:
80
+ with open(path, "r", encoding="utf-8") as f:
81
+ return json.load(f)
82
+ except Exception:
83
+ return default
84
+ return default
85
+
86
+ def save_json(path, data):
87
+ with open(path, "w", encoding="utf-8") as f:
88
+ json.dump(data, f, indent=2, ensure_ascii=False)
89
+
90
+ # Ensure base files exist (local)
91
+ base_defaults = {
92
+ GAMES_FILE: {},
93
+ PLAYERS_FILE: {},
94
+ MESSAGES_FILE: {},
95
+ SESSIONS_FILE: {},
96
+ FRIENDS_FILE: {},
97
+ INBOX_FILE: {}
98
+ }
99
+ for p, d in base_defaults.items():
100
+ if not os.path.exists(p):
101
+ save_json(p, d)
102
+ if not os.path.exists(LEADERBOARD_FILE):
103
+ pd.DataFrame(columns=['name','score','game_id','topics','timestamp','avatar','questions','answers','correct_flags']).to_csv(LEADERBOARD_FILE, index=False)
104
+
105
+ # ----------------- Firebase init -----------------
106
+ def init_firebase_if_needed():
107
+ """Initialize Firebase Admin if available and credentials present. Return (ok,msg)."""
108
+ global FIREBASE_AVAILABLE
109
+ if not FIREBASE_AVAILABLE:
110
+ return False, "firebase-admin not installed"
111
+ if not FIREBASE_DB_URL:
112
+ return False, "FIREBASE_DB_URL not set"
113
+ if not os.path.exists(FIREBASE_CREDENTIALS):
114
+ return False, f"Service account file not found at {FIREBASE_CREDENTIALS}"
115
+ try:
116
+ if not firebase_admin._apps:
117
+ cred = credentials.Certificate(FIREBASE_CREDENTIALS)
118
+ firebase_admin.initialize_app(cred, {"databaseURL": FIREBASE_DB_URL})
119
+ return True, "Firebase initialized"
120
+ except Exception as e:
121
+ return False, f"Firebase init error: {e}"
122
+
123
+ # Firebase helpers
124
+ def fb_get(path):
125
+ try:
126
+ ref = db.reference(path)
127
+ return ref.get()
128
+ except Exception:
129
+ return None
130
+
131
+ def fb_set(path, value):
132
+ ref = db.reference(path)
133
+ ref.set(value)
134
+
135
+ def fb_push(path, value):
136
+ ref = db.reference(path).push()
137
+ ref.set(value)
138
+ return ref.key
139
+
140
+ # ----------------- Unified DB API (Online or Offline) -----------------
141
+ def local_get(collection):
142
+ if collection == "games":
143
+ return load_json(GAMES_FILE, {})
144
+ if collection == "players":
145
+ return load_json(PLAYERS_FILE, {})
146
+ if collection == "messages":
147
+ return load_json(MESSAGES_FILE, {})
148
+ if collection == "sessions":
149
+ return load_json(SESSIONS_FILE, {})
150
+ if collection == "friends":
151
+ return load_json(FRIENDS_FILE, {})
152
+ if collection == "inbox":
153
+ return load_json(INBOX_FILE, {})
154
+ if collection == "leaderboard":
155
+ try:
156
+ df = pd.read_csv(LEADERBOARD_FILE)
157
+ return df.to_dict(orient="records")
158
+ except Exception:
159
+ return []
160
+ return {}
161
+
162
+ def local_set(collection, value):
163
+ if collection == "games":
164
+ save_json(GAMES_FILE, value)
165
+ elif collection == "players":
166
+ save_json(PLAYERS_FILE, value)
167
+ elif collection == "messages":
168
+ save_json(MESSAGES_FILE, value)
169
+ elif collection == "sessions":
170
+ save_json(SESSIONS_FILE, value)
171
+ elif collection == "friends":
172
+ save_json(FRIENDS_FILE, value)
173
+ elif collection == "inbox":
174
+ save_json(INBOX_FILE, value)
175
+ elif collection == "leaderboard":
176
+ try:
177
+ df = pd.DataFrame(value)
178
+ df.to_csv(LEADERBOARD_FILE, index=False)
179
+ except Exception:
180
+ pass
181
+
182
+ def unified_get(collection):
183
+ mode = st.session_state.get("mode_selection", "Offline")
184
+ if mode == "Online":
185
+ ok, msg = init_firebase_if_needed()
186
+ if not ok:
187
+ return local_get(collection)
188
+ # map collection to firebase path
189
+ if collection == "games":
190
+ return fb_get("/games") or {}
191
+ if collection == "players":
192
+ return fb_get("/players") or {}
193
+ if collection == "messages":
194
+ return fb_get("/messages") or {}
195
+ if collection == "sessions":
196
+ return fb_get("/active_sessions") or {}
197
+ if collection == "friends":
198
+ return fb_get("/friends") or {}
199
+ if collection == "inbox":
200
+ return fb_get("/inbox") or {}
201
+ if collection == "leaderboard":
202
+ raw = fb_get("/leaderboard") or {}
203
+ if isinstance(raw, dict):
204
+ return list(raw.values())
205
+ return raw
206
+ else:
207
+ return local_get(collection)
208
+
209
+ def unified_set(collection, data):
210
+ mode = st.session_state.get("mode_selection", "Offline")
211
+ if mode == "Online":
212
+ ok, msg = init_firebase_if_needed()
213
+ if not ok:
214
+ local_set(collection, data)
215
+ return
216
+ if collection == "games":
217
+ fb_set("/games", data); return
218
+ if collection == "players":
219
+ fb_set("/players", data); return
220
+ if collection == "messages":
221
+ fb_set("/messages", data); return
222
+ if collection == "sessions":
223
+ fb_set("/active_sessions", data); return
224
+ if collection == "friends":
225
+ fb_set("/friends", data); return
226
+ if collection == "inbox":
227
+ fb_set("/inbox", data); return
228
+ if collection == "leaderboard":
229
+ local_set(collection, data); return
230
+ else:
231
+ local_set(collection, data)
232
+
233
+ def unified_push_message(game_id, msg_obj):
234
+ mode = st.session_state.get("mode_selection", "Offline")
235
+ if mode == "Online":
236
+ ok, _ = init_firebase_if_needed()
237
+ if ok:
238
+ fb_push(f"/messages/{game_id}", msg_obj)
239
+ return
240
+ all_msgs = unified_get("messages") or {}
241
+ game_msgs = all_msgs.get(game_id, [])
242
+ game_msgs.append(msg_obj)
243
+ if len(game_msgs) > 500:
244
+ game_msgs = game_msgs[-500:]
245
+ all_msgs[game_id] = game_msgs
246
+ unified_set("messages", all_msgs)
247
+
248
+ def unified_push_leaderboard(row):
249
+ mode = st.session_state.get("mode_selection", "Offline")
250
+ if mode == "Online":
251
+ ok, _ = init_firebase_if_needed()
252
+ if ok:
253
+ fb_push("/leaderboard", row)
254
+ # also save local backup
255
+ try:
256
+ df = pd.read_csv(LEADERBOARD_FILE)
257
+ except Exception:
258
+ df = pd.DataFrame(columns=list(row.keys()))
259
+ df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
260
+ df.to_csv(LEADERBOARD_FILE, index=False)
261
+ return
262
+ # offline append CSV
263
+ try:
264
+ df = pd.read_csv(LEADERBOARD_FILE)
265
+ except Exception:
266
+ df = pd.DataFrame(columns=list(row.keys()))
267
+ df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
268
+ df.to_csv(LEADERBOARD_FILE, index=False)
269
+
270
+ # ----------------- Session & Presence helpers -----------------
271
+ def now_iso():
272
+ return datetime.utcnow().isoformat()
273
+
274
+ def parse_iso(s):
275
+ try:
276
+ return datetime.fromisoformat(s)
277
+ except Exception:
278
+ return None
279
+
280
+ def ensure_session_ids():
281
+ if "uid" not in st.session_state:
282
+ st.session_state['uid'] = str(uuid.uuid4())
283
+ if "session_id" not in st.session_state:
284
+ st.session_state['session_id'] = str(uuid.uuid4())
285
+ ensure_session_ids()
286
+
287
+ def claim_session_unified(game_id, username):
288
+ sessions = unified_get("sessions") or {}
289
+ game_sessions = sessions.get(game_id, {})
290
+ rec = game_sessions.get(username)
291
+ now = now_iso()
292
+ if rec is None:
293
+ game_sessions[username] = {"session_id": st.session_state['session_id'], "last_heartbeat": now}
294
+ sessions[game_id] = game_sessions
295
+ unified_set("sessions", sessions)
296
+ return True, ""
297
+ last = parse_iso(rec.get("last_heartbeat"))
298
+ if rec.get("session_id") == st.session_state['session_id']:
299
+ rec["last_heartbeat"] = now
300
+ game_sessions[username] = rec
301
+ sessions[game_id] = game_sessions
302
+ unified_set("sessions", sessions)
303
+ return True, ""
304
+ if last and (datetime.utcnow() - last) < timedelta(seconds=HEARTBEAT_THRESHOLD_SECONDS):
305
+ return False, "You are active in another tab/device. Return to that tab or wait."
306
+ # override stale
307
+ game_sessions[username] = {"session_id": st.session_state['session_id'], "last_heartbeat": now}
308
+ sessions[game_id] = game_sessions
309
+ unified_set("sessions", sessions)
310
+ return True, ""
311
+
312
+ def heartbeat_unified(game_id, username):
313
+ sessions = unified_get("sessions") or {}
314
+ game_sessions = sessions.get(game_id, {})
315
+ rec = game_sessions.get(username, {})
316
+ rec["session_id"] = st.session_state['session_id']
317
+ rec["last_heartbeat"] = now_iso()
318
+ game_sessions[username] = rec
319
+ sessions[game_id] = game_sessions
320
+ unified_set("sessions", sessions)
321
+
322
+ # ----------------- Friends & Inbox helpers -----------------
323
+ def get_friends_map():
324
+ return unified_get("friends") or {}
325
+
326
+ def save_friends_map(m):
327
+ unified_set("friends", m)
328
+
329
+ def get_inbox():
330
+ return unified_get("inbox") or {}
331
+
332
+ def save_inbox(i):
333
+ unified_set("inbox", i)
334
+
335
+ def send_friend_request(from_user, to_user):
336
+ inbox = get_inbox()
337
+ user_inbox = inbox.get(to_user, [])
338
+ user_inbox.append({"type":"friend_request","from":from_user,"ts":now_iso()})
339
+ inbox[to_user] = user_inbox
340
+ save_inbox(inbox)
341
+
342
+ def accept_friend_request(current_user, from_user):
343
+ friends = get_friends_map()
344
+ friends.setdefault(current_user, [])
345
+ friends.setdefault(from_user, [])
346
+ if from_user not in friends[current_user]:
347
+ friends[current_user].append(from_user)
348
+ if current_user not in friends[from_user]:
349
+ friends[from_user].append(current_user)
350
+ save_friends_map(friends)
351
+ # remove request
352
+ inbox = get_inbox()
353
+ entries = inbox.get(current_user, [])
354
+ entries = [e for e in entries if not (e.get('type')=='friend_request' and e.get('from')==from_user)]
355
+ inbox[current_user] = entries
356
+ save_inbox(inbox)
357
+
358
+ def send_game_invite(from_user, to_user, game_id):
359
+ inbox = get_inbox()
360
+ user_inbox = inbox.get(to_user, [])
361
+ user_inbox.append({"type":"invite","from":from_user,"game_id":game_id,"ts":now_iso()})
362
+ inbox[to_user] = user_inbox
363
+ save_inbox(inbox)
364
+
365
+ # ----------------- Game helpers -----------------
366
+ def create_game(host_name, topics, num_questions=5, auto_close=True):
367
+ games = unified_get("games") or {}
368
+ pool = []
369
+ for t in topics:
370
+ pool += questions_db.get(t, [])
371
+ if len(pool) < num_questions:
372
+ num_questions = len(pool)
373
+ questions = random.sample(pool, num_questions)
374
+ game_id = str(uuid.uuid4())[:8]
375
+ game_obj = {
376
+ "game_id": game_id,
377
+ "host": host_name,
378
+ "topics": topics,
379
+ "questions": questions,
380
+ "created_at": now_iso(),
381
+ "closed": False,
382
+ "auto_close_on_submit": bool(auto_close)
383
+ }
384
+ games[game_id] = game_obj
385
+ unified_set("games", games)
386
+ players = unified_get("players") or {}
387
+ players[game_id] = players.get(game_id, {})
388
+ unified_set("players", players)
389
+ msgs = unified_get("messages") or {}
390
+ msgs[game_id] = msgs.get(game_id, [])
391
+ unified_set("messages", msgs)
392
+ return game_id
393
+
394
+ def join_game(game_id, username, avatar):
395
+ games = unified_get("games") or {}
396
+ if game_id not in games:
397
+ return False, "Invalid Game ID"
398
+ if games[game_id].get("closed"):
399
+ return False, "Game is closed"
400
+ players = unified_get("players") or {}
401
+ game_players = players.get(game_id, {})
402
+ if username in game_players:
403
+ game_players[username]['avatar'] = avatar
404
+ game_players[username]['last_joined'] = now_iso()
405
+ else:
406
+ game_players[username] = {"avatar": avatar, "joined_at": now_iso(), "submitted": False}
407
+ players[game_id] = game_players
408
+ unified_set("players", players)
409
+ ok, msg = claim_session_unified(game_id, username)
410
+ if ok:
411
+ players = unified_get("players") or {}
412
+ players[game_id][username]['last_heartbeat'] = now_iso()
413
+ unified_set("players", players)
414
+ return ok, msg
415
+
416
+ def compute_score(questions, answers, times):
417
+ total = 0
418
+ flags = []
419
+ for i, q in enumerate(questions):
420
+ ans = answers[i] if i < len(answers) else ""
421
+ try:
422
+ correct = str(ans).strip().lower() == str(q[2]).strip().lower()
423
+ except Exception:
424
+ correct = False
425
+ if correct:
426
+ base = 10
427
+ t = times[i] if i < len(times) and times[i] else 999
428
+ bonus = max(0, int((max(0, 15 - min(t,15)) / 15) * 5))
429
+ total += base + bonus
430
+ flags.append("Yes")
431
  else:
432
+ flags.append("No")
433
+ return total, flags
434
+
435
+ # ----------------- UI -----------------
436
+ st.sidebar.title("Mode & Profile")
437
+ mode_choice = st.sidebar.selectbox("Mode", ["Offline (local JSON)", "Online (Firebase)"], index=0)
438
+ st.session_state['mode_selection'] = "Online" if mode_choice.startswith("Online") else "Offline"
439
+
440
+ # If user chose Online, attempt to init and show feedback
441
+ if st.session_state['mode_selection'] == "Online":
442
+ ok, msg = init_firebase_if_needed()
443
+ if not ok:
444
+ st.sidebar.error(f"Online init failed: {msg}. Working Offline.")
445
+ st.session_state['mode_selection'] = "Offline"
446
  else:
447
+ st.sidebar.success("Online (Firebase) ready.")
448
+
449
+ st.sidebar.markdown("### You")
450
+ uname = st.sidebar.text_input("Your name", value=st.session_state.get("username",""))
451
+ avat = st.sidebar.selectbox("Avatar", ["🎮","🐱","🐶","🦄","👽","🎩"], index=0)
452
+ st.session_state['username'] = uname or st.session_state.get("username","")
453
+ st.session_state['avatar'] = avat or st.session_state.get("avatar","🎮")
454
+
455
+ if st.sidebar.button("Refresh"):
456
+ st.experimental_rerun()
457
+
458
+ page = st.sidebar.selectbox("Page", ["Home","Create Game","Join Game","Play","Friends","Inbox","Leaderboard"], index=0)
459
+ st.title("AI Quiz Game — Online/Offline (Friends & Chat)")
460
+
461
+ def render_copy_button(val, key):
462
+ copy_html = f'''
463
+ <div style="display:flex;gap:8px;align-items:center;">
464
+ <input id="gid_{key}" value="{val}" readonly style="padding:6px;border:1px solid #ddd;border-radius:6px;">
465
+ <button onclick="navigator.clipboard.writeText(document.getElementById('gid_{key}').value)" style="padding:6px 10px;border-radius:6px;cursor:pointer;">Copy</button>
466
+ </div>
467
+ '''
468
+ html(copy_html)
469
+
470
+ # Home page
471
+ def home_page():
472
+ st.header("Home")
473
+ st.write("Create games, invite friends, play and climb the leaderboard.")
474
+ if st.session_state.get('last_score') is not None:
475
+ st.success(f"Your last score: {st.session_state.get('last_score')} (Game {st.session_state.get('last_game')})")
476
+
477
+ sessions = unified_get("sessions") or {}
478
+ active_list = []
479
+ threshold = timedelta(seconds=HEARTBEAT_THRESHOLD_SECONDS)
480
+ for gid, users in (sessions or {}).items():
481
+ for uname, rec in (users or {}).items():
482
+ last = parse_iso(rec.get('last_heartbeat'))
483
+ if last and (datetime.utcnow() - last) < threshold:
484
+ active_list.append((gid, uname, rec.get('last_heartbeat')))
485
+ st.subheader("Active players (recent)")
486
+ st.markdown(f"**{len(active_list)} players active now**")
487
+ for gid, uname, ts in active_list[:50]:
488
+ st.write(f"• **{uname}** (Game: {gid}) — last {ts}")
489
+
490
+ st.markdown("---")
491
+ games = unified_get("games") or {}
492
+ players_map = unified_get("players") or {}
493
+ st.subheader("Recent games")
494
+ for g in sorted(list(games.values()), key=lambda x: x.get("created_at",""), reverse=True)[:10]:
495
+ gid = g.get("game_id")
496
+ st.markdown(f"### 🎮 Game: **{gid}** {'(Closed)' if g.get('closed') else ''}")
497
+ st.write(f"Host: {g.get('host')} — Topics: {', '.join(g.get('topics',[]))}")
498
+ st.write(f"Created: {g.get('created_at')}")
499
+ players_here = players_map.get(gid, {}) or {}
500
+ st.write(f"Players joined: **{len(players_here)}**")
501
+ if players_here:
502
+ for uname_p, info in players_here.items():
503
+ status = "✅ Submitted" if info.get('submitted') else "⏳ Playing"
504
+ st.write(f"{info.get('avatar','🎮')} **{uname_p}** — {status}")
505
+ if not g.get('closed'):
506
+ st.info(f"Share this Game ID: {gid}")
507
+ render_copy_button(gid, gid)
508
+ if st.session_state.get('username'):
509
+ if st.button(f"Invite your friends to {gid}", key=f"invite_{gid}"):
510
+ friends = get_friends_map().get(st.session_state['username'], [])
511
+ if not friends:
512
+ st.warning("No friends to invite.")
513
+ else:
514
+ for f in friends:
515
+ send_game_invite(st.session_state['username'], f, gid)
516
+ st.success("Invites sent to friends.")
517
+ if st.button(f"Challenge friends with a new game like {gid}", key=f"challenge_{gid}"):
518
+ new_id = create_game(st.session_state.get('username','Host'), g.get('topics',[]), num_questions=len(g.get('questions',[])))
519
+ st.success(f"Challenge created: {new_id}")
520
+ st.markdown("---")
521
+
522
+ # Create game
523
+ def create_game_page():
524
+ st.header("Create Game")
525
+ host = st.text_input("Host name", value=st.session_state.get("username",""))
526
+ topics = st.multiselect("Topics", list(questions_db.keys()))
527
+ num_q = st.selectbox("Number of questions", [5,10,15,20], index=0)
528
+ auto_close = st.checkbox("Auto-close when someone submits", value=True)
529
+ if st.button("Create Game"):
530
+ if not host or not topics:
531
+ st.error("Enter host and choose topics.")
532
+ else:
533
+ gid = create_game(host, topics, num_q, auto_close)
534
+ st.success(f"Created game: {gid}")
535
+ st.session_state['game_id'] = gid
536
+ st.session_state['username'] = host
537
+ st.session_state['avatar'] = st.session_state.get('avatar','🎮')
538
+ st.session_state['is_host'] = True
539
+
540
+ # Join game
541
+ def join_game_page():
542
+ st.header("Join Game")
543
+ gid = st.text_input("Game ID", value=st.session_state.get('game_id',''))
544
+ uname = st.text_input("Your name", value=st.session_state.get('username',''))
545
+ avatar = st.selectbox("Avatar", ["🎮","🐱","🐶","🦄","👽","🎩"], index=0)
546
+ if st.button("Join"):
547
+ if not gid or not uname:
548
+ st.error("Provide Game ID and name.")
549
  else:
550
+ ok, msg = join_game(gid, uname, avatar)
551
+ if not ok:
552
+ st.error(msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  else:
554
+ st.success("Joined game.")
555
+ st.session_state['game_id'] = gid
556
+ st.session_state['username'] = uname
557
+ st.session_state['avatar'] = avatar
558
+ st.session_state['is_host'] = (unified_get("games") or {}).get(gid, {}).get("host") == uname
559
+ g = unified_get("games") or {}
560
+ st.session_state['game_questions'] = g[gid]['questions']
561
+ st.session_state['current_index'] = 0
562
+ st.session_state['answers'] = [""] * len(st.session_state['game_questions'])
563
+ st.session_state['answer_times'] = [None] * len(st.session_state['game_questions'])
564
+ st.experimental_rerun()
565
+
566
+ # Play page
567
+ def play_page():
568
+ st.header("Play")
569
+ gid = st.session_state.get('game_id')
570
+ uname = st.session_state.get('username')
571
+ avatar = st.session_state.get('avatar','🎮')
572
+ if not gid or not uname:
573
+ st.info("Join a game first.")
574
+ return
575
+
576
+ ok, msg = claim_session_unified(gid, uname)
577
+ if not ok:
578
+ st.error(msg)
579
+ return
580
+
581
+ heartbeat_unified(gid, uname)
582
+
583
+ games = unified_get("games") or {}
584
+ game = games.get(gid)
585
+ if not game:
586
+ st.error("Game not found.")
587
+ return
588
+ if game.get('closed'):
589
+ st.error("Game closed.")
590
+ return
591
+
592
+ questions = st.session_state.get('game_questions', game.get('questions', []))
593
+ if not questions:
594
+ st.error("No questions loaded.")
595
+ return
596
+
597
+ idx = st.session_state.get('current_index', 0)
598
+ if idx >= len(questions):
599
+ st.success("All done — submit!")
600
+ return
601
+
602
+ q = questions[idx]
603
+ st.subheader(f"Question {idx+1}/{len(questions)}")
604
+ st.write(q[0])
605
+ if 'question_started_at' not in st.session_state:
606
+ st.session_state['question_started_at'] = time.time()
607
+ choice = st.radio("Choose an answer:", q[1], key=f"choice_{idx}")
608
+ elapsed = int(time.time() - st.session_state['question_started_at'])
609
+ time_limit = 15
610
+ st.markdown(f"**Time left:** {max(0, time_limit - elapsed)} seconds")
611
+
612
+ col1, col2 = st.columns(2)
613
+ with col1:
614
+ if st.button("Next"):
615
+ taken = time.time() - st.session_state['question_started_at']
616
+ answers = st.session_state.get('answers', [""]*len(questions))
617
+ times = st.session_state.get('answer_times', [None]*len(questions))
618
+ answers[idx] = choice
619
+ times[idx] = taken
620
+ st.session_state['answers'] = answers
621
+ st.session_state['answer_times'] = times
622
+ st.session_state['current_index'] = idx + 1
623
+ st.session_state['question_started_at'] = None
624
+ players = unified_get("players") or {}
625
+ if players.get(gid, {}).get(uname):
626
+ players[gid][uname]['last_heartbeat'] = now_iso()
627
+ unified_set("players", players)
628
+ heartbeat_unified(gid, uname)
629
+ st.experimental_rerun()
630
+
631
+ with col2:
632
+ if idx == len(questions)-1:
633
+ if st.button("Submit All Answers"):
634
+ answers = st.session_state.get('answers', [""]*len(questions))
635
+ times = st.session_state.get('answer_times', [None]*len(questions))
636
+ answers[idx] = choice
637
+ times[idx] = time.time() - st.session_state['question_started_at'] if st.session_state.get('question_started_at') else None
638
+ st.session_state['answers'] = answers
639
+ st.session_state['answer_times'] = times
640
+ score, flags = compute_score(questions, answers, times)
641
+ now = now_iso()
642
+ row = {
643
+ "name": uname,
644
+ "score": score,
645
+ "game_id": gid,
646
+ "topics": ",".join(game.get('topics',[])),
647
+ "timestamp": now,
648
+ "avatar": avatar,
649
+ "questions": " || ".join([q[0] for q in questions]),
650
+ "answers": " || ".join([str(a) for a in answers]),
651
+ "correct_flags": " || ".join(flags)
652
+ }
653
+ unified_push_leaderboard(row)
654
+ players = unified_get("players") or {}
655
+ if players.get(gid) and players[gid].get(uname):
656
+ players[gid][uname]['submitted'] = True
657
+ players[gid][uname]['submitted_at'] = now
658
+ unified_set("players", players)
659
+ if game.get('auto_close_on_submit', True):
660
+ games[gid]['closed'] = True
661
+ games[gid]['closed_at'] = now
662
+ unified_set("games", games)
663
+ st.success(f"Submitted! Score: {score}")
664
+ st.balloons()
665
+ st.session_state['last_score'] = score
666
+ st.session_state['last_game'] = gid
667
+ st.session_state['current_index'] = 0
668
+ st.experimental_rerun()
669
+
670
+ st.markdown("---")
671
+ st.subheader("Game Chat")
672
+ msgs = unified_get("messages") or {}
673
+ game_msgs = msgs.get(gid, []) if isinstance(msgs, dict) else msgs
674
+ for m in game_msgs[-100:]:
675
+ st.write(f"**{m.get('user')}** [{m.get('ts')}]: {m.get('text')}")
676
+ new_msg = st.text_input("Send message to game", key=f"chat_{gid}")
677
+ if st.button("Send", key=f"send_{gid}"):
678
+ if new_msg and new_msg.strip():
679
+ unified_push_message(gid, {"user": uname, "text": new_msg.strip(), "ts": now_iso()})
680
+ heartbeat_unified(gid, uname)
681
+ st.experimental_rerun()
682
+
683
+ # Friends page
684
+ def friends_page():
685
+ st.header("Friends")
686
+ user = st.session_state.get('username')
687
+ if not user:
688
+ st.info("Enter your name in the sidebar to use Friends.")
689
+ return
690
+ friends_map = get_friends_map()
691
+ your_friends = friends_map.get(user, [])
692
+ st.subheader("Your friends")
693
+ if your_friends:
694
+ for f in your_friends:
695
+ sessions = unified_get("sessions") or {}
696
+ status = "offline"
697
+ for gid, users in (sessions or {}).items():
698
+ rec = users.get(f)
699
+ if rec:
700
+ last = parse_iso(rec.get('last_heartbeat'))
701
+ if last and (datetime.utcnow() - last) < timedelta(seconds=HEARTBEAT_THRESHOLD_SECONDS):
702
+ status = "online"
703
+ break
704
+ st.write(f"• {f} — **{status}**")
705
+ if st.button(f"Invite {f} to a game", key=f"invitebtn_{f}"):
706
+ gid = st.text_input(f"Enter game id to invite {f} (or leave blank to create)", key=f"inviteinput_{f}")
707
+ if gid:
708
+ send_game_invite(st.session_state['username'], f, gid)
709
+ st.success(f"Invite sent to {f} for game {gid}")
710
+ else:
711
+ topics = list(questions_db.keys())[:1]
712
+ new_id = create_game(st.session_state['username'], topics, num_questions=5)
713
+ send_game_invite(st.session_state['username'], f, new_id)
714
+ st.success(f"Invite sent to {f} for new game {new_id}")
715
+ else:
716
+ st.write("You have no friends yet.")
717
+
718
+ st.markdown("---")
719
+ st.subheader("Find / Add friends")
720
+ all_users = set()
721
+ players = unified_get("players") or {}
722
+ for gid, users in (players or {}).items():
723
+ for u in (users or {}).keys():
724
+ all_users.add(u)
725
+ all_users = all_users.union(set(get_friends_map().keys()))
726
+ all_users.discard(user)
727
+ candidate = st.text_input("Search user to add (exact name)", value="")
728
+ if st.button("Send Friend Request"):
729
+ if not candidate:
730
+ st.error("Enter user name")
731
+ else:
732
+ send_friend_request(user, candidate)
733
+ st.success("Friend request sent.")
734
+
735
+ # Inbox page
736
+ def inbox_page():
737
+ st.header("Inbox")
738
+ user = st.session_state.get('username')
739
+ if not user:
740
+ st.info("Enter your name in the sidebar to view Inbox.")
741
+ return
742
+ inbox = get_inbox()
743
+ items = inbox.get(user, [])
744
+ if not items:
745
+ st.write("No messages.")
746
+ return
747
+ for idx, item in enumerate(items[:50]):
748
+ t = item.get('type')
749
+ if t == "friend_request":
750
+ fr = item.get('from')
751
+ st.write(f"Friend request from **{fr}** at {item.get('ts')}")
752
+ if st.button(f"Accept {idx}"):
753
+ accept_friend_request(user, fr)
754
+ st.success(f"You are now friends with {fr}")
755
+ st.experimental_rerun()
756
+ if st.button(f"Reject {idx}"):
757
+ entries = [it for it in items if not (it.get('type')=='friend_request' and it.get('from')==fr)]
758
+ inbox[user] = entries
759
+ save_inbox(inbox)
760
+ st.success("Rejected")
761
+ st.experimental_rerun()
762
+ elif t == "invite":
763
+ fr = item.get('from'); gid = item.get('game_id')
764
+ st.write(f"Invite from **{fr}** to join game **{gid}** at {item.get('ts')}")
765
+ if st.button(f"Join Invite {idx}"):
766
+ ok, msg = join_game(gid, user, st.session_state.get('avatar','🎮'))
767
+ if ok:
768
+ st.success(f"Joined game {gid}")
769
+ items = [it for it in items if not (it.get('type')=='invite' and it.get('from')==fr and it.get('game_id')==gid)]
770
+ inbox[user] = items
771
+ save_inbox(inbox)
772
+ st.session_state['game_id'] = gid
773
+ st.session_state['username'] = user
774
+ st.experimental_rerun()
775
+ else:
776
+ st.error(msg)
777
+
778
+ # Leaderboard page
779
+ def leaderboard_page():
780
+ st.header("Leaderboard")
781
+ rows = unified_get("leaderboard") or []
782
+ if isinstance(rows, list) and rows:
783
+ df = pd.DataFrame(rows)
784
+ else:
785
+ try:
786
+ df = pd.read_csv(LEADERBOARD_FILE)
787
+ except Exception:
788
+ df = pd.DataFrame()
789
+ if df.empty:
790
+ st.info("No scores yet.")
791
+ return
792
+ st.dataframe(df.sort_values(by="score", ascending=False).head(200))
793
+ fig = px.bar(df.sort_values('score', ascending=False).head(10), x='name', y='score')
794
+ st.plotly_chart(fig, use_container_width=True)
795
+
796
+ # Route pages
797
+ if page == "Home":
798
+ home_page()
799
+ elif page == "Create Game":
800
+ create_game_page()
801
+ elif page == "Join Game":
802
+ join_game_page()
803
+ elif page == "Play":
804
+ play_page()
805
+ elif page == "Friends":
806
+ friends_page()
807
+ elif page == "Inbox":
808
+ inbox_page()
809
+ elif page == "Leaderboard":
810
+ leaderboard_page()
811
+
812
+ # Resume quick action
813
+ if st.session_state.get('game_id') and st.session_state.get('username'):
814
+ players = unified_get("players") or {}
815
+ info = players.get(st.session_state['game_id'], {}).get(st.session_state['username'], {}) if players.get(st.session_state.get('game_id')) else {}
816
+ if info and info.get('submitted'):
817
+ st.info("You already submitted this game.")
818
+ else:
819
+ with st.expander("Resume Game"):
820
+ if st.button("Go to Play"):
821
+ st.experimental_rerun()
822
+
823
+ st.markdown("---")
824
+ st.write("Notes: Online mode requires firebase-admin and service account JSON named 'serviceAccountKey.json' placed next to app.py. Offline mode stores data in ./data/.")
825
+