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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -368
app.py CHANGED
@@ -26,7 +26,9 @@ import pandas as pd
26
  # Patch asyncio for nesting
27
  nest_asyncio.apply()
28
 
29
- # Page Config
 
 
30
  st.set_page_config(
31
  layout="wide",
32
  page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
@@ -39,7 +41,6 @@ st.set_page_config(
39
  }
40
  )
41
 
42
- # Game Config
43
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
  START_LOCATION = "Trailhead Camp โ›บ"
45
  EDGE_TTS_VOICES = [
@@ -67,7 +68,9 @@ PRAIRIE_LOCATIONS = {
67
  "Wyoming Spring Creek": (41.6666, -106.6666)
68
  }
69
 
70
- # Directories and Files
 
 
71
  for d in ["chat_logs", "audio_logs"]:
72
  os.makedirs(d, exist_ok=True)
73
  CHAT_DIR = "chat_logs"
@@ -76,104 +79,46 @@ STATE_FILE = "user_state.txt"
76
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
77
  GAME_STATE_FILE = "game_state.json"
78
 
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)
85
- else:
86
- state = {
87
- "players": {},
88
- "treasures": [
89
- {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
90
- for _ in range(5)
91
- ],
92
- "world_objects": [],
93
- "history": []
94
- }
95
- with open(GAME_STATE_FILE, 'w') as f:
96
- json.dump(state, 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)
103
- load_game_state.clear()
104
- return load_game_state(time.time())
105
-
106
- def reset_game_state():
107
- state = {
108
- "players": {},
109
- "treasures": [
110
- {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
111
- for _ in range(5)
112
- ],
113
- "world_objects": [],
114
- "history": []
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
126
- self.x = random.uniform(-20, 20)
127
- self.z = random.uniform(-40, 40)
128
- self.color = char_data["color"]
129
- self.shape = char_data["shape"]
130
- self.score = 0
131
- self.treasures = 0
132
- self.last_active = time.time()
133
- self.voice = char_data["voice"]
134
-
135
- def to_dict(self):
136
- return {
137
- "username": self.username,
138
- "x": self.x,
139
- "z": self.z,
140
- "color": self.color,
141
- "shape": self.shape,
142
- "score": self.score,
143
- "treasures": self.treasures,
144
- "last_active": self.last_active
145
- }
146
-
147
- def update_from_message(self, message):
148
- if '|' in message:
149
- _, content = message.split('|', 1)
150
- if content.startswith("MOVE:"):
151
- _, x, z = content.split(":")
152
- self.x, self.z = float(x), float(z)
153
- self.last_active = time.time()
154
- elif content.startswith("SCORE:"):
155
- self.score = int(content.split(":")[1])
156
- self.last_active = time.time()
157
- elif content.startswith("TREASURE:"):
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```"
166
- else:
167
- entry = f"[{timestamp}] {username}: {message}"
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,17 +137,17 @@ def play_and_download_audio(file_path):
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 = []
198
- for client_id, ws in st.session_state.active_connections[room_id].items():
199
- try:
200
- await ws.send(message)
201
- except websockets.ConnectionClosed:
202
- disconnected.append(client_id)
203
- for client_id in 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', ''):
@@ -240,6 +185,9 @@ async def load_chat():
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': {},
@@ -275,7 +223,21 @@ def init_session_state():
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"
@@ -379,9 +341,12 @@ async def perform_arxiv_search(query, username):
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,277 +385,279 @@ def log_performance_metrics():
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; }}
437
- canvas {{ width: 100%; height: 100%; display: block; }}
438
- .ui-container {{
439
- position: absolute; top: 10px; left: 10px; color: white;
440
- background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
441
- user-select: none;
442
- }}
443
- .leaderboard {{
444
- position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white;
445
- background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px;
446
- text-align: center;
447
- }}
448
- .controls {{
449
- position: absolute; bottom: 10px; left: 10px; color: white;
450
- background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
451
- }}
452
- #chatBox {{
453
- position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
454
- background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
455
- border-radius: 5px; overflow-y: auto;
456
- }}
457
- </style>
458
  </head>
459
  <body>
460
- <div id="gameContainer">
461
- <div class="ui-container">
462
- <h2>{GAME_NAME}</h2>
463
- <div id="score">Score: 0</div>
464
- <div id="treasures">Treasures: 0</div>
465
- <div id="timeout">Timeout: 60s</div>
466
- </div>
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>
474
-
475
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
476
- <script>
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),
510
- "cube": new THREE.BoxGeometry(2, 2, 2),
511
- "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
512
- "cone": new THREE.ConeGeometry(1, 2, 16),
513
- "torus": new THREE.TorusGeometry(1, 0.4, 16, 100),
514
- "dodecahedron": new THREE.DodecahedronGeometry(1),
515
- "octahedron": new THREE.OctahedronGeometry(1),
516
- "tetrahedron": new THREE.TetrahedronGeometry(1),
517
- "icosahedron": new THREE.IcosahedronGeometry(1)
518
- }};
519
- const playerMaterial = new THREE.MeshPhongMaterial({{ color: {player_color} }});
520
- const playerMesh = new THREE.Mesh(shapeGeometries["{player_shape}"], playerMaterial);
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;
544
- case 'ArrowDown': case 'KeyS': moveDown = false; break;
545
- case 'Space': collect = false; break;
546
- }}
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;
557
- zPos += velocity.y * delta;
558
- xPos = Math.max(-40, Math.min(40, xPos));
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);
568
- delete treasureMeshes[id];
569
- score += 50;
570
- treasureCount += 1;
571
- ws.send(`${{playerName}}|SCORE:${{score}}`);
572
- ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
573
- }}
574
- }}
575
- }}
576
- camera.position.lerp(new THREE.Vector3(xPos, 50, zPos + 50), 0.1);
577
- camera.lookAt(new THREE.Vector3(xPos, 0, zPos));
578
- document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, (60 - (performance.now()/1000 - lastActive)).toFixed(0))}}s`;
579
- document.getElementById('score').textContent = `Score: $${{score}}`;
580
- document.getElementById('treasures').textContent = `Treasures: $${{treasureCount}}`;
581
  }}
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);
589
- mesh.castShadow = true;
590
- scene.add(mesh);
591
- playerMeshes[player.username] = mesh;
592
- }}
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;
600
- treasureCount = player.treasures;
601
- playerMesh.position.set(xPos, 1, zPos);
602
- }}
603
- }});
604
- document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
605
- const leaderboard = playerData.sort((a, b) => b.score - a.score)
606
- .map(p => `${{p.username}}: $${{p.score}}`)
607
- .join('<br>');
608
- document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
609
  }}
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 }})
617
- );
618
- treasure.position.set(t.x, 1, t.z);
619
- treasure.castShadow = true;
620
- treasureMeshes[t.id] = treasure;
621
- treasures.push(treasure);
622
- scene.add(treasure);
623
- }} else {{
624
- treasureMeshes[t.id].position.set(t.x, 1, t.z);
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];
632
- }}
633
- }});
634
  }}
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);
642
- objMesh.position.set(obj.x, 1, obj.z);
643
- objMesh.castShadow = true;
644
- worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
645
- worldObjects.push(objMesh);
646
- scene.add(objMesh);
647
- }}
648
- }});
 
 
 
 
 
 
 
 
649
  }}
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>`;
670
- chatBox.scrollTop = chatBox.scrollHeight;
671
- }}
672
- }};
673
-
674
- let lastTime = performance.now();
675
- function animate() {{
676
- requestAnimationFrame(animate);
677
- const currentTime = performance.now();
678
- const delta = (currentTime - lastTime) / 1000;
679
- lastTime = currentTime;
680
- updatePlayer(delta);
681
- treasures.forEach(t => t.rotation.y += delta);
682
- worldObjects.forEach(o => o.rotation.y += delta * 0.5);
683
- renderer.render(scene, camera);
684
  }}
685
- animate();
686
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  </body>
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:
@@ -701,7 +668,7 @@ def main():
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
 
 
26
  # Patch asyncio for nesting
27
  nest_asyncio.apply()
28
 
29
+ # -----------------------------
30
+ # Page and Game Configuration
31
+ # -----------------------------
32
  st.set_page_config(
33
  layout="wide",
34
  page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
 
41
  }
42
  )
43
 
 
44
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
45
  START_LOCATION = "Trailhead Camp โ›บ"
46
  EDGE_TTS_VOICES = [
 
68
  "Wyoming Spring Creek": (41.6666, -106.6666)
69
  }
70
 
71
+ # -----------------------------
72
+ # Directories and File Paths
73
+ # -----------------------------
74
  for d in ["chat_logs", "audio_logs"]:
75
  os.makedirs(d, exist_ok=True)
76
  CHAT_DIR = "chat_logs"
 
79
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
80
  GAME_STATE_FILE = "game_state.json"
81
 
82
+ # -----------------------------
83
+ # Utility Functions
84
+ # -----------------------------
85
+ def format_timestamp(username=""):
86
+ now = datetime.now(pytz.timezone('US/Central')).strftime("%m%d%Y-%I%M-%p")
87
+ return f"{now}-by-{username}"
88
+
89
+ def clean_text_for_tts(text):
90
+ return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
91
+
92
+ def generate_filename(prompt, username, file_type="md"):
93
+ timestamp = format_timestamp(username)
94
+ cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_")
95
+ return f"{cleaned_prompt}_{timestamp}.{file_type}"
96
+
97
+ def create_file(prompt, username, file_type="md"):
98
+ filename = generate_filename(prompt, username, file_type)
99
+ with open(filename, 'w', encoding='utf-8') as f:
100
+ f.write(prompt)
101
+ return filename
102
+
103
+ def get_download_link(file, file_type="mp3"):
104
+ with open(file, "rb") as f:
105
+ b64 = base64.b64encode(f.read()).decode()
106
+ mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"}
107
+ 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>'
108
+
109
+ def save_username(username):
110
+ with open(STATE_FILE, 'w') as f:
111
+ f.write(username)
112
+
113
+ def load_username():
114
+ if os.path.exists(STATE_FILE):
115
+ with open(STATE_FILE, 'r') as f:
116
+ return f.read().strip()
117
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # -----------------------------
120
+ # Audio and Chat Processing Functions
121
+ # -----------------------------
122
  async def async_edge_tts_generate(text, voice, username):
123
  cache_key = f"{text[:100]}_{voice}"
124
  if cache_key in st.session_state['audio_cache']:
 
137
  st.audio(file_path, start_time=0)
138
  st.markdown(get_download_link(file_path), unsafe_allow_html=True)
139
 
140
+ def log_chat_history(username, message, is_markdown=False):
141
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
142
+ entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
143
+ md_file = create_file(entry, username, "md")
144
+ with open(CHAT_FILE, 'a') as f:
145
+ f.write(f"{entry}\n")
146
+ return entry
147
+
148
+ async def generate_chat_audio(username, message, voice):
149
+ audio_file = await async_edge_tts_generate(message, voice, username)
150
+ return audio_file
151
 
152
  async def save_chat_entry(username, message, voice, is_markdown=False):
153
  if not message.strip() or message == st.session_state.get('last_transcript', ''):
 
185
  content = f.read().strip()
186
  return content.split('\n')
187
 
188
+ # -----------------------------
189
+ # Session State Initialization
190
+ # -----------------------------
191
  def init_session_state():
192
  defaults = {
193
  'server_running': False, 'server_task': None, 'active_connections': {},
 
223
 
224
  init_session_state()
225
 
226
+ # -----------------------------
227
+ # WebSocket and Multiplayer Functions
228
+ # -----------------------------
229
+ async def broadcast_message(message, room_id):
230
+ if room_id in st.session_state.active_connections:
231
+ disconnected = []
232
+ for client_id, ws in st.session_state.active_connections[room_id].items():
233
+ try:
234
+ await ws.send(message)
235
+ except websockets.ConnectionClosed:
236
+ disconnected.append(client_id)
237
+ for client_id in disconnected:
238
+ if client_id in st.session_state.active_connections[room_id]:
239
+ del st.session_state.active_connections[room_id][client_id]
240
+
241
  async def websocket_handler(websocket, path):
242
  client_id = str(uuid.uuid4())
243
  room_id = "quest"
 
341
  )[0]
342
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
343
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
344
+ entry, audio_file = await save_chat_entry(username, result, voice, True)
345
+ return entry, audio_file
346
 
347
+ # -----------------------------
348
+ # Sidebar Functions
349
+ # -----------------------------
350
  def update_marquee_settings_ui():
351
  st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
352
  cols = st.sidebar.columns(2)
 
385
  percentage = (duration / total_time) * 100
386
  st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
387
 
388
+ # -----------------------------
389
+ # Enhanced 3D World HTML
390
+ # -----------------------------
391
+ # Precomputed player_color and player_shape are used for correct interpolation.
392
  rocky_map_html = f"""
393
  <!DOCTYPE html>
394
  <html lang="en">
395
  <head>
396
+ <meta charset="UTF-8" />
397
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
398
+ <title>{GAME_NAME}</title>
399
+ <style>
400
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
401
+ #gameContainer {{ width: 800px; height: 600px; position: relative; }}
402
+ canvas {{ width: 100%; height: 100%; display: block; }}
403
+ .ui-container {{
404
+ position: absolute; top: 10px; left: 10px; color: white;
405
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
406
+ user-select: none;
407
+ }}
408
+ .leaderboard {{
409
+ position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white;
410
+ background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px;
411
+ text-align: center;
412
+ }}
413
+ .controls {{
414
+ position: absolute; bottom: 10px; left: 10px; color: white;
415
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
416
+ }}
417
+ #chatBox {{
418
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
419
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
420
+ border-radius: 5px; overflow-y: auto;
421
+ }}
422
+ </style>
423
  </head>
424
  <body>
425
+ <div id="gameContainer">
426
+ <div class="ui-container">
427
+ <h2>{GAME_NAME}</h2>
428
+ <div id="score">Score: 0</div>
429
+ <div id="treasures">Treasures: 0</div>
430
+ <div id="timeout">Timeout: 60s</div>
 
 
 
 
 
 
 
431
  </div>
432
+ <div class="leaderboard" id="leaderboard">Leaderboard</div>
433
+ <div id="chatBox"></div>
434
+ <div class="controls">
435
+ <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
436
+ <p>Chat to add world features!</p>
437
+ </div>
438
+ </div>
439
+
440
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
441
+ <script>
442
+ const playerName = "{st.session_state.username}";
443
+ let ws = new WebSocket('ws://localhost:8765');
444
+ const scene = new THREE.Scene();
445
+ const camera = new THREE.PerspectiveCamera(75, 800/600, 0.1, 1000);
446
+ camera.position.set(0, 50, 50);
447
+ camera.lookAt(0, 0, 0);
448
+
449
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
450
+ renderer.setSize(800, 600);
451
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
452
+
453
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
454
+ scene.add(ambientLight);
455
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
456
+ sunLight.position.set(50, 50, 50);
457
+ sunLight.castShadow = true;
458
+ scene.add(sunLight);
459
+
460
+ const groundGeometry = new THREE.PlaneGeometry(100, 100);
461
+ const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
462
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
463
+ ground.rotation.x = -Math.PI/2;
464
+ ground.receiveShadow = true;
465
+ scene.add(ground);
466
+
467
+ let velocity = new THREE.Vector2(0, 0);
468
+ const acceleration = 50;
469
+ const friction = 5;
470
+
471
+ let xPos = 0, zPos = 0;
472
+ const shapeGeometries = {{
473
+ "sphere": new THREE.SphereGeometry(1, 16, 16),
474
+ "cube": new THREE.BoxGeometry(2, 2, 2),
475
+ "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
476
+ "cone": new THREE.ConeGeometry(1, 2, 16),
477
+ "torus": new THREE.TorusGeometry(1, 0.4, 16, 100),
478
+ "dodecahedron": new THREE.DodecahedronGeometry(1),
479
+ "octahedron": new THREE.OctahedronGeometry(1),
480
+ "tetrahedron": new THREE.TetrahedronGeometry(1),
481
+ "icosahedron": new THREE.IcosahedronGeometry(1)
482
+ }};
483
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {player_color} }});
484
+ const playerMesh = new THREE.Mesh(shapeGeometries["{player_shape}"], playerMaterial);
485
+ playerMesh.position.set(xPos, 1, zPos);
486
+ playerMesh.castShadow = true;
487
+ scene.add(playerMesh);
488
+
489
+ let score = 0, treasureCount = 0;
490
+ let lastActive = performance.now()/1000;
491
+ let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
492
+
493
+ document.addEventListener('keydown', (event) => {{
494
+ switch(event.code) {{
495
+ case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
496
+ case 'ArrowRight': case 'KeyD': moveRight = true; break;
497
+ case 'ArrowUp': case 'KeyW': moveUp = true; break;
498
+ case 'ArrowDown': case 'KeyS': moveDown = true; break;
499
+ case 'Space': collect = true; break;
500
+ }}
501
+ lastActive = performance.now()/1000;
502
+ }});
503
+ document.addEventListener('keyup', (event) => {{
504
+ switch(event.code) {{
505
+ case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
506
+ case 'ArrowRight': case 'KeyD': moveRight = false; break;
507
+ case 'ArrowUp': case 'KeyW': moveUp = false; break;
508
+ case 'ArrowDown': case 'KeyS': moveDown = false; break;
509
+ case 'Space': collect = false; break;
510
+ }}
511
+ }});
512
+
513
+ function updatePlayer(delta) {{
514
+ if(moveLeft) velocity.x -= acceleration * delta;
515
+ if(moveRight) velocity.x += acceleration * delta;
516
+ if(moveUp) velocity.y -= acceleration * delta;
517
+ if(moveDown) velocity.y += acceleration * delta;
518
+ velocity.x -= velocity.x * friction * delta;
519
+ velocity.y -= velocity.y * friction * delta;
520
+ xPos += velocity.x * delta;
521
+ zPos += velocity.y * delta;
522
+ xPos = Math.max(-40, Math.min(40, xPos));
523
+ zPos = Math.max(-40, Math.min(40, zPos));
524
+ playerMesh.position.set(xPos, 1, zPos);
525
+ ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
526
+ if(collect) {{
527
+ for(let i = treasures.length - 1; i >= 0; i--) {{
528
+ if(playerMesh.position.distanceTo(treasures[i].position) < 2) {{
529
+ const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
530
+ scene.remove(treasures[i]);
531
+ treasures.splice(i, 1);
532
+ delete treasureMeshes[id];
533
+ score += 50;
534
+ treasureCount += 1;
535
+ ws.send(`${{playerName}}|SCORE:${{score}}`);
536
+ ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
537
+ }}
 
538
  }}
539
+ }}
540
+ camera.position.lerp(new THREE.Vector3(xPos, 50, zPos+50), 0.1);
541
+ camera.lookAt(new THREE.Vector3(xPos, 0, zPos));
542
+ document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, (60 - (performance.now()/1000 - lastActive)).toFixed(0))}}s`;
543
+ document.getElementById('score').textContent = `Score: $${{score}}`;
544
+ document.getElementById('treasures').textContent = `Treasures: $${{treasureCount}}`;
545
+ }}
546
+
547
+ function updatePlayers(playerData) {{
548
+ playerData.forEach(player => {{
549
+ if(!playerMeshes[player.username]) {{
550
+ const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
551
+ const material = new THREE.MeshPhongMaterial({{ color: player.color }});
552
+ const mesh = new THREE.Mesh(geometry, material);
553
+ mesh.castShadow = true;
554
+ scene.add(mesh);
555
+ playerMeshes[player.username] = mesh;
 
 
 
 
 
 
 
 
 
 
556
  }}
557
+ const mesh = playerMeshes[player.username];
558
+ mesh.position.set(player.x, 1, player.z);
559
+ players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
560
+ if(player.username === playerName) {{
561
+ xPos = player.x;
562
+ zPos = player.z;
563
+ score = player.score;
564
+ treasureCount = player.treasures;
565
+ playerMesh.position.set(xPos, 1, zPos);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  }}
567
+ }});
568
+ document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
569
+ const leaderboard = playerData.sort((a, b) => b.score - a.score)
570
+ .map(p => `${{p.username}}: $${{p.score}}`)
571
+ .join('<br>');
572
+ document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
573
+ }}
574
+
575
+ function updateTreasures(treasureData) {{
576
+ treasureData.forEach(t => {{
577
+ if(!treasureMeshes[t.id]) {{
578
+ const treasure = new THREE.Mesh(
579
+ new THREE.SphereGeometry(1, 8, 8),
580
+ new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
581
+ );
582
+ treasure.position.set(t.x, 1, t.z);
583
+ treasure.castShadow = true;
584
+ treasureMeshes[t.id] = treasure;
585
+ treasures.push(treasure);
586
+ scene.add(treasure);
587
+ }} else {{
588
+ treasureMeshes[t.id].position.set(t.x, 1, t.z);
589
  }}
590
+ }});
591
+ Object.keys(treasureMeshes).forEach(id => {{
592
+ if(!treasureData.some(t => t.id === id)) {{
593
+ scene.remove(treasureMeshes[id]);
594
+ treasures = treasures.filter(t => t !== treasureMeshes[id]);
595
+ delete treasureMeshes[id];
596
+ }}
597
+ }});
598
+ }}
599
+
600
+ function updateWorldObjects(objectData) {{
601
+ objectData.forEach(obj => {{
602
+ if(!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
603
+ const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
604
+ const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
605
+ const objMesh = new THREE.Mesh(geometry, material);
606
+ objMesh.position.set(obj.x, 1, obj.z);
607
+ objMesh.castShadow = true;
608
+ worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
609
+ worldObjects.push(objMesh);
610
+ scene.add(objMesh);
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  }}
612
+ }});
613
+ }}
614
+
615
+ ws.onmessage = function(event) {{
616
+ const data = event.data;
617
+ if(data.startsWith('MAP_UPDATE:')) {{
618
+ const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
619
+ updatePlayers(playerData);
620
+ }} else if(data.startsWith('CHAT_UPDATE:')) {{
621
+ const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
622
+ const chatBox = document.getElementById('chatBox');
623
+ chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
624
+ chatBox.scrollTop = chatBox.scrollHeight;
625
+ }} else if(data.startsWith('GAME_STATE:')) {{
626
+ const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
627
+ updatePlayers(gameState.players);
628
+ updateTreasures(gameState.treasures);
629
+ updateWorldObjects(gameState.world_objects);
630
+ }} else if(!data.startsWith('PRAIRIE_UPDATE:')) {{
631
+ const [sender, message] = data.split('|');
632
+ const chatBox = document.getElementById('chatBox');
633
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
634
+ chatBox.scrollTop = chatBox.scrollHeight;
635
+ }}
636
+ }};
637
+
638
+ let lastTime = performance.now();
639
+ function animate() {{
640
+ requestAnimationFrame(animate);
641
+ const currentTime = performance.now();
642
+ const delta = (currentTime - lastTime) / 1000;
643
+ lastTime = currentTime;
644
+ updatePlayer(delta);
645
+ treasures.forEach(t => t.rotation.y += delta);
646
+ worldObjects.forEach(o => o.rotation.y += delta * 0.5);
647
+ renderer.render(scene, camera);
648
+ }}
649
+ animate();
650
+ </script>
651
  </body>
652
  </html>
653
  """
654
 
655
+ # -----------------------------
656
+ # Main Application
657
+ # -----------------------------
658
  def main():
659
  st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
660
+ # Chat Input
661
  message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
662
  if st.button("๐ŸŒŸ Send"):
663
  if message:
 
668
  st.session_state.last_activity = time.time()
669
  st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
670
 
671
+ # Display Quest Log as a Python code block (with line numbers if supported)
672
  chat_content = asyncio.run(load_chat())
673
  st.code("\n".join(chat_content[-10:]), language="python")
674