awacke1 commited on
Commit
2099bf7
ยท
verified ยท
1 Parent(s): 970291d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -86
app.py CHANGED
@@ -10,8 +10,11 @@ import glob
10
  import base64
11
  import edge_tts
12
  import nest_asyncio
13
- import re # For regular expressions in clean_text_for_tts
14
- import threading # For running WebSocket server in a separate thread
 
 
 
15
  from gradio_client import Client
16
  from streamlit_marquee import streamlit_marquee
17
 
@@ -29,14 +32,14 @@ st.set_page_config(
29
  GAME_NAME = "Rocky Mountain Quest ๐Ÿ”๏ธ๐ŸŽฎ"
30
  START_LOCATION = "Trailhead Camp โ›บ"
31
  CHARACTERS = {
32
- "Trailblazer Tim ๐ŸŒ„": {"voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!"},
33
- "Meme Queen Mia ๐Ÿ˜‚": {"voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!"},
34
- "Elk Whisperer Eve ๐ŸฆŒ": {"voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!"},
35
- "Tech Titan Tara ๐Ÿ’พ": {"voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!"},
36
- "Ski Guru Sam โ›ท๏ธ": {"voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!"},
37
- "Cosmic Camper Cal ๐ŸŒ ": {"voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!"},
38
- "Rasta Ranger Rick ๐Ÿƒ": {"voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!"},
39
- "Boulder Bro Ben ๐Ÿชจ": {"voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!"}
40
  }
41
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
42
 
@@ -55,10 +58,9 @@ def init_session_state():
55
  'server_running': False, 'server_task': None, 'active_connections': {},
56
  'chat_history': [], 'audio_cache': {}, 'last_transcript': "",
57
  'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
58
- 'marquee_settings': {
59
- "background": "#2E8B57", "color": "#FFFFFF", "font-size": "16px",
60
- "animationDuration": "15s", "width": "100%", "lineHeight": "40px"
61
- }
62
  }
63
  for k, v in defaults.items():
64
  if k not in st.session_state:
@@ -131,8 +133,8 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
131
  await broadcast_message(f"{username}|{message}", "quest")
132
  st.session_state.chat_history.append(entry)
133
  st.session_state.last_transcript = message
134
- st.session_state.score += 10 # Points for participation
135
- st.session_state.treasures += 1 # Audio treasure collected
136
  return md_file, audio_file
137
 
138
  async def load_chat():
@@ -154,7 +156,7 @@ async def perform_arxiv_search(query, username):
154
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
155
  return md_file, audio_file
156
 
157
- # WebSocket for Multiplayer
158
  async def websocket_handler(websocket, path):
159
  client_id = str(uuid.uuid4())
160
  room_id = "quest"
@@ -162,16 +164,30 @@ async def websocket_handler(websocket, path):
162
  st.session_state.active_connections[room_id] = {}
163
  st.session_state.active_connections[room_id][client_id] = websocket
164
  username = st.session_state.get('username', random.choice(list(CHARACTERS.keys())))
 
 
 
 
 
 
165
  await save_chat_entry(username, f"๐Ÿ—บ๏ธ Joins the quest at {START_LOCATION}!", CHARACTERS[username]["voice"])
 
166
  try:
167
  async for message in websocket:
168
  if '|' in message:
169
  username, content = message.split('|', 1)
170
  voice = CHARACTERS.get(username, {"voice": "en-US-AriaNeural"})["voice"]
171
- await save_chat_entry(username, content, voice)
172
- await perform_arxiv_search(content, username) # ArXiv response for every chat
 
 
 
 
 
173
  except websockets.ConnectionClosed:
174
  await save_chat_entry(username, "๐Ÿƒ Leaves the quest!", CHARACTERS[username]["voice"])
 
 
175
  finally:
176
  if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
177
  del st.session_state.active_connections[room_id][client_id]
@@ -188,10 +204,22 @@ async def broadcast_message(message, room_id):
188
  if client_id in st.session_state.active_connections[room_id]:
189
  del st.session_state.active_connections[room_id][client_id]
190
 
 
 
 
 
 
 
 
 
 
 
 
191
  async def run_websocket_server():
192
  if not st.session_state.get('server_running', False):
193
  server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
194
  st.session_state['server_running'] = True
 
195
  await server.wait_closed()
196
 
197
  def start_websocket_server():
@@ -199,19 +227,135 @@ def start_websocket_server():
199
  asyncio.set_event_loop(loop)
200
  loop.run_until_complete(run_websocket_server())
201
 
202
- # Game Quest (Mad Libs)
203
- def generate_quest_story(username, inputs):
204
- locations = ["Peak Summit ๐Ÿž๏ธ", "Elk Valley ๐ŸŒฒ", "Meme Cave ๐Ÿ•ณ๏ธ", "Tech Outpost ๐Ÿ’ป"]
205
- st.session_state.location = random.choice(locations)
206
- story = f"""
207
- ๐ŸŒ„ **{username}โ€™s Quest at {st.session_state.location}:**
208
- ๐Ÿ—บ๏ธ {username} discovers {inputs['quantity']} {inputs['plural_noun']}!
209
- ๐ŸŒŸ Theyโ€™re {inputs['adjective']} and {inputs['action']} everywhere.
210
- ๐ŸŽฏ With a {inputs['tool']}, {username} faces a {inputs['creature']}
211
- โšก that {inputs['event']}โ€”shouting โ€œ{inputs['shout']}!โ€
212
- ๐ŸŽ‰ Victory earns {username} {st.session_state.treasures} audio treasures!
213
- """
214
- return story.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  # Main Game Loop
217
  def main():
@@ -226,63 +370,44 @@ def main():
226
 
227
  st.title(f"๐ŸŽฎ {GAME_NAME}")
228
  st.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
229
- chat_text = " ".join([line.split(": ")[-1] for line in asyncio.run(load_chat()) if ": " in line][-5:])
230
- streamlit_marquee(content=f"๐Ÿ”๏ธ {st.session_state.location} | ๐ŸŽ™๏ธ {st.session_state.username} | ๐Ÿ’ฌ {chat_text}",
231
- **st.session_state['marquee_settings'], key="quest_marquee")
232
-
233
- tab_main = st.radio("๐ŸŽฒ Quest Actions:", ["๐Ÿ—ฃ๏ธ Explore & Chat", "๐ŸŽต Treasure Vault", "๐Ÿ—บ๏ธ Quest Challenge"], horizontal=True)
234
-
235
- if tab_main == "๐Ÿ—ฃ๏ธ Explore & Chat":
236
- st.subheader(f"๐Ÿ—ฃ๏ธ Explore {st.session_state.location} ๐ŸŽ™๏ธ")
237
- chat_content = asyncio.run(load_chat())
238
- st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
239
-
240
- message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Explore the wilds! ๐ŸŒฒ")
241
- if st.button("๐ŸŒŸ Send & Explore ๐ŸŽค"):
242
- if message:
243
- voice = CHARACTERS[st.session_state.username]["voice"]
244
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
245
- if audio_file:
246
- play_and_download_audio(audio_file)
247
- st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
248
-
249
- elif tab_main == "๐ŸŽต Treasure Vault":
250
- st.subheader("๐ŸŽต Audio Treasure Vault ๐Ÿ†")
251
- mp3_files = sorted(glob.glob("*.mp3"), key=os.path.getmtime, reverse=True)
252
- if mp3_files:
253
- st.write(f"๐Ÿ… Treasures Collected: {st.session_state.treasures}")
254
- for i, mp3 in enumerate(mp3_files[:10]):
255
- with st.expander(f"๐ŸŽต Treasure #{i+1}: {os.path.basename(mp3)}"):
256
- play_and_download_audio(mp3)
257
- else:
258
- st.write("๐Ÿ” No treasures yetโ€”explore or complete quests to collect audio loot! ๐ŸŽค")
259
-
260
- elif tab_main == "๐Ÿ—บ๏ธ Quest Challenge":
261
- st.subheader("๐Ÿ—บ๏ธ Rocky Mountain Quest Challenge ๐ŸŒŸ")
262
- st.write("Fill in the blanks to embark on a wild adventure!")
263
- inputs = {}
264
- col1, col2 = st.columns(2)
265
- with col1:
266
- inputs['quantity'] = st.text_input("๐Ÿ”ข How Many?", "3", key="quantity")
267
- inputs['plural_noun'] = st.text_input("๐ŸŒ„ Things?", "elk", key="plural_noun")
268
- inputs['adjective'] = st.text_input("โœจ Describe Them?", "wild", key="adjective")
269
- inputs['action'] = st.text_input("๐Ÿƒ What They Do?", "running", key="action")
270
- with col2:
271
- inputs['tool'] = st.text_input("๐Ÿ› ๏ธ Your Tool?", "map", key="tool")
272
- inputs['creature'] = st.text_input("๐Ÿฆ‡ Encounter?", "bear", key="creature")
273
- inputs['event'] = st.text_input("โšก What Happens?", "roars", key="event")
274
- inputs['shout'] = st.text_input("๐Ÿ—ฃ๏ธ Your Cry?", "Yeehaw!", key="shout")
275
-
276
- if st.button("๐ŸŽ‰ Start Quest! ๐ŸŽค"):
277
- story = generate_quest_story(st.session_state.username, inputs)
278
- st.markdown(f"### ๐ŸŒ„ {st.session_state.username}โ€™s Quest Log")
279
- st.write(story)
280
  voice = CHARACTERS[st.session_state.username]["voice"]
281
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, story, voice, True))
282
  if audio_file:
283
  play_and_download_audio(audio_file)
284
- st.session_state.score += 50 # Bonus for quest completion
285
- st.success(f"๐Ÿ† Quest Complete! +50 points! New Score: {st.session_state.score}")
286
 
287
  # Sidebar: Game HUD
288
  st.sidebar.subheader("๐ŸŽฎ Adventurerโ€™s HUD")
@@ -296,6 +421,7 @@ def main():
296
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
297
  st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
298
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
 
299
 
300
  if not st.session_state.get('server_running', False):
301
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
 
10
  import base64
11
  import edge_tts
12
  import nest_asyncio
13
+ import re
14
+ import threading
15
+ import time
16
+ import json
17
+ import streamlit.components.v1 as components
18
  from gradio_client import Client
19
  from streamlit_marquee import streamlit_marquee
20
 
 
32
  GAME_NAME = "Rocky Mountain Quest ๐Ÿ”๏ธ๐ŸŽฎ"
33
  START_LOCATION = "Trailhead Camp โ›บ"
34
  CHARACTERS = {
35
+ "Trailblazer Tim ๐ŸŒ„": {"voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00},
36
+ "Meme Queen Mia ๐Ÿ˜‚": {"voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff},
37
+ "Elk Whisperer Eve ๐ŸฆŒ": {"voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff},
38
+ "Tech Titan Tara ๐Ÿ’พ": {"voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00},
39
+ "Ski Guru Sam โ›ท๏ธ": {"voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500},
40
+ "Cosmic Camper Cal ๐ŸŒ ": {"voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080},
41
+ "Rasta Ranger Rick ๐Ÿƒ": {"voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff},
42
+ "Boulder Bro Ben ๐Ÿชจ": {"voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!", "color": 0xff4500}
43
  }
44
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
45
 
 
58
  'server_running': False, 'server_task': None, 'active_connections': {},
59
  'chat_history': [], 'audio_cache': {}, 'last_transcript': "",
60
  'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
61
+ 'speech_processed': False, 'players': {}, 'last_update': time.time(),
62
+ 'update_interval': 20, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
63
+ 'move_right': False, 'move_up': False, 'move_down': False
 
64
  }
65
  for k, v in defaults.items():
66
  if k not in st.session_state:
 
133
  await broadcast_message(f"{username}|{message}", "quest")
134
  st.session_state.chat_history.append(entry)
135
  st.session_state.last_transcript = message
136
+ st.session_state.score += 10
137
+ st.session_state.treasures += 1
138
  return md_file, audio_file
139
 
140
  async def load_chat():
 
156
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
157
  return md_file, audio_file
158
 
159
+ # WebSocket for Multiplayer with Map Updates
160
  async def websocket_handler(websocket, path):
161
  client_id = str(uuid.uuid4())
162
  room_id = "quest"
 
164
  st.session_state.active_connections[room_id] = {}
165
  st.session_state.active_connections[room_id][client_id] = websocket
166
  username = st.session_state.get('username', random.choice(list(CHARACTERS.keys())))
167
+ st.session_state.players[client_id] = {
168
+ "username": username,
169
+ "x": random.uniform(-20, 20),
170
+ "z": random.uniform(-50, 50),
171
+ "color": CHARACTERS[username]["color"]
172
+ }
173
  await save_chat_entry(username, f"๐Ÿ—บ๏ธ Joins the quest at {START_LOCATION}!", CHARACTERS[username]["voice"])
174
+
175
  try:
176
  async for message in websocket:
177
  if '|' in message:
178
  username, content = message.split('|', 1)
179
  voice = CHARACTERS.get(username, {"voice": "en-US-AriaNeural"})["voice"]
180
+ if content.startswith("MOVE:"):
181
+ _, x, z = content.split(":")
182
+ st.session_state.players[client_id]["x"] = float(x)
183
+ st.session_state.players[client_id]["z"] = float(z)
184
+ else:
185
+ await save_chat_entry(username, content, voice)
186
+ await perform_arxiv_search(content, username)
187
  except websockets.ConnectionClosed:
188
  await save_chat_entry(username, "๐Ÿƒ Leaves the quest!", CHARACTERS[username]["voice"])
189
+ if client_id in st.session_state.players:
190
+ del st.session_state.players[client_id]
191
  finally:
192
  if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
193
  del st.session_state.active_connections[room_id][client_id]
 
204
  if client_id in st.session_state.active_connections[room_id]:
205
  del st.session_state.active_connections[room_id][client_id]
206
 
207
+ async def periodic_update():
208
+ while True:
209
+ if st.session_state.active_connections.get("quest"):
210
+ player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!"
211
+ message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
212
+ player_data = json.dumps(list(st.session_state.players.values()))
213
+ await broadcast_message(f"System|{message}", "quest")
214
+ await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
215
+ await save_chat_entry("System", message, "en-US-AriaNeural")
216
+ await asyncio.sleep(st.session_state.update_interval)
217
+
218
  async def run_websocket_server():
219
  if not st.session_state.get('server_running', False):
220
  server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
221
  st.session_state['server_running'] = True
222
+ asyncio.create_task(periodic_update())
223
  await server.wait_closed()
224
 
225
  def start_websocket_server():
 
227
  asyncio.set_event_loop(loop)
228
  loop.run_until_complete(run_websocket_server())
229
 
230
+ # Game HTML with Map
231
+ html_code = f"""
232
+ <!DOCTYPE html>
233
+ <html lang="en">
234
+ <head>
235
+ <meta charset="UTF-8">
236
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
237
+ <title>Rocky Mountain Quest Map</title>
238
+ <style>
239
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
240
+ #gameContainer {{ width: 800px; height: 600px; position: relative; }}
241
+ canvas {{ width: 100%; height: 100%; display: block; }}
242
+ #chatBox {{
243
+ position: absolute; bottom: 10px; left: 10px; width: 300px; height: 150px;
244
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
245
+ border-radius: 5px; overflow-y: auto;
246
+ }}
247
+ #status {{
248
+ position: absolute; top: 10px; left: 10px; color: white;
249
+ background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
250
+ }}
251
+ </style>
252
+ </head>
253
+ <body>
254
+ <div id="gameContainer">
255
+ <div id="status">Players: 1</div>
256
+ <div id="chatBox"></div>
257
+ </div>
258
+
259
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
260
+ <script>
261
+ const playerName = "{st.session_state.username}";
262
+ let ws = new WebSocket('ws://localhost:8765');
263
+ const scene = new THREE.Scene();
264
+ const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
265
+ camera.position.set(0, 50, 50);
266
+ camera.lookAt(0, 0, 0);
267
+
268
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
269
+ renderer.setSize(800, 600);
270
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
271
+
272
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
273
+ scene.add(ambientLight);
274
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
275
+ sunLight.position.set(50, 50, 50);
276
+ scene.add(sunLight);
277
+
278
+ const groundGeometry = new THREE.PlaneGeometry(100, 100);
279
+ const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
280
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
281
+ ground.rotation.x = -Math.PI / 2;
282
+ scene.add(ground);
283
+
284
+ let players = {{}};
285
+ const playerMeshes = {{}};
286
+
287
+ function updatePlayers(playerData) {{
288
+ playerData.forEach(player => {{
289
+ if (!playerMeshes[player.username]) {{
290
+ const geometry = new THREE.BoxGeometry(2, 2, 2);
291
+ const material = new THREE.MeshPhongMaterial({{ color: player.color }});
292
+ const mesh = new THREE.Mesh(geometry, material);
293
+ scene.add(mesh);
294
+ playerMeshes[player.username] = mesh;
295
+ }}
296
+ const mesh = playerMeshes[player.username];
297
+ mesh.position.set(player.x, 1, player.z);
298
+ }});
299
+ document.getElementById('status').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
300
+ }}
301
+
302
+ document.addEventListener('keydown', (event) => {{
303
+ const speed = 2;
304
+ switch (event.code) {{
305
+ case 'ArrowLeft': case 'KeyA':
306
+ st.session_state.move_left = true;
307
+ st.session_state.x_pos -= speed;
308
+ ws.send(`{playerName}|MOVE:${{st.session_state.x_pos}}:${{st.session_state.z_pos}}`);
309
+ break;
310
+ case 'ArrowRight': case 'KeyD':
311
+ st.session_state.move_right = true;
312
+ st.session_state.x_pos += speed;
313
+ ws.send(`{playerName}|MOVE:${{st.session_state.x_pos}}:${{st.session_state.z_pos}}`);
314
+ break;
315
+ case 'ArrowUp': case 'KeyW':
316
+ st.session_state.move_up = true;
317
+ st.session_state.z_pos -= speed;
318
+ ws.send(`{playerName}|MOVE:${{st.session_state.x_pos}}:${{st.session_state.z_pos}}`);
319
+ break;
320
+ case 'ArrowDown': case 'KeyS':
321
+ st.session_state.move_down = true;
322
+ st.session_state.z_pos += speed;
323
+ ws.send(`{playerName}|MOVE:${{st.session_state.x_pos}}:${{st.session_state.z_pos}}`);
324
+ break;
325
+ }}
326
+ }});
327
+
328
+ document.addEventListener('keyup', (event) => {{
329
+ switch (event.code) {{
330
+ case 'ArrowLeft': case 'KeyA': st.session_state.move_left = false; break;
331
+ case 'ArrowRight': case 'KeyD': st.session_state.move_right = false; break;
332
+ case 'ArrowUp': case 'KeyW': st.session_state.move_up = false; break;
333
+ case 'ArrowDown': case 'KeyS': st.session_state.move_down = false; break;
334
+ }}
335
+ }});
336
+
337
+ ws.onmessage = function(event) {{
338
+ const data = event.data;
339
+ if (data.startsWith('MAP_UPDATE:')) {{
340
+ const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
341
+ updatePlayers(playerData);
342
+ }} else {{
343
+ const [sender, message] = data.split('|');
344
+ const chatBox = document.getElementById('chatBox');
345
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
346
+ chatBox.scrollTop = chatBox.scrollHeight;
347
+ }}
348
+ }};
349
+
350
+ function animate() {{
351
+ requestAnimationFrame(animate);
352
+ renderer.render(scene, camera);
353
+ }}
354
+ animate();
355
+ </script>
356
+ </body>
357
+ </html>
358
+ """
359
 
360
  # Main Game Loop
361
  def main():
 
370
 
371
  st.title(f"๐ŸŽฎ {GAME_NAME}")
372
  st.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
373
+
374
+ # Countdown Timer
375
+ elapsed = time.time() - st.session_state.last_update
376
+ remaining = max(0, st.session_state.update_interval - elapsed)
377
+ st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
378
+ if remaining <= 0:
379
+ st.session_state.last_update = time.time()
380
+ st.rerun()
381
+
382
+ # Voice Input Component
383
+ mycomponent = components.html(open("speech.html").read(), height=400) # Assuming speech.html is in the same directory
384
+ val = st.session_state.get('speech_input', '')
385
+ if val and val != st.session_state.last_transcript:
386
+ val_stripped = val.strip().replace('\n', ' ')
387
+ if val_stripped:
388
+ voice = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"]
389
+ st.session_state['speech_processed'] = True
390
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
391
+ if audio_file:
392
+ play_and_download_audio(audio_file)
393
+ st.session_state.speech_input = ''
394
+ st.rerun()
395
+
396
+ # Render Map
397
+ components.html(html_code, width=800, height=600)
398
+
399
+ # Chat Interface
400
+ chat_content = asyncio.run(load_chat())
401
+ st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
402
+
403
+ message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
404
+ if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
405
+ if message:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  voice = CHARACTERS[st.session_state.username]["voice"]
407
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
408
  if audio_file:
409
  play_and_download_audio(audio_file)
410
+ st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
 
411
 
412
  # Sidebar: Game HUD
413
  st.sidebar.subheader("๐ŸŽฎ Adventurerโ€™s HUD")
 
421
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
422
  st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
423
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
424
+ st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
425
 
426
  if not st.session_state.get('server_running', False):
427
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)