awacke1 commited on
Commit
564d6f6
ยท
verified ยท
1 Parent(s): 3340884

Update backup2.app.py

Browse files
Files changed (1) hide show
  1. backup2.app.py +268 -119
backup2.app.py CHANGED
@@ -18,6 +18,10 @@ from gradio_client import Client
18
  from streamlit_marquee import streamlit_marquee
19
  import folium
20
  from streamlit_folium import folium_static
 
 
 
 
21
 
22
  # Patch asyncio for nesting
23
  nest_asyncio.apply()
@@ -26,25 +30,37 @@ nest_asyncio.apply()
26
  st.set_page_config(
27
  layout="wide",
28
  page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
29
- page_icon="๐ŸฆŒ"
 
 
 
 
 
 
30
  )
31
 
32
  # Game Config
33
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
34
  START_LOCATION = "Trailhead Camp โ›บ"
35
- CHARACTERS = [
36
- {"name": "Trailblazer Tim ๐ŸŒ„", "voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00},
37
- {"name": "Meme Queen Mia ๐Ÿ˜‚", "voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff},
38
- {"name": "Elk Whisperer Eve ๐ŸฆŒ", "voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff},
39
- {"name": "Tech Titan Tara ๐Ÿ’พ", "voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00},
40
- {"name": "Ski Guru Sam โ›ท๏ธ", "voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500},
41
- {"name": "Cosmic Camper Cal ๐ŸŒ ", "voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080},
42
- {"name": "Rasta Ranger Rick ๐Ÿƒ", "voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff},
43
- {"name": "Boulder Bro Ben ๐Ÿชจ", "voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!", "color": 0xff4500}
 
44
  ]
45
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
46
-
47
- # Prairie Simulator Locations
 
 
 
 
 
48
  PRAIRIE_LOCATIONS = {
49
  "Deadwood, SD": (44.3769, -103.7298),
50
  "Wind Cave National Park": (43.6047, -103.4798),
@@ -61,7 +77,7 @@ STATE_FILE = "user_state.txt"
61
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
62
  GAME_STATE_FILE = "game_state.json"
63
 
64
- # Cached Game State as "ML Model" using st.cache_resource
65
  @st.cache_resource
66
  def load_game_state(_timestamp):
67
  """Load or initialize the game state, treated as a cached resource."""
@@ -78,6 +94,7 @@ def load_game_state(_timestamp):
78
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
79
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
80
  ],
 
81
  "history": []
82
  }
83
  with open(GAME_STATE_FILE, 'w') as f:
@@ -86,9 +103,10 @@ def load_game_state(_timestamp):
86
 
87
  def update_game_state(state):
88
  """Update the game state and persist to file."""
 
89
  with open(GAME_STATE_FILE, 'w') as f:
90
  json.dump(state, f)
91
- load_game_state.clear() # Clear cache to force reload
92
  return load_game_state(time.time())
93
 
94
  def reset_game_state():
@@ -102,13 +120,53 @@ def reset_game_state():
102
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
103
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
104
  ],
 
105
  "history": []
106
  }
107
  return update_game_state(state)
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  # Helpers
110
  def format_timestamp(username=""):
111
- now = datetime.now().strftime("%Y%m%d_%H%M%S")
112
  return f"{now}-by-{username}"
113
 
114
  def clean_text_for_tts(text):
@@ -116,8 +174,8 @@ def clean_text_for_tts(text):
116
 
117
  def generate_filename(prompt, username, file_type="md"):
118
  timestamp = format_timestamp(username)
119
- hash_val = hashlib.md5(prompt.encode()).hexdigest()[:8]
120
- return f"{timestamp}-{hash_val}.{file_type}"
121
 
122
  def create_file(prompt, username, file_type="md"):
123
  filename = generate_filename(prompt, username, file_type)
@@ -147,7 +205,7 @@ async def async_edge_tts_generate(text, voice, username):
147
  if cache_key in st.session_state['audio_cache']:
148
  return st.session_state['audio_cache'][cache_key]
149
  text = clean_text_for_tts(text)
150
- filename = f"{AUDIO_DIR}/{format_timestamp(username)}-{hashlib.md5(text.encode()).hexdigest()[:8]}.mp3"
151
  communicate = edge_tts.Communicate(text, voice)
152
  await communicate.save(filename)
153
  if os.path.exists(filename) and os.path.getsize(filename) > 0:
@@ -157,7 +215,7 @@ async def async_edge_tts_generate(text, voice, username):
157
 
158
  def play_and_download_audio(file_path):
159
  if file_path and os.path.exists(file_path):
160
- st.audio(file_path)
161
  st.markdown(get_download_link(file_path), unsafe_allow_html=True)
162
 
163
  # WebSocket Broadcast
@@ -184,14 +242,25 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
184
  f.write(f"{entry}\n")
185
  audio_file = await async_edge_tts_generate(message, voice, username)
186
  await broadcast_message(f"{username}|{message}", "quest")
187
- st.session_state.chat_history.append(entry)
188
  st.session_state.last_transcript = message
189
  game_state = load_game_state(st.session_state.game_state_timestamp)
190
- game_state["history"].append(entry)
191
  if username in game_state["players"]:
192
  game_state["players"][username]["score"] += 10
193
  game_state["players"][username]["treasures"] += 1
194
  game_state["players"][username]["last_active"] = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
195
  update_game_state(game_state)
196
  return md_file, audio_file
197
 
@@ -212,28 +281,24 @@ def init_session_state():
212
  'speech_processed': False, 'players': {}, 'last_update': time.time(),
213
  'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
214
  'move_right': False, 'move_up': False, 'move_down': False,
215
- 'prairie_players': {}, 'last_chat_update': 0,
216
- 'game_state_timestamp': time.time(), 'name_index': 0,
217
- 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time()
 
218
  }
219
  for k, v in defaults.items():
220
  if k not in st.session_state:
221
  st.session_state[k] = v
222
  if st.session_state.username is None:
223
- st.session_state.name_index = (st.session_state.name_index + 1) % len(CHARACTERS)
224
- st.session_state.username = CHARACTERS[st.session_state.name_index]["name"]
225
- asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Joins the Rocky Mountain Quest!", CHARACTERS[st.session_state.name_index]["voice"]))
226
- save_username(st.session_state.username)
227
  game_state = load_game_state(st.session_state.game_state_timestamp)
228
  if st.session_state.username not in game_state["players"]:
229
- game_state["players"][st.session_state.username] = {
230
- "x": random.uniform(-20, 20),
231
- "z": random.uniform(-40, 40),
232
- "color": CHARACTERS[st.session_state.name_index]["color"],
233
- "score": 0,
234
- "treasures": 0,
235
- "last_active": time.time()
236
- }
237
  update_game_state(game_state)
238
 
239
  init_session_state()
@@ -249,23 +314,19 @@ async def websocket_handler(websocket, path):
249
 
250
  game_state = load_game_state(st.session_state.game_state_timestamp)
251
  if "prairie" in path:
 
252
  st.session_state.prairie_players[client_id] = {
253
  "username": username,
254
  "animal": "prairie_dog",
255
  "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
256
- "color": CHARACTERS[st.session_state.name_index]["color"]
257
  }
258
  await broadcast_message(f"System|{username} joins the prairie!", room_id)
259
  else:
260
  if username not in game_state["players"]:
261
- game_state["players"][username] = {
262
- "x": random.uniform(-20, 20),
263
- "z": random.uniform(-40, 40),
264
- "color": CHARACTERS[st.session_state.name_index]["color"],
265
- "score": 0,
266
- "treasures": 0,
267
- "last_active": time.time()
268
- }
269
  update_game_state(game_state)
270
  st.session_state.players[client_id] = game_state["players"][username]
271
  await broadcast_message(f"System|{username} joins the quest!", room_id)
@@ -274,35 +335,13 @@ async def websocket_handler(websocket, path):
274
  async for message in websocket:
275
  if '|' in message:
276
  sender, content = message.split('|', 1)
277
- voice = next(c["voice"] for c in CHARACTERS if c["name"] == sender)
278
  game_state = load_game_state(st.session_state.game_state_timestamp)
279
- if content.startswith("MOVE:"):
280
- _, x, z = content.split(":")
281
- x, z = float(x), float(z)
282
- st.session_state.players[client_id]["x"] = x
283
- st.session_state.players[client_id]["z"] = z
284
- game_state["players"][sender]["x"] = x
285
- game_state["players"][sender]["z"] = z
286
- game_state["players"][sender]["last_active"] = time.time()
287
- update_game_state(game_state)
288
- elif content.startswith("SCORE:"):
289
- score = int(content.split(":")[1])
290
- st.session_state.players[client_id]["score"] = score
291
- game_state["players"][sender]["score"] = score
292
- game_state["players"][sender]["last_active"] = time.time()
293
- update_game_state(game_state)
294
- elif content.startswith("TREASURE:"):
295
- treasures = int(content.split(":")[1])
296
- st.session_state.players[client_id]["treasures"] = treasures
297
- game_state["players"][sender]["treasures"] = treasures
298
- game_state["players"][sender]["last_active"] = time.time()
299
- for i, t in enumerate(game_state["treasures"]):
300
- if abs(st.session_state.players[client_id]["x"] - t["x"]) < 2 and \
301
- abs(st.session_state.players[client_id]["z"] - t["z"]) < 2:
302
- game_state["treasures"].pop(i)
303
- break
304
- update_game_state(game_state)
305
- elif content.startswith("PRAIRIE:"):
306
  action, value = content.split(":", 1)
307
  if action == "ANIMAL":
308
  st.session_state.prairie_players[client_id]["animal"] = value
@@ -314,6 +353,7 @@ async def websocket_handler(websocket, path):
314
  else:
315
  await save_chat_entry(sender, content, voice)
316
  await perform_arxiv_search(content, sender)
 
317
  except websockets.ConnectionClosed:
318
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
319
  if client_id in st.session_state.players:
@@ -328,12 +368,13 @@ async def periodic_update():
328
  while True:
329
  if st.session_state.active_connections.get("quest"):
330
  game_state = load_game_state(st.session_state.game_state_timestamp)
331
- # Remove inactive players (timeout after 60 seconds)
332
  current_time = time.time()
333
  inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout]
334
  for player in inactive_players:
335
  del game_state["players"][player]
336
  await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
 
 
337
  update_game_state(game_state)
338
 
339
  player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
@@ -371,10 +412,49 @@ async def perform_arxiv_search(query, username):
371
  query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
372
  )[0]
373
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
374
- voice = next(c["voice"] for c in CHARACTERS if c["name"] == username)
375
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
376
  return md_file, audio_file
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  # Enhanced 3D Game HTML
379
  rocky_map_html = f"""
380
  <!DOCTYPE html>
@@ -387,31 +467,41 @@ rocky_map_html = f"""
387
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
388
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
389
  canvas {{ width: 100%; height: 100%; display: block; }}
390
- #ui {{
391
- position: absolute; top: 10px; left: 10px; color: white;
392
- background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
 
393
  }}
394
- #chatBox {{
395
- position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
396
- background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
397
- border-radius: 5px; overflow-y: auto;
398
  }}
399
- #controls {{
400
- position: absolute; bottom: 10px; left: 10px; color: white;
401
- background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
 
 
 
 
 
402
  }}
403
  </style>
404
  </head>
405
  <body>
406
  <div id="gameContainer">
407
- <div id="ui">
408
- <div id="players">Players: 1</div>
409
  <div id="score">Score: 0</div>
410
  <div id="treasures">Treasures: 0</div>
411
  <div id="timeout">Timeout: 60s</div>
412
  </div>
 
413
  <div id="chatBox"></div>
414
- <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
 
 
 
415
  </div>
416
 
417
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
@@ -445,19 +535,30 @@ rocky_map_html = f"""
445
  const playerMeshes = {{}};
446
  let treasures = [];
447
  const treasureMeshes = {{}};
 
 
448
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
449
  let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
450
 
451
- // Player initialization
452
- const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
453
- const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in CHARACTERS if c["name"] == st.session_state.username)} }});
454
- const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
 
 
 
 
 
 
 
 
 
 
455
  playerMesh.position.set(xPos, 1, zPos);
456
  playerMesh.castShadow = true;
457
  scene.add(playerMesh);
458
  players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
459
 
460
- // Treasure updating
461
  function updateTreasures(treasureData) {{
462
  treasureData.forEach(t => {{
463
  if (!treasureMeshes[t.id]) {{
@@ -483,7 +584,21 @@ rocky_map_html = f"""
483
  }});
484
  }}
485
 
486
- // Controls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  document.addEventListener('keydown', (event) => {{
488
  switch (event.code) {{
489
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
@@ -532,12 +647,16 @@ rocky_map_html = f"""
532
  camera.lookAt(xPos, 0, zPos);
533
  const timeout = 60 - (performance.now() / 1000 - lastActive);
534
  document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
 
 
535
  }}
536
 
537
  function updatePlayers(playerData) {{
538
  playerData.forEach(player => {{
539
  if (!playerMeshes[player.username]) {{
540
- const mesh = new THREE.Mesh(playerGeometry, new THREE.MeshPhongMaterial({{ color: player.color }}));
 
 
541
  mesh.castShadow = true;
542
  scene.add(mesh);
543
  playerMeshes[player.username] = mesh;
@@ -554,8 +673,10 @@ rocky_map_html = f"""
554
  }}
555
  }});
556
  document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
557
- document.getElementById('score').textContent = `Score: ${{score}}`;
558
- document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
 
 
559
  }}
560
 
561
  ws.onmessage = function(event) {{
@@ -572,6 +693,7 @@ rocky_map_html = f"""
572
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
573
  updatePlayers(gameState.players);
574
  updateTreasures(gameState.treasures);
 
575
  }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
576
  const [sender, message] = data.split('|');
577
  const chatBox = document.getElementById('chatBox');
@@ -589,6 +711,7 @@ rocky_map_html = f"""
589
 
590
  updatePlayer(delta);
591
  treasures.forEach(t => t.rotation.y += delta);
 
592
  renderer.render(scene, camera);
593
  }}
594
 
@@ -600,41 +723,65 @@ rocky_map_html = f"""
600
 
601
  # Main Game Loop
602
  def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
604
- st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
605
- st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in CHARACTERS if c['name'] == st.session_state.username)}")
606
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
607
- st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
608
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
609
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
610
 
611
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
612
- if st.sidebar.button("Reset World"):
613
  reset_game_state()
614
  st.session_state.game_state_timestamp = time.time()
615
  st.rerun()
616
 
 
 
 
 
 
 
 
 
 
617
  left_col, right_col = st.columns([2, 1])
618
 
619
  with left_col:
620
  components.html(rocky_map_html, width=800, height=600)
621
  chat_content = asyncio.run(load_chat())
622
  st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
623
- message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
624
- if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
625
- if message:
626
- voice = next(c["voice"] for c in CHARACTERS if c["name"] == st.session_state.username)
627
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
628
- if audio_file:
629
- play_and_download_audio(audio_file)
630
- st.session_state.last_activity = time.time()
631
- st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
 
 
632
  mycomponent = components.declare_component("speech_component", path="./speech_component")
633
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
634
  if val and val != st.session_state.last_transcript:
635
  val_stripped = val.strip().replace('\n', ' ')
636
  if val_stripped:
637
- voice = next(c["voice"] for c in CHARACTERS if c["name"] == st.session_state.username)
638
  st.session_state['speech_processed'] = True
639
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
640
  if audio_file:
@@ -648,14 +795,14 @@ def main():
648
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
649
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
650
  game_state = load_game_state(st.session_state.game_state_timestamp)
651
- for player in game_state["players"].values():
652
  folium.CircleMarker(
653
- location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01], # Approximate mapping to lat/lon
654
  radius=5,
655
  color=f"#{player['color']:06x}",
656
  fill=True,
657
  fill_opacity=0.7,
658
- popup=f"{player['username']} (Score: {player['score']}, Treasures: {player['treasures']})"
659
  ).add_to(prairie_map)
660
  for client_id, player in st.session_state.prairie_players.items():
661
  folium.CircleMarker(
@@ -668,9 +815,9 @@ def main():
668
  ).add_to(prairie_map)
669
  folium_static(prairie_map, width=600, height=400)
670
 
671
- animal = st.selectbox("Choose Animal", ["prairie_dog", "deer", "sheep", "groundhog"])
672
- location = st.selectbox("Move to", list(PRAIRIE_LOCATIONS.keys()))
673
- if st.button("Move"):
674
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
675
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
676
  st.session_state.last_activity = time.time()
@@ -680,9 +827,8 @@ def main():
680
  remaining = max(0, st.session_state.update_interval - elapsed)
681
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
682
 
683
- # Timeout and Auto-Refresh Logic
684
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
685
- st.sidebar.warning("Timed out! Refreshing in 5 seconds...")
686
  time.sleep(5)
687
  st.session_state.game_state_timestamp = time.time()
688
  st.rerun()
@@ -698,5 +844,8 @@ def main():
698
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
699
  st.session_state.server_task.start()
700
 
 
 
 
701
  if __name__ == "__main__":
702
  main()
 
18
  from streamlit_marquee import streamlit_marquee
19
  import folium
20
  from streamlit_folium import folium_static
21
+ import glob
22
+ import pytz
23
+ from collections import defaultdict
24
+ import pandas as pd
25
 
26
  # Patch asyncio for nesting
27
  nest_asyncio.apply()
 
30
  st.set_page_config(
31
  layout="wide",
32
  page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
33
+ page_icon="๐ŸฆŒ",
34
+ initial_sidebar_state="auto",
35
+ menu_items={
36
+ 'Get Help': 'https://huggingface.co/awacke1',
37
+ 'Report a bug': 'https://huggingface.co/spaces/awacke1',
38
+ 'About': "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
39
+ }
40
  )
41
 
42
  # Game Config
43
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
  START_LOCATION = "Trailhead Camp โ›บ"
45
+ EDGE_TTS_VOICES = [
46
+ {"name": "Aria ๐ŸŒธ", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4, "shape": "sphere"},
47
+ {"name": "Guy ๐ŸŒŸ", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00, "shape": "cube"},
48
+ {"name": "Jenny ๐ŸŽถ", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00, "shape": "cylinder"},
49
+ {"name": "Sonia ๐ŸŒบ", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF, "shape": "cone"},
50
+ {"name": "Ryan ๐Ÿ› ๏ธ", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF, "shape": "torus"},
51
+ {"name": "Natasha ๐ŸŒŒ", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080, "shape": "dodecahedron"},
52
+ {"name": "William ๐ŸŽป", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500, "shape": "octahedron"},
53
+ {"name": "Clara ๐ŸŒท", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500, "shape": "tetrahedron"},
54
+ {"name": "Liam ๐ŸŒŸ", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700, "shape": "icosahedron"}
55
  ]
56
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
57
+ WORLD_OBJECTS = [
58
+ {"type": "๐ŸŒฟ Plants", "emoji": "๐ŸŒฑ๐ŸŒฟ๐ŸŒพ", "color": 0x228B22, "shape": "cylinder", "count": 10},
59
+ {"type": "๐Ÿฆ Animal Flocks", "emoji": "๐Ÿ•Š๏ธ๐Ÿฆ๐Ÿค", "color": 0x87CEEB, "shape": "sphere", "count": 5},
60
+ {"type": "๐Ÿ”๏ธ Mountains", "emoji": "๐Ÿ”๏ธโ›ฐ๏ธ", "color": 0x808080, "shape": "cone", "count": 3},
61
+ {"type": "๐ŸŒณ Trees", "emoji": "๐ŸŒฒ๐ŸŒณ๐ŸŒด", "color": 0x006400, "shape": "cylinder", "count": 8},
62
+ {"type": "๐Ÿพ Animals", "emoji": "๐Ÿป๐Ÿบ๐ŸฆŒ", "color": 0x8B4513, "shape": "cube", "count": 6}
63
+ ]
64
  PRAIRIE_LOCATIONS = {
65
  "Deadwood, SD": (44.3769, -103.7298),
66
  "Wind Cave National Park": (43.6047, -103.4798),
 
77
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
78
  GAME_STATE_FILE = "game_state.json"
79
 
80
+ # Cached Game State as "Resource"
81
  @st.cache_resource
82
  def load_game_state(_timestamp):
83
  """Load or initialize the game state, treated as a cached resource."""
 
94
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
95
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
96
  ],
97
+ "world_objects": [],
98
  "history": []
99
  }
100
  with open(GAME_STATE_FILE, 'w') as f:
 
103
 
104
  def update_game_state(state):
105
  """Update the game state and persist to file."""
106
+ state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
107
  with open(GAME_STATE_FILE, 'w') as f:
108
  json.dump(state, f)
109
+ load_game_state.clear()
110
  return load_game_state(time.time())
111
 
112
  def reset_game_state():
 
120
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
121
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
122
  ],
123
+ "world_objects": [],
124
  "history": []
125
  }
126
  return update_game_state(state)
127
 
128
+ # Agent Class for Players
129
+ class PlayerAgent:
130
+ def __init__(self, username, char_data):
131
+ self.username = username
132
+ self.x = random.uniform(-20, 20)
133
+ self.z = random.uniform(-40, 40)
134
+ self.color = char_data["color"]
135
+ self.shape = char_data["shape"]
136
+ self.score = 0
137
+ self.treasures = 0
138
+ self.last_active = time.time()
139
+ self.voice = char_data["voice"]
140
+
141
+ def to_dict(self):
142
+ return {
143
+ "username": self.username,
144
+ "x": self.x,
145
+ "z": self.z,
146
+ "color": self.color,
147
+ "shape": self.shape,
148
+ "score": self.score,
149
+ "treasures": self.treasures,
150
+ "last_active": self.last_active
151
+ }
152
+
153
+ def update_from_message(self, message):
154
+ if '|' in message:
155
+ _, content = message.split('|', 1)
156
+ if content.startswith("MOVE:"):
157
+ _, x, z = content.split(":")
158
+ self.x, self.z = float(x), float(z)
159
+ self.last_active = time.time()
160
+ elif content.startswith("SCORE:"):
161
+ self.score = int(content.split(":")[1])
162
+ self.last_active = time.time()
163
+ elif content.startswith("TREASURE:"):
164
+ self.treasures = int(content.split(":")[1])
165
+ self.last_active = time.time()
166
+
167
  # Helpers
168
  def format_timestamp(username=""):
169
+ now = datetime.now(pytz.timezone('US/Central')).strftime("%Y%m%d_%H%M%S")
170
  return f"{now}-by-{username}"
171
 
172
  def clean_text_for_tts(text):
 
174
 
175
  def generate_filename(prompt, username, file_type="md"):
176
  timestamp = format_timestamp(username)
177
+ cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_")
178
+ return f"{cleaned_prompt}_{timestamp}.{file_type}"
179
 
180
  def create_file(prompt, username, file_type="md"):
181
  filename = generate_filename(prompt, username, file_type)
 
205
  if cache_key in st.session_state['audio_cache']:
206
  return st.session_state['audio_cache'][cache_key]
207
  text = clean_text_for_tts(text)
208
+ filename = generate_filename(text, username, "mp3")
209
  communicate = edge_tts.Communicate(text, voice)
210
  await communicate.save(filename)
211
  if os.path.exists(filename) and os.path.getsize(filename) > 0:
 
215
 
216
  def play_and_download_audio(file_path):
217
  if file_path and os.path.exists(file_path):
218
+ st.audio(file_path, start_time=0)
219
  st.markdown(get_download_link(file_path), unsafe_allow_html=True)
220
 
221
  # WebSocket Broadcast
 
242
  f.write(f"{entry}\n")
243
  audio_file = await async_edge_tts_generate(message, voice, username)
244
  await broadcast_message(f"{username}|{message}", "quest")
245
+ st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file})
246
  st.session_state.last_transcript = message
247
  game_state = load_game_state(st.session_state.game_state_timestamp)
 
248
  if username in game_state["players"]:
249
  game_state["players"][username]["score"] += 10
250
  game_state["players"][username]["treasures"] += 1
251
  game_state["players"][username]["last_active"] = time.time()
252
+ game_state["history"].append(entry)
253
+ if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]:
254
+ obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower())
255
+ for _ in range(obj_type["count"]):
256
+ game_state["world_objects"].append({
257
+ "type": obj_type["type"],
258
+ "emoji": random.choice(obj_type["emoji"].split()),
259
+ "x": random.uniform(-40, 40),
260
+ "z": random.uniform(-40, 40),
261
+ "color": obj_type["color"],
262
+ "shape": obj_type["shape"]
263
+ })
264
  update_game_state(game_state)
265
  return md_file, audio_file
266
 
 
281
  'speech_processed': False, 'players': {}, 'last_update': time.time(),
282
  'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
283
  'move_right': False, 'move_up': False, 'move_down': False,
284
+ 'prairie_players': {}, 'last_chat_update': 0, 'game_state_timestamp': time.time(),
285
+ 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time(),
286
+ 'marquee_settings': {"background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px", "animationDuration": "20s", "width": "100%", "lineHeight": "35px"},
287
+ 'operation_timings': {}, 'performance_metrics': defaultdict(list), 'download_link_cache': {}
288
  }
289
  for k, v in defaults.items():
290
  if k not in st.session_state:
291
  st.session_state[k] = v
292
  if st.session_state.username is None:
293
+ char = random.choice(EDGE_TTS_VOICES)
294
+ st.session_state.username = char["name"]
295
+ st.session_state.tts_voice = char["voice"]
296
+ asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ—บ๏ธ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"]))
297
  game_state = load_game_state(st.session_state.game_state_timestamp)
298
  if st.session_state.username not in game_state["players"]:
299
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
300
+ agent = PlayerAgent(st.session_state.username, char)
301
+ game_state["players"][st.session_state.username] = agent.to_dict()
 
 
 
 
 
302
  update_game_state(game_state)
303
 
304
  init_session_state()
 
314
 
315
  game_state = load_game_state(st.session_state.game_state_timestamp)
316
  if "prairie" in path:
317
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
318
  st.session_state.prairie_players[client_id] = {
319
  "username": username,
320
  "animal": "prairie_dog",
321
  "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
322
+ "color": char["color"]
323
  }
324
  await broadcast_message(f"System|{username} joins the prairie!", room_id)
325
  else:
326
  if username not in game_state["players"]:
327
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
328
+ agent = PlayerAgent(username, char)
329
+ game_state["players"][username] = agent.to_dict()
 
 
 
 
 
330
  update_game_state(game_state)
331
  st.session_state.players[client_id] = game_state["players"][username]
332
  await broadcast_message(f"System|{username} joins the quest!", room_id)
 
335
  async for message in websocket:
336
  if '|' in message:
337
  sender, content = message.split('|', 1)
338
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender)
339
  game_state = load_game_state(st.session_state.game_state_timestamp)
340
+ agent = PlayerAgent(sender, next(c for c in EDGE_TTS_VOICES if c["name"] == sender))
341
+ agent.update_from_message(f"{sender}|{content}")
342
+ if sender in game_state["players"]:
343
+ game_state["players"][sender] = agent.to_dict()
344
+ if content.startswith("PRAIRIE:"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  action, value = content.split(":", 1)
346
  if action == "ANIMAL":
347
  st.session_state.prairie_players[client_id]["animal"] = value
 
353
  else:
354
  await save_chat_entry(sender, content, voice)
355
  await perform_arxiv_search(content, sender)
356
+ update_game_state(game_state)
357
  except websockets.ConnectionClosed:
358
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
359
  if client_id in st.session_state.players:
 
368
  while True:
369
  if st.session_state.active_connections.get("quest"):
370
  game_state = load_game_state(st.session_state.game_state_timestamp)
 
371
  current_time = time.time()
372
  inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout]
373
  for player in inactive_players:
374
  del game_state["players"][player]
375
  await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
376
+ for username in game_state["players"]:
377
+ game_state["players"][username]["score"] += int((current_time - game_state["players"][username]["last_active"]) * 60) # $1 per second
378
  update_game_state(game_state)
379
 
380
  player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
 
412
  query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
413
  )[0]
414
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
415
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
416
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
417
  return md_file, audio_file
418
 
419
+ # Sidebar Functions
420
+ def update_marquee_settings_ui():
421
+ st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
422
+ cols = st.sidebar.columns(2)
423
+ with cols[0]:
424
+ bg_color = st.color_picker("๐ŸŽจ Background", st.session_state['marquee_settings']["background"], key="bg_color_picker")
425
+ text_color = st.color_picker("โœ๏ธ Text", st.session_state['marquee_settings']["color"], key="text_color_picker")
426
+ with cols[1]:
427
+ font_size = st.slider("๐Ÿ“ Size", 10, 24, 14, key="font_size_slider")
428
+ duration = st.slider("โฑ๏ธ Speed (secs)", 1, 20, 20, key="duration_slider")
429
+ st.session_state['marquee_settings'].update({
430
+ "background": bg_color,
431
+ "color": text_color,
432
+ "font-size": f"{font_size}px",
433
+ "animationDuration": f"{duration}s"
434
+ })
435
+
436
+ def display_file_history_in_sidebar():
437
+ st.sidebar.markdown("---")
438
+ st.sidebar.markdown("### ๐Ÿ“‚ Chat Gallery")
439
+ chat_files = sorted(glob.glob("*.mp3"), key=os.path.getmtime, reverse=True)
440
+ if not chat_files:
441
+ st.sidebar.write("No chat audio files found.")
442
+ return
443
+ for audio_file in chat_files[:10]:
444
+ with st.sidebar.expander(os.path.basename(audio_file)):
445
+ st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}")
446
+ play_and_download_audio(audio_file)
447
+
448
+ def log_performance_metrics():
449
+ st.sidebar.markdown("### โฑ๏ธ Performance Metrics")
450
+ metrics = st.session_state['operation_timings']
451
+ if metrics:
452
+ total_time = sum(metrics.values())
453
+ st.sidebar.write(f"**Total Processing Time:** {total_time:.2f}s")
454
+ for operation, duration in metrics.items():
455
+ percentage = (duration / total_time) * 100
456
+ st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
457
+
458
  # Enhanced 3D Game HTML
459
  rocky_map_html = f"""
460
  <!DOCTYPE html>
 
467
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
468
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
469
  canvas {{ width: 100%; height: 100%; display: block; }}
470
+ .ui-container {{
471
+ position: absolute; top: 10px; left: 10px; color: white;
472
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
473
+ user-select: none;
474
  }}
475
+ .leaderboard {{
476
+ position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white;
477
+ background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px;
478
+ text-align: center;
479
  }}
480
+ .controls {{
481
+ position: absolute; bottom: 10px; left: 10px; color: white;
482
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
483
+ }}
484
+ #chatBox {{
485
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
486
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
487
+ border-radius: 5px; overflow-y: auto;
488
  }}
489
  </style>
490
  </head>
491
  <body>
492
  <div id="gameContainer">
493
+ <div class="ui-container">
494
+ <h2>{GAME_NAME}</h2>
495
  <div id="score">Score: 0</div>
496
  <div id="treasures">Treasures: 0</div>
497
  <div id="timeout">Timeout: 60s</div>
498
  </div>
499
+ <div class="leaderboard" id="leaderboard">Leaderboard</div>
500
  <div id="chatBox"></div>
501
+ <div class="controls">
502
+ <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
503
+ <p>Chat to add world features!</p>
504
+ </div>
505
  </div>
506
 
507
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 
535
  const playerMeshes = {{}};
536
  let treasures = [];
537
  const treasureMeshes = {{}};
538
+ let worldObjects = [];
539
+ const worldObjectMeshes = {{}};
540
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
541
  let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
542
 
543
+ const shapeGeometries = {{
544
+ "sphere": new THREE.SphereGeometry(1, 16, 16),
545
+ "cube": new THREE.BoxGeometry(2, 2, 2),
546
+ "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
547
+ "cone": new THREE.ConeGeometry(1, 2, 16),
548
+ "torus": new THREE.TorusGeometry(1, 0.4, 16, 100),
549
+ "dodecahedron": new THREE.DodecahedronGeometry(1),
550
+ "octahedron": new THREE.OctahedronGeometry(1),
551
+ "tetrahedron": new THREE.TetrahedronGeometry(1),
552
+ "icosahedron": new THREE.IcosahedronGeometry(1)
553
+ }};
554
+
555
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
556
+ const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
557
  playerMesh.position.set(xPos, 1, zPos);
558
  playerMesh.castShadow = true;
559
  scene.add(playerMesh);
560
  players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
561
 
 
562
  function updateTreasures(treasureData) {{
563
  treasureData.forEach(t => {{
564
  if (!treasureMeshes[t.id]) {{
 
584
  }});
585
  }}
586
 
587
+ function updateWorldObjects(objectData) {{
588
+ objectData.forEach(obj => {{
589
+ if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
590
+ const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
591
+ const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
592
+ const objMesh = new THREE.Mesh(geometry, material);
593
+ objMesh.position.set(obj.x, 1, obj.z);
594
+ objMesh.castShadow = true;
595
+ worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
596
+ worldObjects.push(objMesh);
597
+ scene.add(objMesh);
598
+ }}
599
+ }});
600
+ }}
601
+
602
  document.addEventListener('keydown', (event) => {{
603
  switch (event.code) {{
604
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
 
647
  camera.lookAt(xPos, 0, zPos);
648
  const timeout = 60 - (performance.now() / 1000 - lastActive);
649
  document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
650
+ document.getElementById('score').textContent = `Score: $${{score}}`;
651
+ document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
652
  }}
653
 
654
  function updatePlayers(playerData) {{
655
  playerData.forEach(player => {{
656
  if (!playerMeshes[player.username]) {{
657
+ const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
658
+ const material = new THREE.MeshPhongMaterial({{ color: player.color }});
659
+ const mesh = new THREE.Mesh(geometry, material);
660
  mesh.castShadow = true;
661
  scene.add(mesh);
662
  playerMeshes[player.username] = mesh;
 
673
  }}
674
  }});
675
  document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
676
+ const leaderboard = playerData.sort((a, b) => b.score - a.score)
677
+ .map(p => `${{p.username}}: $${{p.score}}`)
678
+ .join('<br>');
679
+ document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
680
  }}
681
 
682
  ws.onmessage = function(event) {{
 
693
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
694
  updatePlayers(gameState.players);
695
  updateTreasures(gameState.treasures);
696
+ updateWorldObjects(gameState.world_objects);
697
  }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
698
  const [sender, message] = data.split('|');
699
  const chatBox = document.getElementById('chatBox');
 
711
 
712
  updatePlayer(delta);
713
  treasures.forEach(t => t.rotation.y += delta);
714
+ worldObjects.forEach(o => o.rotation.y += delta * 0.5);
715
  renderer.render(scene, camera);
716
  }}
717
 
 
723
 
724
  # Main Game Loop
725
  def main():
726
+ # Top Center Chat and Character Display
727
+ st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
728
+ message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
729
+ if st.button("๐ŸŒŸ Send"):
730
+ if message:
731
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
732
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
733
+ if audio_file:
734
+ play_and_download_audio(audio_file)
735
+ st.session_state.last_activity = time.time()
736
+ st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
737
+
738
+ update_marquee_settings_ui()
739
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
740
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: ${st.session_state.score} ๐Ÿ†")
741
+ st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}")
742
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
743
+ st.sidebar.write(f"๐Ÿ… Score: ${st.session_state.score}")
744
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
745
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
746
 
747
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
748
+ if st.sidebar.button("Reset World ๐ŸŒ"):
749
  reset_game_state()
750
  st.session_state.game_state_timestamp = time.time()
751
  st.rerun()
752
 
753
+ # Demo Buttons
754
+ st.sidebar.markdown("### ๐ŸŒŸ World Additions")
755
+ if st.sidebar.button("๐ŸŒฟ Add Plants"):
756
+ asyncio.run(save_chat_entry(st.session_state.username, "plants", st.session_state.tts_voice))
757
+ if st.sidebar.button("๐Ÿฆ Add Animal Flocks"):
758
+ asyncio.run(save_chat_entry(st.session_state.username, "animal flocks", st.session_state.tts_voice))
759
+ if st.sidebar.button("๐Ÿ”๏ธ Add Mountains"):
760
+ asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice))
761
+
762
  left_col, right_col = st.columns([2, 1])
763
 
764
  with left_col:
765
  components.html(rocky_map_html, width=800, height=600)
766
  chat_content = asyncio.run(load_chat())
767
  st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
768
+ uploaded_file = st.file_uploader("Upload a File ๐Ÿ“ค", type=["txt", "md", "mp3"])
769
+ if uploaded_file:
770
+ with open(uploaded_file.name, "wb") as f:
771
+ f.write(uploaded_file.getbuffer())
772
+ game_state = load_game_state(st.session_state.game_state_timestamp)
773
+ if st.session_state.username in game_state["players"]:
774
+ game_state["players"][st.session_state.username]["score"] += 20
775
+ game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}")
776
+ update_game_state(game_state)
777
+ st.session_state.last_activity = time.time()
778
+ st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}")
779
  mycomponent = components.declare_component("speech_component", path="./speech_component")
780
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
781
  if val and val != st.session_state.last_transcript:
782
  val_stripped = val.strip().replace('\n', ' ')
783
  if val_stripped:
784
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
785
  st.session_state['speech_processed'] = True
786
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
787
  if audio_file:
 
795
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
796
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
797
  game_state = load_game_state(st.session_state.game_state_timestamp)
798
+ for username, player in game_state["players"].items():
799
  folium.CircleMarker(
800
+ location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
801
  radius=5,
802
  color=f"#{player['color']:06x}",
803
  fill=True,
804
  fill_opacity=0.7,
805
+ popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})"
806
  ).add_to(prairie_map)
807
  for client_id, player in st.session_state.prairie_players.items():
808
  folium.CircleMarker(
 
815
  ).add_to(prairie_map)
816
  folium_static(prairie_map, width=600, height=400)
817
 
818
+ animal = st.selectbox("Choose Animal ๐Ÿพ", ["prairie_dog", "deer", "sheep", "groundhog"])
819
+ location = st.selectbox("Move to ๐Ÿ“", list(PRAIRIE_LOCATIONS.keys()))
820
+ if st.button("Move ๐Ÿšถ"):
821
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
822
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
823
  st.session_state.last_activity = time.time()
 
827
  remaining = max(0, st.session_state.update_interval - elapsed)
828
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
829
 
 
830
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
831
+ st.sidebar.warning("Timed out! Refreshing in 5 seconds... โฐ")
832
  time.sleep(5)
833
  st.session_state.game_state_timestamp = time.time()
834
  st.rerun()
 
844
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
845
  st.session_state.server_task.start()
846
 
847
+ display_file_history_in_sidebar()
848
+ log_performance_metrics()
849
+
850
  if __name__ == "__main__":
851
  main()