awacke1 commited on
Commit
0c932ce
ยท
verified ยท
1 Parent(s): a058539

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -132
app.py CHANGED
@@ -17,7 +17,7 @@ import streamlit.components.v1 as components
17
  from gradio_client import Client
18
  from streamlit_marquee import streamlit_marquee
19
  import folium
20
- from streamlit_folium import st_folium # Updated: use st_folium instead of folium_static
21
  import glob
22
  import pytz
23
  from collections import defaultdict
@@ -55,11 +55,11 @@ EDGE_TTS_VOICES = [
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),
@@ -70,7 +70,6 @@ PRAIRIE_LOCATIONS = {
70
  # Directories and Files
71
  for d in ["chat_logs", "audio_logs"]:
72
  os.makedirs(d, exist_ok=True)
73
-
74
  CHAT_DIR = "chat_logs"
75
  AUDIO_DIR = "audio_logs"
76
  STATE_FILE = "user_state.txt"
@@ -80,7 +79,6 @@ GAME_STATE_FILE = "game_state.json"
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."""
84
  if os.path.exists(GAME_STATE_FILE):
85
  with open(GAME_STATE_FILE, 'r') as f:
86
  state = json.load(f)
@@ -99,7 +97,6 @@ def load_game_state(_timestamp):
99
  return state
100
 
101
  def update_game_state(state):
102
- """Update the game state and persist to file."""
103
  state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
104
  with open(GAME_STATE_FILE, 'w') as f:
105
  json.dump(state, f)
@@ -107,7 +104,6 @@ def update_game_state(state):
107
  return load_game_state(time.time())
108
 
109
  def reset_game_state():
110
- """Reset the game state to initial conditions."""
111
  state = {
112
  "players": {},
113
  "treasures": [
@@ -119,44 +115,11 @@ def reset_game_state():
119
  }
120
  return update_game_state(state)
121
 
122
- # Timestamp Formatter (Pattern: mmddYYYY-HHMM-AM/PM)
123
- def format_timestamp(username=""):
124
- now = datetime.now(pytz.timezone('US/Central')).strftime("%m%d%Y-%I%M-%p")
125
- return f"{now}-by-{username}"
126
-
127
- def clean_text_for_tts(text):
128
- return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
129
-
130
- def generate_filename(prompt, username, file_type="md"):
131
- timestamp = format_timestamp(username)
132
- cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_")
133
- return f"{cleaned_prompt}_{timestamp}.{file_type}"
134
-
135
- def create_file(prompt, username, file_type="md"):
136
- filename = generate_filename(prompt, username, file_type)
137
- with open(filename, 'w', encoding='utf-8') as f:
138
- f.write(prompt)
139
- return filename
140
-
141
- def get_download_link(file, file_type="mp3"):
142
- with open(file, "rb") as f:
143
- b64 = base64.b64encode(f.read()).decode()
144
- mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"}
145
- return f'<a href="data:{mime_types.get(file_type, "application/octet-stream")};base64,{b64}" download="{os.path.basename(file)}">{FILE_EMOJIS.get(file_type, "๐Ÿ“ฅ")} {os.path.basename(file)}</a>'
146
-
147
- def save_username(username):
148
- with open(STATE_FILE, 'w') as f:
149
- f.write(username)
150
-
151
- def load_username():
152
- if os.path.exists(STATE_FILE):
153
- with open(STATE_FILE, 'r') as f:
154
- return f.read().strip()
155
- return None
156
 
157
- # -----------------------------------------------------------
158
- # Define PlayerAgent class (used in session state initialization)
159
- # -----------------------------------------------------------
160
  class PlayerAgent:
161
  def __init__(self, username, char_data):
162
  self.username = username
@@ -195,11 +158,8 @@ class PlayerAgent:
195
  self.treasures = int(content.split(":")[1])
196
  self.last_active = time.time()
197
 
198
- # -----------------------------------------------------------
199
- # New Helper Functions: Chat Logging & TTS Generation
200
- # -----------------------------------------------------------
201
  def log_chat_history(username, message, is_markdown=False):
202
- """Log the chat entry to a markdown file."""
203
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
204
  if is_markdown:
205
  entry = f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
@@ -208,16 +168,12 @@ def log_chat_history(username, message, is_markdown=False):
208
  md_file = create_file(entry, username, "md")
209
  with open(CHAT_FILE, 'a') as f:
210
  f.write(f"{entry}\n")
211
- return md_file
212
 
213
  async def generate_chat_audio(username, message, voice):
214
- """Generate TTS audio file for the chat message."""
215
  audio_file = await async_edge_tts_generate(message, voice, username)
216
  return audio_file
217
 
218
- # -----------------------------------------------------------
219
- # Audio Processing Function
220
- # -----------------------------------------------------------
221
  async def async_edge_tts_generate(text, voice, username):
222
  cache_key = f"{text[:100]}_{voice}"
223
  if cache_key in st.session_state['audio_cache']:
@@ -236,9 +192,6 @@ def play_and_download_audio(file_path):
236
  st.audio(file_path, start_time=0)
237
  st.markdown(get_download_link(file_path), unsafe_allow_html=True)
238
 
239
- # -----------------------------------------------------------
240
- # WebSocket Broadcast Function
241
- # -----------------------------------------------------------
242
  async def broadcast_message(message, room_id):
243
  if room_id in st.session_state.active_connections:
244
  disconnected = []
@@ -251,13 +204,10 @@ async def broadcast_message(message, room_id):
251
  if client_id in st.session_state.active_connections[room_id]:
252
  del st.session_state.active_connections[room_id][client_id]
253
 
254
- # -----------------------------------------------------------
255
- # Wrapper Function to Save Chat Entry (Using the Two Helpers Above)
256
- # -----------------------------------------------------------
257
  async def save_chat_entry(username, message, voice, is_markdown=False):
258
  if not message.strip() or message == st.session_state.get('last_transcript', ''):
259
  return None, None
260
- md_file = log_chat_history(username, message, is_markdown)
261
  audio_file = await generate_chat_audio(username, message, voice)
262
  await broadcast_message(f"{username}|{message}", "quest")
263
  st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file})
@@ -267,7 +217,7 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
267
  game_state["players"][username]["score"] += 10
268
  game_state["players"][username]["treasures"] += 1
269
  game_state["players"][username]["last_active"] = time.time()
270
- game_state["history"].append(md_file)
271
  if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]:
272
  obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower())
273
  for _ in range(obj_type["count"]):
@@ -280,7 +230,7 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
280
  "shape": obj_type["shape"]
281
  })
282
  update_game_state(game_state)
283
- return md_file, audio_file
284
 
285
  async def load_chat():
286
  if not os.path.exists(CHAT_FILE):
@@ -290,9 +240,6 @@ async def load_chat():
290
  content = f.read().strip()
291
  return content.split('\n')
292
 
293
- # -----------------------------------------------------------
294
- # Session State Initialization
295
- # -----------------------------------------------------------
296
  def init_session_state():
297
  defaults = {
298
  'server_running': False, 'server_task': None, 'active_connections': {},
@@ -314,6 +261,11 @@ def init_session_state():
314
  st.session_state.username = char["name"]
315
  st.session_state.tts_voice = char["voice"]
316
  asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ—บ๏ธ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"]))
 
 
 
 
 
317
  game_state = load_game_state(st.session_state.game_state_timestamp)
318
  if st.session_state.username not in game_state["players"]:
319
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
@@ -323,12 +275,7 @@ def init_session_state():
323
 
324
  init_session_state()
325
 
326
- # -----------------------------------------------------------
327
  # WebSocket Handler and Related Functions
328
- # -----------------------------------------------------------
329
- async def save_chat_entry_wrapper(username, message, voice, is_markdown=False):
330
- return await save_chat_entry(username, message, voice, is_markdown)
331
-
332
  async def websocket_handler(websocket, path):
333
  client_id = str(uuid.uuid4())
334
  room_id = "quest"
@@ -372,9 +319,10 @@ async def websocket_handler(websocket, path):
372
  target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
373
  st.session_state.prairie_players[client_id]["location"] = target
374
  action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
375
- await save_chat_entry_wrapper(sender, action_msg, voice)
376
  else:
377
- await save_chat_entry_wrapper(sender, content, voice)
 
378
  update_game_state(game_state)
379
  except websockets.ConnectionClosed:
380
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
@@ -409,7 +357,7 @@ async def periodic_update():
409
  chat_content = await load_chat()
410
  await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest")
411
  await broadcast_message(f"GAME_STATE:{json.dumps(game_state)}", "quest")
412
- await save_chat_entry_wrapper("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
413
  await asyncio.sleep(st.session_state.update_interval)
414
 
415
  async def run_websocket_server():
@@ -424,9 +372,6 @@ def start_websocket_server():
424
  asyncio.set_event_loop(loop)
425
  loop.run_until_complete(run_websocket_server())
426
 
427
- # -----------------------------------------------------------
428
- # ArXiv Integration (Unchanged)
429
- # -----------------------------------------------------------
430
  async def perform_arxiv_search(query, username):
431
  gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
432
  refs = gradio_client.predict(
@@ -434,12 +379,9 @@ async def perform_arxiv_search(query, username):
434
  )[0]
435
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
436
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
437
- md_file, audio_file = await save_chat_entry_wrapper(username, result, voice, True)
438
  return md_file, audio_file
439
 
440
- # -----------------------------------------------------------
441
- # Sidebar Functions
442
- # -----------------------------------------------------------
443
  def update_marquee_settings_ui():
444
  st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
445
  cols = st.sidebar.columns(2)
@@ -478,20 +420,17 @@ def log_performance_metrics():
478
  percentage = (duration / total_time) * 100
479
  st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
480
 
481
- # -----------------------------------------------------------
482
- # Enhanced 3D Game HTML (With dynamic insertion of username and fluid movement)
483
- # -----------------------------------------------------------
484
- # Precompute player color and shape for use in the HTML.
485
- player_color = next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
486
- player_shape = next(c["shape"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
487
-
488
  rocky_map_html = f"""
489
  <!DOCTYPE html>
490
  <html lang="en">
491
  <head>
492
  <meta charset="UTF-8">
493
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
494
- <title>Rocky Mountain Quest 3D</title>
495
  <style>
496
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
497
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
@@ -528,7 +467,7 @@ rocky_map_html = f"""
528
  <div class="leaderboard" id="leaderboard">Leaderboard</div>
529
  <div id="chatBox"></div>
530
  <div class="controls">
531
- <p>Controls: Use WASD or Arrow keys for movement. Space to collect treasure.</p>
532
  <p>Chat to add world features!</p>
533
  </div>
534
  </div>
@@ -538,33 +477,33 @@ rocky_map_html = f"""
538
  const playerName = "{st.session_state.username}";
539
  let ws = new WebSocket('ws://localhost:8765');
540
  const scene = new THREE.Scene();
541
- const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
542
  camera.position.set(0, 50, 50);
543
  camera.lookAt(0, 0, 0);
 
544
  const renderer = new THREE.WebGLRenderer({{ antialias: true }});
545
  renderer.setSize(800, 600);
546
  document.getElementById('gameContainer').appendChild(renderer.domElement);
547
 
548
- // Lighting & Ground
549
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
550
  scene.add(ambientLight);
551
  const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
552
  sunLight.position.set(50, 50, 50);
553
  sunLight.castShadow = true;
554
  scene.add(sunLight);
 
555
  const groundGeometry = new THREE.PlaneGeometry(100, 100);
556
  const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
557
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
558
- ground.rotation.x = -Math.PI / 2;
559
  ground.receiveShadow = true;
560
  scene.add(ground);
561
 
562
- // Fluid Movement Variables
563
  let velocity = new THREE.Vector2(0, 0);
564
  const acceleration = 50;
565
  const friction = 5;
566
 
567
- // Player Setup
568
  let xPos = 0, zPos = 0;
569
  const shapeGeometries = {{
570
  "sphere": new THREE.SphereGeometry(1, 16, 16),
@@ -582,22 +521,23 @@ rocky_map_html = f"""
582
  playerMesh.position.set(xPos, 1, zPos);
583
  playerMesh.castShadow = true;
584
  scene.add(playerMesh);
 
585
  let score = 0, treasureCount = 0;
586
- let lastActive = performance.now() / 1000;
587
  let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
588
 
589
  document.addEventListener('keydown', (event) => {{
590
- switch (event.code) {{
591
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
592
  case 'ArrowRight': case 'KeyD': moveRight = true; break;
593
  case 'ArrowUp': case 'KeyW': moveUp = true; break;
594
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
595
  case 'Space': collect = true; break;
596
  }}
597
- lastActive = performance.now() / 1000;
598
  }});
599
  document.addEventListener('keyup', (event) => {{
600
- switch (event.code) {{
601
  case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
602
  case 'ArrowRight': case 'KeyD': moveRight = false; break;
603
  case 'ArrowUp': case 'KeyW': moveUp = false; break;
@@ -607,10 +547,10 @@ rocky_map_html = f"""
607
  }});
608
 
609
  function updatePlayer(delta) {{
610
- if (moveLeft) velocity.x -= acceleration * delta;
611
- if (moveRight) velocity.x += acceleration * delta;
612
- if (moveUp) velocity.y -= acceleration * delta;
613
- if (moveDown) velocity.y += acceleration * delta;
614
  velocity.x -= velocity.x * friction * delta;
615
  velocity.y -= velocity.y * friction * delta;
616
  xPos += velocity.x * delta;
@@ -619,9 +559,9 @@ rocky_map_html = f"""
619
  zPos = Math.max(-40, Math.min(40, zPos));
620
  playerMesh.position.set(xPos, 1, zPos);
621
  ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
622
- if (collect) {{
623
- for (let i = treasures.length - 1; i >= 0; i--) {{
624
- if (playerMesh.position.distanceTo(treasures[i].position) < 2) {{
625
  const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
626
  scene.remove(treasures[i]);
627
  treasures.splice(i, 1);
@@ -642,7 +582,7 @@ rocky_map_html = f"""
642
 
643
  function updatePlayers(playerData) {{
644
  playerData.forEach(player => {{
645
- if (!playerMeshes[player.username]) {{
646
  const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
647
  const material = new THREE.MeshPhongMaterial({{ color: player.color }});
648
  const mesh = new THREE.Mesh(geometry, material);
@@ -653,7 +593,7 @@ rocky_map_html = f"""
653
  const mesh = playerMeshes[player.username];
654
  mesh.position.set(player.x, 1, player.z);
655
  players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
656
- if (player.username === playerName) {{
657
  xPos = player.x;
658
  zPos = player.z;
659
  score = player.score;
@@ -670,7 +610,7 @@ rocky_map_html = f"""
670
 
671
  function updateTreasures(treasureData) {{
672
  treasureData.forEach(t => {{
673
- if (!treasureMeshes[t.id]) {{
674
  const treasure = new THREE.Mesh(
675
  new THREE.SphereGeometry(1, 8, 8),
676
  new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
@@ -685,7 +625,7 @@ rocky_map_html = f"""
685
  }}
686
  }});
687
  Object.keys(treasureMeshes).forEach(id => {{
688
- if (!treasureData.some(t => t.id === id)) {{
689
  scene.remove(treasureMeshes[id]);
690
  treasures = treasures.filter(t => t !== treasureMeshes[id]);
691
  delete treasureMeshes[id];
@@ -695,7 +635,7 @@ rocky_map_html = f"""
695
 
696
  function updateWorldObjects(objectData) {{
697
  objectData.forEach(obj => {{
698
- if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
699
  const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
700
  const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
701
  const objMesh = new THREE.Mesh(geometry, material);
@@ -710,20 +650,20 @@ rocky_map_html = f"""
710
 
711
  ws.onmessage = function(event) {{
712
  const data = event.data;
713
- if (data.startsWith('MAP_UPDATE:')) {{
714
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
715
  updatePlayers(playerData);
716
- }} else if (data.startsWith('CHAT_UPDATE:')) {{
717
  const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
718
  const chatBox = document.getElementById('chatBox');
719
  chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
720
  chatBox.scrollTop = chatBox.scrollHeight;
721
- }} else if (data.startsWith('GAME_STATE:')) {{
722
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
723
  updatePlayers(gameState.players);
724
  updateTreasures(gameState.treasures);
725
  updateWorldObjects(gameState.world_objects);
726
- }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
727
  const [sender, message] = data.split('|');
728
  const chatBox = document.getElementById('chatBox');
729
  chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
@@ -748,22 +688,23 @@ rocky_map_html = f"""
748
  </html>
749
  """
750
 
751
- # -----------------------------------------------------------
752
- # Main Game Loop
753
- # -----------------------------------------------------------
754
  def main():
755
  st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
756
- # Chat Input Area
757
  message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
758
  if st.button("๐ŸŒŸ Send"):
759
  if message:
760
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
761
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
762
  if audio_file:
763
  play_and_download_audio(audio_file)
764
  st.session_state.last_activity = time.time()
765
  st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
766
 
 
 
 
 
767
  update_marquee_settings_ui()
768
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
769
  st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: ${st.session_state.score} ๐Ÿ†")
@@ -789,10 +730,6 @@ def main():
789
 
790
  left_col, right_col = st.columns([2, 1])
791
  with left_col:
792
- # Display the Quest Log as a code block with Python syntax and line numbers (if supported)
793
- chat_content = asyncio.run(load_chat())
794
- st.code("\n".join(chat_content[-10:]), language="python")
795
- # Then display the 3D world
796
  components.html(rocky_map_html, width=800, height=600)
797
  uploaded_file = st.file_uploader("Upload a File ๐Ÿ“ค", type=["txt", "md", "mp3"])
798
  if uploaded_file:
@@ -805,7 +742,6 @@ def main():
805
  update_game_state(game_state)
806
  st.session_state.last_activity = time.time()
807
  st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}")
808
- # Speech component integration remains as before.
809
  mycomponent = components.declare_component("speech_component", path="./speech_component")
810
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
811
  if val and val != st.session_state.last_transcript:
@@ -813,7 +749,7 @@ def main():
813
  if val_stripped:
814
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
815
  st.session_state['speech_processed'] = True
816
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
817
  if audio_file:
818
  play_and_download_audio(audio_file)
819
  st.session_state.last_activity = time.time()
@@ -821,11 +757,9 @@ def main():
821
  with right_col:
822
  st.subheader("๐ŸŒพ Prairie Map")
823
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
824
- # Add base locations
825
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
826
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
827
  game_state = load_game_state(st.session_state.game_state_timestamp)
828
- # Mark players
829
  for username, player in game_state["players"].items():
830
  folium.CircleMarker(
831
  location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
@@ -835,7 +769,6 @@ def main():
835
  fill_opacity=0.7,
836
  popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})"
837
  ).add_to(prairie_map)
838
- # Mark prairie players (animals)
839
  for client_id, player in st.session_state.prairie_players.items():
840
  folium.CircleMarker(
841
  location=player['location'],
@@ -845,7 +778,6 @@ def main():
845
  fill_opacity=0.7,
846
  popup=f"{player['username']} ({player['animal']})"
847
  ).add_to(prairie_map)
848
- # Mark world objects (plants, trees, etc.) with emojis and pins
849
  for obj in game_state["world_objects"]:
850
  lat = 44.0 + obj["x"] * 0.01
851
  lon = -103.0 + obj["z"] * 0.01
 
17
  from gradio_client import Client
18
  from streamlit_marquee import streamlit_marquee
19
  import folium
20
+ from streamlit_folium import st_folium # Use st_folium instead of folium_static
21
  import glob
22
  import pytz
23
  from collections import defaultdict
 
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),
 
70
  # Directories and Files
71
  for d in ["chat_logs", "audio_logs"]:
72
  os.makedirs(d, exist_ok=True)
 
73
  CHAT_DIR = "chat_logs"
74
  AUDIO_DIR = "audio_logs"
75
  STATE_FILE = "user_state.txt"
 
79
  # Cached Game State as "Resource"
80
  @st.cache_resource
81
  def load_game_state(_timestamp):
 
82
  if os.path.exists(GAME_STATE_FILE):
83
  with open(GAME_STATE_FILE, 'r') as f:
84
  state = json.load(f)
 
97
  return state
98
 
99
  def update_game_state(state):
 
100
  state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
101
  with open(GAME_STATE_FILE, 'w') as f:
102
  json.dump(state, f)
 
104
  return load_game_state(time.time())
105
 
106
  def reset_game_state():
 
107
  state = {
108
  "players": {},
109
  "treasures": [
 
115
  }
116
  return update_game_state(state)
117
 
118
+ # Precompute player's attributes for the 3D scene.
119
+ player_color = None
120
+ player_shape = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ # Define PlayerAgent class
 
 
123
  class PlayerAgent:
124
  def __init__(self, username, char_data):
125
  self.username = username
 
158
  self.treasures = int(content.split(":")[1])
159
  self.last_active = time.time()
160
 
161
+ # Helpers for chat logging and TTS generation
 
 
162
  def log_chat_history(username, message, is_markdown=False):
 
163
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
164
  if is_markdown:
165
  entry = f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
 
168
  md_file = create_file(entry, username, "md")
169
  with open(CHAT_FILE, 'a') as f:
170
  f.write(f"{entry}\n")
171
+ return entry
172
 
173
  async def generate_chat_audio(username, message, voice):
 
174
  audio_file = await async_edge_tts_generate(message, voice, username)
175
  return audio_file
176
 
 
 
 
177
  async def async_edge_tts_generate(text, voice, username):
178
  cache_key = f"{text[:100]}_{voice}"
179
  if cache_key in st.session_state['audio_cache']:
 
192
  st.audio(file_path, start_time=0)
193
  st.markdown(get_download_link(file_path), unsafe_allow_html=True)
194
 
 
 
 
195
  async def broadcast_message(message, room_id):
196
  if room_id in st.session_state.active_connections:
197
  disconnected = []
 
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 save_chat_entry(username, message, voice, is_markdown=False):
208
  if not message.strip() or message == st.session_state.get('last_transcript', ''):
209
  return None, None
210
+ entry = log_chat_history(username, message, is_markdown)
211
  audio_file = await generate_chat_audio(username, message, voice)
212
  await broadcast_message(f"{username}|{message}", "quest")
213
  st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file})
 
217
  game_state["players"][username]["score"] += 10
218
  game_state["players"][username]["treasures"] += 1
219
  game_state["players"][username]["last_active"] = time.time()
220
+ game_state["history"].append(entry)
221
  if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]:
222
  obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower())
223
  for _ in range(obj_type["count"]):
 
230
  "shape": obj_type["shape"]
231
  })
232
  update_game_state(game_state)
233
+ return entry, audio_file
234
 
235
  async def load_chat():
236
  if not os.path.exists(CHAT_FILE):
 
240
  content = f.read().strip()
241
  return content.split('\n')
242
 
 
 
 
243
  def init_session_state():
244
  defaults = {
245
  'server_running': False, 'server_task': None, 'active_connections': {},
 
261
  st.session_state.username = char["name"]
262
  st.session_state.tts_voice = char["voice"]
263
  asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ—บ๏ธ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"]))
264
+ global player_color, player_shape
265
+ if player_color is None or player_shape is None:
266
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
267
+ player_color = char["color"]
268
+ player_shape = char["shape"]
269
  game_state = load_game_state(st.session_state.game_state_timestamp)
270
  if st.session_state.username not in game_state["players"]:
271
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
 
275
 
276
  init_session_state()
277
 
 
278
  # WebSocket Handler and Related Functions
 
 
 
 
279
  async def websocket_handler(websocket, path):
280
  client_id = str(uuid.uuid4())
281
  room_id = "quest"
 
319
  target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
320
  st.session_state.prairie_players[client_id]["location"] = target
321
  action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
322
+ await save_chat_entry(sender, action_msg, voice)
323
  else:
324
+ await save_chat_entry(sender, content, voice)
325
+ await perform_arxiv_search(content, sender)
326
  update_game_state(game_state)
327
  except websockets.ConnectionClosed:
328
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
 
357
  chat_content = await load_chat()
358
  await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest")
359
  await broadcast_message(f"GAME_STATE:{json.dumps(game_state)}", "quest")
360
+ await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
361
  await asyncio.sleep(st.session_state.update_interval)
362
 
363
  async def run_websocket_server():
 
372
  asyncio.set_event_loop(loop)
373
  loop.run_until_complete(run_websocket_server())
374
 
 
 
 
375
  async def perform_arxiv_search(query, username):
376
  gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
377
  refs = gradio_client.predict(
 
379
  )[0]
380
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
381
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
382
+ md_file, audio_file = await save_chat_entry(username, result, voice, True)
383
  return md_file, audio_file
384
 
 
 
 
385
  def update_marquee_settings_ui():
386
  st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
387
  cols = st.sidebar.columns(2)
 
420
  percentage = (duration / total_time) * 100
421
  st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
422
 
423
+ # ---------------------------
424
+ # Enhanced 3D Game HTML
425
+ # ---------------------------
426
+ # Use precomputed player_color and player_shape in the JS code.
 
 
 
427
  rocky_map_html = f"""
428
  <!DOCTYPE html>
429
  <html lang="en">
430
  <head>
431
  <meta charset="UTF-8">
432
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
433
+ <title>{GAME_NAME}</title>
434
  <style>
435
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
436
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
 
467
  <div class="leaderboard" id="leaderboard">Leaderboard</div>
468
  <div id="chatBox"></div>
469
  <div class="controls">
470
+ <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
471
  <p>Chat to add world features!</p>
472
  </div>
473
  </div>
 
477
  const playerName = "{st.session_state.username}";
478
  let ws = new WebSocket('ws://localhost:8765');
479
  const scene = new THREE.Scene();
480
+ const camera = new THREE.PerspectiveCamera(75, 800/600, 0.1, 1000);
481
  camera.position.set(0, 50, 50);
482
  camera.lookAt(0, 0, 0);
483
+
484
  const renderer = new THREE.WebGLRenderer({{ antialias: true }});
485
  renderer.setSize(800, 600);
486
  document.getElementById('gameContainer').appendChild(renderer.domElement);
487
 
 
488
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
489
  scene.add(ambientLight);
490
  const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
491
  sunLight.position.set(50, 50, 50);
492
  sunLight.castShadow = true;
493
  scene.add(sunLight);
494
+
495
  const groundGeometry = new THREE.PlaneGeometry(100, 100);
496
  const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
497
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
498
+ ground.rotation.x = -Math.PI/2;
499
  ground.receiveShadow = true;
500
  scene.add(ground);
501
 
502
+ // Fluid movement variables (you can adjust acceleration/friction as needed)
503
  let velocity = new THREE.Vector2(0, 0);
504
  const acceleration = 50;
505
  const friction = 5;
506
 
 
507
  let xPos = 0, zPos = 0;
508
  const shapeGeometries = {{
509
  "sphere": new THREE.SphereGeometry(1, 16, 16),
 
521
  playerMesh.position.set(xPos, 1, zPos);
522
  playerMesh.castShadow = true;
523
  scene.add(playerMesh);
524
+
525
  let score = 0, treasureCount = 0;
526
+ let lastActive = performance.now()/1000;
527
  let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
528
 
529
  document.addEventListener('keydown', (event) => {{
530
+ switch(event.code) {{
531
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
532
  case 'ArrowRight': case 'KeyD': moveRight = true; break;
533
  case 'ArrowUp': case 'KeyW': moveUp = true; break;
534
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
535
  case 'Space': collect = true; break;
536
  }}
537
+ lastActive = performance.now()/1000;
538
  }});
539
  document.addEventListener('keyup', (event) => {{
540
+ switch(event.code) {{
541
  case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
542
  case 'ArrowRight': case 'KeyD': moveRight = false; break;
543
  case 'ArrowUp': case 'KeyW': moveUp = false; break;
 
547
  }});
548
 
549
  function updatePlayer(delta) {{
550
+ if(moveLeft) velocity.x -= acceleration * delta;
551
+ if(moveRight) velocity.x += acceleration * delta;
552
+ if(moveUp) velocity.y -= acceleration * delta;
553
+ if(moveDown) velocity.y += acceleration * delta;
554
  velocity.x -= velocity.x * friction * delta;
555
  velocity.y -= velocity.y * friction * delta;
556
  xPos += velocity.x * delta;
 
559
  zPos = Math.max(-40, Math.min(40, zPos));
560
  playerMesh.position.set(xPos, 1, zPos);
561
  ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
562
+ if(collect) {{
563
+ for(let i = treasures.length - 1; i >= 0; i--) {{
564
+ if(playerMesh.position.distanceTo(treasures[i].position) < 2) {{
565
  const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
566
  scene.remove(treasures[i]);
567
  treasures.splice(i, 1);
 
582
 
583
  function updatePlayers(playerData) {{
584
  playerData.forEach(player => {{
585
+ if(!playerMeshes[player.username]) {{
586
  const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
587
  const material = new THREE.MeshPhongMaterial({{ color: player.color }});
588
  const mesh = new THREE.Mesh(geometry, material);
 
593
  const mesh = playerMeshes[player.username];
594
  mesh.position.set(player.x, 1, player.z);
595
  players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
596
+ if(player.username === playerName) {{
597
  xPos = player.x;
598
  zPos = player.z;
599
  score = player.score;
 
610
 
611
  function updateTreasures(treasureData) {{
612
  treasureData.forEach(t => {{
613
+ if(!treasureMeshes[t.id]) {{
614
  const treasure = new THREE.Mesh(
615
  new THREE.SphereGeometry(1, 8, 8),
616
  new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
 
625
  }}
626
  }});
627
  Object.keys(treasureMeshes).forEach(id => {{
628
+ if(!treasureData.some(t => t.id === id)) {{
629
  scene.remove(treasureMeshes[id]);
630
  treasures = treasures.filter(t => t !== treasureMeshes[id]);
631
  delete treasureMeshes[id];
 
635
 
636
  function updateWorldObjects(objectData) {{
637
  objectData.forEach(obj => {{
638
+ if(!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
639
  const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
640
  const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
641
  const objMesh = new THREE.Mesh(geometry, material);
 
650
 
651
  ws.onmessage = function(event) {{
652
  const data = event.data;
653
+ if(data.startsWith('MAP_UPDATE:')) {{
654
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
655
  updatePlayers(playerData);
656
+ }} else if(data.startsWith('CHAT_UPDATE:')) {{
657
  const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
658
  const chatBox = document.getElementById('chatBox');
659
  chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
660
  chatBox.scrollTop = chatBox.scrollHeight;
661
+ }} else if(data.startsWith('GAME_STATE:')) {{
662
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
663
  updatePlayers(gameState.players);
664
  updateTreasures(gameState.treasures);
665
  updateWorldObjects(gameState.world_objects);
666
+ }} else if(!data.startsWith('PRAIRIE_UPDATE:')) {{
667
  const [sender, message] = data.split('|');
668
  const chatBox = document.getElementById('chatBox');
669
  chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
 
688
  </html>
689
  """
690
 
 
 
 
691
  def main():
692
  st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
693
+ # Display Chat Input above the Quest Log
694
  message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
695
  if st.button("๐ŸŒŸ Send"):
696
  if message:
697
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
698
+ entry, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
699
  if audio_file:
700
  play_and_download_audio(audio_file)
701
  st.session_state.last_activity = time.time()
702
  st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
703
 
704
+ # Display Quest Log as a code block with Python formatting and line numbers
705
+ chat_content = asyncio.run(load_chat())
706
+ st.code("\n".join(chat_content[-10:]), language="python")
707
+
708
  update_marquee_settings_ui()
709
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
710
  st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: ${st.session_state.score} ๐Ÿ†")
 
730
 
731
  left_col, right_col = st.columns([2, 1])
732
  with left_col:
 
 
 
 
733
  components.html(rocky_map_html, width=800, height=600)
734
  uploaded_file = st.file_uploader("Upload a File ๐Ÿ“ค", type=["txt", "md", "mp3"])
735
  if uploaded_file:
 
742
  update_game_state(game_state)
743
  st.session_state.last_activity = time.time()
744
  st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}")
 
745
  mycomponent = components.declare_component("speech_component", path="./speech_component")
746
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
747
  if val and val != st.session_state.last_transcript:
 
749
  if val_stripped:
750
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
751
  st.session_state['speech_processed'] = True
752
+ entry, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
753
  if audio_file:
754
  play_and_download_audio(audio_file)
755
  st.session_state.last_activity = time.time()
 
757
  with right_col:
758
  st.subheader("๐ŸŒพ Prairie Map")
759
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
 
760
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
761
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
762
  game_state = load_game_state(st.session_state.game_state_timestamp)
 
763
  for username, player in game_state["players"].items():
764
  folium.CircleMarker(
765
  location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
 
769
  fill_opacity=0.7,
770
  popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})"
771
  ).add_to(prairie_map)
 
772
  for client_id, player in st.session_state.prairie_players.items():
773
  folium.CircleMarker(
774
  location=player['location'],
 
778
  fill_opacity=0.7,
779
  popup=f"{player['username']} ({player['animal']})"
780
  ).add_to(prairie_map)
 
781
  for obj in game_state["world_objects"]:
782
  lat = 44.0 + obj["x"] * 0.01
783
  lon = -103.0 + obj["z"] * 0.01